Newer
Older
rpg / v2 / packages / engine / src / world / world.ts
/**
 * 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();
    }
}