\ Phase 28 Session 5: Rivers & Lakes Complete - RiverSystem + LakeSystem with biome-aware water rendering"

This commit is contained in:
2025-12-15 19:23:57 +01:00
parent 61f968be6c
commit 2d0ef6d8b9
8 changed files with 949 additions and 8 deletions

View File

@@ -89,10 +89,24 @@ class GameScene extends Phaser.Scene {
this.transitionSystem = new TransitionSystem(this, this.biomeSystem);
console.log('✅ Transition System ready!');
// 🌊 PHASE 28 SESSION 5: RIVER SYSTEM
console.log('🌊 Initializing River System...');
this.riverSystem = new RiverSystem(500, 500, this.biomeSystem);
this.riverSystem.generateRivers();
console.log('✅ River System ready!');
// 🏞️ PHASE 28 SESSION 5: LAKE SYSTEM
console.log('🏞️ Initializing Lake System...');
this.lakeSystem = new LakeSystem(500, 500, this.biomeSystem);
this.lakeSystem.generateLakes(this.riverSystem);
console.log('✅ Lake System ready!');
// Connect systems to terrainSystem
this.terrainSystem.biomeSystem = this.biomeSystem;
this.terrainSystem.chunkManager = this.chunkManager;
this.terrainSystem.transitionSystem = this.transitionSystem;
this.terrainSystem.riverSystem = this.riverSystem;
this.terrainSystem.lakeSystem = this.lakeSystem;
console.log('✅ BiomeSystem & ChunkManager connected to terrainSystem');
await this.terrainSystem.generate();

View File

@@ -414,6 +414,34 @@ class Flat2DTerrainSystem {
tilesRendered++;
// 🌊 PHASE 28 SESSION 5: Check for water (rivers & lakes)
let isWater = false;
// Rivers
if (this.riverSystem && this.riverSystem.isRiver(x, y)) {
const riverColor = this.riverSystem.getRiverColor(x, y);
const waterRect = this.scene.add.rectangle(worldX, worldY, size, size, riverColor, 0.75);
waterRect.setOrigin(0, 0);
waterRect.setDepth(2);
chunk.sprites.push(waterRect);
isWater = true;
}
// Lakes (only if not already river)
if (!isWater && this.lakeSystem && this.lakeSystem.isLake(x, y)) {
const lakeColor = this.lakeSystem.getLakeColor(x, y);
const waterRect = this.scene.add.rectangle(worldX, worldY, size, size, lakeColor, 0.75);
waterRect.setOrigin(0, 0);
waterRect.setDepth(2);
chunk.sprites.push(waterRect);
isWater = true;
}
// Skip features if water tile
if (isWater) {
continue; // Skip to next tile
}
// 🌈 Apply mixed features for transitions
let features = [];

318
src/systems/LakeSystem.js Normal file
View File

@@ -0,0 +1,318 @@
/**
* 🏞️ LAKE SYSTEM
* Generates and manages lakes across the world
* - Creates natural lake shapes
* - Biome-specific lake placement
* - Lake depth and shorelines
* - Connects to river system
*/
export default class LakeSystem {
constructor(worldWidth, worldHeight, biomeSystem) {
this.worldWidth = worldWidth;
this.worldHeight = worldHeight;
this.biomeSystem = biomeSystem;
// Lake map (stores depth: 0-1)
this.lakeMap = new Map();
// Lakes array
this.lakes = [];
// Lake settings
this.lakeCountPerBiome = {
grassland: 2,
forest: 3,
desert: 0, // No lakes in desert (unless oasis)
mountain: 2,
swamp: 4 // Most lakes in swamp
};
//小 pond settings
this.pondCount = 15; // Small ponds in grassland
console.log(`🏞️ Initializing Lake System (${worldWidth}x${worldHeight})`);
}
/**
* Generate all lakes
*/
generateLakes(riverSystem = null) {
console.log(`🏞️ Generating lakes...`);
// 1. Generate major lakes per biome
for (const [biomeName, count] of Object.entries(this.lakeCountPerBiome)) {
for (let i = 0; i < count; i++) {
const lake = this.generateLakeInBiome(biomeName);
if (lake) {
this.lakes.push(lake);
}
}
}
// 2. Generate small ponds
for (let i = 0; i < this.pondCount; i++) {
const pond = this.generatePond();
if (pond) {
this.lakes.push(pond);
}
}
// 3. Generate desert oases (rare)
this.generateOases(2);
console.log(`✅ Generated ${this.lakes.length} lakes with ${this.lakeMap.size} water tiles`);
}
/**
* Generate a lake in specific biome
*/
generateLakeInBiome(biomeName) {
// Find suitable location
const location = this.findLakeLocation(biomeName);
if (!location) return null;
// Lake size based on biome
const size = this.getLakeSizeForBiome(biomeName);
// Create lake
const lake = {
x: location.x,
y: location.y,
biome: biomeName,
size: size,
type: 'lake',
tiles: []
};
// Generate organic lake shape
this.generateLakeShape(lake);
return lake;
}
/**
* Find suitable location for lake
*/
findLakeLocation(biomeName) {
const maxAttempts = 50;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const x = Math.floor(Math.random() * this.worldWidth);
const y = Math.floor(Math.random() * this.worldHeight);
const biome = this.biomeSystem.getBiomeAt(x, y);
if (biome === biomeName) {
// Check not too close to other lakes
const tooClose = this.lakes.some(lake => {
const dist = Math.sqrt((lake.x - x) ** 2 + (lake.y - y) ** 2);
return dist < 50;
});
if (!tooClose) {
return { x, y };
}
}
}
return null;
}
/**
* Get lake size for biome
*/
getLakeSizeForBiome(biomeName) {
switch (biomeName) {
case 'grassland':
return 8 + Math.floor(Math.random() * 7); // 8-15 tiles
case 'forest':
return 10 + Math.floor(Math.random() * 8); // 10-18 tiles
case 'mountain':
return 6 + Math.floor(Math.random() * 5); // 6-11 tiles
case 'swamp':
return 12 + Math.floor(Math.random() * 10); // 12-22 tiles
default:
return 8;
}
}
/**
* Generate organic lake shape
*/
generateLakeShape(lake) {
const centerX = lake.x;
const centerY = lake.y;
const radius = lake.size;
// Use cellular automata-like approach for organic shape
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
const x = centerX + dx;
const y = centerY + dy;
if (x < 0 || x >= this.worldWidth || y < 0 || y >= this.worldHeight) {
continue;
}
// Distance from center
const dist = Math.sqrt(dx * dx + dy * dy);
// Add noise for organic shape
const noise = (Math.random() - 0.5) * 2;
const threshold = radius + noise;
if (dist < threshold) {
// Calculate depth (1.0 at center, 0.0 at edge)
const depth = 1.0 - (dist / radius);
const key = `${x},${y}`;
this.lakeMap.set(key, {
depth: Math.max(0, Math.min(1, depth)),
lakeId: this.lakes.length,
biome: lake.biome
});
lake.tiles.push({ x, y, depth });
}
}
}
}
/**
* Generate small pond
*/
generatePond() {
// Ponds only in grassland
const location = this.findLakeLocation('grassland');
if (!location) return null;
const pond = {
x: location.x,
y: location.y,
biome: 'grassland',
size: 3 + Math.floor(Math.random() * 3), // 3-6 tiles
type: 'pond',
tiles: []
};
this.generateLakeShape(pond);
return pond;
}
/**
* Generate desert oases
*/
generateOases(count) {
for (let i = 0; i < count; i++) {
const location = this.findLakeLocation('desert');
if (!location) continue;
const oasis = {
x: location.x,
y: location.y,
biome: 'desert',
size: 4 + Math.floor(Math.random() * 3), // 4-7 tiles
type: 'oasis',
tiles: []
};
this.generateLakeShape(oasis);
this.lakes.push(oasis);
}
}
/**
* Check if tile is lake
*/
isLake(x, y) {
const key = `${x},${y}`;
return this.lakeMap.has(key);
}
/**
* Get lake data at tile
*/
getLakeData(x, y) {
const key = `${x},${y}`;
return this.lakeMap.get(key) || null;
}
/**
* Get lake color based on depth and biome
*/
getLakeColor(x, y) {
const data = this.getLakeData(x, y);
if (!data) return 0x4682B4;
const biome = data.biome;
const depth = data.depth;
// Base colors
let baseColor;
switch (biome) {
case 'forest':
baseColor = { r: 42, g: 95, b: 79 }; // Dark green
break;
case 'swamp':
baseColor = { r: 61, g: 90, b: 61 }; // Murky
break;
case 'desert':
baseColor = { r: 135, g: 206, b: 235 }; // Oasis blue
break;
case 'mountain':
baseColor = { r: 70, g: 130, b: 180 }; // Mountain blue
break;
default:
baseColor = { r: 30, g: 144, b: 255 }; // Default blue
}
// Darken based on depth
const r = Math.floor(baseColor.r * depth);
const g = Math.floor(baseColor.g * depth);
const b = Math.floor(baseColor.b * depth);
return (r << 16) | (g << 8) | b;
}
/**
* Get statistics
*/
getStats() {
const typeCount = {
lake: 0,
pond: 0,
oasis: 0
};
for (const lake of this.lakes) {
typeCount[lake.type]++;
}
return {
totalLakes: this.lakes.length,
lakes: typeCount.lake,
ponds: typeCount.pond,
oases: typeCount.oasis,
totalWaterTiles: this.lakeMap.size
};
}
/**
* Export lake data
*/
exportData() {
return {
lakes: this.lakes,
lakeMap: Array.from(this.lakeMap.entries())
};
}
/**
* Import lake data
*/
importData(data) {
this.lakes = data.lakes || [];
this.lakeMap = new Map(data.lakeMap || []);
}
}

267
src/systems/RiverSystem.js Normal file
View File

@@ -0,0 +1,267 @@
/**
* 🌊 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
*/
export default 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 || []);
}
}