レシピシステム設計

概要

本ドキュメントは、recipes(GameElementDefinition.md 13 節)の文法を使って、2つ以上のアイテムを組み合わせて 新しいアイテムを作成する仕組み(道具の生成・建物の建築の両方に使う)をどう成立させるかをまとめたものです。 recipes/steps/requires 自体の文法は GameElementDefinition.md 13 節、いつ作れるように なるか(解放条件)は SkillSystem.md を参照してください。

作りかけは「製作中オブジェクト」という普通の物です(1 節)。レシピから自動生成される、完成品とは 別の object_def で、材料を受け取る箱は slots の枠そのもの(3 節)、投入は「入れ物へ落とす」既存 操作そのもの(4 節)です。専用スキーマは要求の宣言(steps/requires)だけで、製作の実行に新しい 操作の語彙を持ち込みません。

定義は RecipeDef、実行時は crafting.ts、レシピの データは src/assets/world-codex/containers.yaml 等、検証は tests/domain/crafting.test.ts です。本書は 実装済みの仕組みの記述で、未決事項は 6 節に整理しています。

1. 製作中オブジェクトは、完成品とは別オブジェクト

「未完成の斧」は、斧自身とは別の、製作中の状態専用オブジェクトとして扱います。製作中オブジェクトは progress プロパティを持ち、工程が宣言した仕事の量の合計を range の上限として、そこへ届いた瞬間に on_max(GameElementDefinition.md 6.3 節)で become: {recipe: none}(同 9.9 節)を発火させます。

進捗が数えるのは片付いた仕事の量で、経過した時間ではありません。 作り手の手際ぶん実際にかかる時間は 短くなります(同 13.5 節)が、進捗が受け取るのは工程が宣言した量そのものです。上限がロード時に決まった ままでいられるのはこのためで、腕で動かすと、作りかけの進み具合が誰が作っているかで違う意味を持ちます。 完成は、同じ個体のまま型が完成品へ変わることです——製作中オブジェクトはレシピの軸を持つ生成型 (同 3.5 節)なので、その軸を落とした座標がちょうど完成品にあたります。

完成を stages で表すことはできません。 段が持てるのは passives だけで、そこに become は 書けないためです(同 9.7 節・10 節)。range の端で反応させるのは、 耐久値 0 での自壊(同 6.3 節)と同じ形です。工程の区切りを段として見せたい場合は、stages を progress へ併せて宣言します(段は表示と passives のためだけに使い、完成は上の仕組みが担います)。

製作中オブジェクトの型は、レシピの内容(工程数・各工程の要求・時間)から、決まった変換規則でロード時に 導出します(GameElementDefinition.md 3.5 節。生成するのは world-codex を読み終えて型を組み立てる WorldCodexYamlLoader.buildAndReset で、ビルド時の前処理ではありません)。人間もパック作成者も、この型を直接書きません。完成品から引き継ぐ情報の 範囲は 5 節です。

2. 材料・道具の要求は専用スキーマ(steps / requires)

既存の conditions/passive/active には「工程ごとに異なる要求について在庫を確認し、指定数量だけ消費し、 足りなければ何もしない」という条件チェック+消費の複合動作を表す語彙がないため、steps/requires という 専用のスキーマを設けています(GameElementDefinition.md 13.1 節)。

進捗は既存の stages をそのまま流用します。専用の進捗表現は設けません。

2.1 工程を進めてよいかは、世界が1箇所で決める

素材が揃っているかとは別に、「今その作業ができる状況か」を世界が1本の条件で持ちます (crafting_conditions、GameElementDefinition.md 13.3 節)。同梱の世界が書いているのは手元の明るさと 風雨で、暗ければどのレシピも進みません(IlluminationSystem.md 5 節)。 嵐の日、屋根の下でなければ進まないのも同じ1本です(ContentSkeleton.md 8.1.4 節)。

工程ごとにも、レシピごとにも書きません。 レシピという形が既に「3 つ以上の物を組み合わせる複雑な 作業」を言っているので、書き分けると、条件の無いレシピが「暗くてもできると決めた」のか「書き忘れた」のか 見分けられなくなります。同じ理由で、条件を足したいときに触るのも 1 箇所だけです。

止めるのは tryAdvanceCrafting で、画面(craftingView)は同じ問いを使って「作業する」の可否と理由を 出します。判定を画面に置かないのは、画面を通らない経路から工程が進んでは困るためです。

3. 「箱」は slots の枠をそのまま使う

ゲーム上、レシピは「箱」として表現します。必要な素材・道具を箱に入れてからボタンを押すと、素材が消費されて 進捗が進みます。この「箱」は、slots の枠(cells、GameElementDefinition.md 7.2 節)そのものです。 レシピ専用の新しいコンテナ概念は作りません。

slots:
  materials:
    cells:
      - {accept: {object: wood}, max: 2}
      - {accept: {object: stone_knife}, max: 1}

何が何個要るかが、そのまま枠の形になります。 板の枠が1つ、棒の枠が1つ(4本まで)と並べば、 プレイヤーは箱を見るだけで必要なものが分かります。要求をタグで書けば枠の accept もタグになるので、 その用途に使えるどの型でも入ります(GameElementDefinition.md 13.1 節)。普通の箱(家具・コンテナ等)も 同じ枠に乗っています——同梱の入れ物は札での受け入れ(accept)とかさの上限(capacity)で書かれ、 レシピの箱は枠ごとに受け入れる型と置ける数(max)を指定します。同じ機構で、違うのは厳しさだけです。

素材も道具も、同じ 1 つのスロットへ入ります(上の例の wood と stone_knife)。置き場所として見れば どちらも「箱に入っているもの」で、分けると、工程が要求を数えるときに両方のスロットを見ることになります。

素材か道具か(consume)と、工程が実際に要求する個数(count)は枠には書かず、requires(同 13.1 節) だけが持ちます。枠は「置ける場所」であって「作るのに要る量」ではないためです。

箱には必要量より多く入れられます。 枠の max は場所の制約でしかないので、余分に入れておいても 工程は要求量だけ消費します。完成すると素材の箱(materials スロット)ごと無くなるので、残った素材は その親へこぼれ出ます(GameElementDefinition.md 9.9 節、こぼれ方は同 9.3 節と同じ)。

中断は製作中オブジェクトを破棄するだけです。 入れてある素材が親へ戻るのは完成時と同じ規則で、 中断のための後始末は要りません。

素材が箱から無くなるのは、工程の所要時間を経過し切った時点です。 時間と効果(消費・完成)の順序は actions/combinations と同じで(ActionSystem.md 2 節)、作業のあいだ素材は箱に在り、 使い切られるのは終えたときです(画面もその通りに見せます、Windows.md 4 節)。

打ち切るのは、経過中に製作中オブジェクト自身が失われた場合だけです(self にあたるもの。ActionSystem.md 6.1 節)。 素材が経過中に失われても工程は成立します——それは開始時に済ませた在庫確認の再判定にあたるためです。

4. 投入操作は「入れ物へ落とす」そのもの

箱へのアイテム投入は、製作中オブジェクト(1 節)専用の操作ではありません。中身を持つカードへ重ねたら そのスロットへ入るという、かごと共通の 1 つの動きです(CardInteraction.md 2 節)。行き先は宣言順で最初に受け取れるスロット(GameElementDefinition.md 7.8 節)、入るか どうかは枠の accept と max が答えるので、レシピ側に宣言は要りません。

投入先の優先順位も、レシピ側では決めません。 スロットの中のどの枠へ入るかは SlotSystem.md 2 節、1 つの物が複数の要求に当てはまるときにどれへ割り当てるかは GameElementDefinition.md 13.1 節が答えます。

逆向き——1 つの枠に入りうる素材が手元に複数あるとき、どれを入れるか——を選ぶ場面は、自動補充にしか ありません。 手で落とすなら運んだ物がそのまま入るので、選ぶ相手が居ません。自動補充が何を採るかは autoFillMaterials が持ちます。

素材が揃っているかの確認・消費・進捗更新(interactions.work 相当)は、通常の操作の conditions/active として表現しますが、「スロットの中身をまとめて検査し、条件を満たせば該当分だけ消費する」という具体的な処理は、 レシピでしか使わない概念であるため、YAML の汎用語彙(conditions/active)を複雑化させず、プログラム側の 専用ロジックとして実装します。

5. 引き継ぐのは、置き場所を言うタグだけ

自動生成される製作中オブジェクトの型は、完成品の tags(GameElementDefinition.md 4.1 節)のうち、 ルートキー in_progress_tags に挙がっているものだけを引き継ぎます(同梱の世界では item と fixture)。挙がっていないタグは付きません。挙げるタグが 1 つも無い世界では、wip だけが付きます。

# core.yaml
in_progress_tags: [item, fixture]

引き継ぐ理由は置き場所です。 枠の accept がタグで書かれている場所(土地の items/fixtures、 キャラクタの hand、入れ物の中)へ、完成させる前に置けなければなりません。住居の壁の枠 (Dwellings.md 1 節)のように、その場で組み上げる物の枠を足すときは、その タグを in_progress_tags へ挙げます。

働きを言うタグは引き継ぎません。 作りかけはまだその物ではないので、道具としても材料としても 働きません。石斧の cutting_tool・weapon まで引き継ぐと、半分できた斧で木を伐り、獣を殴れて しまい、別のレシピが「刃物」として要求する枠にも収まってしまいます——しかも製作中オブジェクトは 完成品の働きに関わる props を持たないため、一撃の重み配分(HuntingSystem.md 1.2 節)が全て0になり、 重みが全て0のときの規約(先頭の候補)で最も重い一撃が必ず出ます。

受け口の側に、作りかけを弾く判定は要りません。 弾く判定は受け口の数だけ要り、受け口が増えたときの 書き忘れが見えません。名乗らなければ、枠も操作も何も知らないまま当てはまらなくなります。

5.1 引き継ぐ props は、置き場所が要求するものだけ

タグを引き継ぐ以上、そのタグが要求するプロパティ(GameElementDefinition.md 4.2 節)も揃っていなければ なりません。item を引き継いだ製作中オブジェクトには weight と volume が要ります。

6. 未決事項・今後の検討課題