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 上のキーワードの意味と
書き方はすべてここに集約します。
本書が定めるのは文法だけです。その文法で具体的なゲーム内容をどう表現するかは、以下の別ドキュメントを 参照してください。
RecipeSystem.md— アイテムの製作SlotSystem.md— スロットの実行時実装(固定位置・受け入れ判定・スタック)ContainerSystem.md— コンテナの容量・重さ・保護LiquidContainerSystem.md— 液体容器(量・飲用・注ぎ移し・蒸発・降雨)ActionSystem.md— 操作の実行時の設計(interactionsの実行時の仕組み)ClimateSystem.md— 季節・天候TerrainGeneration.md— 島の地形生成(軸・LocationTypeマッチング・パスネットワーク生成のアルゴリズム)ExplorationSystem.md— 生成された土地の構造と挙動(スロット・探索・道の発見と移動)
形式的なスキーマ定義(JSON Schema)は WorldCodex.schema.json
(メタ情報は WorldCodexSchema.md)を参照してください。機械的な検証はスキーマ側が担います。
本書は確定した文法のみを記述します。未決定の論点は 17 節にまとめています。
2. 設計原則
- 統一的な実行エンジン: あらゆる概念を共通の枠組み・単一の汎用エンジンで表現し、概念ごとの専用ロジックを増やしません。
- tick 駆動: 時間経過にともなう変化は、すべて tick ごとの独立変数への加減算(
add)で表現します。 - 範囲による状態決定: 数値プロパティの値域から状態(
stages)を決定する仕組みを型定義レベルで共通化します。 - 宣言は、その組み合わせを知っている側に書く: 影響を与える側とは限りません。ナイフは自分が何を切れるかを
知らず、材料は自分が切れることを知っているので、宣言は材料に置きます(12.3 節)。世界は自分の下に何がぶら下がるかを
知らないので、明るさは土地の側が
base(6.5 節)で見に行きます。押す(modify)か引く(base・参照)かは、 知識がどちら側に在るかで決まる従属物です。どちらも相手を知らないときは、両方が知っている共通の親のプロパティを 経由します(焚き火とキャラクタは互いを知らず、どちらも場を知っているのでambient_temperatureがあります)。 ただし操作の瞬間だけ在る関係(11.5 節)は経由しません——環境へ写すと、関係が終わっても残り、同じ立場の者が 複数居れば重なります。 - 可逆な寄与を合成する器は 1 つだけ: 値を合わせる場所は
modify(8.3 節)とbase(6.5 節)で、どちらも 加算です。weight(10.2 節)・duration(11.3 節)のような「その場で決まる数値」はリテラルか参照 1 つしか 取れず、式を書けません。器を増やすと、range(6.3 節)も段(6.4 節)も付いてこない 2 つ目の合成先ができます。 - 数えるものはオブジェクト、量はプロパティ: 1つずつ数えて1つずつ手を加えられるもの(傷・道具)は
object_defにし、合計しか意味を持たないもの(痛み・意識)はpropsにします。判定の基準と境界の例はInjurySystem.md4 節。 - ハードコードしない/ファイル追加だけで拡張できる: 挙動は YAML 定義に置き、パック作成者がファイル追加だけで新要素を 導入・上書きできるようにします。
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 命名規則
-
構造キーワード(
props、traits、interactionsなど)・型名・プロパティ名は、すべて snake_case で統一します。 -
識別子(型名・プロパティ名・トレイト名・アクション名・スロット名・タグ名に共通)は次の正規表現に従います。
^[a-z][a-z0-9_]*$
3.3 ロードとバリデーション
- YAML ファイルは所定のディレクトリ以下のすべてのファイルが自動的に読み込まれます。ゲーム本体の定義とアセットパックの定義は
別々に管理します(
AssetPack.md)。 - マッピングキーの重複は、ローダーの厳格モードでエラーにします(多くの YAML パーサは重複キーを後勝ちで 黙って上書きするため)。
- ロード後に別途バリデーションステップを設けます。
- 同じ ID を持つ定義が複数のファイルに存在する場合、ファイル・ディレクトリ・読み込みのタイミングを問わず 常にエラーとします(意図しない上書き事故の防止のため、後勝ちの上書き規則は持ちません)。アセットパックによる 既存定義の意図的な差し替えは、専用の patch 文法で表現します(3.4節)。
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 適用の順序と、失敗したとき
- 適用はすべてのファイルを読み終えてから、読み込み順に行います。当たる先が揃っている必要が
あるためです。trait の合成(5節)より前なので、
traitsの一覧そのものも変更できます。 - 同じ場所を2つのpatchが差し替えることはできません。先に読んだ方が残ります。 後勝ちにすると、 読み込み順という見えない要因で結果が変わり、勝った側も自分が上書きしたことを知りません。
- 前提が満たされない操作(消そうとしたものが無い、足そうとしたキーが既にある等)は、同梱の
YAMLならロードエラー、アセットパックならその1操作だけを捨てて記録します
(
AssetPack.md6.1節)。 object_defそのものの新設と削除はできません。新しい型はobject_defsに書きます。削除を許すと、 他の定義から参照されている型が消え、壊れ方が patch から遠い場所に出ます。
3.5 ロード時に生成される型
エンジンがロード時に生成する型は、いくつかの軸の値の組で決まり、識別子はその組から組み立てた結果です。 著者もパック作成者もこの識別子を書かず、効果からも名指ししません(9.9 節)。
生成器があります。製作中オブジェクト(13 節、RecipeSystem.md 1 節)は
素の型が完成品・軸が recipe で、著者は何も書きません。変種(variation_axes、3.5.1 節)は
型自身が軸を宣言し、その軸の値ごとに型が生まれます——中身入りの容器
(LiquidContainerSystem.md)がこれにあたります。
- 軸を決めるのは生成器です。 軸の名前も、どの組に型が存在するかも、生成する側(レシピなら 13 節の
宣言を読む側)が決めます。エンジンが持つのは
軸の値の組 ↔ 生成された型の対応だけで、軸の名前が 何を意味するかは知りません。 - 組み立てなかった座標は、その型が無いということです。 そこを指す操作は解決に失敗し、実行できなく なります(9.9 節)。「この入れ物にはこれを入れられない」を、条件ではなく型の有無で表せます。
- 地形生成の
axes(16 節)とは別の話です。あちらは土地の性質を表す座標で、こちらは型の生成規則です。
表は 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つも無ければロード時に落とします。
- プロパティの名前:
props(6節)のキーとして宣言している型が無ければエラー。 - 段の名前(
in_stage/in_stage_or_above、deftnessのfrom_stage): そのプロパティのstages(6.4 節)にその名前を持つ宣言が無ければエラー。 - スロットの名前(条件の
slot/in_slot、amongのslot、moveのto_slot):slots(7節)の キーとして宣言している型が無ければエラー。 - 型の名前(「どの型が当てはまるか」の指定の
{object: ...}(4.1 節)、spawnのobject、location_typesのobject_def):object_defsに無ければエラー(生成された型、3.5 節を含む)。行き先を型で指す宣言(9.4 節・9.6 節)は、 加えて世界にただ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 行しか違わないので、行き先の数だけ絵を描いても同じ絵が並ぶだけになります。
- 名乗るのは型の側です。 「この絵はどの型のものか」をコードや絵の側の一覧で持つと、絵を 1 枚足す
たびに登録が要ります。型が自分の絵の名前を言えば、規約はファイル名だけで済みます(
objectArt.ts)。 - trait 側の宣言も参照できます(自分自身の指定を優先し、無ければちょうど 1 つの trait の指定を 使う。5 節)。共有したい群れは trait で 括られていることが多いので、実際に書くのは trait 側の 1 行だけになります。
- 絵の名前は object_def の識別子と同じ命名規則(3.2 節)に従います。識別子と衝突してかまいません ——ある型の絵を別の型が名指しで借りる形になるだけです。
5. traits(mixin)
複数の object_def で共有するプロパティ・アクション等を traits として定義します。
- 多重継承は禁止: trait が他の trait を参照することはできません(1 階層のみ)。
- プロパティ衝突はエラー: 複数の trait が同名プロパティを宣言していた場合、暗黙の優先順位で解決せずエラーとします。
- 値の扱い:
- trait 側で
valueを持たないプロパティ = object_def 側で値を与えることが必須 - trait 側で
valueを持つプロパティ = デフォルト値。object_def 側で省略可、上書きも可 - object_def 側で一部属性(例:
value)だけを上書きした場合、残りの属性(unitなど)は trait 側の値を引き継ぐ - 例外として
propsのtags(6.7 節)だけは上書きせず足し合わせる
- trait 側で
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 側に書けます。 混ぜ方はキーごとに決まっています。
- 連結(
tags・covers(7.5 節)・visible_slots(7.11 節)など): trait 由来を先に、自分自身の 宣言を後ろに並べます。並びに意味があるものは、この順序がそのまま規約です(visible_slotsの 並びは、そのままタブの表示順になります)。 - 自分自身を優先し、無ければちょうど 1 つの trait(
art(4.3 節)・art_by_stage(6.4 節)・stack_order(7.6 節)・layer(7.5 節)・resists(7.13 節)など): 複数の trait が宣言していれば ロード時エラーです。resistsは並びを書きますが、暗黙の AND で結ばれた 1 つの条件木なので連結しません ——どれが効くかを、混ぜる順序に頼らせないためです。 - どれか 1 つでも真なら真(
storage(7.12 節)・bound_to_owner(7.9 節)・stackable(7.6 節) などの真偽値): 重複はエラーにしません。既定が真のstackableは、falseと書いた宣言が 1 つでも あれば束ねなくなります。
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 が誰の端か
プロパティの値には次の読み方があります。
- 実体値: 書き込みで不可逆に積み上がったもの。動かすのは 9 節の書き込みと、
passivesのaddによる毎 tick の積分(8.4 節)です。 - 実効値: 実体値に可逆な寄与——
modify(8.3 節)・base(6.5 節)・入れ物の中身から受けるweight/load——を重ね、rangeで切ったもの。宣言がプロパティの値を参照したときに返るのは こちらです。実体値を直に見るのは、可逆な寄与を混ぜてはいけない側だけ——端のイベント(次の段落)やtransferの出せる量(9.5 節)がそれにあたります。
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 である、のいずれかが当てはまる場合です。
書けてしまうと「ゲージは満ちているのに何も起きない」「まだ空でないのに尽きる」が起こりえますが、
そのときに何が正しいかをまだ決めていません。
- どちらも、
rangeが定義されていて著者が明示的に書かなかった場合、自分自身を境界値(on_maxならrange.max、on_minならrange.min)へsetする既定の内容が自動的に補われます。rangeを書く だけで上下限のクランプになり、明示するのは繰り上げ・繰り下げのような特別な挙動が要る場合だけです。 - 空のmappingを書く(
on_min: {})と、「宣言だけして何もしない」、つまり既定のクランプを打ち消して 範囲外の値をそのまま許容する、という意味になります。 - 周期を持つ量は、折り返す先ではなく折り返す点を
maxに書きます。 1 日 24 時間のhourは{min: 0, max: 24}で、24 に達した瞬間on_maxが 0 へ戻します。見た目には 23 の次が 0 ですが、 「一瞬 24 になってから戻る」のが実際の動きです。
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の結果が範囲をはみ出した場合の後始末)。
rangeはon_max/on_minを使う場合に必須です。range自体を省略すると(day(15.1 節)のように 上限・下限なく積み上げ続けたい場合)、on_max/on_minの仕組み自体を持ちません。addに書けるプロパティは、自分自身(折り返し)に限らず、同じ object_def 内の他のプロパティ (繰り上げ・繰り下げ先)も指定できます。存在しないプロパティ名はaddの通常の規約と同様に黙って無視されます。- 自分自身の折り返し・繰り下げには
set(9.2 節)ではなくaddを使ってください。1 tick でrangeの幅を 複数回分飛び越えることがあり、setで固定値へ代入するとその超過分を失いますが、addなら差分を保ったまま 反映されます。 - 補正の適用自体が再び判定を呼ぶため、補正が値を境界の内側へ戻す限り、連鎖は同じ tick 内で完全に解決されます。
1 tick で
rangeの幅を複数回分飛び越えた場合も、繰り上げ・繰り下げ先自身がさらに範囲外へ出る連鎖 (分→時→日)も、propsの宣言順に関わらず持ち越しなく解決されます(tests/domain/rangeEvents.test.ts参照)。 - 補正が値を境界の内側へ戻さない場合(空宣言や、値を書き換えない内容だけを書いた場合)、値が境界に居る間は
判定のたびに繰り返し適用されます。
destroyは冪等なので安全で、spawnを伴う場合は同じイベント内でdestroyと組み合わせるのが基本形です(破棄後は tick の対象でなくなるため、2 回目以降の実行は起こりません)。 自分を破棄しないspawnを境界で繰り返させないための口が、次のconditionsです。
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 節)が
切り替わります。
-
段階の区間定義は
min(下限)のみを指定します。上限は次の段階のminによって自動的に決まります (半開区間、[min, 次のminより手前])。この方式により、区間の隙間や重複が構造的に発生しません。 -
最下段の段階は
min: null(または省略)とし、それより下の残り全ての値を拾います。 -
シンボル型プロパティ(6.6 節)の場合は
minを書きません(書いた場合はロード時エラー。シンボル値の 大小関係に意味が無いため)。代わりに、段階のname自体がそのまま比較対象の値になります(値がnameと完全一致する場合だけその段階になる、シンボル名同士の比較)。props: weather: value: clear stages: - name: storm - name: clear passives: - modify: self: ambient_brightness: -2数値型の「最下段(min省略)=フォールバック」という概念は、シンボル型には存在しません。
stagesに 書かなかった値は単に「該当する段階が無い」となり、PropertyDef.stageAtはundefinedを返します。呼び出し側 (WhenOwnStageゲートを含む)は必ずundefinedチェックを行います(PropertyDef.isInStage参照)。 複数のシンボル値を1つの段階名にまとめたい場合は、stagesではなく利用側のconditionsのin(14.1 節)で表現します。stagesは1つのプロパティ自身の値と段階名を1対1で対応させる仕組みです。 -
現在値に基づいて常に一意に段階を決定します。ヒステリシス(上昇時・下降時で閾値をずらす仕組み)は採用しません。
-
1つのプロパティの中で
nameは一意です(重複はロード時エラー)。段階を外から指す術は名前しか無いため (in_stage/in_stage_or_above、14.1 節)、同じ名前が2つあるとどちらを指しているかが決まりません。 -
ステージの
passivesはレベルトリガー(そのステージにいる間ずっと有効)のみです。ステージ切替の瞬間だけ発火する edge-triggered な仕組み(on_enter/on_exit的なもの)は導入していません(2.1 節)。「下限に達した瞬間」はon_min(6.3 節)で表現します。 -
ステージは
name/minという固有の属性を持つため、ステージ自体を配列の要素にはできません。passives:はname/minと対等な兄弟キーとして、専用にネストしたキーのまま持ちます(他のレベルのpassives:と同じく 常に配列です、8 節)。 -
ステージの
passivesブロックにはconditionsも併記できます。この場合、「現在値がこの段階の区間内に あること」(WhenOwnStage)とconditionsの両方を満たす間だけ有効になります(AND、8.2 節)。 -
ステージは
alertで「その段階にいる間、値がどの域にあると見なすか」を宣言できます。値はsafe(安全域)・watch(留意域)・caution(要注意域)・danger(危険域)・fatal(致命的域)の いずれかで、省略時はsafeです。UI はこの宣言だけを見てステータスの見せ方(非表示・表示・明滅)を 決めます(StatusArea.md)。watchとcautionは 今のところ同じ見せ方(出すだけ)ですが、深刻さの違いを段の側で表しておくことで、見せ方を分けたく なったときに YAML を書き直さずに済みます。fatalは放置すると死に至るプロパティ(水分など)に だけ付けます。UI はalertからバーの塗りの色(深刻さで緑から茶へ)と、どちら向きが悪化か (段を下から上へ見て深刻さが単調に上がるなら、増える側が悪い)を導きます (StatusArea.md)。 -
rangeを持つプロパティでは、alertの深刻さは下から上へ単調でなければなりません(ロード時 エラー)。体温のように上下どちらの端も悪い量はバーの向きを決められないため、片側だけの度合い (熱中症・低体温症)を別のプロパティとして持たせます。rangeを持たない(=バーにならない) プロパティにはこの制約はありません。 -
ステージは
artで「その段階にいる間カードに出す絵」を接尾辞として宣言できます (値はsrc/assets/objects/<型の絵の名前(4.3 節)>_<この値>.pngを指す、CardView.md5.1 節)。同じ個体のまま姿だけが変わる状態——気を失った動物、 火のついた炉——を、型を差し替えずに表すためのものです。宣言した絵をまだ用意していない場合は、その型 自身の絵のままになります(絵を描くのと文法を宣言するのを別々の時に行えます)。artを宣言できるのは、その object_def がart_by_stageで名指ししたプロパティの段だけです (object_def 直下の兄弟キー、値はプロパティ名)。1つの object_def につきart_by_stageは高々1つで、 他のプロパティの段がartを宣言するとロードエラーになります——1オブジェクト1絵の原則を、 2つのプロパティが同時に絵を主張する曖昧さごと構造で防ぐためです。art_by_stageは trait 側の宣言も参照できます(自分自身の指定を優先し、無ければちょうど1つの trait の指定を 使う。5節)。- 複数の段が同じ
art値を宣言してかまいません。 料理のできる火力の3段(coals〜blaze、FireSystem.md3節)がすべて同じ「ついている」絵を指す、という使い方はここで賄います。段を 割った意味の粒度と、絵を割る粒度を一致させる必要はありません。
-
ステージは
situationで「その段階にいる間、キャラクター表示エリアに出す状況アイコン」を絵の 識別子として宣言できます(値はsrc/assets/icons/<この値>.pngを指す、ScreenLayout.md4.1.1 節)。宣言した段にいる間だけ出て、宣言しない段では 出ません——「出すか」と「どの絵か」を1つの宣言に畳んであるので、片方だけ書いて黙って出なくなることが ありません。artと違い、名乗れるプロパティを object_def が絞る宣言はありません(画面はプロパティの 意味を知らずに並べるため)。複数の段が同じ識別子を宣言してかまいません(artと同じ理由。手元の 明るさのpitch_darkとdimはどちらも「手元が暗い」の一言に尽きるので、同じ絵を指します)。
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}}
- 読むのは土台の実効値です(相手の
rangeでクランプ済み・modify込み)。自分の側のrangeは、 土台へ加算した後に効きます。 - 1 つのプロパティに
baseは 1 つだけです。土台が増えると、実効値がどこから来たのかが読めなく なります。 subject: selfのときpropは必須です(省略すると自分自身が土台になるため、ロード時エラー)。selfを辿って自分へ戻る循環も、同じ型の中で閉じるのでロード時に検出します。- 何を主語に書けるかは、その宣言が置かれた場所が何を持つかで決まります——一覧は 14.1 節の表(操作の 関係の役は 11.5 節「役を書ける場所」)を見てください。操作の関係の役は、関係が結ばれていない間は 土台が辿り着かないので、その間の寄与は 0 です。
ancestor(8.6 節と同じ探索)は、自分の直接の親から遡り、prop が指すプロパティを定義している
最初の祖先です。
- 直接の親(
parent)固定でなく祖先を遡れるのは、直接の親がそのプロパティを持たない場合に対応するため です(例: アイテムの直接の親はプレイヤーだが、ambient_temperatureは部屋が持つ)。途中の親が定義して いればそこで探索が止まるため、断熱容器のように「途中で値を変えて中継する」表現も、その容器自身がrange・modify等で自分の値を作ればそのまま実現できます。 - すべてのオブジェクトは唯一のシングルトンである world の子孫であるため(7.1 節)、途中に誰も定義していなければ 必ず world まで遡ります。「本当に世界固有の概念かどうか」を著者が事前に区別する必要はありません。
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 # 数値にも真偽値にもならないため、シンボル名として登録される
- シンボル名同士の大小関係(
lt/gt等)は、登録順に依存する恣意的なものであり意味を持ちません。 シンボル型のプロパティはeq/neq/in/not_inで比較する用途を想定しています。 range/on_max/on_minはいずれも数値の大小関係を前提にした仕組みのため、 シンボル型のプロパティと組み合わせても意味のある結果にはなりません。stagesは大小ではなく 名前との完全一致で段を決めます(6.4 節)。
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}
object_defsのタグ(4.1 節)とは別の名前空間です。型の指定(4.1 節)が{tag: ...}で見るのはobject_defのタグだけで、プロパティのタグは参照しません。- 1 つのプロパティに複数のタグを付けられます(上の例の
satietyは「常時ステータスエリアに出す」=statusであり、栄養カテゴリの一員でもあります)。 - trait 経由のタグは合成されます。
propsの他のフィールド(value・rangeなど)は trait 側の 指定を object_def 側が上書きしますが、tagsだけは足し合わせます(5 節)。タグは集合であり、 object_def 側がタグを1つ足しただけで trait 由来のカテゴリを失うのは事故になるためです。 property_tagsのエントリは今のところ中身を持ちません(status:のようにキーだけを書きます)。 タブに出す表示名は他の識別子と同様 locale 側が持ちます(Localization.md)。
6.8 worsens・gauge(値の向きと、カードのバー)
プロパティは worsens で、どちらへ動くと悪いかを宣言できます。増減の記号の色(
Windows.md 8 節)・バーの向き(StatusArea.md 6 節)・
収支表が 1 日に賄う量に数えるか(BalanceStats.md)が、これを見ます。
props:
travel_delay:
# 歩みの遅れ(分)。増えるほど遅くなるので、増える側が悪い。
worsens: up
value: 0
- 書けるのは
up(増えると悪い)かdown(減ると悪い)。 - 向きは他の宣言も述べます——下の
gaugeの両端と、stagesのalertから導く向き (alertDirection、6.4 節)。どれも同じ 1 つの事実を言うので、2 つ以上が述べていて食い違って いればロード時エラーになります。 - 書かなければ、向きは他の宣言に委ねます。 どれも述べていなければ「減ると悪い」です。段を
持っていても
alertを 1 つも書いていなければ、alertは向きを何も述べていません。 - したがって
worsensを書くのは、他のどれもその量の向きを述べられないときです——gaugeにはrangeが要り、alertには域を分ける段が要るので、そのどちらも持たない量(歩みの遅れ)は、 ここでしか名乗れません。
プロパティは 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}
min/maxはrangeの両端に居るときの見せ方で、good(満ち足りている)・bad(尽きている・ 行き過ぎている)・neutral(良し悪しを言わない)から選びます。塗りの色は両端の見せ方だけで 決まるので、UI 側にプロパティ名の対応表は要りません。両端が同じなら値によらず 1 色になります (工程の進捗のように、0 を赤で急かしたくない量)。rangeが必須です。 割合が定義できなければバーとして描きようがないためで、rangeの無い プロパティにgaugeを書くとロード時エラーになります。- どちらの端が危ないかは、上の
worsensと同じ 1 つの事実です。worsens・stagesのalertと食い違っていればロード時エラーになります。 - バーを出すか出さないかも、この宣言の有無だけで決まります。 宣言していないプロパティは カードに出ません。何本出るかも宣言の数で決まるので、UI 側は「いくつまで」を知りません。
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}}
- 指せるのは
singletonの型だけです(15 節)。to_objectに直接書いたときと同じ制約で、複数在りうる 型ではどの個体を指すのか決まらないためです。ロード時に検査します。 - 同じ行き先を 2 か所へ書き写さないための仕組みです。 型の名前を直接書くと、同じ隣を
「航路を立てる先」と「荒天が押し流す先」の両方へ手で書くことになります
(
Voyage.md3.8 節)。 range/on_max/on_min/stagesとは組み合わせても意味のある結果になりません(値の大小に意味が 無いため。シンボル型と違い、名前との完全一致で段を決める仕組み(6.4 節)も持ちません)。- 引く先がこの宣言を持たないプロパティならロード時エラーです。 値は数のままなので、ただの数値
プロパティを引く
to_objectは、実行時に無関係な型を引くか黙って何も起きないかのどちらかになります。 型の名前を直接書いたときにsingletonを要求するのと同じ、書いた時点で分かる誤りです。 - その宣言を持つプロパティを、たまたま持っていないオブジェクトが引いたときは何も起こしません (9.6 節の解決失敗と同じ扱い)。「片側の隣が無い端の海区」を、書き分けずに表せます。
7. slots(親子関係とコンテナ)
7.1 基本構造
- オブジェクト間の所属関係はツリー構造で表現します。1 つの子オブジェクトは必ず 1 つの親に属します。
- 親オブジェクトは複数の「スロット」(配列)を持ちます(例:
inventory、equip、fixtures)。 - 親子関係の正の情報源は親側のスロット配列です。実行時の持ち方と、出入りを集約する単一の操作
(
move_to_slot)はSlotSystem.md1 節。 - スロットに「種類タグ(kind)」は設けません。スロット名を直接参照します。
- すべてのオブジェクトは、唯一のシングルトンである
world(15 節)を根とするこのツリーのどこかに、必ず所属します。 このツリーに繋がっていること自体が「世界に存在すること」を意味します(別途の一覧管理はしません)。destroy(9.3 節)は親スロットからの切り離し、spawn(9.4 節)は生成とこのツリーへの接続として表現できるのは この前提のためです。 - 自分自身と、自分の中身の中へは入れられません。 入れると
worldから切り離された輪ができ、 上の「必ず所属します」が破れるためです。ツリーの形そのものを見る判定なので、枠の要件(7.2 節)とcapacity(7.3 節)がどちらも通っていても失敗します。
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}
cell: すべての枠に共通する要件。cell_countと併せて書けば枠数が決まり、書かなければ 枠数は決まりません(=前詰め。SlotSystem.md3 節)。cells: 枠ごとの要件を並べます。並べた数がそのまま枠の数なのでcell_countは書けません。 「板 1 枚と棒 4 本」のようなレシピの材料欄がこれで、何が何個必要かがそのまま枠の形になります。cell_count: 枠の数。cellsと同時には指定できません。cellとcellsはどちらか一方だけを書きます。どちらも書かなければ、どんな型でも無制限に積める枠が 必要なだけ増えます。
枠の中身は次のキーで決めます。
accept: その枠が受け入れる型(4.1 節の型指定)。省略すると型を問いません。max: その枠に積める同種の個数(省略で無制限)。束ねられる型にだけ効きます——stackable: falseな型(道・かご、7.6 節)は同種でも束ならないので、1 枠には必ず 1 個しか入りません。
入れた物がどの枠へ入るかは 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箇所に割れます。
weight: 物の重さ。自分の値に、中身のweightをそのまま足します(率はかけません)。中身入りの 容器(LiquidContainerSystem.md1 節)はfill × densityが加わります (densityは単位量あたりの重さ = g/mL、水は 1)。籠をそりへ積めば、そりの重さは籠と中身をそのまま 加えたものです。load: 担いだ人が感じる負荷。直接の子のweightに、その子のload_rate(担いだときに感じる 割合)を掛けた分の合計です。持つのはキャラクターだけで、他のオブジェクトは定義しません。宣言しなければ 素の重さがそのまま効き、1を超えれば実際より重く感じます(長物を素手で抱えるような場合)。下限は 見ません——負の率も書いたとおりに効きますが、それは負のmodifyを書けるのと同じことで、端を持つのは 受け取る側(loadのrange)です。
これらの伝播は、エンジンが型に生やす 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 節の排他を宣言と判定へ落としたもので、覆すのに人間の判断は要りません。
- アイテムは
covers(覆う部位のタグの配列。例:[torso])とlayer(重ね着の階層。例:base、outer)を 持ちます。2 つは対でしか宣言できません——片方だけでは競合を判定できない(部位の無い階層は誰とも 重ならず、階層の無い部位はどの段で重なるかを言えない)ので、片方だけならロード時エラーです。 - 競合判定は、同じ
(covers要素, layer)の組が、既に同じ枠に入っている他アイテムと重複する場合に 装備不可とします。 - どの枠が身につける枠か(
equipment)を知っているのはWorldVocabulary(7.4 節)で、枠の宣言からは 導けません。
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の各エントリに指定):
-
stackable(既定true): 同種と束ねてよい型か。falseなら同じ型でも1個ずつ別の枠に並びます。 入れ物ではなく物の性質なので、スロットではなくこちらで宣言します。束ねたくない理由と、枠のmaxとの関係はSlotSystem.md4節。 -
stack_order:{property, ascending}。スタック内での並び順を、指定したプロパティの値で決めます。 「手前に重ねたいものほどリストの末尾に並ぶ」という規約のもとで、ascending: falseは値が小さいものほど 末尾(劣化アイテムなら寿命が短いものほど手前に)、ascending: trueは値が大きいものほど末尾を表します。 省略時は並び順が未定義で、新規インスタンスは常にスタックの末尾(挿入順)へ追加されます。劣化する液体など、 複数のプロパティが絡む並び順のパターンは今後の検討課題です(SlotSystem.md7節)。
object_defs:
wet_log:
props:
freshness:
value: 100
stack_order: {property: freshness, ascending: false}
stack_orderが効くのは新規インスタンスが加わるときだけで、一度並んだ後の扱いはSlotSystem.md6節。
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 は
入れる側が誰かを表します。
autoを外すのは、上限の無いスロットが吸い込むのを止めるためです。上限の無いスロットが宣言順の どこかにあると、それより後ろのスロットも、あふれた先である親も一切使われなくなります。manualを外すのは、プレイヤーが持ちようのない物や、渡す道理の無いスロットです。獣がくわえた物の 枠がこれで、外からは見えても(7.11 節)渡すことはできません。- 走査の対象外であることは、こぼれた先の親(9.4 節)でも変わりません。こぼれ落ちる先でも走査の決まりは 同じで、どの段でも枠の宣言はそのまま効きます。
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 # 身体から離れた「捻挫」は無い
宣言すると次のことが起きます。
- 他の持ち主へ移せない。 いったん親に付いたら、
move(9.6 節)でもドラッグでも別の親へは 移りません(生まれた直後の、まだ親を持たない状態での配置は通ります)。枠のaccept(7.2 節)が 「入れる側から見て、何を受け入れるか」を言うのに対し、こちらは「その物がどう存在するか」なので、 こぼれ落ち(9.4 節)でも移りません——こぼれる代わりに、持ち主と一緒に世界から外れます。 - 親が消えれば道連れになる。
destroy(9.3 節)は中身をその親へこぼしますが、bound_to_ownerな中身はこぼれず、一緒に世界から外れます。
スロットではなくオブジェクトに書くのは、これがその物の性質だからです。同じ「怪我スロット」でも、 入っている物が捻挫なら剥がせず、湿布(治療具)なら剥がせる、という違いはスロット側では言えません。 怪我が治って捻挫が消えるとき、当てていた包帯だけが本人の手元へこぼれ出るのがその現れです。
プレイヤーが怪我を出し入れできないのは、この宣言の帰結です。 取り出せないのは捻挫が
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] # 帆は外から見える。積荷は乗り込まないと分からない
- 既定は空です。名乗らないスロットは外から覗けません。筏は土地と同じ
items/fixtures/charactersを持ちますが、外から覗ける場所にすると「開けば中身が並ぶ箱」になってしまいます。 - 真偽値をスロットへ書くのではなく、オブジェクトが名前を並べます。
property_tags(6.7 節)・recipe_categories(3.1 節)と同じ形です。この形だけが次のすべてを満たします。- 綴り間違いをロード時に捕まえられます(そのスロットを持たなければエラー)。スロット側の真偽値は、 書き間違えても黙って無視されるだけです。
- 表示順を宣言順から切り離せます。 スロットの宣言順は既に自動配置の走査順(7.7 節)が使っていて、
locationtrait がitemsを先頭に置いているのはこぼれた物の行き先を決めるためです。ここに表示順まで 載せると、無関係な 2 つの関心が 1 つの順序を取り合います。 - trait が配ったスロットでも、型ごとに見せるかを変えられます。
- trait と自分自身の宣言は足し合わせます(
tagsと同じ、5 節)。trait 由来を先に、自分自身の宣言を 後ろに並べ、同じスロットを 2 度書いても先に現れた位置を保ちます。取り消しはできません——trait が 見せると決めたスロットを、参照する側が隠すことはできません。 - 画面に定位置を持つスロットは名乗りません。 手持ちは常にレーンに出ているので、タブにも出すと
同じ札が画面に 2 枚出ます(
Windows.md1.1 節)。キャラクタが名乗るのは 装備と怪我だけです。
「見える」と「入れられる」は別です。 筏の積荷は名乗っていませんが、筏のカードへ重ねれば積めます (7.8 節)。中を見るには乗り込む、という違いがそのまま操作の形になります。
7.12 storage(物を溜める入れ物)
storage: true は、その型を物を溜める入れ物として使うことを宣言するオブジェクトレベルのキーです
(既定 false)。名乗った型は、上限(capacity、7.3 節)を持つスロットの詰まり具合をカードのバーに
出します(CardView.md 8 節)。
traits:
item_container:
storage: true # かご。中身の枠の詰まり具合をバーに出す
object_defs:
raft:
storage: true # 筏。積荷の枠が上限を持つ
- 上限を持つスロットがあるだけでは出しません。 「上限がある」と「物を溜める入れ物である」は別の
ことです。液体の容器が抱える量は、スロットではなく容器の
fillが持ち、中身のバーが映します (LiquidContainerSystem.md2 節・3 節)。 - 上限を持つスロットが複数あるときは、最も詰まっているものを出します。 バーが答えるのは 「あとどれだけ入るか」なので、先に一杯になる側を映します——合計で割ると、片方が満杯でも半分に 見えてしまいます。
- trait と自分自身の宣言は OR で合成します(
bound_to_ownerなどの真偽値と同じ、5 節)。
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 と対をなします。あちらが「今の持ち主から離れられない」を言うのに対し、
こちらは成立している間だけ「そもそも持ち主に付けない」を言います。
- 土地だけが持ち主になりません。 土地(
locationタグ)の枠へは普通に置け、土地から土地への移動 (自分で隣へ逃げる)も通ります。それ以外の親——手持ち・かご・台車・罠——へは移れません。 - 入れ物の中で成立したら、こぼれ出ます。 台車に積んだ動物の警戒が上がれば、その瞬間に最も近い
土地へ落ちます(
destroyが中身を親へこぼすのと同じ扱い、9.3 節)。土地以外は受け取らなくなるので、 行き先を名指しせずとも上へ遡るうちに土地が最初の受け取り手になります。 - 読むのは実効値です(8.3 節)。外から与えられた寄与——罠の
modifyが警戒を打ち消すような——も そのまま効くので、掛かっている間は成立せず、取り出した瞬間に成立します。 - 生まれた直後(親を持たない間)の配置は通ります(
bound_to_ownerと同じ免除、7.9 節)。罠が獲物を 自分のcatchへ生むのは、誰かが持ち主になろうとする移動ではありません。通すのは関門だけなので、 拘束しない入れ物の中へ生まれた物は、付いた直後の判定でそのまま土地までこぼれ出ます。 - 判定が走るのは、値が動いたとき・tick のたび・親が変わったときです。tick を見るのは、実体値だけで なく寄与の掛かり方も時間で変わるためです。
- 枠ではなくオブジェクトに書くのは、これがその物の性質だからです。同じ
handの枠でも、入れよう としている物が石なら入り、荒ぶるイノシシなら入らない、という違いは枠側では言えません(7.9 節と 同じ理由)。 - 自分自身の宣言があればそれだけが効き、無ければちょうど 1 つの trait の宣言が効きます(
stack_orderと 同じ、5 節)。並び全体が 1 つの条件木なので、連結すると書いた覚えのない合わせ技になります。
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を持たない場所になる
- 落とすのは混ぜ終わってからなので、誰が配ったスロットでも落とせます——
slotsに同名を書いて 上書きする形(5 節)とは別物で、あちらは同名エントリが複数の trait にあればエラーになります。 - trait にも書けます。 落とすことが「その trait であること」の側の事実——海は物を置ける場所では ない——なら、混ぜ込まれる型ごとに書かせません。
- 落とす先が無ければロードエラーです。綴り間違いも、もう配られていない名前も、通せば「落とした
つもり」の宣言が残ります。同じ宣言の中で
slotsに書きながらremove_slotsにも挙げるのもエラーです。 - 枠を作るかどうかは、置いてほしい物ではなく置けてしまう物で決まります。置けてはいけない物が
あるなら、条件で弾くのではなく枠そのものを作りません
(
GameEndings.md12.7 節)。
8. passives(持続する影響)
passives は、効果の対象との関係(何を対象に書けるかは 8.1 節)とゲート(常時/conditions/
プロパティの stage)に紐づき、その関係が続く限り評価され続ける、持続する影響を表します。
次のレベルで定義でき、いずれも同一の記法・実行原理を用います。
- オブジェクトレベル(例: 防具そのものが持つ複数の恩恵の束)
- プロパティレベル(例: アイテムの重量プロパティが持ち主の負荷に寄与する)
- プロパティのステージレベル(6.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(ゲート)
conditions(14 節と同じ文法・木構造)が真である間、継続的に有効(レベルトリガー)です。1 つのブロック全体 (modify/addに含まれるすべての対象)に対して、共通のconditionsを 1 つだけ持ちます。 ここは条件の並びなので、要素にreasonは書けません(14.6 節)。conditionsを省略すれば「常時(無条件)」を意味します。self/parent対象の効果は、どちらも「このブロックを宣言したオブジェクト自身が今どのスロットに入っているか」 という同じ実体を指すため、conditionsのsubjectは省略(既定値self)して書くのが自然です(例:{in_slot: equip}は「自分自身が親のequipスロットに入っている間」)。childは効果の対象 (8.1 節)専用で、subjectには書けません(ロード時エラー)——ゲートは1つの解決先を要求するのに対し、childは複数へ配るための対象だからです。
8.3 modify
条件が真である間だけ計算式に寄与する、可逆的な修飾子です。条件が偽になった瞬間に寄与は自動的に消えます。値 そのものを書き換えるのではなく、実効値を都度導出するための入力として扱います。
- 同一プロパティに対する複数の
modifyは単純加算で合成します。 - 合成結果の上限・下限(min/max)はプロパティ側の
range(6.3 節)に持たせます。 modifyの対象にしたプロパティにはon_max/on_minを書けません(6.3 節)。- 操作の関係(11.5 節)を対象にできるのは
agentだけです。押した寄与は相手の上で合計され、読んだ側は それが誰の寄与かを見分けられません。agentは 1 つの操作に 1 人しか居ませんが、instrument・patientは 複数の操作に同時に就きうる(2 人が同じ道を歩く、同じ炉に薪をくべる)ので、混ざります。「一意な参加者にだけ 押せる」という 1 つの規則で、役ごとの特例ではありません。- 共有されうる相手へ効かせたいことは、受け手が引く形で書けます。手当ての間だけ出血が止まるなら、傷の側が
conditions: [{subject: agent, ...}]を付けて自分をmodifyします——agentが解決すること自体が 「今、誰かに手当てされている」を意味します。2 人がかりでも止まるのは 1 回ぶんで、押す形より正しくなります。
- 共有されうる相手へ効かせたいことは、受け手が引く形で書けます。手当ての間だけ出血が止まるなら、傷の側が
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 の対で書くのとは、次の点で違います。
- 出せる量しか動きません。 残り 1 の値から 3 を出そうとしても 1 しか動かず、
addの対(出す側-3/受け取る側+3)のように、片方が下限で止まっている間に相手だけが増えることがありません。 - 換算率が 1 箇所にあります。
to_amount(9.5 節)が「出した量に対して受け取る量」そのものなので、addの対のように必ず一致すべき数値を 2 箇所に置く必要がありません。
輸送は宣言順に適用され、互いの結果を見ます(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 建てです(6.0 節)。「時間を数えるもの」は
1 = 1 tickで、range.maxは 「基準レートで何 tick 保つか」そのものです。分へ割ると、宣言の単位と値が動く単位が割れます。 - 同じ
addに、率ではないものが乗っています。 動詞が名乗るのは可逆性だけで、率か拍かを名乗る場所は ありません(8.4 節)——core.yamlのworldがadd: {self: {tick: 1}}で拍を数えているのがそれです。 分へ割るなら、どのaddが率かを名乗らせる宣言を足すことになります。 - tick は動物の 1 手でもあります(
HuntingSystem.md2 節)。率だけを分へ剥がすと、 率で動く値が起こす range イベント(6.3 節)だけが拍から外れ、手番と噛み合う順序が世界に 2 つ並びます。 - 画面がこの刻みを見せています(
../ui/CardInteraction.md7 節)。時間経過の 輪は目盛りごとに一拍置き、状況エリアの時計はその瞬間へ飛びます。値が分ごとに動くなら、この見せ方は 実際の進み方と食い違います。
粒度が残り時間の数字として見えてしまう側は、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
どちらも対象プロパティの実体値へ、その場で一度だけ不可逆に反映します。
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)——も名乗れます。
- 省略、または
into: same_slot:selfが今いる、まさにその場所(親と、selfが現在占めているのと同じスロット)へ そのまま配置します。クラフト・腐敗など「同じ場所で別の物に置き換わる」場合に使う既定動作です。スロットの 走査は行いません。same_slotは省略時と全く同じ意味を持つ、明示したい場合のためのキーワードです。 引き継ぐのは位置だけで、生まれたオブジェクトのプロパティは(配置先の指定によらず)自分の型が 宣言した初期値(6.2節)から始まります。同じ名前のプロパティを置き換え元も持っていても、その値は移りません。 into(対象キーか{subject, prop}): その相手を起点に、持っているスロットを宣言順に走査します。 書ける役は宣言の置き場所で決まり、moveの移動先(9.6 節)と同じ規則です——その場所に居ない役を 書くとロード時エラーになります(on_max/on_minにagentは居ないので、そこへinto: agentとは 書けません)。どの置き場所から何を書けるかは 11.5 節「役を書ける場所」の表が唯一の一覧です。 実行時に相手へ辿り着けなかったとき(into_propの指すインスタンスが世界のどこにも居ない、など)は、 他の対象解決の失敗(9.1 節)と同じく、生まれた物がどこにも置かれずに消えるだけです。into_prop:selfが持つプロパティ名。その実効値をインスタンスIDとして解釈した個体へ入れます (moveのto_propと同じ)。実行時に決まった相手へ置くための形で、相手を覚えるのはset(9.2 節)です。into_object: 行き先の型。世界の木を辿ってその型のインスタンスへ入れます (moveのto_objectと同じ)。世界にただ1つ在る型(singleton、15 節)のためのものです。into: child:selfの子を順に走査し、最初に受け取れた子のスロットへ入れます。走査の決め方は スロットに対するものと同じで、著者は「どの子か」も「どのスロットか」も書きません。同じ効果のspawnは配列の順に1つずつ生んで配置するため、直前に生んだ子へ続けて物を入れるのに使えます (罠が掛けた獲物へ怪我を渡す経路。TrapSystem.md5.3 節)。受け取れる子が 1つも無かった場合の伝播先は、selfの親です。
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
from(省略可、既定値self)・to(省略可、既定値self): 参照ルート。set/addの対象キー(9.1 節)と同じで、何を書けるかは 14.1 節の表が決めます。from_prop・to_prop: それぞれ移送元・移送先のプロパティ名。amount: 一度にfromから出すことを試みる量の上限。to_amount(省略可、既定値はamount):amountの全量を出したときにtoが増える量。fromとtoの単位が違う移送の換算率を、比として表します。省略すれば 1 対 1(同じ単位)です。 実際に増える量は実際に出した量 × to_amount / amountで、allow_overflow: falseのときの 「収まる分まで」の判定は、toの残り容量をfromの単位へ割り戻してから行います。 換算率をエンジンは知りません——飲んだ水が体内の水分へどれだけ寄与するかは液体の性質なので、 移送する側の宣言が持ちます(酒は同じ 1 口でも水より寄与が小さい)。0 以下は書けません。allow_overflow(省略可、既定値false):toのrangeに実際に収まる量までしか移動しません (収まらない分はfromに残ります=液体を無駄にしません)。trueにすると、fromの残量とamountだけで 移動量が決まり、toのrangeを超えた分はtoのon_max(未指定なら既定のクランプ、6.3 節)に 委ねます(あふれた分は失われます)。出どころを消す操作ではtrueが必須です(9.5.2 節)。linked_add(省略可):add(9.1 節)と同じ構造で、実際に移動した量に比例してスケールされる 追加の加減算を表します。各プロパティへの加減算量は宣言値 × 実際に出した量 / amountで確定します。 固定のaddでは表現できない「飲んだ量に比例した副効果」(例: お茶の眠気改善)を表すために使います。
# お茶(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
- 動かす物:
subjectとsubject_propのどちらか一方で指します(両方書く・どちらも書かないのは ロード時エラー)。subject: 指せるのはその宣言が置かれた場所が用意できる相手です(14.1 節の表。操作の関係の役は 11.5 節「役を書ける場所」)。{subject: ancestor, prop: raft_id}のように{subject, prop}の形でも 書けます——その形は指す先のプロパティ名を決めるので、ancestorもそこでは指せます。childは 「一度きりの命令に対してどれを動かすか」が決まらないため、どの場所でも指せません。subject_prop:selfが持つプロパティ名。to_propと同じく実効値をインスタンスIDとして解釈します ——動かす物も行き先も、実行時に初めて確定する個体を指せます(動物が足元の物をくわえる 1 手、HuntingSystem.md5 節)。
- 移動先:
toとto_propとto_objectのどれか1つで指します(複数書く・どれも書かないのは ロード時エラー)。この三択はspawnの配置先(into/into_prop/into_object、9.4 節)と同じもので、 違うのはキーの綴りだけです。to:subjectと同じく、その場所が用意できる相手を指せます(14.1 節)。行き先が定義時点で 決まっている場合に使います。parentは、自分の居る場所へ物を押し出す場合(小島が岸の筏を海区へ出す、下の例)に使います。指した相手が居ない場合は何も起きません。to_prop:selfが持つプロパティ名。その実効値をWorldObjectのインスタンスID(生成時に発行される、 実行時限りの識別子)として解釈し、移動先とみなします。書き込むのは生成器(地上の道のdestination_id)かset(9.2 節。筏が出航した海岸を覚える)です。to_object: 行き先の型。世界の木を辿ってその型のインスタンスを探し、移動先とします。 世界にただ1つ在る型(singleton、15 節)のためのもので、海区・本土のように定義の時点で型は 分かっているが、生成時のインスタンスIDは分からない行き先を指します (Voyage.md4.2 節)。singletonでない型を指すのはロード時エラーです ——複数在りうる型では、どの個体へ着くかを世界の形が決めてしまうためです。 型の書き方は分かれますが、探し方は同じです。object_defsの識別子をそのまま書くか、型を値に持つselfのプロパティを{prop: <名前>}で引きます(6.9 節)。リテラルと参照の見分けがスカラーか マップかなのはsetの値(9.2 節)と同じ規約で、同じ行き先を 2 か所へ書き写さずに済ませるための ものです。
- 移動先の枠:
to_slotに移動先のスロット名を書くと、その枠だけを試します。省略した場合は 宣言順で最初に受け取れる枠(spawnのintoと同じ走査、7.7 節)です。行き先の型が同じものを 受け取れる枠を複数持つとき、宣言順に頼らず狙った枠を指すために使います。
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 とは、書ける場所が構造上重ならないため、両者を混同する余地はありません。
interactionsの各エントリ(11 節・12 節)—trigger/conditions/durationと 対等な兄弟キーpickの各候補(10 節)—weightと対等な兄弟キーpropsのon_max/on_min(6.3 節)— これらは専用のキーの直下にそのまま書きます
同じ場所へいくつでも並べて書け、適用順は 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 節)なので、出入りを伴わない出来事は、
起こした側が告げなければ誰にも届きません。
- プロパティのカウンタで代用しません。 値は効果の入力であって通知路ではなく、増やせば後始末 (0 へ戻す)が要り、増分は実効値の前後比較にしか現れません。
- 告げるのは出来事であって、見せ方ではありません。 どう見せるか(札の上の文字・音)は受け取った
側が決めます(
CardView.md14 節)。 - 表示文言は locale の
signal_texts(Localization.mdsignal_texts節)が持ちます。 要件のreason(14.6 節)と同じく、識別子は表示のためだけに使います。 - 告げられる時点は、これをどこに書いたかで決まります(9.7 節の置ける場所)。
interactionsの中に 書いた分は効果なのでその操作の時間が過ぎ切った後、on_max/on_min(6.3 節)に書いた分は値が 境界に達したその瞬間です。操作の中に書いたうえで、時間が過ぎること自体を先に告げたいときはannounce(11.6 節)を持たせます。
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 に
あたります。
subjectはdestroy(9.3 節)と同じ形で指せて、省略するとselfです。- 行き先の座標に型が居ない組み合わせは、候補になりません(
ActionSystem.md1.1 節)。落とせるのに何も起きない操作を 出さないためで、conditionsの不成立と違ってreason(14.6 節)は持ちません——成立していないのは 条件ではなく行き先の型そのものだからです。 - 9.7 節のとおり、
activeを書ける位置に他の動詞と対等に並べます。
「同じ物が姿を変える」すべてがこの操作ではありません。 行き先は座標なので、手で書いた別々の型の
あいだは行き来できません——石と尖った石、ヤシの実と皮を剥いたヤシの実は、それぞれ独立した型なので、
移り変わりは destroy と spawn が担います(副産物が同時に生まれるのも、そちらなら書けます)。
1 つが 2 つに増える変化は、個体が 1 つ続くというこの操作の前提そのものに合いません。
9.9.1 何を引き継ぎ、何を失うか
- プロパティ: 同じ名前のプロパティは値を引き継ぎます。新しい型にしか無いものは初期値から始まり
(6.2 節の範囲値はここで振ります)、新しい型が持たないものは捨てます。
range(6.3 節)が変わって 引き継いだ値が外れる場合も実体値は丸めずに運び、on_max/on_minは発火させません ——器が変わったのは値の出来事ではないためです(外れた分は、読むときに実効値の側で切られます。6.3 節)。 - スロット: 同じ名前のスロットが中身を引き取ります。新しい型が受け取れなかった中身は、
destroy(9.3 節)と同じ規則で親へこぼれます(bound_to_ownerな中身は移せる先が無いので、 そこで失われます)。行き先のスロットが無い場合も、枠(7.2 節)やcapacity(7.3 節)が足りない 場合も同じ扱いです——変わった先の型が持てない状態を作らない、の 1 つで決まります。中身は 宣言順に配るので、収まる分が残り、溢れた分が出ていきます。 - 個体の同一性: インスタンスIDは変わりません。変わるのは型だけなので、スタックの所属だけを
判定し直します(
SlotSystem.md5 節)。 - その tick の積分: 時間の経過の途中(
on_max/on_minから、8.4 節の積分の最中)に起きても、 その tick 分は落ちません。まだ積分していないプロパティは、作り直された側がその tick 分を 受け取り、既に積分を終えたプロパティへ引き継いだ値に二度は積みません。操作が宣言した持続効果 (11.7 節)が足した分も、同じように最後までその操作の稼ぎとして数えられます (CardInteraction.md10.1 節)。新しい型にしか無いプロパティも 同じで、生えたその tick から数えます——数え先は「どの個体のどのプロパティか」で指すので、 名乗る時点でまだ在る必要がありません。 - 走っている最中の輸送: 出す側の
on_minから起きた場合でも、出した分は受け取る側へ入ります ——transfer(9.5 節)は受け取る側を出した後に辿り直すので、作り直されたプロパティが受け取ります。在庫が 片側から消えるだけ、が起きてはいけないためです。新しい型が受け取る側のプロパティを持たないなら、 そこで失われます——引き継がないプロパティを捨てるのと同じことです。 - その tick の輸送: tick 毎の輸送(8.4.1 節)を走らせている最中に起きた場合は、そこで 打ち切ります——宣言順で後ろの輸送は、その tick には走りません。旧型の宣言を、もうその型でない 物へ当てないためです。残りを新しい型の宣言へ引き継がないのは、輸送の宣言には型をまたぐ 同一性が無いからで——引き継ぐ値をグローバルIDで数えられる積分と、ここが違います——済ませた分と 対応が取れず、同じ在庫を二度動かしかねません。打ち切った分は次の tick が運びます。
- 並べて書いた命令の残り:
becomeと同じ場所に並べた命令(9.7 節)は打ち切らず、変化後の型の 物へ当たります。そこに在るのは著者が書いた 1 続きの手順で、becomeの後ろに置いた命令は 「変わった後にすること」だからです——雨を受け始める操作は、水の入った型にしてから溜まり始めの量を 書き込む、この順で書きます。すぐ上の輸送と逆になるのは、あちらがエンジンの回す一式で、becomeは その中の 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値+条件付き補正)を新設せず、以下のいずれかとして表現します。
- リテラル定数: 外部からの干渉を想定しない候補向け。
- 既存プロパティへの参照(
{subject, prop}、14 節の条件式の葉と同じ書き方。subjectは省略可で既定値はself。書ける起点はその宣言が置かれた場所が決めます、14.1 節の表): 外部から干渉させたい候補向け。参照先は 通常のpropsとして定義された値です。
この二択より豊かな書き方はありません。 式(積・和・条件付きの補正)を書ける場所を作らないのは、
可逆な寄与を合成する器を 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つ選ぶことと、その重みだけ。
- 重みの参照は候補自身を指すので
{subject: picked, prop: ...}と書きます。subjectの既定がselfである規則(10.2 節)はここでも変わりません。 - 全部の重みが 0 以下なら先頭の候補が選ばれます(
pickと同じ規約、10.2 節)。 - 候補が1つも無ければ、その
pickの候補は抽選に出ません。 「相手が居なければ起こらない」を 条件や重みの打ち消しで書く必要がありません。全部の候補が外れれば、そのpickは何も起こしません。
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 |
〜を | 働きかけられる物。操作の宣言が乗っている側 |
格は名前の出どころで、どの物がどの役に就くかを決めるのは「宣言がどちらに乗っているか」です。 だから どちらの名前も、格が言うほど狭くありません。
instrumentは道具や触媒に限りません。塩と肉から塩漬け肉を作る組み合わせは宣言が肉に在るので、塩は道具では ないがinstrumentです(燃料・枠へ入れる物も同じ)。patientも、変化する物や働きかけられる物に限りません。道のtravelは宣言が道に在るので道がpatientですが、道は変わらず、動くのはagentです。
宣言をどちらに置くかを決めているのが 12.3 節で、重ねる操作では「変化の本体に置く」——それが patient に
なります。相手を伴わない操作(menu・tick)では、宣言の置き場所がそのまま役を決めます。
- すべての操作に
agentが居ます。 きっかけが時間でも(trigger: tick、11.1 節)、時間が配るのは手番で、 動くのはその物自身です(動物の 1 手)。居ないのは、操作でもなく、問う側がagentを渡すのでもない場所 (下の表でagentが—の行)だけです。 - 再帰的な操作では、
agentとpatientが同じ個体です。 眠る・休む・動物の 1 手のような、自分に対する行動。 宣言の中で書けるのはself(=patient)とagentで、実行時には両方が同じ物を指します。 - 役は、画面での操作の仕方では決まりません。 枠へ物を入れる操作はカードへ重ねる・レーンへ落とす・端の矢印で
送る、のいずれでも起こせますが、どれでも入れる物が
instrumentです(7.10 節)。きっかけの名前(menu・drag、 11.1 節)が経路を指すのに対し、役はそれが何であるかを指します。
役を書ける場所
宣言が置かれた場所で静的に決まります(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 節) |
— | — | — |
- ✕ は「書くとロード時エラー」、— は「その場所に居ないので解決先を持たず、やはりロード時エラー」です。
区別が要るのは理由が違うからで、
patientの ✕ は居ないからではありません。 patientが操作の宣言の中で書けないのは、役の定義そのものが「宣言が乗っている側」だからです。 どの操作でも必ずselfと同じ物になり、同じ物に 2 つの名前が付くと、読む側は毎回どちらが正かを考えることに なります。agent・instrumentの定義にはそれがないので書けます。agentがselfと同じ物になる操作はあります——自分に対する行動(眠る・動物の 1 手)です。trigger: tickは必ずそうなりますが、menuの眠るも同じで、きっかけでは決まりません。役の定義から 決まるpatientとは違い、その操作が自分に向いているかで決まるので、名前を禁じる理由にはなりません (書けばselfと同じ 1 つの物を指すだけです)。
- 参加者の
propsから 3 役とも書けるのは、そこに「宣言元がどの役に就くか」を静的に決めるものが無いからです。 実行時にたまたま宣言元自身へ解決することはありますが、それはselfと同じ物を指す 1 つの参照であって、 2 つの名前ではありません。 - この表が答えるのは置き場所だけです。 可逆な寄与(
passivesのmodify、8.3 節)を押せる先はさらに狭く、 役ではagentに限られます——絞っているのは置き場所ではなく、同じ物が同時に複数の操作でその役に就きうるか という別の軸です。 - レシピの解放条件・
crafting_conditionsは操作ではありません。 「誰にとって解放されているか」を問う判定 なので、agentだけを問う側が渡します(13.2 節)。 - 製作の 1 工程は操作です(13.4 節「工程を進める操作」)。
patientは製作中オブジェクト、agentは作業して いる者で、instrumentは居ません——素材も道具も運ばれてきた側ではなく、既に材料スロットの中身だからです (運び入れるほうがput_inの操作で、そちらでは入れる物がinstrument)。工程そのものに役を書ける欄は ありません——書けるのは要求と所要時間のリテラルだけ(13.1 節)。レシピの宣言には、この関係の中で解ける役が 1 つもありません。deftnessは役を書きません——見るのは作り手の腕の段だけで、解放条件と同じく問う側がagentを渡す形で解きます(13.2 節)。関係の外で解くのは、押す前に分数を見せる画面が関係を張らずに同じ数を 出せなければならないためです。関係が効くのは、参加者のpropsの行(工程の最中の tick で役が解ける)と、 下のagentの一意性です。crafting_conditionsはこの関係の外で問います——操作ではない(1 つ上の行)ので関係を張らず、画面が 「作業する」の可否を出すときと同じ問いになります。内側で問うと、押す前に見せた可否と実際の可否がずれます。
関係は世界に刻まれます。 条件(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 つの操作に就きます。進行中の操作は中断せず、待たせます——歩き終わって
から倒れる、が答えです。
- 待たせるのは
durationを持つ手番だけです。時間を要さない手番(動物の 1 手)は、その tick でそのまま 起きます——動作主が違うのでagentは重なりません。 - 切れ目は、操作 1 つの実行が終わったところ(効果を適用し終えた後)です。待たされた手番はそこで要件を 引き直してから起きるので、待つ間に限界を抜けていれば起きません。待たされた手番は別の操作なので、 押す前に見せる分数(下の「関係は世界に刻まれます」)には入りません——1 時間の道を歩いて 3 時間経つのは、 歩くのに 3 時間かかったのではなく、歩き終わってから倒れているためです。
- 待たされた手番を起こしている最中に挙がった手番は、その切れ目では起きません——同じ手番でも、別の 手番でも同じです。待たされた手番自身も時間を進めるので、その最中にまた同じ手番が挙がることも、別の値が 尽きて違う手番が挙がることもあります。どちらもその切れ目では起こさず、次に時間が動いたときの待ちとして 拾い直します——さもないと切れ目から抜けられません。落ちるのはその最中に挙がったぶんだけで、切れ目に 着くまでに挙がっていた待ちは、複数あってもその切れ目で順に引き直されます。
中断する側を採らなかったのは、中断とは「時間だけ過ぎて効果が適用されない」ことだからです
(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 時間後になります。
- 告げるのは要件(
conditions)を見た後です。 要件を満たさずに落ちた操作は何も告げません。 - 告げるのは「始まったこと」なので、後から取り消しません。 経過中に関与オブジェクトが失われて
効果を適用せずに終える回(
ActionSystem.md6.1 節)も、告げたことは残ります ——倒れ込んだ後に渇きで死んだのなら、倒れ込んだこと自体は起きています。 - 告げるだけで、世界の形は変えません。 ここに書けるのは
signalだけで、値の増減も物の出入りも 書けません(ロード時エラー)——時間が過ぎる前に世界を変えられると、durationの中で起きたことと 区別が付かなくなります。 - 表示文言の引き先も
signalと同じsignal_textsです(Localization.mdsignal_texts節)。告げられた側から見れば、どの告げ方も「起きたこと」1 件でしかありません。
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 節)、後ろのものはプレイヤーから届かなくなります。相手の指定が交わること自体は構いません——
交わるなら、次のどちらかで同時に成立しないようにします。
- 条件で分ける。 注ぎ移しは相手が中身入りでも(
tag: water)空でも(tag: liquid_container) 当たりますが、注ぎ元として成立するのは相手が中身を抱えているとき、受け皿として成立するのは相手が 空のときなので、重なりません(liquid_containers.yaml)。 - 指定そのものを分ける。 広葉樹を倒すのは柄付きの斧(
tag: chopping_tool)、樹皮を剥ぐのは 握りの刃(tag: handheld_blade)で、石斧はどちらにも当たるcutting_toolを名乗っていても 剥ぐほうには当たりません(timber.yaml)。
「複数のタグを同時に満たすもの」という指定は書けません(型指定が指すのはタグ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
- レシピは 1 つ以上の工程(
steps)からなります。 - 各工程は素材または道具を要求できます(
requires)。1 つも要求しない工程も書けます——1 つの工程が 1 時間を超えられない(ActionSystem.md6.3 節)ので、長い仕事では素材を入れ終えた 後も工程が続きます。要求を無理に散らすと、その工程で実際に使う物とずれます。素材(consume: true)は消費され、道具 (consume: false)は消耗しますが消費はされません。 - 各工程には仕事の量(
duration)が、それを素人が片付けるのにかかるゲーム内時間(分)として定義されます。 リテラル数値だけで、{prop: ...}参照は書けません——進捗の上限がこの合計なので(RecipeSystem.md1 節)、 誰が作っても同じ値でなければ、作りかけの進み具合が作り手ごとに違う意味を持ちます。 実際に経過する時間は作り手の手際ぶん短くなります(13.5 節)。 - 最後の工程まで完了すると、目的のアイテムが生成されます。
要求する相手は、型そのものでもタグでも指せます({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 本だけ持ちます。
- レシピごとには書けません。 「3 つ以上の物を組み合わせる複雑な作業」であることはレシピという形が 既に言っているので、条件をレシピごとに書き分けると、条件の無いレシピがそう決めたのか書き忘れたのか 見分けられなくなります。宣言が 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}
- 工程を進める操作は、レシピから自動生成されます(
RecipeSystem.md4 節)。条件の書き手が成果物も 作りかけも起点にできないのはこのためです(subjectに何を書けるかは 14.1 節の表が唯一の一覧)。 - 判定するのは
crafting.tryAdvanceCraftingです。 interactionsとして宣言されている手作業は、この一律の条件には掛かりません——それぞれが自分のconditionsに書きます。
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 つ足せば必ず倍になり、内容の側が置いた上限に収まらないためです。
deftness(手際) は、どの腕が・どの段から・何分縮めるかの宣言です。作り手のskillがfrom_stage以上なら、工程1つの分数からminutesぶんが引かれます。名乗った腕は、工程を進める たびに伸びもします——どのレシピがどの腕の仕事かを言う宣言はここしか無いので、速さと伸びを同じ 1 本が決めます(SkillSystem.md3.4 節)。minutesは負の値 ——手作業の側(所要時間のpassivesがmodifyで積む量、11.3 節)と向きを揃えます。どの行動をどれだけ速くするかを行動の側が 持つ理由は../world/Skills.md6 節——腕の数は増減しにくく、行動の数は 増減するので、組み合わせを宣言するのは増減する側です。 縮めた先が tick の刻み、1 tick(15 分)を割る宣言はロード時に弾きます(ActionSystem.md6.2 節)——黙って下限で止めると、格子から外れた宣言がそのまま世界に残ります。skillとfrom_stageの 綴りも同じくロード時に検めます(3.6 節)。conditionsから導かず、別に名乗ります。 上位のレシピは複数の腕を連言で要求する(SkillSystem.md4.1 節)ので、解放条件からはどの腕が速さを決めるか 1 つに定まりません。deftnessは参照を書きません。 見るのは作り手の腕の段だけで、読むのは工程を進める最中——そこに 居るのはまだ作りかけなので、宣言元の個体を起点に辿る参照は解決先を持ちません(13.2 節)。
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}
-
subject(省略可、既定値self): 参照ルート。この葉が誰を見るかを決めます。self(宣言した オブジェクト自身)・parent(その親)・ancestor(selfの直接の親から遡り、propが指すプロパティを 定義している最初の祖先。8.6 節と同じ探索ロジック)・agent/instrument/patient(操作の関係の役、 11.5 節)・child(付いている子それぞれ、8.1 節)のいずれか。world(15 節のシングルトン)は未対応(ロード時エラー、17 節)だが、ancestorは見つからなければ 自然にworldまで遡るため、世界固有と思われる概念の参照はancestorで代替できる。どれを書けるかは、その宣言が置かれた場所が何を用意できるかで決まります(一覧を場所ごとに 覚える必要はありません)。
ルート 書ける場所 self/parent宣言元の個体が居る場所(レシピの解放条件・ crafting_conditionsには居ません。理由は 13.2・13.4 節が各々で述べます)ancestor上に加えて、指す先のプロパティ名が決まっているとき。祖先はその名前で探すので、プロパティ名が関わらない場所——オブジェクトそのものを指す場所——では解決先が決まりません agent/instrument/patient置き場所ごとの可否は 11.5 節「役を書ける場所」の表が唯一の一覧で、ここには写しません——2 箇所に書くと必ず食い違います pickedamongを書いた候補の中(10.3 節)childpassivesの対象(付いている子ごとに登録を配る唯一の場所、8.1 節)解決先を持たないルートを書くとロード時にエラーになり、その場所に何が無いのかが理由として出ます。 書けたのに実行時は必ず空振りする、という書き方をここで塞いでいます。
-
prop: 参照するプロパティ名。 -
比較演算子のキー:
lt/lte/gt/gte/eq/neq/in/not_in。値が比較の相手で、 リテラル(数値・真偽値・シンボル名、6.6 節)か{subject, prop}参照(weightの path 参照、 10.2 節と同じ二択)のいずれか。参照はlt/lte/gt/gte/eq/neqでのみ使えます(in/not_inは 複数値との比較のため、単一の参照とは噛み合わずロード時エラーになります。値は配列で書きます)。 -
in_stage/in_stage_or_above: 値はどちらもそのpropの段(6.4 節)の名前で、段の境目をstagesのminと条件の側へ二重に書かずに済みます。in_stageは実効値が今ちょうどその段に居るとき、in_stage_or_aboveはその段かそれより上の段に居るときに真です。段の名前を宣言しているのは 参照先のオブジェクトのpropですが、その名前の段をどの型も宣言していなければロード時に弾きます (3.6 節)。同じプロパティでも段は型ごとなので、指した相手がその段を持たないことは在りえます (そのときは常に偽)。単調に増える値と、境目より上を安全域として読む値は
in_stage_or_aboveで書きます。 腕前 (SkillSystem.md4 節)をin_stage1 つで書くと、腕が上がった瞬間に条件が偽へ戻り、 一度開いたレシピが閉じ直します。上限側の演算子はありません——段で読む値はどれも、下側を要求する形しか 現れていないためです(書きたい宣言が現れたら足します、2.1 節。演算子の過不足の論点は 17 節)。上下は段の下端(
min)の大小で決まり、stagesの宣言順ではありません(6.4 節の段の選び方と同じ見方)。 下端を持たない受け皿の段(6.4 節)はいちばん下なので、名指すと常に真です。値の並びの上に位置を持たない シンボル型(6.6 節)の段は上下を比べられないので、in_stage_or_aboveは常に偽になります。
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}}
slot: 判定対象の、subjectが持つスロット名。matches: そのスロットの中身に求める型(4.1 節の型指定)。{not: ...}を書くと「当てはまらない 中身が1つはある」で、この葉ごと 14.5 節のnotで包む「1つも無い」とは別物です。
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. 本書の対象外
- 地形生成(
axes/location_types/generation_scopes): 記法自体は本書と同じ「識別子をキーとする辞書」を 採用し実装済みですが、フィールドの意味・軸空間マッチングのアルゴリズムは地形生成固有の内容であるため、 本書には含めずTerrainGeneration.mdに委ねます。地形生成によって作られたLocation自身の構造(スロット・探索・道の発見と移動)はExplorationSystem.mdを参照してください(そちらは本書のslots・props・interactions・move・durationの応用例です)。 derived(導出値): 「他の props から計算される値」という概念自体、採否がまだ決まっていません(17 節)。- 表示文字列: 画面に出す名前・説明文は WorldCodex に持たせません。
object_defsのキーはあくまで 識別子であり、表示名は言語ごとの対応表から引きます(Localization.md)。
17. 今後の検討課題
derived(導出値)の採否。採用する場合、stages(6.4 節、minによる半開区間)とキー名・記法を統一するか- ゲーム内時間で重なる操作(別々の
agentが同時に動き、所要時間が重なる)を入れるとき、patientから見た 関係が一意でなくなる(2 人が同じ道を歩けば、道から見てagentが 2 人)。そのときは問い合わせる側が関係を 持ち回る形が要る。書かれた YAML は変わらず、解決の仕組みだけが変わるので、重なる操作が入るまで導入しない。 それまでは「同じ物が 2 つの操作のagentにならない」の実行時検査(11.5 節)が、破れた瞬間に止める pickの位置づけ(候補にmodifyを書けないことの扱い)conditionsから域(alert、6.4 節)を読めないこと(14.1 節)。「安全域にある間だけ」は、安全域の 下端の段をin_stage_or_aboveで名指して書く(VitalsSystem.md3.2 節)。段を足しても 条件は直さずに済むが、どこからが安全域かはalertと条件の側の 2 箇所が持つので、境目そのものを動かすと 両方を直す- action / combinations の比較演算子セット(14.1 節)の過不足、
between等の追加要否 worldをsubject(14.1 節・10.2 節)にした参照(world シングルトンインスタンスの実行時追跡が未実装)は 未対応(ロード時エラー)。ancestor(8.6 節・6.5 節)は見つからなければ自然に world まで遡るため、多くの 場合これで代替できるactive(9 節)の対象キーchild: 相手が1つに決まらないため不可(ロード時エラー)。 passives の child 寄与(8 節、付いている子ごとに登録を配れる場所)とは性質が異なるconditions/weightのvalue: max/value: min(参照先プロパティの range の上限・下限を指す想定と思われる 記法)は、規約が本書上どこにも明文化されていないため未対応(ロード時エラー)slotsが character 専有の概念か、コンテナ全般で使い回す汎用概念か- 重ねる操作に関する未決事項一式(キーの衝突解決、パックによる拡張性など)は
ActionSystem.mdに整理 - レシピに関する未決事項一式は
RecipeSystem.mdに整理 - コンテナの容量・重さに関する未決事項一式は
ContainerSystem.mdに整理