# Сценки и катсцены — `$.story`

Сценка (катсцена, диалог, скриптовая вставка) — это **текстовый файл**: одна
команда на строку, читается и правится в любом редакторе. Порт из
`game/story/story_runner.gd` проекта audm-neko: разбор отдельно, исполнение
отдельно, а сценарий пишется так же, как читается.

```js
$.ready(() => {
    $.story.actor('Некотян', '#companion');   // имя из сценария → узел
    $.story.actor('Часовой', '#guard');

    $.story.play('story/prologue.scene');
});

// Игровой ввод: пока идёт катсцена, игрок не управляет собой.
$.update(() => {
    if ($.story.locked()) return;
    if ($.input.pressed('right')) $('#hero').move(1, 0);
});
```

---

## 1. Язык сценария

```text
@free                              не отбирать управление (иначе катсцена)

~ Где-то капает вода.               рассказчик, без имени
Некотян: Ты очнулся?               реплика, ждёт нажатия
Некотян (радость): Живой!          с эмоцией
bubble Часовой: Кто здесь?         облачко над головой, не ждёт

- Кто ты? -> who                    вариант ответа
- Молчать {silent = 1} -> end       вариант с флагом
:who                                метка
set trust += 1                      флаги: =, +=, -=
if trust >= 2 -> friend             условие: flag, !flag, ==, !=, >, <, >=, <=
goto finale
end

camera Часовой 0.8                  камера к актёру за 0.8 с
move Некотян CampFire               идти к маркеру и ждать (есть `run`)
move Некотян +200                   сместиться на 200 пикселей
face Часовой left|right|Игрок
anim Часовой taunt 1.5              клип и скорость
ai Часовой on|off                   мозг NPC
wait 1.5
image art/cg/prologue.png 0.5       кадр на весь экран
image off
fade out 0.5
fade in 0.5
sound sfx/step.wav
objective Дойди до выхода
```

Правила разбора: пустые строки и `#` пропускаются; варианты ответа идут подряд
после реплики и приклеиваются к одному блоку выборов; непонятная строка попадает
в `errors` и в журнал, но разбор не останавливает — остальная сценка играется.

## 2. Запуск

| Вызов | Смысл |
|---|---|
| `$.story.play(path, opts?)` | прочитать файл через `$.fs` и играть |
| `$.story.play({ text: '…' }, opts?)` | играть текст (тесты, ответ сети) |
| `$.story.play(script, opts?)` | играть уже разобранный сценарий |
| `$.story.stop()` | остановить: полосы и окно убираются, управление возвращается |
| `$.story.running()` | идёт ли сценка |
| `$.story.locked()` | отобрано ли управление |
| `$.story.parse(text)` | разобрать, не играя (проверка сценария) |
| `$.story.last_line` | последняя реплика `{ who, text, emotion }` |

`opts`: `label` — начать с метки, `free` — переопределить `@free`, `done` —
функция после конца.

## 3. Актёры

Сценарий называет актёров по-человечески, игра связывает имя с узлом:

```js
$.story.actor('Некотян', '#companion');   // селектор
$.story.actor('Часовой', guardNode);      // узел или обёртка
$.story.actor('player', '#hero');         // особые имена: player/игрок/я
```

Без объявления движок ищет узел с таким `id` (`#Часовой`), а для `player`
ищет `#player` и `#hero`. Цель `move` — имя актёра или узла-маркера; `camera`
и `face` понимают те же имена.

## 4. Флаги

Флаги **общие на игру**: выбор в прологе виден в финале, значения переживают
смену сцены.

```js
$.story.flags.trust;            // читать
$.story.set('trust', 5);        // поставить из игры
$.story.check('trust >= 2');    // та же логика, что в `if`
$.story.resetFlags();           // новая игра
$.story.flags._last_choice;     // номер последнего выбора
```

Неизвестный флаг — ноль, поэтому `if visited` ложно до первого `set visited = 1`,
а `if !visited` — истинно.

## 5. Катсцена и управление

Сценка **без** `@free` отбирает управление: `$.story.locked()` истинно, сверху и
снизу появляются полосы (9% высоты экрана), а игра должна спрашивать
`$.story.locked()` там, где читает ввод. Сценка **с** `@free` идёт поверх игры и
ничего не блокирует — годится для реплик и подсказок.

Событие `story:lock` шины `$.signal` сообщает о смене состояния, `story:line` —
о новой реплике, `story:objective` — о цели, `story:end` — о конце сценки.

## 6. Автопилот и внешний ответ

```js
$.story.auto(true);        // реплики листаются сами (демо, тесты, трейлер)
$.story.advance();         // листнуть одну реплику
$.story.choose(1);         // ответить за игрока: вариант №1
```

Эти вызовы идут тем же путём, что нажатия игрока, поэтому сценка не может
«застрять» из-за отсутствия ввода.

## 7. Ограничения

* **реплики листаются пробелом, Enter или кликом**, выбор — цифрами, стрелками
  или кликом по кнопке;
* **полосы и окно — простые прямоугольники**: анимация появления не сделана;
* **`move` не обходит препятствия**: это `moveTo` по прямой с ожиданием до
  `MOVE_TIMEOUT` (6 с), а не поиск пути. Застрял — сценка идёт дальше;
* **`anim` не ждёт конца клипа**: как в оригинале, команда запускает клип и
  продолжает (нужно ждать — ставьте `wait`);
* **актёры не «куклы»**: сценка двигает те же узлы, что и игра, поэтому
  физические тела во время сценки должны быть выключены или заморожены;
* **флаги не сохраняются сами**: для сейва кладите `$.story.flags` в `$.save`.

## 8. Проверка

```bash
# язык сценариев: разбор всех команд, условия, set (без движка)
build/_deps/quickjs-build/qjs tests/js/story_test.mjs

# в движке: реплики, выборы, флаги, катсцена и возврат управления
python3 tests/agent/highlevel_story_test.py
```
