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

コードフレームワーク

Lantern を本格的なゲームに育てる

クイックスタートのゲームに、選択肢、派生値、子フレームワーク、リストを加え、各ステップをシナリオで確かめます。

始める前に

クイックスタートで作ったプロジェクトから始めます。各ステップでは、作成または変更するファイルをすべて示します。抜粋は変わる部分を示し、展開するとファイル全体を見られます。ステップごとに「検証してプレビュー」を選びます。

4 つのステップ

  1. 選択肢を加える

    2 つの options を持つ Calling の入力を加えます。

    schemas/keeper.json
        "calling": {
          "label": "Calling",
          "options": ["Guard", "Scout"],
          "defaultValue": "Guard"
        }
    1. 11 行目 固定の選択肢の一覧です。default はその中のどれかにします。

    コンストラクタは新しい入力を受け取り、保存フィールドとして返します。

    rules/keeper.ts
    export function construct(
      name: string,
      max_hp: number,
      calling: string,
    ): { name: string; max_hp: number; hp: number; calling: string } {
      return { name: name, max_hp: max_hp, hp: max_hp, calling: calling };
    }
    1. 4 行目 新しい入力です。
    2. 5-6 行目 calling は保存フィールドになりました。

    両方のシナリオに、parameters と expectedValues で新しい値が必要です。まず new-keeper.json から始めます。

    scenarios/new-keeper.json
    {
      "id": "new-keeper",
      "framework": "keeper",
      "parameters": { "name": "Mira", "max_hp": 10, "calling": "Guard" },
      "expectedValues": {
        "name": "Mira",
        "max_hp": 10,
        "hp": 10,
        "calling": "Guard"
      }
    }
    1. 4 行目 渡す入力です。
    2. 9 行目 期待する保存値です。

    次に、heal.json にも同じ 2 つの変更を加えます。

    scenarios/heal.json
      "parameters": { "name": "Mira", "max_hp": 10, "calling": "Guard" },
      "expectedValues": {
        "name": "Mira",
        "max_hp": 10,
        "hp": 10,
        "calling": "Guard"
      },
    1. 4 行目 同じ入力です。
    2. 9 行目 同じ保存値です。

    「検証してプレビュー」を選びます。次へ進む前に、検証が通るようにしてください。

    プロジェクトに、次のファイルができました。上の抜粋を展開すると、ファイル全体を見られます。

    manifest/project.json
    schemas/keeper.json
    rules/heal.ts
    rules/hurt.ts
    rules/keeper.ts
    layouts/keeper.json
    scenarios/heal.json
    scenarios/new-keeper.json
  2. 値を派生させる

    レジャー関数が calling から Guard を導きます。スキーマで宣言します。

    schemas/keeper.json
      "ledgerFunctions": [
        {
          "id": "guard",
          "name": "Guard",
          "field": "guard",
          "source": "rules/guard.ts"
        }
      ]
    1. 24-27 行目 id と name は関数を示し、field は関数が埋める保存値、source はそのルールファイルです。

    rules/guard.ts を作成します。

    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. 5-7 行目 レジャー関数は calling を読み、派生値を返します。

    コンストラクタは、開始値 0 の guard フィールドも返します。実際の値は、レジャー関数が入れます。

    rules/keeper.ts
    export function construct(
      name: string,
      max_hp: number,
      calling: string,
    ): { name: string; max_hp: number; hp: number; calling: string; guard: number } {
      return { name: name, max_hp: max_hp, hp: max_hp, calling: calling, guard: 0 };
    }
    1. 5-6 行目 guard は戻り値に含まれ、0 から始まります。

    シナリオが関数を確認します。new-keeper.json の期待値に guard を加え、関数を呼び出す ledger の行も加えます。

    scenarios/new-keeper.json
    {
      "id": "new-keeper",
      "framework": "keeper",
      "parameters": { "name": "Mira", "max_hp": 10, "calling": "Guard" },
      "expectedValues": {
        "name": "Mira",
        "max_hp": 10,
        "hp": 10,
        "calling": "Guard",
        "guard": 3
      },
      "ledger": [{ "function": "guard", "currentValue": 0, "expected": 3 }]
    }
    1. 10 行目 レジャー関数が実行されたあとの、派生値です。
    2. 12 行目 現在値 0 で guard 関数を呼び出し、3 を期待します。

    同じ期待値を heal.json にも加えます。

    scenarios/heal.json
      "parameters": { "name": "Mira", "max_hp": 10, "calling": "Guard" },
      "expectedValues": {
        "name": "Mira",
        "max_hp": 10,
        "hp": 10,
        "calling": "Guard",
        "guard": 3
      },
    1. 10 行目 このシナリオでも、派生値は 3 のままです。

    Guard をシートに表示するには、requiredBindings に載せ、それを使うテキストノードを加えます。

    layouts/keeper.json
          { "binding": "element.attributes.guard", "expectedType": "number" }
    1. 15 行目 新しいフィールドのバインディングです。
    layouts/keeper.json
          { "kind": "text", "text": "Guard: {{element.attributes.guard}}" },
    1. 32 行目 値を表示するテキストノードです。

    「検証してプレビュー」を選びます。次へ進む前に、検証が通るようにしてください。

    プロジェクトに、次のファイルができました。上の抜粋を展開すると、ファイル全体を見られます。

    manifest/project.json
    schemas/keeper.json
    rules/guard.ts
    rules/heal.ts
    rules/hurt.ts
    rules/keeper.ts
    layouts/keeper.json
    scenarios/heal.json
    scenarios/new-keeper.json
  3. 子フレームワークを加える

    Keeper は Lamp を持ちます。Lamp は 2 つ目のフレームワークで、独自のスキーマ、コンストラクタ、フレーム、シナリオを持ちます。そのスキーマとシナリオをマニフェストに載せます。

    manifest/project.json
    {
      "formatVersion": "fabled.game-project/v1",
      "projectId": "game-your-game-id",
      "projectKind": "GAME",
      "compilerProfile": "native-game-v1",
      "name": "Lantern Keepers",
      "entryFramework": "keeper",
      "frameworks": ["schemas/keeper.json", "schemas/lamp.json"],
      "scenarios": [
        "scenarios/new-keeper.json",
        "scenarios/heal.json",
        "scenarios/new-lamp.json"
      ]
    }
    1. 8 行目 Lamp のスキーマが、Keeper のスキーマに加わります。
    2. 9-13 行目 Lamp のシナリオが、Keeper のシナリオに加わります。

    Lamp のスキーマを作成します。

    schemas/lamp.json
    {
      "id": "lamp",
      "name": "Lamp",
      "elementType": "item",
      "constructor": "rules/lamp.ts",
      "inputs": {
        "name": { "label": "Name" },
        "oil": { "label": "Oil", "defaultValue": 5 }
      },
      "frames": [{ "id": "card", "source": "layouts/lamp.json", "default": true }]
    }
    1. 6-9 行目 子の入力です。子にも name が必要です。
    2. 10 行目 子には独自のフレームがあります。

    そのコンストラクタ、フレーム、シナリオを作成します。

    rules/lamp.ts
    export function construct(name: string, oil: number): { name: string; oil: number } {
      return { name: name, oil: oil };
    }
    1. 1 行目 コンストラクタは、入力を保存値として返します。
    layouts/lamp.json
    {
      "version": "1.0",
      "metadata": {
        "name": "Lamp card",
        "bindingMode": "bound",
        "frameworkRef": "lamp",
        "context": "BLADE",
        "template": "standard-blade"
      },
      "dataContract": {
        "requiredBindings": [
          { "binding": "element.attributes.name", "expectedType": "string" },
          { "binding": "element.attributes.oil", "expectedType": "number" }
        ],
        "optionalBindings": [],
        "actions": [],
        "regions": []
      },
      "layout": {
        "kind": "container",
        "children": [
          { "kind": "text", "text": "{{element.attributes.name}}" },
          { "kind": "text", "text": "Oil: {{element.attributes.oil}}" }
        ]
      }
    }
    1. 11-14 行目 フレームが表示するすべてのフィールドを、バインディングとして載せます。
    scenarios/new-lamp.json
    {
      "id": "new-lamp",
      "framework": "lamp",
      "parameters": { "name": "Lantern", "oil": 5 },
      "expectedValues": { "name": "Lantern", "oil": 5 }
    }
    1. 4 行目 Lamp を単独で作り、その値を確認します。

    Keeper では、非表示の入力がフレームワーク参照を保持し、childFrameworks がスロットとフレームワークを対応付けます。

    schemas/keeper.json
        "lamp": { "label": "Lamp", "defaultValue": "lamp", "hidden": true }
    1. 14 行目 非表示の入力はウィザードに表示されません。
    schemas/keeper.json
      "childFrameworks": { "lamp": "lamp" }
    1. 31 行目 スロット lamp を、id が lamp のフレームワークに対応付けます。

    コンストラクタは、自身の値と並べて Lamp の入力を返します。

    rules/keeper.ts
    export function construct(
      name: string,
      max_hp: number,
      calling: string,
      lamp: FrameworkRef,
    ): {
      values: { name: string; max_hp: number; hp: number; calling: string; guard: number };
      children: { lamp: { framework: FrameworkRef; params: { name: string; oil: number } } };
    } {
      return {
        values: { name: name, max_hp: max_hp, hp: max_hp, calling: calling, guard: 0 },
        children: { lamp: { framework: lamp, params: { name: "Lantern", oil: 5 } } },
      };
    }
    1. 5 行目 lamp は FrameworkRef です。入力のフレームワーク id から、プラットフォームがそれを作ります。
    2. 6-9 行目 戻り値の型は、保存値と子スロットを並べます。
    3. 12 行目 子スロットです。どのフレームワークを作るか、渡す入力は何かを示します。

    Keeper のシナリオに lamp の入力を渡し、コンストラクタが作る Lamp を期待します。

    scenarios/new-keeper.json
    {
      "id": "new-keeper",
      "framework": "keeper",
      "parameters": {
        "name": "Mira",
        "max_hp": 10,
        "calling": "Guard",
        "lamp": "lamp"
      },
      "expectedValues": {
        "name": "Mira",
        "max_hp": 10,
        "hp": 10,
        "calling": "Guard",
        "guard": 3
      },
      "ledger": [{ "function": "guard", "currentValue": 0, "expected": 3 }],
      "expectedChildren": { "lamp": { "name": "Lantern", "oil": 5 } }
    }
    1. 8 行目 非表示の入力です。
    2. 18 行目 コンストラクタが作る子と、その値です。
    scenarios/heal.json
      "parameters": {
        "name": "Mira",
        "max_hp": 10,
        "calling": "Guard",
        "lamp": "lamp"
      },
      "expectedValues": {
        "name": "Mira",
        "max_hp": 10,
        "hp": 10,
        "calling": "Guard",
        "guard": 3
      },
    1. 8 行目 heal.json の非表示の入力です。
    scenarios/heal.json
      "expectedChildren": { "lamp": { "name": "Lantern", "oil": 5 } }
    1. 30 行目 heal.json で期待する子です。

    「検証してプレビュー」を選びます。次へ進む前に、検証が通るようにしてください。

    プロジェクトに、次のファイルができました。上の抜粋を展開すると、ファイル全体を見られます。

    manifest/project.json
    schemas/keeper.json
    schemas/lamp.json
    rules/guard.ts
    rules/heal.ts
    rules/hurt.ts
    rules/keeper.ts
    rules/lamp.ts
    layouts/keeper.json
    layouts/lamp.json
    scenarios/heal.json
    scenarios/new-keeper.json
    scenarios/new-lamp.json
  4. リストと拒否されるアクションを加える

    リストは、キャラクターがプレイ中に得るコンテンツを保持します。Pack は Trinket を保持します。Trinket は 2 つ目のフレームワークで、そのプリセットがコンテンツです。そのスキーマと 2 つのシナリオをマニフェストに載せます。

    manifest/project.json
    {
      "formatVersion": "fabled.game-project/v1",
      "projectId": "game-your-game-id",
      "projectKind": "GAME",
      "compilerProfile": "native-game-v1",
      "name": "Lantern Keepers",
      "entryFramework": "keeper",
      "frameworks": [
        "schemas/keeper.json",
        "schemas/lamp.json",
        "schemas/trinket.json"
      ],
      "scenarios": [
        "scenarios/new-keeper.json",
        "scenarios/heal.json",
        "scenarios/new-lamp.json",
        "scenarios/new-trinket.json",
        "scenarios/pack.json"
      ]
    }
    1. 8-12 行目 Trinket のスキーマが、ほかの 2 つに加わります。
    2. 17-18 行目 Trinket のシナリオと、pack のシナリオです。

    Trinket のスキーマを作成します。そのフレームには、ゲーム内のほかのフレームが使っていない id が必要です。

    schemas/trinket.json
    {
      "id": "trinket",
      "name": "Trinket",
      "elementType": "item",
      "constructor": "rules/trinket.ts",
      "inputs": {
        "name": { "label": "Name" },
        "weight": { "label": "Weight", "defaultValue": 1, "min": 0, "max": 5 }
      },
      "frames": [
        {
          "id": "trinket-card",
          "source": "layouts/trinket.json",
          "default": true
        }
      ],
      "content": [
        {
          "id": "rope",
          "name": "Rope",
          "parameters": { "name": "Rope", "weight": 2 }
        },
        {
          "id": "anvil",
          "name": "Anvil",
          "parameters": { "name": "Anvil", "weight": 5 }
        }
      ]
    }
    1. 8 行目 上限と下限のある weight です。オファーがそれを読みます。
    2. 10-16 行目 Trinket には独自のフレームがあります。
    3. 17-28 行目 プレイヤーが詰められるプリセットです。シナリオが選ぶのは、その id です。

    そのコンストラクタ、フレーム、シナリオを作成します。

    rules/trinket.ts
    export function construct(
      name: string,
      weight: number,
    ): { name: string; weight: number } {
      return { name: name, weight: weight };
    }
    1. 5 行目 2 つの保存値を返します。
    layouts/trinket.json
    {
      "version": "1.0",
      "metadata": {
        "name": "Trinket card",
        "bindingMode": "bound",
        "frameworkRef": "trinket",
        "context": "BLADE",
        "template": "standard-blade"
      },
      "dataContract": {
        "requiredBindings": [
          { "binding": "element.attributes.name", "expectedType": "string" },
          { "binding": "element.attributes.weight", "expectedType": "number" }
        ],
        "optionalBindings": [],
        "actions": [],
        "regions": []
      },
      "layout": {
        "kind": "container",
        "children": [
          { "kind": "text", "text": "{{element.attributes.name}}" },
          { "kind": "text", "text": "Weight: {{element.attributes.weight}}" }
        ]
      }
    }
    1. 22-23 行目 name と weight を表示します。
    scenarios/new-trinket.json
    {
      "id": "new-trinket",
      "framework": "trinket",
      "parameters": { "name": "Rope", "weight": 2 },
      "expectedValues": { "name": "Rope", "weight": 2 }
    }
    1. 4 行目 Trinket を単独で作り、その値を確認します。

    Keeper のスキーマで、アクションとリストを宣言します。

    schemas/keeper.json
      "actions": [
        { "id": "hurt", "name": "Take damage", "source": "rules/hurt.ts" },
        { "id": "heal", "name": "Heal", "source": "rules/heal.ts" },
        {
          "id": "pack_trinket",
          "name": "Pack a trinket",
          "source": "rules/pack-trinket.ts"
        }
      ],
    1. 19-23 行目 新しいアクションです。
    schemas/keeper.json
      "lists": { "pack": { "label": "Pack", "of": "trinket", "max": 4 } }
    1. 37 行目 Pack のリストです。ラベル、保持するコンテンツのフレームワーク、サイズを示します。

    アクションのルールファイルを作成します。

    rules/pack-trinket.ts
    export function pack_trinket(self: ElementRef, trinket: ContentRef): void {
      list.add(self, "pack", trinket, offer.where({ weight: { lte: 3 } }));
    }
    1. 1 行目 選ばれた trinket は ContentRef として届きます。
    2. 2 行目 オファーが、詰められるものを決めます。重さが 3 以下の trinket だけです。

    オファーは拒否できるので、両方の結果をテストします。scenarios/pack.json を作成します。

    scenarios/pack.json
    {
      "id": "pack-trinkets",
      "framework": "keeper",
      "parameters": {
        "name": "Mira",
        "max_hp": 10,
        "calling": "Guard",
        "lamp": "lamp"
      },
      "expectedValues": {
        "name": "Mira",
        "max_hp": 10,
        "hp": 10,
        "calling": "Guard",
        "guard": 3
      },
      "expectedChildren": { "lamp": { "name": "Lantern", "oil": 5 } },
      "actions": [
        {
          "action": "pack_trinket",
          "parameters": {},
          "choose": { "trinket": "rope" },
          "expectedWrites": [{ "op": "list.add", "key": "pack", "value": "rope" }]
        },
        {
          "action": "pack_trinket",
          "parameters": {},
          "choose": { "trinket": "anvil" },
          "expectedRefusal": "NOT_OFFERED",
          "expectedWrites": []
        }
      ],
      "expectedLists": { "pack": ["rope"] }
    }
    1. 19-24 行目 受け入れ: ロープは十分軽いので、追加されます。
    2. 25-31 行目 拒否: 金床は重すぎます。このステップは、拒否 NOT_OFFERED と書き込みなしを期待します。
    3. 33 行目 最後のステップのあと、Pack にはロープだけが入っています。

    リストを表示するフレームは version 2.0 を使います。version を変更し、dataContract でアクションとリージョンを宣言して、ボタンとリージョンを描画します。

    layouts/keeper.json
      "version": "2.0",
    1. 2 行目 リストのあるフレームは version 2.0 を使います。
    layouts/keeper.json
        "actions": [
          { "name": "hurt", "functionRef": "hurt" },
          { "name": "heal", "functionRef": "heal" },
          { "name": "pack_trinket", "functionRef": "pack_trinket" }
        ],
        "regions": [{ "name": "pack", "listBinding": "element.lists.pack" }]
    1. 21 行目 新しいアクションのエイリアスです。
    2. 23 行目 リストのリージョンを宣言します。
    layouts/keeper.json
          {
            "kind": "button",
            "label": "Pack a trinket",
            "actionRef": "pack_trinket"
          },
          {
            "kind": "region",
            "id": "pack_list",
            "listBinding": "{{element.lists.pack}}",
            "emptyText": "Your pack is empty."
          }
    1. 37-39 行目 アクションを実行するボタンです。
    2. 42 行目 リージョンはリストを描きます。
    3. 44 行目 描画するリストです。
    4. 45 行目 リストが空の間に表示されます。

    「検証してプレビュー」を選びます。次へ進む前に、検証が通るようにしてください。

    プロジェクトに、次のファイルができました。上の抜粋を展開すると、ファイル全体を見られます。

    manifest/project.json
    schemas/keeper.json
    schemas/lamp.json
    schemas/trinket.json
    rules/guard.ts
    rules/heal.ts
    rules/hurt.ts
    rules/keeper.ts
    rules/lamp.ts
    rules/pack-trinket.ts
    rules/trinket.ts
    layouts/keeper.json
    layouts/lamp.json
    layouts/trinket.json
    scenarios/heal.json
    scenarios/new-keeper.json
    scenarios/new-lamp.json
    scenarios/new-trinket.json
    scenarios/pack.json

作ったもの

Keeper に、選択肢、派生値、子フレームワーク、拒否されるアクション付きのリスト、そしてそれぞれを確かめるシナリオができました。レシピでは、より小さな変更を単独で紹介します。