メインコンテンツへスキップ

コードフレームワーク

ルールの書き方

コンストラクタ、アクション、レジャー関数を、小さく安全な TypeScript のサブセットで書きます。

3 種類のルールファイル

種類形できること
コンストラクタexport function construct(...inputs): { fields }入力を読み取り、初期値を返します。書き込みはしません。
アクションexport function id(self: ElementRef, ...inputs): voidledger.set と ledger.add で値を変更します。
レジャー関数export function id(currentValue: T, self: ElementRef): T値を読み取り、派生した値を返します。書き込みはしません。

コンストラクタのファイルは関数を 1 つエクスポートします。アクションまたはレジャーのファイルは、各エクスポート名がそのファイルに宣言された ID(アクション ID またはレジャー関数 ID)であれば、複数をエクスポートできます。保存されるフィールド名には、小文字、数字、アンダースコアを使います。

レジャー関数はテキストも返せます。currentValue と結果を string にし、シナリオのレジャーエントリには文字列の 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. 1 行目 テキストを受け取り、テキストを返します。
rules/hurt.ts
export function hurt(self: ElementRef, amount: number): void {
  ledger.add(self, "hp", 0 - amount);
}
  1. 1 行目 最初にエレメントが self として渡され、次にアクション自身の入力が続きます。アクションは void を返します。

ルールファイルには補助関数も置けます。export を付けずに書き、同じファイル内で名前を呼び出します。ファイルがエクスポートするのは、引き続き関数 1 つだけ、または宣言したアクションやレジャー関数だけです。

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. 1-3 行目 ヘルパーです。export はなく、同じファイル内から名前で呼び出されます。
  2. 7 行目 ヘルパーの呼び出しです。

ルールファイルはコードをインポートできません。アクションファイルは、複数のアクションが共有する場合を除き、関数を 1 つだけエクスポートします。余分なエクスポートが単独である場合は、次のエラーになります。

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

補助関数をアクション間で共有するには、それらのアクションで 1 つのファイルを共有します。各エクスポートには実行するアクション名を付け、すべてのエクスポートをアクション ID として宣言し、補助関数には export を付けません。アクションは別のフレームワークに属していてもかまいませんが、アクションから別のアクションは呼び出せません。各関数はコンパイル後のゲームでファイル全体のコピーを持つため、共有ファイルは短く保ってください。

補助関数は、計算して値を返すことしかできません。ledger.set や ledger.add は呼び出せません。書き込む補助関数は、アクションが共有するファイルの中でも拒否されます。

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

補助関数には値を返させ、書き込みは各アクションが行います。次の 2 つのアクションは、1 つの純粋な補助関数を共有しています。

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. 1-3 行目 純粋な補助関数です。export はなく、値を返すだけです。
  2. 8 行目 アクションが、補助関数の返した値を書き込みます。
  3. 14 行目 2 つ目のアクションも同じ補助関数を使います。

子スロットを埋めるコンストラクタは、単純なレコードではなく { values, children } を返します。クイックスタートでは単純なレコードを示し、フレームワークスキーマのページで子の形を示します。

TypeScript のサブセット

ルールはどこでも同じように実行され、必ず終了します。そのため、言語は小さくなっています。

使えるもの使えないもの
明示的な型、const と letimport、class、アロー関数、new、this
if と else、for of、returnwhile ループ、for (;;)、switch、break、continue、async のコード、例外
数値、文字列、真偽値、レコード、配列のリテラルテンプレート文字列、スプレッド、オプショナルチェーン、??、ブロックコメント(// を使ってください)
算術、厳密比較(=== と !==)、論理演算子、三項演算子== と !=、**、++ と +=、typeof、オブジェクトの直接変更、パラメータのデフォルト値
ホスト関数の呼び出し、同じファイルのヘルパー関数の呼び出し、それらが返す値への as キャスト"a".toUpperCase() のようなメソッド呼び出し、Math、Date、JSON、fetch などのグローバル

すべての文はセミコロンで終えてください。次のような間違いは、プロジェクトを検証するときに検出されます。

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

戻り値の型を持つ関数は、すべての経路で値を返す必要があります。

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

チェックで見つかるその他の間違い

各行は、メッセージの一部を引用し、代わりに何を書くべきかを示します。

メッセージ代わりに書くもの
block comments are not allowed// の行コメントを使ってください。
switch is not allowedif と else を使ってください。
break is not allowedループを書き換えるか、本体の残りを if で囲んでください。
continue is not allowedループ本体の残りを if で囲んでください。
classic for loops are not allowedfor (const item of list) を使ってください。
loose equality is not allowed=== を使ってください。
loose inequality is not allowed!== を使ってください。
exponentiation (**) is not allowedmath.pow を使ってください。
increment (++) is not allowedcount = count + 1 と書いてください。
compound assignment is not allowedcount = count + value と書いてください。
typeof is not allowedリテラルと === で比較してください。
class is not allowed単純な関数を使ってください。
arrow functions are not allowedファイルの先頭にヘルパー関数を書いてください。
async is not allowedasync を削除してください。ルールは 1 ステップで実行されます。
template literals are not allowed文字列リテラルを使ってください。
spread/rest (...) is not allowed項目を 1 つずつ書き出してください。
optional chaining (?.) is not allowed先に null と比較してください。
nullish coalescing (??) is not allowednull と比較する三項演算子を使ってください。
throw is not allowed何も書き込まずにルールを終了してください。
try/catch is not allowedtry と catch を削除してください。
new is not allowed代わりにリテラルを書いてください。
this is not allowedself パラメータを使ってください。
method calls are not allowed呼び出せるのは、ホスト関数と同じファイルのヘルパーだけです。
default parameter values (=) are not allowedルールを使う場所で、すべてのパラメータに値を渡してください。
assignment targets must be local variablesオブジェクトを変更せず、新しい値を作ってください。
functions but only the actionヘルパー関数から export を削除してください。
constructors must be pure and cannot read or write host state読み書きは、アクションとレジャー関数で行ってください。
is not available untilレジャー関数は書き込めません。アクションを使ってください。
effectful action helpers are not supported補助関数に値を返させ、書き込みはアクションで行います。

ホスト呼び出し

ルールは、ホスト呼び出しを通じて Fabled にアクセスします。ledger.add(...) のようにそのまま書きます。インポートはしません。

これらのシグネチャで、HostValue はホスト呼び出しが返しうる任意の値です。使う前に as でキャストしてください。AbiValue は、数値、テキスト、真偽値、null、またはそれらのリストやレコードです。省略可能な sticky パラメータは Fabled の内部用なので、指定しないでください。

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;
呼び出し使える場所動作
element.varアクション、レジャー関数エレメントの保存値を読み取ります。結果は as number、as string、as boolean でキャストします。
ledger.setアクション保存フィールドに値を設定します。
ledger.addアクション保存フィールドに数値を加えます。マイナスの数を加えると減ります。
math.floorどこでも切り捨てます。
math.ceilどこでも切り上げます。
math.roundどこでも最も近い整数に丸めます。
math.maxどこでも渡された数値のうち最大のものを返します。
math.minどこでも渡された数値のうち最小のものを返します。
math.absどこでも符号を除いた数値の大きさを返します。
math.powどこでも底を指数で累乗します。
math.sqrtどこでも平方根を返します。
list.addアクション選ばれたエントリをリストに追加します。追加できるものはオファーが決めます。NOT_OFFERED、ALREADY_IN_LIST、LIST_FULL で拒否されます。
list.removeアクション選ばれたエントリをリストから削除します。NOT_IN_LIST で拒否されます。
list.countアクションリストが持つエントリ数を返します。list.add の前のガードで使います。
offer.whereアクションlist.add 用のオファーを作ります。リテラルで書いた条件にフィールドが合うエントリが対象です。

リスト呼び出しは、フレームワークのリストをリテラルのキーで指定します。エントリは ContentRef で、プレイヤーが選ぶエントリを保持する入力です。オファーが取れるのは、offer.where({ weight: { lte: 3 } }) のようなリテラルの条件だけです。各フィールドには eq、ne、gte、lte、in、has を使えます。オファーはエレメントを読めません。キャラクターの値に応じて変えたいときは、list.add の前に if で確認します。

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. 2 行目 パックがすでに 2 つのエントリを持っているなら、止めます。
  2. 3 行目 オファーが許可するのは、weight が 3 以下のエントリだけです。
rules/unpack-trinket.ts
export function unpack_trinket(self: ElementRef, trinket: ContentRef): void {
  list.remove(self, "pack", trinket);
}
  1. 2 行目 選ばれたエントリを削除します。削除だけを行うリストアクションには、受け入れられるステップが 1 つ必要です。

フィールド名はそのまま書きます

アクションでは、フィールドの名前を、コードに書かれた "hp" のような文字列にする必要があります。値から作ることはできません。次のようなルールは失敗します。

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.

そのため、フィールドの一覧がない限り、1 つのアクションで任意のスキルを名前から選んで判定することはできません。スキルごとにアクションを 1 つ書き、それぞれが自分のフィールドを読み取ります。「スキルに対してロールする」レシピに、コピーできる短いアクションがあります。 例外は、フィールドを一覧にした入力です。アクションの入力に ["lore", "library_use"] のような "fields" の一覧を指定すると、ledger.add(self, skill, amount) のように、その入力を名前として渡せます。チェックはアクションを一覧のすべてのフィールドに触れるものとして扱い、サーバーはプレイヤーが選んだフィールドへの書き込みだけを受け付けます。

このアクションは、プレイヤーが選んだ技能を上げます。

schemas/keeper.json
    {
      "id": "improve",
      "name": "Improve a skill",
      "source": "rules/improve.ts",
      "inputs": {
        "skill": { "label": "Skill", "fields": ["lore", "library_use"] }
      }
    }
  1. 25 行目 skill という入力は、これらのフィールドのうち 1 つを名前で選びます。一覧には 2 つ以上が必要です。
rules/improve.ts
export function improve(self: ElementRef, skill: string): void {
  ledger.add(self, skill, 1);
}
  1. 2 行目 skill には、プレイヤーが選んだフィールドの名前が入ります。

fields の各名前は、アクションが書き込める、そのフレームワークの保存フィールドでなければなりません。レジャー関数が導出するフィールドや修正値は拒否されます。フィールドと同名のスキーマ入力に label がある場合、ダイアログはそのラベルを表示します。ない場合は library_use のようなフィールド ID を表示します。たとえば inputs.library_use.label で選択肢を「図書館利用」と表示できます。選択した値、シナリオのパラメーター、{skill} のようなログのプレースホルダーには、引き続きフィールド ID を使います。 ネイティブゲームの文字列の選択肢がすべて書き込み可能なスカラーフィールドの名前である場合も、同じラベルを使います。

fields "skill_total" is not a writable state field

宣言済みのフィールドのみ

アクションが書き込めるのは、コンストラクタが返すフィールドだけです。検証では、見つからないフィールドの名前が示されます。

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

次へ