Документация Nyxtale

Платформа NyxtaleЯзык Nyx

Синтаксис

Полная грамматика .nsx: заголовок, реплики, выборы, метки, ветвления, директивы.

.nsx — построчный формат. Одна строка — одна мысль; вложенность задаётся отступом. Пустые строки игнорируются.

Строк всего четыре вида, и каждый узнаётся по первому символу:

Сигил Что это Пример
@ инструкция движку @bg forest_night
(нет) реплика — то, что читают Марк: Кто здесь?
* опция выбора * choice "Войти"
-> переход -> inside

Сигилов четыре, а не один, чтобы сцена листалась как рукопись, а не разбиралась как конфиг. Реплика — самая частая строка в истории — не носит никакого сигила вообще: проза выглядит прозой.

Заголовок

@nsx_version 1
@scene doorway
@render horror + creepypasta
движок принял

Реплики

narrator: Тишина стояла такая, что было слышно лампу.
Марк: Кто здесь?
{hero}: Я вернулся.
движок принял line×3

Говорящий — это отображаемый текст, а не идентификатор: Марк, Смотритель, любые буквы любого алфавита. narrator — обычное имя говорящего, просто читатель рисует его без плашки.

{hero} — подстановка флага в имя: имя персонажа подставится на лету. То же работает и в тексте реплики.

Эмоцию можно указать в скобках — читатель получает её отдельным полем:

mark (fear): Здесь кто-то был.
движок принял line

Идентификатор эмоции — только латиницей. Марк (страх): сейчас не разбирается, хотя имя говорящего кириллицей работает. Это ограничение парсера, а не решение формата.

Комментарии

// в начале строки или после директивы — комментарий до конца строки; /* … */ — блочный, можно на несколько строк. Внутри реплики оба знака литеральны: проза есть проза, и движок не имеет права съесть половину предложения за то, что автор поставил в нём слэш.

// заметка себе: здесь потом добавить звук
@bg hall_night // фон выбран нарочно тусклым
/* этот кусок пока отложен —
   вернуться к нему после главы 3 */
narrator: Он ушёл; она осталась стоять.
движок принял lineeffect

Точка с запятой комментарием не является — это была ранняя версия языка, и её сняли ровно потому, что ; слишком часто встречается в живой фразе.

Выбор

* choice "Войти" @if has_key == 1
    @set trespassed = 1
    narrator: Дверь поддалась.
    -> inside
* choice "Постучать" @timer 8000
    -> knock
* choice "Заплатить сторожу" @cost 5 candles
    -> bribe
движок принял linesetchoicejump×3

Слово 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
движок принял choicelineeffectjump×3

Текст опции остаётся обязательным даже когда виден только значок: его читает вслух скринридер и его показывает граф в редакторе. Без фона (@bg) точки ставить некуда — они лягут поверх кадра, который остался от прошлой сцены.

Картинка может зависеть от состояния. В @icon работает та же подстановка {флаг}, что и в тексте реплики — тогда имя картинки берётся в момент показа опции:

@set back = rand(1, 2)
* choice "Карта" @at 0.5 0.5 @icon "card_{back}"
    -> tower
движок принял choicesetjump

Имя с подстановкой пишется в кавычках — без них { не часть имени. Проверить такую картинку заранее нечем: валидатор скажет об этом отдельной строкой, а недостающий вариант находит прогон-бот, который проходит историю целиком.

Собрать всё

@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: Ты перевернула все три.
движок принял line×4choiceeffect

Строка после опций — то, куда сбор выходит, когда брать больше нечего. Что уже взято, лежит во флагах, поэтому сохранение, продолжение и шаг назад несут это с собой сами.

Выборы вкладываются: тело опции может содержать свой * choice.

Переходы и метки

@label inside
narrator: Внутри было теплее, чем снаружи. Это и пугало.
-> hall
движок принял linejump

@label <имя> — адрес. -> <цель> или @jump <цель> — переход к нему. Метки не видны читателю.

Условия

Строкой — когда нужно просто увести:

@if fear > 5 -> panic
@unless has_key == 1 -> locked_out
движок принял branch×2jump×2

Блоком — когда под условием несколько действий:

@if fear > 5
    @heartbeat 110
    narrator: Руки не слушались.
    -> panic
движок принял lineeffectbranchjump

@unless — то же самое с отрицанием.

Флаги

@set fear = 0
@set fear += 2
@set fear -= 1
@set name = 0
движок принял set×4

Операторы — =, +=, -=. Флаг нужно задать до того, как его прочитают: если найдётся путь, на котором чтение случается раньше записи, валидатор скажет об этом кодом NSX_READ_BEFORE_WRITE.

Выражения

Приоритет от низкого к высокому:

or  <  and  <  not  <  сравнения (== != < <= > >=)  <  + -  <  * / %  <  унарный -  <  primary

and, 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
движок принял branchset×3jump

Арифметика только целочисленная. Деление отбрасывает дробь в сторону нуля, деление на ноль даёт 0. Это не упрощение: превью в браузере, движок на телефоне и повтор по сохранению обязаны сойтись бит в бит, иначе превью ведёт автора в одну концовку, а игрока — в другую.

rand() детерминирован: он ходит от rng_seed, который лежит в состоянии и в сохранении. Одно и то же прохождение всегда даёт один и тот же бросок.

Директивы

Всё, что начинается с @ и не перечислено выше, — директива-эффект:

@bg forest_night
@ambient wind_howl loop
@jumpscare shadow_figure @sound stinger_01
@sanity_show
движок принял effect×4

Аргументы позиционные; именованные пишутся как @подфлаг значение. Ассеты — по идентификатору.

Все 94 директивы с типами аргументов и границами — в справочнике. Он собран из реестра движка, поэтому не может отстать от него.

Концовки

@end trespass
движок принял end

@end [имя] завершает сцену. Имя необязательно, но именованная концовка — это то, что читатель «нашёл», и то, что движок считает при обходе.

Что проверяется

Компилятор ловит синтаксис, пока вы печатаете. Валидатор ловит целостность: битые ссылки, недостижимые узлы, тупики, ловушки-циклы, необъявленные флаги, чтение до записи, отсутствующие ассеты, лицензию без атрибуции.

Коды и починки — в справочнике диагностик.