/**
* Процедурные генераторы воксельных моделей — основной путь генерации контента
* (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' });
}
// ---- Снаряжение для креплений к персонажам (трек 32) ----
/** Палитра снаряжения: слоты называются, незаданные — дефолт (как у манекена). */
export interface GearPalette {
/** Кожа изделия (рюкзак, обмотка). */
leather?: string;
/** Ремни/лямки/обугленное дерево. */
strap?: string;
/** Пряжка. */
metal?: string;
/** Древко факела. */
wood?: string;
/** Пламя (внешний слой). */
flame?: string;
/** Пламя (ядро). */
flameCore?: string;
}
const GEAR_DEFAULTS: Required<GearPalette> = {
leather: '#6f4a2a', strap: '#3a2c1e', metal: '#c9a04a',
wood: '#5e4526', flame: '#e0962e', flameCore: '#f2c14e',
};
/**
* Рюкзак: кожаный меш 6×7×2 (клапан темным ремнём, пряжка спереди),
* угол-origine — нижний левый задний угол; на манекене ставится за торс
* (attach pos ≈ [2, 5, -1] в связочном пространстве). Без сида — дизайн
* фиксирован, детерминирован.
*/
export function backpackModel(p: GearPalette = {}): VoxelModel {
const c = { ...GEAR_DEFAULTS, ...p };
const g = new VoxelGrid(6, 7, 2);
for (let y = 0; y <= 5; y++) for (let z = 0; z <= 1; z++) for (let x = 0; x <= 5; x++) g.set(x, y, z, 1);
for (let x = 0; x <= 5; x++) for (let z = 0; z <= 1; z++) g.set(x, 6, z, 2); // клапан
g.set(2, 4, 1, 3); // пряжка клапана спереди
g.set(3, 4, 1, 3);
return encodeModel(g, { 1: c.leather!, 2: c.strap!, 3: c.metal! });
}
/** Факел: рукоять 1×6×1 (обмотка у головки), пламя с ярким ядром; угол — низ рукояти. */
export function torchModel(p: GearPalette = {}): VoxelModel {
const c = { ...GEAR_DEFAULTS, ...p };
const g = new VoxelGrid(1, 6, 1);
for (let y = 0; y <= 2; y++) g.set(0, y, 0, 4); // древко
g.set(0, 3, 0, 2); // обмотка
g.set(0, 4, 0, 5); // пламя
g.set(0, 5, 0, 6); // ядро пламени
return encodeModel(g, { 2: c.strap!, 4: c.wood!, 5: c.flame!, 6: c.flameCore! });
}