Newer
Older
rpg / v2 / packages / engine / src / animation / __tests__ / attach.test.ts
/**
 * Тесты креплений предметов к костям риги: покой/поворот кости (предмет
 * едет с кистью), сдвиг id тайлей, неизвестная кость, детерминизм.
 */
import { describe, expect, it } from 'vitest';
import { VoxelGrid } from '../../voxel/grid';
import { encodeModel } from '../../models/format';
import type { VoxelModel } from '../../models/format';
import { mannequin } from '../../models/mannequin';
import { backpackModel } from '../../models/generators';
import { attachVoxels, attachTiles } from '../attach';

/** Модель-кубик 1×1×1 одного слота. */
function dot(color: string): VoxelModel {
    const g = new VoxelGrid(1, 1, 1);
    g.set(0, 0, 0, 1);
    return encodeModel(g, { 1: color });
}

const man = mannequin();
const IDLE = { root: { rot: [0, 0, 0] as const } };

describe('attachVoxels', () => {
    it('пустой список креплений = число вокселей базы', () => {
        const base = attachVoxels(man.rig, man.model, IDLE, []);
        expect(base.length).toBe(164); // все воксели манекена
    });

    it('предмет у точки крепления, цвет — из палитры предмета', () => {
        const atts = [{ bone: 'root', model: dot('#ff0000'), pos: [2, 5, -1] as const }];
        const v = attachVoxels(man.rig, man.model, IDLE, atts);
        const item = v[v.length - 1]!;
        expect(item.color).toBe('#ff0000');
        expect(item.pos).toEqual([2.5, 5.5, -0.5]); // центр вокселя предмета
        expect(v.length).toBe(165);
    });

    it('предмет на кости руки едет вместе с её поворотом', () => {
        const atts = [{ bone: 'armR', model: dot('#00ff00'), pos: [8, 5, 2] as const }];
        const idle = attachVoxels(man.rig, man.model, IDLE, atts);
        const swing = attachVoxels(man.rig, man.model, { armR: { rot: [0.55, 0, 0] as const } }, atts);
        // предмет сместился ровно как воксель кисти (та же аффинна кости)
        const armVoxel = (list: typeof idle) => list.find((v) => v.index === 0)!;
        const dxItem = idle[idle.length - 1]!.pos[1] - swing[swing.length - 1]!.pos[1];
        const dxArm = armVoxel(idle).pos[1] - armVoxel(swing).pos[1];
        expect(Math.abs(dxItem - dxArm)).toBeLessThan(1e-9);
        expect(Math.abs(dxItem)).toBeGreaterThan(0.5); // рука с предметом реально качнулась
    });

    it('неизвестная кость — ошибка', () => {
        expect(() => attachVoxels(man.rig, man.model, IDLE, [{ bone: 'tail', model: dot('#fff') }]))
            .toThrow(/tail/);
    });

    it('тайли предметов сдвигаются за базу (и в вокселях, и в CloudTiles)', () => {
        const g = new VoxelGrid(1, 1, 1);
        g.set(0, 0, 0, 1);
        const face = btoa(String.fromCharCode(1, 1, 1, 1)); // size 2, слот 1
        const item = encodeModel(g, { 1: '#123456' }, {
            tiles: { 1: { size: 2, faces: [face, face, face, face, face, face] } }, // тайль id 1
            tex: new Uint8Array([1]),
        });
        const atts = [{ bone: 'root', model: item, pos: [2, 5, -1] as const }];
        const v = attachVoxels(man.rig, man.model, IDLE, atts);
        // у манекена 19 тайлей (id 1..19) — тайль предмета стал id 20
        expect(v[v.length - 1]!.tile).toBe(20);
        const tiles = attachTiles(man.model, atts);
        expect(Object.keys(tiles).map(Number).sort((a, b) => a - b)).toEqual(
            [...Array(20).keys()].map((i) => i + 1),
        );
        expect(tiles[20]!.faces.length).toBe(6);
    });

    it('детерминизм: два вызова — глубокое равенство', () => {
        const atts = [{ bone: 'root', model: backpackModel(), pos: [2, 5, -1] as const }];
        expect(attachVoxels(man.rig, man.model, IDLE, atts))
            .toEqual(attachVoxels(man.rig, man.model, IDLE, atts));
    });
});