266 lines
9.2 KiB
JavaScript
266 lines
9.2 KiB
JavaScript
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 };
|