Newer
Older
rpg / docs / engine / cutscene.md

Кат-сцены: CutsceneRunner

packages/engine/src/cutscene/CutsceneRunner.ts — view-агностичный раннер кат-сцен. Сцена — это список шагов; раннер исполняет их последовательно и сообщает active, чтобы игра поставила геймплей на паузу. Никакой привязки к Pixi/вью — только эффекты и камера, передаваемые в шагах.

Шаги

import { CutsceneRunner, type CutsceneStep } from '@rpg/engine';

const steps: CutsceneStep[] = [
    // Произвольный вызов (диалог, тост, логика сцены) + длительность:
    { kind: 'call', fn: () => scene.showToast('Пепел отступил.'), seconds: 1 },

    // Плавное перемещение камеры (в мировых юнитах) к точке:
    { kind: 'cameraMove', x: tower.x, y: tower.y, seconds: 1.2, camera: scene.camera },

    // Плавное перемещение вьюхи (актёра, объекта) в локальных координатах родителя:
    { kind: 'moveView', view: milaView, x: exitPx.x, y: exitPx.y, seconds: 2 },

    // Разовый эффект (звук, кольцо резонанса, вспышка частиц):
    { kind: 'burst', fn: () => scene.ringAt(tower), seconds: 1 },

    // Поднять флаг прохождения (мгновенно):
    { kind: 'setFlag', flag: 'act1_done', state: game.state },

    // Просто пауза:
    { kind: 'wait', seconds: 0.5 },

    // Ждать внешнее событие (закрылся диалог, актёр дошёл, таймер игры):
    { kind: 'until', predicate: () => !scene.dialogueActive, label: 'диалог' },
];

const runner = new CutsceneRunner();
runner.onComplete = () => console.log('сцена закончилась');
runner.play(steps);

Семантика

  • Последовательность. Каждый шаг при старте выполняет свой эффект; раннер ждёт seconds и переходит к следующему.
  • Параллельные шаги. Шаг с seconds: 0 (или undefined у burst/call) не блокирует: следующий стартует в том же тике. Так делают «проверку воздуха» — кольцо и звук в один кадр с продолжением сцены.
  • Ожидание события. until держит шаг, пока predicate() не вернёт true (проверка каждый тик; уже выполненный предикат шаг не блокирует). Опциональный timeout (сек) завершает шаг принудительно — без него шаг может длиться вечно, поэтому until без timeout ставьте только на события, которые гарантированно наступят (закрытие диалога, приход актёра). Типовая пара «диалог посреди сцены»: { kind: 'call', fn: () => scene.talkTo(npc) } + { kind: 'until', predicate: () => !scene.dialogueActive }.
  • Пауза геймплея — ответственность сцены: в update проверять runner.active, обновлять раннер и выходить (см. LocationScene.update). Исполнитель шага until (например, ходок актёра) должен тикаться сценой независимо от раннера.
  • Движение камеры и вьюх — по sineInOutmoveView можно свой ease); точка назначения достигается точно. Интерполяция — чистая applyEase(from, to, k, ease) из cutscene/tween. Координаты moveViewлокальные px родителя вьюхи: для актёра в worldRoot сцена сама переводит мировые координаты (worldToScreen). Движок не знает ни Pixi, ни актёров — шагу подходит любой объект с position: {x, y} (Movable).

Скип

runner.skip() дожимает сцену до конца немедленно: шаги-движения ставят вьюхи/камеру в конечные точки, until форсируются (скип конечен даже на шаге-ожидании), все call/burst выполняются. Агентный мост (scene:skipCutscene) пользуется именно им. Следствие: until в катсцене должен быть достижим при обычном проигрывании — скип не обязан ждать реального события, он его имитирует.

Наблюдение за шагами

Конструктор принимает наблюдателя — удобно для отладки:

const runner = new CutsceneRunner((step) => console.log('шаг:', step.kind));

Тесты

cutscene/__tests__/CutsceneRunner.test.ts — порядок шагов, паузы, интерполяция камеры (точное попадание в точку в конце), флаги, порядок наблюдателя. Конструируется без DOM: new Camera(viewWidth, viewHeight).