239 lines
9.6 KiB
JavaScript
239 lines
9.6 KiB
JavaScript
// ==================== ГЕНЕРАЦИЯ 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 };
|