WorldCodex YAML 文法リファレンス

1. 位置づけ

本ドキュメントは、WorldCodex(ゲーム内のあらゆる要素を定義する YAML)の文法そのものを体系的にまとめた、唯一の リファレンスです。traits・object_defs・props・stages・slots・interactions・trigger・recipes・passives・active・ modify・add・destroy・spawn・move・pick・duration・conditions など、YAML 上のキーワードの意味と 書き方はすべてここに集約します。

本書が定めるのは文法だけです。その文法で具体的なゲーム内容をどう表現するかは、以下の別ドキュメントを 参照してください。

形式的なスキーマ定義(JSON Schema)は WorldCodex.schema.json (メタ情報は WorldCodexSchema.md)を参照してください。機械的な検証はスキーマ側が担います。

本書は確定した文法のみを記述します。未決定の論点は 17 節にまとめています。

2. 設計原則

2.1 使う場面の無い拡張は、場面が出るまで入れない【確定】

書きたい宣言が今の世界に1つも無い文法は、案が書けていても入れません。 場面が出たときに、そこで 作ります。

書ける形を思いつくことと、書きたい宣言が在ることは別です。使う場面の無いまま入れた文法は、それで 書かれた定義が世界に1つも無いので、正しいかを誰も確かめないまま本書と実装の面積だけを増やします。 場面が出てから作れば、その場面が最初の呼び手になります。

出どころ: #2286(今すぐ使う場面の無い文法の拡張は、場面が出るまで入れない。2026-09-19に回答)

3. ファイル構造とロード

3.1 ルート構造

ファイル全体が 1 つの WorldCodex を表します。専用のルートキーは置かず、object_defs・traits と、 プロパティのカテゴリを宣言する property_tags(6.7 節)をトップレベルキーとして持ちます。

ほかに、画面の都合だけを持つルートキーがあります。recipe_categories は、レシピ一覧の棚に使う タグ(4.1 節)を優先順位の順に並べた列で、意味は Windows.md 9.2 節が持ちます。 card_filters は、フィルターバーのボタンを並べる順に挙げた列で、要素は id(そのままアイコンの 名前)と、絵がまだ無いときに置く絵文字 icon、そのボタンが指すタグの列 tags を持ちます。意味は ScreenLayout.md 8.1.3 節が持ちます。どちらも分類のための語彙を新設せず、 既にあるタグを指すだけです。 その世界の約束を書く required_props(4.2 節)と、製作中オブジェクトが完成品から引き継ぐタグを挙げる in_progress_tags(RecipeSystem.md 5 節)もルートキーです。 地形生成の axes・location_types・generation_scopes は TerrainGeneration.md が持ちます。

3.2 命名規則

3.3 ロードとバリデーション

3.4 patch_object_defs(既存の型への変更)

既に読み込まれた object_def を変更する唯一の口です。主に使うのはアセットパック (AssetPack.md)で、同梱の土地の探索結果に自分のアイテムを足す、といった 変更を書きます。同梱のYAMLでも同じ文法が使えます。

トップレベルの patch_object_defs に、操作を1つずつ並べます。

patch_object_defs:
  - append: sandy_beach.actions.explore.pick
    value:
      weight: 2
      spawn: {object: healing_potion, into: self}

動詞がパスの読み方を決めます。

動詞 パスが指すもの 前提 材料
add 新しく作るキー そのキーが無い value
append 配列 その配列がある value(要素1つ)
set 既にある値、または配列の要素 ある value(配列の要素なら where も)
remove 既にあるキー、または配列の要素 ある (配列の要素なら where)

add と append を分けているのは、1つの動詞にまとめると配列名の打ち間違いが「新しいキーの 作成」として通ってしまうからです(...explore.picks は explore に存在しないキーなので、 add としては成立してしまう)。分けておけば、その場で「配列がありません」と言えます。

パスはドット区切りで、先頭が object_def の識別子です。識別子に . は入らない(3.2節)ので 曖昧になりません。

3.4.1 配列の要素は、位置ではなく中身で指す

set と remove が配列の要素を指すときは where を書きます。当てはまる要素が0個でも2個以上でも エラーです。

patch_object_defs:
  - remove: sandy_beach.actions.explore.pick
    where: {spawn: {object: woven_basket, into: self}}

添字(pick[2])を使わないのは、別のパックが1つ足しただけで同じ添字が別の要素を指すうえ、 それが黙って成功するからです。where は書いたキーだけを見る部分一致で、書いていないキーは 何であってもかまいません(配列だけは、書いたとおりの並びと個数を求めます)。

配列の要素の一部だけを変えることはできません。要素を丸ごと書き直します。

3.4.2 適用の順序と、失敗したとき

3.5 ロード時に生成される型

エンジンがロード時に生成する型は、いくつかの軸の値の組で決まり、識別子はその組から組み立てた結果です。 著者もパック作成者もこの識別子を書かず、効果からも名指ししません(9.9 節)。

生成器があります。製作中オブジェクト(13 節、RecipeSystem.md 1 節)は 素の型が完成品・軸が recipe で、著者は何も書きません。変種(variation_axes、3.5.1 節)は 型自身が軸を宣言し、その軸の値ごとに型が生まれます——中身入りの容器 (LiquidContainerSystem.md)がこれにあたります。

表は WorldCodex.generatedTypes が持ち、両方向に引けます——生成型からその座標へも、座標からそこに 居る型へも。どの軸かで扱いを分けません——作りかけも中身入りも「素の型の変種」で、違うのは軸の 名前と、その名前に紐づく書式(Localization.md variation_names節)だけです。

3.5.1 variation_axes(変種を並べる軸)

variation_axes は、その型の変種がどの軸で並ぶかを宣言します。 軸の名前は著者が付け、値に なれる型は of(4.1 節の型指定)が選びます。

object_defs:
  jar:
    traits: [liquid_container, narrow_open_container]
    variation_axes:
      content: {of: {tag: liquid}}

変種は「素の型に、軸の値の型が持つ traits を配ったもの」です。 props・tags・actions・ combinations・passives の合成は trait の mixin(5 節)がそのまま行うので、生成に固有の合成規則は ありません。軸の値になれるのは traits だけを宣言した型で、自前のメンバーを持つ型を指すと ロードエラーになります——配る仕組みが mixin そのものである以上、mixin に載らない宣言は配れません。

宣言は of だけです。 変種が素の型と何が違うかは、配られた trait だけが決めます——この仕組みは 何を配っているのかを知りません。素の型ごとに違う数値(容器ごとの容量)も、素の型が自分の props と して持てば変種へそのまま引き継がれます。

素の型へ戻る道も宣言の側にあります。 量が尽きたら空へ戻る、という規則は on_min(6.3 節)へ become: {<軸>: none} を書いて表します——「何かが尽きたら変種が変わる」を仕組みの側が想定しては いけません。

variation_axes と recipes は変種へ写しません。変種の変種は作らず、作れるのは素の型のほうです (空の容器を作ってから中身を入れる)。

3.6 宣言が名指しする別の宣言

条件の prop、base の土台、効果の行き先、deftness の skill、スロットや型の指定——別の宣言を名前で名指しする口は いくつもあります。名指した名前は、それを宣言している型が世界に1つも無ければロード時に落とします。

名指しの綴りを間違えると、実行時にはただ「そのプロパティを持っていない」「その段に居ない」と読まれ、 宣言は落ちずに一度も効きません。 名前を作る口と名指しする口が同じでは、綴り違いがその場で新しい 名前になって通ってしまうので、ローダーは両者を別の口として持ちます。

見るのは「どこかの型が宣言しているか」までです。 誰がその値を持つかは実行時にしか決まらない (条件の subject も作り手も、就く相手は状況で変わる)ので、指した相手がそれを持っていないことは 誤りではありません——「その腕を持つ者だけが速い」は正しい書き方です。

タグ(4.1 節)の名前はこの検査の外です。 タグには定義の場が無く、tags に書くこと自体が定義なので、 綴りを照らし合わせる相手がありません。

4. object_defs(型定義)

型定義は object_defs の下に、識別子をキーとして表現します(値ではありません)。

object_defs:
  stone:
    props:
      weight:
        value: 1000

4.1 tags と型の指定

tags に文字列の配列を書くと、その object_def にタグを付けられます。traits(5節)も同じ tags フィールドを 持て、その trait を参照するあらゆる object_def へタグが合成されます。tags は単純な集合なので、trait 間・ trait と object_def 自身の間で同じタグが重複しても衝突エラーにはなりません。

traits:
  location:
    tags: [location]
object_defs:
  forest:
    traits: [location]   # traitのtagsが合成され、locationタグを持つ
  beach:
    tags: [location]     # locationトレイトを一切参照しなくても、直接同じタグを宣言できる

ここでいうタグは object_def に付くもので、プロパティに付けるタグ(6.7 節)とは別の名前空間です。

「どの型が当てはまるか」の指定は、{tag: ...}・{object: ...}・{not: ...} のうち、ちょうど1つを 書く形です。 タグで指せばそのタグを持つあらゆる型(パック追加分も含む)が、object_defs の id で 指せばまさにその型だけが当てはまり、not は中に書いた指定を裏返します。 trait 名ではどの形でもマッチングしません(trait は合成後に消える存在であり、外部から参照すべきでは ないため)。 beach の例のように trait を経由せず直接 tags を書いても、同じタグを共有していれば同じ扱いを受けます。

どちらで書くかは、後から仲間が増える余地があるかで決めます。刃物・燃料のように「その役ができる物」の 集まりならタグ、火種(burning_tinder)のように種類を増やす予定がその型そのものに無いなら id です。 id で書けば、そのためだけの単発タグを新設せずに済みます。

4.2 required_props(タグが要求するプロパティ)

ルートキー required_props は、タグ名 → そのタグを持つ型が宣言していなければならないプロパティ名の列です。 1 つでも欠けている型があればロード時エラーになります。

required_props:
  item: [weight]      # 持ち歩ける物は必ず重さを名乗る
  character: [load]   # 担ぐ者は必ず負荷を受け取る口を持つ

タグは「その物が何であるか」を言うので、名乗った以上そこに要る値は揃っているはず——という、 その世界の約束をそのまま書きます。エンジンは何が要るかを知らず、要求を 1 つも書かない世界も成立します。

宣言漏れが静かに効く値ほど、ここへ書く価値があります。重さを書き忘れた道具は 0g として無限に持ち運べ、 負荷を書き忘れたキャラクタは何を担いでも平気になります——どちらもエラーにはならず、ただ遊びが壊れます。

同じタグを複数のファイルが書けます(パックが自分のタグの約束を足す)。後から現れた宣言は上書きではなく 合流し、要求は増える方向にだけ動きます。

4.3 art(1枚の絵を複数の型で共有する)

型は art で「カードに出す絵の名前」を名乗れます。値は src/assets/objects/<この値>.png を指し (CardView.md 5 節)、宣言しなければ型の識別子そのものです。

traits:
  sea_route:
    art: sea_route      # この trait を持つ航路が、1枚の sea_route.png を共有する

同じ絵しか持ちようがない型の群れのためのものです。 航路(Voyage.md 3 節)は行き先の 1 行しか違わないので、行き先の数だけ絵を描いても同じ絵が並ぶだけになります。

5. traits(mixin)

複数の object_def で共有するプロパティ・アクション等を traits として定義します。

traits:
  spoilable:
    props:
      shelf_life:      # valueなし→継承先で必須
        unit: days
      decay_rate:
        value: {min: 1, max: 3}

object_defs:
  apple:
    traits: [spoilable]
    props:
      shelf_life:
        value: 7

props(6 節)以外にも、interactions・recipes・slots・passives・tags(4.1 節)を trait 経由で まとめて配布できます。value を持たないプロパティは、5 節のルール通り空のマッピング({})として表現します。 slots・interactions のエントリも props と同じく、object_def 側で一部のキーだけを書くと、残りのキーは trait 側の値を引き継ぎます(操作のきっかけと効果を trait に置いたまま、炉ごとの conditions だけを足す、など)。 同名のエントリが複数の trait にあればエラーになるのも props と同じです。

オブジェクトレベルのキーも trait 側に書けます。 混ぜ方はキーごとに決まっています。

6. props(プロパティ)

数値プロパティの値は有限の数値で、value/range/stages/modify/add/transfer など、 数値を扱うすべての場所で小数を書けます(値域は 32bit 整数の幅、精度は倍精度)。細かい傾きを 「1 単位を何分割するか」で表す必要はありません——-0.35/tick と直接書けます。

ただし小数を持てるのは連続量だけです。 数えるもの(tick や残り時間のようにプロパティ自身が刻みを 持つもの)と、シンボル型(6.6 節)は整数で書きます。エンジンは stages の eq を完全一致で判定するため、 「最小の刻みが 1」であることに依存した書き方は、整数で書くプロパティにだけ使えます。

6.0 数値のスケール規約

数値プロパティは次のクラスのいずれかに属し、書かれている数値の読み方はクラスで決まります。

クラス 基準 値の読み方 例
時間を数えるもの 1 = 1 tick 値がそのまま tick 数 tick・satiety・hydration・body_fat・wakefulness・各栄養・severity・durability・atmospheric_moisture・season_remaining・thermal_level
現実の単位に載るもの 1 = 1mL / 1 = 1g / 1 = 1g/mL / 1 = 1℃ 値がそのまま実単位 volume・weight・density・ambient_temperature

境界が「人や物の状態か、物の物理量か」と一致します。 人の状態はすべて「基準のレートで何 tick 保つか」 として読め、物の物理量だけが現実の単位に載ります。

# 満腹は1日(96 tick)で空になる
satiety:
  value: 96
  range: {min: 0, max: 96}
  passives:
    - add: {self: {satiety: -1}}   # 基準レート = 1/tick

その tick の数を、コメントで人に読める長さへ言い換えるときの書き方は DocumentStyle.md 11 節が持ちます(上の例の 1日(96 tick) がその形です)。

時間を数えるクラスでは、range.max が「基準レートで何 tick 保つか」を直接表します。「基準レート」が何かは プロパティごとに宣言側が決めます(satiety なら消費、atmospheric_moisture なら雨季の上昇)。基準から外れる レート(雨季以外の季節、素材ごとの劣化速度)は小数で書きます。

物理量のクラスは、単位が互いに噛み合っている点が要です。weight = volume × density が換算定数なしで 成立します(ContainerSystem.md 1 節)。これらのクラスの間の換算は、 必要な場所が宣言として持ちます——飲用が水分へどれだけ寄与するかは transfer の amount/to_amount (9.5 節)が持ち、エンジンは換算率を知りません。

比率として読むものは、どちらのクラスにも属しません(load_rate は率、pick の weight は 正規化される相対値、*_weight は天気候補の相対重み)。

6.1 固定値

最も単純な形は、value に固定値を 1 つ持つだけの形です(4 節の stone.props.weight を参照)。

6.2 範囲値(生成時に1回ロール)

固定値の代わりに {min, max} の範囲を value に持たせると、インスタンスの生成(spawn)時に 1 回だけ、 範囲内の一様乱数で初期値をロールします。以後は固定値と同じ扱いで、tick ごとの再ロールは行いません (PropertyDef.rollInitialValue 参照)。

props:
  max_durability:
    value: {min: 80, max: 120}   # 個体ごとに生成時1回だけロールされる

6.3 range / on_max / on_min(周回・上下限)

上限・下限に達したら折り返す(あるいは繰り上げる)プロパティは、range(取りうる上下限)と、on_max (上限到達時の挙動)・on_min(下限到達時の挙動、on_max の下限側の鏡像)を持たせます。value の {min, max}(6.2 節、生成時に 1 回ロールする初期値の範囲)とは別の仕組みです。

on_max/on_min は、値が境界に達した瞬間に発火します(>= range.max / <= range.min)。境界は 「そこに居てはいけない値」ではなく「そこに着いたら何かが起きる値」で、尽きた・満ちたことそのものへの 反応をここに書きます。transfer(9.5 節)は range.min を「出せる量の床」と見るので、在庫を出し切る 操作は下限を割りません——「注ぎ切った」「飲み干した」に反応できるのは、境界そのもので発火するからです。

on_max/on_min の中身は active(9 節)と同じ型の内容で、pick(10 節)も他の動詞と 並べて書けます。発火するのは値が端に着いた瞬間で、誰かが操作しているとは限りません——操作が直に 動かした値でも、宣言元はそれを誰が動かしたかを知らないので、居るのは宣言元の個体だけです。何を 起点に書けるかはそこから決まるので、一覧は 14.1 節の表(操作の関係の役は 11.5 節「役を書ける場所」) を見てください。

実体値と実効値、そして range が誰の端か

プロパティの値には次の読み方があります。

on_max/on_min が見るのは実体値です。 端のイベントは「不可逆に動いた値が端に着いた瞬間」への 反応なので、可逆な寄与では起こりません。

エンジンは実体値をクランプしません。 値を範囲へ戻しているのは既定の on_max/on_min (自分自身を境界へ set する)であって、暗黙の丸めではありません。だから著者がそれを上書きすれば、 実体値は範囲の外へ出られます——on_min: {} と書くと範囲外の値をそのまま許容できるのはこのためです。

become(9.9.1 節)で型が変わり、運んだ値が新しい型の range に収まらない場合も丸めません。 読むときに実効値の側で切られるからです。その場では on_max/on_min は起きません(器が変わっただけで、 値は動いていないため)——次にその値へ書き込みが起きたときに、新しい型の宣言に従って発火します。

実効値が実体値と食い違いうるプロパティには、on_max/on_min を書けません(ロード時エラー)。 modify の対象になっている・base を持つ・weight/load である、のいずれかが当てはまる場合です。 書けてしまうと「ゲージは満ちているのに何も起きない」「まだ空でないのに尽きる」が起こりえますが、 そのときに何が正しいかをまだ決めていません。

props:
  minute_of_day:
    value: 0
    range: {min: 0, max: 1440}
    on_max:
      add:
        self:
          minute_of_day: -1440   # 1440に達したら自分を1440引いて0に戻す
          day: 1                 # 同時にdayへ+1する
  # on_minを省略しているため、下限(0)に達した場合は既定の
  # `set: {self: {minute_of_day: 0}}` が自動的に適用される(単純なクランプ)。

  hp:
    value: 100
    range: {min: 0, max: 100}
    # on_max/on_minをどちらも省略しているため、単純に[0, 100]へクランプされるだけの
    # プロパティになる(毎tick再計算するmodify/addの結果が範囲をはみ出した場合の後始末)。

conditions(どういうときに走るか)

on_max/on_min は conditions(14 節)を持てます。満たす回だけが著者の書いた内容へ、満たさない回は 既定のクランプ(自分自身を境界へ set)へ倒れます——on_max を書くとクランプが消えるのは、著者が 境界での振る舞いを引き受けたからで、条件を満たさない回は引き受けていないためです。値は境界に張り付いた ままになり、書き込みは何度でも続けられます。ここは条件の並びなので、要素に reason は書けません (14.6 節)。

「一度だけ起こす」を、呼ぶ側ではなく起こす側に書くための口です。 境界に居る値は書き込みのたびに イベントを呼び直すので、これが無いと「もう起こした」を呼ぶ側(そこへ書き込むアクションの conditions)が 覚えることになり、そのアクション自体が打ち切られます。海区の見張りが航路を一度だけ湧かせる例が これで(Voyage.md 3 節)、条件が見るのは印ではなく湧かせた物そのものです。

# 沿岸の海区(voyage.yaml の coastal_waters)
props:
  exploration_progress:
    value: 0
    range:
      min: 0
      max: 2
    on_max:
      conditions:
        - not: {slot: fixtures, matches: {object: route_to_kelp_belt}}   # まだ湧かせていない
      spawn:   # 辺の両端へ1本ずつ立てる
        - {object: route_to_kelp_belt, into: self}
        - {object: route_to_coastal_waters, into_object: {prop: zone_toward_mainland}}
props:
  durability:
    value: 100
    range: {min: 0, max: 100}
    passives:
      - add:
          self:
            durability: -1
    on_min:
      destroy: self   # 0に達した瞬間に自壊する

6.4 stages(段階)

数値プロパティは stages を持つことができ、現在値がどの段階にあるかによって、有効になる passives(8 節)が 切り替わります。

props:
  progress:
    stages:
      - name: none
        min: 0
      - name: mild
        min: 20
        alert: caution
        passives:
          - add:
              parent:
                temperature: 1
      - name: feverish
        min: 50
        alert: danger
        passives:
          - add:
              parent:
                temperature: 2
                hydration: -1

6.5 base(値の土台)

base を書くと、そこが指すプロパティの実効値を土台にして、自分の値をその上へ加算します (getEffectiveValue、可逆)。土台が辿り着けなければ寄与は 0 で、自分の値だけが残ります。

土台の指し方は、対象 1 つ・プロパティ 1 つの参照 {subject, prop}(14.1 節)です。subject を省くと self、prop を省くと同名のプロパティを指します。

props:
  # 祖先の同名(propを省略)
  ambient_brightness: {value: -9, base: {subject: ancestor}}
  # 自分の別のプロパティ
  hand_brightness: {value: 0, base: {subject: self, prop: ambient_brightness}}
  # 祖先の、別名のプロパティ
  looking_brightness: {value: 0, base: {subject: ancestor, prop: ambient_brightness}}

ancestor(8.6 節と同じ探索)は、自分の直接の親から遡り、prop が指すプロパティを定義している 最初の祖先です。

object_defs:
  room:
    props:
      ambient_temperature:
        value: 20
  food:
    props:
      ambient_temperature:
        value: -2                  # 自分のオフセット
        base: {subject: ancestor}  # 祖先(通常はroom)のambient_temperatureを土台にする

6.6 シンボル型の値

value に数値にも真偽値にもならない識別子(^[a-z][a-z0-9_]*$、 3.2 節と同じ命名規則)を書くと、その識別子をシンボル名として自動的に整数へ変換します。専用の宣言 (symbol: true 等)は不要で、value の書き方だけで区別できます。液体の種類のような「離散した状態」を、 enum 型等を新設せず既存の props/conditions/set の語彙だけで表現するための仕組みです。値の比較・代入は 9.2 節・14.1 節の通り、他の数値プロパティと全く同じように書けます。

props:
  content:
    value: water   # 数値にも真偽値にもならないため、シンボル名として登録される

6.7 tags(プロパティのカテゴリ)

プロパティは tags でカテゴリ分けできます。キャラクターのように多数のプロパティを持つ型を、UI が 「健康」「栄養」のようなまとまりに分けて見せるための仕組みです。

タグ名はトップレベルの property_tags で宣言し、props 側はそこで宣言した名前しか使えません (未宣言の名前はロード時エラー。綴り間違いをその場で捕まえるため)。宣言した順がそのまま UI での 表示順になります。

property_tags:
  status:
  health:
  nutrition:

object_defs:
  character:
    props:
      satiety:
        tags: [status, nutrition]
        value: 96
        range: {min: 0, max: 96}

6.8 worsens・gauge(値の向きと、カードのバー)

プロパティは worsens で、どちらへ動くと悪いかを宣言できます。増減の記号の色( Windows.md 8 節)・バーの向き(StatusArea.md 6 節)・ 収支表が 1 日に賄う量に数えるか(BalanceStats.md)が、これを見ます。

props:
  travel_delay:
    # 歩みの遅れ(分)。増えるほど遅くなるので、増える側が悪い。
    worsens: up
    value: 0

プロパティは gauge で、自分をカードのバーとして出すかと、両端がどう見えるかを宣言できます (CardView.md 8 節)。

props:
  durability:
    # 尽きると使えなくなる量。満タン側が good なので、減るほど赤へ寄る。
    gauge: {min: bad, max: good}
    value: 960
    range: {min: 0, max: 960}
  severity:
    # 残っている傷。増えるほど悪いので、耐久度とは両端が入れ替わる。
    gauge: {min: good, max: bad}

6.9 型を指す値

value に {object: <識別子>} を書くと、その値は object_defs の型を 1 つ指すものになります。実行時の 値は整数(その型のグローバル ID)で、シンボル型(6.6 節)と同じく名前を整数へ畳んだ値です。ロードした 時点で決まる定数なので、生成の順序にも世界の形にも依存しません。

props:
  # この海区の、本土の側の隣(voyage.yaml)。
  zone_toward_mainland: {value: {object: kelp_belt}}

引くのは行き先を型で指す口——move の to_object・spawn の into_object(9.6 節・9.4 節)で、 {prop: <名前>} と書くとこの値を読みます。探し方は型の名前をその場に書いたときとまったく同じなので、 違うのは名前をどこに書くかだけです。

move: {subject: picked, to_object: {prop: zone_toward_mainland}}

7. slots(親子関係とコンテナ)

7.1 基本構造

object_defs:
  character:
    props:
      hp:
        value: 100
    slots:
      equip: {}
      inventory: {}
      injuries: {}

7.2 cells(枠ごとの要件)

スロットは中身を直接持たず、枠(セル)の並びを持ちます。 何が入るかも何個入るかも、スロット 全体ではなく枠ごとに決まります。

object_defs:
  character:
    slots:
      inventory: {}                              # 制約なし・枠数も未定
      hand:
        cell_count: 6                            # 枠が6つ
        cell: {accept: {tag: item}}              # どの枠も同じ要件
  chair_in_progress:
    slots:
      materials:
        cells:                                   # 枠ごとに違う要件。並べた数がそのまま枠数
          - {accept: {object: board}, max: 1}
          - {accept: {object: stick}, max: 4}

枠の中身は次のキーで決めます。

入れた物がどの枠へ入るかは SlotSystem.md 2 節。

レシピが要求する個数も、素材か道具か(consume)も、ここには書きません(13.1 節の requires)。枠は 「置ける場所」であって「作るのに要る量」ではないため、両方に数を書くと二重管理になります(数を枠へ 戻さないと決めた経緯は DesignNotes.md)。

7.3 capacity(合計サイズの制約)

capacity を書くと、スロット内のアイテムの合計サイズ(各アイテムの volume プロパティの総和)が上限を超えないよう 制約できます。誰がいつ何と突き合わせて検証するかは SlotSystem.md 2 節。

slots:
  inventory:
    capacity: 50

props:
  volume:
    value: 3

7.4 weight / load(重さと負荷)

weight・load・density・load_rate は、エンジンが名前で直接参照する規約プロパティです (内部設計は ContainerSystem.md)。いずれも実効値として読むたびに導出されます。

コードがYAMLの単語へ寄せている依存は WorldVocabulary に集めてあります。 ここに挙げた規約プロパティ (engine)と、この世界のルールが名前で知っている単語(world。時間・天候・手持ちの枠・探索の進捗など)に 分かれていて、「この単語をYAMLから消したら何が壊れるか」を1箇所で読めます。IDはinternで取る (無ければ登録する)ので、誰も宣言していない単語は「そのIDを持つ物が1つも居ない」という形で自然に扱われ、 語彙の有無で分岐する場所はありません。

見せ方だけを決める単語(カードの枠色を選ぶタグ、覆いを出す段の名前など)は載せていません。あちらは 「この段なら覆いを出す」のように結果と対でしか意味を持たないので、名前だけを引き剥がすと規則が 2箇所に割れます。

これらの伝播は、エンジンが型に生やす modify(8.3 節)として表されます。押し上げる当人が名乗る形で、 寄与の登録も影響の一覧も著者が書いた modify と同じ経路を通ります。著者に書かせないのは、この宣言に 自由度が 1 つも無いためです(詳細は ContainerSystem.md 1 節)。fill を宣言する型は weight も宣言しなければなりません——抱えている量の重さを載せる先が要るためで、無ければロード時エラーです。

率をスロットではなくアイテムが持つのは、同じ入れ物でも背負うか手に提げるかで体感が変わるからです。 どのスロットにいるかは passives の in_slot(14.2 節)で切り替えます。

object_defs:
  sledge:
    props:
      weight: {value: 8000}
      load_rate:
        value: 1
        passives:
          - conditions: [{in_slot: hand}]
            modify: {self: {load_rate: -0.45}}  # 引きずるので体感は55%

  castaway:
    props:
      weight: {value: 70000}   # 自重。荷物は中身として上に乗る
      load: {value: 0}

石(weight 1000)を載せたそり(自重 8000)を手で引くと、そりの weight は 9000、キャラクターの weight は 自重込みの 79000、load は 9000 × 0.55 = 4950 になります。

7.5 装備の排他制御(covers / layer)

7.5.1 装備の排他は、枠ではなくアイテムの属性で決める【確定】

身につける枠には複数のアイテムを同時に格納できます。排他制御はスロットの数ではなく、アイテムが持つ 属性で行います。

競合解決は「ブロック型」です。 競合するアイテムがある場合、プレイヤーが先に既存の装備を外す必要が あります(自動退避は採用しません)。

この判定が効くのは身につける枠だけです。手持ちや地面では同じ衣類を何着でも並べられます。

出どころ: ユーザーの指示(装備の排他は枠の数ではなくアイテムの属性で決める。#2026)

7.5.2 覆う部位と階層の宣言

7.5.1 節の排他を宣言と判定へ落としたもので、覆すのに人間の判断は要りません。

object_defs:
  armor_leather:
    covers: [torso]
    layer: base

7.6 スタック表示

同種オブジェクトをまとめて1つの視覚的なまとまり(スタック)として表示するための設定です。世界の実体 (slotsの中身が個々のオブジェクトの集まりであること)は変わりません。個々のスタックの中は stack_order(無ければ挿入順)で並びます。スロットが実行時にこれをどう持つかは SlotSystem.md 1節。

スロット側(slotsの各エントリに指定): 枠の数(cell_count / cells)と枠ごとの max (7.2節)。枠数を決めたスロットと決めていないスロットで、空いた枠の扱いも位置の指し方も変わります (SlotSystem.md 3節)。

object_defs:
  character:
    slots:
      hand:
        cell_count: 6
        cell: {accept: {tag: item}}
      furnace_intake:
        cell_count: 2          # 燃料はstackable: falseなので、同じ燃料でも2つで2枠

オブジェクト側(object_defsの各エントリに指定):

object_defs:
  wet_log:
    props:
      freshness:
        value: 100
    stack_order: {property: freshness, ascending: false}

spawn(9.4節)との関係: intoを省略(same_slot)した置き換えで、破棄されたオブジェクトが占めて いた位置を新しいオブジェクトへ引き継がせるには、destroyをspawnより先に書きます(9.3節・9.4節参照)。 著者が決められるのは書いた順だけで、それを受けてどこへ置くかの決まりは SlotSystem.md 3節。

7.7 placement(誰がここへ物を入れてよいか)

placement は、そのスロットへ誰が物を入れてよいかを並べるスロットレベルのキーです。 省略すると両方([auto, manual])です。

値 意味
auto spawn(9.4 節)・move(9.6 節)の宣言順走査の宛先になる。エンジンが自分で選んでよい
manual プレイヤーが札を重ねて入れられる(7.8 節)

どちらも同じ 1 つの規約(宣言順に探して最初に受け取れる枠へ入れる)の、入口の違いです。 どの枠が その走査に参加するかを placement が決めます。走査する枠を集めるのが誰かは SlotSystem.md 2 節。

slots:
  hand:
    cell_count: 6
    cell: {accept: {tag: item}}
  equipment:
    cell: {accept: {tag: item}}
    placement: [manual]        # 手持ちがあふれても流れ込まない。着けるのは意図した操作だけ
  spoils:
    cell: {accept: {tag: item}}
    placement: [auto]          # くわえるのは獣自身。プレイヤーは物を渡せない
  undiscovered_fixtures:
    cell: {accept: {tag: fixture}}
    placement: []              # 生成が名指しで入れるだけ。誰にも選ばせない

枠の要件(7.2 節)・capacity(7.3 節)が「何をいくつ入れられるか」の制約であるのに対し、placement は 入れる側が誰かを表します。

7.8 カードへ重ねた物の行き先

カードへ重ねた物は、そのオブジェクトのスロットのうち、宣言順で最初に受け取れるものへ入ります (CardInteraction.md 2 節)。受け取れるとは、manual を許していて (7.7 節)、今その物が実際に入る、ということです——枠の要件(7.2 節)だけでなく、空きと容量まで 見ます。型が合うだけで選ぶと、手が塞がっているときに装備の枠が空いていても落とせなくなります。

スロットがあるなら入れられる、が既定です。 入れられて困るスロットが placement で自分から断ります。

行き先は重ねる物で変わります。 怪我の札は治療具だけを受け取り、かごは持ち物を受け取ります。同じ カードでも、何を重ねたかで行き先が決まる(あるいは決まらない)ので、カードごとに 1 つの行き先を持つ 形では表せません。

複数のスロットが受け取れるときは、宣言順で最初のものです。 編み籠は item でも fixture でも あるので、筏へ重ねると積荷(items)に入り、設置物の枠には落ちません。先の枠が埋まっていれば、 次に受け取れる枠が行き先になります。この順序は自動配置の走査順 (7.7 節)と同じもので、location trait が items を先頭に置いているのはこぼれた物の行き先を 決めるためです。入る先が名乗り(slot_texts の put_in)か値段を持つ枠なら、落とす前に吹き出しが その枠を名乗ります(7.10 節)。

中が見えるかとは別です(visible_slots、7.11 節)。筏の積荷は外から見えませんが、札へ重ねれば 積めます——中を確かめるには乗り込む、という違いがそのまま操作の形になります。

「押すと開くスロット」(7.11 節)とも「容量のバー」(7.12 節)とも別の宣言です。 かごのように どれも一致する型が多いのですが、一致するとは限らないので 1 つのキーでは兼ねません——筏は 「重ねれば積めるが、中身は外から見えない」です。

7.9 bound_to_owner(持ち主とともにしか在れない)

bound_to_owner: true は、その型のオブジェクトが単独では存在できないことを宣言する オブジェクトレベルのキーです(stack_order と同じ位置に書きます)。捻挫・液体・道がこれにあたります。

object_defs:
  twisted_knee:
    bound_to_owner: true      # 身体から離れた「捻挫」は無い

宣言すると次のことが起きます。

スロットではなくオブジェクトに書くのは、これがその物の性質だからです。同じ「怪我スロット」でも、 入っている物が捻挫なら剥がせず、湿布(治療具)なら剥がせる、という違いはスロット側では言えません。 怪我が治って捻挫が消えるとき、当てていた包帯だけが本人の手元へこぼれ出るのがその現れです。

プレイヤーが怪我を出し入れできないのは、この宣言の帰結です。 取り出せないのは捻挫が bound_to_owner だから、入れられないのは手元に怪我を持ちようがないから。スロット側に「書き込み禁止」 のような別の属性は要りません。

7.10 put_in(入れるのにかかる時間)

put_in: {duration: ...} を書くと、そのスロットへ物を入れるのにゲーム内時間がかかります(既定は一瞬)。 duration は操作のそれと同じ形(リテラルか {subject, prop} 参照、10.2 節)で、self が枠の 持ち主、instrument が入れる物、agent が入れている者です。ここも操作の宣言なので patient は 書けません——枠の持ち主が patient で、self と同じ物だからです(11.5 節「役を書ける場所」)。

slots:
  treatment:
    cell_count: 1
    cell: {accept: {tag: treatment}, max: 1}
    put_in: {duration: 30}      # 包帯を当てるのに30分

時間を課すのは入れる側だけで、出すのは常に一瞬です。当てるのに手間がかかっても外すのは一瞬、という 非対称の方が普通のためです。

値段は枠が持ち、経路は持ちません。 プレイヤーが物を入れる操作はカードへ重ねる・レーンへ落とす・端の 矢印で送る、のいずれでも起こせますが、どれも同じだけかかります。重ねる操作に duration を書いて同じことを 表そうとすると、スロットへ直接落とす経路だけが無料になり、同じことが経路で違う値段になります。

離す前に値段を見せるのは画面の責務です(CardInteraction.md 2 節)。操作の呼び名と説明は slot_texts の put_in(Localization.md)に書きます。

7.11 visible_slots(外から中身が見えるスロット)

visible_slots は、その型の中身が外から見えるスロットを、見せたい順に並べるオブジェクトレベルの キーです(stack_order と同じ位置に書きます)。並んだスロットは子ウィンドウのタブになります (Windows.md 1.2 節)。

traits:
  item_container:
    visible_slots: [contents]     # かごは中身が見える器。それがこの型の存在意義そのもの

object_defs:
  raft:
    traits: [location]
    visible_slots: [structure]    # 帆は外から見える。積荷は乗り込まないと分からない

「見える」と「入れられる」は別です。 筏の積荷は名乗っていませんが、筏のカードへ重ねれば積めます (7.8 節)。中を見るには乗り込む、という違いがそのまま操作の形になります。

7.12 storage(物を溜める入れ物)

storage: true は、その型を物を溜める入れ物として使うことを宣言するオブジェクトレベルのキーです (既定 false)。名乗った型は、上限(capacity、7.3 節)を持つスロットの詰まり具合をカードのバーに 出します(CardView.md 8 節)。

traits:
  item_container:
    storage: true             # かご。中身の枠の詰まり具合をバーに出す

object_defs:
  raft:
    storage: true             # 筏。積荷の枠が上限を持つ

7.13 resists(条件が成立する間、持ち主を持てない)

resists は、その条件が成立している間はどの持ち主にも付けないことを宣言するオブジェクトレベルの キーです(stack_order と同じ位置に書きます)。値は条件の並びで、書き方は conditions(14 節)と 同じ——暗黙の AND で、主語の既定は self です。条件の並びなので、要素に reason は書けません (14.6 節)。宣言しなければ、いつでも誰の下へでも入れます。

object_defs:
  feral_boar:
    tags: [item]
    resists: [{prop: wariness, gte: 1}]   # 荒ぶっている間は手にも台車にも入らない

7.9 節の bound_to_owner と対をなします。あちらが「今の持ち主から離れられない」を言うのに対し、 こちらは成立している間だけ「そもそも持ち主に付けない」を言います。

7.14 remove_slots(配られたスロットを落とす)

remove_slots は、混ぜ終わった slots から名指ししたスロットを落とします(slots と同じ位置に書く オブジェクトレベルのキー)。trait が配ったスロットを持たない型を書くための唯一の口です。

traits:
  location:
    slots:
      items: {cell: {accept: {tag: item}}}
      fixtures: {cell: {accept: {tag: fixture}}}
  sea_zone:
    remove_slots: [items]        # 海は物を置ける場所ではない

object_defs:
  coastal_waters:
    traits: [location, sea_zone] # itemsを持たない場所になる

8. passives(持続する影響)

passives は、効果の対象との関係(何を対象に書けるかは 8.1 節)とゲート(常時/conditions/ プロパティの stage)に紐づき、その関係が続く限り評価され続ける、持続する影響を表します。 次のレベルで定義でき、いずれも同一の記法・実行原理を用います。

  1. オブジェクトレベル(例: 防具そのものが持つ複数の恩恵の束)
  2. プロパティレベル(例: アイテムの重量プロパティが持ち主の負荷に寄与する)
  3. プロパティのステージレベル(6.4 節)
  4. 操作レベル(11.7 節。duration を進めている間だけ効く。書ける動詞はここより狭い)

passives は常に配列です(conditions/stages/cells/pick と同じ規約で、要素が1つの場合も配列で書きます)。 配列の各要素が独立した1つのブロック(8.1 節)で、複数のブロックを並べることで、conditions ごとに 異なる効果を与えられます(8.5 節)。

8.1 文法: 操作が上位、対象が下位

passives の各要素は、操作をキーとする辞書型です(書ける操作は以下の各節)。modify/add の中には、効果の対象を識別子とする対象キーの辞書がぶら下がります。何を対象キーに書けるかは、場所ごとに 数え上げるのではなく、その宣言が置かれた場所が何を持つかで決まります——一覧は 14.1 節の表(操作の関係の役は 11.5 節「役を書ける場所」)を見てください。ここは child(付いている子それぞれへ登録を配る)を対象に書ける 唯一の場所です。modify の対象にできる範囲はさらに狭く、操作の関係の役では agent にしか押せません (add にこの制限はありません)。理由は 8.3 節。

object_defs:
  armor_leather:
    covers: [torso]
    layer: base
    passives:
      - conditions:
          - {in_slot: equip}
        modify:
          parent:
            defense: 5
            speed: 3
            accuracy: 2

8.2 conditions(ゲート)

8.3 modify

条件が真である間だけ計算式に寄与する、可逆的な修飾子です。条件が偽になった瞬間に寄与は自動的に消えます。値 そのものを書き換えるのではなく、実効値を都度導出するための入力として扱います。

8.4 add

条件が真である tick 毎に、対象プロパティの実体値へ直接・不可逆に加減算します(例: 出血中の血液量減少、耐久値の 毎 tick 減少)。modify と全く同じ登録・ゲートの仕組みを使い、違いは可逆か不可逆かだけです。

操作の関係の役では agent にしか押せない、という 8.3 節の制限は add にはありません(対象に何を書けるかは 8.1 節)。絞られているのは、可逆な寄与が実効値の上で合計され、誰の寄与かを見分けられなくなるためです。add が 積むのは実体値で、積まれた結果は世界に起きた事実なので、後から「誰の操作の分か」を問う必要がありません——2 人が 同じ傷を手当てすれば 2 倍治るのが、add の意味そのものです。

動詞が名乗るのは可逆性だけで、一度きりか tick 毎かは置き場所が決めます。 同じ add(9.2 節)を 兄弟キーとして直に書けば実行の瞬間に 1 回、passives の中に書けば条件が真である tick 毎。transfer (9.5 節)も同じ 1 語で両方に置けます。動詞にも「一度きり/継続」を持たせると、ブロックと動詞という 必ず一致しなければならない 2 つの宣言を書くことになり、一致しない組み合わせはロード時エラーにしか なりません(つまり動詞側は情報を足していません)。

この線は interactions の中でも同じで、操作も両方を持てます(11.7 節)。ただしそちらの passives に置けるのは modify と add だけです——tick 毎の輸送は宣言した物の tick で走るので (8.4.1 節)、物ではない操作には走らせる時点がありません。

逆に modify は passives にしか置けません。一度きりの可逆な寄与は成立しない——誰がいつ戻すのかが 決まらないためです。この非対称は例外ではなく、動詞が可逆性を名乗っていることの帰結です。

継続の意味を持たない動詞(set・destroy・spawn・move・signal・become)は active にしか 置けません。毎 tick 壊し続ける物は最初の 1 回で居なくなります。

8.4.1 passives の中の transfer

transfer(9.5 節)を passives に置くと、条件が真である tick 毎に走る輸送になります。

props:
  stomach:
    value: 0
    range: {min: 0, max: 32}
  digesting:
    value: 0
    range: {min: 0, max: 64}
passives:
  # 胃の中身が毎tick 1ずつ腸へ移る。空の胃からは動かない(出せる量しか動かないため、条件は要らない)
  - transfer: {from_prop: stomach, to_prop: digesting, amount: 1}
  # 腸から蓄えへ。to_amount がそのまま吸収率になる
  - transfer: {from_prop: digesting, to_prop: body_fat, amount: 1, to_amount: 1}

add の対で書くのとは、次の点で違います。

輸送は宣言順に適用され、互いの結果を見ます(active の命令と同じ、9 節)。上の例で 1 tick に 起きるのは「胃から 1 出る → 腸から 1 出る」の順で、腸へ届いた分はその tick のうちに蓄えへ抜けます。

そのため、直列に繋いだ輸送の中間に在庫を持たせるのは、速度の差です。上流を速くすればその差が 中間へ溜まり(amount: 2 → amount: 1 なら毎 tick 1 ずつ)、同じ速度なら中間は素通りになります。 エンジン側で「同じ tick では 1 段しか進ませない」ようにしても、稼げる遅れは 1 tick(15 分)だけで、 何時間もの緩衝は結局この差が作ることになります。置き場所が変わっても動詞の意味は変わらない、を 保つほうを選んでいます。

輸送は寄与として登録されません(2 つのプロパティを同時に動かす操作は、どちらか一方への寄与としては 表せないため)。宣言したオブジェクトの tick で走り、from/to はそのつど辿り直されます。

8.4.2 積む刻みは tick で、分へは割らない

passives の add が積む刻みは tick で、ゲーム内の分ではありません。 tick 境界を跨がない時間経過 では、率で動く値は 1 つも動きません。跨いだときは跨いだ tick の数だけ宣言した量がそのまま積まれ、 端数の分は、持ち越されるのでも捨てられるのでもなく、そもそも量を持ちません。

これで総量はずれません。 tick が回るのは絶対時刻が minutes_per_tick の倍数になる瞬間 (ActionSystem.md 6.2 節)なので、ある区間で回る tick の数は、そこを何回に 刻んで進めても変わりません。 刻みの粗さが出るのは、区間の中の見え方だけです。

分へ割らないのは、tick が率の刻みであると同時に、率ではないものの刻みでもあるためです。

粒度が残り時間の数字として見えてしまう側は、tick の格子の上で答えを出すことで解いてあります (PropertyValue.ticksUntilMax と World.minutesUntilTick、../ui/CardView.md 15 節)。

見張るのは tests/world-codex/rateIntegrationGrain.test.ts。総量を見る検査では分へ割る積分と見分けが 付かないので、跨がない経過で動かないことと、端数が量を持たないことを見ています。

8.5 複数ブロック(conditions の使い分け)

同じ対象(例: parent)に対して、状況によって異なる効果を与えたい場合は、passives に複数のブロックを並べます。 典型例は、装備するスロットによってボーナス量を変えたい武器です。

object_defs:
  sword:
    passives:
      - conditions:
          - {in_slot: main_hand}
        modify:
          parent:
            attack: 5
      - conditions:
          - {in_slot: off_hand}
        modify:
          parent:
            attack: 2

各ブロックは独立して登録・評価されます。同じ対象・同じプロパティに対して複数のブロックが同時に有効になった場合、 modify は 8.3 節の通り単純加算で合成され、add も有効な全ブロックの量がそれぞれ tick 毎に反映されます。

8.6 ancestor(祖先を辿る対象)

ancestor は、このpassivesを宣言したオブジェクト自身の直接の親から遡り、対象プロパティを定義している 最初の祖先を対象にします(見つからなければこの効果は何も起きません)。parent(1階層固定)と異なり、 途中の親がそのプロパティを持たない場合でも、さらに上の祖先へ自然に届きます。

典型例は、部屋に置かれた暖炉が、その部屋(自分の直接の親とは限らない)の気温を上げる効果です。

object_defs:
  fireplace:
    passives:
      - conditions:
          - {prop: lit, eq: 1}
        modify:
          ancestor:
            ambient_temperature: 5

暖炉自身が ambient_temperature を持たない容器の中に置かれていても、その容器を素通りして、実際に定義している 祖先に効果が届きます。祖先を辿った先が後で移動した場合は再解決され、新しい祖先へ効果が自動的に切り替わります。

ancestor は conditions(14 節)の subject でも、base(6.5 節)の土台の指し先でも、命令の対象でも、 共通の1つの祖先探索です。探す手がかりはプロパティ名なので、書けるのは指す先のプロパティ名が決まって いる場所だけです(14.1 節)。

9. active(一時的な命令)

active は、アクション・組み合わせ・確率分岐の結果が確定した瞬間に、無条件で1回だけ適用される命令を指す 概念です。持続する条件を表す conditions/ゲートは持たず、passives(8 節)のような登録の仕組みにも乗りません。

active という語自体は、YAML 上の専用キーとしては書きません。この節で説明する操作を、 それが書ける場所(9.7 節)の中に、 trigger/conditions/weight と対等な兄弟キーとして直接書きます。確率分岐の pick(10 節)も これらと対等な兄弟キーで、いくつでも並べて書け、書いた順に実行されます(9.7 節)。

9.1 文法: 操作が上位、対象が下位

操作をキーとして直接書きます。set/add の中には、対象キーの辞書が ぶら下がります。何を対象キーに書けるかは、場所ごとに数え上げるのではなく、その宣言が置かれた場所が何を 持つかで決まります——一覧は 14.1 節の表(操作の関係の役は 11.5 節「役を書ける場所」)を見てください。 ancestor を対象に書いた set/add は、その祖先(8.6 節)へ書き込みます。

実行時に対象へ辿り着けなかった操作は、何も起こしません(指す相手が今どこにも居ない、相手がその プロパティを持たない、など)。

interactions:
  snack:
    trigger: menu
    add:
      agent:
        satiety: 10
    destroy: self

9.2 set / add

どちらも対象プロパティの実体値へ、その場で一度だけ不可逆に反映します。

同じ add を passives の中に書けば、条件が真である tick 毎の加算になります(8.4 節)。一度きりか 継続かを決めるのは動詞ではなく置き場所です。set は active にしか置けません(毎 tick 同じ値を代入し 続ける意味が無いため)。

set/add の値は、どちらもリテラル(数値・真偽値・シンボル名、6.6 節)だけです。他のプロパティを指す {subject, prop} 参照は、この形の参照を値に取れる他の場所と違って使えません。値の 算出を YAML へ持ち込むと定義ファイルがプログラム化していくためです。在庫に応じて動く量が変わる移送は、 専用の動詞である transfer(9.5 節)で表します。

set だけは、値の位置に「その対象キーが名指す個体」を書けます({subject: parent})。書き込まれるのは その個体のインスタンスID(9.6 節)で、実行時に決まった相手を覚える唯一の手段です。覚えた相手は move の to_prop・spawn の into_prop がそのまま引きます——筏は出航のときに自分が居た海岸を覚え、 島へ戻る航路はその個体へ帰します(Voyage.md 3.5 節)。

# 筏(voyage.yaml の raft)。海岸に居るかの条件は省いている
set_sail:
  trigger: menu
  duration: 60
  set: {self: {home_coast_id: {subject: parent}}}   # 押し出す前に、出た海岸を覚える
  move: {subject: agent, to: self}
  pick:   # 押し出す先の海区は、出た海岸ごとの重みで引く
    - weight: {subject: parent, prop: offshore_coastal_waters}
      move: {subject: self, to_object: coastal_waters}
    - weight: {subject: parent, prop: offshore_tide_rip}
      move: {subject: self, to_object: tide_rip}
    - weight: {subject: parent, prop: offshore_gull_rock}
      move: {subject: self, to_object: gull_rock}
  spawn: {object: route_to_shore, into: parent}   # 島へ引き返す道

書けるのは対象キーだけで、prop は書けません。{subject, prop} は「そのプロパティの実効値が指す個体」 ——つまり値を読む形なので、上の規則のとおり値の位置には置けません。リテラルとの見分けはスカラーか マップかで、値の位置ではスカラーが既にリテラルの綴りだからです(parent と書いてもシンボル名です)。

個体は割れないので、収まらない分は捨てます。 カード1枚を消費する操作(食べる・飲み干す・くべる)は、 add が宣言した量をまるごと効かせます。受け取るプロパティの range.max を越えた分は on_max (既定のクランプ、6.3 節)が捨てるだけで、カードが食べかけで残ることはありません。満腹まであと少しの ときに大きな食料を食べれば、その差は失われます——「入る分だけ食べる」を書こうとすると、食べかけの オブジェクトという状態を型ごとに用意することになります。

一部だけを使えるのは中身入りの容器(LiquidContainerSystem.md 1 節) だけです。そちらは transfer(9.5 節)が「入る分だけ」動かし、余りを移し元へ残します。この違いが個体と 量の境目そのもので、どちらとして表すかを決めれば、あふれた分の行方も決まります。

9.3 destroy

削除したい対象を直接指定します。単一の対象なら destroy: self のようにスカラーで、複数の対象を同時に削除する なら destroy: [self, instrument] のようにリストで書きます。対象は move の subject と同じ二択で、 対象キー(何を書けるかは、その宣言が置かれた場所が何を持つかで決まります——一覧は 14.1 節の表、操作の 関係の役は 11.5 節「役を書ける場所」)のほかに、インスタンスIDを保持するプロパティ (destroy: {prop: smash_target}、9.6 節)でも指せます。すべてのオブジェクトは必ずworldを根とするツリーに 所属するため(7.1 節)、destroy は「現在の親スロットから切り離す」こととして表現できます。繰り返し実行しても 安全(冪等)です。spawn と併記するときは destroy を先に書きます(9.4 節)。

中身は道連れにならず、消える物の親へこぼれ出ます。 壊れたかごの中の物は地面に散らばるのであって、 一緒に消えるわけではありません。ただし単独では在れない物(bound_to_owner、7.9 節)は例外で、 こぼれずに一緒に外れます——割れた器の水は残りません。こぼれた先がすでにその tick の中身を数え終えて いれば、その物はそのtickの残りだけ動きません(1 tick の取りこぼしは、1日96 tickの刻みでは無視できます)。

消し方に名前を添える(reason)

どう消したのかは、消した宣言がその場で名乗ります。 対象をマップで書くと reason を添えられ、 名前は消された側に残ります(WorldObject.destroyedReason)。

hydration:
  range: {min: 0, max: 336}
  on_min:
    destroy: {subject: self, reason: dehydrated}   # 渇きで死んだ

マップの中身は subject(省略時は self)・prop・reason で、prop を書けばその実効値が インスタンスIDとして指す相手(destroy: {prop: smash_target, reason: crushed})、書かなければ subject そのものを指します。スカラーとリストの書き方はそのままで、リストの要素にもマップを置けます。 空のマップ(destroy: {})はロード時エラーです——既定に任せれば動きますが、それは何も書かずに destroy: self を得る抜け道で、書く理由がありません。

表示文言は locale の destroy_reason_texts(Localization.md destroy_reason_texts節)が持ちます。段(6.4節)とも要件の reason(14.6節)とも別の名前空間で、 同じ綴りを当てても構いませんが、揃っていることを当てにはしません。

名乗らない消滅は、何も残しません。 立ち去った獣(stay_remaining の on_min)と渇きで死んだ獣は、 残った値から見れば同じ「下限で消えた」ですが、名乗るかどうかで分かれます——消えたことの意味を 知っているのは消した側だけなので、後から状態を見て推測しません (VitalsSystem.md 6 節)。識別子は表示のためだけに使い(signal の名前・要件の reason と同じ扱い、9.8 節・14.6 節)、エンジンは名前の意味を知りません。

9.4 spawn

{object, into} を指定すると、新規オブジェクトを生成し、指定した場所へ配置します。spawn は常に self (この active を宣言したオブジェクト自身)が実行するものとみなすため、set/add/destroy のような対象 キーのラップを挟みません。

種類が変わる演出は destroy → spawn の順に書きます({destroy: self, spawn: {...}})。破棄によって 位置が空いてから配置することで、スタック表示(7.6節)における位置の引き継ぎが素直に実現できるためです。 ただし、これはそう書けば入れ替わったように見せられる場合がある、という程度の規則です。destroy で そのセルが必ず空くとは限らず(他の物がスタックしている・そもそも destroy の対象が居ない)、位置の 引き継ぎは配置時のスロットの状態から決まります(SlotSystem.md 3節)。同じ個体の まま型を差し替えるのは become(9.9 節)です。 destroy → spawn で立つのは別の個体なので、プロパティもスロットの中身も 引き継ぎません。

挿入先のスロット名は書きません。生成物を受け取るオブジェクト(配置先が指す起点)が持つスロットを 宣言順に走査し、最初に配置できたスロットへ入れます。著者はスロット名を知る必要がありません。走査の対象外に できるスロット(placement)については 7.7 節を参照してください。

配置先は into と into_prop と into_object のどれか1つで指します。個体を指す書き方は move の 移動先(to/to_prop/to_object、9.6 節)とまったく同じで、違うのはキーの綴りだけです。into だけは、 個体ではないもの——位置(same_slot)と走査(child)——も名乗れます。

spawn: {object: rotten_wood}
destroy: self
spawn: {object: item_coconut, into: agent}

count(生む個数、省略時は1) は、同じ宣言を並べるのと同じ意味です。1個ずつ順に生んで配置します (「ヤシの実が2個見つかる」を、同じ行を2度書かずに書けるようにするためのものです)。

spawn: {object: item_coconut, count: 2, into: self}

配置先が指す起点のどのスロットにも枠の要件・capacityが合わず配置できなかった場合、fallbackはYAML上に 存在せず、その起点自身の親へこぼれます。こぼれた先でも枠の宣言はそのまま効きます——受け取ってもらえ なければさらにその親、さらにその親…と遡り、どこにも入らなければそのオブジェクトは世界から消えます (7.1 節。存在しなかったのと同じ扱いです)。たとえば上のinto: agentの例は、収穫したアイテムをまず プレイヤーの手持ちへ入れようとし、埋まっていればagentが今いる場所(agentの親)へ落ちる、という 「取得アイテムの置き場所」パターンになります。

枠が受け入れないものを押し込むことはしません。 器に入らない物は器の外——手に持てなければ足元へ、 足元にも置けなければ失われる、という順で落ちていくだけです。この落ち方は destroy のこぼし(9.3 節)とも become が受け取れなかった中身(9.9 節)とも同じ1つの規則です。

9.5 transfer

set/add は固定の値・量しか書けないため、「在庫に応じて実際に動く量が変わる」移送(例: 容器に入っている 分だけ回復する水分補給)を表現できません。transfer は、from が指すプロパティの実体値のうち、実際に 出せる量と amount の小さい方だけを、to が指すプロパティへ移す専用の動詞です。

出せる量の床は、from のプロパティに range があれば range.min、無ければ 0 です。 出せる量は 実体値から床を引いた分(0未満にはならない)で、出し切った値はちょうど下限に着きます——それでも 「出し切った」に反応できるのは、on_min が下限そのもので発火するからです(6.3 節)。

add と同じく、passives の中に置けば tick 毎に走る輸送になります(8.4.1 節)。以下の文法は どちらの置き場所でも同じです。

interactions:
  sip:
    trigger: menu
    transfer:
      amount: 2000
      from_prop: volume
      to: agent
      to_prop: hydration
# お茶(liquid_containers.yaml の tea_liquid trait)。喉が渇いているかの条件は省いている
interactions:
  drink:
    trigger: menu
    duration: 5
    transfer:
      # 250mL出し、飲みきると水分が10 tick分回復する
      amount: 250
      to_amount: 10
      from_prop: fill        # 器の中のお茶の量(mL)
      to: agent
      to_prop: hydration
      linked_add:
        agent:
          # 実際に出した量に比例: 250 飲めば +2、125 飲めば +1
          wakefulness: 2

from/from_prop/to/to_prop をフラットな4フィールドにしているのは、conditions(14.1 節)の subject/prop と同じ理由です(from・to それぞれ参照は常に1組であり、ネストする必要がないため)。 移送の端点は主語ではないので、subject ではなく方向を表す from/to がそのまま名前になります。

9.5.1 準量的な物をくべる操作は、入り切らない端数を切り捨てる【確定】

塊で数える物が量を持ち、それを器へ移して物ごと消す操作——炉へ薪をくべる (FireSystem.md 2 節)・罠へ餌を仕掛ける(TrapSystem.md 4 節)・ 囲いへ飼葉を入れる——では、器に入り切らなかった分を切り捨てます。 残りを物へ戻すことも、丸ごと 入るまで受け取らないこともしません。

受け取らない形にすると、補給そのものが塞がります。 いちばん小さい薪 1 つぶんより器の空きが小さい 間、どの薪も入らなくなる——器の容量が最小の塊より小さい設備では、一度も補給できません。捨てる量は 高々 1 つぶんで、それは器が 1 つも受け取れないなら操作が成立しないという規則(12.4 節)が 保証します——満杯を拒む条件(FireSystem.md 2 節)が担っているのは、断る理由を届けることだけです。

出どころ: ユーザーの指示・2026-09-29(「端数は切り捨ててください。入り切らない場合に受け取らないと、 燃料アイテムの最小単位より容量のほうが小さいときに燃料補給が不可能になります。これは準量的アイテムの 基本ルールです」)

9.5.2 切り捨てることは、宣言の側が名乗る

9.5.1 節の切り捨ては、transfer へ allow_overflow: true(9.5 節)を書いて名乗ります。 相手(instrument)から移して同じ操作がその相手を destroy するのに、これを書いていない宣言は ロード時に落ちます(src/loader/parseInteractions.ts。pick(10 節)の候補へ分けて書かれていても 見ます)。見るのは操作の効果だけです——destroy を書ける場所(9.7 節)のうち、instrument が居るのは 操作だけだからです(11.5 節「役を書ける場所」)。既定の false は「入り切らない分は出どころに残す」という意味ですが、その 出どころが消えるなら残した分も消えるので、黙って捨てる形と区別が付かなくなるからです。

名乗らせ方は仮決めです(9.5.1 節が確定しているのは切り捨てること自体だけ)。専用のキーを足して 「端数は切り捨てる」を直接言わせる形も書けますが、allow_overflow の既存の意味(あふれる分は to の on_max へ委ねる、9.5 節)がそのまま切り捨てなので、同じことを言うキーを 2 つ持たせていません。

9.6 move

move 以外の操作(9 節)は、対象の所属先(どの親のどのスロットに入っているか)そのものは 変更しません。move は、既に世界に存在するオブジェクトを、別の場所(生成時に確定した特定のインスタンス) の中へ移動させる専用の動詞です。transfer(9.5 節)が連続量(プロパティの値)を移すのに対し、move は オブジェクト自身の所属を移します。典型例は、土地と土地を繋ぐ道の移動アクション(agent を移動先の土地へ移す)です。

interactions:
  travel:
    trigger: menu
    move:
      subject: agent
      to_prop: destination_id

move は1つでも配列でも書けます(destroy・transfer と同じ)。同じ一手で2つ動かす——筏に乗り込んで から、その筏ごと漕ぎ出す——のに使います。適用は書かれた順です。

# 小島の岸から漕ぎ出す(voyage.yaml の offshore_islet の launch)。条件は省いている
launch:
  trigger: menu
  duration: 30
  pick:
    - weight: 1
      among: {slot: fixtures, matches: {object: raft}}   # 岸の筏を picked にする(10.3 節)
      move:
        - {subject: agent, to: picked}
        - {subject: picked, to: parent}   # 小島が居る海区へ

プロパティが個体を object_defs の id(型)ではなくインスタンスIDで指すのは、典型例(生成された特定の道が 指す特定の移動先)が、定義時点では決まらず、実行時に初めて確定する個体を指す必要があるためです。実際の解決は、self から所属ツリーの根(world、15 節)まで遡り、そこから InstanceId が 一致する子孫を探すことで行います。解決できない場合(to_prop を持たない、該当インスタンスが世界のどこにも 存在しない)は、他の対象解決の失敗(9.1 節)と同じく、何も起きません。

この解決は、インスタンスIDをプロパティから引く口すべてに共通です——move の to_prop・subject_prop、 spawn の into_prop(9.4 節)、destroy の prop(9.3 節)。そのどれでも、書き込まれる前の 0 は どの個体も名乗りません(NO_INSTANCE、src/domain/WorldObject.ts)ので、宣言が既定値のまま置かれていても 上の「解決できない場合」と同じ扱いになります。個体を指すプロパティを新しく宣言するとき、既定値に 0 を 書いてよいのはこのためです。

to: self は、組み合わせた相手(instrument)を宣言している側の中へ入れる操作(液体を注ぎ移す等)を 書くための形です。

traits:
  pourable_vessel:
    interactions:
      pour_in:
        trigger: {drag: {tag: liquid}}
        move: {subject: instrument, to: self}

物を入れ物へしまうだけなら、これを書く必要はありません。 受け取れるスロットを持つカード( 7.8 節)へ重ねたらそのスロットへ入る、という動きが画面側にあります (CardInteraction.md 2 節)。重ねる操作を書くのは、 入れる以上のこと(量として混ざる・時間がかかる・条件が要る)を起こしたいときだけです。

移動先が移動対象自身か、その中身である場合(入れ物を自分の中へ入れる)は、配置は必ず失敗します (7.1 節)。この判定は 枠の要件・capacity とは別の木構造の不変条件なので、こぼれ落ち(9.4 節)の 途中でも省かれません。

配置先のスロット名は書きません。spawn の into(9.4 節)と同様、移動先が持つスロットを宣言順に走査し、 最初に配置できたスロットへ入れます。ただし spawn と異なり、move には親へのこぼれ落ち(9.4 節)は ありません。移動対象は今いる場所から動かないだけで済むので、配置に失敗した場合は単に元の場所に留まります。

move は interactions/pick の各候補のいずれでも書けます。on_max/on_min (6.3 節)でも書けますが、そこに実行者は居ないので 操作の関係の役(11.5 節)を指す形だけがロード時エラーに なります(self と型・プロパティで書いた移動は成立します——距離が尽きた瞬間に本土へ着く、など)。

9.7 この節の操作が書ける場所と、実行される順

この節の操作は次のいずれかの位置に、専用のラップを挟まず直接書きます。持続する条件を表す conditions/ゲートを 持つ passives とは、書ける場所が構造上重ならないため、両者を混同する余地はありません。

同じ場所へいくつでも並べて書け、適用順は YAML に書かれた順です。 動詞ごとの優先順位はありません (pick(10 節)も、この節の動詞と対等に並べられる 1 つの操作です)。順序に意味がある組み合わせは、 その意味のとおりの順に書きます——同じプロパティへの set と add、置き換えの destroy と spawn (9.4 節)などです。

interactions:
  flip:
    trigger: menu
    add: {self: {fatigue: 1}}   # 分岐に関わらず必ず起こること
    pick:                       # 分岐する部分。共通処理を全候補へ複製しなくてよい
      - weight: 10
        spawn: {object: gore_wound, into: agent}
      - weight: 5
        destroy: self

9.8 signal(起きたことを告げる)

世界の形を何も変えず、出来事が起きたことだけを観測する側へ渡します。 値は出来事の識別子です。

interactions:
  swat:
    trigger: {drag: {tag: weapon}}
    pick:
      - weight: 70
        spawn: {object: laceration, into: self}
        signal: hit
      - weight: 30
        signal: missed          # 世界は何も変わらないが、外したことは起きている

誰の身に起きたかは、他の操作と同じ対象キー(9.1 節)で指せます。 対象を省いた signal: missed は self に起きたこととして告げます(宣言した型そのものに起きる場合が大半のため)。

interactions:
  swat:
    trigger: {drag: {tag: weapon}}
    signal: {instrument: chipped}   # 殴った側(重ねた武器)に起きたこととして告げる

対象キーは誰に起きたかを言うだけで、指す先のプロパティ名を決めません。そのため、その名前で祖先を探す ancestor はここでは書けません(ロード時エラー。14.1 節の表)。

外した回に何も書かないと、その一手は「起きなかった手」と見分けが付きません。観測する側が読むのは 世界に起きた物の出入り(ActionSystem.md 7 節)なので、出入りを伴わない出来事は、 起こした側が告げなければ誰にも届きません。

9.9 become(同じ個体のまま型を差し替える)

インスタンスをそのままに、型だけを別の型へ差し替えます。 水を汲んだ甕が「水入りの甕」になる、 といった同じ個体の作り変えのための操作です。

destroy → spawn(9.4 節)では代用できません。あちらが引き継げるのは位置だけで、インスタンスは 別物になります——札は消えて出直し、プロパティの値も移らず、スロットの中身は親へこぼれます。

interactions:
  collect_sap:
    trigger: {drag: {tag: liquid_container}}
    conditions:
      - {reason: not_empty, subject: instrument, prop: fill, eq: 0}
    duration: 5
    become: {subject: instrument, content: sap_liquid}

行き先は識別子ではなく座標で指します(3.5 節)。subject 以外のキーは軸の値で、書かなかった軸は 変化前の自分から引き継ぎます。上の例は「重ねられた容器を、中身が樹液の版にする」で、木の側はどの容器が 来るかを知りません。中身を空にするのは become: {subject: self, content: none} です。

生成されていない素の型も、座標の一点です。 書かなかった軸を引き継ぐという規則が成り立つには、 どの型もどこかの座標に居なければなりません——液体容器なら、手で書いた空の容器が content: none に あたります。

「同じ物が姿を変える」すべてがこの操作ではありません。 行き先は座標なので、手で書いた別々の型の あいだは行き来できません——石と尖った石、ヤシの実と皮を剥いたヤシの実は、それぞれ独立した型なので、 移り変わりは destroy と spawn が担います(副産物が同時に生まれるのも、そちらなら書けます)。 1 つが 2 つに増える変化は、個体が 1 つ続くというこの操作の前提そのものに合いません。

9.9.1 何を引き継ぎ、何を失うか

スロットが合わないことを理由に、変化そのものを拒みはしません。 拒むと、起きなかったことも その理由も画面に出ません。こぼれるなら結果が見えますし、スロットの違う型へ意図して変えることもできます。

製作の完成——progress の on_max で作りかけが完成品になる——がこの操作です (RecipeSystem.md 1 節)。

10. pick(重み付き確率分岐)

pick は、9 節の操作を直接書ける場所であればどこでも、それらと 対等な兄弟キーとして書ける、重み付き候補のリストです(並べた場合の実行順は 9.7 節)。passives (無条件/conditions/stage 内)には書けません。passives は「いつ振るか」という瞬間を持たない、 関係とゲートに基づく継続的な評価だからです。

10.1 基本構造

各候補は weight に加えて、9 節の操作(文法そのまま)を weight と対等な 兄弟キーとして直接持ちます。候補が1つしかない場合は、重みの値に関わらず必ずそれが選ばれます。

interactions:
  attack:
    trigger: menu
    pick:
      - weight: 50
        destroy: self
      - weight: 50
        destroy: agent

候補ごとに影響を受ける対象(self/agent など)そのものが異なるケースも、このように表現できます。pick の 入れ子は再帰的であり、候補の中にさらに別の pick を書くこともできます。

各候補に書ける内容は、一時的な命令(9 節)に限られます。modify は関係が続く限り評価され続けることに意味がある仕組みのため、1回選ばれて終わる pick の候補に書く意味がありません (add/transfer を書けば、passives の中と違って一度きり効きます、8.4 節)。

10.2 weight

weight は、専用の計算式(base値+条件付き補正)を新設せず、以下のいずれかとして表現します。

この二択より豊かな書き方はありません。 式(積・和・条件付きの補正)を書ける場所を作らないのは、 可逆な寄与を合成する器を modify・base の 1 つに保つためです(2 節)。参照が解決できなければ 0 で、 値が無いこととは区別しません(宣言はされているため)。

負の重みは 0 として扱い、どの候補の重みも 0 以下なら宣言順で先頭の候補を選びます。 何も起きない 手番を作らないための規約で、フォールバック候補を YAML 側で名指しする記法はありません。1 つでも正の 重みが残っていれば抽選は成立します——負の候補が混ざっていても、そちらが選ばれないだけです。

2 つの値を合わせたいときは、合わせた結果を持つプロパティを 1 つ作り、そこを参照します。土台を他の オブジェクトから引くなら base(6.5 節)、条件で足し引きするなら modify(8.3 節)で、どちらも加算です。

pick:
  - weight: {prop: accuracy}
    add:
      self: {hp: -10}
  - weight: {prop: evasion}
    destroy: instrument

外部からの干渉(「戦闘スキルが高いほど命中しやすい」等)は、weight 自体に専用の補正記法を用意するのではなく、 参照先のプロパティに対する通常の modify 効果として表現します。

10.3 among(周りの物から相手を選ぶ)

候補に among を書くと、その候補が選ばれたときに、周りの物からさらに相手を1つ選んでから効果を 当てます。選ばれた相手は picked(14.1 節の対象キー)で指します。

- weight: {prop: snatch}
  among:
    subject: parent            # 候補を探す相手(省略時は self)
    slot: items                # そのスロットの中身が候補
    matches: {not: {tag: quarry}}   # 絞り込み(省略時は中身すべて)
    weight: {subject: picked, prop: volume}   # 候補ごとの重み(省略時は一律)
  move: {subject: picked, to: self}

集合の指し方(subject + slot + matches)は、条件の「そのスロットに当てはまる子が居るか」 (14.3 節)とまったく同じ形です。足しているのはその中から1つ選ぶことと、その重みだけ。

picked を指せるのは、among を書いた候補の中(重みと効果)だけです。それ以外の場所で指すと ロード時エラーになります(14.1 節)。

11. interactions(操作)

操作(eat、travel、chop など)は、きっかけ(trigger)と条件(conditions)と実行結果 (9 節の操作、または pick)、実行にかかるゲーム内時間(duration)、経過している間ずっと効くもの (passives、11.7 節)、および始まったことの告知 (announce)を1つの定義としてまとめて持ちます。object_defs/traits の中に interactions として 配置します(トップレベル独立キーにはしません)。

操作は1つの節・1つの名前空間です。 押して起こすものも、時間が起こすものも、カードを重ねて起こす ものも、違うのはきっかけだけ——条件・効果・所要時間の書き方は丸ごと同じです。同じカードに同名の 操作が2つ並ぶとプレイヤーが見分けられないので、名前は1つの節の中で一意になります。表示文字列も 1つの節へ書きます(Localization.md 対応表の形式)。

traits:
  eatable:
    interactions:
      snack:
        trigger: menu
        conditions:
          - {subject: agent, prop: satiety, lt: max}
        add:
          agent:
            satiety: 10
        destroy: self

11.1 trigger(きっかけ)

何がこの操作を起こすかを宣言します。必須です。

書き方 起こすもの 画面のボタン
trigger: menu プレイヤーが押したとき 出る
trigger: tick 時間が経ったとき 出ない
trigger: {drag: ...} そのカードを重ねたとき(12 節) 出ない(相手が要るため)

画面に出すかは、きっかけから決まります。 ボタンとして並ぶのは menu だけです。

tick は、プレイヤーが押す機会の無い操作です。名前で指して実行される点だけが menu と同じに なります(時間の経過が配る動物の 1 手、HuntingSystem.md 5 節。尽きた値が 起こす強制的な時間経過、../world/Characters.md 限界節)。

気を失っている物には配られません(VitalsSystem.md 6 節)。手番が回るかどうかの 判断は配る側が1箇所で持つので、tick の操作を足すときに「意識があれば」を書き足す必要はありません。

duration を持つ tick の操作は、配られたその場では起きません。 手番を配るのは時間を進めている 最中で、その中で時間は進められないためで、操作の切れ目まで待ってから起きます(11.5 節)。待つ間に 要件を満たさなくなっていれば、そこで落ちます。

時間が配るのは手番で、動くのはその物自身です。 きっかけが違うだけで役は同じなので、tick の操作にも agent が居ます(11.5 節)——動物の 1 手の agent は動物です。

11.2 conditions

葉と all/any/not の複合ノードからなる条件木です(書ける葉の一覧は 14 節)。トップレベルは 常に配列で、暗黙の all(AND)として扱われます。ここは要件の並びなので、要素ごとに reason (14.6 節)を書けます。

11.3 duration(実行にかかるゲーム内時間)

duration は、この操作を実行するのにかかるゲーム内時間(分)です。trigger/conditionsと対等な 兄弟キーとして直接書きます。省略すれば時間を消費しません(例: eatのような即座に終わる操作)。 きっかけによらず同じキーを同じ意味で持ちます。

interactions:
  explore:
    duration: 15          # 探索は1回15分かかる
    ...
  travel:
    duration: {prop: travel_minutes}    # 道が持つ移動時間をそのまま使う
    ...

値は、weight(10.2 節)と同じリテラル数値・{subject, prop} 参照の二択で、10.2 節と同じ解決規則を 共有します。操作の中なので subject には操作の関係の役も書けます——どの役を書けるかは、きっかけによって 違います(11.5 節「役を書ける場所」)。

所要時間を持つ側と、それを伸ばす側が別のオブジェクトのときも、ここには式を書きません。 参照先のプロパティが base(6.5 節)で相手の値を土台にすれば、合成は既にそこで済んでいます——道は自分の長さに歩く人の遅れを継ぎ、 duration はその 1 つを読むだけです(11.5 節の例)。

行っている人の腕で縮む時間も、同じ場所が持ちます。 参照先のプロパティが自分の passives(8 節)に 「その腕がこの段以上なら自分を N 分縮める」を書き、duration はその 1 つを読むだけです。passives の conditions には agent を書けるので(11.5 節)、どの腕が・どの段から・何分縮めるかは、その操作の側の 宣言 1 つに収まります(設計は ../world/Skills.md 6 節)。

props:
  spin_minutes:
    value: 30
    passives:
      - conditions:
          - {subject: agent, prop: skill_cordage, in_stage_or_above: skilled}
        modify: {self: {spin_minutes: -15}}

素の分数も、縮めた先も、tick の格子に乗せます(ActionSystem.md 6.2 節)。

条件(conditions)が不成立で操作自体が実行されなかった場合、durationは消費されません。時間の消費は 実行結果が適用される前に行われます(行動してから結果が出る順序であり、作ったもの・見つけたものが 自分の制作時間・探索時間ぶんのtickを浴びずに済むため)。

その経過中に、tick駆動の変化(劣化によるdestroyなど)がself/instrument/agentを消してしまうことが あります。その場合はその操作を成立しなかったものとして打ち切り、効果を適用しません(時間は経過した ままです)。詳細はActionSystem.md 6.1節。

11.4 active / pick

このアクションが実行された瞬間に、9 節の操作(active の実体)と pick(10 節)が、 書かれた順に1回ずつ適用されます(9.7 節)。trigger/conditions/durationと 対等な兄弟キーとして直接書き、専用の active: ラップは挟みません。1つも指定しなければ、条件成立時に 何も起きないアクションになります。

agent は、その操作で動いている個体です(11.5 節)。parent(木構造上の直接の格納先)とは独立した参照で、 プレイヤーが起こす操作(menu・drag)ではプレイヤーキャラクターを指します。

11.5 操作の関係(agent・instrument・patient)

操作は「A が B で C を D する」の形をしていて、参加者には次の役があります。名前は格から採っています。

役 格 何を指すか
agent 〜が その操作で動いている個体
instrument 〜で 宣言が乗っていない側の参加者。運ばれてきて働きかけに使われる物
patient 〜を 働きかけられる物。操作の宣言が乗っている側

格は名前の出どころで、どの物がどの役に就くかを決めるのは「宣言がどちらに乗っているか」です。 だから どちらの名前も、格が言うほど狭くありません。

宣言をどちらに置くかを決めているのが 12.3 節で、重ねる操作では「変化の本体に置く」——それが patient に なります。相手を伴わない操作(menu・tick)では、宣言の置き場所がそのまま役を決めます。

役を書ける場所

宣言が置かれた場所で静的に決まります(14.1 節の「書ける場所は宣言が置かれた場所で決まる」と同じ線)。 この表が唯一の一覧で、14.1 節の表はここを指します。

宣言の置き場所 agent instrument patient
参加者の props(base・passives) ○ ○ ○
interactions(menu・tick) ○ — ✕
interactions(trigger: {drag: ...}、12 節) ○ ○ ✕
put_in(7.10 節) ○ ○ ✕
レシピの解放条件・deftness・crafting_conditions(13.2・13.5・13.4 節) ○ — —
on_max/on_min(6.3 節) — — —
resists(7.13 節) — — —

関係は世界に刻まれます。 条件(conditions)の判定・所要時間(duration)の問い合わせ・効果の適用のすべてを、 関係を張った状態で行います。押す前に見せる分数と、実際に進む分数が同じ数になるのはこのためで、片方だけで 張ると見せた数と進む数がずれます。

役を指せるのは参加者からだけです。 参加していない物(担ぎ手の手の中の道具など)から役を指しても解決しません。

同じ物が複数の関係に加わることがあります。 実行中の操作が関係を張り続けている間に、その参加者が別の 関係にも加わったときです(時間を要さない手番がその場で起きる、押す前の分数を問い合わせる)。このとき、 その物の props から役を指すと最も内側の関係が答えます——後から加わったほうが勝ち、抜ければ外側へ 戻ります。

寄与を受け取っている側が内側へ加わっても、外側の関係の相手が押した寄与は残ります。 内側の関係へ 加わったことは、外側の関係から抜けたことではありません。内側で型が変わっても(9.9 節)、外側で押された 寄与は作り直されたプロパティへ載り直します。押している側が内側へ加わったときは、1 つ上の段落のとおり ——その props の役は内側の関係が答えるので、押す先は内側の相手へ移り、抜ければ外側へ戻ります。

不変条件: 同じ物が 2 つの操作の agent になることはありません(動作主は一度に 1 つの動作しかできない)。 実行時に検査します。instrument・patient は複数の操作に同時に就けます——引くだけなら混ざらないためで、 押す側の制限は 8.3 節にあります。

数えるのは、同時に張られている関係です。 押す前に分数を見せるための問い合わせは、候補ごとに1 つずつ 張って外すので、問い合わせどうしは重なりません(同じ土地から道が 2 本出ていても、2 本ぶんの関係が同時に 在ることはない)。agent として重なるのは、実行中の操作が関係を張り続けている間に、同じ個体を agent とする別の操作が始まったときだけです。

この不変条件から、時間を要する手番は操作の切れ目でしか起きません。 長い移動の最中にも tick は回るので、 そこで同じ個体の別の操作(眠気が限界に達しての強制睡眠、../world/Characters.md 限界節)が始まると、agent が 2 つの操作に就きます。進行中の操作は中断せず、待たせます——歩き終わって から倒れる、が答えです。

中断する側を採らなかったのは、中断とは「時間だけ過ぎて効果が適用されない」ことだからです (ActionSystem.md 6.1 節と同じ形)。6 時間歩いて着かない・6 時間かけて何も作れない、が 限界に達するたびに起きることになり、限界そのものより中断のほうが重い罰になります。

荷が重いほど移動に時間がかかる、はこう書きます。

object_defs:
  path:
    props:
      # 道の長さに、今歩いている人の遅れを継ぐ。誰も歩いていなければ60のまま
      travel_minutes:
        value: 60
        base: {subject: agent, prop: travel_delay}
    interactions:
      travel:
        trigger: menu
        duration: {prop: travel_minutes}

  character:
    props:
      # 歩みの遅れ(分)。素は0で、荷重や怪我など遅くするものが自分の段から押す
      travel_delay: {value: 0}

道が知っているのは「自分の所要時間は歩く人の遅れで伸びる」ことだけで、遅れの内訳(荷・怪我・暗さ)は 知りません。 遅くするものが増えても押す先は人の travel_delay なので、道の宣言は変わりません。

11.6 announce(始まったことを告げる)

duration を進める前に、その操作が始まったことを告げます。 書き方は signal(9.8 節)と同じで、 対象を省いた形(self へ告げる)も、対象キーごとに書く形も取れます。

interactions:
  collapse:
    trigger: tick
    conditions:
      - {prop: stamina, lte: 0}
    announce: exhausted      # 倒れ込む120分は、これを告げてから過ぎる
    duration: 120
    passives:
      - add: {self: {stamina: 1}}

signal との違いは告げる時点だけです。 操作の中に書いた signal は効果なので、その操作の時間が 過ぎ切ってからしか出せません(11.3 節)。それで足りるのは、告げたい出来事が結果そのもの (当たり・空振り)であるときです。 足りないのは、時間が過ぎること自体を告げたいときです——尽きた値が起こす強制的な時間経過 (../world/Characters.md 限界節)は、プレイヤーが押した覚えの無い数時間を 進めるので、過ぎ切ってから理由を言えば、眠り込みでは 6 時間後になります。

11.7 passives(経過している間ずっと効く)

duration を進めている間だけ効く持続効果です。 文法は物の passives(8 節)と丸ごと同じで、 違うのは登録が続く長さ——操作が始まったところで登録し、経過し終えたところで外します。

interactions:
  rest:
    trigger: menu
    duration: 60
    passives:
      - add: {self: {stamina: 1}}

「一度きりか tick 毎かは置き場所が決める」の線をそのまま伸ばしたものです(8.4 節)。

書ける対象は、その操作の他のキーと同じ(11.5 節「役を書ける場所」)です。child は書けません ——付いている子ごとに登録を配れるのは、子を持つ物の宣言だけだからです(8.1 節)。

押す先も、ゲート(8.2 節)が見る役も、この操作の関係が答えます。 宣言元が経過中に別の関係へも 加わっても(11.5 節「同じ物が複数の関係に加わることがあります」)どちらも動きません——最も内側の関係が 答えるのは物の props から役を指したときで、こちらの宣言は物ではなく操作のものです。片方だけを 辿り直すと、「誰に効いているか」と「何を見て効くか」が食い違います——焼く者の力を見て焼く者を温める 宣言が、窯自身の手番の内側では窯の力を見ることになります。

tick 毎の輸送(transfer、8.4.1 節)は書けません(ロード時エラー)。輸送は寄与として登録できず、 宣言した物の tick で走るので、物ではない操作には走らせる時点がありません。

duration を持たない操作にも書けません(ロード時エラー)。時間を消費しない操作の中では tick が 1 回も回らないので、書いても一度も効きません。

受け取る量は「宣言 − その間の減り」になります。 tick 毎の増減は、対象の値がその tick に受け取る ほかの増減と同じ足し算にまとまってから実体値へ入るので(ActionSystem.md 6 節)、値が下限に 居ても正味で釣り合います。active の add を経過の後に置くと、下限に張り付いた値では経過中の減りを 既定のクランプが吸い、宣言した量がまるごと残ります——荷を担いだまま倒れ込むほうが、同じ時間を休むより 得になるのはこの形です(../world/Characters.md 限界節)。

時計の位相で戻る量は揺れます。 tick が回るのは通算分が minutes_per_tick の倍数になる瞬間なので、 duration がその倍数でない操作は、押した時刻によって経過する tick 数が 1 つ違います。刻んで書く以上は 避けられないので、揺れることを許容します(ロード時の検査は置きません)。

12. trigger: drag(カードを重ねる操作)

きっかけを {drag: ...} と書いた操作は、変化の本体と言えるオブジェクトに定義します(どちらへ ドラッグしても成立するので、受け側かどうかで決めません。12.3 節)。drag の値が相手のカードに求める型 (4.1 節)で、宣言している側が self、相手が instrument になります。

object_defs:
  wood:
    interactions:
      cleave:
        trigger: {drag: {tag: axe_tool}}
        conditions:
          - {subject: instrument, prop: durability, gt: 0}
        add:
          instrument:
            durability: -1
        destroy: self
        spawn: {object: logs}
  logs: {}
  axe_tool:
    tags: [axe_tool]
    props:
      durability:
        value: 10

12.1 相手の指定

マッチング対象は、4.1 節の型指定で書きます。

interactions:
  cleave:
    trigger: {drag: {tag: axe_tool}}          # そのタグを持つあらゆるカード
  ignite:
    trigger: {drag: {object: burning_tinder}} # その型そのものだけ

{tag: ...} は、そのタグを持つあらゆるカード(パック追加分・trait を介さず直接タグを宣言したカードも 含む)と一致します。{object: ...} はその型だけと一致し、相手が 1 種類しか無い組み合わせのために 単発のタグを新設せずに済みます。trait 名ではどの形でもマッチングしません(4.1 節)。

1 つのオブジェクトは、同じ相手に対して同時に成立する重ねる操作を複数持てません。 画面が出せるのは 1 つだけで、成立するものが複数あれば宣言順の先頭が必ず勝つため(CardInteraction.md 2 節)、後ろのものはプレイヤーから届かなくなります。相手の指定が交わること自体は構いません—— 交わるなら、次のどちらかで同時に成立しないようにします。

「複数のタグを同時に満たすもの」という指定は書けません(型指定が指すのはタグ1つか型1つ)。 足りなくなれば、相手の型を見る判定が条件へ降りてくる形で現れます——理由(14.6節)を伴わない {subject: instrument, matches: ...} がそれで、候補から静かに外したいだけの絞り込みは、本来なら きっかけの側に書きたかったものです。同梱の宣言にその形が現れたら tests/world-codex/dragInstrumentTypes.test.ts が落ちるので、上の「指定そのものを分ける」で 畳めなくなったことに気付けます。

どちらも断る場面が残るなら、狭いほうの指定を先に書きます。 断る組み合わせが複数並んでも画面へ出るのは 先頭だけなので(CardInteraction.md 2 節)、広いほうを先に置くと狭いほうの 理由は一度も出ません——満水の器どうしで出したいのは「もう入らない」であって、「中身が入っている」では ありません。

重ねる操作も duration(11.3 節)を持てます。{subject, prop} 参照では instrument も指せるため、 「使う道具側のプロパティが所要時間を決める」(切れ味の悪い刃物ほど時間がかかる、など)も書けます。

12.2 instrument

重ねる操作では、運ばれてきた側が instrument(11.5 節の役。「それで働きかける物」)になります。 効果の対象キーにも subject にも書けます——どこに書けるかを決めるのはキーの種類ではなく宣言の 置き場所で、一覧は 11.5 節「役を書ける場所」の表です。

画面でどちらの札を掴んだかによらず、self は宣言している側、instrument は運ばれてきた側です(12.3 節)。 どちらを掴んだかは経路であって役ではないので、宣言の読み方は変わりません。

12.3 どちらのオブジェクトに書くか

1 つの組み合わせは、片方のオブジェクトにだけ書きます。 画面は落とされた側の宣言も掴んだ側の宣言も 引くので(CardInteraction.md 2 節)、 どちらの札をどちらへドラッグしても同じ 1 つの宣言が実行されます。両側に書くと 2 つの宣言になり、 同じ内容である保証がありません。

書く先は、素材の側です。道具や触媒(刃物・火起こし具)の側ではありません。どちらが素材とも言えない ときは、変化の本体——その組み合わせで姿を変えるほう——に書きます。塩と肉から塩漬け肉ができるなら、 肉に書きます。これが 2 節の「知っている側に書く」の一例で、ナイフは自分が何を切れるかを知りませんが、 材料は自分が切れることも焼けることも叩けることも知っています。書く先が patient(11.5 節)です。

object_defs:
  # 槍の柄になる棒が本体。縄はそれを縛る側なので、宣言は棒にだけ置く。
  stick:
    interactions:
      craft_spear:
        trigger: {drag: {tag: rope}}
        destroy: [self, instrument]
        spawn: {object: spear}
  twine:
    tags: [rope]

上の例では、縄を棒へ重ねても棒を縄へ重ねても craft_spear が実行され、どちらの場合も self は棒、 instrument は縄です。

同じ相手に対して両側が別々の組み合わせを宣言している場合は、今成立するほうの宣言が実行されます ——両側とも成立するなら落とされた側です(同 2 節)。「刃物を肉へ重ねたら切る」「肉を刃物へ重ねたら切る」のように向きで別の結果にしたいときだけ、 両側に書きます。

12.4 allow_multiple(まとめて重ねる)

既定では、1 回のドロップで組み合わさるのは 1 枚だけです。 きっかけの中に allow_multiple: true を 書くと、束からまとめて運んできた枚数ぶんを続けて実行します。きっかけの中に置くのは、重ねる操作 以外では意味を持たないからです——外に出すと、書けてしまうが効かないキーができます。

interactions:
  add_fuel:
    trigger: {drag: {tag: fuel}, allow_multiple: true}
    transfer: {amount: 999, from: instrument, from_prop: fuel, to_prop: fuel}
    destroy: instrument

何枚受け取れるかは宣言しません。 効果が動かす器——値域(range)を持つプロパティ——の残りから エンジンが数えます(炉の fuel が満ちるまで何本くべられるか)。プレイヤーが運んだ枚数と、器が 受け取れる枚数の小さいほうが実際の枚数です。

器が満ちていれば、その組み合わせは候補になりません(allow_multiple の有無によらず)。1 つも 入らないのに実行させると、destroy: instrument のように相手を消す効果が、何も入れないまま相手だけを 失わせます。拒むために条件を書く必要はありません——上限は range が既に言っています。

断る理由を届けたいなら、同じ上限を条件としても書きます。 容量で落ちた組み合わせは、理由を言う ためだけの落とし先としても残らない(CardInteraction.md 2.1 節)ので、 reason(14.6 節)を宣言する場所がここしかないためです。拒む判定が二重になることは問題では ありません——条件と容量は同時に落ち、どちらか一方でも拒みます。

それでも許可を宣言させるのは、構造として受け取れることと、まとめて実行させてよいことが別だから です。duration の大きい操作をまとめて実行すると、プレイヤーが止める手段の無いまま何時間も進みます。 数えられるかはエンジンが答えられますが、それをしてよいかは書いた人にしか分かりません。

許可できるのは、まとめた枚数の上限を決める器をちょうど 1 つ持つ効果だけです。器を持たない・複数持つ・ pick を含む(引くたびに起きることが変わるので 2 回目が何をするか分からない)宣言に allow_multiple を 書くとロード時エラーになります。逆に、器を持つのに書かない宣言は咎めません——それが許可であることの 意味です。

枠へ入れる操作(7.8 節)はこの宣言を持ちません。効果も値段も枠の宣言で決まっていて、書いた人が 選べる余地が無いためです。

重ねる操作の実行時の設計(マッチングの解決、キーの衝突の扱いなど)は ActionSystem.md を参照してください。

13. recipes(レシピ)

レシピは、成果物の object_defs に、名前をキーとする辞書として埋め込みます。traits には書けません (複数の型へ混ざる mixin では、どれが成果物か決まらないため。ロード時エラー)。

object_defs:
  axe:
    recipes:
      basic:
        conditions:
          - {subject: agent, prop: skill_woodwork, in_stage_or_above: basic}
        deftness: {skill: skill_woodwork, from_stage: skilled, minutes: -15}
        steps:
          - requires:
              - {object: wood, count: 2, consume: true}
              - {object: stone_knife, consume: false}
            duration: 30
          - requires:
              - {object: rope, count: 1, consume: true}
            duration: 30

13.1 steps / requires

要求する相手は、型そのものでもタグでも指せます({object: ...} / {tag: ...} のいずれか一方、 4.1 節)。ここだけは {not: ...} を書けません——型の指定が count/consume と同じマップに同居する ので、他のキーと同居できない not の形にならないためです。道具は「その用途に使える物」で あって特定の型ではないので、刃物を {tag: cutting_tool} で求められます。

recipes:
  sewn:
    steps:
      - requires:
          - {object: rawhide, count: 2, consume: true}
          - {tag: sewing_tool, consume: false}
        duration: 60

1 つの物が 2 つの要求を同時には満たしません。 タグで書くと、1 つの物が複数の要求に当てはまり えます(尖った石は cutting_tool にも sharp_stone にも当てはまる)。

それでも、成立する割り当てが在るなら断りません。 充足の判定も消費も、当てた先を必要に応じて 振り替えて割り当てを探します(crafting.allocateContentsToRequirements)。尖った石 1 つと石斧 1 つなら、 {tag: cutting_tool} と {object: sharp_stone} が並ぶ工程はそれで揃います——要求を書く順も、箱に 入れた順も、揃うかどうかを変えません。 揃わないときに「あと何個」として出る数(CardView.md 13 節)も、 当てられる数が最大の割り当てから出ます。

物はまず、入れた枠の要求へ当てます。 材料スロットの枠は要求ごとに 1 つなので、置いた場所がそのまま 「どの要求のつもりか」を言っています。振り替えるのはそのままでは揃わないときだけで、置いたとおりで 揃うなら割り当ても置いたとおりです(枠に出る印は CardView.md 13 節)。

同時に満たせないのは同じ工程の中だけです。 前の工程で道具として使った物は消えないので、後の工程が それを素材として消費できます。数え方も工程ごとで、素材は工程をまたいで足し合わせ、道具は最も多い工程の ぶんだけ数えます(crafting.heldPerRemainingRequirement。要求の側の数え方と同じ)。

count(要求する個数)は省略すると 1 です。かさ(volume、mL)とは別物なので、体積の語と 混ざらない名前にしています。consume は省略できません(素材か道具かに既定値を置くと、書き忘れが どちらかとして黙って通るため)。

13.2 conditions(解放条件)

そのレシピを知っているかの条件です(14 節の条件木。設計は SkillSystem.md 4 節)。 省略すれば最初から作れます。素材が揃っているかは 13.1 節の requires が見るので、両者は別物です。

解放条件を評価する時点では、成果物のインスタンスがまだ存在しません。 宣言元の個体を起点とするルートが 指せないのはこのためです(subject に何を書けるかは 14.1 節の表が唯一の一覧)。

ここは操作ではないので、11.5 節の関係はありません。 「誰にとって解放されているか」を問う判定なので、 agent は問う側(一覧を出す画面)が渡します。操作の関係が結ばれていなくても解決する agent です。

ここは要件の並びなので、要素ごとに reason(14.6 節)を書けます。 未解放の レシピも一覧へ出し、そこでなぜ作れないかを言うため、可否と理由が同じ 1 回の評価から得られます。

13.3 crafting_conditions(工程を進めるのに要る条件)【確定】

すべてのレシピの工程に一律で掛かる条件を、世界が 1 本だけ持ちます。

レシピの内部設計(製作中オブジェクトの自動生成、枠の要件との連携など)は RecipeSystem.md を参照してください。

出どころ: #656(crafting_conditions)

13.4 crafting_conditions の置き場と書き方

13.3 節の 1 本をどこにどう書くかを決めたもので、覆すのに人間の判断は要りません。

ルートキー(core.yaml)で持ちます。書き方は conditions(14 節)と同じで、ここは要件の並びなので、 要素ごとに reason(14.6 節)を持てます。

# core.yaml
crafting_conditions:
  - reason: too_dark
    subject: agent
    prop: hand_brightness
    in_stage_or_above: bright
  - reason: too_stormy
    not: {subject: agent, prop: wind_speed, in_stage_or_above: gale}

13.5 deftness(作り手の腕が効く先)

作る速さを決める腕を、レシピが名乗ります(設計は Skills.md 7 節)。省略でき、 省けばそのレシピに腕は効きません。

recipes:
  braided:
    conditions:
      - {subject: agent, prop: skill_cordage, in_stage_or_above: basic}
    deftness: {skill: skill_cordage, from_stage: skilled, minutes: -15}
    steps:
      - requires: [{object: plant_fiber, count: 2, consume: true}]
        duration: 30

腕が効くのは速さだけで、歩留まりには効きません。 余分が取れるかの卓(pick、10 節)をレシピは 持てず(Skills.md 7.2 節)、書けばロード時に未知のキーとして弾かれます。 成果物は進捗が上限へ届いた瞬間の become 1 回ぶんで常に 1 つ(RecipeSystem.md 1 節) なので、余分を 1 つ足せば必ず倍になり、内容の側が置いた上限に収まらないためです。

14. conditions(条件式)

条件は、1つの主語について何かを判定する「葉」と、複数の条件を論理積・論理和・否定で束ねる 「複合ノード」からなる木構造です。トップレベルの conditions: は常に配列で、暗黙の all(論理積)として 扱われます(passives/stages/cells/pick と同じ、複数形キーは常に配列という規約、8 節)。

葉はどれも 誰を見るかを決めるキー(subject)・主語を絞るキー(prop/slot)と、演算子キー (値が比較の相手) でできています。subject は省略でき、既定値は self です。

主語 使える演算子キー 節
prop(subject のそのプロパティの実効値) lt/lte/gt/gte/eq/neq/in/not_in/in_stage/in_stage_or_above 14.1節
slot(subject のそのスロットの中身) matches 14.3節
無し(subject 自身) in_slot(親の中での位置)/matches(subject 自身の型) 14.2・14.4節

同じ matches でも、量化は主語が決めます。 slot があれば「その中身に当てはまるものが1つでもあるか」、 無ければ「subject 自身が当てはまるか」です(14.3 節・14.4 節)。

演算子キーは1つの葉に複数書けます。 同じマップ内の複数キーは配列と同じく暗黙の all なので、範囲は {prop: x, gte: 100, lt: 200} と書けます。同じ prop を2度書いて片方だけ直す事故を避けるためで、矛盾した 組み合わせ({gte: 100, lt: 50})はロード時には捕まりません(all で分けて書いても同じことが書けるため)。 演算子キーが1つも無い葉はロード時エラーです。

14.1 葉: プロパティ比較・段判定

conditions:
  - {subject: agent, prop: satiety, lt: 12}
  - {prop: heat, in_stage: ember}
  - {subject: agent, prop: skill_cordage, in_stage_or_above: basic}

not_in/neq は not で包む形(14.5 節)と同じではありません。参照が解決できない葉は偽になるため、 not で包むと「解決できなかった」が真に反転します。「祖先に天候の概念が無ければ蒸発しない」のように、 分からないときに効かせたくない条件は否定形の演算子で書きます。

conditions:
  - {prop: content, eq: {subject: instrument, prop: content}}   # 2つの動的な値同士を比較する

subject/prop をフラットな2フィールドにしているのは、対象1つ・プロパティ1つの単純な参照であり、 ネストする必要が無いためです。

prop が参照する値は、実体値(生の値)ではなく実効値(8.3 節の modify を加味した値)です。これにより、 modify だけで決まる派生プロパティ(例: 天候と時間から決まる日照)も、他の conditions から参照できます。 value が {subject, prop} 参照の場合も同様に実効値を読みます。ただし、あるプロパティの modify のゲートが、 直接・間接を問わず自分自身の実効値に依存する(循環参照)と、実行時に例外を送出します (PropertyValue.getEffectiveValue 参照)。この種の循環はエンジン側では防げず、YAML の書き方で避ける必要が あります。

14.2 葉: スロット位置判定

subject が指すオブジェクトが、今まさに親のどのスロットに入っているかを判定します(外側から見た 位置)。常に等価判定なので、スロット名そのものが値になります(否定したい場合は 14.5 節の not で包みます)。

conditions:
  - {subject: self, in_slot: equip}

in_slot は subject 自身を主語とする演算子キーなので、prop を主語にした葉には書けません(ロード時 エラー)。プロパティ名とスロット名は別の名前空間であり、同じ識別子でも衝突しません。

ancestor は「prop が指すプロパティ名で祖先を探す」という仕組みのため、prop を主語にしない葉では subject に指定できません(14.1 節の表)。

14.3 葉: スロット中身判定

subject が持つそのスロットの中に、matches に当てはまる子オブジェクトが1つでもあるかを判定します (内側から見た中身、14.2 節のスロット位置判定とは向きが逆)。常に存在判定です。

conditions:
  - {subject: self, slot: treatment, matches: {tag: immobilizing}}

subject がそのスロットを持たなければ偽になります(解決できない葉は偽、という他の葉と同じ約束)。 スロット名は型をまたいで1つの名前空間にあるので、一部の型だけが持つ枠を見る条件を、その枠を持たない 型にも配られる trait へ書けます——持たない側では常に偽になり、not で包めば常に真です。蓋を載せて いるかを液体の側から見る宣言(LiquidContainerSystem.md 6.2 節)が この形で、蓋の枠を持たない器の宣言に手を入れずに済みます。

slot は 14.2 節の in_slot とは別フィールドです。in_slot は「subject が親の中でどこにいるか」、slot は 「subject 自身の中に何が入っているか」という、参照する木構造上の向きが逆であるため、キー名自体を分けて 区別しています。

怪我に当てた治療具のように「枠に何が入っているかで効き目や取れる行動が変わる」ケースに使います。種類が 1つの型で足りるなら {object: ...}、治療具のように仲間が増える役割なら {tag: ...} で書きます(4.1 節)。 容器の中身の種類(水入りの甕など)はスロットではなく変種の軸で持ち、中身が配るタグを器自身が名乗るので、 この葉ではなく器のタグを見ます(LiquidContainerSystem.md 1節・10節)。

object_defs:
  cracked_rib:
    slots:
      treatment:
        cell_count: 1
        cell: {accept: {tag: treatment}, max: 1}
    passives:
      - conditions:
          - not: {slot: treatment, matches: {tag: immobilizing}}
        modify:
          parent:
            load: 18000
      - conditions:
          - {slot: treatment, matches: {tag: immobilizing}}
        modify:
          parent:
            load: 9000

14.4 葉: 型判定

subject が指すオブジェクト自身が、matches に当てはまるかを判定します(否定したい場合は 14.5 節の not で包みます)。

conditions:
  - {subject: instrument, matches: {tag: liquid}}      # instrumentがliquidタグを持つ
  - {subject: instrument, matches: {object: raw_meat}} # instrumentが生肉そのもの

14.3 節と同じ matches で、違うのは主語だけです。 slot を書けば「その中に当てはまる子がいるか」、 書かなければ「subject 自身が当てはまるか」になります。量化をキー名にも書くと、同じ指定の形に2つの 名前が付くことになるため、主語だけに担わせています。

14.5 複合ノード: all / any / not

複数の条件を組み合わせたい場合、all(論理積)・any(論理和)・not(否定)で入れ子にできます。

conditions:
  - any:
      - {in_slot: main_hand}
      - {in_slot: off_hand}

トップレベルの配列自体が暗黙の all であるため、単純な AND 条件は複合ノードを使わず、葉を並べるだけで書けます。

14.6 reason(満たさなかったときに出す理由)

プレイヤーへ差し出す選択肢の可否を答える条件の並びを、要件の並びと呼びます。 その要素には 満たさなかったときにプレイヤーへ出す理由を reason(識別子)で添えられます。文言は locale の reason_texts が持ちます(Localization.md)。

interactions:
  travel:
    trigger: menu
    conditions:
      - {in_slot: fixtures}
      - reason: too_heavy
        not: {subject: agent, prop: load, in_stage_or_above: too_heavy}

UI は、実行できないアクションのボタンを押せない見た目にし、理由の文言を出します (Windows.md 4 節)。出すのは、宣言順で最初に満たしていない要素の文言です。重ねる操作(12 節)でも同じです——成立しない 組み合わせは候補から消えますが、reason を宣言しているものだけは落とし先として残り、重ねている間に 理由を出します(CardInteraction.md 2.1 節)。ただし become の行き先の型を 持たない相手(塩漬けにできない物へ塩を重ねる)では、その手前の門 (ActionSystem.md 1.1 節)で候補ごと消えるため理由が出ません。 reason を持たない要素は理由を出しません(未発見の道の in_slot のように、そもそもプレイヤーへ 見せる必要が無い前提条件のためです)。

書けるのは要件の並びの要素にだけで、入れ子の all/any/not の中には書けません。条件木の形から 文を組み立てる(否定・入れ子を自然な文へ直す)方法はどの言語でも一般には無いため、判定の木は機械のもの、 説明は人のものとして、要件1つ=文1つで著者が書きます。

書き方が同じでも、要件の並びでなければ reason は持てません。 画面がその選択肢を出すときに 「できるか」を問い、できなければこの文を出す——という相手が居て初めて理由が要ります。試みた者が 居るとは限らない場面でも走る条件は、文を出す相手が居ないので、ただの条件の並びです。 どちらであるかは、その並びを書ける場所を定める節が述べます。

15. singleton(唯一のインスタンス)

「インスタンスが1つだけ存在すべき」という制約は、object_defs のエントリに singleton: true を追加することで 表現します。

object_defs:
  world:
    singleton: true
    props:
      day:
        value: 1
      hour:
        value: 12
        range: {min: 0, max: 24}
        on_max:
          add:
            self:
              hour: -24
              day: 1
      minute:
        value: 0
        range: {min: 0, max: 60}
        on_max:
          add:
            self:
              minute: -60
              hour: 1

(実際の定義は src/assets/world-codex/core.yaml 参照。ここに挙げたのは日時だけで、累積 tick 数・ 天候・季節なども同じ world が持つ。)

「1つだけ存在すべき」は「世界を作った時点で在る」と読みます。 ただし湧かせるのは、world が直に 受け入れられる型だけです(NewGame.spawnSingletonsAcceptedByWorld)——海区・本土(Voyage.md 4.2 節)は world の locations 枠に入るのでそこに在り、キャラクタも singleton ですが、world のどの枠にも入らない (土地の characters 枠に入る物です)ので湧きません。何が最初から在るかを決めるのは枠の宣言だけで、 エンジンは型の名前を1つも知りません。これにより、型の名前で行き先を指す move の to_object(9.6 節)は 必ず解決できます。

日時・天候はオブジェクトから直接参照されるのではなく、環境がオブジェクトに影響を与えるという位置づけです (例: 明るさによって行動可否が変わる)。直接のプロパティ参照は 14 節の conditions で subject: ancestor を通して行います(subject: world は 未対応、14.1 節)。

15.1 日は上限を持たず、そのまま数え続ける【確定】

day は折り返さず、年をまたぐ単位(year)も持ちません。 1 周回は年をまたぐ長さではない (積み上げた日数は ContentSkeleton.md 8.3 節)ので、年をまたぐ遊びに なりません。上限を持たせると、日付の出し方も季節の巡り(ClimateSystem.md 2 節)も 「年をまたいだとき」を考えることになります。

day に range が生えていないこと・world が year を持たないことは tests/world-codex/coreYaml.test.ts が見張ります。

出どころ: #2286(ゲーム内の日数は上限を持たず、そのまま数え続ける。2026-09-19に回答)

16. 本書の対象外

17. 今後の検討課題