@jgengine/core 0.24.0 → 0.26.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 (98) hide show
  1. package/CHANGELOG.md +92 -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/notices.d.ts +4 -0
  50. package/dist/game/notices.js +4 -3
  51. package/dist/game/playableGame.d.ts +9 -1
  52. package/dist/gameplay.d.ts +4 -0
  53. package/dist/gameplay.js +4 -0
  54. package/dist/meta/changelog.d.ts +1 -1
  55. package/dist/meta/changelog.js +78 -1
  56. package/dist/nav/navCrowd.d.ts +138 -0
  57. package/dist/nav/navCrowd.js +412 -0
  58. package/dist/nav/navRuntime.d.ts +108 -0
  59. package/dist/nav/navRuntime.js +256 -0
  60. package/dist/render/colorPipeline.d.ts +53 -0
  61. package/dist/render/colorPipeline.js +79 -0
  62. package/dist/render/lightProbes.d.ts +97 -0
  63. package/dist/render/lightProbes.js +221 -0
  64. package/dist/render/lookPreset.js +4 -4
  65. package/dist/render/physicalSky.d.ts +39 -0
  66. package/dist/render/physicalSky.js +89 -0
  67. package/dist/render/postProcessing.d.ts +40 -8
  68. package/dist/render/postProcessing.js +1 -1
  69. package/dist/render/skyOwnedLighting.d.ts +31 -0
  70. package/dist/render/skyOwnedLighting.js +46 -0
  71. package/dist/sensor/perception.d.ts +66 -5
  72. package/dist/sensor/perception.js +167 -42
  73. package/dist/sim/nearAgents.d.ts +68 -0
  74. package/dist/sim/nearAgents.js +152 -0
  75. package/dist/sim/needs.d.ts +60 -0
  76. package/dist/sim/needs.js +108 -0
  77. package/dist/sim/offScreen.d.ts +181 -0
  78. package/dist/sim/offScreen.js +450 -0
  79. package/dist/sim/routine.d.ts +79 -0
  80. package/dist/sim/routine.js +137 -0
  81. package/dist/visibility/visibilitySystem.js +21 -0
  82. package/dist/world/features.d.ts +8 -0
  83. package/dist/world.d.ts +5 -0
  84. package/dist/world.js +5 -0
  85. package/package.json +1 -1
  86. package/skills/jgengine/api.md +59 -6
  87. package/skills/jgengine/capabilities.md +4 -0
  88. package/skills/jgengine-combat/SKILL.md +17 -0
  89. package/skills/jgengine-combat/api.md +246 -0
  90. package/skills/jgengine-combat/capabilities.md +112 -0
  91. package/skills/jgengine-combat/reference.md +32 -0
  92. package/skills/jgengine-gameplay/SKILL.md +8 -0
  93. package/skills/jgengine-gameplay/api.md +71 -1
  94. package/skills/jgengine-gameplay/capabilities.md +16 -0
  95. package/skills/jgengine-world/SKILL.md +3 -1
  96. package/skills/jgengine-world/api.md +107 -3
  97. package/skills/jgengine-world/capabilities.md +22 -2
  98. package/skills/jgengine-world/recipes/npc-senses.md +8 -2
@@ -12,6 +12,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
12
12
 
13
13
  - `createAntiOneShotPolicy` (function) · `import { createAntiOneShotPolicy } from "@jgengine/core/combat"`
14
14
 
15
+ ## armor-bypass — ask whether a damage type ignores armor points
16
+
17
+ - `typeBypassesArmor` (function) · `import { typeBypassesArmor } from "@jgengine/core/combat"`
18
+
19
+ ## armor-mitigation — read the fraction of a hit armor removes at a given armor value
20
+
21
+ - `armorMitigation` (function) · `import { armorMitigation } from "@jgengine/core/combat"`
22
+
15
23
  ## capture-bonus-levels — convert wild level and capture effectiveness into bonus levels
16
24
 
17
25
  - `captureBonusLevels` (function) · `import { captureBonusLevels } from "@jgengine/core/stats/spawnLevelStats"`
@@ -40,6 +48,26 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
40
48
 
41
49
  - `createComboPoints` (function) · `import { createComboPoints } from "@jgengine/core/combat"`
42
50
 
51
+ ## cover-field — authored-geometry cover with a bounded nearest query and rebuild on edit
52
+
53
+ - `createCoverField` (function) · `import { createCoverField } from "@jgengine/core/combat"`
54
+
55
+ ## cover-points-from-solids — derive stand-able cover positions from authored solid boxes
56
+
57
+ - `coverPointsFromSolids` (function) · `import { coverPointsFromSolids } from "@jgengine/core/combat"`
58
+
59
+ ## cover-points-near — bounded nearest-cover query
60
+
61
+ - `coverPointsNear` (function) · `import { coverPointsNear } from "@jgengine/core/combat"`
62
+
63
+ ## cover-score — threat-driven cover point scoring
64
+
65
+ - `scoreCoverPoint` (function) · `import { scoreCoverPoint } from "@jgengine/core/combat"`
66
+
67
+ ## cover-select — select cover by threat, role range and flank angle
68
+
69
+ - `selectCover` (function) · `import { selectCover } from "@jgengine/core/combat"`
70
+
43
71
  ## damage-clamp — cap incoming damage per hit at a configured maximum
44
72
 
45
73
  - `createDamageClamp` (function) · `import { createDamageClamp } from "@jgengine/core/combat"`
@@ -60,6 +88,42 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
60
88
 
61
89
  - `createDamagePipeline` (function) · `import { createDamagePipeline } from "@jgengine/core/combat"`
62
90
 
91
+ ## damage-runtime — resolve hits through a damage table, hit zones, and armor, and tick data-defined statuses with declared stacking
92
+
93
+ - `createDamageRuntime` (function) · `import { createDamageRuntime } from "@jgengine/core/combat"`
94
+
95
+ ## damage-table — score a data-defined damage type against a resistance class, with the outcome readable as a table
96
+
97
+ - `typeMultiplier` (function) · `import { typeMultiplier } from "@jgengine/core/combat"`
98
+
99
+ ## damage-table-format — render damage-table rows as fixed-width lines for a probe, a CLI dump, or a test
100
+
101
+ - `formatDamageTable` (function) · `import { formatDamageTable } from "@jgengine/core/combat"`
102
+
103
+ ## damage-table-read — read a damage table as rows of type × resistance × armor mitigation, with final damage per base
104
+
105
+ - `readDamageTable` (function) · `import { readDamageTable } from "@jgengine/core/combat"`
106
+
107
+ ## damage-table-resolve — resolve a damage type against a resistance class and armor into a multiplier, mitigation, and final damage
108
+
109
+ - `resolveTableDamage` (function) · `import { resolveTableDamage } from "@jgengine/core/combat"`
110
+
111
+ ## damage-table-validate — reject a damage table with duplicate types or non-finite multipliers before combat runs
112
+
113
+ - `assertDamageTable` (function) · `import { assertDamageTable } from "@jgengine/core/combat"`
114
+
115
+ ## damage-target-unknown — reject a hit on a target with no registered resistance or armor profile
116
+
117
+ - `UnknownTargetProfileError` (class) · `import { UnknownTargetProfileError } from "@jgengine/core/combat"`
118
+
119
+ ## damage-type-duplicate — reject a damage table that defines one type twice
120
+
121
+ - `DuplicateDamageTypeError` (class) · `import { DuplicateDamageTypeError } from "@jgengine/core/combat"`
122
+
123
+ ## damage-type-unknown — reject a hit whose damage type the table does not define instead of dealing 1×
124
+
125
+ - `UnknownDamageTypeError` (class) · `import { UnknownDamageTypeError } from "@jgengine/core/combat"`
126
+
63
127
  ## death-system — resolve entity death and its on-death consequences
64
128
 
65
129
  - `createDeathSystem` (function) · `import { createDeathSystem } from "@jgengine/core/combat"`
@@ -132,6 +196,22 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
132
196
 
133
197
  - `createAbilityKit` (function) · `import { createAbilityKit } from "@jgengine/core/combat"`
134
198
 
199
+ ## hit-zone — resolve a hit zone into one damage multiplier, crit by zone with an injected RNG
200
+
201
+ - `resolveHitZone` (function) · `import { resolveHitZone } from "@jgengine/core/combat"`
202
+
203
+ ## hit-zone-multiplier — quote a zone's multiplier for a known crit outcome without rolling
204
+
205
+ - `hitZoneMultiplier` (function) · `import { hitZoneMultiplier } from "@jgengine/core/combat"`
206
+
207
+ ## hit-zone-unknown — reject a hit zone table whose named default does not exist
208
+
209
+ - `UnknownHitZoneError` (class) · `import { UnknownHitZoneError } from "@jgengine/core/combat"`
210
+
211
+ ## hit-zone-validate — reject a hit-zone table with duplicate zones or a missing default
212
+
213
+ - `assertHitZoneTable` (function) · `import { assertHitZoneTable } from "@jgengine/core/combat"`
214
+
135
215
  ## hitscan-shot — aim a hitscan shot (shoot a gun): resolve origin and direction from the eye or muzzle for a raycast
136
216
 
137
217
  - `resolveShot` (function) · `import { resolveShot } from "@jgengine/core/combat"`
@@ -199,6 +279,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
199
279
 
200
280
  - `resolveSpawnStats` (function) · `import { resolveSpawnStats } from "@jgengine/core/stats/spawnLevelStats"`
201
281
 
282
+ ## squad-combat — threat-driven cover, flanking, suppression, retreat and role-driven fire discipline
283
+
284
+ - `createSquadCombat` (function) · `import { createSquadCombat } from "@jgengine/core/combat"`
285
+
286
+ ## squad-roles — deterministic role assignment over a caller-authored composition
287
+
288
+ - `assignSquadRoles` (function) · `import { assignSquadRoles } from "@jgengine/core/combat"`
289
+
202
290
  ## stagger-meter — accumulate an ailment buildup (bleed, freeze) until it procs
203
291
 
204
292
  - `createBuildupMeter` (function) · `import { createBuildupMeter } from "@jgengine/core/combat"`
@@ -236,6 +324,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
236
324
 
237
325
  - `createEffectSystem` (function) · `import { createEffectSystem } from "@jgengine/core/combat"`
238
326
 
327
+ ## status-runtime — tick data-defined statuses (burn, shock, slow, corrode) with declared stacking rules and per-tick effects
328
+
329
+ - `createStatusRuntime` (function) · `import { createStatusRuntime } from "@jgengine/core/combat"`
330
+
331
+ ## status-unknown — reject a status id the runtime has no definition for
332
+
333
+ - `UnknownStatusError` (class) · `import { UnknownStatusError } from "@jgengine/core/combat"`
334
+
335
+ ## threats-from-memory — turn perception memory into a bounded, ranked threat list
336
+
337
+ - `threatsFromMemory` (function) · `import { threatsFromMemory } from "@jgengine/core/combat"`
338
+
239
339
  ## threshold-crossings — emit ordered crossing events as a numeric source passes named thresholds with hysteresis and once/repeat policy
240
340
 
241
341
  - `createThresholdTracker` (function) · `import { createThresholdTracker } from "@jgengine/core/combat"`
@@ -248,6 +348,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
248
348
 
249
349
  - `vfxPresets` (const) · `import { vfxPresets } from "@jgengine/core/combat"`
250
350
 
351
+ ## weapon-cadence — convert a rounds-per-minute weapon rate into the interval a fire gate takes
352
+
353
+ - `weaponIntervalMs` (function) · `import { weaponIntervalMs } from "@jgengine/core/combat"`
354
+
355
+ ## weapon-definition-validate — reject a malformed weapon definition at construction with the field named
356
+
357
+ - `assertWeaponDefinition` (function) · `import { assertWeaponDefinition } from "@jgengine/core/combat"`
358
+
251
359
  ## weapon-feedback — turn weapon shots, recoil kick and impacts into per-tick feedback-mixer signals for camera shake, audio and rumble
252
360
 
253
361
  - `createWeaponFeedbackSignals` (function) · `import { createWeaponFeedbackSignals } from "@jgengine/core/combat/weaponFeedback"`
@@ -267,3 +375,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
267
375
  ## weapon-runtime — compose cadence, magazine, a caller-owned raycast, and portable damage resolution into a fire controller
268
376
 
269
377
  - `createWeaponRuntime` (function) · `import { createWeaponRuntime } from "@jgengine/core/combat"`
378
+
379
+ ## weapon-system — run a data-defined weapon: fire modes, recoil and bloom, hitscan or ballistic projectiles, interrupted reloads and readable ammo, all resolving through the damage runtime
380
+
381
+ - `createWeaponSystem` (function) · `import { createWeaponSystem } from "@jgengine/core/combat"`
@@ -141,6 +141,27 @@ Genre systems layered over the same effects/projectiles/targeting/loot primitive
141
141
  - **Resistance matrix** (`combat/resistance`) — a damage-category × target-property table distinct from `combat/attackTags` (those are defense tags). `resolveResistance(matrix, category, targetProperties)` → `{ verdict: "immune" | "resist" | "normal" | "vulnerable", multiplier, immune }`; `resistanceScale` returns just the multiplier (default `immune:0, resist:0.5, normal:1, vulnerable:2`, overridable). Immune on any property wins; resists stack multiplicatively. Sits over the `receive` gate — the handler multiplies its effect `amount` by the scale (Bloons lead/camo immunities, elemental RPG weaknesses).
142
142
  - **Run draft** (`game/runDraft`, built on `world/scatterItems` `pickWeighted` + `stats/statModifiers`) — `createRunModifierStack(offers)` accumulates stacking picks (`add`, `count`, `atMax`, `total(stat)` aggregating adds × exponentiated multiplies, `apply(stats)` onto a `createStats` source). `createRunDraft({ offers, rng? })` adds the pause-flow: `present(n)` draws N distinct weighted offers (excluding maxed-out ones), `choose(id)` applies to the stack. Pause/resume is game-side (`ctx.time.pause()` before `present`, `play()` after `choose`). Vampire Survivors level-up picks, Hades boons, Risk of Rain stacking items.
143
143
 
144
+ ## Damage model, armor, crits, and statuses
145
+
146
+ `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
+
148
+ - **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
+ - **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
+ - **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.
151
+
152
+ ## Weapons as data
153
+
154
+ `createWeaponSystem` (`combat/weaponSystem`) runs one `WeaponDefinition` (`combat/weaponDefinition`): plain JSON with `roundsPerMinute`, `fireMode` (`single` / `auto` / `burst` + `burst.count`), `delivery` (`hitscan` + `maxRange`, or `projectile` + `speed` / `gravity` / `drag` / `lifetime`), optional `magazine`, `handling` (the same recoil bloom and ADS tuning `createWeaponHandling` takes), `damage` (`type`, `base`, optional `statuses`), optional `falloff`, `pellets`, `maxInFlight` and `autoReload`. Nothing in it is a function, so a generator rolls it and a save stores it; `assertWeaponDefinition` rejects a bad roll at construction with the field named.
155
+
156
+ - **Loop.** `setAim(yaw, pitch)` from look input, `setTrigger(down)`, `tick(dt, stance)` → `{ shots, impacts }`. A press survives to the next tick that can use it; a tap does not repeat. Recoil is applied to the aim before the shot leaves, so `record.directions` and the world query see the kicked vector, and `aim()` reports it with recovery folded in.
157
+ - **Spread and bounds.** Each pellet samples the cone uniformly over solid angle from the injected `rng`; zero spread draws nothing. One tick fires at most `maxShotsPerTick` (default 4, clamp 16) catch-up shots and keeps the remaining budget, so a stalled frame cannot turn into a long burst.
158
+ - **Delivery.** A hitscan calls your `raycast(from, direction, maxRange)` on the firing tick. A projectile is stepped with semi-implicit Euler (drag, then gravity), swept as one segment per tick so it cannot tunnel, and resolved when `sweep(from, to)` reports a contact. Both build the same `DamageRuntimeHitInput` for `createDamageRuntime().hit()`, so zone, type, armor, status rolls and provenance are identical; only the distance differs. Falloff scales `base` by the distance the pellet actually travelled.
159
+ - **Live pellets are capped** by `maxInFlight` (default 32, max 256); excess pellets of a shot are counted in `stats().pelletsDropped` rather than queued.
160
+ - **Reload states.** `reload()` starts or continues; `interruptReload()` suspends through `Magazine.pauseReload` keeping progress; `resumeReload()` continues from it; `cancelReload()` discards. `ammo()` returns loaded / capacity / reserve / state / reload fraction / burst remaining / cadence fraction, and `stats()` carries shots, rounds spent, pulls, reloads, drops and damage dealt — the whole ammo economy is readable without touching internals.
161
+ - **State.** `snapshot()` / `restore()` carry aim, handling, magazine, trigger, burst, pellets and counters as plain data; `retune(definition)` swaps rate, feel, damage and capacity without losing ammo or aim (a shrinking magazine discards rounds above the new capacity); a `devtoolsName` registers a one-line probe.
162
+ - **When to use which.** `createWeaponRuntime` (`combat/weaponFire`) is the minimal plumbing for a game that already owns its fire loop and raycaster; `createWeaponSystem` is the definition-driven version with fire modes, ballistics, reload states and ammo accounting. Both leave models, audio, muzzle flash, camera and input to the game; feel numbers come from `measureWeapon` and presentation from `weaponPresentation`.
163
+ - **Evidence** (`combat/weaponSystem.test.ts`): the test fixture at 600 rpm with a 30-round magazine empties in 2.9 s, climbs 17.0° pitch and 3.1° yaw over the magazine, is back to half climb in 0.37 s and within 5% in 0.6 s; bloom runs 2.0 → 8.0 → 12.0 mrad (cap) by the tenth shot and settles to base 0.63 s after the last shot. A hitscan and a projectile contact at the same distance produce the same 20 damage (30 on a 1.5x head zone), and one second of held fire cannot exceed four shots.
164
+
144
165
  ## Encounter sequencing
145
166
 
146
167
  `combat/encounterSequence` is the genre-agnostic hierarchical sequencer behind arenas, raids, tutorials, races, defense phases, and scripted objectives — a serializable state machine over caller-defined phase `data` with **no built-in wave/boss concept**. State is plain JSON (the working tree, per-node runtime, active path, elapsed) so a mid-encounter save round-trips; the predicate/spawn registries live in the config you re-supply alongside it, the way `advanceSpawnDirector` pairs config with state.
@@ -149,3 +170,14 @@ Genre systems layered over the same effects/projectiles/targeting/loot primitive
149
170
  - **Completion predicates** — `completion: { kind, params }` names a registered evaluator; `BUILTIN_COMPLETION_PREDICATES` ships `immediate`, `manual`/`flag`, `timer` (phase seconds), `cleared` (tagged count drained), `metric` (live threshold), `event` (cumulative count), and `quorum` (N of nested predicates). Signals (`counts`/`metrics`/`events`/`flags`) come in through `ctx.signals`; evaluation is deterministic. Register your own via `config.predicates`.
150
171
  - **Spawn providers** — spawning is one optional node adapter: `spawn: { kind, params }` invoked on enter. `BUILTIN_SPAWN_PROVIDERS.points` reads a named authored scene point list from `ctx.spawnPoints` (coordinates stay in the scene, referenced by key — never embedded in the sequence); `list` echoes inline requests. Register catalog/director-backed providers via `config.spawnProviders`.
151
172
  - **Injection & scripted control** — `injectPhase(state, phase, { after | before | childOf })` inserts a phase or branch at runtime (e.g. a boss after the final wave) into the serializable tree without disturbing active/completed nodes; `forceCompletePhase` completes the active phase regardless of its predicate for scripted skips and failure/retry, restoring the authored predicate afterward.
173
+
174
+ ## Enemy squad AI
175
+
176
+ `createSquadCombat` (`combat/squadCombat`) is the combat half of an NPC squad: it reads the perception memory the game already keeps (`createPerception`, B1.4) and moves through the nav crowd it already owns (`createNavCrowd`, B1.2). It never sees a global player position — a member reacts to what its own memory or a squadmate's shared sighting says, so losing contact means losing the fight.
177
+
178
+ - **Cover comes from geometry.** `createCoverField({ solids })` turns authored solid boxes (a `WorldSolid` list drops in) into stand points on every vertical face, clear of the corners and filtered by `walkable: (p) => nav.findPolygon(p) !== null`; `rebuild(solids)` re-derives after an editor edit. A game with its own spatial index supplies the narrower `CoverSource` instead: `near(position, radius, limit)`, bounded per call. Never author a coordinate list for cover.
179
+ - **Selection is by threat.** `selectCover(points, query)` scores each point on `hidden` (confidence-weighted share of remembered threats whose line of sight it blocks, including a body-sized footprint), the role's `preferredRange`, its `flankAngle` around the threat, distance, a squadmate's claim, and whether the walk to it is a detour. A point that hides from one of two shooters is not cover; a memory below `minConfidence` stops steering.
180
+ - **Roles are content.** `assignSquadRoles(members, { roles })` maps names and counts to members deterministically; a role carries `preferredRange`, `flankAngle`, `retreatBelow`, `holdCover`, `discipline` (hold/peek/peek distance/fire interval) and `cover` weights. The engine has no role list, and `"assault"`, `"support"`, `"flanker"` exist only in the game's composition.
181
+ - **Behavior over a tick.** A member picks cover, walks to it, leans out for `peekSeconds` to shoot (`peekSpot` steps around the corner it hides behind), then drops back. `underFire(memberId, amount)` raises suppression, which pins the member to its cover and cancels its peeks until the meter decays. Below a role's `retreatBelow`, the member falls back to the point furthest from the threat. After `relocateAfterSeconds` of continuous exposure it stops trusting its point and moves, which is what breaks a long spell in the open.
182
+ - **Wiring.** `engage(memberId, targetId, distance)` is the paced shot: resolve damage in your own pipeline, call `underFire` on the victim, push a `damage` stimulus at the shooter's position so a member learns about a shooter it cannot see. `healthOf` reads authoritative health, `teamOf` decides who is hostile, `blocked` is your collision layer. `snapshot()`/`restore()` carry member decisions, the crowd and the tuning, so a save mid-fight resumes the same fight.
183
+ - **Evidence.** `combat/squadCombat.soak.test.ts` is the reference run: 2v4 in an authored arena, 90 s, exposure measured every tick against the walls from live positions. Worst exposure 2.33 s against a 3 s bar, 0 of 38 exposure spells over 3 s, p50 0.05 ms per tick. Copy its shape (authored boxes, nav grid with the same holes, perception observing live positions) rather than trusting a flag.
@@ -63,6 +63,14 @@ bounded batches, repair located issues and require explicit semantic approval.
63
63
 
64
64
  Prefer `defineSystem` and `defineGame({ systems })` for scheduled capabilities. Keep boot/join in the game loop only when it is truly lifecycle glue. System ordering, frequency, and serialization are explicit; avoid a giant per-frame callback.
65
65
 
66
+ ## Days and off-screen agents
67
+
68
+ `resolveRoutine` (`sim/routine`) resolves an authored day (home, work, meal) against the world clock: injected travel time makes each stop depart early enough to arrive on time, and an arrival that runs late slips instead of teleporting. The timeline is pure, so a save, a forecast and a live agent read the same day.
69
+
70
+ `createOffScreenWorld` (`sim/offScreen`) keeps every resident either near or cheap. Near residents spawn into your own simulation through `NearAgents`; far ones are a position, a routine phase and a needs record that only move when the clock crosses a routine boundary — no navigation, no needs tick, no utility decision. The distance radius, optional cost model and per-tick switch budget are game config. Crossing back in spawns the resident where the cheap state says it is, with its schedule progress and needs intact, so it cannot pop in at the origin.
71
+
72
+ `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
+
66
74
  For staffed production, use `stationOutputRate` (`work/staffedStation`) to resolve
67
75
  caller-owned worker stats and efficiency, then feed the rate to `tickProduction`.
68
76
  Keep staffing and production progress in the same serializable store; the
@@ -1298,6 +1298,17 @@
1298
1298
  - `MountSlotDef` (interface): interface MountSlotDef { id: string; accepts: string | readonly string[]; required?: boolean } — ⚠ undocumented · used by `slotAccepts`: Attach parts into an item's mount slots and resolve the combined stats.
1299
1299
  - `MultiRegionHealth` (interface): interface MultiRegionHealth — Per-limb / per-region health track with treat/damage/heal APIs.
1300
1300
  - `NEUTRAL_AXIS` (const): const NEUTRAL_AXIS: AxisInput — ⚠ undocumented
1301
+ - `NearAgentSnapshot` (interface): interface NearAgentSnapshot — Serializable state of one near agent: where it walks, what it needs, what it last chose.
1302
+ - `NearAgentView` (interface): interface NearAgentView — Everything a near agent is doing, for a HUD, a probe or a test. Allocates.
1303
+ - `NearAgents` (interface): interface NearAgents — The near half of the world: the game's real simulation. Only residents inside the radius are ever spawned here, which is what keeps far agents free of navigation and utility scoring.
1304
+ - `NearAgentsOptions` (interface): interface NearAgentsOptions — What the near side needs from the game: a nav mesh, the shared need definitions, and what agents can do.
1305
+ - `NearCrowdAgents` (interface): interface NearCrowdAgents extends NearAgents — The near half of the off-screen world: one nav crowd for movement, one need set and utility brain per agent for what it wants. Resident identity survives the handoff because the off-screen world spawns and despawns through this seam and keeps the saved state.
1306
+ - `NearHandoff` (interface): interface NearHandoff — What a near-side simulator hands back when a resident leaves range.
1307
+ - `NearSpawn` (interface): interface NearSpawn — Everything a near-side simulator is told when a resident crosses into range.
1308
+ - `NeedDefinition` (type): type NeedDefinition = DecayMeterConfig — A decaying need is a survival decay meter (`max`/`min`/`start`/`rate`, optional moodle thresholds) whose satisfaction an agent brain reads to score actions. The meter owns the numbers; per-need appeal lives in the game's utility options.
1309
+ - `NeedReading` (interface): interface NeedReading — One need's current satisfaction, worst-first ready: `fraction` is 0 (empty) … 1 (full).
1310
+ - `NeedSet` (interface): interface NeedSet — Named needs that drain or recover on game time — hunger, energy, hygiene, bladder, fun. Reads are index-addressed (`indexOf` once, `fractionAt` per tick) so a thousand-agent loop never re-resolves ids; the whole set round-trips through {@link NeedSnapshot}.
1311
+ - `NeedSnapshot` (interface): interface NeedSnapshot — Serializable need state: values plus any in-play retuning, so a save keeps the dials.
1301
1312
  - `NotificationCenterOptions` (interface): interface NotificationCenterOptions — Options for {@link createNotificationCenter}.
1302
1313
  - `NotificationEntry` (interface): interface NotificationEntry<TMeta = unknown> — One persistent notification — unlike a transient toast, it stays in the log until dismissed.
1303
1314
  - `NotificationFilter` (interface): interface NotificationFilter — Filter for {@link NotificationStore.list}.
@@ -1306,6 +1317,15 @@
1306
1317
  - `NumericBounds` (interface): interface NumericBounds — Optional inclusive `[min, max]` clamp applied after a write. Omit an edge for unbounded.
1307
1318
  - `ObjectStyle` (interface): interface ObjectStyle { color?: string; opacity?: number; hidden?: boolean } — ⚠ undocumented
1308
1319
  - `ObserverCameraConfig` (interface): interface ObserverCameraConfig — Detached spectator/photo cam (#120) — binds to any entity or fixed point, never reads player input.
1320
+ - `OffScreenAgentView` (interface): interface OffScreenAgentView — A resident as a game or the devtools overlay reads it. Allocates a need copy — debug and tests.
1321
+ - `OffScreenMode` (type): type OffScreenMode = "near" | "far" — Which side of the distance policy a resident is on: simulated near the viewer, or cheap state.
1322
+ - `OffScreenOptions` (interface): interface OffScreenOptions — Options for {@link createOffScreenWorld}.
1323
+ - `OffScreenPolicy` (interface): interface OffScreenPolicy — Where the near/far boundary is and what it costs. The radius, the cost model and the budget are the game's call — a city block and a haunted-house hallway do not share a number.
1324
+ - `OffScreenPolicyPatch` (interface): interface OffScreenPolicyPatch — A policy change that stays reachable in play.
1325
+ - `OffScreenResidentSpec` (interface): interface OffScreenResidentSpec — One resident of the world: an identity, where it belongs, and the day it follows.
1326
+ - `OffScreenSnapshot` (interface): interface OffScreenSnapshot — Serializable world state: the policy and every resident, near ones included.
1327
+ - `OffScreenStats` (interface): interface OffScreenStats — Counters a devtools probe or a test reads to prove far agents cost nothing.
1328
+ - `OffScreenWorld` (interface): interface OffScreenWorld — The off-screen world: every resident exists, but only the ones within the policy radius are simulated. The rest are a position, a routine phase and a needs record that only move when the clock crosses a routine boundary — no navigation, no utility scoring, no per-frame need decay.
1309
1329
  - `Overdraft` (type): type Overdraft = boolean | { max: number } — Opt-in debt affordance for {@link charge}/{@link chargeAll}: `true` allows the balance to go arbitrarily negative, a number caps how far into the red it may go (the charge is rejected once `balance - amount` would fall below `-max`). Omitted (the default) keeps the strict no-debt rule.
1310
1330
  - `PING_FEED_ACTION` (const): const PING_FEED_ACTION: "party.ping" — ⚠ undocumented
1311
1331
  - `PairKeyCodec` (interface): interface PairKeyCodec — Canonicalizes a two-part relation identity into a single delimiter-safe record key.
@@ -1361,13 +1381,18 @@
1361
1381
  - `ResourcePolicy` (type): type ResourcePolicy = ( txn: ResourceTransaction, ctx: PolicyContext, ) => readonly ResourceTransaction[] — A composable transform over one transaction. Return the transactions to keep: `[]` rejects it, `[txn]` passes/transforms it, and multiple entries split it.
1362
1382
  - `ResourceTransaction` (interface): interface ResourceTransaction — One resource movement produced by a due cycle, before it is applied to balances.
1363
1383
  - `RetainedVfxKind` (type): type RetainedVfxKind = string — The archetype of a retained (long-lived, updatable) VFX effect — an open string, not a closed union, so a renderer registers new kinds (beam, tether, zone, target line, looping emitter) without a central branch. `"beam"` is the first shipped retained renderer.
1364
- - `Ring` (interface): interface Ring { at(time: number): RingSample; isOutside(time: number, position: RingPoint): boolean; distanceOutside(time: number, position: RingPoint): number; damageOutside(time: number, dt: number, positions: Iterable<{id: string; position: RingPoint}… — ⚠ undocumented
1384
+ - `Ring` (interface): interface Ring — ⚠ undocumented
1365
1385
  - `RingConfig` (interface): interface RingConfig { center: RingPoint; phases: readonly RingPhase[] } — ⚠ undocumented
1366
1386
  - `RingPhase` (interface): interface RingPhase { startTime: number; shrinkDuration: number; fromRadius: number; toRadius: number; damagePerSecond: number; center?: RingPoint } — ⚠ undocumented
1367
1387
  - `RoleSpec` (interface): interface RoleSpec { id: string; count?: number; ratio?: number } — ⚠ undocumented
1368
1388
  - `Rotation` (type): type Rotation = 0 | 1 | 2 | 3 — ⚠ undocumented · used by `canPlace` (@jgengine/core/inventory/shapedGrid): Check bounds and overlap for a discrete footprint, optionally ignoring the item being moved.
1369
1389
  - `RoundConfig` (interface): interface RoundConfig<TPhase extends string = RoundPhase> { phases: Record<TPhase, number>; teams: readonly string[] | readonly RoundTeam[]; phaseOrder?: readonly TPhase[]; winCondition?: (state: RoundSnapshot<TPhase>) => string | null; maxRounds?: number; winReward?: number; lossBonus?: LossBonus… — ⚠ undocumented
1370
1390
  - `RoundSnapshot` (interface): interface RoundSnapshot<TPhase extends string = RoundPhase> { round: number; phase: TPhase; timeLeft: number; scores: Record<string, number>; lossStreaks: Record<string, number>; roles: Record<string, string | undefined>; matchOver: boolean; pendingWinner: string | null } — ⚠ undocumented
1391
+ - `RoutineConfig` (interface): interface RoutineConfig — Everything needed to resolve a routine: its authored stops, its cycle, and how far apart places are.
1392
+ - `RoutinePhase` (interface): interface RoutinePhase — Where a routine puts its agent at one instant.
1393
+ - `RoutineStop` (interface): interface RoutineStop — One stop after travel is accounted for: the authored time and the time the agent really gets there.
1394
+ - `RoutineStopSpec` (interface): interface RoutineStopSpec — One authored stop in a day: where the agent belongs, and when it wants to get there.
1395
+ - `RoutineTimeline` (interface): interface RoutineTimeline — An authored day resolved against time: arrival times after walking, departures early enough to make the next stop, and a phase for any instant. Pure — there is no state to tick, so a far agent and a loaded save read the same day from the same clock.
1371
1396
  - `RtsCameraConfig` (interface): interface RtsCameraConfig extends TopDownCameraConfig — Free-pan / edge-scroll RTS rig (#24) — pan/rotate/zoom independent of any avatar.
1372
1397
  - `RuleCursor` (interface): interface RuleCursor — Mutable per-rule progress, kept separate from the immutable {@link ScheduledRule} definition.
1373
1398
  - `RuleDef` (interface): interface RuleDef<TPayload = unknown> — A selectable rule over a tagged pool. `tags` classify it for include/exclude filtering and for `requires`/`conflicts` matching (which match either another rule's id or one of its tags). `layers` are the parameter layers this rule contributes when active; `payload` is opaque caller data.
@@ -1564,7 +1589,10 @@
1564
1589
  - `createMoodleStack` (function): function createMoodleStack(): MoodleStack — A stateful holder for timed status moodles (food buffs, temporary shelter, warmth). Meters and multi-region health derive their own moodles on read; combine all three through `stackMoodles(stack.list(), meterMoodles, ailmentMoodles)` for one display.
1565
1590
  - `createMultiRegionHealth` (function): function createMultiRegionHealth(config: MultiRegionHealthConfig): MultiRegionHealth — Per-region/limb health tracked separately, so each body part takes and heals damage on its own.
1566
1591
  - `createNameGenerator` (function): function createNameGenerator(options: NameGeneratorOptions): NameGenerator — Generate procedural names from templates and word banks with an injected random source.
1592
+ - `createNearAgents` (function): function createNearAgents(options: NearAgentsOptions): NearCrowdAgents — Create the near-side simulation: agents on a nav mesh with decaying needs and a utility brain each. The off-screen world decides which agents exist here, so a resident beyond its radius costs this seam nothing at all — no path, no need tick, no decision.
1593
+ - `createNeedSet` (function): function createNeedSet(definitions: readonly NeedDefinition[]): NeedSet — Create the needs an agent or a park guest carries. Decay reuses the survival meter formulas (`survival/decayMeter`), so moodle thresholds, clamping and rate modifiers behave exactly like every other meter; this set adds index-addressed reads and single-need retuning for hot AI loops. Score these needs into an action with `createUtilityBrain` (`@jgengine/core/world`).
1567
1594
  - `createNotificationCenter` (function): function createNotificationCenter<TMeta = unknown>(options: NotificationCenterOptions = {}): NotificationStore<TMeta> — A persistent notification log with read/unread tracking — the "notification center" surface toasts don't cover. Newest-first, capped, serializable, and observable; a game pushes durable events (quest updates, trades, party news) here and a HUD renders the list + an unread badge. `snapshot`/`restore` round-trip through a save.
1595
+ - `createOffScreenWorld` (function): function createOffScreenWorld(options: OffScreenOptions): OffScreenWorld — Create the world that decides which agents deserve a simulation. Residents are cheap state until they come near, and cheap again when they leave; the cheap state is the schedule, the needs and the last place, so a returning resident never pops in at the origin.
1568
1596
  - `createPairKeyCodec` (function): function createPairKeyCodec(options: PairKeyOptions = {}): PairKeyCodec — Build a pair-key codec for keyed relation values. Ids are escaped before joining, so any id (including ones containing the separator or a backslash) round-trips through {@link PairKeyCodec.key} → {@link PairKeyCodec.parse} without collision. Undirected codecs (the default) canonicalize so `key(a, b) === key(b, a)`.
1569
1597
  - `createPingSystem` (function): function createPingSystem(deps: PingSystemDeps): PingSystem — Contextual ping/marker communication between teammates, classified by what was pinged.
1570
1598
  - `createPriceHistory` (function): function createPriceHistory(config: PriceHistoryConfig): PriceHistory — The "market price" readout behind every auction house: a bounded rolling record of completed sales per item, aggregated into min/max/volume-weighted-average/latest unit-price stats so games can show current value and recent trends. Memory is bounded by `maxSamplesPerItem` (oldest samples drop first) and optionally by a sliding time window; timestamps are injected by the caller so the history stays deterministic.
@@ -1688,6 +1716,7 @@
1688
1716
  - `resolveOneShotClip` (function): function resolveOneShotClip(oneShots: Record<string, string | readonly string[]> | undefined, event: string, roll: number): string | null — Resolves the clip name a one-shot `event` should play from a model's `animation.oneShots` map, or `null` if the event isn't bound. A `string[]` binding picks a variant by `roll` (a value in `[0, 1)`), so combat can vary attack swings. Pure and deterministic given `roll` — the shell supplies the randomness.
1689
1717
  - `resolveParams` (function): function resolveParams(base: Readonly<Record<string, number>>, layers: readonly ParamLayer[]): ParamSnapshot — Fold an ordered stack of layers over `base` and return the effective {@link ParamSnapshot}. Layers apply in {@link orderLayers} sequence; within a layer each parameter's ops fold left-to-right. Every op is recorded in `contributions` for provenance/preview. Parameters a layer introduces but `base` omits start from `0`. Pure and deterministic — no ambient randomness or time.
1690
1718
  - `resolvePowerGrid` (function): function resolvePowerGrid(supply: number, consumers: readonly PowerConsumer[]): PowerGridResult — ⚠ undocumented
1719
+ - `resolveRoutine` (function): function resolveRoutine(config: RoutineConfig): RoutineTimeline — Resolve an authored routine into a timeline driven by the world clock.
1691
1720
  - `resolveSelection` (function): function resolveSelection(base: Readonly<Record<string, number>>, registry: LayerRegistry, ids: LayerSelection): { readonly snapshot: ParamSnapshot; readonly unknown: readonly string[] } — Reproducible session setup in one call: resolve a saved {@link LayerSelection} through a registry, fold the found layers over `base`, and return the snapshot alongside any unknown ids. The same base + registry + selection always yields the same snapshot.
1692
1721
  - `resumeJob` (function): function resumeJob<TSpec, TReserve>(state: WorkQueueState<TSpec, TReserve>, id: JobId): WorkQueueState<TSpec, TReserve> — Resume a paused job; it re-enters the queue and competes for a slot by ordering.
1693
1722
  - `resumeRule` (function): function resumeRule(ledger: ResourceLedger, ruleId: string): ResourceLedger — Resume a paused rule. Its `nextDueSeconds` is unchanged, so the paused span counts as missed cycles that the rule's {@link CatchUpPolicy} decides how to settle on the next advance.
@@ -2123,6 +2152,47 @@
2123
2152
  - `RoundState` (interface): interface RoundState<TPhase extends string = RoundPhase> { tick(dt: number): RoundEvent<TPhase>[]; concludeRound(winner: string): RoundEvent<TPhase>[]; evaluate(): RoundEvent<TPhase>[]; onPhaseEnd(hook: PhaseEndHook<TPhase>): () => void; phase(): TPhase; round(): number; timeLeft(): number; score… — ⚠ undocumented
2124
2153
  - `RoundTeam` (interface): interface RoundTeam — A team entry with an optional role tag (e.g. "attacker", "defender") retrievable via `RoundState.roleOf`.
2125
2154
 
2155
+ ## @jgengine/core/sim/nearAgents
2156
+
2157
+ - `NearAgentSnapshot` (interface): interface NearAgentSnapshot — Serializable state of one near agent: where it walks, what it needs, what it last chose.
2158
+ - `NearAgentView` (interface): interface NearAgentView — Everything a near agent is doing, for a HUD, a probe or a test. Allocates.
2159
+ - `NearAgentsOptions` (interface): interface NearAgentsOptions — What the near side needs from the game: a nav mesh, the shared need definitions, and what agents can do.
2160
+ - `NearCrowdAgents` (interface): interface NearCrowdAgents extends NearAgents — The near half of the off-screen world: one nav crowd for movement, one need set and utility brain per agent for what it wants. Resident identity survives the handoff because the off-screen world spawns and despawns through this seam and keeps the saved state.
2161
+ - `createNearAgents` (function): function createNearAgents(options: NearAgentsOptions): NearCrowdAgents — Create the near-side simulation: agents on a nav mesh with decaying needs and a utility brain each. The off-screen world decides which agents exist here, so a resident beyond its radius costs this seam nothing at all — no path, no need tick, no decision.
2162
+
2163
+ ## @jgengine/core/sim/needs
2164
+
2165
+ - `NeedDefinition` (type): type NeedDefinition = DecayMeterConfig — A decaying need is a survival decay meter (`max`/`min`/`start`/`rate`, optional moodle thresholds) whose satisfaction an agent brain reads to score actions. The meter owns the numbers; per-need appeal lives in the game's utility options.
2166
+ - `NeedReading` (interface): interface NeedReading — One need's current satisfaction, worst-first ready: `fraction` is 0 (empty) … 1 (full).
2167
+ - `NeedSet` (interface): interface NeedSet — Named needs that drain or recover on game time — hunger, energy, hygiene, bladder, fun. Reads are index-addressed (`indexOf` once, `fractionAt` per tick) so a thousand-agent loop never re-resolves ids; the whole set round-trips through {@link NeedSnapshot}.
2168
+ - `NeedSnapshot` (interface): interface NeedSnapshot — Serializable need state: values plus any in-play retuning, so a save keeps the dials.
2169
+ - `createNeedSet` (function): function createNeedSet(definitions: readonly NeedDefinition[]): NeedSet — Create the needs an agent or a park guest carries. Decay reuses the survival meter formulas (`survival/decayMeter`), so moodle thresholds, clamping and rate modifiers behave exactly like every other meter; this set adds index-addressed reads and single-need retuning for hot AI loops. Score these needs into an action with `createUtilityBrain` (`@jgengine/core/world`).
2170
+
2171
+ ## @jgengine/core/sim/offScreen
2172
+
2173
+ - `NearAgents` (interface): interface NearAgents — The near half of the world: the game's real simulation. Only residents inside the radius are ever spawned here, which is what keeps far agents free of navigation and utility scoring.
2174
+ - `NearHandoff` (interface): interface NearHandoff — What a near-side simulator hands back when a resident leaves range.
2175
+ - `NearSpawn` (interface): interface NearSpawn — Everything a near-side simulator is told when a resident crosses into range.
2176
+ - `OffScreenAgentView` (interface): interface OffScreenAgentView — A resident as a game or the devtools overlay reads it. Allocates a need copy — debug and tests.
2177
+ - `OffScreenMode` (type): type OffScreenMode = "near" | "far" — Which side of the distance policy a resident is on: simulated near the viewer, or cheap state.
2178
+ - `OffScreenOptions` (interface): interface OffScreenOptions — Options for {@link createOffScreenWorld}.
2179
+ - `OffScreenPolicy` (interface): interface OffScreenPolicy — Where the near/far boundary is and what it costs. The radius, the cost model and the budget are the game's call — a city block and a haunted-house hallway do not share a number.
2180
+ - `OffScreenPolicyPatch` (interface): interface OffScreenPolicyPatch — A policy change that stays reachable in play.
2181
+ - `OffScreenResidentSpec` (interface): interface OffScreenResidentSpec — One resident of the world: an identity, where it belongs, and the day it follows.
2182
+ - `OffScreenSnapshot` (interface): interface OffScreenSnapshot — Serializable world state: the policy and every resident, near ones included.
2183
+ - `OffScreenStats` (interface): interface OffScreenStats — Counters a devtools probe or a test reads to prove far agents cost nothing.
2184
+ - `OffScreenWorld` (interface): interface OffScreenWorld — The off-screen world: every resident exists, but only the ones within the policy radius are simulated. The rest are a position, a routine phase and a needs record that only move when the clock crosses a routine boundary — no navigation, no utility scoring, no per-frame need decay.
2185
+ - `createOffScreenWorld` (function): function createOffScreenWorld(options: OffScreenOptions): OffScreenWorld — Create the world that decides which agents deserve a simulation. Residents are cheap state until they come near, and cheap again when they leave; the cheap state is the schedule, the needs and the last place, so a returning resident never pops in at the origin.
2186
+
2187
+ ## @jgengine/core/sim/routine
2188
+
2189
+ - `RoutineConfig` (interface): interface RoutineConfig — Everything needed to resolve a routine: its authored stops, its cycle, and how far apart places are.
2190
+ - `RoutinePhase` (interface): interface RoutinePhase — Where a routine puts its agent at one instant.
2191
+ - `RoutineStop` (interface): interface RoutineStop — One stop after travel is accounted for: the authored time and the time the agent really gets there.
2192
+ - `RoutineStopSpec` (interface): interface RoutineStopSpec — One authored stop in a day: where the agent belongs, and when it wants to get there.
2193
+ - `RoutineTimeline` (interface): interface RoutineTimeline — An authored day resolved against time: arrival times after walking, departures early enough to make the next stop, and a phase for any instant. Pure — there is no state to tick, so a far agent and a loaded save read the same day from the same clock.
2194
+ - `resolveRoutine` (function): function resolveRoutine(config: RoutineConfig): RoutineTimeline — Resolve an authored routine into a timeline driven by the world clock.
2195
+
2126
2196
  ## @jgengine/core/store/defineKeyedStore
2127
2197
 
2128
2198
  - `KeyedStoreHandle` (interface): interface KeyedStoreHandle<T> — A typed, cast-free handle onto a per-owner keyed family of slots on the reactive game store (`ctx.game.store`) — the {@link StoreHandle} shape widened with an `id` (userId, instanceId, …) so one definition covers every owner instead of one `defineStore` call per key. `keyFor` composes the underlying store key from the id; call sites never see it. Writes flow through the engine store, so they bump `ctx.version()`, serialize into a {@link WorldSnapshot}, and replay under host authority — the same guarantees a hand-rolled `` `prefix:${id}` `` string plus `store.get(key) as T` gives up.
@@ -329,6 +329,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
329
329
 
330
330
  - `freezeNarrativeCanon` (function) · `import { freezeNarrativeCanon } from "@jgengine/core/game/narrativeAuthoring"`
331
331
 
332
+ ## near-agents — nav crowd plus per-agent needs and utility brains for on-screen residents
333
+
334
+ - `createNearAgents` (function) · `import { createNearAgents } from "@jgengine/core/gameplay"`
335
+
336
+ ## needs — decaying needs an agent reads by index to decide what it wants next
337
+
338
+ - `createNeedSet` (function) · `import { createNeedSet } from "@jgengine/core/gameplay"`
339
+
332
340
  ## notice-feed — serializable tone-tagged notice/event feed bounded by count and age (toasts, life events, killfeed)
333
341
 
334
342
  - `appendFeed` (function) · `import { appendFeed } from "@jgengine/core/gameplay"`
@@ -343,6 +351,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
343
351
 
344
352
  - `evaluateObjective` (function) · `import { evaluateObjective } from "@jgengine/core/gameplay"`
345
353
 
354
+ ## offscreen-world — schedules agents near the player as real state and far ones as cheap state
355
+
356
+ - `createOffScreenWorld` (function) · `import { createOffScreenWorld } from "@jgengine/core/gameplay"`
357
+
346
358
  ## param-layers — register parameter layers by stable id and resolve serialized selections
347
359
 
348
360
  - `createLayerRegistry` (function) · `import { createLayerRegistry } from "@jgengine/core/gameplay"`
@@ -470,6 +482,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
470
482
 
471
483
  - `createVfxInstanceStore` (function) · `import { createVfxInstanceStore } from "@jgengine/core/gameplay"`
472
484
 
485
+ ## routine-timeline — authored daily routines (home, work, meal) resolved against the world clock
486
+
487
+ - `resolveRoutine` (function) · `import { resolveRoutine } from "@jgengine/core/gameplay"`
488
+
473
489
  ## rule-selection — register rules and bridge a selection to its contributed parameter layers
474
490
 
475
491
  - `createRuleRegistry` (function) · `import { createRuleRegistry } from "@jgengine/core/gameplay"`
@@ -59,7 +59,9 @@ 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.
63
65
 
64
66
  ### Fallback-seam diagnostics
65
67