README.md 14,4 КБ
Newer Older
Nikitos's avatar
ф  
Nikitos включено в состав коммита
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# Демо «Руси-тян: Бака!» — визуальная новелла на `$.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`](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`](../../src/highlevel/timeline.js). Полный
справочник — [`docs/highlevel/timeline.md`](../../docs/highlevel/timeline.md).

---

## 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`](../ui/vn-dialog.rml) + [`.rcss`](../ui/vn-dialog.rcss),
[`demos/ui/vn-hud.rml`](../ui/vn-hud.rml) + [`.rcss`](../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/`](voice/): `lines_for_minimax.txt` — 49
реплик по строке на каждую (порядок = порядок в новелле), `lines.json` — карта
«файл ↔ реплика», `README.md` — таблица. Нарация (6 строк) не озвучивается:
это голос за кадром, а не героиня.

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

1. **Заведите каталог** `demos/моя_новелла/index.js` с экспортом `install($)`
   и допишите строку `'./моя_новелла/index.js'` в `MODULES` в
   [`demos/main.js`](../main.js). Кнопка в меню появится сама, если добавить
   подпись в `TITLES` в [`demos/launcher.js`](../launcher.js).
2. **Положите картинки**: позы — в `demos/assets/art/vn/<герой>/`, локации — в
   `demos/assets/art/vn/bg/`. Позы удобно готовить скриптом
   [`tools/make_vn_sprites.py`](../../tools/make_vn_sprites.py) (вырезает фон,
   ровняет по общей рамке), локации — [`tools/make_vn_backgrounds.py`](../../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`](../assets/CREDITS.md)).