/**
* Turntable-лист: все варианты генераторов × 4 поворота на подставках.
* Визуальный гейт сгенерированного контента (урок №2): лист смотрится
* глазами через скриншот гейта; мост отдаёт геометрию листа, чтобы
* проверка считала экранные ячейки по той же проекции, что и камера.
*/
import {
VoxelRenderer, VoxelGrid, meshVoxels, treeModel, boulderModel, decodeModel,
} from '@rpg/engine';
import type { VoxelModel, VoxelPalette } from '@rpg/engine';
import * as THREE from 'three';
// Палитра арт-библии — та же, что в демке (слоты → hex)
const PALETTE: VoxelPalette = {
colors: {
1: '#4a5340', 2: '#343b2c', 3: '#626c4c', 4: '#5c3b2e', 5: '#7d8662',
6: '#5b5b66', 7: '#94949e', 8: '#82503a', 9: '#d99a32', 10: '#44454e',
},
};
/** Модели листа: колонки — 6 моделей (деревья/валуны, все варианты). */
const MODELS: ReadonlyArray<{ name: string; make: () => VoxelModel }> = [
{ name: 'tree0', make: () => treeModel(0, 5) },
{ name: 'tree1', make: () => treeModel(1, 5) },
{ name: 'tree2', make: () => treeModel(2, 5) },
{ name: 'boulder0', make: () => boulderModel(0, 5) },
{ name: 'boulder1', make: () => boulderModel(1, 5) },
{ name: 'boulder2', make: () => boulderModel(2, 5) },
];
const COLS = MODELS.length; // модели по оси X
const ROWS = 4; // повороты по оси Z
const PITCH = 9; // шаг ячеек (одинаковый по X и Z)
const YAWS = [0, Math.PI / 4, Math.PI / 2, (3 * Math.PI) / 4]; // диагонали дают другой силуэт
const sheetW = COLS * PITCH;
const sheetD = ROWS * PITCH;
const target: [number, number, number] = [sheetW / 2, 2, sheetD / 2];
const canvas = document.getElementById('scene') as HTMLCanvasElement;
const renderer = new VoxelRenderer(canvas, { width: 480, height: 360, target, viewSize: 32 });
/** Центр ячейки (целый: воксельные координаты должны быть целыми). */
function cellCenter(col: number, row: number): [number, number] {
return [col * PITCH + Math.floor(PITCH / 2), row * PITCH + Math.floor(PITCH / 2)];
}
/** Подставки ячеек: одна сетка на весь лист, плита 8×8×2 в центре каждой. */
function buildPads(): VoxelGrid {
const grid = new VoxelGrid(sheetW, 2, sheetD);
for (let col = 0; col < COLS; col++)
for (let row = 0; row < ROWS; row++) {
const [cx, cz] = cellCenter(col, row);
for (let dx = -4; dx < 4; dx++)
for (let dz = -4; dz < 4; dz++) {
grid.set(cx + dx, 0, cz + dz, 2);
const patch = ((cx + dx) * 7 + (cz + dz) * 13) % 11 < 3;
grid.set(cx + dx, 1, cz + dz, patch ? 3 : 1);
}
}
return grid;
}
renderer.addMesh(meshVoxels(buildPads(), PALETTE));
// Модели: вращение вокруг центра ячейки, диагональные ракурсы
const modelMeshes: THREE.Mesh[] = [];
for (let col = 0; col < COLS; col++)
for (let row = 0; row < ROWS; row++) {
const [cx, cz] = cellCenter(col, row);
const grid = decodeModel(MODELS[col].make());
const mesh = renderer.addMesh(meshVoxels(grid, PALETTE));
mesh.geometry.translate(-grid.sx / 2, 0, -grid.sz / 2); // ось вращения — центр модели
mesh.position.set(cx, 2, cz);
mesh.rotation.y = YAWS[row];
modelMeshes.push(mesh);
}
function stats(): { vertices: number; triangles: number } {
let vertices = 0, triangles = 0;
for (const m of modelMeshes) {
vertices += m.geometry.getAttribute('position').count;
triangles += (m.geometry.getIndex()?.count ?? 0) / 3;
}
return { vertices, triangles };
}
/** Геометрия листа для расчёта ячеек в гейте (та же проекция, что у камеры). */
function sheetInfo() {
return {
cols: COLS, rows: ROWS, pitch: PITCH,
target, viewSize: 30, width: 480, height: 360, scale: 2,
models: MODELS.map((m) => m.name),
};
}
const agent = {
stats,
sheetInfo,
/** Скрыть/показать модели (подставки остаются) — референсный кадр гейта. */
setModels(on: boolean): void {
for (const m of modelMeshes) m.visible = on;
},
};
(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;