# Демо «Руси-тян: Бака!» — визуальная новелла на `$.timeline`

Одна героиня, пять локаций, три выбора, две концовки, тряска экрана, вспышки
и семь поз — и всё это описано **данными**, без единого клика по сцене.

```bash
./build/russiano2d --game demos --scene russi_vn   # сразу новелла
./build/russiano2d --game demos                    # кнопка «Руси-тян (ВН)»
```

| Управление | Что делает |
|---|---|
| `Space` / `Enter` / клик | дальше (досрочно допечатать реплику) |
| `↑` / `↓` | выбрать вариант ответа |
| `Ctrl` (держать) | быстрый пропуск: реплики летят |
| `A` | авто-режим чтения |
| `R` | начать новеллу заново |
| `Esc` | в меню демо |

Прогресс (флаг `trust`, открытые концовки) живёт в `$.store` и переживает
перезапуск: у новеллы свой файл `demos/vn_save.json`, чтобы не подмешиваться
в сохранение основной игры.

---

## 1. Из чего состоит новелла

Демо — один файл [`index.js`](https://github.com/Nikide/russiano2d/blob/main/demos/russi_vn/index.js): он объявляет ассеты, вызывает
`$.animatedTimelineScene2d(spec)` и дорисовывает HUD. Всё остальное делает
движок.

```js
$.animatedTimelineScene2d({
    id: 'russi_vn',              // имя таймлайна и (по умолчанию) сцены
    scene: 'russi_vn',           // какую сцену $.scene зарегистрировать
    exitScene: 'launcher',       // куда уйти по Esc с карточки концовки
    speed: 55,                   // скорость печатной машинки, символов в секунду
    style: 'vn_line',            // стиль текста из $.font
    location: 'room',            // с какой локации начинать
    locations: { room: { bg: '…', music: '…', title: 'Комната · 03:07' } },
    cast: { russi: { name: 'Руси-тян', poses: { neutral: '…', angry: '…' } } },
    dialogTheme: { panel: '#0b1220e6', speaker: '#ffb3d9' },
    enter() { /* HUD */ }, exit() { /* уборка */ }, update(dt) { /* хоткеи */ },
    script: [ /* биты — см. §3 */ ],
});
```

`$.timeline` — это и есть новая функция высокого уровня: модуль
[`src/highlevel/timeline.js`](https://github.com/Nikide/russiano2d/blob/main/src/highlevel/timeline.js). Полный
справочник — [`docs/highlevel/timeline.md`](highlevel/timeline).

---

## 2. Локации, герои, позы

**Локация** — фон, музыка и необязательный оттенок («настроение»). Оттенок
рисуется между фоном и героями, поэтому не съедает цвета персонажа.

```js
locations: {
    roof: {
        title: 'Крыша · закат',                 // всплывает табличкой при входе
        bg: 'demos/assets/art/vn/bg/rooftop_sunset.png',
        music: 'demos/assets/audio/music/menu.ogg',
        volume: 0.5,
        mood: { color: '#ff8a4c', alpha: 0.14 }, // необязательно
        fade: 500,                               // мс перекрёстного затухания
    },
}
```

**Герой** — имя, позы и место на экране. Позы задаются таблицей, поэтому
переключение — это один ключ в бите, а не отдельная анимация:

```js
cast: {
    russi: {
        name: 'Руси-тян',
        poses: { neutral: '…neutral.png', angry: '…angry.png', blush: '…blush.png' },
        pose: 'neutral',   // с какой начать
        x: 0.62,           // центр по горизонтали, доля ширины окна
        bottom: 1.02,      // низ спрайта, доля высоты (1.02 — чуть ниже кадра)
        height: 0.94,      // высота героя, доля высоты окна
        idle: true,        // дыхание
        mirror: false,     // отразить по горизонтали
    },
}
```

Позы вырезаны по **общей рамке** (`tools/make_vn_sprites.py`), поэтому смена
позы не двигает героиню ни на пиксель — это и делает возможной «анимацию» из
статичных картинок.

---

## 3. Биты: язык сценария

Скрипт — массив объектов. Бит может совмещать несколько действий: сначала
выполняются мгновенные (поза, звук, тряска), потом то, что занимает время
(реплика, пауза).

| Бит | Что делает | Ждёт? |
|---|---|---|
| `{ say: '…' }` | реплика героини (её имя подставляется само) | да, нажатия игрока |
| `{ narrate: '…' }` | текст без имени говорящего | да |
| `{ say: '…', who: 'russi', pose: 'angry' }` | реплика сразу с позой | да |
| `{ choose: [{ text, goto, add, set, do, if }] }` | варианты ответа | да, выбора |
| `{ location: 'roof' }` | смена локации с затуханием | нет |
| `{ pose: 'blush' }` | сменить позу | нет |
| `{ show: 'russi', from: 'left' }` | выход героя (`left`/`right`/`bottom`/`fade`) | да |
| `{ hide: 'russi', to: 'right' }` | уход героя | да |
| `{ anim: 'bounce' }` | акцент: `pop`, `bounce`, `nod`, `lean`, `away`, `sigh`, `step`, `tremble`, `shiver` | да |
| `{ shake: 14, ms: 450 }` | тряска экрана (камера) | нет |
| `{ flash: { color: '#ff5a5a', alpha: 0.45, ms: 300 } }` | вспышка поверх интерфейса | нет |
| `{ fade: '#000000', ms: 400 }` / `{ fade: null }` | затемнение и проявление | да |
| `{ zoom: 1.2, ms: 600 }` | наезд камеры | нет |
| `{ sfx: '…' }` / `{ music: '…' }` | звук и музыка | нет |
| `{ wait: 300 }` | пауза | да |
| `{ set: { trust: 1 } }` / `{ add: { trust: 1 } }` | записать флаг в `$.store` | нет |
| `{ if: 'trust >= 2', then: [...], else: [...] }` | ветка | по содержимому |
| `{ goto: 'love' }` / `{ label: 'love' }` | переход и метка | нет |
| `{ do: ($, tl) => { … } }` | свой код | нет |
| `{ emit: 'имя', data: {} }` | своё событие | нет |
| `{ ending: { id, title, text, mood } }` | концовка и финальная карточка | конец прогона |

Условие понимает четыре формы: функцию, `true`/`false`, флаг (`'has_pass'`,
`'!has_pass'`) и сравнение (`'trust >= 2'`, `'route == "love"'`).

Реплика-строка — сахар: `'Бака!'` то же самое, что `{ say: 'Бака!' }`.

Пример из демо — ссора из-за Python:

```js
{ label: 'python' },
{ anim: 'tremble' },
{ shake: 14, ms: 450 },
{ sfx: sfx.glitch },
{ flash: { color: '#ff5a5a', alpha: 0.45, ms: 300 } },
{ pose: 'angry' },
{ say: 'ПИТОН?! Ты… ты… БАКА!!!' },
{ shake: 18, ms: 500 },
{ say: 'Python — это язык, на котором аналитики считают таблички! А тут ДВИЖОК!' },
```

---

## 4. UI — на RmlUi

Новелла ничего не рисует узлами `<ui.*>`: панель реплики, кнопки выбора и весь
HUD — это документы RmlUi, обычные HTML-подобная разметка и CSS. Перенос строк,
шрифт, рамку, скругления и подсветку кнопки под курсором делает RmlUi, а игра
только пишет значения:

```js
// реплика: разметка и стили наши, состояние — из $.dialog
dialogView: { kind: 'rml', doc: 'demos/ui/vn-dialog.rml' },
locationCard: false,                      // табличку локации рисует vn-hud.rml

// HUD
const hud = $.ui.doc('demos/ui/vn-hud.rml').show();
hud.style('vn-meter-fill', 'width', '60%');   // руси-метр
hud.text('vn-place', 'Крыша · закат');        // табличка локации
hud.cls('vn-auto', 'off', false);             // индикатор авто-режима
```

Файлы: [`demos/ui/vn-dialog.rml`](https://github.com/Nikide/russiano2d/blob/main/demos/ui/vn-dialog.rml) + [`.rcss`](https://github.com/Nikide/russiano2d/blob/main/demos/ui/vn-dialog.rcss),
[`demos/ui/vn-hud.rml`](https://github.com/Nikide/russiano2d/blob/main/demos/ui/vn-hud.rml) + [`.rcss`](https://github.com/Nikide/russiano2d/blob/main/demos/ui/vn-hud.rcss).
Кнопок выбора в разметке ровно шесть — как `maxChoices` у `$.dialog`: элементы
создаются один раз, поэтому подписка на клик не теряется при смене реплики.

## 5. Озвучка героини

Голос подключается по простому правилу: **файл на реплику, имя = id реплики**
(`<dir>/<id>.mp3`). Нет файла — реплика идёт молча, поэтому озвучку можно
дописывать по одной и в любом порядке.

```js
voice: { dir: 'demos/russi_vn/voice', ext: 'mp3', volume: 1 },
```

Список реплик для записи голоса отдаёт сам таймлайн:

```bash
echo '{"cmd":"eval","code":"JSON.stringify($.timeline.lines())"}' | \
    ./build/russiano2d --game demos --scene russi_vn --agent --headless
```

Готовые материалы лежат в [`voice/`](https://github.com/Nikide/russiano2d/tree/main/demos/russi_vn/voice): `lines_for_minimax.txt` — 49
реплик по строке на каждую (порядок = порядок в новелле), `lines.json` — карта
«файл ↔ реплика», `README.md` — таблица. Нарация (6 строк) не озвучивается:
это голос за кадром, а не героиня.

## 6. Своя новелла за десять минут

1. **Заведите каталог** `demos/моя_новелла/index.js` с экспортом `install($)`
   и допишите строку `'./моя_новелла/index.js'` в `MODULES` в
   [`demos/main.js`](https://github.com/Nikide/russiano2d/blob/main/demos/main.js). Кнопка в меню появится сама, если добавить
   подпись в `TITLES` в [`demos/launcher.js`](https://github.com/Nikide/russiano2d/blob/main/demos/launcher.js).
2. **Положите картинки**: позы — в `demos/assets/art/vn/<герой>/`, локации — в
   `demos/assets/art/vn/bg/`. Позы удобно готовить скриптом
   [`tools/make_vn_sprites.py`](https://github.com/Nikide/russiano2d/blob/main/tools/make_vn_sprites.py) (вырезает фон,
   ровняет по общей рамке), локации — [`tools/make_vn_backgrounds.py`](https://github.com/Nikide/russiano2d/blob/main/tools/make_vn_backgrounds.py).
3. **Объявите стиль текста** до новеллы:
   `$.font.define('vn_line', { size: 22, color: '#eef3ff' });`
4. **Опишите сцену** вызовом `$.animatedTimelineScene2d({ … })` — минимум это
   `id`, `cast`, `locations` и `script`.
5. **Проверьте без человека**: юнит-тесты модуля и прогон через агента.

```bash
build/_deps/quickjs-build/qjs tests/js/timeline_test.mjs          # логика модуля
python3 tools/vn_playthrough.py --route love --out build/vn_shots # вся новелла до концовки
```

Скрипт `tools/vn_playthrough.py` проходит новеллу в агентском режиме, сам
нажимает «дальше», выбирает варианты по маршруту и складывает скриншоты
ключевых моментов — так демо проверяется целиком, а не «должно работать».

---

## 7. Что под капотом

| Слой | Кто отвечает |
|---|---|
| панель реплики, выборы, руси-метр, подсказки, табличка локации | **RmlUi** — `demos/ui/vn-dialog.rml` и `vn-hud.rml` (+ `.rcss`) |
| текст, печатная машинка, выборы, `Enter`/`↑`/`↓` | `$.dialog` (таймлайн только листает реплики) |
| фон и локации | два спрайта в мире + перекрёстное затухание |
| герой, позы, дыхание, вход/выход, акценты | «риг» персонажа + `$.tween` |
| тряска экрана | `$.camera.shake` (камера приколота к центру окна, поэтому мир = экран) |
| вспышки и затемнения | свои `ui.panel` поверх интерфейса |
| музыка и звук | `$.sound` |
| флаги и концовки | `$.store` |

Отладка: `$.timeline.state()` отдаёт снимок прогона (локация, поза, ожидание,
число битов, флаги), а `$.agent.expose('vn_trust')` и родственные поля
попадают в снимок агента — через них новеллу видят тесты:

```bash
echo '{"cmd":"eval","code":"JSON.stringify($.timeline.state())"}' | \
    ./build/russiano2d --game demos --scene russi_vn --agent --headless
```

---

## 8. Ассеты

* позы Руси-тян — присланные картинки, обработанные
  `tools/make_vn_sprites.py` (фон вырезан в альфу, общая рамка, ×0.5);
* класс — CC0-фото «Classroom 002» (OpenGameArt);
* небо для заката — CC0-кадр из «40 game backgrounds, painted style» (OpenGameArt);
* комната, ночная улица, школьный двор и крыша — процедурная графика
  `tools/make_vn_backgrounds.py` (та же CC0, что и движок);
* музыка — два трека автора проекта (`vn_tension.mp3`, `vn_afternoon.mp3`,
  исходные WAV 28 и 29 МБ сжаты в 2,9 МБ), плюс CC0-записи Juhani Junkala для
  звуков (см. [`demos/assets/CREDITS.md`](demos/assets/CREDITS)).
