319 lines
8.4 KiB
JavaScript
319 lines
8.4 KiB
JavaScript
/**
|
|
* 🏞️ 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 || []);
|
|
}
|
|
}
|