/**
* Катсцены игры как данные (сюжетные сцены трека 19): метки точек камеры
* (тайлы v1), именованные действия и реестр сцен. Рантайм — движковый
* CutsceneRunner; resolveCutscenes связывает данные с действиями и
* валидирует при старте (как validateDialogue — битый каталог падает сразу).
*/
import { UNIT, GROUND_Y } from '../areas';
import { QUEST_TOWER_TILE, QUEST_TONE_TILES } from './quests';
import type { CutsceneStep } from '@rpg/engine';
/** Шаг катсцены в данных: длительность + метки действия/камеры. */
export interface CutsceneStepData {
/** Длительность шага (сек ручного времени цикла — детерминизм гейта). */
dur: number;
/** Именованное действие (ключ реестра действий main.ts). */
action?: string;
/** Аргумент действия (текст тоста). */
text?: string;
/** Метка точки камеры для глайда (ключ CUTSCENE_CAMS). */
cam?: string;
/** Точка для действия без глайда шага (ключ CUTSCENE_CAMS). */
at?: string;
}
/** Метки точек камеры (тайлы v1; мир — центр тайла, ноги на земле). */
export const CUTSCENE_CAMS: Record<string, { x: number; y: number }> = {
tower: QUEST_TOWER_TILE, // башня Звенца (сдача цветов)
clint_resonator: { x: 24, y: 16 }, // резонатор Клинта (clint_resonator)
tone_tree: { x: QUEST_TONE_TILES[1]![0], y: QUEST_TONE_TILES[1]![1] }, // дерево-механизм рощи
machine_mouth: { x: 13, y: 1 }, // устье печи (свет machine_mouth, проп old_machine)
};
/** Действие катсцены: точка (если задана at/cam) и аргумент (текст). */
export type CutsceneAction = (point: { x: number; z: number } | null, text: string | undefined) => void;
/** Реестр сюжетных катсцен: id → шаги в данных. */
export const CUTSCENES: Record<string, CutsceneStepData[]> = {
/** Сдача цветов: посадка → камера к башне → зелень → финал акта 1. */
hand_in: [
{ dur: 0.8, action: 'plant_flowers' },
{ dur: 1.2, cam: 'tower' },
{ dur: 1.2, action: 'raise_tower_green' },
{ dur: 1.0, action: 'toast', text: 'Пепел отступил у тропы. Машина дышит тише... но дышит.' },
],
/** Настройка резонатора (акт 2): камера к резонатору, звон,
* роща отвечает (камера к прозвеневшему дереву), тост-финал квеста. */
tune: [
{ dur: 1.0, cam: 'clint_resonator' },
{ dur: 1.0, action: 'fauna_startle', at: 'clint_resonator' },
{ dur: 1.2, cam: 'tone_tree' },
{ dur: 0.8, action: 'toast', text: 'Резонатор держит тон. Роща слышит.' },
],
/** Финал акта 2 — «правильный звон»: камера спускается к устью Машины,
* резонатор держит тон, Машина засыпает (тост — финальный шаг). */
machine_lull: [
{ dur: 1.2, cam: 'machine_mouth' },
{ dur: 1.0 }, // правильный звон держит
{ dur: 1.2 }, // дыхание обрывается — угольки гаснут
{ dur: 0.8, action: 'toast', text: 'Машина дышит тише. Впервые за годы — тише.' },
],
};
/** Мир-точка метки: центр тайла, ноги на земле (граница тайлы → воксели). */
export function cutscenePoint(label: string): [number, number, number] {
const t = CUTSCENE_CAMS[label];
if (!t) throw new Error(`cutscenes: неизвестная точка камеры «${label}»`);
return [t.x * UNIT + UNIT / 2, GROUND_Y, t.y * UNIT + UNIT / 2];
}
/** Связать каталог с действиями: шаги для движкового CutsceneRunner.
* Неизвестная метка/действие — throw (валидация при старте). */
export function resolveCutscenes(actions: Record<string, CutsceneAction>): Record<string, CutsceneStep[]> {
const out: Record<string, CutsceneStep[]> = {};
for (const [id, steps] of Object.entries(CUTSCENES)) {
if (!steps.length) throw new Error(`cutscenes: сцена «${id}» пуста`);
out[id] = steps.map((s) => {
if (s.cam) cutscenePoint(s.cam); // метка камеры валидна
const act = s.action ? actions[s.action] : undefined;
if (s.action && !act) throw new Error(`cutscenes: неизвестное действие «${s.action}» (сцена ${id})`);
const pointLabel = s.at ?? s.cam;
return {
dur: s.dur,
cam: s.cam ? cutscenePoint(s.cam) : undefined,
fn: act
? () => {
const p = pointLabel ? cutscenePoint(pointLabel) : null;
act(p ? { x: p[0], z: p[2] } : null, s.text);
}
: undefined,
};
});
}
return out;
}