@jgengine/core 0.26.0 → 0.28.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 (143) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/dist/ai/policeResponse.d.ts +253 -0
  3. package/dist/ai/policeResponse.js +421 -0
  4. package/dist/anim/actionLayer.d.ts +47 -0
  5. package/dist/anim/actionLayer.js +63 -0
  6. package/dist/anim/ikSolver.d.ts +14 -0
  7. package/dist/anim/ikSolver.js +37 -0
  8. package/dist/anim/locomotionGraph.d.ts +58 -3
  9. package/dist/anim/locomotionGraph.js +142 -15
  10. package/dist/anim/ragdollRig.d.ts +53 -0
  11. package/dist/anim/ragdollRig.js +132 -0
  12. package/dist/anim/skeletonStandard.d.ts +140 -0
  13. package/dist/anim/skeletonStandard.js +326 -0
  14. package/dist/combat/damageRuntime.d.ts +14 -1
  15. package/dist/combat/damageRuntime.js +62 -4
  16. package/dist/combat/death.js +7 -3
  17. package/dist/combat/deathReason.d.ts +7 -1
  18. package/dist/combat/effects.d.ts +25 -0
  19. package/dist/combat/effects.js +16 -0
  20. package/dist/combat/statusRuntime.d.ts +2 -0
  21. package/dist/combat/statusRuntime.js +1 -0
  22. package/dist/economy/loanBook.d.ts +182 -0
  23. package/dist/economy/loanBook.js +303 -0
  24. package/dist/economy/market.d.ts +161 -0
  25. package/dist/economy/market.js +248 -0
  26. package/dist/economy/transactionLedger.d.ts +162 -0
  27. package/dist/economy/transactionLedger.js +301 -0
  28. package/dist/editor/creatorStorage.js +3 -1
  29. package/dist/editor/simulation.d.ts +5 -2
  30. package/dist/editor/simulation.js +29 -19
  31. package/dist/game/cameraConfig.d.ts +14 -0
  32. package/dist/game/clipRoles.d.ts +5 -4
  33. package/dist/game/clipRoles.js +34 -4
  34. package/dist/game/events.d.ts +27 -0
  35. package/dist/game/gameSave.d.ts +142 -0
  36. package/dist/game/gameSave.js +174 -0
  37. package/dist/game/playableGame.d.ts +74 -6
  38. package/dist/game/saveSlotIndex.d.ts +54 -0
  39. package/dist/game/saveSlotIndex.js +71 -0
  40. package/dist/game/saveSlots.d.ts +2 -0
  41. package/dist/game/saveSlots.js +1 -1
  42. package/dist/game/saveStore.d.ts +8 -0
  43. package/dist/game/saveStore.js +16 -0
  44. package/dist/gameplay.d.ts +17 -2
  45. package/dist/gameplay.js +15 -0
  46. package/dist/inventory/carryLoad.d.ts +48 -0
  47. package/dist/inventory/carryLoad.js +44 -0
  48. package/dist/inventory/equipment.d.ts +95 -0
  49. package/dist/inventory/equipment.js +169 -0
  50. package/dist/inventory/inventoryModel.d.ts +15 -6
  51. package/dist/inventory/inventoryModel.js +54 -5
  52. package/dist/inventory/shapedGrid.d.ts +26 -0
  53. package/dist/inventory/shapedGrid.js +47 -0
  54. package/dist/loot/lootGen.d.ts +186 -0
  55. package/dist/loot/lootGen.js +437 -0
  56. package/dist/loot/lootWeapon.d.ts +35 -0
  57. package/dist/loot/lootWeapon.js +70 -0
  58. package/dist/meta/changelog.d.ts +1 -1
  59. package/dist/meta/changelog.js +156 -1
  60. package/dist/mission/compositions.d.ts +85 -0
  61. package/dist/mission/compositions.js +130 -0
  62. package/dist/mission/mission.d.ts +435 -0
  63. package/dist/mission/mission.js +770 -0
  64. package/dist/mission/triggers.d.ts +61 -0
  65. package/dist/mission/triggers.js +108 -0
  66. package/dist/nav/laneGraph.d.ts +178 -0
  67. package/dist/nav/laneGraph.js +671 -0
  68. package/dist/nav/traffic.d.ts +222 -0
  69. package/dist/nav/traffic.js +918 -0
  70. package/dist/physics/kinematicVehicle.d.ts +36 -0
  71. package/dist/physics/kinematicVehicle.js +15 -1
  72. package/dist/physics/vehicleDamage.d.ts +136 -0
  73. package/dist/physics/vehicleDamage.js +214 -0
  74. package/dist/physics/vehicleParts.d.ts +62 -0
  75. package/dist/physics/vehicleParts.js +65 -0
  76. package/dist/progression/gearScore.d.ts +56 -0
  77. package/dist/progression/gearScore.js +59 -0
  78. package/dist/progression/skillTree.d.ts +129 -0
  79. package/dist/progression/skillTree.js +265 -0
  80. package/dist/progression/xpCurve.d.ts +60 -0
  81. package/dist/progression/xpCurve.js +141 -0
  82. package/dist/runtime/context/combat.js +11 -0
  83. package/dist/scene/builtinSceneKinds.js +2 -0
  84. package/dist/settings/accessibilitySettings.d.ts +81 -0
  85. package/dist/settings/accessibilitySettings.js +152 -0
  86. package/dist/settings/graphicsProfile.d.ts +3 -0
  87. package/dist/settings/graphicsProfile.js +4 -0
  88. package/dist/settings/settingsModel.d.ts +1 -1
  89. package/dist/settings/settingsModel.js +1 -0
  90. package/dist/sim/nearAgents.d.ts +8 -1
  91. package/dist/sim/nearAgents.js +34 -17
  92. package/dist/sim/offScreen.d.ts +2 -1
  93. package/dist/sim/offScreen.js +14 -3
  94. package/dist/sim/population.d.ts +147 -0
  95. package/dist/sim/population.js +182 -0
  96. package/dist/sim/queue.d.ts +90 -0
  97. package/dist/sim/queue.js +171 -0
  98. package/dist/sim/rides.d.ts +287 -0
  99. package/dist/sim/rides.js +471 -0
  100. package/dist/sim/routine.js +19 -11
  101. package/dist/ui/loadingTips.d.ts +41 -0
  102. package/dist/ui/loadingTips.js +64 -0
  103. package/dist/ui/menuNav.d.ts +79 -0
  104. package/dist/ui/menuNav.js +147 -0
  105. package/dist/ui/menuTransition.d.ts +50 -0
  106. package/dist/ui/menuTransition.js +67 -0
  107. package/dist/ui.d.ts +2 -0
  108. package/dist/ui.js +2 -0
  109. package/dist/vfx/effectLibrary.d.ts +98 -0
  110. package/dist/vfx/effectLibrary.js +181 -0
  111. package/dist/vfx/particleBudget.d.ts +40 -0
  112. package/dist/vfx/particleBudget.js +39 -0
  113. package/dist/vfx/particleDirector.d.ts +16 -0
  114. package/dist/vfx/particleDirector.js +18 -0
  115. package/dist/world/authoredSimulation.js +7 -1
  116. package/dist/world/captureCamera.d.ts +30 -0
  117. package/dist/world/captureCamera.js +19 -0
  118. package/dist/world/decalKind.d.ts +48 -0
  119. package/dist/world/decalKind.js +86 -0
  120. package/dist/world/features.d.ts +18 -5
  121. package/dist/world/features.js +1 -0
  122. package/dist/world/grassKind.d.ts +5 -0
  123. package/dist/world/grassKind.js +3 -0
  124. package/dist/world/terraform.d.ts +8 -0
  125. package/dist/world/terrain.d.ts +1 -0
  126. package/dist/world/terrain.js +2 -0
  127. package/dist/world.d.ts +7 -2
  128. package/dist/world.js +5 -0
  129. package/package.json +1 -1
  130. package/skills/jgengine/api.md +2 -2
  131. package/skills/jgengine-combat/SKILL.md +9 -1
  132. package/skills/jgengine-combat/api.md +5 -4
  133. package/skills/jgengine-combat/reference.md +4 -0
  134. package/skills/jgengine-gameplay/SKILL.md +86 -0
  135. package/skills/jgengine-gameplay/api.md +485 -16
  136. package/skills/jgengine-gameplay/capabilities.md +131 -4
  137. package/skills/jgengine-gameplay/reference-systems.md +118 -0
  138. package/skills/jgengine-world/SKILL.md +8 -0
  139. package/skills/jgengine-world/api.md +243 -11
  140. package/skills/jgengine-world/capabilities.md +61 -1
  141. package/skills/jgengine-world/recipes/character-animation.md +25 -2
  142. package/skills/jgengine-world/recipes/vehicle-feel.md +5 -0
  143. package/skills/jgengine-world/reference.md +1 -1
@@ -24,7 +24,9 @@ spread, damage — hand the definition to `createWeaponSystem`
24
24
  (`combat/weaponSystem`): it runs single/auto/burst, hitscan or ballistic
25
25
  projectiles, interrupted reloads and a readable ammo economy, and resolves both
26
26
  deliveries through `createDamageRuntime`. Generated guns (rolled parts) are the
27
- same `WeaponDefinition`; see
27
+ same `WeaponDefinition`: `loot/lootGen` rolls the item and `loot/lootWeapon`'s
28
+ `lootWeaponDefinition` maps its stats onto one, so no second weapon type exists;
29
+ see
28
30
  [reference.md](reference.md#weapons-as-data).
29
31
  For scene-backed `createProjectileSystem`, inject authoritative `rng`; spread
30
32
  samples independent pellet rays and prediction stays on the center ray. The
@@ -42,6 +44,12 @@ compose through `createDamageRuntime` (`combat/damageRuntime`): a readable table
42
44
  time-to-kill quotes, and status ticks that follow the declared stack rule. It
43
45
  snapshots/restores as plain data; see
44
46
  [reference.md](reference.md#damage-runtime-armor-crits-and-statuses).
47
+ Both damage paths raise `combat.damage` on `ctx.game.events`, one event per target
48
+ application: the runtime with the table `type`, and `ctx.scene.entity.effect` with
49
+ the effect id as `type` and `remaining` read from the last pool in the receive
50
+ order. Use the runtime for new damage that needs types, zones or armor; keep the
51
+ effect path for games that already drain `receive` pools. Neither delegates to
52
+ the other, so a hit never raises the event twice.
45
53
 
46
54
  Closure-backed magazines expose snapshot/restore and `retune` for capacity/reload
47
55
  changes without reconstructing ammo state; capacity overflow is explicit.
@@ -69,7 +69,7 @@
69
69
  - `DamageTableDef` (interface): interface DamageTableDef — A serializable damage table: damage types scored against resistance classes, plus the armor curve those rows are read with. It is data a generated gun (B3.1) and a weapon runtime (B2.3) pass in; the engine holds no built-in elemental chart.
70
70
  - `DamageTableRow` (interface): interface DamageTableRow — One readable row: a type against a resistance class at a given armor value.
71
71
  - `DamageTypeDef` (interface): interface DamageTypeDef — One damage type's row in a damage table: a caller-owned id (`"kinetic"`, `"fire"`) and how it fares against each resistance class a target can present (`"flesh"`, `"shield"`, `"armor"`). Values below `1` are resisted, `1` is neutral, above `1` is a weakness, and `0` is immune.
72
- - `DeathReason` (type): type DeathReason = | { kind: "player_kill"; killerUserId: string; via?: { item?: string } } | { kind: "environment"; source: string } | { kind: "self"; source: string } — Why an entity died — who or what gets credit, for drop/command rules and the `entity.died` event.
72
+ - `DeathReason` (type): type DeathReason = | { kind: "player_kill"; killerUserId: string; source?: string; via?: { item?: string } } | { kind: "environment"; source: string; attacker?: string } | { kind: "self"; source: string } — Why an entity died — who or what gets credit, for drop/command rules and the `entity.died` event. `source` names what dealt it: a player kill's killer instance id, or the hazard/item of an environment death; `attacker` carries the non-player entity's instance id when one dealt it.
73
73
  - `DefenseResolution` (interface): interface DefenseResolution — Result of resolving an incoming hit against a defensive window: `outcome` is the graded defense (`"parry"`/`"block"`/`"iframe"`/`"hit"`) and `perfect` is true when the input landed inside the tight just-frame at the window's start rather than its wider active span.
74
74
  - `DuplicateDamageTypeError` (class): class DuplicateDamageTypeError extends Error — Raised when a table lists the same damage type twice, which would make the winner depend on order.
75
75
  - `EncounterConfig` (interface): interface EncounterConfig<TData = unknown> — Non-serializable configuration for an encounter: the authored phase tree plus predicate and spawn-provider registries. Re-supplied alongside a loaded {@link EncounterState}, the way `advanceSpawnDirector` pairs config with state, so the state itself stays pure data.
@@ -243,7 +243,7 @@
243
243
  - `createCoverField` (function): function createCoverField(options: CoverFieldOptions): CoverField — Cover over authored boxes: derive the stand points once, re-derive them after an edit, and answer bounded nearest-cover queries. `walkable` is injected, so the seam checks the game's own ground test (`nav.findPolygon(point) !== null`) instead of assuming a flat floor.
244
244
  - `createDamageClamp` (function): function createDamageClamp(config: DamageClampConfig): DamageInterceptor — A stateless interceptor that caps any single application at `maxPerHit`.
245
245
  - `createDamagePipeline` (function): function createDamagePipeline(initial?: readonly DamageInterceptor[]): DamagePipeline — Create a mutable damage interception chain that state transitions install into and remove from — e.g. add an invulnerability interceptor on phase enter and drop it on phase exit. Evaluation order is install order.
246
- - `createDamageRuntime` (function): function createDamageRuntime(config: DamageRuntimeConfig): DamageRuntime — Build a damage model from data. The number a hit lands is `base × zone multiplier × type multiplier × (1 − armor mitigation)`; statuses tick through the same table and armor, so a burn on an armored target is mitigated like a bullet.
246
+ - `createDamageRuntime` (function): function createDamageRuntime(config: DamageRuntimeConfig): DamageRuntime — Build a damage model from data. The number a hit lands is `base × zone multiplier × type multiplier × (1 − armor mitigation)`; statuses tick through the same table and armor, so a burn on an armored target is mitigated like a bullet. Every application — a hit or a status tick — is announced as `combat.damage` carrying the target, source, type, amount, absorbed, remaining health and a lethal flag, so a HUD names the blow instead of guessing.
247
247
  - `createDeathSystem` (function): function createDeathSystem(deps: DeathSystemDeps): DeathSystem — Resolve entity death and the on-death consequences — drops, respawn eligibility, kill credit.
248
248
  - `createDefensiveWindow` (function): function createDefensiveWindow(config: DefensiveWindowConfig): DefensiveWindow — Open a timed defensive window — block, parry, or i-frames — and test incoming hits against it.
249
249
  - `createDotField` (function): function createDotField(): DotField — Builds an empty {@link DotField}; `apply` DoTs onto it and drain damage each frame with `tick`.
@@ -462,7 +462,7 @@
462
462
  - `TargetProfile` (interface): interface TargetProfile — What a target presents to the table: one resistance class and its armor points.
463
463
  - `TimeToKill` (interface): interface TimeToKill — A quoted time to kill for one fixed hit against one health pool.
464
464
  - `UnknownTargetProfileError` (class): class UnknownTargetProfileError extends Error — Raised when a hit names a target that was never registered — a wiring error, not a 0-damage hit.
465
- - `createDamageRuntime` (function): function createDamageRuntime(config: DamageRuntimeConfig): DamageRuntime — Build a damage model from data. The number a hit lands is `base × zone multiplier × type multiplier × (1 − armor mitigation)`; statuses tick through the same table and armor, so a burn on an armored target is mitigated like a bullet.
465
+ - `createDamageRuntime` (function): function createDamageRuntime(config: DamageRuntimeConfig): DamageRuntime — Build a damage model from data. The number a hit lands is `base × zone multiplier × type multiplier × (1 − armor mitigation)`; statuses tick through the same table and armor, so a burn on an armored target is mitigated like a bullet. Every application — a hit or a status tick — is announced as `combat.damage` carrying the target, source, type, amount, absorbed, remaining health and a lethal flag, so a HUD names the blow instead of guessing.
466
466
 
467
467
  ## @jgengine/core/combat/damageTable
468
468
 
@@ -501,7 +501,7 @@
501
501
 
502
502
  ## @jgengine/core/combat/deathReason
503
503
 
504
- - `DeathReason` (type): type DeathReason = | { kind: "player_kill"; killerUserId: string; via?: { item?: string } } | { kind: "environment"; source: string } | { kind: "self"; source: string } — Why an entity died — who or what gets credit, for drop/command rules and the `entity.died` event.
504
+ - `DeathReason` (type): type DeathReason = | { kind: "player_kill"; killerUserId: string; source?: string; via?: { item?: string } } | { kind: "environment"; source: string; attacker?: string } | { kind: "self"; source: string } — Why an entity died — who or what gets credit, for drop/command rules and the `entity.died` event. `source` names what dealt it: a player kill's killer instance id, or the hazard/item of an environment death; `attacker` carries the non-player entity's instance id when one dealt it.
505
505
 
506
506
  ## @jgengine/core/combat/defensiveWindow
507
507
 
@@ -541,6 +541,7 @@
541
541
  - `AreaTarget` (interface): interface AreaTarget { instanceId: string; scale: number } — ⚠ undocumented
542
542
  - `AreaTargetInput` (interface): interface AreaTargetInput { at: EntityPosition; radius: number; falloff?: "linear" | "none"; los?: boolean } — ⚠ undocumented
543
543
  - `CombatSpatialDeps` (interface): interface CombatSpatialDeps { inRadius(center: EntityPosition, radius: number): string[]; hasLineOfSight(from: EntityPosition | string, to: string): boolean; positionOf(instanceId: string): EntityPosition | undefined } — ⚠ undocumented
544
+ - `EffectDamageApplication` (interface): interface EffectDamageApplication — One effect application that drained a target's pools, as an announcer needs it. The effect path has no damage table, so the caller names the channel (`effect`) and reads health its own way.
544
545
  - `EffectInput` (type): type EffectInput = SingleTargetEffectInput | AreaEffectInput — ⚠ undocumented
545
546
  - `EffectResult` (interface): interface EffectResult { instanceId: string; effect: string; applied: AppliedPoolDelta[]; lethal: boolean; slain?: SlainIdentity } — ⚠ undocumented
546
547
  - `EffectSystem` (interface): interface EffectSystem { canReceive(instanceId: string, effect: string, magnitude?: number): string | null; preview(input: SingleTargetEffectInput): number; applyEffect(input: EffectInput): EffectResult[] } — ⚠ undocumented · used by `createEffectSystem`: Resolve direct or area resource effects through caller-owned stat pools, including ordered absorption, restoration, modifiers, and lethal co…
@@ -49,6 +49,8 @@ AoE: `inRadius(at, radius)` → LoS filter (default on) → `canReceive` per tar
49
49
 
50
50
  Each per-target `EffectResult` carries `{ instanceId, effect, applied, lethal }`. On a **lethal** hit it additionally carries `slain: { catalogId, name?, userId? }` — the victim's identity captured **before** the death system despawns it, so kill credit / XP reads the slain kind straight off the call result without a spawn-time id→kind registry (a non-lethal hit omits `slain`). This is the synchronous complement to the `entity.died` event: use `slain` when the code awarding the kill is the same code that fired the effect; use `entity.died` for decoupled listeners (quests, feeds, leaderboards).
51
51
 
52
+ Every draining application also raises one `combat.damage` (`DamageAppliedEvent`) on `ctx.game.events` per target it lands on: `source` is the effect's `from`, `type` is the effect id, `amount` is the total drained, `absorbed` is the incoming magnitude the receive modifiers and depleted pools stopped, and `remaining`/`lethal` read the **last** stat in the receive order — the pool whose exhaustion counts as death. A restorative application (`via: { amount: -n }`) raises no damage event.
53
+
52
54
  `canReceive`'s optional signed `magnitude` makes the check direction-aware: positive (or omitted) keeps the standard `"pools-depleted"` check; negative checks the opposite direction and returns `"pools-full"` only when every stat in the receive order is already at max — so a heal (`via: { amount: -n }`) correctly reaches a fully-depleted target and is rejected only when there's nothing left to restore.
53
55
 
54
56
  **Projectiles** (aim-based — no target ids):
@@ -145,6 +147,8 @@ Genre systems layered over the same effects/projectiles/targeting/loot primitive
145
147
 
146
148
  `createDamageRuntime` (`combat/damageRuntime`) is the stateful in-process composition of the table, the zones, the armor curve, and carried statuses. A number reads as `base × zone multiplier × type multiplier × (1 − armor mitigation)`; `hit()` returns every stage, `tick()` ticks statuses through the same table and armor, `timeToKill()` quotes a fight, and `table()` reads the whole chart. `snapshot()`/`restore()` carry registered targets and carried statuses; `retune()` swaps table, zones, or definitions without dropping either. For multiplayer authority, keep the pure stages (`resolveMatchup`, `resolveReceivedDamage`, `resolveStatusApplication`) and replicate their results instead.
147
149
 
150
+ With `events: ctx.game.events` and `healthOf`, every hit and status tick raises one `combat.damage` carrying the table `type`, the final `amount`, what resistance stopped (`absorbed`), and `remaining`/`lethal`. The runtime and the effect path each announce their own application; neither calls the other, so a game that uses both gets one event per blow, not two. Prefer the runtime for damage that needs types, zones, armor or time-to-kill; the effect path stays for pools drained directly through the entity catalog's `receive` rules and now announces the same event with the effect id as `type`.
151
+
148
152
  - **Type × resistance table** (`combat/damageTable`) — `{ types: [{ id, vs: { armor: 2, flesh: 0.5 }, bypassArmor? }], armor: { halfValue, maxMitigation }, defaultMultiplier? }`. `typeMultiplier` reads one pair; `resolveTableDamage` folds type, armor, and base into a row; `readDamageTable` returns every row at given armor values and `formatDamageTable` prints them. Armor uses `armor / (armor + halfValue)` — armor equal to `halfValue` halves the hit — so a designer reads the number rather than guessing a percentage per point. Unknown types throw; unknown resistance classes use the default.
149
153
  - **Hit zones and crits** (`combat/hitZones`) — `{ zones: [{ id, multiplier?, critChance?, critMultiplier? }], defaultZone, critChance?, critMultiplier? }`. `resolveHitZone(table, zone, rng)` rolls once and returns **one** multiplier: a crit replaces the zone's normal multiplier (head 1.5× normal, 2.5× crit → 2.5×, never 3.75×). `hitZoneMultiplier(table, zone, crit)` quotes the same number without a roll, which is what `timeToKill({ crit: true })` uses.
150
154
  - **Statuses** (`combat/statusRuntime`) — `createStatusRuntime({ statuses, rng?, applyDamage? })`; each `StatusDef` declares `{ id, stack, durationMs, intervalMs?, magnitude?, effect?, chance? }`. `stack` is the existing `StatusStackPolicy` (`refresh` / `stack` to `max` / `replace` / `ignore`), and it is the only thing that changes stacks: a tick deals `magnitude × stacks`, so two burns at 4 per tick deal 8, never 16. `effect` is `{ kind: "damage", channel }` (routed through `applyDamage`, so resistances and armor apply) or `{ kind: "stat", stat }` (read with `statDelta`, e.g. negative `moveSpeed` for slow or negative `armor` for corrode). Reapplication restarts the tick clock instead of banking a burst; ticks per call are bounded and the remainder carries. `dotField` stays the cheap no-stacking scheduler.
@@ -38,10 +38,60 @@ Pins consume no choice RNG and must satisfy the same pool and constraints;
38
38
  incompatible pins backtrack earlier unpinned choices or return `unsatisfiable`
39
39
  within the configured attempt and pick budgets.
40
40
 
41
+ Roll whole named gear from an authored catalog with `rollLoot`
42
+ (`loot/lootGen`): bases with mount slots, parts, affix pools, weighted rarity
43
+ tiers, manufacturer stat personalities and a level curve, all caller data.
44
+ `validateLootItem` names each defect, `compareLootItems` returns per-stat deltas
45
+ and a weighted score, and `lootWeaponDefinition` (`loot/lootWeapon`) projects a
46
+ rolled item onto a `WeaponDefinition` for `createWeaponSystem`. `createLootRoller`
47
+ counts drops per rarity for a debug probe and snapshots its counters. Sample
48
+ catalog, distribution check and generated-weapon proof are in
49
+ [reference-systems.md](reference-systems.md#procedural-gear).
50
+
51
+ Resolve an authored XP curve with `defineXpCurve` (`progression/xpCurve`): one growth rule
52
+ (`flat`, `linear`, `power`, `geometric`, `table`), a `maxLevel` cap and a stated `rule` string;
53
+ `table()` and `totalXpFor()` read the same data, and `track()` hands those thresholds to
54
+ `leveling`. Spend a character's points in `createSkillTree` (`progression/skillTree`): nodes carry
55
+ a point `cost`, `requires`, `excludes`, `passives` and `actives`, `synergies` pay off two or more
56
+ ranked nodes together, `respec()` refunds exactly the points spent, and `resolved()` returns the
57
+ `StatModifierSet` plus ability-slot-shaped actives to feed `createStats` / `createAbilityKit`.
58
+ Score a whole loadout with `gearScore` (`progression/gearScore`) on the same weight policy as
59
+ `compareLootItems`. [reference-systems.md](reference-systems.md#progression-and-gear-score) shows
60
+ the data shapes and a two-build comparison.
61
+
62
+ Wear gear with `createEquipment` (`inventory/equipment`): slot defs own accepted kinds and the slots
63
+ they block, item defs own the slots an item may occupy, a filled slot swaps and hands back the
64
+ replaced item, and `state()` / `restore()` round-trip the worn set. Weigh a character with
65
+ `characterLoad` (`inventory/carryLoad`), which turns carried inventories plus worn gear into the
66
+ shared encumbrance bands (`moveMultiplier`, sprint/jump gates, `remaining`), so `canCarry` gates a
67
+ pickup. Declare a stash as its own inventory (`tier: "banked"`) and move items with
68
+ `InventorySet.move`; `snapshot()` / `restore()` save both as detached data. The bag and stash render
69
+ through `<InventoryGrid>` with `<EquipmentSlots>` above it. See
70
+ [inventory, equipment and weight](reference-systems.md#inventory-equipment-and-weight).
71
+
41
72
  Validate generated quest catalogs with `validateQuestCatalog` (`game/questCatalog`).
42
73
  Follow the [quest authoring recipe](recipes/quest-authoring.md) for bounded batches,
43
74
  canonical facts, external unlock declarations, and precise repair diagnostics.
44
75
 
76
+ Run a mission with `createMissionRuntime` (`mission/mission`): a mission is data — a graph of
77
+ objectives the machine walks, with named fail states, per-objective timers, conditional branches and
78
+ rewards. `escortMission`, `chaseMission` and `defendMission` (`mission/compositions`) are compositions
79
+ of the same `reach`/`interact`/`survive`/`pursue`/`kill`/`collect` kinds, not archetypes: an escort is
80
+ a `reach` with an `actor-died` guard, a defend is a `survive` with a survivor floor, a chase is a
81
+ `pursue` with an escape guard. Start, advance and fail missions from authored scene markers with
82
+ `registerMissionTriggerActions` and `createMissionTriggerBridge` (`mission/triggers`); positional
83
+ objectives read `missionMarkers(document)`. Rewards route through the quest appliers and the loot
84
+ registry (`applyMissionRewards`). `validateMissionCatalog` is the content gate and it fails a mission
85
+ whose success path is counters alone — `createMissionRuntime` runs it at construction, so a
86
+ counter-only mission throws instead of shipping.
87
+
88
+ Report what happened instead of polling your own counters: `tick(dt, { events })` takes
89
+ `MissionWorldEvent`s (`kill`/`collect`/`deliver` with a target, actor and tags), an objective's `match`
90
+ filter credits only matching ones - a decoy, a wrong weapon or a rival's kill does not advance it - and
91
+ a `failOn` of kind `event` or `left-area` ends the run on a matching event or on an actor leaving an
92
+ authored marker. `view()` is everything a HUD needs: label, `remaining`, `counted`/`count`,
93
+ `step`/`steps`, `outcome` and `reason`.
94
+
45
95
  For cross-catalog sources and skill gates, compose `validateContentReferences` and
46
96
  `validateContentProgression` (`game/contentValidation`) with `validateRecipeCatalog`
47
97
  (`crafting/recipeCatalog`). Follow [content validation](reference-content-validation.md).
@@ -71,6 +121,10 @@ Prefer `defineSystem` and `defineGame({ systems })` for scheduled capabilities.
71
121
 
72
122
  `createNearAgents` (`sim/nearAgents`) is the default near half: a nav crowd with a need set and a utility brain per agent, counting its decisions for tests and probes. Give it the same need definitions the off-screen world decays, or the two halves disagree at the boundary.
73
123
 
124
+ `createVisitorPopulation` (`sim/population`) is the crowd a scene reports, and it is the bodies: a visitor joins only when your `VisitorWorld.admit` has made its body, and only while `VisitorWorld.has` holds it, so `onSite()` is the member count a HUD reads in O(1) and `arrivalsToday()` is the sum of the admissions the world accepted rather than a tally kept beside them. Arrival rate, capacity (a number or a function of the clock), the leaving rule and the `inside` bounds check are game policy; a refused body or one placed outside the bounds is released and never counted. `rollDay()` closes the day without emptying the park, `adopt` takes over a body a save or a seed made, and a restore only brings back members the world still holds.
125
+
126
+ Rides and their lines are `createRideSystem` and `createWaitQueue` (`sim/rides`, `sim/queue`): a ride cycles load → ride → unload at its own capacity, wears against its own breakdown curve, and reports boards, alights, breakdowns and repair costs through a sink; the wait a guest faces comes from the queue's own measured boarding throughput, not a configured constant. Guest choice stays on the utility brain — `rideChoiceOptions` compiles a guest's `RideTaste` (intensity comfort, nausea comfort, queue tolerance) and the park's ratings into options, `rideChoiceCandidates` costs each one by its own line's wait, and `createRideChooser` wires both to one guest. Ratings, cycle times, breakdown curves and costs are caller data, and `retune` changes them in play.
127
+
74
128
  For staffed production, use `stationOutputRate` (`work/staffedStation`) to resolve
75
129
  caller-owned worker stats and efficiency, then feed the rate to `tickProduction`.
76
130
  Keep staffing and production progress in the same serializable store; the
@@ -120,6 +174,38 @@ A character kit-bashed from primitives/`ModelPart`s (no skeleton, no clips) anim
120
174
  - `ctx.game.chat.send(userId, text)` targets global chat; `recent({ limit })` returns at most 100 messages. The default is 240 characters and one message per author per channel every two seconds. Rejected messages need visible UI feedback.
121
175
  - Hosts reuse `validateChatMessage` and `decideRateWindow`, storing the rate window transactionally with each accepted operation. Preserve chat rate windows when saving/restoring chat state.
122
176
 
177
+ Price goods with `createMarket` (`economy/market`): `price = clamp(basePrice × (coverPeriods × demand / max(stock, 1))^elasticity)`
178
+ between `basePrice × minFactor` and `basePrice × maxFactor`, over finite integer
179
+ stock. `sell` refuses a sale it cannot cover and names the shortfall, `restock`
180
+ lowers the price, `recordDemand(unitsSold, unitsUnmet)` moves demand from what
181
+ buyers actually took, and `priceCurve`/`retune` change the policy. Amounts are
182
+ integer minor units, so a long simulation never drifts.
183
+
184
+ Keep the books with `createTransactionLedger` (`economy/transactionLedger`):
185
+ per-account balances in integer minor units plus a journal recording every
186
+ movement, the balance it produced and a caller-owned `reason`. `post` is the one
187
+ money path, and `totals()` keeps a per-reason count and net past the `maxEntries`
188
+ journal cap. Register upkeep, rent or a subscription with `addCharge` and settle
189
+ it on its cadence with `chargeDue` — bounded per charge and per advance, and a
190
+ cycle with no funds is counted as missed rather than overdrawing.
191
+
192
+ Lend and borrow with `buildLoanSchedule` / `createLoanBook` (`economy/loanBook`):
193
+ a schedule in integer minor units (`"amortized"` or `"flat"`), the principal
194
+ posted on the draw, interest and principal posted as separate reason-tagged
195
+ entries, and arrears that count once per advance until the loan defaults at
196
+ `maxMissed`. `repay` pays principal early; a save stores terms and progress and
197
+ the schedule is rebuilt on restore.
198
+
199
+ One document saves the whole world. `createGameSave` (`game/gameSave`) holds one versioned
200
+ `GameSaveDocument` with a section per registered system, named slots, caller-policy autosave and a
201
+ `migrate` path, over the same injected `SaveBackend` `createSaveStore` uses. Register each stateful
202
+ system once by `name` with its `snapshot()`/`restore()`; `capture()` builds the document,
203
+ `save()`/`touch()` persist it, `load()`/`switchSlot()` restore every section, and
204
+ `digest()`/`digests()` hash the state for a save/reload determinism test. Pass `rng: { seed, stream }`
205
+ so the world's shared seed and cursor survive the save. A snapshot field that means "never" must
206
+ serialize as `null` and be mapped back on restore - `Infinity` becomes `null` through JSON, which
207
+ silently changes a restored run.
208
+
123
209
  For timed away missions, compose the pure transitions in `work/expedition` with
124
210
  your existing `defineStore` and clock. The [expedition recipe](recipes/expeditions.md)
125
211
  keeps bounded offline catch-up, finite supplies, return/death, capacity and RNG