Newer
Older
rpg / v2 / packages / engine / src / models / generators.ts
/**
 * Процедурные генераторы воксельных моделей — основной путь генерации контента
 * (docs/plan.md, «Направление v2»). Чистые функции: (вид, сид) → VoxelModel,
 * детерминированно; никакой зависимости от мира и рендера.
 *
 * Слоты палитры — из арт-библии v1 (демка передаёт свою карту слот→hex).
 */
import { Rng } from '../math/rng';
import { VoxelGrid } from '../voxel/grid';
import { encodeModel } from './format';
import type { VoxelModel } from './format';

// Слоты арт-библии, используемые генераторами
const TRUNK = 4;      // R1 — ствол
const CROWN_DARK = 3; // G2 — крона, тень
const CROWN_LIGHT = 5; // G2H — крона, свет
const ROCK = 6;       // P4 — камень
const ROCK_LIGHT = 7; // P6 — камень, свет

export type Variant = 0 | 1 | 2;

/**
 * Дерево: вариант 0 — круглая крона, 1 — колонна, 2 — ель.
 * Ствол входит в крону (соседство 6-связности), крона пятнистая —
 * свет только сверху/снаружи, как в арт-библии.
 */
export function treeModel(variant: Variant, seed: number): VoxelModel {
    const rng = new Rng(seed ^ 0x7ee1);
    const trunkH = rng.range(3, 5);
    let sx = 7, sz = 7, cx = 3, ch: number;
    if (variant === 1) { sx = 5; sz = 5; cx = 2; ch = rng.range(3, 4); }
    else if (variant === 2) { sx = 5; sz = 5; cx = 2; ch = rng.range(4, 5); }
    else ch = rng.range(2, 3);
    const g = new VoxelGrid(sx, trunkH + ch + 1, sz);
    const mid = Math.floor(sx / 2);

    for (let y = 0; y < trunkH; y++) g.set(mid, y, mid, TRUNK);

    // Крона: предикат формы на слой dy от основания кроны (y = trunkH-1+dy)
    const crownBase = trunkH - 1;
    const r = 2 + rng.next() * 0.6; // джиттер радиуса фиксируем заранее
    for (let dy = 0; dy < ch; dy++) {
        const y = crownBase + dy;
        for (let dz = -mid; dz <= mid; dz++)
            for (let dx = -mid; dx <= mid; dx++) {
                if (!inCrown(variant, dx, dy, dz, ch, r)) continue;
                const light = dy === ch - 1 || rng.chance(0.3);
                g.set(cx + dx, y, cx + dz, light ? CROWN_LIGHT : CROWN_DARK);
            }
    }
    return encodeModel(g, {
        [TRUNK]: '#5c3b2e', [CROWN_DARK]: '#626c4c', [CROWN_LIGHT]: '#7d8662',
    });
}

/** Предикат формы кроны по варианту (dx/dz от центра, dy от основания). */
function inCrown(variant: Variant, dx: number, dy: number, dz: number, ch: number, r: number): boolean {
    const d2 = dx * dx + dz * dz;
    if (variant === 0) return d2 + dy * dy <= r * r;
    if (variant === 1) return d2 <= 1 && dy < ch; // колонна: радиус 1
    // ель: радиус слоя сужается кверху
    const layerR = Math.max(0, 2 - Math.floor(dy / 2));
    return d2 <= layerR * layerR;
}

/**
 * Валун: union 2–4 сфер-глыб (вариант = размер и число глыб), свет
 * на верхней поверхности каждого столбца. Всегда непустой.
 */
export function boulderModel(variant: Variant, seed: number): VoxelModel {
    const rng = new Rng(seed ^ 0x3b9a);
    const s = 5 + variant;
    const g = new VoxelGrid(s, s - 1, s);
    const blobs = 2 + variant;
    const spheres: Array<{ cx: number; cy: number; cz: number; r: number }> = [];
    for (let i = 0; i < blobs; i++) {
        spheres.push({
            cx: rng.range(1, s - 2), cy: rng.range(0, 1),
            cz: rng.range(1, s - 2), r: rng.range(1, 2),
        });
    }
    for (let z = 0; z < g.sz; z++)
        for (let y = 0; y < g.sy; y++)
            for (let x = 0; x < g.sx; x++) {
                const hit = spheres.some((b) =>
                    (x - b.cx) ** 2 + (y - b.cy) ** 2 + (z - b.cz) ** 2 <= b.r * b.r);
                if (hit) g.set(x, y, z, ROCK);
            }
    if (g.count() === 0) g.set(Math.floor(s / 2), 0, Math.floor(s / 2), ROCK); // страховка
    // свет — верхняя поверхность каждого столбца
    for (let z = 0; z < g.sz; z++)
        for (let x = 0; x < g.sx; x++)
            for (let y = g.sy - 1; y >= 0; y--)
                if (g.get(x, y, z) !== 0) { g.set(x, y, z, ROCK_LIGHT); break; }
    return encodeModel(g, { [ROCK]: '#5b5b66', [ROCK_LIGHT]: '#94949e' });
}