コンテナの容量・重さ・保護

概要

物を入れて運ぶ・しまう入れ物(かご・箱・そり)を、どう成立させるかのドキュメントです。実際に ゲームへ登場させる入れ物の一覧と比較は ../world/Containers.md、 液体の容器は構造が別で LiquidContainerSystem.md が扱います。

入れ物は専用の仕組みを持たず、スロット(GameElementDefinition.md 7 節)そのもので表します。 item_container trait が container タグと中身のスロット contents を 与え、そのスロットを visible_slots(同 7.11 節)に並べることで、カードを押すと中身が並び、 カードへ重ねた物はそこへ入ります(投入用の combination は要りません)。入れ物ごとに決めるのは、 何種類入るか(cell_count)と、どれだけのかさが入るか(capacity)だけです。液体の容器は 「中身が容器の変種になり、量は容器自身が fill として持つ」別の構造なので、trait もタグも分けています (スロットを持つのは固形物側だけです)。定義は src/assets/world-codex/containers.yaml、検証は tests/world-codex/containersYaml.test.ts です。

この構造の上で本書が扱うのは数値をどう決めるかです——物の重さがどう積み上がり、それを担ぐ人が どれだけの負荷を感じるか(1〜4 節)、負荷が移動へどう効くか(5 節)、入れ物が中身を環境から守ることを どう表すか(6 節)、物のかさと入れ物の容量をどう見積もるか(7 節)。capacity そのものは、宣言が GameElementDefinition.md 7.3 節、受け入れ判定での扱いが SlotSystem.md 2 節にあるだけの単純な仕組みです。

本ドキュメントは検討結果であり、確定仕様書ではありません。未決事項は 8 節に整理しています。

1. weight は率をかけない純粋な合算

オブジェクトの weight は、自分自身の重さに、中身の weight をそのまま足したものです。

weight の実効値 = weight.value + 通常の modify + Σ( 子の weight実効値 ) + fill × density

後ろの 2 項はエンジンの特別扱いではなく、modify(GameElementDefinition.md 8.3 節)そのものです。 重さを持つ型には「自分の weight ぶん、親の weight を押し上げる」という宣言を、 量を抱える型には「fill × density ぶん、自分の weight を押し上げる」という宣言を、エンジンが生やします (containerPropagation)。寄与の登録・解除・実効値への合算・影響の一覧(Windows.md 8 節)は、著者が書いた modify とまったく同じ経路を通ります。

書いているのは与える側——押し上げられる側が中身を数えて回るのではなく、押し上げる当人が名乗ります。 それでも著者に書かせないのは、この宣言に自由度が 1 つも無いからです。重さを持つ物が、それを担いでいる 物を重くしない世界はありません。書き忘れも書き換えも起こらないものは宣言ではなく法則なので、エンジンが 代筆します(書かせれば、weight は宣言したが伝播を書き忘れた物、という静かな事故が生まれます)。

量を抱える型(fill)は weight を宣言していなければなりません(ロード時エラー)。fill × density を載せる先が無いと、抱えている量の重さがどこにも現れないまま消えるためです。量そのものが重さで器の 自重を持たない物は weight: {value: 0} と書きます。

ここに体感の率は一切かかりません。 籠をそりへ積めば、そりの重さは籠の重さをそのまま加えたものになります。 そりが荷物を軽くするわけではないからです。軽減は「担いだ人がどう感じるか」の話であり、物の重さの話ではないので、 接触点である 2 節にだけ現れます。

weight は物理量として素直に上へ伝わります。この一貫性のおかげで、キャラクタを筏に乗せれば、 自重と荷物を合わせた重さがそのまま積載として効きます(Voyage.md)。

中身入りの容器は、抱えている量ぶんだけ自分が重くなります——fill × density を自分の重さへ足します (density は単位量あたりの重さ = g/mL、LiquidContainerSystem.md 8 節)。換算定数は要りません——mL × g/mL = g が そのまま成立するよう、単位を噛み合わせて選んでいます。

weight の単位は g です(1 = 1g、1kg = 1000)。 液体の量の単位(1 = 1mL)と対になっていて別々には 選べません(LiquidContainerSystem.md 5 節)。

1.1 実体のある物は、目方を名乗る

積み上がる先は直接の親なので、weight を名乗らない物が間に挟まると、そこから上へは何も伝わらない まま消えます。筏に据えた炉が目方を名乗らなければ、炉に詰めた土器は筏を重くしません。乗員が名乗らな ければ、荷物を手に持って乗るだけで積載が 0 になります。

そこで、この世界に実体として在る物は目方を名乗るという約束を required_props (GameElementDefinition.md 4.2 節、core.yaml)に置いています。 「持ち運べるか」ではありません——fixture も not(item) も「動かせない物」の印にはならず(編み籠は両方を名乗り、椅子もそうなります)、動くかどうかと 目方があるかどうかは別の話だからです。

地形そのもの(道・洞窟の入口・湧き水)は weight: {value: 0} と書きます。0 は宣言漏れではなく、 そう決めたという印です。名乗らないでいられるのは土地と world だけ——どちらも誰かの中へ入って 担がれることがなく、受け取る相手が居ません。

2. 体感の率はアイテムが持ち、load の計算にだけ効く

キャラクターだけが load(負荷)を持ちます。 これは「今の担ぎ方で、どれだけの重さを感じているか」です。

load の実効値 = load.value + 通常の modify + Σ( 直接の子の weight実効値 × 子の load_rate実効値 )

こちらも 1 節と同じ、エンジンが子の側に生やす modify です。違うのは量が積の 2 項になることだけで、 load_rate を宣言していない物は素の重さがそのまま効きます(因子に並ばないので、既定値という規約が 要りません)。

load_rate(担いだときに感じる割合)はアイテム側のプロパティで、スロット位置に応じて passives が 動かします。GameElementDefinition.md 8.5 節の「装備するスロットによってボーナス量を変える剣」と同じ形です。

object_defs:
  sledge:
    props:
      # 8kg
      weight: {value: 8000}
      load_rate:
        value: 1
        passives:
          - conditions: [{in_slot: hand}]
            # 引きずるので、割り引いてしか感じない
            modify: {self: {load_rate: -0.45}}

率をスロットではなくアイテムが持つのは、同じ入れ物でも担ぎ方で体感が変わるからです。背負い袋は背負えば 楽ですが、手に提げれば重い。中身も自重も同じなのに違うので、率はアイテムに固有の値ではありません。かといって スロット側に一律の率を置くと、同じ equipment に入る鎧と背負い袋と肩掛け袋を区別できません。

「率はアイテムが持ち、どのスロットにいるかで切り替える」とすれば、両方が同時に成立します。部位のような 中間概念も要りません。

load を読むのはキャラクター自身だけなので、他のオブジェクトは load を定義しません(未定義のプロパティの 実効値は 0)。

率の下限をエンジンは見ません。 負の率(担ぐと荷が減る)を書けば書いたとおりに効きますが、これは負の modify を書けるのと同じことで、load そのものが range の下限で 0 に止まります——端を持つのは受け取る 側という、他のプロパティと同じ決まりです。

率は自分の自重にもかかります。 そりのような「自重が重い代わりに引きやすい」道具を、軽い荷では不利に したい場合、率を高くするほどその狙いから遠ざかります。率が高いほど自重の枷が打ち消されるためです。 素朴な入れ物(率 0)と引く道具のどちらが軽く感じるかが逆転する積載量は、次の関係で決まります。

1 − load_rate = (引く道具の自重 − 素朴な入れ物の重さ) ÷ (引く道具の自重 + 逆転させたい積載量)

割り引きを深くしすぎると、逆転点は 0 まで下がります——荷が空のときに 編み籠(800g)と並び、少しでも積めばそりが勝つので、籠を持つ理由が無くなります。今のそりの load_rate は、 逆転点を 8kg に置いてこの式から逆算したものです(tests/world-codex/loadEffects.test.ts が線を見ます)。逆転させたい積載量を先に決めて率を逆算するのが、 数値の決め方として素直です。実際に置いた線は ../world/Containers.md 2節に あります。

3. 例: 石を載せたそりを引く

対象 weight.value 子から weight実効値 load_rate load
石 1000(1kg) 0 1000 — —
そり 8000(8kg) 石の実効値 自重 + 石 1 に -0.45 を足した値(handにいる) —
キャラクター 65000(自重65kg) そりの実効値 自重 + そり — そりの実効値 × そりの率

キャラクターの weight は自重込みで正直なままです。負荷の load は荷だけ(自重を含まない)なので、段階の 閾値を「荷物をどれだけ持ったら」とそのまま読めます。

編み籠をそりへ積んだ場合、籠は hand にいないので率のかかりようがなく、そりの weight には籠と中身が 丸ごと加算されます。効くのは引いているそりの率だけです。率を持つ物どうしでも同じで、hand にいない 側の率は使われません——そり(315L)が台車(容量200L)に入らないのはかさの話であって、率とは別です (7節)。

4. 実効値方式であることが必須

伝播は「スロットへの出入りの瞬間に 1 回だけ加減算する」のではなく、読むたびに導出する実効値として扱います。

load は率がスロット位置に依存するため、出入りの瞬間の加減算では帳尻が合いません。そりを hand へ入れる 瞬間はまだ hand にいないので率は 1、出す瞬間は割り引かれた率——差分が残ります。

液体はさらに直接的です。注ぎ移し(transfer)と蒸発(add)で量が絶えず変わるため、出入りの 瞬間しか見ない方式では、水を注いでも蒸発しても容器の重さが更新されません。

実効値であれば、宣言が値を参照するたびに getEffectiveValue で導出されるため、 キャラクターの load に stages を持たせるだけで参照口が揃います。新しい参照の仕組みは要りません。

# 自重(characters/player_character.yaml)
weight: {value: 65000}
# 荷の段の境目は人物ごと(characters/engineer.yaml)
load:
  tags: [status, health]
  value: 0
  range:
    min: 0
    max: 27000
  stages:   # 段ごとの寄与(歩みの遅れ・体力の削り)は省いている
    - {name: light}
    - {name: laden, alert: watch, min: 6750}
    - {name: heavy, alert: caution, min: 13500}
    - name: too_heavy
      min: 22500
      alert: danger

移動可否は道の travel アクションの conditions が、load の段の名前で直接書けます(5 節)。

4.1 毎回導出しても、貯め込みは要らない

読むたびに導出するので、weight を 1 回読むとその物の部分木を丸ごと辿ります(load に並ぶ寄与は 直接の子のぶんだけですが、その 1 件ずつが子の weight を読むので、辿る範囲は同じです)。それでも 控え(キャッシュ)を置きません。1 物あたりが小さいからです。

担いだ木 8000 物で、weight と load を 1 回ずつ読むのに 1.19 ミリ秒——1 物あたり 0.15 マイクロ秒でした(2026-09-18 時点、Intel Xeon @ 2.10GHz・Node v22 で計測)。この 8000 物でも 1 フレーム(16 ミリ秒)の 1 割に届きません——1% に収まるのは千物ほどまでです。

「担げる量で頭打ちだから安い」ではありません。 枠(cell_count)とかさ(capacity、7 節)が 決めるのは物の数ではなく、詰める物 1 個のかさとの割り算なので、かさの小さい物で埋めれば数は いくらでも増えます(籠 1 つを骨針で埋める、など)。それでも読み取りが 1 フレームに並ぶ のは 10 万物の桁で、1 万物なら 1.5 ミリ秒(1 フレームの 1 割)です。

頭打ちが在るのは範囲のほうだけです——目方を名乗らないでいられるのは土地と world だけ(1.1 節)で、 その 2 つは誰かの中へ入らないので、島に物がいくつ在っても、担ぎ手の木には入ってきません。

1 物あたりが保たれていることは tests/diagnostics/interactionCost.test.ts が見ます——同じ規模の 木を組んで測り、桁の変わった遅さで赤くなります。

5. 負荷の効かせ方

load の段(stages、GameElementDefinition.md 6.4 節)の危険域に入ると、 移動できなくなります。閾値はキャラクタごと(characters/)に置くので、担ぎ慣れの個人差はここに出ます。 域そのものの見せ方は、ステータスの一種として StatusArea.mdが扱います。

段が駆動するのは移動の可否だけではありません。 歩みの遅れと stamina の削りも同じ段が持ちます (../world/Characters.md 荷重の効き方節)。本節が決めるのは load が どう積み上がるかまでで、段に何をぶら下げるかはキャラクタの側です。

書き方は、道の travel に agent の load を見る条件を足すだけです。

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

閾値ではなく段の名前で見るのは、同じ境目を stages の min と条件の側に二重に書くと、片方だけ 直したときに「バーは危険域なのに歩ける」が黙って成立するためです。段の名前は too_heavy で固定し、 閾値だけをキャラクタごとに変えます(../world/Characters.md)。 止めたいのは too_heavy の下端を越えた荷なので、演算子は「その段以上」の側です (in_stage_or_above、GameElementDefinition.md 14.1 節)。

実行できないアクションのボタンは押せない見た目になり、押している間だけ理由が出ます (reason: too_heavy、GameElementDefinition.md 14.6 節)。

6. 環境からの保護は sheltered プロパティ1つで表す

蓋のある入れ物に入れた物が天候から守られることは、sheltered(0/1)というプロパティ1つで表します。 天候を見ている側の add や conditions を、次の条件でゲートするだけです。

conditions:
  - {subject: ancestor, prop: sheltered, eq: 0}

要点は、守る側だけが sheltered: 1 を定義し、守らない側は定義しないことです(world が sheltered: 0 を定義して既定値を与えます)。ancestor は「そのプロパティを定義している最初の祖先」を 探すため(同 8.6 節)、定義しない入れ物は素通りされます。結果として、蓋のない籠を蓋つきの箱にしまえば籠の 中身まで守られ、その籠を地面に置けば world まで遡って 0 になる、という入れ子の振る舞いが、新しい 文法なしで正しく出ます。将来の屋根・小屋も同じプロパティを定義するだけで同じように働きます。

守るのは入れ物だけではありません。 浅い洞窟(../world/Dwellings.md 5.1 節)は 場所として sheltered: 1 を宣言し、中に置いた物・据えた炉が同じ1行で守られます。守られているかを知る道は このプロパティ1つだけで、それをどこでどう読むかは読む側が決めます——たとえば屋外劣化 (DurabilitySystem.md 2 節)は上の条件でそのまま止め、キャラクタは base(GameElementDefinition.md 6.5 節)で値を自分へ写してから、体温の削り(VitalsSystem.md 8.3 節)と状況アイコン (../ui/ScreenLayout.md 4.1.2 節)で読みます。守る側は誰がどう読むかを 知りません——読み手が増えても、増えるのは読む側の宣言だけです。

雨をしのぐことを weather の宣言で表すことはしません。 洞窟が自分の weather を宣言すれば雨を見ている 条件は確かに止まりますが、それは「そこでは天気そのものが違う」という宣言になり、天気を読む先(明るさ・ 気温・湿度)が増えるたびに巻き込まれます。守られているかと、空がどうなっているかは別のことです。

守れるのは天候による屋外劣化だけです。保存温度由来の腐敗は別の add なので止まりません (DurabilitySystem.md 3 節)。蓋つきの箱に生肉を入れても「屋外・晴れ・通常温度の2日」が「通常温度の 2.5日」に戻るだけで、保冷は別の軸のまま残ります。保護を強くしすぎない歯止めとして、この分離をそのまま 使います。

7. 固形物のかさは、大きい物は実占有体積・小さい物は外接直方体

volume の単位は液体と同じ mL です。 値の見積もり方は、その物が「隙間を埋める側か、埋めてもらう側か」で 分かれます。

見積もり方 例
1L を超える物 実際の占有体積(楕円体・円柱として計算) 熟したヤシの実 25×17×17cm の楕円体 = 3,800
1L 以下の物 外接直方体(寸法を掛けるだけ) 石 12×9×7cm = 700
ばらけた材料・ばねのある物 嵩そのまま(詰めても縮まないため) ヤシの実の皮 600g ÷ 嵩密度 0.15 = 4,000

大きい物に隙間ぶんの割り増しを乗せないのは、その隙間を小さい物が埋められるからです。 籠にヤシの実を 入れれば実と実の間に空きが出ますが、そこへ果肉や石を差し込めます。大きい物の側で隙間を負担させると、 同じ空きに二重の値段を付けることになります。

逆に、小さい物には外接直方体を使います。 こちらは隙間を埋めてもらう相手がいない——籠の中で石の周りに できる空きは、石より小さい物がなければ死んだ空間です。その死ぬぶんを外接直方体の割り増しが表します。

ばらけた材料は例外で、嵩のまま見ます。 ほぐれた繊維の塊や葉物の束は、押し込んでも縮まないし、 繊維の間へ石を差し込むこともできません。この場合は「実占有体積 = 嵩」であって、割り増しではありません。

この見積もり方から、次のことが従います。

入れ物の capacity は内容積そのままです(編み籠は 20L)。

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