コンテナの容量・重さ・保護
概要
物を入れて運ぶ・しまう入れ物(かご・箱・そり)を、どう成立させるかのドキュメントです。実際に
ゲームへ登場させる入れ物の一覧と比較は ../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 |
大きい物に隙間ぶんの割り増しを乗せないのは、その隙間を小さい物が埋められるからです。 籠にヤシの実を 入れれば実と実の間に空きが出ますが、そこへ果肉や石を差し込めます。大きい物の側で隙間を負担させると、 同じ空きに二重の値段を付けることになります。
逆に、小さい物には外接直方体を使います。 こちらは隙間を埋めてもらう相手がいない——籠の中で石の周りに できる空きは、石より小さい物がなければ死んだ空間です。その死ぬぶんを外接直方体の割り増しが表します。
ばらけた材料は例外で、嵩のまま見ます。 ほぐれた繊維の塊や葉物の束は、押し込んでも縮まないし、 繊維の間へ石を差し込むこともできません。この場合は「実占有体積 = 嵩」であって、割り増しではありません。
この見積もり方から、次のことが従います。
- 見かけの密度(
weight ÷ volume)は実際の比重に近づきます。 実占有体積で見る物では比重そのもので、 外接直方体で見る小さい物では 1/1.9 程度に薄まります(直方体は卵型の約 1.91 倍)。石(花崗岩 2.7g/mL) を超える値が出たら、どちらかの数値が桁違いです(tests/world-codex/containersYaml.test.tsが全数を 検査します)。 - 入れ子(籠の中の籠)は自然に止まります。 編み籠のかさ(29L)は自分の容量(20L)より大きいので、
別の籠を入れることができません。エンジンが構造として断るのは輪になる配置だけで
(
GameElementDefinition.md7.1 節)、入れ子に専用の禁止規則は要りません。 - 重い物とかさばる物で、先に効く上限が違います。 石を担げば
loadが先に尽き、ヤシの葉のような 嵩のある物はcapacityが先に尽きます。種類を少しずつ持ち歩けば、枠数(cell_count)が先に尽きます ——入れ物は「重さ・かさ・種類」の理由で満杯になり、どれが先に来るかは荷の中身が決めます。 何種類入るかは../world/Containers.md1節が決めます(編み籠は10枠)。 - 重ねられる物は、1個ぶんの外寸より小さく見ます。 ヤシの殻の器(200)は半球の外寸(450)より小さい 値です。重ねて入る物のかさは「1個目の外寸」ではなく「1個増やすたびに増える分」なので、そちらを採ります。
入れ物の capacity は内容積そのままです(編み籠は 20L)。
8. 未決事項・今後の検討課題
- 荷重の段ごとの遅れと
staminaの削り(../world/Characters.md荷重の効き方節)。「1 日ぶんと、荷を下ろした睡眠1回の回復が同じ桁」という置き方だけが決まっていて、ladenとheavyの差をどれだけ開けるかは、1 日にどれだけ往復するのが普通かが見えてから決める。 段の削りを変えると、担いだままの休息で戻る量も一緒に動く(回復は削りと同じ足し算に入る、GameElementDefinition.md11.7 節)ので、動かすのは 2 つの数ではない - キャラクターの
loadを modify で調整して個人差や怪我を表す場合、stagesの閾値は型定義なので動かせない。 怪我は「荷が重く感じる」向きの modify として表すことになる