@jgengine/core 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/dist/ai/rvo.d.ts +106 -0
  3. package/dist/ai/rvo.js +581 -0
  4. package/dist/ai/utilityBrain.d.ts +173 -0
  5. package/dist/ai/utilityBrain.js +277 -0
  6. package/dist/combat/cover.d.ts +169 -0
  7. package/dist/combat/cover.js +264 -0
  8. package/dist/combat/damageRuntime.d.ts +150 -0
  9. package/dist/combat/damageRuntime.js +208 -0
  10. package/dist/combat/damageTable.d.ts +116 -0
  11. package/dist/combat/damageTable.js +165 -0
  12. package/dist/combat/hitZones.d.ts +70 -0
  13. package/dist/combat/hitZones.js +77 -0
  14. package/dist/combat/magazine.d.ts +8 -0
  15. package/dist/combat/magazine.js +20 -2
  16. package/dist/combat/squadCombat.d.ts +167 -0
  17. package/dist/combat/squadCombat.js +557 -0
  18. package/dist/combat/squadTactics.d.ts +73 -0
  19. package/dist/combat/squadTactics.js +49 -0
  20. package/dist/combat/statusRuntime.d.ts +142 -0
  21. package/dist/combat/statusRuntime.js +236 -0
  22. package/dist/combat/weaponDefinition.d.ts +106 -0
  23. package/dist/combat/weaponDefinition.js +108 -0
  24. package/dist/combat/weaponSystem.d.ts +189 -0
  25. package/dist/combat/weaponSystem.js +469 -0
  26. package/dist/combat.d.ts +9 -0
  27. package/dist/combat.js +9 -0
  28. package/dist/devtools/devtools.d.ts +4 -2
  29. package/dist/devtools/devtools.js +2 -1
  30. package/dist/devtools/discover.d.ts +2 -1
  31. package/dist/devtools/discover.js +38 -0
  32. package/dist/devtools/frame.d.ts +0 -2
  33. package/dist/devtools/frame.js +1 -2
  34. package/dist/devtools/logBus.d.ts +58 -0
  35. package/dist/devtools/logBus.js +79 -0
  36. package/dist/devtools/logs.d.ts +6 -3
  37. package/dist/devtools/logs.js +17 -13
  38. package/dist/devtools/report.d.ts +120 -0
  39. package/dist/devtools/report.js +49 -0
  40. package/dist/devtools/reportEndpoint.d.ts +17 -0
  41. package/dist/devtools/reportEndpoint.js +7 -0
  42. package/dist/devtools/types.d.ts +48 -11
  43. package/dist/editor/document.js +210 -0
  44. package/dist/editor/environment.d.ts +48 -1
  45. package/dist/editor/environment.js +161 -0
  46. package/dist/editor/index.d.ts +6 -2
  47. package/dist/editor/index.js +1 -1
  48. package/dist/editor/types.d.ts +54 -0
  49. package/dist/game/playableGame.d.ts +19 -2
  50. package/dist/gameplay.d.ts +9 -0
  51. package/dist/gameplay.js +9 -0
  52. package/dist/inventory/carryLoad.d.ts +48 -0
  53. package/dist/inventory/carryLoad.js +44 -0
  54. package/dist/inventory/equipment.d.ts +95 -0
  55. package/dist/inventory/equipment.js +169 -0
  56. package/dist/inventory/inventoryModel.d.ts +15 -6
  57. package/dist/inventory/inventoryModel.js +54 -5
  58. package/dist/inventory/shapedGrid.d.ts +26 -0
  59. package/dist/inventory/shapedGrid.js +47 -0
  60. package/dist/loot/lootGen.d.ts +186 -0
  61. package/dist/loot/lootGen.js +437 -0
  62. package/dist/loot/lootWeapon.d.ts +35 -0
  63. package/dist/loot/lootWeapon.js +70 -0
  64. package/dist/meta/changelog.d.ts +1 -1
  65. package/dist/meta/changelog.js +92 -1
  66. package/dist/nav/laneGraph.d.ts +178 -0
  67. package/dist/nav/laneGraph.js +671 -0
  68. package/dist/nav/navCrowd.d.ts +138 -0
  69. package/dist/nav/navCrowd.js +412 -0
  70. package/dist/nav/navRuntime.d.ts +108 -0
  71. package/dist/nav/navRuntime.js +256 -0
  72. package/dist/nav/traffic.d.ts +223 -0
  73. package/dist/nav/traffic.js +919 -0
  74. package/dist/physics/kinematicVehicle.d.ts +36 -0
  75. package/dist/physics/kinematicVehicle.js +15 -1
  76. package/dist/physics/vehicleDamage.d.ts +136 -0
  77. package/dist/physics/vehicleDamage.js +214 -0
  78. package/dist/physics/vehicleParts.d.ts +62 -0
  79. package/dist/physics/vehicleParts.js +65 -0
  80. package/dist/progression/gearScore.d.ts +56 -0
  81. package/dist/progression/gearScore.js +59 -0
  82. package/dist/progression/skillTree.d.ts +129 -0
  83. package/dist/progression/skillTree.js +265 -0
  84. package/dist/progression/xpCurve.d.ts +60 -0
  85. package/dist/progression/xpCurve.js +141 -0
  86. package/dist/render/colorPipeline.d.ts +53 -0
  87. package/dist/render/colorPipeline.js +79 -0
  88. package/dist/render/lightProbes.d.ts +97 -0
  89. package/dist/render/lightProbes.js +221 -0
  90. package/dist/render/lookPreset.js +4 -4
  91. package/dist/render/physicalSky.d.ts +39 -0
  92. package/dist/render/physicalSky.js +89 -0
  93. package/dist/render/postProcessing.d.ts +40 -8
  94. package/dist/render/postProcessing.js +1 -1
  95. package/dist/render/skyOwnedLighting.d.ts +31 -0
  96. package/dist/render/skyOwnedLighting.js +46 -0
  97. package/dist/scene/builtinSceneKinds.js +2 -0
  98. package/dist/sensor/perception.d.ts +66 -5
  99. package/dist/sensor/perception.js +167 -42
  100. package/dist/sim/nearAgents.d.ts +68 -0
  101. package/dist/sim/nearAgents.js +152 -0
  102. package/dist/sim/needs.d.ts +60 -0
  103. package/dist/sim/needs.js +108 -0
  104. package/dist/sim/offScreen.d.ts +181 -0
  105. package/dist/sim/offScreen.js +450 -0
  106. package/dist/sim/routine.d.ts +79 -0
  107. package/dist/sim/routine.js +137 -0
  108. package/dist/visibility/visibilitySystem.js +21 -0
  109. package/dist/world/decalKind.d.ts +48 -0
  110. package/dist/world/decalKind.js +86 -0
  111. package/dist/world/features.d.ts +11 -1
  112. package/dist/world/features.js +1 -0
  113. package/dist/world/grassKind.d.ts +5 -0
  114. package/dist/world/grassKind.js +3 -0
  115. package/dist/world/terraform.d.ts +8 -0
  116. package/dist/world.d.ts +10 -1
  117. package/dist/world.js +9 -0
  118. package/package.json +1 -1
  119. package/skills/jgengine/api.md +59 -6
  120. package/skills/jgengine/capabilities.md +4 -0
  121. package/skills/jgengine-combat/SKILL.md +19 -0
  122. package/skills/jgengine-combat/api.md +246 -0
  123. package/skills/jgengine-combat/capabilities.md +112 -0
  124. package/skills/jgengine-combat/reference.md +32 -0
  125. package/skills/jgengine-gameplay/SKILL.md +39 -0
  126. package/skills/jgengine-gameplay/api.md +221 -7
  127. package/skills/jgengine-gameplay/capabilities.md +64 -3
  128. package/skills/jgengine-gameplay/reference-systems.md +118 -0
  129. package/skills/jgengine-world/SKILL.md +7 -1
  130. package/skills/jgengine-world/api.md +222 -8
  131. package/skills/jgengine-world/capabilities.md +42 -2
  132. package/skills/jgengine-world/recipes/npc-senses.md +8 -2
  133. package/skills/jgengine-world/recipes/vehicle-feel.md +5 -0
@@ -36,12 +36,15 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
36
36
 
37
37
  - `cancelJob` (function) · `import { cancelJob } from "@jgengine/core/gameplay"`
38
38
 
39
- ## carry-encumbrance — carried mass vs capacity into movement penalty tiers and a speed multiplier
39
+ ## carry-encumbrance — gate a pickup against the carrier's remaining capacity
40
40
 
41
+ - `canCarry` (function) · `import { canCarry } from "@jgengine/core/inventory/carryLoad"`
42
+ - `characterLoad` (function) · `import { characterLoad } from "@jgengine/core/inventory/carryLoad"`
41
43
  - `resolveEncumbrance` (function) · `import { resolveEncumbrance } from "@jgengine/core/inventory/encumbrance"`
42
44
 
43
- ## carry-load — sum carried mass from item stacks via an injected mass resolver
45
+ ## carry-load — read carried stacks out of one or more inventories for a weight query
44
46
 
47
+ - `carriedEntries` (function) · `import { carriedEntries } from "@jgengine/core/inventory/carryLoad"`
45
48
  - `totalLoad` (function) · `import { totalLoad } from "@jgengine/core/inventory/encumbrance"`
46
49
 
47
50
  ## cell-grid — immutable 2D board of cells with row clears, gravity collapse, and match runs
@@ -148,6 +151,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
148
151
 
149
152
  - `enqueue` (function) · `import { enqueue } from "@jgengine/core/gameplay"`
150
153
 
154
+ ## equipment-slot-inventory — wear gear by slot with item-kind rules, two-handed blocking and replace-on-equip
155
+
156
+ - `createEquipment` (function) · `import { createEquipment } from "@jgengine/core/inventory/equipment"`
157
+
151
158
  ## event-bus — typed publish/subscribe bus for gameplay events
152
159
 
153
160
  - `createGameEvents` (function) · `import { createGameEvents } from "@jgengine/core/gameplay"`
@@ -188,6 +195,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
188
195
 
189
196
  - `SystemDefinition` (interface) · `import { SystemDefinition } from "@jgengine/core/gameplay"`
190
197
 
198
+ ## gear-score — score a loadout from stat weights, level, rarity and slot coverage
199
+
200
+ - `gearScore` (function) · `import { gearScore } from "@jgengine/core/gameplay"`
201
+
191
202
  ## group-credits — turn a flat list of credited things into ordered credit sections
192
203
 
193
204
  - `groupCredits` (function) · `import { groupCredits } from "@jgengine/core/game/credits"`
@@ -212,7 +223,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
212
223
 
213
224
  - `createEmptyInventory` (function) · `import { createEmptyInventory } from "@jgengine/core/inventory/inventoryModel"`
214
225
 
215
- ## inventory-split — split a stack into a target or first-empty slot
226
+ ## inventory-split — split a stack into a target or first-empty slot, honoring a stack limit
216
227
 
217
228
  - `splitStack` (function) · `import { splitStack } from "@jgengine/core/inventory/inventoryModel"`
218
229
 
@@ -287,20 +298,45 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
287
298
 
288
299
  - `RarityStyle` (interface) · `import { RarityStyle } from "@jgengine/core/gameplay"`
289
300
 
301
+ ## loot-comparison — compare two rolled items with per-stat deltas and a weighted score
302
+
303
+ - `compareLootItems` (function) · `import { compareLootItems } from "@jgengine/core/gameplay"`
304
+
290
305
  ## loot-filter — filter and highlight drops by rarity/name rules
291
306
 
292
307
  - `evaluateLootFilter` (function) · `import { evaluateLootFilter } from "@jgengine/core/gameplay"`
293
308
 
309
+ ## loot-generation — roll a named item from bases, parts, affixes, rarity weights, manufacturer personalities and level scaling
310
+
311
+ - `rollLoot` (function) · `import { rollLoot } from "@jgengine/core/gameplay"`
312
+
294
313
  ## loot-pipeline — compose ordered loot pools with fallbacks, roll modifiers, and provenance
295
314
 
296
315
  - `createLootPipeline` (function) · `import { createLootPipeline } from "@jgengine/core/gameplay"`
297
316
  - `defineLootPipeline` (function) · `import { defineLootPipeline } from "@jgengine/core/gameplay"`
298
317
 
318
+ ## loot-roller — roll items from one validated catalog while counting drops per rarity for a debug probe
319
+
320
+ - `createLootRoller` (function) · `import { createLootRoller } from "@jgengine/core/gameplay"`
321
+
322
+ ## loot-score — split one stat block into per-stat weighted contributions
323
+
324
+ - `lootStatContributions` (function) · `import { lootStatContributions } from "@jgengine/core/gameplay"`
325
+ - `scoreLootStats` (function) · `import { scoreLootStats } from "@jgengine/core/gameplay"`
326
+
299
327
  ## loot-table — register loot tables and roll weighted randomized drops
300
328
 
301
329
  - `createLootRegistry` (function) · `import { createLootRegistry } from "@jgengine/core/gameplay"`
302
330
  - `lootTable` (function) · `import { lootTable } from "@jgengine/core/gameplay"`
303
331
 
332
+ ## loot-validation — check a rolled item against its catalog and report each defect with its code
333
+
334
+ - `validateLootItem` (function) · `import { validateLootItem } from "@jgengine/core/gameplay"`
335
+
336
+ ## loot-weapon — feed a rolled item into a weapon definition so generated gear fires through the weapon system
337
+
338
+ - `lootWeaponDefinition` (function) · `import { lootWeaponDefinition } from "@jgengine/core/gameplay"`
339
+
304
340
  ## maturation-stage — resolve the baby/juvenile/adolescent/adult stage from elapsed maturation
305
341
 
306
342
  - `maturationStage` (function) · `import { maturationStage } from "@jgengine/core/game/breeding"`
@@ -329,6 +365,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
329
365
 
330
366
  - `freezeNarrativeCanon` (function) · `import { freezeNarrativeCanon } from "@jgengine/core/game/narrativeAuthoring"`
331
367
 
368
+ ## near-agents — nav crowd plus per-agent needs and utility brains for on-screen residents
369
+
370
+ - `createNearAgents` (function) · `import { createNearAgents } from "@jgengine/core/gameplay"`
371
+
372
+ ## needs — decaying needs an agent reads by index to decide what it wants next
373
+
374
+ - `createNeedSet` (function) · `import { createNeedSet } from "@jgengine/core/gameplay"`
375
+
332
376
  ## notice-feed — serializable tone-tagged notice/event feed bounded by count and age (toasts, life events, killfeed)
333
377
 
334
378
  - `appendFeed` (function) · `import { appendFeed } from "@jgengine/core/gameplay"`
@@ -343,6 +387,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
343
387
 
344
388
  - `evaluateObjective` (function) · `import { evaluateObjective } from "@jgengine/core/gameplay"`
345
389
 
390
+ ## offscreen-world — schedules agents near the player as real state and far ones as cheap state
391
+
392
+ - `createOffScreenWorld` (function) · `import { createOffScreenWorld } from "@jgengine/core/gameplay"`
393
+
346
394
  ## param-layers — register parameter layers by stable id and resolve serialized selections
347
395
 
348
396
  - `createLayerRegistry` (function) · `import { createLayerRegistry } from "@jgengine/core/gameplay"`
@@ -470,6 +518,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
470
518
 
471
519
  - `createVfxInstanceStore` (function) · `import { createVfxInstanceStore } from "@jgengine/core/gameplay"`
472
520
 
521
+ ## routine-timeline — authored daily routines (home, work, meal) resolved against the world clock
522
+
523
+ - `resolveRoutine` (function) · `import { resolveRoutine } from "@jgengine/core/gameplay"`
524
+
473
525
  ## rule-selection — register rules and bridge a selection to its contributed parameter layers
474
526
 
475
527
  - `createRuleRegistry` (function) · `import { createRuleRegistry } from "@jgengine/core/gameplay"`
@@ -507,11 +559,16 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
507
559
  ## shaped-placement — find a shaped inventory item's first fit with ordered rotations, row-major placement, and an explicit search budget
508
560
 
509
561
  - `findShapedPlacement` (function) · `import { findShapedPlacement } from "@jgengine/core/inventory/shapedGrid"`
562
+ - `fitShapedItem` (function) · `import { fitShapedItem } from "@jgengine/core/inventory/shapedGrid"`
510
563
 
511
564
  ## shop-grid — serializable vendor/shop stock with buy/sell over a caller-owned wallet — finite/unlimited qty, sell-back, restock/setPrice, snapshot/restore
512
565
 
513
566
  - `createShopStock` (function) · `import { createShopStock } from "@jgengine/core/gameplay"`
514
567
 
568
+ ## skill-tree — spend, refuse and refund points over an authored tree with passives, actives and synergies
569
+
570
+ - `createSkillTree` (function) · `import { createSkillTree } from "@jgengine/core/gameplay"`
571
+
515
572
  ## social-emotes — emotes and social interactions between nearby players
516
573
 
517
574
  - `createSocial` (function) · `import { createSocial } from "@jgengine/core/gameplay"`
@@ -644,3 +701,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
644
701
  ## work-queued-jobs — list jobs waiting for a slot, in selection order
645
702
 
646
703
  - `queuedJobs` (function) · `import { queuedJobs } from "@jgengine/core/gameplay"`
704
+
705
+ ## xp-curve — resolve an authored xp growth rule into per-level thresholds, totals and a level cap
706
+
707
+ - `defineXpCurve` (function) · `import { defineXpCurve } from "@jgengine/core/gameplay"`
@@ -28,6 +28,124 @@ and level state, then emits every reached level in ascending order. State stays
28
28
  in the caller's save format and round-trips as ordinary JSON. See the
29
29
  [portable XP/leveling recipe](recipes/portable-xp-leveling.md).
30
30
 
31
+ ## Procedural gear
32
+
33
+ `loot/lootGen` rolls a whole named item from a caller-authored `LootCatalog`:
34
+ bases (`baseStats`, affix `pools`, mount `slots`), parts, affix pools, weighted
35
+ `RarityTier`s, `LootManufacturerDef`s (name, flat `bias`, per-stat `scale`,
36
+ favored pools) and a `levelScale` curve of `[level, multiplier]` points. A roll
37
+ picks maker, base, rarity, one part per slot and `affixCount` affixes; stats are
38
+ `base × rarity.statScale × levelScale`, plus part stats/multipliers, maker
39
+ personality, then additive affixes and multiplicative ones. Additive affix
40
+ rolls scale with level; multiplicative ones do not. Names follow one rule:
41
+ maker, rarity word, first part word, first prefix affix, base name, first suffix
42
+ affix. Every random draw uses the injected `rng`, so a seed reproduces the item.
43
+
44
+ ```ts
45
+ import { compareLootItems, createLootRoller, lootWeaponDefinition, seededRng } from "@jgengine/core/gameplay";
46
+
47
+ const roller = createLootRoller(catalog, { devtoolsName: "loot" });
48
+ const item = roller.roll(seededRng(dropSeed), { level: 30 });
49
+ const delta = compareLootItems(item, equipped, { lowerIsBetter: ["reload"] });
50
+ const weapon = lootWeaponDefinition(item, { template, stats: { damage: "power", roundsPerMinute: "cadence" } });
51
+ ```
52
+
53
+ `validateLootItem` returns coded issues (`unknown-base`, `part-category`,
54
+ `missing-slot-part`, `unknown-affix`, `empty-name`…) and the roller's counters
55
+ snapshot/restore as plain data. Register a roll through
56
+ `proceduralLootEntry` into `item/itemInstanceRegistry` and hand its runtime id
57
+ to `game/lootTable`; the weapon projection only maps stats it is told to, so a
58
+ catalog without weapon stats stays usable for non-weapon gear. Check a
59
+ distribution with a fixed seed over 10,000 drops against each tier's 4σ
60
+ binomial bound rather than eyeballing shares.
61
+
62
+ ## Progression and gear score
63
+
64
+ `progression/xpCurve` turns one authored growth rule into the level table, the cap and a
65
+ plain-words rule line, so the balance lives in save-visible data:
66
+
67
+ ```ts
68
+ import { createSkillTree, defineXpCurve, gearScore } from "@jgengine/core/gameplay";
69
+
70
+ const xp = defineXpCurve({ growth: "power", step: 100, growthRate: 1.5, maxLevel: 40, round: "floor" });
71
+ // rule: "power: 100 x level^1.5, floor (levels 1-40)"; xpForLevel(2) is 282, xpForLevel(40) is 0
72
+ xp.levelForXp(xp.totalXpFor(7)).level; // 7
73
+ xp.track(); // the same thresholds as a `leveling` track for a stat adapter
74
+
75
+ const tree = createSkillTree({ nodes, synergies, points: 6 }, { devtoolsName: "skills" });
76
+ tree.spend("strike"); // { ok: true, spent: 1, pointsAvailable: 5 }
77
+ tree.canSpend("sight"); // { ok: false, reason: "requires", detail: '...needs rank 1 of "focus"' }
78
+ const { stats, actives, synergies: live } = tree.resolved();
79
+ tree.respec(); // { refunded: 6, pointsAvailable: 6 }
80
+ ```
81
+
82
+ A `SkillNodeDef` is `{ id, branch?, cost?, maxRank?, requires?, requiresPointsInBranch?,
83
+ excludes?, passives?, actives?, tags? }`; a `SkillSynergyDef` lists at least two `requires` and
84
+ adds passives/actives only while they all hold. `excludes` is symmetric, so two nodes named as
85
+ alternatives block each other in both directions. Actives are `{ id, cooldownMs?, resourceCost?,
86
+ castType?, groups? }`, shaped to bind into `createAbilityKit` slots. Snapshot/restore carries
87
+ `{ points, ranks }`; restore refuses unknown nodes, over-ranked nodes and negative points without
88
+ mutating. A tree where breadth beats depth is the point: in the test fixture, four points down
89
+ `strike` give `damage +3`, while the same four points in `strike x2`, `focus`, `sight` give
90
+ `damage +4` plus the `overload` synergy and its action.
91
+
92
+ `gearScore(set, options)` scores `slot -> { stats, level?, rarity? }` as
93
+ `Σ_stat value × weight × (lowerIsBetter ? -1 : 1) + levelWeight × level + rarityScore[rarity]`,
94
+ summed over filled slots, minus `emptySlotPenalty` per expected-but-empty slot; it also returns
95
+ `bySlot`, `byStat`, `average`, `coverage` and `emptySlots`. `compareGearSets` ranks two loadouts.
96
+ The stat term comes from `lootStatContributions`, the same function behind `scoreLootStats` and
97
+ `compareLootItems`, so per-item and whole-loadout scores cannot drift apart.
98
+
99
+ ## Inventory, equipment and weight
100
+
101
+ A carried bag and a stash are two declared inventories, so they hold separate plain state from the
102
+ first frame and persist as `InventorySet.snapshot()`:
103
+
104
+ ```ts
105
+ import { createInventorySet } from "@jgengine/core/inventory/inventoryModel";
106
+ import { canCarry, characterLoad } from "@jgengine/core/inventory/carryLoad";
107
+ import { createEquipment } from "@jgengine/core/inventory/equipment";
108
+
109
+ const items = createInventorySet({ bag: { slots: 24 }, stash: { slots: 60 } }, traits);
110
+ items.move("bag", 3, "stash", 0); // across inventories; a rejected move leaves both untouched
111
+ const saved = items.snapshot(); // detached; JSON-safe; restore() refuses a bad save without mutating
112
+
113
+ const gear = createEquipment(
114
+ [{ id: "weapon", blocks: ["offhand"] }, { id: "offhand" }, { id: "chest", accepts: ["armor"] }],
115
+ (itemId) => baseDefs[registry.get(itemId)?.baseId ?? itemId]?.gear, // rolled item -> its base's gear facts
116
+ { devtoolsName: "gear" },
117
+ );
118
+ gear.equip("chest", "plate"); // { status: "ok", replaced: null, ... }
119
+ gear.equipBest("greatsword"); // first free preferred slot, else the first one it may replace
120
+
121
+ const load = characterLoad({
122
+ carried: [items.state("bag")],
123
+ worn: gear.worn().map((entry) => entry.itemId),
124
+ weightOf: (itemId) => baseDefs[itemId]?.weight ?? 0,
125
+ capacity: 60 + stats.value("carryCapacity"),
126
+ });
127
+ load.moveMultiplier; // 1 unencumbered, decays across the soft band, 0 at capacity
128
+ canCarry(load, 12); // gate a pickup; the overload policy stays the game's
129
+ ```
130
+
131
+ `EquipSlotDef` owns `accepts` (item kinds) and `blocks` (slots it occupies while filled), and
132
+ `EquipItemDef` owns the `slots` an item may occupy plus its `kind`; a filled slot swaps and returns
133
+ the replaced item, so nothing is silently destroyed, and `state()` / `restore()` carry
134
+ `{ slots: [[slotId, itemId]] }`. The stash is simply left out of `carried`, so its contents add no
135
+ weight, and declaring it `tier: "banked"` keeps it out of `partitionOnDeath`'s lost half.
136
+
137
+ For a footprint bag, `fitShapedItem` (`inventory/shapedGrid`) places the first legal spot or names
138
+ why it does not fit — `too-large` with both sizes, `no-space` for a full board, `budget-exceeded` for
139
+ an exhausted search. Stack rules stay in `ItemTraits`: `putItem` fills partial stacks then opens new
140
+ ones up to the max stack, `moveItem` merges with a remainder, and `splitStack(..., limit)` refuses an
141
+ `over-stack` merge instead of breaking the cap.
142
+
143
+ The rendered side is the engine's: `<InventoryGrid inventoryId="bag" />` (`packages/react`) binds a
144
+ live declared inventory with drag, swap, merge and split through the `inventory.move` /
145
+ `inventory.split` commands, and `<EquipmentSlots items={...} />` paints the worn slots from
146
+ `gear.worn()`. What the grid cannot yet do is drag from one inventory to another (bag to stash), so a
147
+ stash transfer goes through `InventorySet.move` behind a button or command.
148
+
31
149
  ## Coherent authored content
32
150
 
33
151
  Keep stable ids and canonical facts for characters, factions, places, and world rules in the game's authored data. Record each chapter's prerequisites, consequences, and which facts it may introduce or change. The game's creative pillars, story voice, content palette, and desired player experience come from its author; generated prose, items, art, and assets must serve those constraints. Reuse constrained generation and validation code across games while each game owns its content and visual identity; do not fill catalogs from default genre kits or mass generic content.
@@ -59,7 +59,13 @@ NPC decisions are a `decisionGraph` behavior: the graph and its blackboard are d
59
59
 
60
60
  For a bounded local detour over movement collision geometry, use `planSolidRoute` (`nav/solidRoute`) with an indexed `solidObstaclesNear` or `sourceObstaclesNear` query. Handle `no-path` and `budget` separately; retain caller movement, goals, terrain and retry policy. See [reference.md](reference.md#local-collision-routes) for limits.
61
61
 
62
- Route on a polygon nav mesh through one `createNavMeshQuery(mesh)` per mesh; price terrain and gate doors with `areaCosts` via `retune`, and keep `maxNodes` bounded so a long request returns a `partial` route instead of stalling a frame.
62
+ Route on an editor-baked polygon nav mesh through `createNavRuntime` (`nav/navRuntime`): read the bake off the scene document with `navMeshFromBake(document.bakes)`, price terrain and gate doors with `areaCosts`, author doors, ladders and jumps as `AuthoredNavLink` world points, and call `applyMesh(nextBake)` after an edit — polygons that survive keep their identity through the returned `remap`, so only routes crossing changed polygons need re-planning (`createNavCrowd`'s `revalidate`). A bare `createNavMeshQuery` per mesh is still the lower-level seam; keep `maxNodes` bounded so a long request returns a `partial` route instead of stalling a frame.
63
+
64
+ Move many agents across that mesh with `createNavCrowd` (`nav/navCrowd`): it plans corridors, follows the funnel-smoothed waypoints, keeps them apart with `createRvoCrowd` (`ai/rvo`) reciprocal velocity obstacles, bounds path searches to a per-tick time and count budget, and serializes through `snapshot`/`restore`. Give a crowd converging on one spot a `spread` radius so agents fan out (`ai/crowd`'s stable per-id offset) instead of stacking. Avoidance keeps agents inside the mesh's outer bounds; interior walls remain the game's collision layer.
65
+
66
+ ### Street traffic
67
+
68
+ Vehicles on roads do not path-find: they follow authored lane spans. `createLaneGraph` (`nav/laneGraph`) resolves those spans — `traffic_lane` paths, or `road` paths from the editor's street bake, split where streets cross — into junctions, crossings with a signal program or first-come right-of-way, and parking spots; `laneGraphFromDocument` reads that straight off the scene document, so editing a road moves its traffic. `createTraffic` (`nav/traffic`) then drives cars on it: IDM car following and braking, one car at a time through each intersection box (only once the exit lane has room, which is what keeps a blocked crossing from wedging the network), signals with phases, lane changes across authored `left`/`right` neighbours, parking that reserves a spot, and `setDemand(count)` for fleet size — the time-of-day curve is the caller's, not the engine's. Cars are posed, not driven: when a player or a chase needs real handling, feed intent into `physics/kinematicVehicle` and let this seam keep the ambient city.
63
69
 
64
70
  ### Fallback-seam diagnostics
65
71