/** * 🌊 RIVER SYSTEM * Generates and manages rivers across the 500x500 world * - Creates flowing rivers from mountains to lakes * - Handles river width, curves, and junctions * - Biome-aware water coloring */ class RiverSystem { constructor(worldWidth, worldHeight, biomeSystem) { this.worldWidth = worldWidth; this.worldHeight = worldHeight; this.biomeSystem = biomeSystem; // River map (stores river type: 'river', 'tributary', 'spring') this.riverMap = new Map(); // River paths (array of segments) this.rivers = []; // River settings this.riverCount = 3; // Number of major rivers this.minRiverLength = 50; // Minimum river length this.maxRiverLength = 200; // Maximum river length this.riverWidth = 2; // Base width (tiles) this.tributaryChance = 0.15; // Chance to spawn tributary console.log(`🌊 Initializing River System (${worldWidth}x${worldHeight})`); } /** * Generate all rivers */ generateRivers() { console.log(`🌊 Generating ${this.riverCount} rivers...`); // 1. Find river sources (springs in mountains) const sources = this.findRiverSources(); // 2. Generate river paths from each source for (let i = 0; i < sources.length; i++) { const source = sources[i]; const river = this.generateRiverPath(source, i); if (river && river.length > this.minRiverLength) { this.rivers.push(river); this.markRiverTiles(river); } } console.log(`✅ Generated ${this.rivers.length} rivers with ${this.riverMap.size} water tiles`); } /** * Find river sources (prefer mountains) */ findRiverSources() { const sources = []; const attempts = this.riverCount * 3; for (let i = 0; i < attempts && sources.length < this.riverCount; i++) { const x = Math.floor(Math.random() * this.worldWidth); const y = Math.floor(Math.random() * this.worldHeight); const biome = this.biomeSystem.getBiomeAt(x, y); // Prefer mountain sources, but allow forest if (biome === 'mountain' || (biome === 'forest' && Math.random() < 0.3)) { // Check not too close to other sources const tooClose = sources.some(s => Math.abs(s.x - x) < 100 && Math.abs(s.y - y) < 100 ); if (!tooClose) { sources.push({ x, y, biome }); } } } // If not enough, add random sources while (sources.length < this.riverCount) { sources.push({ x: Math.floor(Math.random() * this.worldWidth), y: Math.floor(Math.random() * this.worldHeight), biome: 'forest' }); } console.log(`🏔️ Found ${sources.length} river sources:`, sources); return sources; } /** * Generate a single river path from source */ generateRiverPath(source, riverIndex) { const path = []; let x = source.x; let y = source.y; // Random target direction (generally downward/outward) const targetAngle = Math.random() * Math.PI * 2; // River length const length = this.minRiverLength + Math.floor(Math.random() * (this.maxRiverLength - this.minRiverLength)); // Generate path using noise for (let step = 0; step < length; step++) { // Add current position to path path.push({ x: Math.floor(x), y: Math.floor(y) }); // Check bounds if (x < 0 || x >= this.worldWidth || y < 0 || y >= this.worldHeight) { break; } // Check if reached lake const biome = this.biomeSystem.getBiomeAt(Math.floor(x), Math.floor(y)); if (biome === 'swamp' && Math.random() < 0.3) { // River ends in swamp lake break; } // Move river forward // Add some randomness to create curves const noise = (Math.random() - 0.5) * 0.5; const angle = targetAngle + noise; const dx = Math.cos(angle); const dy = Math.sin(angle); x += dx; y += dy; // Occasionally create tributary if (Math.random() < this.tributaryChance && path.length > 10) { this.createTributary(x, y, 10); } } return path; } /** * Create a small tributary */ createTributary(startX, startY, length) { const path = []; let x = startX; let y = startY; const angle = Math.random() * Math.PI * 2; for (let i = 0; i < length; i++) { path.push({ x: Math.floor(x), y: Math.floor(y) }); const noise = (Math.random() - 0.5) * 0.8; x += Math.cos(angle + noise); y += Math.sin(angle + noise); if (x < 0 || x >= this.worldWidth || y < 0 || y >= this.worldHeight) { break; } } this.markRiverTiles(path, 'tributary'); } /** * Mark river tiles on the river map */ markRiverTiles(path, type = 'river') { for (const point of path) { const key = `${point.x},${point.y}`; // Main river tile this.riverMap.set(key, { type, width: this.riverWidth }); // Add width (make river wider) const width = type === 'tributary' ? 1 : this.riverWidth; for (let dy = -width; dy <= width; dy++) { for (let dx = -width; dx <= width; dx++) { if (dx === 0 && dy === 0) continue; // Check if within circle const dist = Math.sqrt(dx * dx + dy * dy); if (dist <= width) { const nx = point.x + dx; const ny = point.y + dy; if (nx >= 0 && nx < this.worldWidth && ny >= 0 && ny < this.worldHeight) { const nkey = `${nx},${ny}`; if (!this.riverMap.has(nkey)) { this.riverMap.set(nkey, { type: 'riverbank', width }); } } } } } } } /** * Check if tile is river */ isRiver(x, y) { const key = `${x},${y}`; return this.riverMap.has(key); } /** * Get river data at tile */ getRiverData(x, y) { const key = `${x},${y}`; return this.riverMap.get(key) || null; } /** * Get river color based on biome */ getRiverColor(x, y) { const biome = this.biomeSystem.getBiomeAt(x, y); switch (biome) { case 'forest': return 0x2a5f4f; // Dark green water case 'swamp': return 0x3d5a3d; // Murky swamp water case 'desert': return 0x87CEEB; // Clear oasis water case 'mountain': return 0x4682B4; // Cool mountain water default: return 0x1E90FF; // Default blue water } } /** * Get statistics */ getStats() { return { riverCount: this.rivers.length, totalWaterTiles: this.riverMap.size, avgRiverLength: this.rivers.reduce((sum, r) => sum + r.length, 0) / this.rivers.length || 0 }; } /** * Export river data for saving */ exportData() { return { rivers: this.rivers, riverMap: Array.from(this.riverMap.entries()) }; } /** * Import river data from save */ importData(data) { this.rivers = data.rivers || []; this.riverMap = new Map(data.riverMap || []); } }