レシピシステム設計
概要
本ドキュメントは、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 が要ります。
weightは 0。 作りかけそのものに目方は無く、重さは材料スロットに入っている物がそのまま持ちます (ContainerSystem.md1 節)。担ぐことになるのは投入した材料の重さです。volumeは完成品のものを写します。 かさは重さと違って中身から導出されません(入れ物のかさは 外側の大きさで、中身を足しても膨らまない)。写さないと 0 になり、容量のある入れ物へ作りかけを いくらでも詰め込めてしまいます。編みかけの籠は、編み上がった籠とほぼ同じ場所を取ります。
6. 未決事項・今後の検討課題
steps.requiresをタグで書いたときの枠の見せ方(当てはまる型が 1 つに定まらないので、今は 1 秒ごとに 順に出している。materialCells)