Newer
Older
rpg / packages / engine / tools / __tests__ / frames.test.mjs
import { describe, expect, it } from 'vitest';
import { shiftFrame, bobFrame, squashFrame, mirrorFrame, swingFrame, deriveCycle } from '../frames.mjs';

/** Кадр 4×4 из строк символов: '.' — прозрачный, '#' — непрозрачный белый. */
const frame = (rows) => {
    const w = rows[0].length;
    const h = rows.length;
    const rgba = new Uint8Array(w * h * 4);
    for (let y = 0; y < h; y++) {
        for (let x = 0; x < w; x++) {
            if (rows[y][x] === '#') rgba[(y * w + x) * 4 + 3] = 255;
        }
    }
    return rgba;
};

/** Кадр обратно в строки символов (по alpha). */
const toRows = (rgba, w, h) => {
    const rows = [];
    for (let y = 0; y < h; y++) {
        let row = '';
        for (let x = 0; x < w; x++) row += rgba[(y * w + x) * 4 + 3] ? '#' : '.';
        rows.push(row);
    }
    return rows;
};

const SRC = frame([
    '....',
    '.##.',
    '.##.',
    '....'
]);

describe('shiftFrame', () => {
    it('сдвигает содержимое, пустота — прозрачность', () => {
        expect(toRows(shiftFrame(SRC, 4, 4, 1, 0), 4, 4)).toEqual([
            '....',
            '..##',
            '..##',
            '....'
        ]);
        expect(toRows(shiftFrame(SRC, 4, 4, 0, -1), 4, 4)).toEqual([
            '.##.',
            '.##.',
            '....',
            '....'
        ]);
        // Сдвиг за край — кадр полностью прозрачный
        expect(toRows(shiftFrame(SRC, 4, 4, 4, 0), 4, 4)).toEqual(['....', '....', '....', '....']);
    });
});

describe('bobFrame', () => {
    it('поднимает тело на dy (по умолчанию 1)', () => {
        expect(toRows(bobFrame(SRC, 4, 4), 4, 4)).toEqual([
            '.##.',
            '.##.',
            '....',
            '....'
        ]);
        expect(toRows(bobFrame(SRC, 4, 4, 2), 4, 4)).toEqual([
            '.##.',
            '....',
            '....',
            '....'
        ]);
    });
});

describe('squashFrame', () => {
    it('приседание: спрайт на 1 px вниз, нижний ряд срезается', () => {
        const src = frame([
            '....',
            '.##.',
            '.##.',
            '.##.'
        ]);
        expect(toRows(squashFrame(src, 4, 4), 4, 4)).toEqual([
            '....',
            '....',
            '.##.',
            '.##.'
        ]);
        // Спрайт стал ниже: было 3 ряда содержимого, осталось 2
        const out = squashFrame(src, 4, 4);
        expect(out[(0 * 4 + 1) * 4 + 3]).toBe(0);
        expect(out[(3 * 4 + 1) * 4 + 3]).toBe(255);
    });
});

describe('mirrorFrame', () => {
    it('отражает по горизонтали', () => {
        const src = frame([
            '#...',
            '.##.',
            '....',
            '....'
        ]);
        expect(toRows(mirrorFrame(src, 4, 4), 4, 4)).toEqual([
            '...#',
            '.##.',
            '....',
            '....'
        ]);
    });
});

describe('swingFrame', () => {
    it('поворот вокруг низ-центра: 0 рад — кадр без изменений', () => {
        expect(swingFrame(SRC, 4, 4, 0)).toEqual(SRC);
    });

    it('точка опоры остаётся на месте при малом угле', () => {
        const out = swingFrame(SRC, 4, 4, 0.2);
        // Нижняя пара пикселей фигуры (y=2) около оси — почти не сместилась
        expect(out[(2 * 4 + 1) * 4 + 3]).toBe(255);
        // Верх фигуры (y=1) ушла за пределы или сместилась — но что-то осталось
        expect(toRows(out, 4, 4).join('')).not.toBe('................');
    });
});

describe('deriveCycle', () => {
    it('по дескриптору на кадр, операции применяются по порядку', () => {
        const cycle = deriveCycle(SRC, 4, 4, [{}, { bob: 1 }, { mirror: true }]);
        expect(cycle).toHaveLength(3);
        expect(toRows(cycle[0], 4, 4)).toEqual(toRows(SRC, 4, 4));
        expect(toRows(cycle[1], 4, 4)).toEqual(toRows(bobFrame(SRC, 4, 4), 4, 4));
        expect(toRows(cycle[2], 4, 4)).toEqual(toRows(mirrorFrame(SRC, 4, 4), 4, 4));
    });
});