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

// SpriteAnimator пишет texture в Sprite — минимальный стаб.
function makeSprite() {
    return { texture: null as unknown, destroyed: false } as unknown as import('pixi.js').Sprite;
}

function frames(n: number): import('pixi.js').Texture[] {
    return Array.from({ length: n }, (_, i) => ({ idx: i }) as unknown as import('pixi.js').Texture);
}

import { SpriteAnimator } from '../SpriteAnimator';

describe('SpriteAnimator', () => {
    it('конструктор без клипа: показывает стартовый шаг, время стоит', () => {
        const sprite = makeSprite();
        const a = new SpriteAnimator(sprite, { idle: { frames: frames(4) } }, undefined, { startStep: 2 });
        expect(a.clip).toBe('idle');
        expect(a.step).toBe(2);
        expect(a.paused).toBe(true);
        a.update(1);
        expect(a.step).toBe(2); // заморожен
    });

    it('loop: шаги идут по fps, по кругу', () => {
        const sprite = makeSprite();
        const a = new SpriteAnimator(sprite, { walk: { frames: frames(3), fps: 10 } }, 'walk');
        expect(a.step).toBe(0);
        a.update(0.1); // 1 шаг при 10 fps
        expect(a.step).toBe(1);
        a.update(0.2); // ещё 2 — на конец
        expect(a.step).toBe(0); // 3 % 3
    });

    it('тот же клип без restart продолжается; restart начинает с нуля', () => {
        const sprite = makeSprite();
        const a = new SpriteAnimator(sprite, { walk: { frames: frames(4), fps: 10 } }, 'walk');
        a.update(0.15);
        expect(a.step).toBe(1);
        a.play('walk');
        expect(a.step).toBe(1); // продолжение
        a.play('walk', { restart: true });
        expect(a.step).toBe(0);
    });

    it('once: доигрывает до последнего шага, paused, onFinish один раз', () => {
        const sprite = makeSprite();
        const finished: string[] = [];
        const a = new SpriteAnimator(sprite, { hit: { frames: frames(3), fps: 10, loop: 'once' } }, 'hit');
        a.onFinish = (c) => finished.push(c);
        for (let i = 0; i < 10; i++) a.update(0.1); // 1 с — клип 0.3 с
        expect(a.step).toBe(2);
        expect(a.paused).toBe(true);
        expect(finished).toEqual(['hit']);
    });

    it('большой dt проматывает промежуточные шаги (сообщается финальный)', () => {
        const sprite = makeSprite();
        const seen: number[] = [];
        const a = new SpriteAnimator(sprite, { walk: { frames: frames(8), fps: 10 } }, 'walk');
        a.onFrame = (s) => seen.push(s);
        a.update(1); // 10 шагов за тик
        expect(a.step).toBe(2); // 10 % 8
        expect(seen).toEqual([2]); // промежуточные не долетели
    });

    it('randomPhase: одинаковый seed — одинаковая фаза (шаги совпадают)', () => {
        const mk = (seed: number) =>
            new SpriteAnimator(makeSprite(), { walk: { frames: frames(3) } }, undefined, { randomPhase: seed });
        const a = mk(1234);
        const b = mk(1234);
        // Фаза приватна, но детерминизм виден через play + апдейты:
        // time = phase, поэтому при одинаковых апдейтах шаги совпадают.
        a.play('walk');
        b.play('walk');
        a.update(0.05);
        b.update(0.05);
        expect(a.step).toBe(b.step);
        a.update(0.3);
        b.update(0.3);
        expect(a.step).toBe(b.step);
    });

    it('setClipFrames: кадры меняются на лету, шаг клэмпится', () => {
        const sprite = makeSprite();
        const a = new SpriteAnimator(sprite, { walk: { frames: frames(4), fps: 10 } }, 'walk');
        a.update(0.3); // шаг 3
        expect(a.step).toBe(3);
        a.setClipFrames('walk', frames(2));
        expect(a.step).toBe(1); // клэмп к 2-1
    });

    it('несуществующий клип — ошибка', () => {
        const a = new SpriteAnimator(makeSprite(), { walk: { frames: frames(2) } });
        expect(() => a.play('fly')).toThrow('fly');
    });

    it('destroyed спрайта пробрасывается (самозачистка из engine.fx)', () => {
        const sprite = makeSprite();
        const a = new SpriteAnimator(sprite, { walk: { frames: frames(2) } });
        expect(a.destroyed).toBe(false);
        sprite.destroyed = true;
        expect(a.destroyed).toBe(true);
    });

    it('speedScale замедляет/ускоряет клип', () => {
        const a = new SpriteAnimator(makeSprite(), { walk: { frames: frames(10), fps: 10 } }, 'walk', { speedScale: 0.5 });
        a.update(0.1);
        expect(a.step).toBe(0); // полшага — не дошёл
        a.update(0.1);
        expect(a.step).toBe(1);
    });

    it('пустой набор клипов допустим (без падений)', () => {
        const a = new SpriteAnimator(makeSprite(), {});
        expect(a.clip).toBeNull();
        expect(() => a.update(0.1)).not.toThrow();
    });

    it('onFrame получает номер шага', () => {
        const onFrame = vi.fn();
        const a = new SpriteAnimator(makeSprite(), { walk: { frames: frames(2), fps: 10 } }, 'walk');
        a.onFrame = onFrame;
        a.update(0.1);
        expect(onFrame).toHaveBeenCalledWith(1);
    });
});