import { TILE, SEA_GY, BEDROCK_GY, GEN_MARGIN_X } from '../core/constants.js'; import { state } from '../core/state.js'; import { grid, blocks, setBlock, getBlock, k } from './world-storage.js'; // Генерация (по X, на всю глубину до bedrock) const generated = state.generated; // Set of gx already generated // ========== BIOME SYSTEM ========== export const BIOMES = { plains: { name:'Равнина', surface:'grass', subsurface:'dirt', trees:true, flowers:true, treeChance:0.12 }, desert: { name:'Пустыня', surface:'sand', subsurface:'sand', trees:false, flowers:false, treeChance:0 }, tundra: { name:'Тундра', surface:'snow', subsurface:'dirt', trees:true, flowers:false, treeChance:0.06 }, swamp: { name:'Болото', surface:'moss', subsurface:'dirt', trees:true, flowers:false, treeChance:0.10 }, mountains:{ name:'Горы', surface:'stone', subsurface:'stone', trees:false, flowers:false, treeChance:0 } }; export function getBiome(gx) { const temp = Math.sin(gx*0.003 + state.worldSeed*0.01)*0.5 + Math.sin(gx*0.007 + state.worldSeed*0.02)*0.3 + 0.5; const humid = Math.sin(gx*0.004 + state.worldSeed*0.015 + 1000)*0.5 + Math.cos(gx*0.006 + state.worldSeed*0.02 + 2000)*0.3 + 0.5; const mtVal = Math.sin(gx*0.001 + state.worldSeed*0.005)*0.5 + 0.5; if (temp > 0.7) return 'desert'; if (temp < 0.3) return 'tundra'; if (humid > 0.7 && temp >= 0.3 && temp <= 0.7) return 'swamp'; if (temp >= 0.5 && temp <= 0.7 && mtVal > 0.75) return 'mountains'; return 'plains'; } const biomeCache = {}; export function getCachedBiome(gx) { const chunk = Math.floor(gx / 8); if (biomeCache[chunk] === undefined) biomeCache[chunk] = getBiome(chunk * 8); return biomeCache[chunk]; } // ========== STRUCTURES ========== function placeStructure(startGx, startGy, pattern) { for (let dy = 0; dy < pattern.length; dy++) { for (let dx = 0; dx < pattern[dy].length; dx++) { const bt = pattern[dy][dx]; if (bt && bt !== 'air') { setBlock(startGx + dx, startGy + dy, bt); } } } } const PYRAMID_PATTERN = [ ['sand','sand','sand','sand','sand','sand','sand'], ['sand','stone','stone','stone','stone','stone','sand'], ['sand','stone','air','air','air','stone','sand'], ['sand','stone','air','air','air','stone','sand'], ['sand','stone','air','air','air','stone','sand'], ['sand','sand','stone','stone','stone','sand','sand'] ]; const HOUSE_PATTERN = [ ['air','planks','planks','planks','planks','air'], ['planks','air','air','air','air','planks'], ['planks','air','torch','air','air','planks'], ['planks','air','air','air','air','planks'], ['planks','planks','air','planks','planks','planks'] ]; const HUT_PATTERN = [ ['air','wood','wood','wood','air'], ['wood','air','air','air','wood'], ['wood','air','torch','air','wood'], ['moss','moss','air','moss','moss'] ]; function surfaceGyAt(gx) { const biome = getCachedBiome(gx); const n1 = Math.sin(gx*0.025 + state.worldSeed*0.001)*8; const n2 = Math.sin(gx*0.012 + state.worldSeed*0.002)*12; const n3 = Math.sin(gx*0.006 + state.worldSeed*0.003)*6; const n4 = Math.sin(gx*0.045 + state.worldSeed*0.004)*4; const n5 = Math.cos(gx*0.018 + state.worldSeed*0.005)*5; let h; switch(biome) { case 'desert': h = Math.floor(SEA_GY - 4 + n3*0.3 + n4*0.5); break; case 'tundra': h = Math.floor(SEA_GY - 6 + n2*0.5 + n3*0.4 + n5*0.3); break; case 'swamp': h = Math.floor(SEA_GY - 2 + n3*0.2 + n4*0.3); h = Math.max(h, SEA_GY - 3); break; case 'mountains': h = Math.floor(SEA_GY - 15 + n1*1.5 + n2*1.2 + n3); break; default: // plains h = Math.floor(SEA_GY - 8 + n1 + n2 + n3 + n4 + n5); } return h; } function genColumn(gx) { if (generated.has(gx)) return; generated.add(gx); const sgy = surfaceGyAt(gx); const biome = getCachedBiome(gx); // === Вода и поверхность === if (sgy > SEA_GY) { for (let gy = SEA_GY; gy < sgy; gy++) { const blockType = (biome === 'swamp' && gy >= SEA_GY - 1) ? 'swamp_water' : 'water'; setBlock(gx, gy, blockType); } setBlock(gx, sgy, 'sand'); } else { const b = BIOMES[biome]; setBlock(gx, sgy, b.surface); // болото: случайные лужи болотной воды if (biome === 'swamp' && seededRandom(gx*3, sgy) < 0.15) { setBlock(gx, sgy-1, 'swamp_water'); } // тундра: лёд на воде рядом if (biome === 'tundra' && sgy === SEA_GY && seededRandom(gx, SEA_GY-1) < 0.3) { setBlock(gx, SEA_GY-1, 'ice'); } } // === Подповерхностные слои === for (let gy = sgy + 1; gy <= BEDROCK_GY; gy++) { if (gy === BEDROCK_GY) { setBlock(gx, gy, 'bedrock'); continue; } let t = BIOMES[biome].subsurface; // глубже — камень if (gy > sgy + 3) t = 'stone'; // пустыня: sand глубже if (biome === 'desert' && gy <= sgy + 6) t = 'sand'; // болото: глина ближе к поверхности if (biome === 'swamp' && gy <= sgy + 2 && seededRandom(gx, gy) < 0.3) t = 'clay'; // горы: gravel if (biome === 'mountains' && gy > sgy + 4 && seededRandom(gx, gy) < 0.12) t = 'gravel'; // общие руды const depth = gy - sgy; const r = seededRandom(gx, gy); if (t === 'stone') { if (r < 0.06) t = 'coal'; else if (r < 0.10) t = 'copper_ore'; else if (r < 0.13) t = 'iron_ore'; else if (depth > 40 && r < 0.145) t = 'gold_ore'; else if (depth > 70 && r < 0.152) t = 'diamond_ore'; } setBlock(gx, gy, t); } // === Растительность и декор === const b = BIOMES[biome]; const top = getBlock(gx, sgy); // цветы (только plains) if (biome === 'plains' && top && top.t === 'grass' && seededRandom(gx, sgy-1) < 0.10) { setBlock(gx, sgy-1, 'flower'); } // деревья if (b.trees && seededRandom(gx*7, sgy-2) < b.treeChance) { if (biome === 'tundra') { const th = 3 + Math.floor(seededRandom(gx, sgy) * 3); for (let i = 0; i < th; i++) setBlock(gx, sgy-1-i, 'wood'); for (let row = 0; row < th; row++) { const w = Math.min(row + 1, 2); for (let dx = -w; dx <= w; dx++) { const ly = sgy-1-th+row; if (ly >= 0) setBlock(gx+dx, ly, 'spruce_leaves'); } } } else if (biome === 'swamp') { setBlock(gx, sgy-1, 'wood'); setBlock(gx, sgy-2, 'moss'); setBlock(gx-1, sgy-2, 'leaves'); setBlock(gx+1, sgy-2, 'leaves'); } else { setBlock(gx, sgy-1, 'wood'); setBlock(gx, sgy-2, 'wood'); setBlock(gx, sgy-3, 'leaves'); setBlock(gx-1, sgy-3, 'leaves'); setBlock(gx+1, sgy-3, 'leaves'); } } // кактусы (пустыня) if (biome === 'desert' && seededRandom(gx*11, sgy) < 0.04) { const ch = 1 + Math.floor(seededRandom(gx, sgy+1) * 2); for (let i = 0; i < ch; i++) setBlock(gx, sgy-1-i, 'cactus'); } // грибы (болото) if (biome === 'swamp' && seededRandom(gx*13, sgy) < 0.06) { setBlock(gx, sgy-1, 'mushroom'); } // сухие кусты (пустыня) if (biome === 'desert' && seededRandom(gx*17, sgy) < 0.05) { setBlock(gx, sgy-1, 'dead_bush'); } // === Структуры мира === // Пирамида в пустыне if (biome === 'desert' && ((gx % 200 + 200) % 200) === 47 && sgy < SEA_GY && sgy > SEA_GY - 5) { placeStructure(gx, sgy - 5, PYRAMID_PATTERN); } // Дом в равнине if (biome === 'plains' && ((gx % 150 + 150) % 150) === 33 && sgy < SEA_GY) { placeStructure(gx, sgy - 4, HOUSE_PATTERN); } // Хижина в болоте if (biome === 'swamp' && ((gx % 180 + 180) % 180) === 55 && sgy < SEA_GY) { placeStructure(gx, sgy - 3, HUT_PATTERN); } // Применяем серверные оверрайды для этой колонны const colPrefix = gx + ','; for (const [key, ov] of state.serverOverrides) { if (!key.startsWith(colPrefix)) continue; if (ov.op === 'remove') { const b = grid.get(key); if (b) { grid.delete(key); b.dead = true; } } else if (ov.op === 'set') { if (!grid.has(key)) { const gy = parseInt(key.split(',')[1]); const nb = { gx, gy, t: ov.t, dead: false, active: false, fuse: 0 }; grid.set(key, nb); blocks.push(nb); } } } } // Перегенерация видимых чанков (используется при загрузке сохранения) function regenerateVisibleChunks() { const gx0 = Math.floor(state.camX / TILE); for (let gx = gx0 - GEN_MARGIN_X; gx <= gx0 + GEN_MARGIN_X; gx++) { generated.delete(gx); genColumn(gx); } } function ensureGenAroundCamera() { const gx0 = Math.floor(state.camX / TILE); for (let gx = gx0 - GEN_MARGIN_X; gx <= gx0 + GEN_MARGIN_X; gx++) { genColumn(gx); } } function seededRandom(gx, gy) { const n = Math.sin(gx * 12.9898 + gy * 78.233 + state.worldSeed * 0.1) * 43758.5453; return n - Math.floor(n); } export { generated, surfaceGyAt, genColumn, regenerateVisibleChunks, ensureGenAroundCamera, seededRandom, PYRAMID_PATTERN, HOUSE_PATTERN, HUT_PATTERN };