Zum Hauptinhalt springen

Code-Frameworks

Regeln schreiben

Schreibe Konstruktoren, Aktionen und Ledger-Funktionen in einer kleinen, sicheren Teilmenge von TypeScript.

Drei Arten von Regeldateien

ArtFormWas sie darf
Constructorexport function construct(...inputs): { fields }Liest ihre Eingaben und gibt die Startwerte zurück. Sie schreibt nie.
Actionexport function id(self: ElementRef, ...inputs): voidÄndert Werte mit ledger.set und ledger.add.
Ledger functionexport function id(currentValue: T, self: ElementRef): TLiest Werte und gibt einen abgeleiteten Wert zurück. Sie schreibt nie.

Eine Konstruktor-Datei exportiert eine Funktion. Eine Aktions- oder Ledger-Datei darf mehrere exportieren, wenn jeder Exportname eine für diese Datei deklarierte ID ist: eine Aktions-ID oder eine Ledger-Funktions-ID. Gespeicherte Feldnamen verwenden Kleinbuchstaben, Ziffern und Unterstriche.

Eine Ledger-Funktion kann auch Text zurückgeben. Gib currentValue und das Ergebnis als string an, und gib dem Ledger-Eintrag des Szenarios einen Text als 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. Zeile 1 Text hinein, Text heraus.
rules/hurt.ts
export function hurt(self: ElementRef, amount: number): void {
  ledger.add(self, "hp", 0 - amount);
}
  1. Zeile 1 Das Element kommt zuerst als self, dann die eigenen Eingaben der Aktion. Eine Aktion gibt void zurück.

Eine Regeldatei kann auch Hilfsfunktionen enthalten. Schreibe sie ohne export und rufe sie in derselben Datei beim Namen auf. Die Datei exportiert weiterhin genau eine Funktion oder nur die Aktionen oder Ledger-Funktionen, die sie deklariert.

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. Zeilen 1-3 Eine Hilfsfunktion: kein export, und sie wird in derselben Datei beim Namen aufgerufen.
  2. Zeile 7 Der Aufruf der Hilfsfunktion.

Eine Regeldatei kann keinen Code importieren. Eine Aktionsdatei exportiert eine Funktion, es sei denn, mehrere Aktionen teilen sie. Ein zusätzlicher Export allein scheitert mit:

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

Um eine Hilfsfunktion zwischen Aktionen zu teilen, lasse diese Aktionen eine Datei teilen. Benenne jeden Export nach der Aktion, die er ausführt, deklariere jeden Export als Aktions-ID und lasse die Hilfsfunktion ohne export. Die Aktionen dürfen zu verschiedenen Frameworks gehören, und eine Aktion kann keine andere aufrufen. Jede Funktion trägt eine Kopie ihrer ganzen Datei im kompilierten Spiel, also halte geteilte Dateien kurz.

Eine Hilfsfunktion darf nur berechnen und einen Wert zurückgeben. Sie darf ledger.set oder ledger.add nicht aufrufen. Eine Hilfsfunktion, die schreibt, wird abgelehnt, auch in einer Datei, die Aktionen teilen:

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

Lasse die Hilfsfunktion den Wert zurückgeben und jede Aktion selbst schreiben. Diese beiden Aktionen teilen eine reine Hilfsfunktion:

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. Zeilen 1-3 Eine reine Hilfsfunktion: kein export, und sie gibt nur einen Wert zurück.
  2. Zeile 8 Die Aktion schreibt den Wert, den die Hilfsfunktion zurückgegeben hat.
  3. Zeile 14 Die zweite Aktion verwendet die Hilfsfunktion wieder.

Ein Konstruktor, der untergeordnete Plätze füllt, gibt { values, children } statt eines einfachen Records zurück. Der Schnellstart zeigt den einfachen Record, und die Seite Framework-Schemas zeigt die Form mit untergeordneten Elementen.

Die TypeScript-Teilmenge

Regeln laufen überall gleich ab, und sie enden immer. Deshalb ist die Sprache klein.

ErlaubtNicht erlaubt
Explizite Typen, const und letimport, Klassen, Pfeilfunktionen, new, this
if und else, for of, returnwhile-Schleifen, for (;;), switch, break, continue, async-Code, Ausnahmen
Literale für Zahlen, Text, Wahrheitswerte, Records und ArraysTemplate-Strings, Spread, optionale Verkettung, ??, Blockkommentare (nutze //)
Arithmetik, strikte Vergleiche (=== und !==), boolesche Operatoren, ternäre Ausdrücke== und !=, **, ++ und +=, typeof, Objekte an Ort und Stelle ändern, Standardwerte für Parameter
Aufrufe von Host-Funktionen und Hilfsfunktionen derselben Datei sowie as-Casts ihrer RückgabewerteMethodenaufrufe wie "a".toUpperCase(), Math, Date, JSON, fetch und andere Globals

Beende jede Anweisung mit einem Semikolon. Jeder dieser Fehler wird beim Prüfen des Projekts erkannt.

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

Eine Funktion mit Rückgabetyp muss auf jedem Pfad einen Wert zurückgeben.

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

Weitere Fehler, die die Prüfung findet

Jede Zeile zitiert einen Teil einer Meldung und sagt, was du stattdessen schreibst.

MeldungWas du stattdessen schreibst
block comments are not allowedNutze //-Zeilenkommentare.
switch is not allowedNutze if und else.
break is not allowedÄndere die Schleife oder packe den Rest ihres Rumpfs in ein if.
continue is not allowedPacke den Rest des Schleifenrumpfs in ein if.
classic for loops are not allowedNutze for (const item of list).
loose equality is not allowedNutze ===.
loose inequality is not allowedNutze !==.
exponentiation (**) is not allowedNutze math.pow.
increment (++) is not allowedSchreibe count = count + 1.
compound assignment is not allowedSchreibe count = count + value.
typeof is not allowedVergleiche mit === gegen ein Literal.
class is not allowedNutze einfache Funktionen.
arrow functions are not allowedSchreibe eine Hilfsfunktion oben in der Datei.
async is not allowedEntferne async. Regeln laufen in einem Schritt.
template literals are not allowedNutze Text-Literale.
spread/rest (...) is not allowedSchreibe die Einträge einzeln aus.
optional chaining (?.) is not allowedVergleiche zuerst mit null.
nullish coalescing (??) is not allowedNutze einen ternären Ausdruck, der mit null vergleicht.
throw is not allowedVerlasse die Regel, ohne zu schreiben.
try/catch is not allowedEntferne try und catch.
new is not allowedSchreibe stattdessen Literale.
this is not allowedNutze den Parameter self.
method calls are not allowedRufe nur Host-Funktionen und Hilfsfunktionen derselben Datei auf.
default parameter values (=) are not allowedGib jedem Parameter dort einen Wert, wo die Regel genutzt wird.
assignment targets must be local variablesBaue einen neuen Wert, statt ein Objekt zu ändern.
functions but only the actionEntferne export von den Hilfsfunktionen.
constructors must be pure and cannot read or write host stateLies und schreibe in Aktionen und Ledger-Funktionen.
is not available untilEine Ledger-Funktion kann nicht schreiben. Nutze eine Aktion.
effectful action helpers are not supportedLasse die Hilfsfunktion einen Wert zurückgeben und schreibe ihn in der Aktion.

Host-Aufrufe

Regeln erreichen Fabled über Host-Aufrufe. Du schreibst sie so, wie sie sind, zum Beispiel ledger.add(...). Du importierst sie nie.

In diesen Signaturen ist HostValue jeder Wert, den ein Host-Aufruf zurückgeben kann. Caste ihn also mit as, bevor du ihn nutzt. AbiValue ist eine Zahl, ein Text, ein Boolean oder null, oder eine Liste oder ein Record davon. Ein optionaler Parameter sticky ist für Fabled selbst, also lass ihn weg.

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;
AufrufWoWas er tut
element.varAktionen, Ledger-FunktionenLiest einen gespeicherten Wert eines Elements. Caste das Ergebnis mit as number, as string oder as boolean.
ledger.setAktionenSetzt ein gespeichertes Feld auf einen Wert.
ledger.addAktionenAddiert eine Zahl zu einem gespeicherten Feld. Addiere eine negative Zahl, um abzuziehen.
math.floorÜberallRundet ab.
math.ceilÜberallRundet auf.
math.roundÜberallRundet auf die nächste ganze Zahl.
math.maxÜberallGibt die größte seiner Zahlen zurück.
math.minÜberallGibt die kleinste seiner Zahlen zurück.
math.absÜberallGibt den Betrag einer Zahl ohne Vorzeichen zurück.
math.powÜberallPotenziert eine Basis mit einem Exponenten.
math.sqrtÜberallGibt die Quadratwurzel zurück.
list.addAktionenFügt den gewählten Eintrag einer Liste hinzu. Das Angebot entscheidet, was hinzugefügt werden darf. Wird mit NOT_OFFERED, ALREADY_IN_LIST oder LIST_FULL abgelehnt.
list.removeAktionenEntfernt den gewählten Eintrag aus einer Liste. Wird mit NOT_IN_LIST abgelehnt.
list.countAktionenGibt zurück, wie viele Einträge eine Liste hält. Nutze es in einer Absicherung vor list.add.
offer.whereAktionenBaut das Angebot für list.add: die Einträge, deren Felder zu Bedingungen passen, die du als Literale schreibst.

Ein Listen-Aufruf nennt eine Liste des Frameworks mit einem literalen Schlüssel, und der Eintrag ist ein ContentRef, eine Eingabe, die den vom Spieler gewählten Eintrag hält. Ein Angebot nimmt nur literale Bedingungen, zum Beispiel offer.where({ weight: { lte: 3 } }). Jedes Feld nimmt eq, ne, gte, lte, in oder has. Ein Angebot kann das Element nicht lesen. Wenn es von einem Wert des Charakters abhängen soll, prüfe ihn mit if vor 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. Zeile 2 Brich ab, wenn das Pack schon zwei Einträge hält.
  2. Zeile 3 Das Angebot erlaubt nur Einträge, die höchstens 3 wiegen.
rules/unpack-trinket.ts
export function unpack_trinket(self: ElementRef, trinket: ContentRef): void {
  list.remove(self, "pack", trinket);
}
  1. Zeile 2 Entferne den gewählten Eintrag. Eine Listen-Aktion, die nur entfernt, braucht einen angenommenen Schritt.

Feldnamen werden ausgeschrieben

In einer Aktion muss der Name eines Feldes ein im Code ausgeschriebener Text sein, zum Beispiel "hp". Er kann nicht aus einem Wert kommen. Eine Regel wie diese schlägt fehl:

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.

Ohne Feldliste kann eine Aktion also nicht jeden Skill nach Namen prüfen. Schreibe für jeden Skill eine eigene Aktion, und lass jede ihr eigenes Feld lesen. Das Rezept Gegen einen Skill würfeln zeigt eine kurze Aktion zum Kopieren. Die Ausnahme ist eine Eingabe, die ihre Felder auflistet. Gib der Aktionseingabe eine Liste "fields", etwa ["lore", "library_use"], dann darf die Aktion diese Eingabe als Namen übergeben, wie in ledger.add(self, skill, amount). Die Prüfung behandelt die Aktion so, als berühre sie jedes aufgelistete Feld, und der Server akzeptiert nur einen Schreibzugriff auf das Feld, das der Spieler gewählt hat.

Diese Aktion erhöht die Fertigkeit, die der Spieler wählt:

schemas/keeper.json
    {
      "id": "improve",
      "name": "Improve a skill",
      "source": "rules/improve.ts",
      "inputs": {
        "skill": { "label": "Skill", "fields": ["lore", "library_use"] }
      }
    }
  1. Zeile 25 Die Eingabe skill wählt eines dieser Felder nach Namen. Eine Liste braucht mindestens zwei.
rules/improve.ts
export function improve(self: ElementRef, skill: string): void {
  ledger.add(self, skill, 1);
}
  1. Zeile 2 skill enthält den Namen des Feldes, das der Spieler gewählt hat.

Jeder Name in fields muss ein gespeichertes Feld des Frameworks sein, das eine Aktion schreiben darf. Ein von einer Ledger-Funktion abgeleitetes Feld oder ein Modifikator wird abgelehnt. Hat ein Feld eine gleichnamige Schema-Eingabe mit label, zeigt der Dialog diese Beschriftung; andernfalls zeigt er die Feld-ID, etwa library_use. Beispielsweise kann inputs.library_use.label die Auswahl Bibliotheksnutzung nennen. Der ausgewählte Wert, Szenario-Parameter und Log-Platzhalter wie {skill} verwenden weiterhin die Feld-ID. Jede native Textauswahlliste, deren Werte alle schreibbare skalare Felder benennen, verwendet dieselben Beschriftungen.

fields "skill_total" is not a writable state field

Nur deklarierte Felder

Eine Aktion darf nur Felder schreiben, die der Konstruktor zurückgibt. Die Prüfung nennt das Feld, das sie nicht findet.

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

Weiter