import { sineInOut, type EaseFn } from '../core/easing';
import { GameState } from '../core/GameState';
import type { Camera } from '../render/Camera';
import type { Vec2 } from '../math/Vec2';
import { applyEase } from './tween';
/**
* Раннер кат-сцен: view-агностичная последовательность шагов.
* Каждый шаг при старте выполняет свой эффект; раннер ждёт его длительность
* (0 — шаг «параллельный», следующий стартует сразу) и переходит к следующему.
* Сцена на время сцены ставит геймплей на паузу — раннер только сообщает `active`.
*/
/** Всё, что можно двигать шагом moveView: структурно — любой Container. */
export interface Movable {
position: Vec2;
}
export type CutsceneStep =
/** Подождать N секунд (ничего не делая). */
| { kind: 'wait'; seconds: number }
/** Ждать внешнее событие (диалог, приход актёра): пока predicate() не true.
* timeout — принудительное завершение шага (сек); без него шаг может
* длиться вечно — скип/агентный мост на таком шаге не повиснет только
* если событие реально наступит. */
| { kind: 'until'; predicate: () => boolean; label?: string; timeout?: number }
/** Плавно переместить камеру (в юнитах) к точке. */
| { kind: 'cameraMove'; x: number; y: number; seconds: number; camera: Camera }
/** Плавно сменить зум камеры (seconds: 0 — мгновенно, параллелен ходу). */
| { kind: 'cameraZoom'; camera: Camera; z: number; seconds?: number }
/** Плавно переместить вьюху (актёра, объект) в локальных координатах её родителя. */
| { kind: 'moveView'; view: Movable; x: number; y: number; seconds: number; ease?: EaseFn }
/** Поднять флаг прохождения (мгновенно). */
| { kind: 'setFlag'; flag: string; state: GameState }
/** Разовый эффект (звук, кольцо, вспышка частиц). */
| { kind: 'burst'; fn: () => void; seconds?: number }
/** Произвольный вызов (диалог, посадка, тост и т.п.). */
| { kind: 'call'; fn: () => void; seconds?: number };
interface RunningStep {
seconds: number;
/** Ожидание внешнего события: шаг кончается, когда предикат стал true. */
until?: { predicate: () => boolean };
/** Движение камеры: from -> to по easing. */
cameraMove?: { from: Vec2; to: Vec2; camera: Camera };
/** Движение вьюхи: from -> to по easing. */
moveView?: { from: Vec2; to: Vec2; view: Movable; ease: EaseFn };
}
/** Плавность движения камеры по умолчанию. */
const EASE: EaseFn = sineInOut;
export class CutsceneRunner {
private queue: CutsceneStep[] = [];
private current: RunningStep | null = null;
private t = 0;
/** Внутри skip(): until форсируются, сцена обязана дойти до конца. */
private skipping = false;
/** Вызывается, когда все шаги дошли до конца. */
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 'until': {
// Предикат уже выполнен — шаг не блокирует (секунды 0 → сразу дальше).
const done = step.predicate();
this.current = {
seconds: done ? 0 : (step.timeout ?? Infinity),
until: done ? undefined : { predicate: step.predicate }
};
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 'cameraZoom':
step.camera.zoomTo(step.z, step.seconds ?? 0);
this.current = { seconds: step.seconds ?? 0 };
break;
case 'moveView':
this.current = {
seconds: step.seconds,
moveView: {
from: { x: step.view.position.x, y: step.view.position.y },
to: { x: step.x, y: step.y },
view: step.view,
ease: step.ease ?? EASE
}
};
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 current = this.current;
// Ожидание события: предикат сработал — шаг кончен (timeout ловит «>=» ниже).
// Скип форсирует любые until: катсцена обязана кончиться.
if (current.until && (this.skipping || current.until.predicate())) {
this.t = 0;
this.next();
return;
}
const move = current.cameraMove;
if (move) {
const p = applyEase(move.from, move.to, this.t / current.seconds, EASE);
move.camera.x = p.x;
move.camera.y = p.y;
}
const viewMove = current.moveView;
if (viewMove) {
const p = applyEase(viewMove.from, viewMove.to, this.t / current.seconds, viewMove.ease);
viewMove.view.position.x = p.x;
viewMove.view.position.y = p.y;
}
if (this.t >= current.seconds) {
this.t = 0;
this.next();
}
}
/** Дожать сцену до конца немедленно: until форсируются, движение — в конечные точки. */
skip(): void {
if (!this.current) return;
this.skipping = true;
// Лимит — защита от шага, который при скипе порождает шаги бесконечно.
for (let i = 0; i < 1000 && this.current; i++) {
const remain = this.current.until ? 0 : this.current.seconds - this.t;
this.update(Math.max(0, remain) + 0.001);
}
this.skipping = false;
}
}