/**
* VoxelWorld — мир как словарь чанков. Мировые координаты целые (1 = размер
* вокселя), проекция — только на границе рендера (урок №1).
*
* Dirty-трекинг: set помечает чанк изменённым; изменение на боковой границе
* чанка помечает и соседа — иначе culled/AO-шов между чанками устареет.
*/
import { Chunk, CHUNK_SX, CHUNK_SZ } from './chunk';
export class VoxelWorld {
readonly chunkHeight: number;
private readonly chunks = new Map<string, Chunk>();
private readonly dirty = new Set<string>();
constructor(chunkHeight = 64) {
this.chunkHeight = chunkHeight;
}
/** Ключ чанка по мировым координатам (и по чанковым — тоже работает). */
static key(cx: number, cz: number): string {
return `${cx},${cz}`;
}
chunkCoords(x: number, z: number): { cx: number; cz: number } {
return { cx: Math.floor(x / CHUNK_SX), cz: Math.floor(z / CHUNK_SZ) };
}
/** Чанк по чанковым координатам (без создания). */
chunkAt(cx: number, cz: number): Chunk | undefined {
return this.chunks.get(VoxelWorld.key(cx, cz));
}
/** Чанк с созданием при отсутствии. */
private ensureChunk(cx: number, cz: number): Chunk {
const key = VoxelWorld.key(cx, cz);
let c = this.chunks.get(key);
if (!c) {
c = new Chunk(cx, cz, this.chunkHeight);
this.chunks.set(key, c);
this.dirty.add(key);
}
return c;
}
/** Цвет вокселя в мире (0 — пусто; несуществующий чанк пуст). */
get(x: number, y: number, z: number): number {
const { cx, cz } = this.chunkCoords(x, z);
const c = this.chunkAt(cx, cz);
if (!c) return 0;
return c.get(x - cx * CHUNK_SX, y, z - cz * CHUNK_SZ);
}
/**
* Поставить воксель (0 — стереть); чанк создаётся при необходимости.
* Возвращает false, если координата вне вертикали мира (y вне 0..H-1).
*/
set(x: number, y: number, z: number, color: number): boolean {
if (y < 0 || y >= this.chunkHeight) return false;
const { cx, cz } = this.chunkCoords(x, z);
const c = this.ensureChunk(cx, cz);
const lx = x - cx * CHUNK_SX, lz = z - cz * CHUNK_SZ;
c.set(lx, y, lz, color);
this.dirty.add(VoxelWorld.key(cx, cz));
// боковая граница — соседний чанк тоже меняет меш (culled/AO)
if (lx === 0) this.dirty.add(VoxelWorld.key(cx - 1, cz));
if (lx === CHUNK_SX - 1) this.dirty.add(VoxelWorld.key(cx + 1, cz));
if (lz === 0) this.dirty.add(VoxelWorld.key(cx, cz - 1));
if (lz === CHUNK_SZ - 1) this.dirty.add(VoxelWorld.key(cx, cz + 1));
return true;
}
/** Список изменённых чанков (ключи «cx,cz») без очистки. */
dirtyChunks(): string[] {
return [...this.dirty];
}
/** Забрать и очистить список изменённых чанков (для ремешинга). */
takeDirty(): string[] {
const out = this.dirtyChunks();
this.dirty.clear();
return out;
}
/** Число существующих чанков. */
get chunkCount(): number {
return this.chunks.size;
}
/** Все ключи чанков (стабильный порядок сортировки). */
chunkKeys(): string[] {
return [...this.chunks.keys()].sort();
}
}