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

## 概要

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

**入れ物は専用の仕組みを持たず、スロット（[`GameElementDefinition.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`](./GameElementDefinition.md) 7.3 節、受け入れ判定での扱いが
[`SlotSystem.md`](./SlotSystem.md) 2 節にあるだけの単純な仕組みです。

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

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

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

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

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

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

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

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

`weight` は物理量として素直に上へ伝わります。この一貫性のおかげで、キャラクタを筏に乗せれば、
自重と荷物を合わせた重さがそのまま積載として効きます（[`Voyage.md`](../world/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`](./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 節の「装備するスロットによってボーナス量を変える剣」と同じ形です。

```yaml codex: containers.yaml
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<!-- codex: containers.yaml object_defs.woven_basket.props.weight.value -->）と並び、少しでも積めばそりが勝つので、籠を持つ理由が無くなります。今のそりの `load_rate` は、
逆転点を 8kg に置いてこの式から逆算したものです（`tests/world-codex/loadEffects.test.ts` が線を見ます）。**逆転させたい積載量を先に決めて率を逆算する**のが、
数値の決め方として素直です。実際に置いた線は [`../world/Containers.md`](../world/Containers.md) 2節に
あります。

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

| 対象 | weight.value | 子から | weight実効値 | load_rate | load |
|---|---|---|---|---|---|
| 石 | 1000<!-- codex: locations.yaml object_defs.stone.props.weight.value -->（1kg） | 0 | **1000** | — | — |
| そり | 8000<!-- codex: containers.yaml object_defs.sledge.props.weight.value -->（8kg） | 石の実効値 | **自重 + 石** | 1<!-- codex: containers.yaml object_defs.sledge.props.load_rate.value --> に -0.45<!-- codex: containers.yaml object_defs.sledge.props.load_rate.passives.0.modify.self.load_rate --> を足した値（handにいる） | — |
| キャラクター | 65000<!-- codex: characters/player_character.yaml traits.player_character.props.weight.value -->（自重65kg） | そりの実効値 | **自重 + そり** | — | **そりの実効値 × そりの率** |

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

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

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

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

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

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

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

```yaml codex: characters/player_character.yaml traits.player_character.props
# 自重（characters/player_character.yaml）
weight: {value: 65000}
```

```yaml codex: characters/engineer.yaml object_defs.engineer.props
# 荷の段の境目は人物ごと（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`](./GameElementDefinition.md) 6.4 節）の危険域に入ると、
移動できなくなります。閾値はキャラクタごと（`characters/`）に置くので、担ぎ慣れの個人差はここに出ます。
域そのものの見せ方は、ステータスの一種として [`StatusArea.md`](../ui/StatusArea.md)が扱います。

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

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

```yaml codex: locations.yaml object_defs.path
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`](../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` を、次の条件でゲートするだけです。

```yaml codex: drying.yaml traits.dryable.props.drying_remaining.passives.2
conditions:
  - {subject: ancestor, prop: sheltered, eq: 0}
```

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

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

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

守れるのは天候による屋外劣化だけです。保存温度由来の腐敗は別の `add` なので止まりません
（`DurabilitySystem.md` 3 節）。蓋つきの箱に生肉を入れても「屋外・晴れ・通常温度の2日<!-- stats: durations.yaml durations object=raw_meat property=durability shortest_days -->」が「通常温度の
2.5日<!-- stats: durations.yaml durations object=raw_meat property=durability days -->」に戻るだけで、保冷は別の軸のまま残ります。保護を強くしすぎない歯止めとして、この分離をそのまま
使います。

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

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

| | 見積もり方 | 例 |
|---|---|---|
| **1L を超える物** | **実際の占有体積**（楕円体・円柱として計算） | 熟したヤシの実 25×17×17cm の楕円体 = 3,800<!-- codex: coconut.yaml object_defs.coconut.props.volume.value --> |
| **1L 以下の物** | **外接直方体**（寸法を掛けるだけ） | 石 12×9×7cm = 700<!-- codex: locations.yaml object_defs.stone.props.volume.value --> |
| **ばらけた材料・ばねのある物** | **嵩そのまま**（詰めても縮まないため） | ヤシの実の皮 600g<!-- codex: coconut.yaml object_defs.coconut_husk.props.weight.value --> ÷ 嵩密度 0.15 = 4,000<!-- codex: coconut.yaml object_defs.coconut_husk.props.volume.value --> |

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

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

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

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

- **見かけの密度（`weight ÷ volume`）は実際の比重に近づきます。** 実占有体積で見る物では比重そのもので、
  外接直方体で見る小さい物では 1/1.9 程度に薄まります（直方体は卵型の約 1.91 倍）。**石（花崗岩 2.7g/mL）
  を超える値が出たら、どちらかの数値が桁違いです**（`tests/world-codex/containersYaml.test.ts` が全数を
  検査します）。
- **入れ子（籠の中の籠）は自然に止まります。** 編み籠のかさ（29L）は自分の容量（20L）より大きいので、
  別の籠を入れることができません。エンジンが構造として断るのは輪になる配置だけで
  （[`GameElementDefinition.md`](./GameElementDefinition.md) 7.1 節）、入れ子に専用の禁止規則は要りません。
- **重い物とかさばる物で、先に効く上限が違います。** 石を担げば `load` が先に尽き、ヤシの葉のような
  嵩のある物は `capacity` が先に尽きます。**種類を少しずつ持ち歩けば、枠数（`cell_count`）が先に尽きます**
  ——入れ物は「重さ・かさ・種類」の理由で満杯になり、どれが先に来るかは荷の中身が決めます。
  何種類入るかは [`../world/Containers.md`](../world/Containers.md) 1節が決めます（編み籠は10<!-- codex: containers.yaml object_defs.woven_basket.slots.contents.cell_count -->枠）。
- **重ねられる物は、1個ぶんの外寸より小さく見ます。** ヤシの殻の器（200<!-- codex: liquid_containers.yaml object_defs.coconut_bowl.props.volume.value -->）は半球の外寸（450）より小さい
  値です。重ねて入る物のかさは「1個目の外寸」ではなく「1個増やすたびに増える分」なので、そちらを採ります。

**入れ物の `capacity` は内容積そのまま**です（編み籠は 20L）。

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

- **荷重の段ごとの遅れと `stamina` の削り**（[`../world/Characters.md`](../world/Characters.md)
  荷重の効き方節）。「1 日ぶんと、荷を下ろした睡眠1回の回復が同じ桁」という置き方だけが決まっていて、
  `laden` と `heavy` の差をどれだけ開けるかは、**1 日にどれだけ往復するのが普通か**が見えてから決める。
  **段の削りを変えると、担いだままの休息で戻る量も一緒に動く**（回復は削りと同じ足し算に入る、
  [`GameElementDefinition.md`](./GameElementDefinition.md) 11.7 節）ので、動かすのは 2 つの数ではない
- キャラクターの `load` を modify で調整して個人差や怪我を表す場合、`stages` の閾値は型定義なので動かせない。
  怪我は「荷が重く感じる」向きの modify として表すことになる
