Frameworks de code
Écrire des règles
Écrivez des constructeurs, des actions et des fonctions de registre dans un petit sous-ensemble sûr de TypeScript.
Trois types de fichiers de règles
| Type | Forme | Ce qu'il peut faire |
|---|---|---|
| Constructeur | export function construct(...inputs): { fields } | Lit ses entrées et retourne les valeurs de départ. N'écrit jamais. |
| Action | export function id(self: ElementRef, ...inputs): void | Modifie les valeurs avec ledger.set et ledger.add. |
| Fonction de registre | export function id(currentValue: T, self: ElementRef): T | Lit des valeurs et retourne une valeur dérivée. N'écrit jamais. |
Un fichier de constructeur exporte une fonction. Un fichier d'action ou de registre peut en exporter plusieurs, si chaque nom exporté est un id déclaré pour ce fichier : un id d'action ou un id de fonction de registre. Les noms de champs stockés utilisent des minuscules, des chiffres et des tirets bas.
Une fonction de registre peut aussi renvoyer du texte. Typez currentValue et le résultat en string, et donnez à l'entrée de registre du scénario un currentValue de texte.
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";
}- Ligne 1 Du texte en entrée, du texte en sortie.
export function hurt(self: ElementRef, amount: number): void {
ledger.add(self, "hp", 0 - amount);
}- Ligne 1 L'Élément vient en premier comme self, puis les entrées propres à l'action. Une action retourne void.
Un fichier de règles peut aussi contenir des fonctions d'aide. Écrivez-les sans export et appelez-les par leur nom depuis le même fichier. Le fichier exporte toujours exactement une fonction, ou seulement les actions ou fonctions de registre qu'il déclare.
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);
}- Lignes 1-3 Une fonction d'aide : pas d'export, et elle est appelée par son nom dans le même fichier.
- Ligne 7 L'appel à la fonction d'aide.
Un fichier de règles ne peut pas importer de code. Un fichier d'action exporte une fonction, sauf si plusieurs actions le partagent. Un export en trop échoue seul avec :
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 itPour partager une fonction d'aide entre actions, faites partager un fichier à ces actions. Nommez chaque export d'après l'action qu'il exécute, déclarez chaque export comme id d'action et laissez la fonction d'aide sans export. Les actions peuvent appartenir à des Frameworks différents, et une action ne peut pas en appeler une autre. Chaque fonction porte une copie de tout son fichier dans le jeu compilé, donc gardez les fichiers partagés courts.
Une fonction d'aide ne peut que calculer et renvoyer une valeur. Elle ne peut pas appeler ledger.set ni ledger.add. Une fonction d'aide qui écrit est refusée, même dans un fichier que des actions partagent :
function wound(self: ElementRef): void {
ledger.add(self, "hp", -1);
}effectful action helpers are not supported until call-target provenance is enforcedLaissez la fonction d'aide renvoyer la valeur, et laissez chaque action faire sa propre écriture. Ces deux actions partagent une fonction d'aide pure :
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));
}- Lignes 1-3 Une fonction d'aide pure : sans export, elle ne fait que renvoyer une valeur.
- Ligne 8 L'action écrit la valeur que la fonction d'aide a renvoyée.
- Ligne 14 La seconde action réutilise la fonction d'aide.
Un constructeur qui remplit des emplacements enfants retourne { values, children } au lieu d'un simple enregistrement. Le démarrage rapide montre l'enregistrement simple, et la page des schémas de Framework montre la forme avec enfants.
Le sous-ensemble de TypeScript
Les règles s'exécutent partout de la même façon, et elles se terminent toujours. C'est pourquoi le langage est petit.
| Autorisé | Interdit |
|---|---|
| Types explicites, const et let | import, classes, fonctions fléchées, new, this |
| if et else, for of, return | Boucles while, for (;;), switch, break, continue, code async, exceptions |
| Littéraux de nombre, de texte, de booléen, d'enregistrement et de tableau | Chaînes de gabarit, spread, chaînage optionnel, ??, commentaires de bloc (utilisez //) |
| Arithmétique, comparaison stricte (=== et !==), opérateurs booléens, ternaires | == et !=, **, ++ et +=, typeof, modifier des objets sur place, valeurs de paramètre par défaut |
| Appels de fonctions hôtes, de fonctions d'aide du même fichier, et as pour convertir leurs valeurs retournées | Appels de méthode comme "a".toUpperCase(), Math, Date, JSON, fetch et autres globaux |
Terminez chaque instruction par un point-virgule. Chacune de ces erreurs est détectée quand vous vérifiez le projet.
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 allowedUne fonction avec un type de retour doit retourner une valeur sur chaque chemin.
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 pathAutres erreurs que la vérification détecte
Chaque ligne cite une partie d'un message et dit quoi écrire à la place.
| Message | Quoi écrire à la place |
|---|---|
| block comments are not allowed | Utilisez des commentaires de ligne //. |
| switch is not allowed | Utilisez if et else. |
| break is not allowed | Changez la boucle, ou placez le reste de son corps dans un if. |
| continue is not allowed | Placez le reste du corps de la boucle dans un if. |
| classic for loops are not allowed | Utilisez for (const item of list). |
| loose equality is not allowed | Utilisez ===. |
| loose inequality is not allowed | Utilisez !==. |
| exponentiation (**) is not allowed | Utilisez math.pow. |
| increment (++) is not allowed | Écrivez count = count + 1. |
| compound assignment is not allowed | Écrivez count = count + value. |
| typeof is not allowed | Comparez avec === à un littéral. |
| class is not allowed | Utilisez des fonctions simples. |
| arrow functions are not allowed | Écrivez une fonction d'aide en haut du fichier. |
| async is not allowed | Retirez async. Les règles s'exécutent en une seule étape. |
| template literals are not allowed | Utilisez des chaînes littérales. |
| spread/rest (...) is not allowed | Écrivez les éléments un par un. |
| optional chaining (?.) is not allowed | Comparez d'abord avec null. |
| nullish coalescing (??) is not allowed | Utilisez un ternaire qui compare avec null. |
| throw is not allowed | Quittez la règle sans écrire. |
| try/catch is not allowed | Retirez try et catch. |
| new is not allowed | Écrivez plutôt des littéraux. |
| this is not allowed | Utilisez le paramètre self. |
| method calls are not allowed | Appelez seulement des fonctions hôtes et des fonctions d'aide du même fichier. |
| default parameter values (=) are not allowed | Donnez une valeur à chaque paramètre là où la règle est utilisée. |
| assignment targets must be local variables | Construisez une nouvelle valeur au lieu de modifier un objet. |
| functions but only the action | Retirez export des fonctions d'aide. |
| constructors must be pure and cannot read or write host state | Lisez et écrivez dans les actions et les fonctions de registre. |
| is not available until | Une fonction de registre ne peut pas écrire. Utilisez une action. |
| effectful action helpers are not supported | Faites renvoyer une valeur à la fonction d'aide et écrivez-la dans l'action. |
Appels hôtes
Les règles atteignent Fabled par des appels hôtes. Vous les écrivez tels quels, comme ledger.add(...). Vous ne les importez jamais.
Dans ces signatures, HostValue est toute valeur qu'un appel hôte peut renvoyer, donc convertissez-la avec as avant de l'utiliser. AbiValue est un nombre, un texte, un booléen ou null, ou une liste ou un enregistrement de ces valeurs. Un paramètre sticky facultatif est réservé à Fabled, donc omettez-le.
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;| Appel | Où | Ce qu'il fait |
|---|---|---|
| element.var | Actions, fonctions de registre | Lit une valeur stockée d'un Élément. Convertissez le résultat avec as number, as string ou as boolean. |
| ledger.set | Actions | Donne une valeur à un champ stocké. |
| ledger.add | Actions | Ajoute un nombre à un champ stocké. Ajoutez un nombre négatif pour soustraire. |
| math.floor | Partout | Arrondit vers le bas. |
| math.ceil | Partout | Arrondit vers le haut. |
| math.round | Partout | Arrondit au nombre entier le plus proche. |
| math.max | Partout | Retourne le plus grand de ses nombres. |
| math.min | Partout | Retourne le plus petit de ses nombres. |
| math.abs | Partout | Retourne la taille d'un nombre sans son signe. |
| math.pow | Partout | Élève une base à un exposant. |
| math.sqrt | Partout | Retourne la racine carrée. |
| list.add | Actions | Ajoute l'entrée choisie à une liste. L'offre décide de ce qui peut être ajouté. Refusé avec NOT_OFFERED, ALREADY_IN_LIST ou LIST_FULL. |
| list.remove | Actions | Retire l'entrée choisie d'une liste. Refusé avec NOT_IN_LIST. |
| list.count | Actions | Retourne le nombre d'entrées d'une liste. Utilisez-le dans une garde avant list.add. |
| offer.where | Actions | Construit l'offre pour list.add : les entrées dont les champs correspondent à des conditions que vous écrivez comme littéraux. |
Un appel de liste nomme une liste du Framework par une clé littérale, et l'entrée est un ContentRef, une entrée qui contient l'entrée choisie par le joueur. Une offre accepte seulement des conditions littérales, comme offer.where({ weight: { lte: 3 } }). Chaque champ accepte eq, ne, gte, lte, in ou has. Une offre ne peut pas lire l'Élément. Pour dépendre d'une valeur du personnage, testez-la avec if avant 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 } }));
}
}- Ligne 2 Arrêtez-vous quand le sac contient déjà deux entrées.
- Ligne 3 L'offre n'autorise que les entrées de poids 3 ou moins.
export function unpack_trinket(self: ElementRef, trinket: ContentRef): void {
list.remove(self, "pack", trinket);
}- Ligne 2 Retirez l'entrée choisie. Une action de liste qui ne fait que retirer a besoin d'une étape acceptée.
Les noms de champs s'écrivent en toutes lettres
Dans une action, le nom d'un champ doit être un texte écrit dans le code, comme "hp". Il ne peut pas venir d'une valeur. Une règle comme celle-ci échoue :
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.Sans liste de champs, une même action ne peut donc pas tester n'importe quelle compétence par son nom. Écrivez une action pour chaque compétence, et laissez chacune lire son propre champ. La recette Jet contre une compétence montre une courte action à copier. L'exception est une entrée qui énumère ses champs. Donnez à l'entrée de l'action une liste "fields", comme ["lore", "library_use"], et l'action peut passer cette entrée comme nom, comme dans ledger.add(self, skill, amount). La vérification traite l'action comme si elle touchait chaque champ listé, et le serveur n'accepte qu'une écriture dans le champ choisi par le joueur.
Cette action augmente la compétence que le joueur choisit :
{
"id": "improve",
"name": "Improve a skill",
"source": "rules/improve.ts",
"inputs": {
"skill": { "label": "Skill", "fields": ["lore", "library_use"] }
}
}- Ligne 25 L'entrée skill choisit l'un de ces champs par son nom. Une liste en demande au moins deux.
export function improve(self: ElementRef, skill: string): void {
ledger.add(self, skill, 1);
}- Ligne 2 skill contient le nom du champ que le joueur a choisi.
Chaque nom de fields doit être un champ stocké du Framework qu'une action peut écrire. Un champ qu'une fonction de registre dérive, ou un modificateur, est refusé. Si le champ possède une entrée de schéma du même nom avec un label, la boîte de dialogue affiche ce libellé ; sinon, elle affiche l'identifiant, comme library_use. Par exemple, inputs.library_use.label peut nommer le choix Recherche en bibliothèque. La valeur sélectionnée, les paramètres des scénarios et les espaces réservés du journal comme {skill} utilisent toujours l'identifiant du champ. Toute liste native de choix textuels dont les valeurs nomment toutes des champs scalaires modifiables utilise les mêmes libellés.
fields "skill_total" is not a writable state fieldSeulement les champs déclarés
Une action ne peut écrire que les champs que le constructeur retourne. La vérification nomme le champ introuvable.
Action "keeper.heal" calls ledger.set(self, "mana", ...), and "mana" is not declared on this schema. Declared scalar fields: hp, max_hp, name.
