Платформа NyxtaleЯзык Nyx
Синтаксис
Полная грамматика .nsx: заголовок, реплики, выборы, метки, ветвления, директивы.
.nsx — построчный формат. Одна строка — одна мысль; вложенность задаётся отступом. Пустые строки
игнорируются.
Строк всего четыре вида, и каждый узнаётся по первому символу:
| Сигил | Что это | Пример |
|---|---|---|
@ |
инструкция движку | @bg forest_night |
| (нет) | реплика — то, что читают | Марк: Кто здесь? |
* |
опция выбора | * choice "Войти" |
-> |
переход | -> inside |
Сигилов четыре, а не один, чтобы сцена листалась как рукопись, а не разбиралась как конфиг. Реплика — самая частая строка в истории — не носит никакого сигила вообще: проза выглядит прозой.
Заголовок
@nsx_version 1
@scene doorway
@render horror + creepypasta
@nsx_version <целое>— обязательна, идёт первой.@scene <идентификатор>— имя сцены: латиница, цифры, подчёркивание.@render <тема> [+ <примесь> …]— оформление. Без неё движок берётdefaultи предупреждает.
Реплики
narrator: Тишина стояла такая, что было слышно лампу.
Марк: Кто здесь?
{hero}: Я вернулся.
Говорящий — это отображаемый текст, а не идентификатор: Марк, Смотритель, любые буквы любого
алфавита. narrator — обычное имя говорящего, просто читатель рисует его без плашки.
{hero} — подстановка флага в имя: имя персонажа подставится на лету. То же работает и в тексте
реплики.
Эмоцию можно указать в скобках — читатель получает её отдельным полем:
mark (fear): Здесь кто-то был.
Идентификатор эмоции — только латиницей.
Марк (страх):сейчас не разбирается, хотя имя говорящего кириллицей работает. Это ограничение парсера, а не решение формата.
Комментарии
// в начале строки или после директивы — комментарий до конца строки; /* … */ — блочный, можно на
несколько строк. Внутри реплики оба знака литеральны: проза есть проза, и движок не имеет права
съесть половину предложения за то, что автор поставил в нём слэш.
// заметка себе: здесь потом добавить звук
@bg hall_night // фон выбран нарочно тусклым
/* этот кусок пока отложен —
вернуться к нему после главы 3 */
narrator: Он ушёл; она осталась стоять.
Точка с запятой комментарием не является — это была ранняя версия языка, и её сняли ровно потому,
что ; слишком часто встречается в живой фразе.
Выбор
* choice "Войти" @if has_key == 1
@set trespassed = 1
narrator: Дверь поддалась.
-> inside
* choice "Постучать" @timer 8000
-> knock
* choice "Заплатить сторожу" @cost 5 candles
-> bribe
Слово choice обязательно, текст — в кавычках. Идущие подряд * choice на одном отступе — один
узел выбора с несколькими опциями. Тело опции — её отступ.
Модификаторы в шапке опции:
| Модификатор | Что делает |
|---|---|
@if <выражение> |
опция видна, только если выражение истинно |
@cost <N> <валюта> |
опция платная |
@timer <миллисекунд> |
на выбор ставится таймер |
@at <x> <y> |
опция стоит ТОЧКОЙ НА КАРТИНКЕ, а не строкой в списке |
@icon <id> |
точка рисуется картинкой (метка на карте, рубашка карты) |
@gather |
выбор не отпускает, пока не взяты все опции |
@timerсчитает миллисекунды:@timer 8000— это восемь секунд.@timer 8— выбор, который истечёт за восемь миллисекунд, то есть мгновенно. Валидатор предупредит о слишком коротком окне.
Модификаторы ставятся на шапку опции; @timer и @gather относятся ко ВСЕМУ узлу выбора и пишутся
на первой опции.
Выбор точками на картинке
Опция с @at — это место на кадре, а не строка под текстом: карта с тремя тропами, три карты
рубашкой вверх, предмет в луче фонаря. Координаты — доли кадра от 0 до 1, не пиксели: одна и та
же история открывается и на телефоне, и в окне на компьютере.
@bg cart
narrator: Три тропы. Какая ведёт к малиннику?
* choice "Левая" @at 0.2 0.37 @icon cart_1
-> left
* choice "Средняя" @at 0.5 0.65 @icon cart_2
-> middle
* choice "Правая" @at 0.79 0.42 @icon cart_3
-> right
Текст опции остаётся обязательным даже когда виден только значок: его читает вслух скринридер и его
показывает граф в редакторе. Без фона (@bg) точки ставить некуда — они лягут поверх кадра, который
остался от прошлой сцены.
Картинка может зависеть от состояния. В @icon работает та же подстановка {флаг}, что и в
тексте реплики — тогда имя картинки берётся в момент показа опции:
@set back = rand(1, 2)
* choice "Карта" @at 0.5 0.5 @icon "card_{back}"
-> tower
Имя с подстановкой пишется в кавычках — без них { не часть имени. Проверить такую картинку
заранее нечем: валидатор скажет об этом отдельной строкой, а недостающий вариант находит прогон-бот,
который проходит историю целиком.
Собрать всё
@gather на первой опции превращает выбор в сбор: взятая опция уходит из списка, меню держит
читателя, пока не кончатся все, и только потом история идёт дальше. Три карты, которые надо
перевернуть, пять предметов в темноте, четыре двери — один узел, а не рукопашный цикл на флагах.
@bg taro_bg
* choice "Башня" @gather @at 0.2 0.5 @icon rub
narrator: Башня. Всё рушится.
* choice "Тройка мечей" @at 0.5 0.5 @icon rub
narrator: Тройка мечей. Больно.
* choice "Смерть" @at 0.8 0.5 @icon rub
narrator: Смерть. Перемена.
narrator: Ты перевернула все три.
Строка после опций — то, куда сбор выходит, когда брать больше нечего. Что уже взято, лежит во флагах, поэтому сохранение, продолжение и шаг назад несут это с собой сами.
Выборы вкладываются: тело опции может содержать свой * choice.
Переходы и метки
@label inside
narrator: Внутри было теплее, чем снаружи. Это и пугало.
-> hall
@label <имя> — адрес. -> <цель> или @jump <цель> — переход к нему. Метки не видны читателю.
Условия
Строкой — когда нужно просто увести:
@if fear > 5 -> panic
@unless has_key == 1 -> locked_out
Блоком — когда под условием несколько действий:
@if fear > 5
@heartbeat 110
narrator: Руки не слушались.
-> panic
@unless — то же самое с отрицанием.
Флаги
@set fear = 0
@set fear += 2
@set fear -= 1
@set name = 0
Операторы — =, +=, -=. Флаг нужно задать до того, как его прочитают: если найдётся путь, на
котором чтение случается раньше записи, валидатор скажет об этом кодом NSX_READ_BEFORE_WRITE.
Выражения
Приоритет от низкого к высокому:
or < and < not < сравнения (== != < <= > >=) < + - < * / % < унарный - < primaryand, or, not пишутся и как &&, ||, ! — одно и то же.
Primary — целое число, true / false, флаг, скобки или функция: rand, min, max, clamp,
abs.
@set roll = 0
@set roll = rand(1, 6)
@set roll = clamp(roll, 1, 5)
@if roll > 3 and fear < 10 -> steady
Арифметика только целочисленная. Деление отбрасывает дробь в сторону нуля, деление на ноль даёт
0. Это не упрощение: превью в браузере, движок на телефоне и повтор по сохранению обязаны сойтись
бит в бит, иначе превью ведёт автора в одну концовку, а игрока — в другую.
rand() детерминирован: он ходит от rng_seed, который лежит в состоянии и в сохранении. Одно и то
же прохождение всегда даёт один и тот же бросок.
Директивы
Всё, что начинается с @ и не перечислено выше, — директива-эффект:
@bg forest_night
@ambient wind_howl loop
@jumpscare shadow_figure @sound stinger_01
@sanity_show
Аргументы позиционные; именованные пишутся как @подфлаг значение. Ассеты — по идентификатору.
Все 94 директивы с типами аргументов и границами — в справочнике. Он собран из реестра движка, поэтому не может отстать от него.
Концовки
@end trespass
@end [имя] завершает сцену. Имя необязательно, но именованная концовка — это то, что читатель
«нашёл», и то, что движок считает при обходе.
Что проверяется
Компилятор ловит синтаксис, пока вы печатаете. Валидатор ловит целостность: битые ссылки, недостижимые узлы, тупики, ловушки-циклы, необъявленные флаги, чтение до записи, отсутствующие ассеты, лицензию без атрибуции.
Коды и починки — в справочнике диагностик.