アクションシステム設計
概要
プレイヤーがカードに対して行う操作が、実行時にどう実装されているかを記述する設計ドキュメントです。
YAML上の文法そのものは GameElementDefinition.md(interactions は同 11 節・12 節、active は同 9 節、pick は同 10 節、conditions は同 14 節)、操作の画面側の
入口(アクションの行・ドラッグ&ドロップ)は ../ui/Windows.md 4 節・
../ui/CardInteraction.md が扱います。
入口が分かれていても、実行は1本です。 きっかけ(trigger: menu / tick / {drag: ...})が違うだけで、
中身は同じ InteractionDef 1つ。どれも同じ実行パイプライン
(マッチング → conditions → duration の解決 → 時間進行 → 生存確認 → 効果の適用、2 節)を通ります。
起きたことは分岐名ではなく世界に起きた変化として観測します(7 節)。操作専用の新しい文法はありません。
実装は src/domain/InteractionDef.ts(操作の中身)と src/domain/InteractionTrigger.ts(きっかけ)、
src/domain/WorldObject.ts・WorldSession.ts、検証は tests/domain/interaction.test.ts・
actionDuration.test.ts・worldChanges.test.ts です。本書は実装済みの仕組みの記述で、
未決事項は 8 節に整理しています。
1. 入口: menu・tick・drag
操作の入口はきっかけの数だけで、どれも object_def に宣言的に定義される。
- メニュー型(
trigger: menu、MenuTrigger): 1枚のカード(self)だけで完結する操作。 カード選択時にボタンとして表示され、クリックで実行される。agent(プレイヤーキャラクター)は 常に暗黙的に参加する。 - 時間が起こすもの(
trigger: tick、TickTrigger): プレイヤーが押す機会を持たない操作 (動物の1手、HuntingSystem.md5 節)。ボタンには出ず、名前で指して実行される。 - ドラッグ型(
trigger: {drag: ...}、DragTrigger): カードを別のカードへ ドラッグ&ドロップする操作。組み合わせを宣言している側がself、相手がinstrumentで、 きっかけ{drag: ...}(タグかobject_defのidで書く型の指定、GameElementDefinition.md12.1節)がinstrumentとのマッチング条件になる。 宣言は素材の側に1つだけ置き(同 12.3節)、どちらの札をどちらへ運んでも同じ宣言が実行される ——どちらをselfとして試すかの順序は UI層が決める(../ui/CardInteraction.md2 節)。
違うのはきっかけだけなので、差はきっかけの側に集める。 宣言はYAML上も1つの節(interactions)で、
中身(要件・効果・所要時間・実行、2節)は InteractionDef 1クラスが持つ。きっかけが宣言を
ぶら下げる(InteractionTrigger.interaction)ので、差を足すのは派生の DragTrigger だけ
——相手のマッチングと allow_multiple。
ObjectDef はきっかけ別に束ねて持つ(menuTriggers・tickTriggers・dragTriggers)。
ボタンを並べる側も時間の側も自分に関わるものだけを受け取るので、種類を見分ける必要がない。
どの束に入るかはきっかけ自身が答える(addTo)ので、種類を足したら実装しないとコンパイルが通らない。
1.1 宣言と、相手の決まった操作
きっかけと、それがぶら下げている宣言は ObjectDef のもので、どの個体の話かを知らない。実行にも
所要時間の解決にも self(instrument・agent)が要るので、宣言へ直に頼むと呼び出し側がそれらを毎回
渡し直すことになる。そこで WorldObject から引いた時点で相手を結び付け、Action・Combination
(Interaction.ts)として返す。以降は名前も相手も渡さない。
引く口は次のとおり。
menuActionsFor(agent)— このカードへ起こせる、画面のボタンに出る操作を宣言順に (menuTriggers。絞り込みは要らない)。tryGetAction(actionName, agent)— 名指しで1つ。土地のexplore、道のtravel、動物の1手が使う。 探すのは相手を伴わないきっかけ(menu・tick)だけ。combinationsWith(instrument, agent)— ドラッグ中のハイライト等のために、今成立する重ねる操作を 宣言順に列挙する。相手のマッチング(1)だけでなくconditions(2)まで見る——実行できないものを 黙って落とし先にすると、落とせるのに何も起きないという形になるため。refusedCombinationsWith(instrument, agent)— 逆に、conditionsで成立せず、断る理由 (reason、GameElementDefinition.md14.6節)を宣言しているものだけを列挙する。画面がこちらを 理由を言うためだけの落とし先として出すのは、成立するものが1つも無いときだけ (../ui/CardInteraction.md2.1 節)。上の「黙って」がここに掛かる——理由が出るなら、実行できない落とし先を出してよい。
どちらの列挙も、相手のマッチングのほかに行き先の型(blocksOperation。become の行き先に型が
居ない、GameElementDefinition.md 9.9節)で先に絞る。ここで落ちたものは
reason を宣言していてもどちらにも入らない=理由が出ない。「なぜ理由が出ないのか」を追うときは、
まずここを見る。
容量(acceptedCountIncludingSelf が0の相手)はこの門ではなく、combinationsWith の側の条件
(Combination.canExecute)。0は「何個受け取れるか」の答え=断る理由であって、候補になるかどうかの
判定ではない。両方の門にすると、満杯を conditions にも書いた宣言(満杯の罠の trap_baited)は条件と
容量が同時に落ちるので、宣言した理由が決して届かなくなる。
どちらの札を self として引くか(両向きとも引いたうえで、成立するほうを先に採る。どちらの向きも
成立しないなら落とされた側)と、複数マッチした場合にどれを実行するかの解決はUI層に委ねる
(../ui/CardInteraction.md 2 節、cardOperations.combinationBetween)。
まとめて重ねる操作(allow_multiple、GameElementDefinition.md 12.4節)も
Combination が持つ。
acceptedCountIncludingSelf(followers) が落とす前に、掴んだ1枚を含めて何枚落ちるかを答え、
executeWithFollowers(followers) がその繰り返しを行う——1つ実行するたびに世界が変わるので、
都度まだ成立するかを見直し、成立しなくなった時点で止める。
1.2 ドラッグ型をメニュー型へ畳まない
相手が要る操作をドラッグ型で持つのは、相手の個体が要るから。 運ばれてきた札がそのまま
instrument になるので、宣言はその1つを名指して見られるし、減らせる。
メニュー型には instrument が居ない(役を書ける場所は
GameElementDefinition.md 11.5節)。代わりに書けるのは
「その型の物を持っているか」の存在判定(同 14.3節のスロット中身判定)までで、どれを使ったかは
答えない。 だから道具を見る要件も道具へ及ぶ効果も、書き換えた先に置き場が無い。
置き場が無いものを落とすと、操作そのものが変わる。 刃の余力を見る要件が落ちれば 尽きた斧でも木に刃を入れられ、刃を減らす効果が落ちれば倒しても斧が減らない。相手に触れない宣言 (草を刈る)だけは成果物まで同じものが書けるが、そこでも相手の居場所が要件になる——ドラッグ型は 運ばれてきた札を見るので居場所を問わないのに対し、枠を名指す条件は地面に置いた刃物へ届かない。
この節が言う差は tests/world-codex/dragAsMenu.test.ts が留める——実データの宣言を引いた側と、
それをメニュー型へ写した側を並べて測っている。
1.3 掴んだ瞬間の走査は、貯め込まなくても安い
札を掴むと、受け入れられる札のふちを光らせるために、並んでいる札のどれとも組み合わせを問います
(CardDragController.showAcceptingCards → ShownCards.acceptingCells)。問うのは両向きで、成立する
もの(combinationsWith)と断るもの(refusedCombinationsWith、1.1節)の両方です。
それでも答えを貯め込みません。大半の札は conditions まで届かないからです——相手として
受け入れる宣言を持たない札は、型のタグを照らし合わせたところで候補が空になり、要件(3節)は
一度も評価されません。評価が走るのは、掴んだ物を相手として名指している宣言を持つ札だけです。
現在地に150枚の札を並べ、いちばん多くの札と噛み合う物(この時点では salt。150枚のうち48枚が
組み合わせを返す=要件まで届いた札の下限)を掴んで全部を両向きに問うのに 0.51ミリ秒でした
(2026-09-18 時点、
Intel Xeon @ 2.10GHz・Node v22 で計測)。
並ぶ枚数に頭打ちはありません——束ねない型(stackable: false、SlotSystem.md
4節)は個体ごとに1枚の札になるので、同じ型を並べればいくらでも増えます。なので「この枚数で収まった」
だけでは足りず、周りの枚数を倍にしても1枚あたりの判定が変わらないことも併せて見ています。
ここまでは問われた側の値段です。それを回す画面側にも上限を引いています。 落とし先を1つ
問うたびに、その場所に並ぶ札は丸ごと作り直される(PlayScreenView.cardsIn)ので、画面が枠を1つ
ずつ問いに行くと、判定がいくら安くても枚数の2乗で伸びます。 だから画面はどの枠が受け入れるかを
一度にまとめて訊き(ShownCards.acceptingCells)、並びを場所ごとに1度だけ作ります。同じ150枚を掴んだときの
走査1回が 2.2ミリ秒でした(2026-09-20 時点、Intel Xeon @ 2.80GHz・Node v22 で計測)。
掴んだ指が待つのはこの時間なので、ここだけは上限を1フレーム(16ミリ秒)で引いています。
どれも tests/diagnostics/interactionCost.test.ts が見ます——同じ並びを組んで測り、桁の変わった
遅さでも、枚数に連れて伸びる形でも赤くなります。
2. 実行パイプライン
実行は次の順に進み、途中で失敗すると false を返して世界の形は何も変えずに終える。順序に意味が
あるため、実装は InteractionDef に1つだけ置く(相手のマッチングだけは Combination が先に見る)。
withマッチング(combinations のみ):instrumentの型がwithの指定に当てはまるか (TypeMatchRule。タグならそのタグを持つか、object_defならその型そのものか)。conditions評価(3節): 省略時は常に真。announce(GameElementDefinition.md11.6 節): この操作が始まった ことを告げる。世界の形を変えないので、後の段が落ちても取り消さない——6 が成立しなかったと 判定した操作も、始まったこと自体は起きている。durationの解決: 「今のself(combinations ではinstrumentも)の状態から見て、どれだけかかるか」 なので、時間を進める前に分数だけ確定させる(切れ味の悪い刃物ほど時間がかかる、が書けるように)。- 時間進行(6節): 効果の適用より先に進める。行動してから結果が出る順序であり、作ったもの・
見つけたものが自分の制作時間・探索時間ぶんの tick を浴びずに済む。この間だけ、その操作が宣言した
持続効果(
passives、GameElementDefinition.md11.7 節)を登録する。 - 関与オブジェクトの生存確認(6節): 経過中に失われていたら、その行動は成立しなかったものとして
falseを返し、効果を適用せずに終える。 - 効果の適用:
WorldObject.applyActiveEffect(effect, context)(5節)。self も役もReferenceContext(4節)が1つにまとめて持つ。 - 待たされていた手番(
trigger: tickでdurationを持つもの、GameElementDefinition.md11.5 節)を起こす。ここが操作の切れ目で、 5 の経過中に配られた手番はその場では起きずにここまで待つ(WorldSession.runToSeam)。 時間を進める操作は他にもある(製作の 1 工程・枠へ入れる)が、どれも実行として関係を張るので、 切れ目を名乗るのは関係を張る側(InteractionRelation.whileActing)だけ——操作の外で時間だけが 動いた場合は、その進行そのものが切れ目になる。
3. 実行可能条件(conditions)
ConditionNode の木。葉と、複合の all / any / not からなり、条件の並びを書ける場所は
どこも同じ木を共用する(一度きりの判定も、passives の持続的なゲート(GameElementDefinition.md 8.2節)も)。書ける葉と演算子キーは
GameElementDefinition.md 14節の表が唯一の一覧で、ここには写さない。
参照先が解決できない場合(親が無い等)、その葉は偽になる。
4. 条件・効果から参照できるオブジェクト
conditions の subject、効果の対象キー、{subject, prop} 参照はすべて共通の起点
ReferenceRoot を使う。self.prop のような1階層の参照のみで、パス連結はない。
| 起点 | 解決先 | 解決先を持つ場所 |
|---|---|---|
self |
宣言元の個体自身 | 宣言元の個体が居る場所(rangeイベント・passives も含む) |
parent |
self の直接の親 | 同上 |
agent |
その操作で動いている個体 | 操作の関係の役(同 11.5節)。書ける場所を持つのは ReferenceScope(下の段落) |
instrument |
運ばれてきて働きかけに使われる参加者。宣言が乗っていない側 | 同上 |
patient |
働きかけられる参加者。操作の宣言が乗っている側 | 同上(操作の宣言の中では self と同じ物になるので書けない、同 11.5節) |
picked |
among(同 10.3節)が周りから選んだ相手 |
among を書いた候補の中(重みと効果)だけ |
ancestor |
self の親から遡り、参照プロパティを定義する最初の祖先 | その場所が、指す先のプロパティ名を決めているときだけ——祖先はその名前で探すので、プロパティ名が関わらない場所(オブジェクトそのものを指す場所)では相手が決まらない |
どの起点が書けるかは、場所ごとに数え上げるのではなく、宣言が置かれた場所が何を持つかから導く
(ReferenceScope)。ロード時に弾く根拠と、実行時に組む ReferenceContext は同じ1つの事実なので、
書けたのに実行時は必ず空振りする、という宣言が作れない。同 11.5節「役を書ける場所」の表の各行は、
ReferenceScope の static(と withInstrument のような派生)のどれかへ対応する——行と static は
1対1ではない。 drag と put_in は同じ acting.withInstrument を、on_max/on_min と resists
(同 7.13節)は同じ declaration を共有する。表に無い場所が現れたら、持つものが同じ static を使う。
world は起点として未対応(ロード時エラー)。すべてのオブジェクトは world の下にぶら下がるため、
world 固有プロパティの参照は ancestor で代替できる。起点としての child は passives の target 専用で、
この文脈では使えない(5節の into が名乗る child は行き先の名前で、起点ではない)。
解決できない対象への適用は、その命令だけ無視される(実行全体は失敗しない)。
5. 効果(ActiveEffect)
効果はポリモーフィックな ActiveEffect で、自身を再帰的に組み合わせる1つの型。何を並べて書けるかは
GameElementDefinition.md 9.7節が持つ。
経過の間ずっと効くものは、ここには入らない。 操作が持つ passives(同 11.7節)は
InteractionDef が効果と別に持つ——効くのが経過の各tickで、効果が効くのは経過し終えてからなので、
1つにまとめると起きる時点を言えなくなる(announce と同じ理由)。
- 宣言順合成(
ActiveEffectSequence): 並べた命令をYAMLに書かれた順で適用する (effectsInDeclarationOrder)。動詞ごとの優先順位は無く、順序に意味がある組み合わせは著者が その順に書く(同 9.7節)。 pick(PickEffect、同 10節):weight(リテラルかプロパティ参照)による重み付き抽選で 1候補を選んで適用する。候補の効果もActiveEffectなので、pick のネストができる。ConditionalEffect: rangeイベントのconditions(同 6.3節)を満たす回だけ中身を適用し、 満たさない回は既定のクランプへ倒れる。
設計上の要点:
set/addの値に{subject, prop}参照は書けない(同 9.2節)。値の算出をYAMLへ持ち込まないためで、 在庫に応じて動く量はtransfer(同 9.5節)が担う。setだけは値の位置に対象キーを書いて個体を 指せる——実行時に決まった相手を覚える唯一の手段(同 9.2節)。spawnの配置先(into、同 9.4節)は、個体を指す形がmoveの移動先とまったく同じで、書ける起点は 4節の表のとおり。個体でないものを名乗れるのはintoだけで、same_slot(既定)とchild。same_slotは、適用の入口で捕捉した 「self が占めていた位置」のスナップショット(SameSlotSpawnSite)を使い、destroy で self が 消えた後でもその位置を引き継げる。配置に失敗した場合は起点の親、さらにその親…とこぼれ落ち、 どこにも入らなければ世界から消える(同 9.4 節)。どの段でも枠の宣言はそのまま効く。transfer(同 9.5節)は「出せる量」と(allow_overflow: falseなら)「受け取れる量」で実移動量を決め、linked_addを実移動量に比例スケールして適用する。moveは、動かす物(subject/subject_prop)と行き先(to/to_prop/to_object)を別々に指し、 行き先の指し方はspawnのintoと共通。行き先が定義時点で決まらず生成時に確定する場合 (道の移動アクション)は、object_def参照ではなくselfのプロパティが保持する インスタンスID(to_prop)で指す。- プロパティの rangeイベント(
on_max/on_min、同 6.3節)も同じActiveEffectと適用経路(WorldObject.applyActiveEffect)を使う。書ける動詞に差は無く、pickもmoveも並べて書ける(海区のstorm_driftのon_maxが、amongで選んだ筏をmoveで 隣の海区へ流す。voyage.yaml)。ここは操作ではなく、値が端に着いた瞬間への反応なので、rangeイベントで あることを理由に落ちる起点は操作の関係の 3 役(agent・instrument・patient、GameElementDefinition.md11.5節)だけ。ほかの起点の可否は 4節の表のとおりで、rangeイベントだからという上乗せの制限は無い。
6. 時間の経過(duration)
-
interactionsのdurationはゲーム内の分。リテラルか{subject, prop}参照 (weightと同じ二択)で、省略時は時間を消費しない。subjectにどの役を書けるかは、4節の表と同じく その宣言が置かれた場所に何が居るかで決まる(ReferenceScope)。きっかけによって連れてくるものが 違うので、同じinteractionsでも書ける役は一様ではない。枠へ入れるのにかかる時間(put_inのduration、GameElementDefinition.md7.10節)も同じ形の宣言で、そこでも入れる物をinstrumentとして指せる (SlotDef.putInMinutes)。 -
時間進行は
InteractionDef自身がWorldSession.advanceWorldTime(amount)を呼んで完結させる。 呼び出し側(UI層)が実行後に別途時間を進める必要はない。解決した分数は実行前にも引ける (minutesFor。UI層が実行前に所要時間を見せるため、CardInteraction.md2 節)。 -
advanceWorldTimeは分を進めながら、tick 境界(world のminutes_per_tickプロパティ、 現状15分)を跨ぐたびに world ツリー全体のtick()を1回実行する。長いdurationの action は、 その間のadd・rangeイベントをすべて経験する。跨がなければ、進むのは時計だけ——率を分へ割る ことはしない(GameElementDefinition.md8.4.2 節)。 -
経過している間だけ、その操作の
passives(GameElementDefinition.md11.7節)を登録する。 対象の値がその tick に 受け取るほかの増減と同じ足し算に入るので、値が下限に居ても正味で釣り合う——経過し終えてから足すと、 下限に張り付いた値では経過中の減りを既定のクランプが吸ってしまう (../world/Characters.md限界節)。 -
Worldを持たないセッション(時間の概念が無い単体テスト等)では時間進行をスキップする。
6.1 経過中に関与オブジェクトが失われた場合
時間を効果より先に進めるため、経過中の tick が self/instrument/agent を破棄しうる(使っていた道具が
行動の途中で壊れる)。破棄は「親スロットから切り離す」ことなので(GameElementDefinition.md 9.3節)
そのまま効果を適用しても例外にはならないが、same_slot の spawn が置き場所を失うなどして黙って
何も起きない結果になる。追跡できない失敗になるため、関与オブジェクトが1つでも世界から失われて
いたら、その行動は成立しなかったものとして false を返し、効果を適用しない
(spendDurationAndReportParticipantsAlive)。
- 見るのは「経過前に世界に居たのに、経過後は居ない」ものだけ。もともと世界の木に繋がっていない オブジェクトは、失われたわけではないので対象にしない。
- 時間は既に経過している。 1時間かけて道具が壊れ、何も得られなかった、という結果になる。
- 打ち切るのは失われた場合だけで、
conditionsの再判定は行わない。長い行動ほど「なぜか失敗する」が 増えて予測できなくなるため。
6.2 行動の長さは tick の格子に乗る【確定】
世界のどの行動も、所要時間は 0分・5分・15分・15の倍数のいずれかでなければならない。どの段の キャラクタが行っても外れない——腕が縮める分も、荷が伸ばす分も、刻みの倍数で宣言する。
tick が回るのは絶対時刻が15分の倍数になる瞬間なので、格子から外れた長さの行動は、いつ押したかで
跨ぐ tick の数が変わる。tick は動物の1手(HuntingSystem.md 2 節)であり、腐敗・
火の衰え・空腹が動く瞬間でもあるので、これは端数の話ではなく同じ行動が獣に手番を与えたり与えなかったり
するという性質の違いになる。腕が上がるほど世界の進行を飛ばせる回が出る、という向きの誤りも同じ根から出る。
- 0分は「1 tick に何度でもできる」を意味する。 繰り返すと得をする操作を0分にすると、無限に 繰り返せてしまうので、0分にしてよいのは、繰り返しても2回目以降が何も生まないか、受け手の側が 上限で止める操作だけ——渡す物を消す1手の受け渡しがこれに当たる。これは素の分数を決める側の 判断で、腕でそこへ落ちる道は別に塞ぐ——縮めきっても0分にならないことを見張る(腕を上げた者だけが 無限に繰り返せる形は、この線の内側には無い)。
- 5分は、1 tick を渡すには短すぎる手のためにだけ在る。そこへ刻みを足すと 格子から外れる(5 + 15 = 20)ので、5分の所要時間は何にも動かさない。 5分の行動だけは、跨ぐ tick が0回か1回かで揺れたままになる——15の倍数の行動は、開始時刻がどこでも 必ず「長さ ÷ 15」回だけ跨ぐが、5分にはそれが無い。だから5分にしてよいのは、手番を争わない手元の 受け渡しだけ——獣に手番を渡すか渡さないかが押した時刻で変わっても、それが得にも損にもならない操作。 これは0分と同じく素の分数を決める側の判断で、宣言から引けるのは次の項だけ。
- プレイヤー自身を動かす手は、5分にも0分にもしない。 場所を移るのは獣から逃げる手でもあるので、 跨ぐ tick が揺れると同じ逃げ方が通るか通らないかが押す前に読めない。0分にもしないのは、移るのに 何も要らなければ獣から離れる手が常に無料で通るから——上の「繰り返しても2回目以降が何も生まない」 には当たるが、繰り返しの向きを変えれば得が出る。刻みの倍数にして、必ず1手を渡す。
- 縮める宣言は、縮めた先が15分を割ってはならない。 レシピの
deftness(GameElementDefinition.md13.5 節)はロード時に弾く——黙って下限で 止めると、格子から外れた宣言が世界に残る。 - 見張るのは
tests/world-codex/actionMinutesGrid.test.ts。今ずれている箇所の数え上げではなく、 世界じゅうの行動と、所要時間を動かしうる宣言のすべてを定義から引いて見る。場所を移る手かどうかも 定義から引く——moveがagentを動かす宣言(同 9.6 節)がそれで、名前では見分けない。 手番を争うかどうかは引けないので、5分の側で機械が言えるのは「場所を移る手がそこに居ない」まで。 ただし「縮めきっても0分にならない」だけは、同じ名前を複数の型が名乗っているとき、その宣言の 中しか見ない——どの型の素へ積む分なのかが名前からは決まらないため。
出どころ: #2185(行動の所要時間を15分の格子へ戻し、腕の効き方を行動ごとの宣言にする)と #2245(5分は手番を争わない受け渡しだけに残し、場所を移る手はそこから外す。2026-09-18に回答)
6.3 1回の行動は1時間を超えない【確定】
1回の操作でも、1つのレシピの工程でも、枠へ入れる1回でも、1時間を超えて時間を進めてはならない。 超える仕事は、途中でプレイヤーが割り込める切れ目へ分ける。
押している間、プレイヤーには何もできない。半日ぶんの時間が1回のクリックで過ぎる行動は、盤面をその
ぶん取り上げる(DesignPrinciples.md「プレイヤーから考える時間を
奪わない」「時間経過に任せる区間を作らない」)。tick はそのまま獣の手番でもある
(HuntingSystem.md 2 節)ので、長い行動は獣に手を渡したまま戻ってこない
時間でもある。
分け方には形がある。
- 回数を数えるのではなく、対象の側に状態を持たせる。 木は「あと何回叩いたか」ではなく、まだ断ち 切れていない幹がどれだけ残っているかを持ち、斧を入れるたびに減って、削り切ったところで倒れる。
- 同じ作業の単純な繰り返しにしない。 分けた各回が違うものを返すか、対象の状態が進むかのどちらか。 死体を捌く手は、回ごとに出る物が違う。
出どころ: #2182(1時間を超える行動を、途中でプレイヤーが割り込める切れ目へ分ける)
6.4 分けた手の書き方
見張るのは tests/world-codex/actionLength.test.ts。今長い箇所の数え上げではなく、世界じゅうの
操作・レシピの工程・枠と、線の外に居てよい相手のすべてを定義から引いて見る。
- 進み具合は対象のプロパティが持ち、札の桟のバーに出す(
gauge、../ui/CardView.md8節)。木は幹の残り(trunk_integrity)、死体は捌けたところまで(butchering_progress)。 見えないと、途中で止めて戻ってくることができない。 - 後ろの手は、自分の番が来る状態を条件で名乗る(
GameElementDefinition.md12.1 節「条件で分ける」)。同じ道具を重ねても、今の状態に当たる手だけが成立する。その条件にreasonは書かない——断っているのではなく、そこから先は別の手が受け持つので、画面に出るのは その手のほう。 - レシピは工程を割ればそのまま切れ目になる。 進捗と材料スロットの仕組みが既にあり、工程の区切りで
余った材料がこぼれる(
crafting.spillUnneeded)。材料の要求は、その工程で実際に使うものへ割り 当てる——足りなければ何も要求しない工程でよい(GameElementDefinition.md13.1 節)。要求を無理に散らすと、その工程で実際に使う物とずれる。 - 1時間をわずかに超えるだけの手は、割らずに縮めてよい。 割った先が「同じ手をもう一度」に
なるだけなら、切れ目を増やしてもプレイヤーに選べるものが増えない——ヤシの葉を編むのは60分へ
詰めてある(
weaving.yaml)。割るのは、分けた回ごとに違うものが返るか、対象の状態が進むとき。 - レシピの工程を割ると、腕が縮める分(
deftness、同 13.5節)も工程の数だけ積まれる。 同じ仕事を 同じ合計時間で持っても、腕の届いた作り手にとっては速くなる。縮む幅を変えたくないなら、deftnessの分数を工程の数で割る——今はどのレシピも割っていない。 - 元手も、手前の手の時間も、分けた後の1手には現れない(
analysis/craftingSteps.ts)。収支を 定義から出す側は、後ろの手にそこまでの手間を足し、同じ物を使い切る手はどれも元手1つぶんを 食べたものとして数える——足さないと、丸太2本が1時間で採れることになる。
6.5 経過そのものが結果になる操作は、線の外
休む・眠る(characters/player_character.yaml・bedding.yaml)と、渡る・歩く(voyage.yaml の
cross・locations.yaml の travel)は、1時間を超えてよい。
- 休息の切れ目は、札の選択のほうに在る。 長さの違う休息が並んでいて、どれだけ時間を渡すかは
押す前にプレイヤーが決める(
Characters.md休息節)。渡るものは経過の 各tickに積まれる(passives、GameElementDefinition.md11.7 節)ので、 1つの札を割っても同じ札を続けて押す繰り返しが増えるだけになる。 - 渡り切った先そのものが結果で、途中の地点は世界に無い。 海の上にも道の途中にも立てる場所が無く、
横断時間は漕ぎ出すときの風で決まる(
GameEndings.md12.3 節)。 - どちらも受け取るものを経過の終わりに渡さない——物が生まれず(
spawn)、値も動かない (add・set・transfer・become)。見張りが線の外を見分けるのもこの形で、宣言の側から引く。
7. 起きたことの観測
効果を適用した結果として世界に何が起きたかを、外から観測できます(WorldSession.observeChanges)。
UI が演出のために「誰が何をしたか」を要る(HuntingSystem.md 6 節)ためのもので、
observeTicks と同じく観測の解除も観測口が受け持ちます(呼び出し側に外し忘れの余地を残さない)。
7.1 観測するのは物の出入りだけ
| 変化 | 観測の仕方 | なぜ |
|---|---|---|
| 物の出入り(生まれた・移った・世界から出た) | 1件ずつのログ(WorldChange) |
前後の比較では、移動元と「誰の仕業か」が失われる |
| 値の増減 | 前後の比較(statusChangesBetween) |
実効値は誰も書かないまま動くので、書き込みを記録しても現れない |
| 形の変わらない出来事(空振り) | 起こした側が告げる(7.4 節) | 世界を読み直しても現れない |
値をログに寄せられないのは実効値の定義そのものが理由です。 画面に出るのは modify・base を
加味した値(GameElementDefinition.md 8.3 節)で、包帯を当てると痛みは
下がりますが、痛みの実体値は 0 のままで、押し上げていた寄与が減るだけです。
逆に出入りの側は前後の比較では足りません。同じ tick に 2 匹が暴れれば「どちらが壊したか」は差分から 決められず、持ち去られた物は画面から消えるだけで壊された物と同じ形に見えます。
7.2 記録するのは配置の関門
出入りを記録するのは配置を行う関門そのもの(attachToSlotOrRejection・destroy)です。呼び出し側が「動かしたら
記録する」手順を覚える形は採りません——覚える側が増えるほど、記録し忘れた経路ができます。
移動前の居場所は切り離す前に控えます。切り離した後では、どこから来たのかを誰も知りません。
7.3 主体は applyActiveEffect が決める
1 件の変化には、それを起こした効果を宣言していたオブジェクト(self)が主体として付きます。境界は
効果適用の入口(5 節・applyActiveEffect)で、その中で起きた出入りはすべて同じ主体になります。
pickのどの候補が選ばれたかによらず 1 つに決まります。 観測する側は分岐を知らずに「このオブジェクトが 何をしたか」を読めるので、候補を足しても観測する側は変わりません- 入れ子は内側が勝ちます。効果の適用中に別のオブジェクトの range イベントが走れば、そこで起きた変化は そのオブジェクトのものになります(治りきった怪我が自分を消すのは、殴った側の仕業ではありません)
- プレイヤーの操作が直にワールドを動かした場合(カードのドラッグ、シナリオの開始状態)は主体を持ちません。 世界の側に主体が居ないという意味で、そのときは UI 自身が起点だと知っています
7.4 形の変わらない出来事は、起こした側が告げる
世界の形が変わらない出来事は、告げると宣言された分だけが観測できます
(WorldSession.observeSignals)。空振り・回避は「何も起きなかった」のではなく「外したことが起きた」ので、
世界を読み直しても現れない以上、起こした側が告げる以外に伝える道がありません。
どの告げ方でも、観測口は1つです。 告げると宣言された出来事は、告げ方(書き方は
GameElementDefinition.md 9.8 節)によらず同じ口へ同じ形(WorldSignal)で
流れます——受け取る側にとっては、どれも「起きたこと」1 件でしかありません。
告げる語彙を持っても、7.1 節の分担は変わりません。 観測できる出来事はワールドの著者が書いた分
だけで、エンジンが pick の分岐名を横流しするわけではありません——候補を足しても、その候補が
何も告げなければ観測する側には何も増えません。
出入り(WorldChange)とは別の観測口にします。片方は世界の形が変わったこと、もう片方は変わらないままの
出来事で、同じログに混ぜると受け取る側が毎回どちらかを選り分けることになります。
誰の身に起きたかは、主体(7.3 節)ではなく効果が指した対象です(WorldSignal.object)。出入りは
「動いた物」と「動かした者」を要しますが、告げられた出来事はどこから見ても 1 つの札の上のこと
でしかありません——殴って外した出来事は、殴った側ではなく殴られた側に起きています。
8. 未決事項・今後の検討課題
今は残っていない。