import { sineInOut, type EaseFn } from '../core/easing';
import { GameState } from '../core/GameState';
import type { Camera } from '../render/Camera';
import type { Vec2 } from '../math/Vec2';
/**
* Раннер кат-сцен: view-агностичная последовательность шагов.
* Каждый шаг при старте выполняет свой эффект; раннер ждёт его длительность
* (0 — шаг «параллельный», следующий стартует сразу) и переходит к следующему.
* Сцена на время сцены ставит геймплей на паузу — раннер только сообщает `active`.
*/
export type CutsceneStep =
/** Подождать N секунд (ничего не делая). */
| { kind: 'wait'; seconds: number }
/** Плавно переместить камеру (в юнитах) к точке. */
| { kind: 'cameraMove'; x: number; y: number; seconds: number; camera: Camera }
/** Поднять флаг прохождения (мгновенно). */
| { kind: 'setFlag'; flag: string; state: GameState }
/** Разовый эффект (звук, кольцо, вспышка частиц). */
| { kind: 'burst'; fn: () => void; seconds?: number }
/** Произвольный вызов (диалог, посадка, тост и т.п.). */
| { kind: 'call'; fn: () => void; seconds?: number };
interface RunningStep {
seconds: number;
/** Движение камеры: from -> to по easing. */
cameraMove?: { from: Vec2; to: Vec2; camera: Camera };
}
/** Плавность движения камеры по умолчанию. */
const EASE: EaseFn = sineInOut;
export class CutsceneRunner {
private queue: CutsceneStep[] = [];
private current: RunningStep | null = null;
private t = 0;
/** Вызывается, когда все шаги дошли до конца. */
onComplete: (() => void) | null = null;
constructor(
/** Наблюдатель: вызывается при старте каждого шага (лог, отладка). */
private onStep?: (step: CutsceneStep) => void
) {}
/** Запустить последовательность шагов (немедленно). */
play(steps: CutsceneStep[]): void {
this.queue = [...steps];
this.t = 0;
this.next();
}
/** Идёт ли сейчас сцена (геймплей на паузе). */
get active(): boolean {
return this.current !== null;
}
/** Секунд прошло с начала текущего шага. */
get time(): number {
return this.t;
}
private next(): void {
const step = this.queue.shift();
if (!step) {
this.current = null;
this.onComplete?.();
return;
}
this.onStep?.(step);
switch (step.kind) {
case 'wait':
this.current = { seconds: step.seconds };
break;
case 'cameraMove':
this.current = {
seconds: step.seconds,
cameraMove: {
from: { x: step.camera.x, y: step.camera.y },
to: { x: step.x, y: step.y },
camera: step.camera
}
};
break;
case 'setFlag':
step.state.setFlag(step.flag);
this.current = { seconds: 0 };
break;
case 'burst':
case 'call':
step.fn();
this.current = { seconds: step.seconds ?? 0 };
break;
}
// Параллельный шаг (0 секунд) — сразу к следующему.
if (this.current.seconds <= 0) this.next();
}
update(dt: number): void {
if (!this.current) return;
this.t += dt;
const move = this.current.cameraMove;
if (move) {
const k = Math.min(1, this.t / this.current.seconds);
const e = EASE(k);
move.camera.x = move.from.x + (move.to.x - move.from.x) * e;
move.camera.y = move.from.y + (move.to.y - move.from.y) * e;
}
if (this.t >= this.current.seconds) {
this.t = 0;
this.next();
}
}
}