Code-Frameworks
Regeln schreiben
Schreibe Konstruktoren, Aktionen und Ledger-Funktionen in einer kleinen, sicheren Teilmenge von TypeScript.
Drei Arten von Regeldateien
| Art | Form | Was sie darf |
|---|---|---|
| Constructor | export function construct(...inputs): { fields } | Liest ihre Eingaben und gibt die Startwerte zurück. Sie schreibt nie. |
| Action | export function id(self: ElementRef, ...inputs): void | Ändert Werte mit ledger.set und ledger.add. |
| Ledger function | export function id(currentValue: T, self: ElementRef): T | Liest 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.
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";
}- Zeile 1 Text hinein, Text heraus.
export function hurt(self: ElementRef, amount: number): void {
ledger.add(self, "hp", 0 - amount);
}- 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.
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);
}- Zeilen 1-3 Eine Hilfsfunktion: kein export, und sie wird in derselben Datei beim Namen aufgerufen.
- 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 itUm 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 enforcedLasse die Hilfsfunktion den Wert zurückgeben und jede Aktion selbst schreiben. Diese beiden Aktionen teilen eine reine Hilfsfunktion:
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));
}- Zeilen 1-3 Eine reine Hilfsfunktion: kein export, und sie gibt nur einen Wert zurück.
- Zeile 8 Die Aktion schreibt den Wert, den die Hilfsfunktion zurückgegeben hat.
- 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.
| Erlaubt | Nicht erlaubt |
|---|---|
| Explizite Typen, const und let | import, Klassen, Pfeilfunktionen, new, this |
| if und else, for of, return | while-Schleifen, for (;;), switch, break, continue, async-Code, Ausnahmen |
| Literale für Zahlen, Text, Wahrheitswerte, Records und Arrays | Template-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ückgabewerte | Methodenaufrufe 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 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 allowedEine 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 pathWeitere Fehler, die die Prüfung findet
Jede Zeile zitiert einen Teil einer Meldung und sagt, was du stattdessen schreibst.
| Meldung | Was du stattdessen schreibst |
|---|---|
| block comments are not allowed | Nutze //-Zeilenkommentare. |
| switch is not allowed | Nutze if und else. |
| break is not allowed | Ändere die Schleife oder packe den Rest ihres Rumpfs in ein if. |
| continue is not allowed | Packe den Rest des Schleifenrumpfs in ein if. |
| classic for loops are not allowed | Nutze for (const item of list). |
| loose equality is not allowed | Nutze ===. |
| loose inequality is not allowed | Nutze !==. |
| exponentiation (**) is not allowed | Nutze math.pow. |
| increment (++) is not allowed | Schreibe count = count + 1. |
| compound assignment is not allowed | Schreibe count = count + value. |
| typeof is not allowed | Vergleiche mit === gegen ein Literal. |
| class is not allowed | Nutze einfache Funktionen. |
| arrow functions are not allowed | Schreibe eine Hilfsfunktion oben in der Datei. |
| async is not allowed | Entferne async. Regeln laufen in einem Schritt. |
| template literals are not allowed | Nutze Text-Literale. |
| spread/rest (...) is not allowed | Schreibe die Einträge einzeln aus. |
| optional chaining (?.) is not allowed | Vergleiche zuerst mit null. |
| nullish coalescing (??) is not allowed | Nutze einen ternären Ausdruck, der mit null vergleicht. |
| throw is not allowed | Verlasse die Regel, ohne zu schreiben. |
| try/catch is not allowed | Entferne try und catch. |
| new is not allowed | Schreibe stattdessen Literale. |
| this is not allowed | Nutze den Parameter self. |
| method calls are not allowed | Rufe nur Host-Funktionen und Hilfsfunktionen derselben Datei auf. |
| default parameter values (=) are not allowed | Gib jedem Parameter dort einen Wert, wo die Regel genutzt wird. |
| assignment targets must be local variables | Baue einen neuen Wert, statt ein Objekt zu ändern. |
| functions but only the action | Entferne export von den Hilfsfunktionen. |
| constructors must be pure and cannot read or write host state | Lies und schreibe in Aktionen und Ledger-Funktionen. |
| is not available until | Eine Ledger-Funktion kann nicht schreiben. Nutze eine Aktion. |
| effectful action helpers are not supported | Lasse 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;| Aufruf | Wo | Was er tut |
|---|---|---|
| element.var | Aktionen, Ledger-Funktionen | Liest einen gespeicherten Wert eines Elements. Caste das Ergebnis mit as number, as string oder as boolean. |
| ledger.set | Aktionen | Setzt ein gespeichertes Feld auf einen Wert. |
| ledger.add | Aktionen | Addiert eine Zahl zu einem gespeicherten Feld. Addiere eine negative Zahl, um abzuziehen. |
| math.floor | Überall | Rundet ab. |
| math.ceil | Überall | Rundet auf. |
| math.round | Überall | Rundet auf die nächste ganze Zahl. |
| math.max | Überall | Gibt die größte seiner Zahlen zurück. |
| math.min | Überall | Gibt die kleinste seiner Zahlen zurück. |
| math.abs | Überall | Gibt den Betrag einer Zahl ohne Vorzeichen zurück. |
| math.pow | Überall | Potenziert eine Basis mit einem Exponenten. |
| math.sqrt | Überall | Gibt die Quadratwurzel zurück. |
| list.add | Aktionen | Fü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.remove | Aktionen | Entfernt den gewählten Eintrag aus einer Liste. Wird mit NOT_IN_LIST abgelehnt. |
| list.count | Aktionen | Gibt zurück, wie viele Einträge eine Liste hält. Nutze es in einer Absicherung vor list.add. |
| offer.where | Aktionen | Baut 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.
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 } }));
}
}- Zeile 2 Brich ab, wenn das Pack schon zwei Einträge hält.
- Zeile 3 Das Angebot erlaubt nur Einträge, die höchstens 3 wiegen.
export function unpack_trinket(self: ElementRef, trinket: ContentRef): void {
list.remove(self, "pack", trinket);
}- 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:
{
"id": "improve",
"name": "Improve a skill",
"source": "rules/improve.ts",
"inputs": {
"skill": { "label": "Skill", "fields": ["lore", "library_use"] }
}
}- Zeile 25 Die Eingabe skill wählt eines dieser Felder nach Namen. Eine Liste braucht mindestens zwei.
export function improve(self: ElementRef, skill: string): void {
ledger.add(self, skill, 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 fieldNur 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.
