Pular para o conteúdo principal

Code frameworks

Escrevendo regras

Escreva constructors, ações e funções de ledger em um subconjunto pequeno e seguro de TypeScript.

Três tipos de arquivo de regras

TipoFormatoO que pode fazer
Constructorexport function construct(...inputs): { fields }Lê suas entradas e retorna os valores iniciais. Nunca escreve.
Açãoexport function id(self: ElementRef, ...inputs): voidMuda valores com ledger.set e ledger.add.
Função de ledgerexport function id(currentValue: T, self: ElementRef): TLê valores e retorna um valor derivado. Nunca escreve.

Um arquivo de construtor exporta uma função. Um arquivo de ação ou de ledger pode exportar várias, se cada nome exportado for um id declarado para esse arquivo: um id de ação ou um id de função de ledger. Os nomes dos campos guardados usam minúsculas, dígitos e sublinhados.

Uma função de ledger também pode devolver texto. Declare currentValue e o resultado como string, e dê à entrada de ledger do cenário um currentValue de texto.

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. Linha 1 Entra texto, sai texto.
rules/hurt.ts
export function hurt(self: ElementRef, amount: number): void {
  ledger.add(self, "hp", 0 - amount);
}
  1. Linha 1 O Element vem primeiro como self, depois as próprias entradas da ação. Uma ação retorna void.

Um arquivo de regras também pode conter funções auxiliares. Escreva-as sem export e chame-as pelo nome no mesmo arquivo. O arquivo continua a exportar exatamente uma função, ou apenas as ações ou funções de ledger que declara.

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. Linhas 1-3 Uma função auxiliar: sem export, e chamada pelo nome no mesmo arquivo.
  2. Linha 7 A chamada à função auxiliar.

Um arquivo de regras não pode importar código. Um arquivo de ação exporta uma função, a menos que várias ações o partilhem. Uma exportação a mais, sozinha, falha com:

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

Para partilhar uma função auxiliar entre ações, faça essas ações partilharem um arquivo. Dê a cada exportação o nome da ação que ela executa, declare cada exportação como id de ação e deixe a função auxiliar sem export. As ações podem pertencer a Frameworks diferentes, e uma ação não pode chamar outra. Cada função leva uma cópia de todo o arquivo no jogo compilado, então mantenha os arquivos partilhados curtos.

Uma função auxiliar só pode calcular e devolver um valor. Ela não pode chamar ledger.set nem ledger.add. Uma função auxiliar que escreve é recusada, mesmo em um arquivo que ações compartilham:

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

Deixe a função auxiliar devolver o valor, e deixe cada ação fazer a sua própria escrita. Estas duas ações compartilham uma função auxiliar pura:

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. Linhas 1-3 Uma função auxiliar pura: sem export, e só devolve um valor.
  2. Linha 8 A ação escreve o valor que a função auxiliar devolveu.
  3. Linha 14 A segunda ação reutiliza a função auxiliar.

Um constructor que preenche espaços filhos retorna { values, children } em vez de um record simples. O início rápido mostra o record simples, e a página Schemas de Framework mostra a forma com filhos.

O subconjunto de TypeScript

As regras rodam do mesmo jeito em qualquer lugar e sempre terminam. Por isso a linguagem é pequena.

PermitidoNão permitido
Tipos explícitos, const e letimport, classes, arrow functions, new, this
if e else, for of, returnloops while, for (;;), switch, break, continue, código async, exceções
Literais de número, texto, booleano, record e arrayTemplate strings, spread, optional chaining, ??, comentários de bloco (use //)
Aritmética, comparação estrita (=== e !==), operadores booleanos, ternários== e !=, **, ++ e +=, typeof, mudar objetos no lugar, valores padrão de parâmetros
Chamadas a funções do host, funções auxiliares do mesmo arquivo e casts com as dos valores que elas retornamChamadas de método como "a".toUpperCase(), Math, Date, JSON, fetch e outros globais

Termine cada instrução com ponto e vírgula. Cada um destes erros é detectado quando você verifica o projeto.

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

Uma função com tipo de retorno deve retornar um valor em todos os caminhos.

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

Mais erros que a verificação detecta

Cada linha cita parte de uma mensagem e diz o que escrever no lugar.

MensagemO que escrever no lugar
block comments are not allowedUse comentários de linha com //.
switch is not allowedUse if e else.
break is not allowedMude o loop, ou coloque o resto do corpo dele dentro de um if.
continue is not allowedColoque o resto do corpo do loop dentro de um if.
classic for loops are not allowedUse for (const item of list).
loose equality is not allowedUse ===.
loose inequality is not allowedUse !==.
exponentiation (**) is not allowedUse math.pow.
increment (++) is not allowedEscreva count = count + 1.
compound assignment is not allowedEscreva count = count + value.
typeof is not allowedCompare com === contra um literal.
class is not allowedUse funções simples.
arrow functions are not allowedEscreva uma função auxiliar no topo do arquivo.
async is not allowedRemova async. As regras rodam em um único passo.
template literals are not allowedUse literais de texto.
spread/rest (...) is not allowedEscreva os itens um a um.
optional chaining (?.) is not allowedCompare com null primeiro.
nullish coalescing (??) is not allowedUse um ternário que compara com null.
throw is not allowedSaia da regra sem escrever.
try/catch is not allowedRemova try e catch.
new is not allowedEscreva literais no lugar.
this is not allowedUse o parâmetro self.
method calls are not allowedChame só funções do host e funções auxiliares do mesmo arquivo.
default parameter values (=) are not allowedDê um valor a cada parâmetro onde a regra é usada.
assignment targets must be local variablesMonte um valor novo em vez de mudar um objeto.
functions but only the actionRemova export das funções auxiliares.
constructors must be pure and cannot read or write host stateLeia e escreva em ações e funções de ledger.
is not available untilUma função de ledger não pode escrever. Use uma ação.
effectful action helpers are not supportedFaça a função auxiliar devolver um valor e escreva-o na ação.

Chamadas ao host

As regras chegam ao Fabled por chamadas ao host. Você as escreve como elas são, como ledger.add(...). Você nunca as importa.

Nestas assinaturas, HostValue é qualquer valor que uma chamada ao host pode devolver, então converta-o com as antes de usá-lo. AbiValue é um número, texto, booleano ou null, ou uma lista ou um registro desses. Um parâmetro opcional sticky é de uso do próprio Fabled, então deixe-o de fora.

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;
ChamadaOndeO que faz
element.varAções, funções de ledgerLê um valor guardado de um Element. Converta o resultado com as number, as string ou as boolean.
ledger.setAçõesDefine um campo guardado com um valor.
ledger.addAçõesSoma um número a um campo guardado. Some um número negativo para subtrair.
math.floorEm qualquer lugarArredonda para baixo.
math.ceilEm qualquer lugarArredonda para cima.
math.roundEm qualquer lugarArredonda para o número inteiro mais próximo.
math.maxEm qualquer lugarDevolve o maior dos seus números.
math.minEm qualquer lugarDevolve o menor dos seus números.
math.absEm qualquer lugarDevolve o tamanho de um número sem o sinal.
math.powEm qualquer lugarEleva uma base a um expoente.
math.sqrtEm qualquer lugarDevolve a raiz quadrada.
list.addAçõesAdiciona o item escolhido a uma lista. A oferta decide o que pode ser adicionado. Recusado com NOT_OFFERED, ALREADY_IN_LIST ou LIST_FULL.
list.removeAçõesRemove o item escolhido de uma lista. Recusado com NOT_IN_LIST.
list.countAçõesDevolve quantos itens uma lista guarda. Use em uma proteção antes de list.add.
offer.whereAçõesCria a oferta para list.add: os itens cujos campos combinam com condições que você escreve como literais.

Uma chamada de lista nomeia uma lista do Framework por uma chave literal, e o item é um ContentRef, uma entrada que guarda o item que o jogador escolhe. Uma oferta recebe só condições literais, como offer.where({ weight: { lte: 3 } }). Cada campo aceita eq, ne, gte, lte, in ou has. Uma oferta não pode ler o Element. Para depender de um valor do personagem, teste com if antes de 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. Linha 2 Pare quando o pacote já guarda dois itens.
  2. Linha 3 A oferta permite apenas itens que pesam 3 ou menos.
rules/unpack-trinket.ts
export function unpack_trinket(self: ElementRef, trinket: ContentRef): void {
  list.remove(self, "pack", trinket);
}
  1. Linha 2 Remova o item escolhido. Uma ação de lista que só remove precisa de um passo aceito.

Nomes de campo são escritos por extenso

Em uma ação, o nome de um campo deve ser um texto escrito no código, como "hp". Ele não pode vir de um valor. Uma regra como esta falha:

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.

Então, sem uma lista de campos, uma ação não pode testar qualquer perícia pelo nome. Escreva uma ação para cada perícia, e deixe cada uma ler o seu próprio campo. A receita Rolar contra uma perícia mostra uma ação curta para copiar. A exceção é uma entrada que lista os seus campos. Dê à entrada da ação uma lista "fields", como ["lore", "library_use"], e a ação pode passar essa entrada como nome, como em ledger.add(self, skill, amount). A verificação trata a ação como se tocasse em todos os campos da lista, e o servidor só aceita uma escrita no campo que o jogador escolheu.

Esta ação aumenta a perícia que o jogador escolher:

schemas/keeper.json
    {
      "id": "improve",
      "name": "Improve a skill",
      "source": "rules/improve.ts",
      "inputs": {
        "skill": { "label": "Skill", "fields": ["lore", "library_use"] }
      }
    }
  1. Linha 25 A entrada skill escolhe um destes campos pelo nome. Uma lista precisa de pelo menos dois.
rules/improve.ts
export function improve(self: ElementRef, skill: string): void {
  ledger.add(self, skill, 1);
}
  1. Linha 2 skill contém o nome do campo que o jogador escolheu.

Cada nome em fields deve ser um campo armazenado do Framework que uma ação possa escrever. Um campo que uma função de ledger deriva, ou um modificador, é recusado. Se o campo tiver uma entrada de esquema com o mesmo nome e um label, o diálogo mostra esse rótulo; caso contrário, mostra o identificador, como library_use. Por exemplo, inputs.library_use.label pode chamar a opção Uso de biblioteca. O valor selecionado, os parâmetros dos cenários e os marcadores do registro, como {skill}, continuam usando o identificador do campo. Qualquer lista nativa de opções de texto cujos valores sejam todos nomes de campos escalares graváveis usa os mesmos rótulos.

fields "skill_total" is not a writable state field

Só campos declarados

Uma ação só pode escrever os campos que o constructor retorna. A verificação mostra o nome do campo que não encontrou.

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

Próximo