Фреймворки на коде
Написание правил
Пишите конструкторы, действия и функции журнала на небольшом безопасном подмножестве TypeScript.
Три вида файлов правил
| Вид | Форма | Что может делать |
|---|---|---|
| Конструктор | export function construct(...inputs): { fields } | Читает входные данные и возвращает начальные значения. Никогда не пишет. |
| Действие | export function id(self: ElementRef, ...inputs): void | Меняет значения через ledger.set и ledger.add. |
| Функция журнала | export function id(currentValue: T, self: ElementRef): T | Читает значения и возвращает производное значение. Никогда не пишет. |
Файл конструктора экспортирует одну функцию. Файл действия или журнала может экспортировать несколько, если каждое экспортируемое имя является идентификатором, объявленным для этого файла: идентификатором действия или функции журнала. Имена сохраняемых полей состоят из строчных букв, цифр и подчёркиваний.
Функция журнала может возвращать и текст. Укажите для currentValue и результата тип string, а в записи журнала сценария задайте текстовый currentValue.
export function last_band(currentValue: string, self: ElementRef): string {
const roll: number = element.var(self, "last_roll") as number;
const target: number = element.var(self, "last_target") as number;
if (roll === 0) {
return "No roll yet";
}
if (roll <= math.floor(target / 5)) {
return "Flawless";
}
if (roll <= math.floor(target / 2)) {
return "Strong";
}
if (roll <= target) {
return "Pass";
}
return "Miss";
}- Строка 1 Текст на входе, текст на выходе.
export function hurt(self: ElementRef, amount: number): void {
ledger.add(self, "hp", 0 - amount);
}- Строка 1 Сначала идёт элемент как self, затем собственные входные данные действия. Действие возвращает void.
Файл правил может содержать и вспомогательные функции. Пишите их без export и вызывайте по имени из того же файла. Файл по-прежнему экспортирует ровно одну функцию или только те действия или функции журнала, которые объявляет.
function base_guard(calling: string): number {
return calling === "Guard" ? 3 : 1;
}
export function guard(currentValue: number, self: ElementRef): number {
const calling: string = element.var(self, "calling") as string;
return base_guard(calling);
}- Строки 1-3 Вспомогательная функция: без export, вызывается по имени из того же файла.
- Строка 7 Вызов вспомогательной функции.
Файл правил не может импортировать код. Файл действия экспортирует одну функцию, если его не разделяют несколько действий. Лишний экспорт сам по себе приводит к ошибке:
export function hurt(self: ElementRef, amount: number): void {
ledger.add(self, "hp", 0 - amount);
}
export function hurt_twice(self: ElementRef, amount: number): void {
ledger.add(self, "hp", 0 - amount - amount);
}functions but only the action hurt uses itЧтобы разделить вспомогательную функцию между действиями, пусть эти действия используют один файл. Назовите каждый экспорт именем действия, которое он запускает, объявите каждый экспорт идентификатором действия и оставьте вспомогательную функцию без export. Действия могут принадлежать разным Frameworks, а одно действие не может вызвать другое. Каждая функция несёт копию всего своего файла в собранной игре, поэтому делайте общие файлы короткими.
Вспомогательная функция может только вычислять и возвращать значение. Она не может вызывать ledger.set или ledger.add. Вспомогательная функция, которая пишет, отклоняется, даже в файле, общем для действий:
function wound(self: ElementRef): void {
ledger.add(self, "hp", -1);
}effectful action helpers are not supported until call-target provenance is enforcedПусть вспомогательная функция возвращает значение, а каждое действие делает свою запись. Эти два действия делят одну чистую вспомогательную функцию:
function clamp(value: number, low: number, high: number): number {
return math.max(low, math.min(high, value));
}
export function hurt(self: ElementRef, amount: number): void {
const hp: number = element.var(self, "hp") as number;
const max_hp: number = element.var(self, "max_hp") as number;
ledger.set(self, "hp", clamp(hp - amount, 0, max_hp));
}
export function heal(self: ElementRef, amount: number): void {
const hp: number = element.var(self, "hp") as number;
const max_hp: number = element.var(self, "max_hp") as number;
ledger.set(self, "hp", clamp(hp + amount, 0, max_hp));
}- Строки 1-3 Чистая вспомогательная функция: без export, она только возвращает значение.
- Строка 8 Действие записывает значение, которое вернула вспомогательная функция.
- Строка 14 Второе действие использует ту же вспомогательную функцию.
Конструктор, который заполняет дочерние слоты, возвращает { values, children } вместо простой записи. Быстрый старт показывает простую запись, а страница схем Framework показывает форму с дочерними элементами.
Подмножество TypeScript
Правила выполняются одинаково везде и всегда завершаются. Поэтому язык небольшой.
| Разрешено | Запрещено |
|---|---|
| Явные типы, const и let | import, class, стрелочные функции, new, this |
| if и else, for of, return | циклы while, for (;;), switch, break, continue, async-код, исключения |
| Литералы чисел, строк, булевых значений, записей и массивов | Шаблонные строки, spread, optional chaining, ??, блочные комментарии (используйте //) |
| Арифметика, строгое сравнение (=== и !==), булевы операторы, тернарные операторы | == и !=, **, ++ и +=, typeof, изменение объектов на месте, значения параметров по умолчанию |
| Вызовы функций хоста, вспомогательные функции того же файла и приведения as для значений, которые они возвращают | Вызовы методов, например "a".toUpperCase(), Math, Date, JSON, fetch и другие глобальные объекты |
Заканчивайте каждую инструкцию точкой с запятой. Каждая из этих ошибок находится при проверке проекта.
export function heal(self: ElementRef, amount: number): void {
while (amount > 0) {
ledger.add(self, "hp", 1);
amount = amount - 1;
}
}while loops are not allowedexport function heal(self: ElementRef, amount: number): void {
ledger.set(self, "hp", Math.min(10, amount));
}'Math' is a host global and not part of the TypeScript subsetimport { clamp } from "./clamp";import is not allowedФункция с типом возврата должна возвращать значение на каждом пути.
export function hurt(self: ElementRef, amount: number): number {
ledger.add(self, "hp", 0 - amount);
}a non-void function must return a value on every reachable pathДругие ошибки, которые находит проверка
В каждой строке приведена часть сообщения и сказано, что писать вместо этого.
| Сообщение | Что писать вместо этого |
|---|---|
| block comments are not allowed | Используйте строчные комментарии //. |
| switch is not allowed | Используйте if и else. |
| break is not allowed | Измените цикл или оберните остаток его тела в if. |
| continue is not allowed | Оберните остаток тела цикла в if. |
| classic for loops are not allowed | Используйте for (const item of list). |
| loose equality is not allowed | Используйте ===. |
| loose inequality is not allowed | Используйте !==. |
| exponentiation (**) is not allowed | Используйте math.pow. |
| increment (++) is not allowed | Пишите count = count + 1. |
| compound assignment is not allowed | Пишите count = count + value. |
| typeof is not allowed | Сравнивайте через === с литералом. |
| class is not allowed | Используйте обычные функции. |
| arrow functions are not allowed | Напишите вспомогательную функцию в начале файла. |
| async is not allowed | Уберите async. Правила выполняются за один шаг. |
| template literals are not allowed | Используйте строковые литералы. |
| spread/rest (...) is not allowed | Перечислите элементы явно. |
| optional chaining (?.) is not allowed | Сначала сравните с null. |
| nullish coalescing (??) is not allowed | Используйте тернарный оператор со сравнением с null. |
| throw is not allowed | Выйдите из правила, ничего не записав. |
| try/catch is not allowed | Уберите try и catch. |
| new is not allowed | Пишите вместо этого литералы. |
| this is not allowed | Используйте параметр self. |
| method calls are not allowed | Вызывайте только функции хоста и вспомогательные функции того же файла. |
| default parameter values (=) are not allowed | Передавайте значение каждого параметра там, где используется правило. |
| assignment targets must be local variables | Постройте новое значение вместо изменения объекта. |
| functions but only the action | Уберите export у вспомогательных функций. |
| constructors must be pure and cannot read or write host state | Читайте и пишите в действиях и функциях журнала. |
| is not available until | Функция журнала не может писать. Используйте действие. |
| effectful action helpers are not supported | Пусть вспомогательная функция возвращает значение, а запишите его в действии. |
Вызовы хоста
Правила обращаются к Fabled через вызовы хоста. Пишите их как есть, например ledger.add(...). Импортировать их не нужно.
В этих сигнатурах HostValue это любое значение, которое может вернуть вызов хоста, поэтому приведите его через as перед использованием. AbiValue это число, текст, boolean или null либо список или запись из них. Необязательный параметр sticky нужен самому Fabled, поэтому не указывайте его.
element.var(ref: ElementRef, name: HostValue, sticky?: AbiValue): HostValue;
ledger.set(ref: ElementRef, key: string, value: AbiValue): void;
ledger.add(ref: ElementRef, key: string, value: number): void;
math.floor(x: number): number;
math.ceil(x: number): number;
math.round(x: number): number;
math.max(...values: number[]): number;
math.min(...values: number[]): number;
math.abs(x: number): number;
math.pow(base: number, exponent: number): number;
math.sqrt(x: number): number;
list.add(ref: ElementRef, key: string, item: ContentRef, offer: ContentOffer): void;
list.remove(ref: ElementRef, key: string, item: ContentRef): void;
list.count(ref: ElementRef, key: string): number;
offer.where(where: ContentWhere, opts?: { maxItems?: number; flag?: { by: string; map: Record<string, string> } }): ContentOffer;| Вызов | Где | Что делает |
|---|---|---|
| element.var | Действия, функции журнала | Читает хранимое значение Element. Приведите результат через as number, as string или as boolean. |
| ledger.set | Действия | Устанавливает значение хранимого поля. |
| ledger.add | Действия | Прибавляет число к хранимому полю. Чтобы вычесть, прибавьте отрицательное число. |
| math.floor | Везде | Округляет вниз. |
| math.ceil | Везде | Округляет вверх. |
| math.round | Везде | Округляет до ближайшего целого числа. |
| math.max | Везде | Возвращает наибольшее из своих чисел. |
| math.min | Везде | Возвращает наименьшее из своих чисел. |
| math.abs | Везде | Возвращает величину числа без знака. |
| math.pow | Везде | Возводит основание в степень. |
| math.sqrt | Везде | Возвращает квадратный корень. |
| list.add | Действия | Добавляет выбранную запись в список. Предложение решает, что можно добавить. Отклоняется с кодами NOT_OFFERED, ALREADY_IN_LIST или LIST_FULL. |
| list.remove | Действия | Удаляет выбранную запись из списка. Отклоняется с кодом NOT_IN_LIST. |
| list.count | Действия | Возвращает, сколько записей в списке. Используйте в проверке перед list.add. |
| offer.where | Действия | Строит предложение для list.add: записи, чьи поля совпадают с условиями, которые вы пишете как литералы. |
Вызов списка называет список Framework литеральным ключом, а запись это ContentRef, входное значение, которое хранит запись, выбранную игроком. Предложение принимает только литеральные условия, например offer.where({ weight: { lte: 3 } }). Каждое поле принимает eq, ne, gte, lte, in или has. Предложение не может читать Element. Чтобы зависеть от значения персонажа, проверьте его через if перед list.add.
export function pack_trinket(self: ElementRef, trinket: ContentRef): void {
if (list.count(self, "pack") < 2) {
list.add(self, "pack", trinket, offer.where({ weight: { lte: 3 } }));
}
}- Строка 2 Остановитесь, если в рюкзаке уже две записи.
- Строка 3 Предложение допускает только записи с весом 3 или меньше.
export function unpack_trinket(self: ElementRef, trinket: ContentRef): void {
list.remove(self, "pack", trinket);
}- Строка 2 Удалите выбранную запись. Действию списка, которое только удаляет, нужен один принятый шаг.
Имена полей пишутся явно
В действии имя поля должно быть строкой, записанной прямо в коде, например "hp". Оно не может приходить из значения. Такое правило не проходит проверку:
export function hurt(self: ElementRef, skill: string): void {
const value: number = element.var(self, skill) as number;
ledger.set(self, "hp", value);
}Action accesses require literal keys on self.Поэтому без списка полей одно действие не может проверить любой навык по имени. Напишите по одному действию на каждый навык, и пусть каждое читает своё поле. Рецепт «Бросок против навыка» показывает короткое действие, которое можно скопировать. Исключение: вход, который перечисляет свои поля. Задайте входу действия список "fields", например ["lore", "library_use"], и действие сможет передать этот вход как имя, как в ledger.add(self, skill, amount). Проверка считает, что действие затрагивает каждое поле из списка, а сервер принимает запись только в поле, выбранное игроком.
Это действие повышает навык, который выбирает игрок:
{
"id": "improve",
"name": "Improve a skill",
"source": "rules/improve.ts",
"inputs": {
"skill": { "label": "Skill", "fields": ["lore", "library_use"] }
}
}- Строка 25 Вход skill выбирает одно из этих полей по имени. В списке нужно минимум два.
export function improve(self: ElementRef, skill: string): void {
ledger.add(self, skill, 1);
}- Строка 2 skill содержит имя поля, которое выбрал игрок.
Каждое имя в fields должно быть сохранённым полем Framework, которое действие может записать. Поле, которое выводит функция журнала, и модификатор отклоняются. Если в схеме есть одноимённый вход с label, диалог показывает эту подпись; иначе он показывает идентификатор поля, например library_use. Например, inputs.library_use.label может назвать вариант «Работа в библиотеке». Выбранное значение, параметры сценариев и подстановки в журнале, такие как {skill}, по-прежнему используют идентификатор поля. Любой нативный список текстовых вариантов, все значения которого называют доступные для записи скалярные поля, использует те же подписи.
fields "skill_total" is not a writable state fieldТолько объявленные поля
Действие может писать только в поля, которые возвращает конструктор. Проверка называет поле, которое не нашла.
Action "keeper.heal" calls ledger.set(self, "mana", ...), and "mana" is not declared on this schema. Declared scalar fields: hp, max_hp, name.
