Przejdź do głównej treści

Frameworki kodowe

Pisanie reguł

Pisz konstruktory, akcje i funkcje ledgera w małym, bezpiecznym podzbiorze TypeScript.

Trzy rodzaje plików reguł

RodzajKształtCo może robić
Konstruktorexport function construct(...inputs): { fields }Czyta dane wejściowe i zwraca wartości startowe. Nigdy nie zapisuje.
Akcjaexport function id(self: ElementRef, ...inputs): voidZmienia wartości przez ledger.set i ledger.add.
Funkcja ledgeraexport function id(currentValue: T, self: ElementRef): TCzyta wartości i zwraca wartość pochodną. Nigdy nie zapisuje.

Plik konstruktora eksportuje jedną funkcję. Plik akcji lub ledgera może wyeksportować kilka, jeśli każda nazwa eksportu jest identyfikatorem zadeklarowanym dla tego pliku: identyfikatorem akcji lub funkcji ledgera. Nazwy zapisanych pól używają małych liter, cyfr i podkreśleń.

Funkcja ledgera może też zwracać tekst. Zadeklaruj currentValue i wynik jako string, a we wpisie ledgera scenariusza podaj tekstowe 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. Wiersz 1 Tekst na wejściu, tekst na wyjściu.
rules/hurt.ts
export function hurt(self: ElementRef, amount: number): void {
  ledger.add(self, "hp", 0 - amount);
}
  1. Wiersz 1 Element jest pierwszy jako self, potem własne dane wejściowe akcji. Akcja zwraca void.

Plik reguł może też zawierać funkcje pomocnicze. Pisz je bez export i wywołuj po nazwie z tego samego pliku. Plik nadal eksportuje dokładnie jedną funkcję albo tylko akcje lub funkcje ledgera, które deklaruje.

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. Wiersze 1-3 Funkcja pomocnicza: bez export, wywoływana po nazwie z tego samego pliku.
  2. Wiersz 7 Wywołanie funkcji pomocniczej.

Plik reguł nie może importować kodu. Plik akcji eksportuje jedną funkcję, chyba że współdzieli go kilka akcji. Dodatkowy eksport sam kończy się błędem:

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

Aby współdzielić funkcję pomocniczą między akcjami, niech te akcje współdzielą jeden plik. Nazwij każdy eksport nazwą akcji, którą uruchamia, zadeklaruj każdy eksport jako identyfikator akcji i zostaw funkcję pomocniczą bez export. Akcje mogą należeć do różnych Frameworków, a jedna akcja nie może wywołać innej. Każda funkcja niesie kopię całego pliku w skompilowanej grze, więc trzymaj pliki współdzielone krótkie.

Funkcja pomocnicza może tylko obliczać i zwracać wartość. Nie może wywoływać ledger.set ani ledger.add. Funkcja pomocnicza, która zapisuje, jest odrzucana, nawet w pliku współdzielonym przez akcje:

function wound(self: ElementRef): void {
  ledger.add(self, "hp", -1);
}
effectful action helpers are not supported until call-target provenance is enforced

Niech funkcja pomocnicza zwraca wartość, a każda akcja niech sama zapisuje. Te dwie akcje współdzielą jedną czystą funkcję pomocniczą:

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. Wiersze 1-3 Czysta funkcja pomocnicza: bez export, tylko zwraca wartość.
  2. Wiersz 8 Akcja zapisuje wartość zwróconą przez funkcję pomocniczą.
  3. Wiersz 14 Druga akcja używa tej samej funkcji pomocniczej.

Konstruktor, który wypełnia miejsca podrzędne, zwraca { values, children } zamiast zwykłego rekordu. Szybki start pokazuje zwykły rekord, a strona o schematach frameworków pokazuje formę z elementami podrzędnymi.

Podzbiór TypeScript

Reguły działają wszędzie tak samo i zawsze się kończą. Dlatego język jest mały.

DozwoloneNiedozwolone
Jawne typy, const i letimport, klasy, funkcje strzałkowe, new, this
if i else, for of, returnpętle while, for (;;), switch, break, continue, kod async, wyjątki
Literały liczb, tekstów, wartości logicznych, rekordów i tablicSzablony tekstowe, spread, optional chaining, ??, komentarze blokowe (użyj //)
Arytmetyka, ścisłe porównania (=== i !==), operatory logiczne, operatory trójargumentowe== i !=, **, ++ i +=, typeof, zmiana obiektów w miejscu, domyślne wartości parametrów
Wywołania funkcji hosta, funkcji pomocniczych z tego samego pliku i rzutowania as ich wynikówWywołania metod, np. "a".toUpperCase(), Math, Date, JSON, fetch i inne globalne obiekty

Kończ każdą instrukcję średnikiem. Każdy z tych błędów zostanie wychwycony przy sprawdzaniu projektu.

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

Funkcja z typem zwracanym musi zwracać wartość na każdej ścieżce.

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

Więcej błędów wychwytywanych przez sprawdzanie

Każdy wiersz cytuje fragment komunikatu i mówi, co napisać zamiast tego.

KomunikatCo napisać zamiast tego
block comments are not allowedUżyj komentarzy liniowych //.
switch is not allowedUżyj if i else.
break is not allowedZmień pętlę albo opakuj resztę jej treści w if.
continue is not allowedOpakuj resztę treści pętli w if.
classic for loops are not allowedUżyj for (const item of list).
loose equality is not allowedUżyj ===.
loose inequality is not allowedUżyj !==.
exponentiation (**) is not allowedUżyj math.pow.
increment (++) is not allowedNapisz count = count + 1.
compound assignment is not allowedNapisz count = count + value.
typeof is not allowedPorównaj przez === z literałem.
class is not allowedUżyj zwykłych funkcji.
arrow functions are not allowedNapisz funkcję pomocniczą na początku pliku.
async is not allowedUsuń async. Reguły działają w jednym kroku.
template literals are not allowedUżyj literałów tekstowych.
spread/rest (...) is not allowedWypisz elementy po kolei.
optional chaining (?.) is not allowedNajpierw porównaj z null.
nullish coalescing (??) is not allowedUżyj operatora trójargumentowego, który porównuje z null.
throw is not allowedWyjdź z reguły bez zapisywania.
try/catch is not allowedUsuń try i catch.
new is not allowedNapisz zamiast tego literały.
this is not allowedUżyj parametru self.
method calls are not allowedWywołuj tylko funkcje hosta i funkcje pomocnicze z tego samego pliku.
default parameter values (=) are not allowedPodaj wartość każdego parametru tam, gdzie reguła jest używana.
assignment targets must be local variablesZbuduj nową wartość zamiast zmieniać obiekt.
functions but only the actionUsuń export z funkcji pomocniczych.
constructors must be pure and cannot read or write host stateCzytaj i zapisuj w akcjach i funkcjach ledgera.
is not available untilFunkcja ledgera nie może zapisywać. Użyj akcji.
effectful action helpers are not supportedNiech funkcja pomocnicza zwraca wartość, a zapisz ją w akcji.

Wywołania hosta

Reguły łączą się z Fabled przez wywołania hosta. Piszesz je tak, jak są, na przykład ledger.add(...). Nigdy ich nie importujesz.

W tych sygnaturach HostValue to dowolna wartość, którą może zwrócić wywołanie hosta, więc zrzutuj ją przez as, zanim jej użyjesz. AbiValue to liczba, tekst, wartość logiczna lub null albo lista lub rekord takich wartości. Opcjonalny parametr sticky jest do użytku samego Fabled, więc go pomiń.

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;
WywołanieGdzieCo robi
element.varAkcje, funkcje ledgeraCzyta zapisaną wartość elementu. Zrzutuj wynik przez as number, as string lub as boolean.
ledger.setAkcjeUstawia zapisane pole na wartość.
ledger.addAkcjeDodaje liczbę do zapisanego pola. Dodaj liczbę ujemną, aby odjąć.
math.floorWszędzieZaokrągla w dół.
math.ceilWszędzieZaokrągla w górę.
math.roundWszędzieZaokrągla do najbliższej liczby całkowitej.
math.maxWszędzieZwraca największą z podanych liczb.
math.minWszędzieZwraca najmniejszą z podanych liczb.
math.absWszędzieZwraca wartość liczby bez znaku.
math.powWszędziePodnosi podstawę do potęgi.
math.sqrtWszędzieZwraca pierwiastek kwadratowy.
list.addAkcjeDodaje wybrany wpis do listy. Oferta decyduje, co można dodać. Odmowa z NOT_OFFERED, ALREADY_IN_LIST lub LIST_FULL.
list.removeAkcjeUsuwa wybrany wpis z listy. Odmowa z NOT_IN_LIST.
list.countAkcjeZwraca, ile wpisów zawiera lista. Użyj tego w warunku przed list.add.
offer.whereAkcjeBuduje ofertę dla list.add: wpisy, których pola spełniają warunki zapisane jako literały.

Wywołanie listy podaje listę frameworka przez klucz zapisany wprost, a wpis to ContentRef, czyli dane wejściowe, które przechowują wpis wybrany przez gracza. Oferta przyjmuje tylko warunki zapisane jako literały, na przykład offer.where({ weight: { lte: 3 } }). Każde pole przyjmuje eq, ne, gte, lte, in lub has. Oferta nie może czytać elementu. Aby zależeć od wartości postaci, sprawdź ją przez if przed 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. Wiersz 2 Zatrzymaj się, gdy plecak ma już dwa wpisy.
  2. Wiersz 3 Oferta dopuszcza tylko wpisy o wadze 3 lub mniejszej.
rules/unpack-trinket.ts
export function unpack_trinket(self: ElementRef, trinket: ContentRef): void {
  list.remove(self, "pack", trinket);
}
  1. Wiersz 2 Usuń wybrany wpis. Akcja listy, która tylko usuwa, potrzebuje jednego kroku przyjętego.

Nazwy pól są zapisane wprost

W akcji nazwa pola musi być tekstem zapisanym w kodzie, na przykład "hp". Nie może pochodzić z wartości. Taka reguła kończy się błędem:

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.

Bez listy pól jedna akcja nie może więc sprawdzać dowolnej umiejętności po nazwie. Napisz po jednej akcji dla każdej umiejętności i niech każda czyta swoje pole. Przepis Rzut przeciw umiejętności pokazuje krótką akcję do skopiowania. Wyjątkiem jest wejście, które wylicza swoje pola. Nadaj wejściu akcji listę "fields", na przykład ["lore", "library_use"], a akcja może przekazać to wejście jako nazwę, jak w ledger.add(self, skill, amount). Sprawdzanie traktuje akcję tak, jakby dotykała każdego pola z listy, a serwer przyjmuje zapis tylko do pola wybranego przez gracza.

Ta akcja podnosi umiejętność wybraną przez gracza:

schemas/keeper.json
    {
      "id": "improve",
      "name": "Improve a skill",
      "source": "rules/improve.ts",
      "inputs": {
        "skill": { "label": "Skill", "fields": ["lore", "library_use"] }
      }
    }
  1. Wiersz 25 Dane wejściowe skill wybierają jedno z tych pól po nazwie. Lista wymaga co najmniej dwóch.
rules/improve.ts
export function improve(self: ElementRef, skill: string): void {
  ledger.add(self, skill, 1);
}
  1. Wiersz 2 skill zawiera nazwę pola wybranego przez gracza.

Każda nazwa w fields musi wskazywać zapisane pole Frameworka, które akcja może zmieniać. Pole wyliczane przez funkcję rejestru lub modyfikator jest odrzucane. Jeśli pole ma wejście schematu o tej samej nazwie z label, okno pokazuje tę etykietę; w przeciwnym razie pokazuje identyfikator, na przykład library_use. Na przykład inputs.library_use.label może nazwać opcję Korzystanie z biblioteki. Wybrana wartość, parametry scenariuszy i symbole zastępcze w dzienniku, takie jak {skill}, nadal używają identyfikatora pola. Każda natywna lista opcji tekstowych, której wszystkie wartości są nazwami zapisywalnych pól skalarnych, używa tych samych etykiet.

fields "skill_total" is not a writable state field

Tylko zadeklarowane pola

Akcja może zapisywać tylko pola zwracane przez konstruktor. Sprawdzenie podaje nazwę pola, którego nie może znaleźć.

Action "keeper.heal" calls ledger.set(self, "mana", ...), and "mana" is not declared on this schema. Declared scalar fields: hp, max_hp, name.

Dalej