/**
* Манекен — эталонная модель для рига и анимации (docs/plan.md, трек 6).
* Путник 14×24×8 (~2.4 тайла UNIT=10; фасад — сторона +Z: глаза, нос,
* мыски сапог). Силуэт собирается деталями, а не кубом: сапоги с мысками,
* штаны, ремень, плащ (расширяется книзу), рукава с кистями, шея, голова
* с глазами и волосами (по палитре — капюшоном). Палитра — параметр:
* у героя и NPC свои цвета (по умолчанию — деревянный болванчик). Части
* известны при генерации, поэтому привязка к костям передаётся явно
* (bindNearest здесь не нужен: торс рядом с плечами и Voronoi по pivot
* отнёс бы его к рукам).
*/
import { VoxelGrid } from '../voxel/grid';
import { encodeModel } from './format';
import type { VoxelModel } from './format';
import { makeRig } from '../animation/rig';
import type { VoxelRig } from '../animation/rig';
// Слоты модели (палитра — параметр mannequin())
const TORSO = 4; // плащ
const LEG = 6; // штаны
const SKIN = 7; // лицо/кисти
const ARM = 8; // рукава
const BOOT = 2; // сапоги
const HAIR = 5; // волосы/капюшон
const BELT = 9; // ремень
const EYE = 10; // глаза (тёмный, не зависит от ремня)
export const MANNEQUIN_BONES = ['root', 'legL', 'legR', 'armL', 'armR'] as const;
export type MannequinBone = (typeof MANNEQUIN_BONES)[number];
/** Палитра персонажа: цвет слота модели (по умолчанию — дерево). */
export interface MannequinPalette {
/** Лицо и кисти рук. */
skin?: string;
/** Волосы или капюшон. */
hair?: string;
/** Торс / плащ. */
torso?: string;
/** Штаны. */
legs?: string;
/** Рукава. */
arms?: string;
/** Сапоги. */
boots?: string;
/** Ремень. */
belt?: string;
/** Глаза (по умолчанию тёмные — не зависят от ремня). */
eyes?: string;
}
export interface Mannequin {
model: VoxelModel;
rig: VoxelRig;
}
/** Детерминированный манекен с ригой: root → legL/legR/armL/armR. */
export function mannequin(palette: MannequinPalette = {}): Mannequin {
const g = new VoxelGrid(14, 24, 8);
// привязка: индекс вокселя → индекс кости (root=0, legL=1, legR=2, armL=3, armR=4)
const binding = new Array<number>(g.data.length).fill(-1);
const put = (x: number, y: number, z: number, slot: number, bone: number): void => {
g.set(x, y, z, slot);
binding[x + g.sx * (y + g.sy * z)] = bone;
};
// параллелепипед [x0..x1]×[y0..y1]×[z0..z1] одной костью
const box = (
x0: number, x1: number, y0: number, y1: number, z0: number, z1: number,
slot: number, bone: number,
): void => {
for (let y = y0; y <= y1; y++)
for (let z = z0; z <= z1; z++)
for (let x = x0; x <= x1; x++) put(x, y, z, slot, bone);
};
// ноги: колонны 2×2 (x3..4 и x9..10, z3..4), y2..9; сапоги y0..1 с мыском к +Z
box(3, 4, 2, 9, 3, 4, LEG, 1);
box(9, 10, 2, 9, 3, 4, LEG, 2);
for (const [x0, bone] of [[3, 1], [9, 2]] as const) {
box(x0, x0 + 1, 0, 1, 3, 4, BOOT, bone);
box(x0, x0 + 1, 0, 0, 5, 6, BOOT, bone); // мысок
}
// торс: юбка плаща y10..12 шире, пояс y13, грудь y14..15, плечи y16..17
box(3, 10, 10, 12, 2, 5, TORSO, 0);
box(4, 9, 13, 13, 2, 5, BELT, 0);
box(4, 9, 14, 15, 2, 5, TORSO, 0);
box(3, 10, 16, 17, 2, 5, TORSO, 0);
// руки: рукава 2×2 (x1..2 и x11..12, z3..4) y12..17, кисти (кожа) y10..11
for (const [x0, bone] of [[1, 3], [11, 4]] as const) {
box(x0, x0 + 1, 12, 17, 3, 4, ARM, bone);
box(x0, x0 + 1, 10, 11, 3, 4, SKIN, bone);
}
// шея и голова: y18 шея, голова y19..23. Череп ступенькой: фасад лица
// (z5) только до y21, волосы-шапка назад до z4 — с игрового ракурса
// (камера сверху ~55°) над глазами не должно быть вокселей, иначе
// луч к ним проходит через шапку и лицо не читается
box(6, 7, 18, 18, 3, 4, SKIN, 0);
box(4, 9, 19, 23, 2, 4, SKIN, 0);
box(4, 9, 19, 21, 5, 5, SKIN, 0);
box(4, 9, 22, 23, 2, 4, HAIR, 0);
box(4, 9, 19, 21, 2, 2, HAIR, 0);
// лицо на фасаде (+Z): глаза, нос выступает к +Z
put(5, 21, 5, EYE, 0);
put(8, 21, 5, EYE, 0);
put(6, 20, 6, SKIN, 0);
put(7, 20, 6, SKIN, 0);
const model = encodeModel(g, {
4: palette.torso ?? '#5c3b2e',
6: palette.legs ?? '#5b5b66',
7: palette.skin ?? '#94949e',
8: palette.arms ?? '#82503a',
2: palette.boots ?? '#44454e',
5: palette.hair ?? '#6e4a30',
9: palette.belt ?? '#3a3438',
10: palette.eyes ?? '#2a2624',
});
const rig = makeRig(
[
{ name: 'root', parent: -1, pivot: [6.5, 0, 4] }, // земля, центр пят
{ name: 'legL', parent: 0, pivot: [3.5, 10, 4] }, // бедро левой
{ name: 'legR', parent: 0, pivot: [9.5, 10, 4] }, // бедро правой
{ name: 'armL', parent: 0, pivot: [1.5, 17, 4] }, // плечо левой
{ name: 'armR', parent: 0, pivot: [11.5, 17, 4] }, // плечо правой
],
model,
binding,
);
return { model, rig };
}