Newer
Older
rpg / packages / engine / src / anim / __tests__ / motion.test.ts
import { describe, expect, it } from 'vitest';
import { sampleMotion } from '../motion';

describe('sampleMotion', () => {
    it('bob: синус по Y, округлён до целого px', () => {
        const p = { bob: { amplitude: 2, period: 1 } };
        expect(sampleMotion(p, 0).y).toBe(0); // 0
        expect(sampleMotion(p, 0.25).y).toBe(2); // пик
        expect(sampleMotion(p, 0.75).y).toBe(-2); // низ
        // Между узлами — целые: субпиксельного дрожания нет
        const mid = sampleMotion(p, 0.05).y;
        expect(Number.isInteger(mid)).toBe(true);
    });

    it('sway: угол в рад, амплитуда соблюдена', () => {
        const p = { sway: { amplitude: 0.1, period: 2 } };
        expect(sampleMotion(p, 0.5).angle).toBeCloseTo(0.1, 6); // четверть периода = пик
        expect(sampleMotion(p, 1.5).angle).toBeCloseTo(-0.1, 6);
        expect(sampleMotion(p, 0).angle).toBeCloseTo(0, 6);
    });

    it('pulse: границы min/max, треугольная волна', () => {
        const p = { pulse: { min: 0.4, max: 1, period: 2 } };
        expect(sampleMotion(p, 0).alpha).toBeCloseTo(0.4); // старт с минимума
        expect(sampleMotion(p, 1).alpha).toBeCloseTo(1); // середина = max
        expect(sampleMotion(p, 2).alpha).toBeCloseTo(0.4); // период замкнулся
        expect(sampleMotion(p, 1).scale).toBeCloseTo(1);
    });

    it('blink: строб 0/1 с duty', () => {
        const p = { blink: { period: 1, duty: 0.25 } };
        expect(sampleMotion(p, 0).alpha).toBe(1);
        expect(sampleMotion(p, 0.2).alpha).toBe(1); // внутри duty
        expect(sampleMotion(p, 0.5).alpha).toBe(0); // вне duty
        expect(sampleMotion(p, 0.95).alpha).toBe(0);
    });

    it('композиция: bob+sway+pulse+blink независимы', () => {
        const p = {
            bob: { amplitude: 2, period: 1 },
            sway: { amplitude: 0.1, period: 1 },
            pulse: { min: 0.5, max: 1, period: 1 },
            blink: { period: 1, duty: 1 } // всегда включён
        };
        const s = sampleMotion(p, 0.25);
        expect(s.y).toBe(2);
        expect(s.angle).toBeCloseTo(0.1, 6);
        expect(s.alpha).toBeCloseTo(0.75, 6); // pulse: tri(0.25)=0.5 → k=0.75, blink=1
    });

    it('phase сдвигает волну (доля периода)', () => {
        const p = { bob: { amplitude: 2, period: 1, phase: 0.25 } };
        expect(sampleMotion(p, 0).y).toBe(2); // при t=0 уже пик
    });

    it('пустые параметры — нейтральная выборка', () => {
        const s = sampleMotion({}, 5);
        expect(s).toEqual({ y: 0, angle: 0, alpha: 1, scale: 1 });
    });

    it('волна замкнута на границе периода (без скачка)', () => {
        const p = { bob: { amplitude: 3, period: 2 } };
        expect(sampleMotion(p, 0).y).toBe(sampleMotion(p, 2).y);
    });
});