Newer
Older
rpg / v2 / apps / demo / src / main.ts
/**
 * Демо-полигон v2: воксельный мир 6×6 чанков (пепельный луг) под скриншот-гейтом.
 * Мир живёт в воксельных единицах, проекция — только в VoxelRenderer.
 * Деревья/валуны — из процедурных генераторов моделей (штамповка с клиппингом);
 * дом — пример ручного пути. Ремешинг по dirty-чанкам: правки моста
 * меняют ровно затронутые чанки. Палитра — слоты арт-библии v1.
 */
import {
    VoxelWorld, VoxelRenderer, meshChunk, CHUNK_SX,
    treeModel, boulderModel, stampModel, type Variant,
} from '@rpg/engine';
import * as THREE from 'three';

// Слоты палитры арт-библии: индексы сцены → hex
const PALETTE = {
    colors: {
        1: '#4a5340', // G1 — базовый луг
        2: '#343b2c', // G0 — тень травы (нижний слой)
        3: '#626c4c', // G2 — трава, свет (пятна)
        4: '#5c3b2e', // R1 — стволы/крыша тень
        5: '#7d8662', // G2H — крона, свет
        6: '#5b5b66', // P4 — камень, стены
        7: '#94949e', // P6 — свет камня
        8: '#82503a', // R2 — светлая ржавчина (крыша)
        9: '#d99a32', // B1 — цветок (жизнь!)
        10: '#44454e', // M1 — металл
    },
};

const CHUNKS = 6;          // мир 6×6 чанков (96×96) — демо масштаба движка
const S = CHUNKS * CHUNK_SX; // 96 — сторона площадки

/** Сценка: земля, генераторные деревья/валуны, дом, цветы. */
function buildScene(world: VoxelWorld): { trees: number; boulders: number; treeVariants: number[] } {
    for (let x = 0; x < S; x++)
        for (let z = 0; z < S; z++) {
            world.set(x, 0, z, 2);
            const patch = (x * 7 + z * 13) % 11 < 3; // детерминированные пятна
            world.set(x, 1, z, patch ? 3 : 1);
        }
    // деревья из генератора: [x, z, вариант, сид]; (16,20) — на шве x=CHUNK_SX
    const treeSpots: ReadonlyArray<readonly [number, number, Variant, number]> = [
        [6, 8, 0, 11], [24, 12, 1, 12], [10, 25, 2, 13], [CHUNK_SX, 20, 0, 14],
        [52, 30, 1, 15], [62, 54, 2, 16], [40, 70, 0, 17], [30, 60, 1, 18],
        [70, 40, 2, 19], [86, 22, 0, 20], [80, 80, 1, 21], [50, 88, 2, 22],
    ];
    const treeVariants = [0, 0, 0];
    for (const [tx, tz, v, seed] of treeSpots) {
        stampModel(world, treeModel(v, seed), tx, 2, tz);
        treeVariants[v]++;
    }
    house(world, 44, 44);
    // валуны из генератора: [x, z, вариант, сид]
    const boulderSpots: ReadonlyArray<readonly [number, number, Variant, number]> = [
        [52, 52, 1, 31], [70, 72, 0, 32], [20, 76, 2, 33],
    ];
    for (const [bx, bz, v, seed] of boulderSpots) {
        stampModel(world, boulderModel(v, seed), bx, 2, bz);
    }
    for (const [fx, fz] of [[38, 50], [46, 56], [56, 48], [36, 38]] as const) {
        world.set(fx, 2, fz, 9); // цветок — единственный тёплый цвет
    }
    return { trees: treeSpots.length, boulders: boulderSpots.length, treeVariants };
}

/** Дом: стены P4 со светлыми пилястрами, ступенчатая крыша R2. */
function house(world: VoxelWorld, x: number, z: number): void {
    const w = 6, d = 5, hWall = 3;
    for (let i = 0; i < w; i++)
        for (let j = 0; j < d; j++) {
            const wall = i === 0 || j === 0 || i === w - 1 || j === d - 1;
            for (let y = 2; y < 2 + hWall; y++) {
                if (wall) world.set(x + i, y, z + j, (i + j) % 3 === 0 ? 7 : 6);
                else world.set(x + i, y, z + j, 10); // интерьер темнее
            }
        }
    // дверной проём с юга
    for (let y = 2; y < 4; y++) world.set(x + 3, y, z, 0);
    // крыша: три сужающиеся ступени (скат вверх к коньку)
    for (let t = 0; t < 3; t++) {
        const j0 = z + t, j1 = z + d - 1 - t;
        for (let i = 0; i < w; i++)
            for (let j = j0; j <= j1; j++) world.set(x + i, 2 + hWall + t, j, t === 2 ? 4 : 8);
    }
}

/** Валун — из генератора boulderModel (ручной вариант удалён: урок №4). */

const world = new VoxelWorld(24);
const buildStart = performance.now();
const sceneInfo = buildScene(world);

const canvas = document.getElementById('scene') as HTMLCanvasElement;
const renderer = new VoxelRenderer(canvas, { target: [S / 2, 2, S / 2], viewSize: 17 });

// Меш на чанк; ремешинг только dirty-чанков
const chunkMeshes = new Map<string, THREE.Mesh>();
function remeshDirty(): void {
    for (const key of world.takeDirty()) {
        const [cx, cz] = key.split(',').map(Number);
        const old = chunkMeshes.get(key);
        if (old) {
            renderer.scene.remove(old);
            old.geometry.dispose();
        }
        const mesh = meshChunk(world, cx, cz, PALETTE);
        if (mesh.triangleCount > 0) chunkMeshes.set(key, renderer.addMesh(mesh));
        else chunkMeshes.delete(key);
    }
}
remeshDirty();
const buildMs = performance.now() - buildStart;

// Агентный мост (DEV): солнце, статистика, правка мира атомарным ремешингом
let sunK = 0.5;
function stats(): { vertices: number; triangles: number; sun: number } {
    let vertices = 0, triangles = 0;
    for (const m of chunkMeshes.values()) {
        vertices += m.geometry.getAttribute('position').count;
        triangles += (m.geometry.getIndex()?.count ?? 0) / 3;
    }
    return { vertices, triangles, sun: sunK };
}
function chunkStats(): { chunks: number; meshes: number } {
    return { chunks: world.chunkCount, meshes: chunkMeshes.size };
}
/** Статистика генераторных моделей в сцене. */
function modelStats(): { trees: number; boulders: number; treeVariants: number[] } {
    return { ...sceneInfo };
}
/** Пробная штамповка дерева генератором (для гейта: детерминизм, варианты). */
function stampTree(v: number, x: number, z: number, seed: number): number {
    const placed = stampModel(world, treeModel(v as Variant, seed), x, 2, z);
    remeshDirty();
    return placed;
}
const agent = {
    setSun(k: number): void { sunK = k; renderer.setSun(k); },
    setVoxel(x: number, y: number, z: number, color: number): void {
        world.set(x, y, z, color);
        remeshDirty();
    },
    stats,
    chunkStats,
    modelStats,
    stampTree,
    perf(): { chunks: number; meshes: number; buildMs: number; vertices: number; triangles: number } {
        return { ...chunkStats(), buildMs, ...stats() };
    },
};
(window as unknown as Record<string, unknown>).__agent = agent;

function frame(): void {
    renderer.render();
    requestAnimationFrame(frame);
}
frame();
(window as unknown as Record<string, unknown>).__beacon = true;