Phase 28 Session 1: Foundation - BiomeSystem & ChunkManager

WORLD EXPANSION - Foundation systems created:

1. BiomeSystem.js (250 lines)
   - 5 biome definitions (Grassland, Forest, Desert, Mountain, Swamp)
   - 500x500 biome map generation
   - Region-based biome placement
   - Feature spawn probability per biome
   - Biome-specific tile coloring
   - Transition detection
   - Statistics tracking

2. ChunkManager.js (200 lines)
   - 50x50 tile chunk system
   - 3x3 chunk loading (9 active chunks)
   - Auto-load/unload based on player position
   - Performance optimization (loads 22,500 tiles vs 250,000)
   - 91% memory reduction
   - Chunk caching and statistics

3. Documentation
   - PHASE28_WORLD_EXPANSION_PLAN.md (complete roadmap)
   - PHASE28_SESSION1_LOG.md (progress tracking)

Integration:
- Both systems added to index.html
- Ready for GameScene integration

Next steps:
- Initialize BiomeSystem in GameScene
- Initialize ChunkManager in GameScene
- Update Flat2DTerrainSystem for biome support
- Expand world to 500x500
- Update camera bounds

Status: Foundation complete (60% of Session 1)
Time: 40 minutes
Files: 5 created, 1 modified
This commit is contained in:
2025-12-15 17:01:13 +01:00
parent 0f94058f68
commit 1713e7a955
5 changed files with 915 additions and 0 deletions

285
src/systems/BiomeSystem.js Normal file
View File

@@ -0,0 +1,285 @@
// BiomeSystem - Manages world biomes and generation
class BiomeSystem {
constructor(scene) {
this.scene = scene;
// World size
this.worldWidth = 500;
this.worldHeight = 500;
// Biome map (500x500 grid of biome IDs)
this.biomeMap = [];
// Biome definitions
this.biomes = {
grassland: {
id: 'grassland',
name: 'Grassland',
color: 0x4a9d5f,
tileColor: '#4a9d5f',
features: {
trees: 0.05, // 5% tree coverage
rocks: 0.02,
flowers: 0.15
},
weather: 'normal',
temperature: 20 // Celsius
},
forest: {
id: 'forest',
name: 'Forest',
color: 0x2d5016,
tileColor: '#2d5016',
features: {
trees: 0.60, // 60% tree coverage!
rocks: 0.05,
bushes: 0.20,
mushrooms: 0.10
},
weather: 'rainy',
temperature: 15
},
desert: {
id: 'desert',
name: 'Desert',
color: 0xd4c4a1,
tileColor: '#d4c4a1',
features: {
cacti: 0.08,
rocks: 0.15,
deadTrees: 0.03
},
weather: 'hot',
temperature: 35
},
mountain: {
id: 'mountain',
name: 'Mountain',
color: 0x808080,
tileColor: '#808080',
features: {
rocks: 0.40,
largeRocks: 0.20,
snow: 0.10 // At peaks
},
weather: 'cold',
temperature: -5
},
swamp: {
id: 'swamp',
name: 'Swamp',
color: 0x3d5a3d,
tileColor: '#3d5a3d',
features: {
water: 0.30,
deadTrees: 0.25,
vines: 0.15,
fog: true
},
weather: 'foggy',
temperature: 18
}
};
console.log('🌍 BiomeSystem initialized (500x500 world)');
}
// Generate biome map using Perlin-like noise
generateBiomeMap() {
console.log('🌍 Generating biome map...');
this.biomeMap = [];
// Initialize empty map
for (let y = 0; y < this.worldHeight; y++) {
this.biomeMap[y] = [];
for (let x = 0; x < this.worldWidth; x++) {
this.biomeMap[y][x] = null;
}
}
// Center is always grassland (farm area)
const centerX = Math.floor(this.worldWidth / 2);
const centerY = Math.floor(this.worldHeight / 2);
const farmRadius = 50; // 100x100 farm area
// Define biome centers (for now, simple regions)
const biomeRegions = [
{ biome: 'grassland', centerX: 250, centerY: 250, radius: 80 }, // Center (FARM)
{ biome: 'forest', centerX: 150, centerY: 150, radius: 100 }, // Northwest
{ biome: 'forest', centerX: 350, centerY: 150, radius: 80 }, // Northeast
{ biome: 'desert', centerX: 400, centerY: 350, radius: 90 }, // Southeast
{ biome: 'mountain', centerX: 100, centerY: 100, radius: 70 }, // Far northwest
{ biome: 'swamp', centerX: 100, centerY: 400, radius: 80 } // Southwest
];
// Fill biomes based on distance to region centers
for (let y = 0; y < this.worldHeight; y++) {
for (let x = 0; x < this.worldWidth; x++) {
// Find closest biome region
let closestBiome = 'grassland'; // Default
let minDistance = Infinity;
for (const region of biomeRegions) {
const dx = x - region.centerX;
const dy = y - region.centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < minDistance) {
minDistance = distance;
closestBiome = region.biome;
}
}
this.biomeMap[y][x] = closestBiome;
}
}
console.log('✅ Biome map generated!');
}
// Get biome at specific coordinates
getBiomeAt(x, y) {
if (x < 0 || x >= this.worldWidth || y < 0 || y >= this.worldHeight) {
return 'grassland'; // Default outside bounds
}
return this.biomeMap[y][x] || 'grassland';
}
// Get biome data
getBiomeData(biomeId) {
return this.biomes[biomeId] || this.biomes.grassland;
}
// Check if feature should spawn at location
shouldSpawnFeature(x, y, featureType) {
const biomeId = this.getBiomeAt(x, y);
const biomeData = this.getBiomeData(biomeId);
if (!biomeData.features[featureType]) return false;
const chance = biomeData.features[featureType];
return Math.random() < chance;
}
// Get tile color for biome
getTileColor(x, y) {
const biomeId = this.getBiomeAt(x, y);
const biomeData = this.getBiomeData(biomeId);
return biomeData.tileColor;
}
// Apply biome-specific features during world generation
applyBiomeFeatures(x, y) {
const biomeId = this.getBiomeAt(x, y);
const biomeData = this.getBiomeData(biomeId);
const features = [];
// Trees
if (this.shouldSpawnFeature(x, y, 'trees')) {
features.push({ type: 'tree', variant: Math.floor(Math.random() * 3) });
}
// Rocks
if (this.shouldSpawnFeature(x, y, 'rocks')) {
features.push({ type: 'rock', size: Math.random() > 0.7 ? 'large' : 'small' });
}
// Biome-specific features
if (biomeId === 'forest') {
if (this.shouldSpawnFeature(x, y, 'bushes')) {
features.push({ type: 'bush' });
}
if (this.shouldSpawnFeature(x, y, 'mushrooms')) {
features.push({ type: 'mushroom' });
}
} else if (biomeId === 'desert') {
if (this.shouldSpawnFeature(x, y, 'cacti')) {
features.push({ type: 'cactus' });
}
if (this.shouldSpawnFeature(x, y, 'deadTrees')) {
features.push({ type: 'deadTree' });
}
} else if (biomeId === 'mountain') {
if (this.shouldSpawnFeature(x, y, 'largeRocks')) {
features.push({ type: 'boulder' });
}
} else if (biomeId === 'swamp') {
if (this.shouldSpawnFeature(x, y, 'deadTrees')) {
features.push({ type: 'deadTree' });
}
if (this.shouldSpawnFeature(x, y, 'vines')) {
features.push({ type: 'vine' });
}
}
return features;
}
// Get biome transitions (blend zones)
getBiomeBlend(x, y, radius = 3) {
// Check surrounding tiles for different biomes
const centerBiome = this.getBiomeAt(x, y);
const surroundingBiomes = new Set();
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
const biome = this.getBiomeAt(x + dx, y + dy);
if (biome !== centerBiome) {
surroundingBiomes.add(biome);
}
}
}
return {
isTransition: surroundingBiomes.size > 0,
mainBiome: centerBiome,
nearbyBiomes: Array.from(surroundingBiomes)
};
}
// Get biome statistics (for debugging/UI)
getBiomeStats() {
const stats = {};
for (const biomeId in this.biomes) {
stats[biomeId] = 0;
}
for (let y = 0; y < this.worldHeight; y++) {
for (let x = 0; x < this.worldWidth; x++) {
const biome = this.getBiomeAt(x, y);
stats[biome] = (stats[biome] || 0) + 1;
}
}
// Convert to percentages
const total = this.worldWidth * this.worldHeight;
for (const biomeId in stats) {
stats[biomeId] = {
tiles: stats[biomeId],
percentage: ((stats[biomeId] / total) * 100).toFixed(1)
};
}
return stats;
}
// Export biome map for debugging/visualization
exportBiomeMap() {
return {
width: this.worldWidth,
height: this.worldHeight,
biomes: this.biomes,
map: this.biomeMap
};
}
// Destroy
destroy() {
this.biomeMap = [];
console.log('🌍 BiomeSystem destroyed');
}
}

215
src/systems/ChunkManager.js Normal file
View File

@@ -0,0 +1,215 @@
// ChunkManager - Handles chunk-based terrain loading for large maps
class ChunkManager {
constructor(scene, chunkSize = 50) {
this.scene = scene;
this.chunkSize = chunkSize; // 50x50 tiles per chunk
// Active chunks (currently loaded)
this.activeChunks = new Map(); // Key: "chunkX,chunkY", Value: chunk data
// Chunk load radius (how many chunks around player)
this.loadRadius = 1; // Load 3x3 = 9 chunks at once
// Player position tracking
this.lastPlayerChunkX = -1;
this.lastPlayerChunkY = -1;
console.log(`💾 ChunkManager initialized (chunk size: ${chunkSize}x${chunkSize})`);
}
// Get chunk coordinates from world coordinates
worldToChunk(worldX, worldY) {
return {
chunkX: Math.floor(worldX / this.chunkSize),
chunkY: Math.floor(worldY / this.chunkSize)
};
}
// Get chunk key string
getChunkKey(chunkX, chunkY) {
return `${chunkX},${chunkY}`;
}
// Check if chunk is loaded
isChunkLoaded(chunkX, chunkY) {
return this.activeChunks.has(this.getChunkKey(chunkX, chunkY));
}
// Load a single chunk
loadChunk(chunkX, chunkY) {
const key = this.getChunkKey(chunkX, chunkY);
// Already loaded
if (this.activeChunks.has(key)) {
return this.activeChunks.get(key);
}
console.log(`📦 Loading chunk (${chunkX}, ${chunkY})`);
// Create chunk data
const chunk = {
chunkX,
chunkY,
key,
tiles: [],
objects: [], // Trees, rocks, decorations
sprites: [] // Phaser sprites for this chunk
};
// Generate or load chunk tiles
const startX = chunkX * this.chunkSize;
const startY = chunkY * this.chunkSize;
for (let y = 0; y < this.chunkSize; y++) {
for (let x = 0; x < this.chunkSize; x++) {
const worldX = startX + x;
const worldY = startY + y;
// Get biome for this tile
let biomeId = 'grassland';
if (this.scene.biomeSystem) {
biomeId = this.scene.biomeSystem.getBiomeAt(worldX, worldY);
}
chunk.tiles.push({
x: worldX,
y: worldY,
biome: biomeId
});
}
}
// Store chunk
this.activeChunks.set(key, chunk);
// Render chunk (if terrain system available)
if (this.scene.terrainSystem && this.scene.terrainSystem.renderChunk) {
this.scene.terrainSystem.renderChunk(chunk);
}
return chunk;
}
// Unload a single chunk
unloadChunk(chunkX, chunkY) {
const key = this.getChunkKey(chunkX, chunkY);
if (!this.activeChunks.has(key)) return;
console.log(`📤 Unloading chunk (${chunkX}, ${chunkY})`);
const chunk = this.activeChunks.get(key);
// Destroy all sprites in chunk
if (chunk.sprites) {
chunk.sprites.forEach(sprite => {
if (sprite && sprite.destroy) {
sprite.destroy();
}
});
}
// Remove chunk
this.activeChunks.delete(key);
}
// Update active chunks based on player position
updateActiveChunks(playerX, playerY) {
const { chunkX, chunkY } = this.worldToChunk(playerX, playerY);
// Player hasn't changed chunks
if (chunkX === this.lastPlayerChunkX && chunkY === this.lastPlayerChunkY) {
return;
}
console.log(`🔄 Player moved to chunk (${chunkX}, ${chunkY})`);
this.lastPlayerChunkX = chunkX;
this.lastPlayerChunkY = chunkY;
// Determine which chunks should be loaded
const chunksToLoad = new Set();
for (let dy = -this.loadRadius; dy <= this.loadRadius; dy++) {
for (let dx = -this.loadRadius; dx <= this.loadRadius; dx++) {
const targetChunkX = chunkX + dx;
const targetChunkY = chunkY + dy;
chunksToLoad.add(this.getChunkKey(targetChunkX, targetChunkY));
}
}
// Unload chunks that are too far
const chunksToUnload = [];
for (const [key, chunk] of this.activeChunks) {
if (!chunksToLoad.has(key)) {
chunksToUnload.push({ x: chunk.chunkX, y: chunk.chunkY });
}
}
chunksToUnload.forEach(({ x, y }) => this.unloadChunk(x, y));
// Load new chunks
for (let dy = -this.loadRadius; dy <= this.loadRadius; dy++) {
for (let dx = -this.loadRadius; dx <= this.loadRadius; dx++) {
const targetChunkX = chunkX + dx;
const targetChunkY = chunkY + dy;
if (!this.isChunkLoaded(targetChunkX, targetChunkY)) {
this.loadChunk(targetChunkX, targetChunkY);
}
}
}
}
// Force reload all chunks (for debugging)
reloadAllChunks() {
console.log('🔄 Reloading all chunks...');
const chunksToReload = [];
for (const [key, chunk] of this.activeChunks) {
chunksToReload.push({ x: chunk.chunkX, y: chunk.chunkY });
}
// Unload all
chunksToReload.forEach(({ x, y }) => this.unloadChunk(x, y));
// Reload based on player position
if (this.scene.player) {
const pos = this.scene.player.getPosition();
this.updateActiveChunks(pos.x, pos.y);
}
}
// Get chunk at world position
getChunkAt(worldX, worldY) {
const { chunkX, chunkY } = this.worldToChunk(worldX, worldY);
return this.activeChunks.get(this.getChunkKey(chunkX, chunkY));
}
// Get statistics
getStats() {
return {
activeChunks: this.activeChunks.size,
chunkSize: this.chunkSize,
loadRadius: this.loadRadius,
maxChunks: Math.pow((this.loadRadius * 2 + 1), 2),
totalTilesLoaded: this.activeChunks.size * this.chunkSize * this.chunkSize
};
}
// Destroy all chunks
destroy() {
console.log('💾 ChunkManager destroying all chunks...');
for (const [key, chunk] of this.activeChunks) {
if (chunk.sprites) {
chunk.sprites.forEach(sprite => {
if (sprite && sprite.destroy) sprite.destroy();
});
}
}
this.activeChunks.clear();
console.log('💾 ChunkManager destroyed');
}
}