Демо «Руси-тян: Бака!» — визуальная новелла на $.timeline
Одна героиня, пять локаций, три выбора, две концовки, тряска экрана, вспышки и семь поз — и всё это описано данными, без единого клика по сцене.
./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: он объявляет ассеты, вызывает
$.animatedTimelineScene2d(spec) и дорисовывает HUD. Всё остальное делает
движок.
$.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. Полный
справочник — docs/highlevel/timeline.md.
2. Локации, герои, позы
Локация — фон, музыка и необязательный оттенок («настроение»). Оттенок рисуется между фоном и героями, поэтому не съедает цвета персонажа.
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, // мс перекрёстного затухания
},
}
Герой — имя, позы и место на экране. Позы задаются таблицей, поэтому переключение — это один ключ в бите, а не отдельная анимация:
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:
{ 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, а игра
только пишет значения:
// реплика: разметка и стили наши, состояние — из $.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 + .rcss,
demos/ui/vn-hud.rml + .rcss.
Кнопок выбора в разметке ровно шесть — как maxChoices у $.dialog: элементы
создаются один раз, поэтому подписка на клик не теряется при смене реплики.
5. Озвучка героини
Голос подключается по простому правилу: файл на реплику, имя = id реплики
(<dir>/<id>.mp3). Нет файла — реплика идёт молча, поэтому озвучку можно
дописывать по одной и в любом порядке.
voice: { dir: 'demos/russi_vn/voice', ext: 'mp3', volume: 1 },
Список реплик для записи голоса отдаёт сам таймлайн:
echo '{"cmd":"eval","code":"JSON.stringify($.timeline.lines())"}' | \
./build/russiano2d --game demos --scene russi_vn --agent --headless
Готовые материалы лежат в voice/: lines_for_minimax.txt — 49
реплик по строке на каждую (порядок = порядок в новелле), lines.json — карта
«файл README.md — таблица. Нарация (6 строк) не озвучивается:
это голос за кадром, а не героиня.
6. Своя новелла за десять минут
-
Заведите каталог
demos/моя_новелла/index.jsс экспортомinstall($)и допишите строку'./моя_новелла/index.js'вMODULESвdemos/main.js. Кнопка в меню появится сама, если добавить подпись вTITLESвdemos/launcher.js. -
Положите картинки: позы — в
demos/assets/art/vn/<герой>/, локации — вdemos/assets/art/vn/bg/. Позы удобно готовить скриптомtools/make_vn_sprites.py(вырезает фон, ровняет по общей рамке), локации —tools/make_vn_backgrounds.py. -
Объявите стиль текста до новеллы:
$.font.define('vn_line', { size: 22, color: '#eef3ff' }); -
Опишите сцену вызовом
$.animatedTimelineScene2d({ … })— минимум этоid,cast,locationsиscript. - Проверьте без человека: юнит-тесты модуля и прогон через агента.
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') и родственные поля
попадают в снимок агента — через них новеллу видят тесты:
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).