import { describe, expect, it } from 'vitest';
import { GameState } from '../../core/GameState';
import { linear } from '../../core/easing';
import { Camera } from '../../render/Camera';
import { CutsceneRunner } from '../CutsceneRunner';
import { applyEase } from '../tween';
/** Продвинуть раннер: dt N раз. */
function tick(runner: CutsceneRunner, dt: number, times: number): void {
for (let i = 0; i < times; i++) runner.update(dt);
}
/** Фиктивный «взгляд»: камера с заданным центром. */
function cameraAt(x: number, y: number): Camera {
const cam = new Camera(480, 270);
cam.x = x;
cam.y = y;
return cam;
}
describe('CutsceneRunner — последовательность шагов', () => {
it('шаги выполняются по порядку, wait держит паузу', () => {
const runner = new CutsceneRunner();
const order: string[] = [];
runner.play([
{ kind: 'call', fn: () => order.push('a') },
{ kind: 'wait', seconds: 0.5 },
{ kind: 'call', fn: () => order.push('b') }
]);
// Первый шаг стартовал сразу, второй ещё ждёт.
expect(order).toEqual(['a']);
expect(runner.active).toBe(true);
tick(runner, 0.1, 4); // 0.4с — всё ещё wait
expect(order).toEqual(['a']);
tick(runner, 0.2, 1); // 0.6с — wait кончился
expect(order).toEqual(['a', 'b']);
expect(runner.active).toBe(false);
});
it('шаг без seconds параллелен: следующий стартует сразу', () => {
const runner = new CutsceneRunner();
const order: string[] = [];
runner.play([
{ kind: 'burst', fn: () => order.push('burst') },
{ kind: 'call', fn: () => order.push('call') }
]);
// Шаги без длительности не блокируют: оба стартовали в том же тике.
expect(order).toEqual(['burst', 'call']);
expect(runner.active).toBe(false);
});
it('cameraMove плавно ведёт камеру к точке', () => {
const runner = new CutsceneRunner();
const cam = cameraAt(0, 0);
runner.play([{ kind: 'cameraMove', x: 10, y: -20, seconds: 1, camera: cam }]);
tick(runner, 0.25, 2); // половина пути
expect(cam.x).toBeGreaterThan(0);
expect(cam.x).toBeLessThan(10);
expect(cam.y).toBeLessThan(0);
tick(runner, 0.25, 3); // время вышло — точно в точке
expect(cam.x).toBe(10);
expect(cam.y).toBe(-20);
expect(runner.active).toBe(false);
});
it('setFlag поднимает флаги, onComplete вызывается в конце', () => {
const runner = new CutsceneRunner();
const state = new GameState();
let done = 0;
runner.onComplete = () => done++;
runner.play([
{ kind: 'setFlag', flag: 'flag1', state },
{ kind: 'setFlag', flag: 'flag2', state }
]);
expect(state.hasFlag('flag1')).toBe(true);
expect(state.hasFlag('flag2')).toBe(true);
expect(done).toBe(1);
});
it('onStep наблюдает порядок шагов', () => {
const seen: string[] = [];
const runner = new CutsceneRunner((s) => seen.push(s.kind));
runner.play([
{ kind: 'call', fn: () => undefined, seconds: 0.1 },
{ kind: 'wait', seconds: 0.1 }
]);
// Первый шаг стартовал; второй — после его длительности.
expect(seen).toEqual(['call']);
tick(runner, 0.2, 1);
expect(seen).toEqual(['call', 'wait']);
});
it('until ждёт внешнее событие и завершается по предикату', () => {
const runner = new CutsceneRunner();
const order: string[] = [];
let ready = false;
runner.play([
{ kind: 'until', predicate: () => ready, label: 'событие' },
{ kind: 'call', fn: () => order.push('after') }
]);
tick(runner, 0.1, 5); // событие не наступило — стоим на месте
expect(order).toEqual([]);
expect(runner.active).toBe(true);
ready = true;
runner.update(0.1);
expect(order).toEqual(['after']);
expect(runner.active).toBe(false);
});
it('until с уже выполненным предикатом не блокирует', () => {
const runner = new CutsceneRunner();
const order: string[] = [];
runner.play([
{ kind: 'until', predicate: () => true },
{ kind: 'call', fn: () => order.push('next') }
]);
expect(order).toEqual(['next']);
expect(runner.active).toBe(false);
});
it('until с timeout завершается принудительно по истечении', () => {
const runner = new CutsceneRunner();
let done = 0;
runner.onComplete = () => done++;
runner.play([{ kind: 'until', predicate: () => false, timeout: 0.3 }]);
tick(runner, 0.1, 2); // 0.2с — ещё ждём
expect(runner.active).toBe(true);
tick(runner, 0.1, 1); // 0.3с — timeout
expect(runner.active).toBe(false);
expect(done).toBe(1);
});
});
describe('tween — applyEase', () => {
it('интерполирует точку по кривой easing', () => {
const from = { x: 0, y: 0 };
const to = { x: 10, y: -20 };
const mid = applyEase(from, to, 0.5, linear);
expect(mid.x).toBeCloseTo(5);
expect(mid.y).toBeCloseTo(-10);
});
it('клэмпит k за пределы 0..1', () => {
const end = applyEase({ x: 0, y: 0 }, { x: 8, y: 4 }, 2, linear);
expect(end.x).toBe(8);
expect(end.y).toBe(4);
const start = applyEase({ x: 0, y: 0 }, { x: 8, y: 4 }, -1, linear);
expect(start.x).toBe(0);
});
});