Перейти к основному содержимому

Фреймворки на коде

Написание правил

Пишите конструкторы, действия и функции журнала на небольшом безопасном подмножестве 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.

rules/band.ts
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. Строка 1 Текст на входе, текст на выходе.
rules/hurt.ts
export function hurt(self: ElementRef, amount: number): void {
  ledger.add(self, "hp", 0 - amount);
}
  1. Строка 1 Сначала идёт элемент как self, затем собственные входные данные действия. Действие возвращает void.

Файл правил может содержать и вспомогательные функции. Пишите их без export и вызывайте по имени из того же файла. Файл по-прежнему экспортирует ровно одну функцию или только те действия или функции журнала, которые объявляет.

rules/guard.ts
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. Строки 1-3 Вспомогательная функция: без export, вызывается по имени из того же файла.
  2. Строка 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

Пусть вспомогательная функция возвращает значение, а каждое действие делает свою запись. Эти два действия делят одну чистую вспомогательную функцию:

rules/hp.ts
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. Строки 1-3 Чистая вспомогательная функция: без export, она только возвращает значение.
  2. Строка 8 Действие записывает значение, которое вернула вспомогательная функция.
  3. Строка 14 Второе действие использует ту же вспомогательную функцию.

Конструктор, который заполняет дочерние слоты, возвращает { values, children } вместо простой записи. Быстрый старт показывает простую запись, а страница схем Framework показывает форму с дочерними элементами.

Подмножество TypeScript

Правила выполняются одинаково везде и всегда завершаются. Поэтому язык небольшой.

РазрешеноЗапрещено
Явные типы, const и letimport, 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 allowed
export 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 subset
import { 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.

rules/pack-trinket.ts
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 } }));
  }
}
  1. Строка 2 Остановитесь, если в рюкзаке уже две записи.
  2. Строка 3 Предложение допускает только записи с весом 3 или меньше.
rules/unpack-trinket.ts
export function unpack_trinket(self: ElementRef, trinket: ContentRef): void {
  list.remove(self, "pack", trinket);
}
  1. Строка 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). Проверка считает, что действие затрагивает каждое поле из списка, а сервер принимает запись только в поле, выбранное игроком.

Это действие повышает навык, который выбирает игрок:

schemas/keeper.json
    {
      "id": "improve",
      "name": "Improve a skill",
      "source": "rules/improve.ts",
      "inputs": {
        "skill": { "label": "Skill", "fields": ["lore", "library_use"] }
      }
    }
  1. Строка 25 Вход skill выбирает одно из этих полей по имени. В списке нужно минимум два.
rules/improve.ts
export function improve(self: ElementRef, skill: string): void {
  ledger.add(self, skill, 1);
}
  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.

Дальше