Архітектура квесту

Скриптів немає. Логіка є: як влаштовані ноди та змінні в Romergo

Коли говорять про логіку в ігровому редакторі, зазвичай швидко доходять до скриптів. Десь автор пише JavaScript, десь Python, а десь вивчає власну мову движка. Це дає майже необмежену свободу, але разом з нею призводить до синтаксичних помилок, завислих циклів, несумісних плагінів і коду, який через півроку страшно відкривати навіть його автору.

У Romergo я пішов іншим шляхом. Довільних скриптів всередині квесту немає. Не можна вставити функцію, зробити мережевий запит або отримати прямий доступ до програвача. Замість цього історія складається з обмеженого, але досить виразного набору: логічних вузлів, змінних, умов і ефектів.

Це не означає, що в Romergo можна робити лише прості розгалуження. Можна запам'ятовувати рішення гравця, рахувати очки, відкривати та приховувати варіанти, вибирати різні маршрути в наступних главах, створювати випадкові події та інтерактивні інтерфейси. Просто замість команди «виконай цей код» автор описує «при такому стані повинно статися ось це».

Граф уже є програмою

Найбільш базова логіка Romergo знаходиться прямо на карті глави. Сцени з'єднуються переходами, а спеціальні вузли вирішують, куди піде проходження далі.

Якщо сильно спростити, маленький квест виглядає так:

Старт
  ↓
Розмова з охоронцем
  ↓
Є перепустка?
  ├─ так  → Закритий архів
  └─ ні → Обхідний шлях

Це вже виконавча логіка. Player входить у стартовий вузол, показує сцену, читає стан історії і обирає відповідний перехід. Автор бачить той самий маршрут як зрозумілу карту, а не як набір if, goto та службових ідентифікаторів.

![Flow Romergo: Final Accusation проходить через Switch і три If / Else до різних кінцівок, знизу відкрита панель Logic](../assets/logic-without-scripts-flow-nodes.webp)

*Фрагмент реального Flow у Builder: сцена Final Accusation передає рішення в Switch, потім кілька If / Else перевіряють стан і розводять історію до різних кінцівок. Відкрита панель Logic унизу показує весь доступний набір: Dead End, Checkpoint, If / Else, Switch, Random і Teleport.*

У панелі логіки зараз є шість основних вузлів:

Старт і Фініш обрамляють главу, звичайні сцени показують зміст, а логічні вузли перетворюють їх на маршрут. Уже на цьому рівні можна зібрати лінійну історію, розгалуження, кілька кінцівок, випадкову подію та безпечну точку повернення — без жодного рядка скрипта.

Змінна — це пам’ять історії

Одного графа недостатньо, якщо історія повинна пам’ятати, що сталося раніше. Для цього в квесті є змінні трьох простих типів:

У кожної змінної є ключ, зрозуміла автору підпис і початкове значення. Числову змінну можна додатково описати мінімумом і максимумом, а службову змінну — сховати з панелі стану гравця або показувати тільки при виконанні умови.

Головне тут те, що змінні належать проходженню всього квесту, а не одній сцені. Якщо в першій главі гравець отримав пропуск, у четвертій главі можна перевірити той самий прапорець. Під час переходу через Фініш у наступну главу стан не обнуляється. Він зберігається разом із обраними варіантами, застосованими ефектами, контрольним пунктом і поточним кроком випадковості.

Виходить простий авторський цикл:

вибір гравця записує значення
              ↓
змінна зберігає стан
              ↓
умова читає його пізніше
              ↓
граф, сцена або інтерфейс реагує

Наприклад, у першій сцені вибір «Показати знайдену фотографію» може встановити portrait_revealed = true. Пізніше вузол **Якщо / Інакше** перевірить цей прапор і відкриє нову сцену розмови. Ще пізніше інтерфейс терміналу покаже додатковий запис лише тим гравцям, які розкрили фотографію.

Одна змінна пов'язує три різні частини квесту. Для цього не потрібно вручну передавати значення між сценами або главами: вони читають один загальний стан проходження.

![Сторінка змінної route показує ключ, назву, тип і видимість стану](../assets/logic-without-scripts-variables.webp)

*У змінної route на одній картці задано стабільний ключ, зрозумілу назву, тип значення та видимість стану. Зв'язки з виборами та гілками знаходяться нижче на тій же сторінці.*

Вибори пишуть, гілки читають

У звичайній сцені найзрозуміліший спосіб змінити стан — додати ефект до варіанту вибору. У візуальному редакторі це виглядає як присвоєння:

«Довіритися Марі» → ally = "mary"
«Піти самому»     → ally = "none"

Після цього Switch може направити гравця в потрібну сцену за значенням ally. Сторінка змінних окремо показує, які вибори записують змінну і які гілки її читають. Це невелика деталь, але у великому квесті вона замінює стомлюючий пошук по десятках сцен.

![Налаштування Switch з гілками Laboratory та Habitat, які порівнюють змінну обраного маршруту](../assets/logic-without-scripts-switch-rules.webp)

*Гілки Switch перевіряються зверху вниз. У цьому прикладі маршрут Laboratory спрацьовує при route = lab, а Habitat — при route = habitat.*

Умови підтримують звичайні порівняння:

У простому випадку автор обирає змінну, оператор і значення прямо в налаштуваннях If / Else або Switch. Для більш складної логіки загальний runtime розуміє групи **усі умови**, **будь-яка умова** та заперечення.

Таке правило можна описати структурно:

{
  "op": "all",
  "conditions": [
    { "op": "gte", "key": "evidence", "value": 3 },
    { "op": "eq", "key": "alarm_active", "value": false }
  ]
}

Це схоже на маленький скрипт, але принципово ним не є. Тут не можна викликати функцію або змінювати щось випадкове. Romergo заздалегідь знає всі допустимі операції, перевіряє структуру і однаково виконує її у попередньому перегляді Builder і в опублікованому Player.

Ефекти: маленькі команди без мови програмування

Умова читає стан, а ефект його змінює. Runtime Romergo підтримує кілька атомарних операцій:

В звичайному інспекторі вибору основний сценарій навмисне простий: вибрати змінну і записати нове значення через set. У просунутій логіці інтерфейсних сцен умови та ефекти можна задавати як перевірювані структури. Наприклад, перехід між станами терміналу може одночасно запам’ятати рішення, додати докази та увімкнути тривогу:

![Редактор сцени Romergo: три варіанти вибору записують lab, habitat і server у змінну route](../assets/logic-without-scripts-choice-effects.webp)

*Ефект прикріплений безпосередньо до варіанту відповіді: три варіанти записують у route значення lab, habitat та server. Поле змінної та нове значення залишаються роздільними навіть у вузькому інспекторі.*

[
  { "op": "set", "key": "terminal_decision", "value": "cut-power" },
  { "op": "inc", "key": "evidence", "amount": 1 },
  { "op": "toggle", "key": "alarm_active" }
]

У такого підходу є важливе обмеження: це не калькулятор довільних формул. Неможливо написати evidence * trust / 2, запустити цикл або створити власну функцію. Складна поведінка збирається з кількох явних кроків, а розширення логіки відбувається через нові перевіряємі операції runtime, а не через виконання невідомого коду.

Для автора це трохи менш безмежно, зате історія залишається передбачуваною. Одні й ті ж ефекти можна перевірити до публікації, зберегти, синхронізувати між гравцями і відтворити після завантаження прогресу.

Чому всередині логічного вузла поки немає ще одного Flow

При всій передбачуваності у нинішнього підходу є чесний недолік: людині без досвіду розробки форма «змінна — оператор — значення» вже може здаватися програмуванням. А коли поряд з’являються групи умов і кілька ефектів, декларативна структура залишається безпечною для Player, але не обов’язково стає простою для автора.

Здається природним одного разу прийти до візуальних нод і всередині самої логіки. Замість списку умов можна було б з'єднувати блоки **І**, **АБО**, **НЕ**, порівняння змінної та зміну значення. На папері це виглядає дружелюбніше за текст чи JSON.

Але потім я уявляю реальний сценарій: автор відкриває логічну ноду на карті глави, а всередині неї виявляє ще один Flow з власними нодами і зв'язками. Виходить граф всередині графу. Потрібно розуміти, де ти зараз знаходишся, як повернутися на рівень історії і в якому з двох Flow шукати помилку. Для новачка така вкладеність може виявитися страшнішою за нинішню форму.

Тому наразі використовується компроміс. Маршрут історії залишається візуальним, а умови та ефекти всередині нод показуються компактними явними полями. Це працює, але я не вважаю інтерфейс остаточно вирішеним. Можливо, пізніше з’явиться більш зрозуме поєднання готових шаблонів, покрокового налаштування та візуальних блоків — таке, яке справді зменшить складність, а не просто перенесе її в ще один редактор.

Логіка живе не лише на карті

Інтерфейсні сцени використовують той же стан. Віджет, повідомлення, варіант відповіді, завдання, сповіщення або медіапотік можуть з’являтися лише при збігу showWhen. Значення метрики можна пов’язати зі змінною, а перехід між станами віджета — запустити після затримки, за умовою або після дії гравця.

Наприклад, екран бортового комп’ютера може поводитися так:

1. Спочатку видно лише заблокований термінал.

2. Після вибору гравця ефект встановлює terminal_unlocked = true.

3. Умова відкриває схему корабля та нові кнопки.

4. Натискання кнопки змінює стан віджета та збільшує evidence.

5. Коли evidence >= 3, з’являється приховане повідомлення і стає доступною нова гілка квесту.

Карта глави, звичайні вибори та інтерактивний інтерфейс не створюють три окремі системи. Вони читають і змінюють один об’єкт стану. Тому знайдена в діалозі підказка може відкрити елемент інтерфейсу, а дія всередині інтерфейсу — змінити наступну сцену.

Саме тут декларативна логіка починає відчуватися як справжнє програмування. Тільки замість файлів, функцій і подієвої шини автор працює з зрозумілими сутностями історії: фактами, виборами, умовами та переходами.

Навіщо я навмисне не додаю довільний JavaScript

Можливість вставити скрипт здається найкоротшим шляхом до будь-якої нової механіки. Але щойно опублікований квест отримує право виконувати довільний код, змінюється вся модель продукту.

Потрібно вирішити, які браузерні API доступні цьому коду, як обмежити мережу та сховище, що робити з нескінченними циклами, як переносити квест офлайн, як синхронізувати його в спільній грі та як гарантувати, що старий проєкт продовжить працювати після оновлення Player.

Декларативне правило значно нудніше за довільну функцію — і саме тому надійніше. Його можна перевірити до запуску. Можна зрозуміти, які змінні воно читає та записує. Можна безпечно зберегти прогрес посеред ланцюжка. Можна однаково виконати квест у браузері, автономній збірці та вбудованому Player.

Обмежений словник тут не заважає логіці, а задає її межі. Якщо з’являється дійсно корисна операція, її краще додати у загальний runtime і дати всім авторам однакову перевірену поведінку, ніж змушувати кожен проєкт вигадувати власний двигун.

На десерт — свій HTML і CSS

При цьому Romergo все ж залишає місце для справжнього коду там, де він найбезпечніший: у оформленні звичайної сцени.

У проєкті можна створити власну тему зовнішнього вигляду на HTML та CSS і використовувати її в різних главах. HTML задає розташування підготовлених областей — вікна репліки, портрета, імені персонажа, тексту, питання та кнопок вибору. CSS дозволяє помітно змінювати ці області: рамки, фон, форму карток, відступи, типографіку та реакцію на розмір екрана.

Готові теми **Standard**, **Minimalism**, **Brutalism**, **Romance** та **Neon** показують підтверджений діапазон без ручної верстки. При створенні користувацької теми Romergo копіює обраний шаблон: після цього його HTML та CSS можна редагувати та одразу перевіряти у справжньому попередньому перегляді Player.

![Реальний редактор оформлення Romergo: CSS теми Brutalism та мобільний попередній перегляд діалогу](../assets/logic-without-scripts-custom-html-css.webp)

*Тут немає концепт-арту: зліва показано справжній CSS вбудованої теми Brutalism, справа — результат того ж коду в телефонному перегляді. Перемикач Desktop / Mobile дозволяє одразу перевірити адаптивні правила, а користувацька тема починає роботу з копії обраного шаблону.*

Але межа залишається колишньою. У користувацькому HTML немає <script>, обробників подій, форм, iframe та зовнішніх ресурсів. CSS не може завантажити зовнішній url() або @import. Обов’язкові області сцени повинні залишатися на місці; якщо шаблон їх втратив, Player повертається до безпечного стандартного оформлення.

Тобто HTML і CSS відповідають за те, **як історія виглядає**, а вузли, змінні, умови та ефекти — за те, **як вона працює**.

Мені подобається саме таке розділення. Автор може зробити квест візуально неповторним на тлі інших, але його проходження все одно залишається частиною загального перевірюваного runtime. Зовнішній шар можна радикально перекрашувати та перебудовувати, не перетворюючи кожну тему на окрему програму.

Код без коду — не відсутність логіки

У Romergo немає мети замінити мову програмування картинками. Граф погано підходить для складної математики, а набір атомарних ефектів не стане універсальним двигуном автоматизації.

Зате для інтерактивної історії частіше потрібні інші речі: запам’ятати факт, змінити лічильник, відкрити варіант, обрати маршрут, показати стан інтерфейсу, зберегти точку повернення та донести всі ці рішення до наступної глави.

Для цього вузли та змінні виявляються не спрощеною версією скриптів, а більш прямою мовою самої історії. Автор описує не команди комп’ютеру, а причинно-наслідкові зв’язки світу: гравець зробив вибір, стан змінився, історія відреагувала.

А якщо хочеться трохи справжнього коду, HTML і CSS все одно чекають на десерт.

Назад до dev-блогу