// ==================== ГЕНЕРАЦИЯ 3D — GrechkaCraft v3 ==================== // Объёмный мир: столбцы (x,z), рельеф из 2D-шума, пещеры, руды, растительность. // Та же система биомов, что в 2D-игре, но по двум координатам. import { TILE, SEA_GY, BEDROCK_GY, GEN_MARGIN_X } from '../core/constants.js'; import { state } from '../core/state.js'; import { getBlock, setBlock, k3 } from './world-storage3d.js'; // Радиус мира по Z — насколько далеко вбок генерируем вокруг игрока export const WORLD_DEPTH = 20; // полу-ширина: z от -20 до +20 const CHUNK = 8; // шаг генерации // ========== ШУМ (детерминированный, от worldSeed) ========== function hash2(x, z) { let h = x * 374761393 + z * 668265263 + state.worldSeed * 1274126177; h = (h ^ (h >> 13)) * 1274126177; return ((h ^ (h >> 16)) >>> 0) / 4294967295; } function smooth(t) { return t * t * (3 - 2 * t); } function valueNoise2(x, z, scale) { const fx = x / scale, fz = z / scale; const x0 = Math.floor(fx), z0 = Math.floor(fz); const tx = smooth(fx - x0), tz = smooth(fz - z0); const a = hash2(x0, z0), b = hash2(x0 + 1, z0); const c = hash2(x0, z0 + 1), d = hash2(x0 + 1, z0 + 1); return (a * (1 - tx) + b * tx) * (1 - tz) + (c * (1 - tx) + d * tx) * tz; } function fbm2(x, z, octaves = 4) { let sum = 0, amp = 0.5, freq = 1, norm = 0; for (let i = 0; i < octaves; i++) { sum += valueNoise2(x * freq, z * freq, 1) * amp; norm += amp; amp *= 0.5; freq *= 2; } return sum / norm; } // ========== БИОМЫ (те же пять, что в 2D) ========== export const BIOMES = { plains: { name:'Равнина', surface:'grass', subsurface:'dirt', trees:true, treeChance:0.11 }, desert: { name:'Пустыня', surface:'sand', subsurface:'sand', trees:false, treeChance:0 }, tundra: { name:'Тундра', surface:'snow', subsurface:'dirt', trees:true, treeChance:0.05 }, swamp: { name:'Болото', surface:'moss', subsurface:'dirt', trees:true, treeChance:0.09 }, mountains: { name:'Горы', surface:'stone', subsurface:'stone', trees:false, treeChance:0 }, }; export function getBiome(gx, gz) { const s = state.worldSeed; const temp = fbm2(gx * 0.6, gz * 0.6 + 11, 3) * 0.5 + Math.sin(gx * 0.003 + s * 0.01) * 0.25 + 0.4; const humid = fbm2(gx * 0.5 + 500, gz * 0.5 + 700, 3) * 0.5 + Math.cos(gx * 0.006 + s * 0.02) * 0.2 + 0.4; const mt = fbm2(gx * 0.25 + 900, gz * 0.25 + 300, 3); if (mt > 0.78) return 'mountains'; if (temp > 0.66) return 'desert'; if (temp < 0.34) return 'tundra'; if (humid > 0.66) return 'swamp'; return 'plains'; } const biomeCache = new Map(); export function getCachedBiome(gx, gz) { const cx = Math.floor(gx / CHUNK), cz = Math.floor(gz / CHUNK); const key = cx + ',' + cz; if (!biomeCache.has(key)) biomeCache.set(key, getBiome(cx * CHUNK, cz * CHUNK)); return biomeCache.get(key); } // ========== РЕЛЬЕФ ========== // Высота поверхности в колонке (x,z). Одна и та же функция на сервере и клиенте. export function surfaceGyAt(gx, gz) { const biome = getCachedBiome(gx, gz); const n1 = Math.sin(gx * 0.025 + state.worldSeed * 0.001) * 8; const n2 = Math.sin(gz * 0.021 - state.worldSeed * 0.0015) * 7; const n3 = Math.sin((gx + gz) * 0.012 + state.worldSeed * 0.002) * 10; const n4 = fbm2(gx * 0.5, gz * 0.5, 3) * 9; let h; switch (biome) { case 'desert': h = 8 + n1 * 0.5 + n3 * 0.3 + n4 * 0.2; break; case 'mountains': h = 16 + n3 * 1.1 + n4 * 1.2; break; case 'tundra': h = 9 + n1 * 0.6 + n3 * 0.5 + n4 * 0.4; break; case 'swamp': h = 4 + n1 * 0.4 + n2 * 0.6 + n4 * 0.3; break; default: h = 10 + n1 * 0.7 + n2 * 0.7 + n3 * 0.4 + n4 * 0.4; } return Math.max(2, Math.min(BEDROCK_GY - 12, Math.round(h))); } export function seaLevel() { return SEA_GY; } // ========== ПЕЩЕРЫ ========== function inCave(gx, gy, gz) { if (gy < 3) return false; const c1 = fbm2(gx * 0.9 + 11, gz * 0.9 + gy * 1.7, 2); const c2 = fbm2(gx * 0.9 - 40, gz * 0.9 + gy * 1.7 + 90, 2); // две «трубки» шума: пересечение даёт объёмные полости return c1 > 0.62 && c2 > 0.60 && gy < surfaceGyAt(gx, gz) - 3; } // ========== РУДЫ ========== function oreAt(gx, gy, gz) { if (gy < 2) return null; const depth = gy / BEDROCK_GY; const r = hash2(gx * 7 + gy, gz * 13); const r2 = hash2(gx * 3 - gy, gz * 5 + 17); if (depth < 0.30 && r > 0.982) return 'coal'; if (depth < 0.26 && r > 0.990 && r2 > 0.6) return 'copper_ore'; if (depth < 0.20 && r > 0.988 && r2 > 0.4) return 'iron_ore'; if (depth < 0.13 && r > 0.994) return 'gold_ore'; if (depth < 0.09 && r > 0.9965) return 'diamond_ore'; return null; } // ========== ДЕРЕВО (по XY, заполняет и Z) ========== function growTree(gx, gy, gz, biome) { const def = BIOMES[biome]; if (!def || !def.trees) return; if (hash2(gx * 31, gz * 17) > def.treeChance) return; const h = 4 + Math.floor(hash2(gx * 5, gz * 11) * 3); const leafType = biome === 'tundra' || biome === 'swamp' ? 'spruce_leaves' : 'leaves'; for (let i = 1; i <= h; i++) { if (!getBlock(gx, gy - i, gz)) setBlock(gx, gy - i, gz, 'wood'); } // крона объёмом: псевдосфера const cy = gy - h; const rad = 2; for (let dy = -2; dy <= 1; dy++) { for (let dz = -rad; dz <= rad; dz++) { for (let dx = -rad; dx <= rad; dx++) { const dist = dx * dx + dy * dy * 2 + dz * dz; if (dist > rad * rad + 1) continue; const yy = cy + dy; if (!getBlock(gx + dx, yy, gz + dz)) setBlock(gx + dx, yy, gz + dz, leafType); } } } } function growDecor(gx, gy, gz, biome) { const def = BIOMES[biome]; const r = hash2(gx * 23 + 5, gz * 29 + 7); if (def.flowers && r > 0.96 && !getBlock(gx, gy, gz)) setBlock(gx, gy, gz, 'flower'); if ((biome === 'swamp' || biome === 'plains') && r > 0.985 && !getBlock(gx, gy, gz)) setBlock(gx, gy, gz, 'mushroom'); if (biome === 'desert' && r > 0.978 && !getBlock(gx, gy, gz) && gy + 1 < surfaceGyAt(gx, gz)) setBlock(gx, gy, gz, 'cactus'); if (biome === 'tundra' && r > 0.978 && !getBlock(gx, gy, gz) && gy + 2 < surfaceGyAt(gx, gz)) setBlock(gx, gy, gz, 'ice'); } const generated = new Set(); // "cx,cz" — уже сгенерированные чанки // ========== ГЕНЕРАЦИЯ ЧАНКА ========== export function genChunk(cx, cz) { const key = cx + ',' + cz; if (generated.has(key)) return false; generated.add(key); const x0 = cx * CHUNK, z0 = cz * CHUNK; for (let x = x0; x < x0 + CHUNK; x++) { for (let z = z0; z < z0 + CHUNK; z++) { genColumn(x, z); } } return true; } export function genColumn(gx, gz) { const biome = getCachedBiome(gx, gz); const top = surfaceGyAt(gx, gz); const sea = seaLevel(); // Генерируем до bedrock, но не ниже уровня «пещерной зоны» — экономим память for (let gy = Math.min(BEDROCK_GY - 1, 44); gy >= 0; gy--) { // бедрок внизу if (gy >= BEDROCK_GY - 2) { if (!getBlock(gx, gy, gz)) setBlock(gx, gy, gz, 'bedrock'); continue; } if (gy > top) { // над поверхностью: вода до уровня моря if (gy <= sea) { const t = (biome === 'swamp') ? 'swamp_water' : 'water'; if (!getBlock(gx, gy, gz)) setBlock(gx, gy, gz, t); } continue; } // пещеры — только под поверхностью if (gy < top - 2 && inCave(gx, gy, gz)) continue; const ore = oreAt(gx, gy, gz); if (ore) { setBlock(gx, gy, gz, ore); continue; } const def = BIOMES[biome]; if (gy === top) setBlock(gx, gy, gz, def.surface); else if (gy > top - (biome === 'desert' || biome === 'mountains' ? 5 : 3)) setBlock(gx, gy, gz, def.subsurface); else setBlock(gx, gy, gz, 'stone'); } // вода в пещерах не заливаем — это гравитация // растительность const above = getBlock(gx, top, gz); if (above && !BLOCKS_NO_SOLID(above.t)) growDecor(gx, top, gz, biome); if (above && !BLOCKS_NO_SOLID(above.t) && hash2(gx * 13, gz * 17) > 0.93) growTree(gx, top, gz, biome); } function BLOCKS_NO_SOLID(t) { return t === 'water' || t === 'swamp_water' || t === 'air'; } // =========─ ГЕНЕРАЦИЯ ВОКРУГ ИГРОКА =========─ // Держим чанки сгенерированными вокруг позиции игрока по X и Z. const genChunks = new Set(); export function ensureGenAroundCamera() { const px = Math.floor(state.player.x / TILE); const pz = Math.floor(state.player.z / TILE); const rx = 3, rz = Math.ceil(WORLD_DEPTH / CHUNK) + 1; const cx = Math.floor(px / CHUNK), cz = Math.floor(pz / CHUNK); for (let dx = -rx; dx <= rx; dx++) { for (let dz = -rz; dz <= rz; dz++) { const kx = cx + dx, kz = cz + dz; const key = kx + ',' + kz; if (genChunks.has(key)) continue; genChunks.add(key); genChunk(kx, kz); } } // чистим слишком далёкие for (const key of genChunks) { const [a, b] = key.split(',').map(Number); if (Math.abs(a - cx) > rx + 2 || Math.abs(b - cz) > rz + 2) { genChunks.delete(key); generated.delete(key); } } } export function resetGenerationCache() { generated.clear(); genChunks.clear(); biomeCache.clear(); } export { CHUNK, k3, getBlock, setBlock };