268 lines
7.9 KiB
JavaScript
268 lines
7.9 KiB
JavaScript
/**
|
|
* 🌊 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 || []);
|
|
}
|
|
}
|