Привет, Хабр! Мы разрабатываем собственный плеер для визуальных новелл. Чтобы протестировать его возможности в «боевых» условиях, мы решили создать небольшую игру с нелинейным повествованием и RPG-элементами.
В качестве сценарного движка мы используем Ink — язык от студии Inkle, ставший стандартом в индустрии нарративных игр. А в качестве ролевой системы мы выбрали простую и элегантную Lasers & Feelings Джона Харпера, адаптировав её под формат одиночной визуальной новеллы.
Весь код проекта доступен в открытом репозитории, в саму новеллу можно поиграть в нашем плеере. А в этой статье мы расскажем, как меняли правила под формат новеллы и с какими интересными нюансами столкнулись при реализации игровой логики на языке, изначально предназначенном для текста.

Оригинальная Lasers & Feelings подкупает минимализмом: у персонажа есть всего одна характеристика — число от 2 до 5.
LASERS (Лазеры) — это логика, техника, холодный расчёт. Чтобы преуспеть, нужно выбросить на d6 значение меньше или равное вашему Числу.
FEELINGS (Чувства) — интуиция, дипломатия, страсть. Успех — это значение больше или равное Числу.
Для формата визуальной новеллы мы внесли несколько изменений в оригинальную систему:
Добавили управление партией. В оригинале каждый игрок управляет одним персонажем. В нашей новелле в самом начале игрок собирает команду из трёх специалистов. А в начале каждой сцены нужно выбрать из них «активного» персонажа, который будет проходить проверки, используя свои характеристики и специализацию.
Упростили криты. Мы отказались от механики «Laser-feelings» (когда выпадает число, точно равное характеристике, что даёт особый нарративный эффект). Это позволило не усложнять структуру ветвления сюжета, оставив бинарную систему: успех или провал.
Добавили встречные броски (Opposed Rolls). Мы добавили механику противостояния, которой нет в базовых правилах. Если персонаж соревнуется с NPC, броски делают оба. Результаты сравниваются, а в случае ничьей происходит автоматический переброс. Это добавило динамики в напряжённые сцены и позволило более тонко управлять сложностью.
Адаптировали механику подготовки. Оригинальные правила поощряют игроков, которые тратят время на подготовку. Чтобы сохранить дух этого правила в формате новеллы, перед каждой важной проверкой игрок делает выбор: действовать быстро и резко или вдумчиво и не торопясь. Правильный выбор, соответствующий контексту сцены (например, скорость в экстренной ситуации или вдумчивость при анализе данных), даёт бонусный кубик.
Система бонусов и штрафов (добавление или отнимание кубиков d6 с выбором лучшего результата) осталась классической: учитывается специализация персонажа и последствия предыдущих выборов.

Как мы побеждали особенности Ink
Ink идеально подходит для написания текста и ветвления выборов. Однако реализация математики кубов, инвентаря (списка персонажей) и проверок характеристик потребовала использования неочевидных приёмов.
Персонажи в мире без объектов
В Ink нет классов или структур. Мы не можем создать объект Character с полями name, stats, specialization.
Для хранения состава команды мы использовали тип данных LIST. Важно понимать, что LIST в Ink — это не массив, а скорее набор именованных флагов (регистров), которые могут быть включены или выключены.
// Доступные игроку персонажи.
LIST player_chars = Alex, Igor, Vadim, Ekaterina, Maria, Olga
// Текущий персонаж в сцене, выбранный из команды
VAR current_char = ()
Чтобы связать идентификатор из листа (например, Alex) с его характеристиками, мы применили «функции-справочники».
// Характеристика лазера-чувства (int 0-5) персонажа.
=== function char_laser(id) ===
{ id:
- Alex: ~ return 5
- Igor: ~ return 2
// ... остальные персонажи
}
При проверках характеристики мы просто передаём текущий идентификатор персонажа в нужную функцию и получаем значение.

Динамические меню выбора
Функции в Ink удобны для логики, но у них есть ограничение: в них нельзя выводить выборы (choices) для игрока.
Для создания меню набора людей в команду (где нужно показать только доступных персонажей и убирать уже выбранных) нам пришлось использовать узлы (knots) в связке с рекурсией.
Узел — это основная структурная единица, которая служит для организации повествования. По своей сути, узел — это именованный блок, содержащий текст, выборы и игровую логику. То есть, это самостоятельный фрагмент, к которому можно обратиться из любой другой части кода. С точки зрения нарративного дизайна, узлы используются для разделения истории на крупные части, такие как главы, сцены или локации.
Итого: мы создаём временный список, копируя туда доступных персонажей. Узел рекурсивно вызывает сам себя, на каждой итерации «вытаскивая» одного персонажа из списка, создавая для него опцию выбора и удаляя из временного списка. Это продолжается, пока список не опустеет или игрок не наберёт полную команду.
Код этого решения выглядит не очень-то красиво, но это рабочий способ реализовать динамическое меню в рамках парадигмы Ink. Полную реализацию можно посмотреть в файле characters.ink в нашем репозитории.

Броски кубиков и отсутствие циклов
В Ink нет привычных циклов for или while. Нам нужно было реализовать механику, где бросается несколько кубиков (база + бонусы - штрафы) и выбирается лучший результат.
Мы решили это с помощью рекурсивных функций. В Ink функции (=== function name ===) отличаются от узлов (knots) тем, что после выполнения возвращают поток повествования в ту же точку, не требуя ручного контроля переходов.
Логика выглядит так:
Функция рассчитывает итоговое количество кубиков.
Запускается рекурсивная функция броска. Она генерирует случайное число RANDOM(1, 6), сравнивает его с текущим лучшим результатом и вызывает сама себя, уменьшая счётчик оставшихся кубиков на один.
Когда кубики заканчиваются, функция возвращает лучший результат наверх, где он сравнивается с характеристикой персонажа.
Практика — лучшее доказательство
Мы столкнулись с некоторыми особенностями использования Ink для не-нарративной логики:
Отсутствие циклов (приходится использовать рекурсию).
Отсутствие ООП (используем списки и функции-справочники).
Слабая типизация (редактор не предупредит, если вы передадите число туда, где ожидается элемент листа).
И несмотря на это, нам удалось создать интересную игровую механику — а Ink доказал свою гибкость и пригодность для таких задач. При должном уровне абстракции он позволяет реализовать полноценную RPG-систему, бесшовно вплетённую в текст истории.
Если вам интересно сделать свою новеллу на подобной системе или другой, будем рады видеть вас на конкурсе интерактивных историй (новелл), который стартует 1 ноября.