/**
* Аним-лист: 8 кадров walk-цикла манекена (4×2) на подставках.
* Превью-лист кадров анимации (docs/plan.md, трек 6): клип процедурный
* (walkClip), позы считаются samplePose→poseVoxels, рисуются instanced-облаком.
* Мост отдаёт геометрию листа и числовые снапшоты поз — гейт проверяет
* и картинку, и данные (контакт стопы, противофазы, замкнутость цикла).
*/
import {
VoxelRenderer, VoxelGrid, meshVoxels, mannequin, walkClip, samplePose, poseVoxels,
validateClip, footMinY,
} from '@rpg/engine';
import type { VoxelPalette, VoxelCloud } from '@rpg/engine';
// Палитра арт-библии — та же, что в демке (слоты → 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',
},
};
const COLS = 4; // кадры 0..3 по оси X
const ROWS = 2; // кадры 4..7 по оси Z
const FRAMES = 8;
const PITCH = 18; // манекен 14 шириной + шаг ног — с запасом
// walk-цикл манекена: параметры локомоции (агент правит их текстом через клип)
const DURATION = 0.8;
const clip = walkClip({ duration: DURATION, legSwing: 0.55, armSwing: 0.4, bob: 0.25, sway: 0.15 });
const { model, rig } = mannequin();
const sheetW = COLS * PITCH;
const sheetD = ROWS * PITCH;
const target: [number, number, number] = [sheetW / 2, 2, sheetD / 2];
const VIEW_SIZE = 44; // лист 72×36 + рост манекена — с запасом
const canvas = document.getElementById('scene') as HTMLCanvasElement;
const renderer = new VoxelRenderer(canvas, { width: 480, height: 360, target, viewSize: VIEW_SIZE });
/** Центр ячейки (целый: воксельные координаты должны быть целыми). */
function cellCenter(col: number, row: number): [number, number] {
return [col * PITCH + Math.floor(PITCH / 2), row * PITCH + Math.floor(PITCH / 2)];
}
/** Подставки ячеек: одна сетка на весь лист, плита 14×14×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 = -7; dx < 7; dx++)
for (let dz = -7; dz < 7; 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));
// Кадры цикла: поза i → instanced-облако в ячейке (row = i / COLS)
const clouds: VoxelCloud[] = [];
for (let i = 0; i < FRAMES; i++) {
const col = i % COLS, row = Math.floor(i / COLS);
const [cx, cz] = cellCenter(col, row);
const pose = samplePose(clip, (DURATION * i) / FRAMES);
const posed = poseVoxels(rig, model, pose).map((v) => ({
// манекен 14×24×8 — центр по XZ ячейки, на подставке (y=2)
pos: [v.pos[0] + cx - model.size[0] / 2, v.pos[1] + 2, v.pos[2] + cz - model.size[2] / 2] as [number, number, number],
color: v.color,
}));
clouds.push(renderer.addVoxelCloud(posed));
}
function stats(): { clouds: number; voxels: number } {
return { clouds: clouds.length, voxels: clouds.reduce((n, c) => n + c.mesh.count, 0) };
}
/** Геометрия листа для расчёта ячеек в гейте (та же проекция, что у камеры). */
function sheetInfo() {
return {
cols: COLS, rows: ROWS, pitch: PITCH, frames: FRAMES, duration: DURATION,
target, viewSize: VIEW_SIZE, width: 480, height: 360, scale: 2,
};
}
/** Числовый снапшот позы кадра i: углы, качание корня, контакт стоп. */
function poseStats(i: number) {
const t = (DURATION * i) / FRAMES;
const pose = samplePose(clip, t);
return {
t,
legL: pose.legL?.rot?.[0] ?? 0,
legR: pose.legR?.rot?.[0] ?? 0,
armL: pose.armL?.rot?.[0] ?? 0,
armR: pose.armR?.rot?.[0] ?? 0,
rootPos: pose.root?.pos ?? [0, 0, 0],
footMinL: footMinY(rig, model, clip, 'legL', t),
footMinR: footMinY(rig, model, clip, 'legR', t),
};
}
const agent = {
stats,
sheetInfo,
poseStats,
/** Числовая валидация клипа (замкнутость цикла) — через мост, как валидация агентом. */
clipCheck(): string[] {
return validateClip(clip);
},
/** Скрыть/показать кадры (подставки остаются) — референсный кадр гейта. */
setModels(on: boolean): void {
for (const c of clouds) c.mesh.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;