@jgengine/core 0.18.0 → 0.18.1

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 (174) hide show
  1. package/CHANGELOG.md +114 -2
  2. package/dist/ai/decisionGraph.d.ts +52 -0
  3. package/dist/ai/decisionGraph.js +82 -0
  4. package/dist/ai/mobBrainGraph.d.ts +3 -0
  5. package/dist/ai/mobBrainGraph.js +25 -0
  6. package/dist/ai/tacticalQueries.d.ts +30 -0
  7. package/dist/ai/tacticalQueries.js +45 -0
  8. package/dist/anim/animGraph.d.ts +144 -0
  9. package/dist/anim/animGraph.js +286 -0
  10. package/dist/anim/ikSolver.d.ts +51 -0
  11. package/dist/anim/ikSolver.js +124 -0
  12. package/dist/anim/locomotionGraph.d.ts +26 -0
  13. package/dist/anim/locomotionGraph.js +33 -0
  14. package/dist/assets/spriteAtlas.d.ts +21 -0
  15. package/dist/assets/spriteAtlas.js +103 -0
  16. package/dist/audio/audioFalloff.d.ts +10 -0
  17. package/dist/economy/currency.d.ts +16 -2
  18. package/dist/economy/currency.js +55 -14
  19. package/dist/economy/wallet.d.ts +5 -4
  20. package/dist/economy/wallet.js +8 -4
  21. package/dist/editor/commandApply.js +1 -0
  22. package/dist/editor/commands.d.ts +4 -1
  23. package/dist/editor/document.d.ts +2 -1
  24. package/dist/editor/document.js +89 -0
  25. package/dist/editor/environment.d.ts +3 -0
  26. package/dist/editor/environment.js +16 -0
  27. package/dist/editor/index.d.ts +4 -2
  28. package/dist/editor/index.js +1 -1
  29. package/dist/editor/types.d.ts +16 -0
  30. package/dist/game/cameraConfig.d.ts +8 -0
  31. package/dist/game/chat.d.ts +22 -1
  32. package/dist/game/chat.js +48 -19
  33. package/dist/game/controlGate.d.ts +6 -0
  34. package/dist/game/controlGate.js +30 -1
  35. package/dist/game/defineGame.d.ts +10 -1
  36. package/dist/game/events.d.ts +8 -1
  37. package/dist/game/pixelPerfect.d.ts +25 -0
  38. package/dist/game/pixelPerfect.js +29 -0
  39. package/dist/game/playableGame.d.ts +97 -3
  40. package/dist/game/systemRuntime.js +6 -1
  41. package/dist/input/actionBindings.d.ts +2 -1
  42. package/dist/input/actionBindings.js +10 -8
  43. package/dist/input/actionContexts.d.ts +21 -0
  44. package/dist/input/actionContexts.js +56 -0
  45. package/dist/input/gamepadModel.d.ts +45 -0
  46. package/dist/input/gamepadModel.js +69 -0
  47. package/dist/meta/changelog.d.ts +1 -1
  48. package/dist/meta/changelog.js +85 -1
  49. package/dist/movement/characterController.d.ts +76 -0
  50. package/dist/movement/characterController.js +216 -0
  51. package/dist/movement/freeFlight.d.ts +119 -0
  52. package/dist/movement/freeFlight.js +294 -0
  53. package/dist/movement/playerMovement.d.ts +10 -2
  54. package/dist/movement/playerMovement.js +218 -4
  55. package/dist/movement/serverStep.d.ts +5 -0
  56. package/dist/movement/serverStep.js +6 -0
  57. package/dist/movement/solidObstacles.d.ts +30 -7
  58. package/dist/movement/solidObstacles.js +27 -4
  59. package/dist/multiplayer/poseSyncGate.d.ts +3 -1
  60. package/dist/multiplayer/poseSyncGate.js +9 -1
  61. package/dist/multiplayer/presenceContract.d.ts +6 -0
  62. package/dist/multiplayer/presenceModel.d.ts +2 -0
  63. package/dist/multiplayer/presenceModel.js +11 -0
  64. package/dist/nav/navMesh.d.ts +33 -0
  65. package/dist/nav/navMesh.js +164 -0
  66. package/dist/physics/flightDynamics.d.ts +8 -0
  67. package/dist/physics/kinematicVehicle.d.ts +6 -1
  68. package/dist/physics/physicsBackend.d.ts +191 -0
  69. package/dist/physics/physicsBackend.js +49 -0
  70. package/dist/physics/physicsBackendConformance.d.ts +27 -0
  71. package/dist/physics/physicsBackendConformance.js +117 -0
  72. package/dist/physics/physicsWorldBackend.d.ts +44 -0
  73. package/dist/physics/physicsWorldBackend.js +485 -0
  74. package/dist/physics/worldColliders.d.ts +13 -0
  75. package/dist/physics/worldColliders.js +96 -0
  76. package/dist/random/rng.d.ts +12 -1
  77. package/dist/random/rng.js +20 -3
  78. package/dist/render/environment.d.ts +23 -0
  79. package/dist/render/environment.js +1 -0
  80. package/dist/render/lookPreset.d.ts +11 -5
  81. package/dist/render/lookPreset.js +29 -4
  82. package/dist/render/postProcessing.d.ts +10 -1
  83. package/dist/render/sprite2d.d.ts +38 -0
  84. package/dist/render/sprite2d.js +40 -0
  85. package/dist/runtime/adapter.d.ts +20 -1
  86. package/dist/runtime/adapter.js +8 -1
  87. package/dist/runtime/commandInput.d.ts +15 -0
  88. package/dist/runtime/commandInput.js +13 -0
  89. package/dist/runtime/commandRunner.d.ts +12 -7
  90. package/dist/runtime/commandRunner.js +7 -5
  91. package/dist/runtime/gameContext.js +11 -1
  92. package/dist/runtime/gameContextTypes.d.ts +14 -4
  93. package/dist/runtime/gameRuntime.d.ts +20 -4
  94. package/dist/runtime/gameRuntime.js +17 -4
  95. package/dist/runtime/headlessRunner.d.ts +10 -0
  96. package/dist/runtime/headlessRunner.js +26 -8
  97. package/dist/runtime/hostPolicy.d.ts +3 -2
  98. package/dist/runtime/hostPolicy.js +8 -10
  99. package/dist/runtime/hostedGameRunner.js +34 -6
  100. package/dist/runtime/hostedWorldSession.d.ts +12 -16
  101. package/dist/runtime/hostedWorldSession.js +29 -6
  102. package/dist/runtime/hostedWorldStore.d.ts +20 -0
  103. package/dist/runtime/hostedWorldStore.js +1 -0
  104. package/dist/runtime/inputRecorder.d.ts +30 -0
  105. package/dist/runtime/inputRecorder.js +59 -0
  106. package/dist/runtime/inputSnapshot.d.ts +8 -0
  107. package/dist/runtime/inputSnapshot.js +1 -0
  108. package/dist/runtime/poseInterpolation.d.ts +36 -0
  109. package/dist/runtime/poseInterpolation.js +91 -0
  110. package/dist/runtime/prediction.d.ts +45 -0
  111. package/dist/runtime/prediction.js +62 -0
  112. package/dist/runtime/simContext.d.ts +45 -0
  113. package/dist/runtime/simContext.js +65 -0
  114. package/dist/runtime/simLoop.d.ts +52 -0
  115. package/dist/runtime/simLoop.js +72 -0
  116. package/dist/runtime/simSnapshot.d.ts +32 -0
  117. package/dist/runtime/simSnapshot.js +60 -0
  118. package/dist/runtime/snapshot.d.ts +8 -0
  119. package/dist/runtime/snapshotBuffer.d.ts +33 -0
  120. package/dist/runtime/snapshotBuffer.js +58 -0
  121. package/dist/runtime/transport.d.ts +32 -2
  122. package/dist/runtime/worldChunks.d.ts +2 -0
  123. package/dist/runtime/worldChunks.js +14 -0
  124. package/dist/runtime/worldProjection.d.ts +9 -0
  125. package/dist/runtime/worldProjection.js +17 -1
  126. package/dist/runtime/worldSnapshot.d.ts +6 -0
  127. package/dist/runtime/worldSnapshot.js +3 -0
  128. package/dist/scene/behaviorRuntime.d.ts +19 -1
  129. package/dist/scene/behaviorRuntime.js +55 -4
  130. package/dist/scene/behaviors.d.ts +8 -1
  131. package/dist/scene/builtinSceneKinds.js +10 -0
  132. package/dist/sensor/perception.d.ts +60 -0
  133. package/dist/sensor/perception.js +142 -0
  134. package/dist/settings/graphicsProfile.d.ts +19 -0
  135. package/dist/settings/graphicsProfile.js +36 -0
  136. package/dist/time/accrueSince.d.ts +10 -0
  137. package/dist/time/accrueSince.js +9 -0
  138. package/dist/time/rateWindow.d.ts +16 -0
  139. package/dist/time/rateWindow.js +14 -0
  140. package/dist/time/serverTick.d.ts +21 -3
  141. package/dist/time/serverTick.js +14 -13
  142. package/dist/vfx/particleDirector.d.ts +68 -0
  143. package/dist/vfx/particleDirector.js +81 -0
  144. package/dist/world/buildings.d.ts +12 -0
  145. package/dist/world/buildings.js +43 -3
  146. package/dist/world/cityContent.js +103 -25
  147. package/dist/world/cityKind.d.ts +132 -0
  148. package/dist/world/cityKind.js +315 -15
  149. package/dist/world/features.d.ts +27 -1
  150. package/dist/world/features.js +7 -2
  151. package/dist/world/placement.d.ts +7 -0
  152. package/dist/world/placement.js +2 -0
  153. package/dist/world/placementController.d.ts +1 -1
  154. package/dist/world/placementController.js +7 -0
  155. package/dist/world/raceCircuit.d.ts +168 -0
  156. package/dist/world/raceCircuit.js +797 -0
  157. package/dist/world/terrain.d.ts +5 -1
  158. package/dist/world/terrain.js +5 -0
  159. package/dist/world/territory.d.ts +97 -0
  160. package/dist/world/territory.js +182 -0
  161. package/dist/world.d.ts +5 -3
  162. package/dist/world.js +4 -2
  163. package/package.json +1 -1
  164. package/skills/ce-handoff/SKILL.md +1 -1
  165. package/skills/game-design/SKILL.md +11 -1
  166. package/skills/jgengine/SKILL.md +2 -0
  167. package/skills/jgengine/api.md +94 -19
  168. package/skills/jgengine/capabilities.md +28 -0
  169. package/skills/jgengine-gameplay/SKILL.md +10 -0
  170. package/skills/jgengine-gameplay/api.md +40 -18
  171. package/skills/jgengine-gameplay/capabilities.md +12 -0
  172. package/skills/jgengine-world/SKILL.md +6 -0
  173. package/skills/jgengine-world/api.md +312 -12
  174. package/skills/jgengine-world/capabilities.md +102 -0
@@ -171,6 +171,9 @@
171
171
  - `CurrencyAdjustment` (type): type CurrencyAdjustment = | { success: true; newBalance: number; appliedDelta: number } | { success: false; reason: string } — ⚠ undocumented
172
172
  - `CurrencyDefinition` (interface): interface CurrencyDefinition<TCurrencyId extends string = string> — ⚠ undocumented
173
173
  - `CurrencyOperation` (type): type CurrencyOperation = "add" | "deduct" — ⚠ undocumented
174
+ - `formatCurrency` (function): function formatCurrency(currency: CurrencyDefinition, value: number): string — Format a major-unit value using the currency's declared precision.
175
+ - `fromMinorUnits` (function): function fromMinorUnits(currency: Pick<CurrencyDefinition, "decimals"> | undefined, value: number): number — Convert stored integer minor units to major units for display or existing balance records.
176
+ - `toMinorUnits` (function): function toMinorUnits(currency: Pick<CurrencyDefinition, "decimals"> | undefined, value: number): number — Convert major units to integer minor units, rounding once at the write boundary.
174
177
 
175
178
  ## @jgengine/core/economy/listingBook
176
179
 
@@ -269,13 +272,13 @@
269
272
  - `ChargeResult` (type): type ChargeResult = { status: "ok"; state: WalletState } | { status: "rejected"; reason: "insufficient-funds" } — Outcome of a {@link charge}/{@link chargeAll} attempt: `status: "ok"` carries the debited {@link WalletState}, while `status: "rejected"` leaves the wallet untouched and reports why (currently only `"insufficient-funds"`). Discriminate on `status` before reading `state`.
270
273
  - `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.
271
274
  - `WalletState` (interface): interface WalletState — ⚠ undocumented
272
- - `balance` (function): function balance(state: WalletState, currency: string): number — ⚠ undocumented
275
+ - `balance` (function): function balance(state: WalletState, currency: string | CurrencyDefinition): number — ⚠ undocumented
273
276
  - `canAfford` (function): function canAfford(state: WalletState, costs: Readonly<Record<string, number>>): boolean — True when every currency in `costs` has at least that much balance (a pure, non-mutating check).
274
- - `charge` (function): function charge(state: WalletState, currency: string, amount: number, options?: ChargeOptions): ChargeResult — Deduct `amount`, rejecting when it would leave the balance negative unless `options.overdraft` opts into carrying debt (`true` unlimited, `{ max }` capped) — the strict same-tick affordability check stays the default with `options` omitted.
277
+ - `charge` (function): function charge(state: WalletState, currency: string | CurrencyDefinition, amount: number, options?: ChargeOptions): ChargeResult — Deduct `amount`, rejecting when it would leave the balance negative unless `options.overdraft` opts into carrying debt (`true` unlimited, `{ max }` capped) — the strict same-tick affordability check stays the default with `options` omitted.
275
278
  - `chargeAll` (function): function chargeAll(state: WalletState, costs: Readonly<Record<string, number>>, options?: ChargeOptions): ChargeResult — ⚠ undocumented
276
279
  - `createEmptyWallet` (function): function createEmptyWallet(): WalletState — Hold per-currency balances with affordability checks and charge/grant operations.
277
- - `grant` (function): function grant(state: WalletState, currency: string, amount: number): WalletState — ⚠ undocumented
278
- - `isOverdrawn` (function): function isOverdrawn(state: WalletState, currency: string): boolean — True once `balance(state, currency)` has gone negative under an overdraft-enabled charge.
280
+ - `grant` (function): function grant(state: WalletState, currency: string | CurrencyDefinition, amount: number): WalletState — ⚠ undocumented
281
+ - `isOverdrawn` (function): function isOverdrawn(state: WalletState, currency: string | CurrencyDefinition): boolean — True once `balance(state, currency)` has gone negative under an overdraft-enabled charge.
279
282
 
280
283
  ## @jgengine/core/game/achievements
281
284
 
@@ -323,6 +326,7 @@
323
326
  - `InspectionZoomAnchor` (type): type InspectionZoomAnchor = "target" | "cursor" | "center" — How scroll-zoom re-anchors the view for the inspection rig (#207.7): - `target` — dolly toward the orbit target (classic OrbitControls behavior). - `cursor` — dolly toward the point under the pointer. - `center` — dolly toward the viewport center; equivalent to `target` for an OrbitControls-driven rig, since the camera always faces `target` and that point already projects to the exact center of the viewport.
324
327
  - `LockOnCameraConfig` (interface): interface LockOnCameraConfig — Lock-on / strafe rig (#26) — yaw bound to player→target, move axis becomes strafe.
325
328
  - `ObserverCameraConfig` (interface): interface ObserverCameraConfig — Detached spectator/photo cam (#120) — binds to any entity or fixed point, never reads player input.
329
+ - `PixelPerfectCameraConfig` (interface): interface PixelPerfectCameraConfig — Pixel-art orthographic camera tuning. `pixelsPerUnit` is the authored pixel density.
326
330
  - `PlayerFovConfig` (interface): interface PlayerFovConfig — Player-facing FOV preference applied across every perspective camera rig. Orthographic projections ignore it.
327
331
  - `RtsCameraConfig` (interface): interface RtsCameraConfig extends TopDownCameraConfig — Free-pan / edge-scroll RTS rig (#24) — pan/rotate/zoom independent of any avatar.
328
332
  - `ShoulderCameraConfig` (interface): interface ShoulderCameraConfig — Over-the-shoulder combat rig (#25) — offset, ADS, shoulder swap, decoupled reticle.
@@ -342,13 +346,15 @@
342
346
  - `ChatRecipients` (type): type ChatRecipients = readonly string[] | "all" — ⚠ undocumented
343
347
  - `ChatSendResult` (type): type ChatSendResult = | { message: ChatMessage; recipients: ChatRecipients } | { reason: string } — ⚠ undocumented
344
348
  - `ChatSnapshot` (interface): interface ChatSnapshot — ⚠ undocumented
345
- - `DEFAULT_CHAT_BODY_LENGTH` (const): const DEFAULT_CHAT_BODY_LENGTH: 500 — ⚠ undocumented
349
+ - `ChatValidation` (type): type ChatValidation = { ok: true; text: string } | { ok: false; reason: string } — Sanitized chat text or a displayable validation failure.
350
+ - `DEFAULT_CHAT_BODY_LENGTH` (const): const DEFAULT_CHAT_BODY_LENGTH: 240 — ⚠ undocumented
346
351
  - `DEFAULT_CHAT_HISTORY_LIMIT` (const): const DEFAULT_CHAT_HISTORY_LIMIT: 100 — ⚠ undocumented
347
352
  - `DEFAULT_CHAT_RATE_LIMIT` (const): const DEFAULT_CHAT_RATE_LIMIT: ChatRateLimit — ⚠ undocumented
348
353
  - `DEFAULT_PROXIMITY_CHAT_RADIUS` (const): const DEFAULT_PROXIMITY_CHAT_RADIUS: 20 — ⚠ undocumented
349
354
  - `WHISPER_CHANNEL_PREFIX` (const): const WHISPER_CHANNEL_PREFIX: "whisper:" — ⚠ undocumented
350
355
  - `createChat` (function): function createChat(deps: ChatDeps): Chat — ⚠ undocumented
351
356
  - `createChatRateLimiter` (function): function createChatRateLimiter(limit: ChatRateLimit): ChatRateLimiter — ⚠ undocumented
357
+ - `validateChatMessage` (function): function validateChatMessage(value: unknown, options: { maxLength?: number } = {}): ChatValidation — Strip control characters and enforce the shared chat message policy before storage or broadcast.
352
358
  - `whisperChannelId` (function): function whisperChannelId(a: string, b: string): string — ⚠ undocumented
353
359
 
354
360
  ## @jgengine/core/game/chatFilter
@@ -387,6 +393,8 @@
387
393
  ## @jgengine/core/game/controlGate
388
394
 
389
395
  - `PLAY_CONTROLS_STORE_KEY` (const): const PLAY_CONTROLS_STORE_KEY: "jg.playControls" — ⚠ undocumented
396
+ - `actionContextStack` (function): function actionContextStack(ctx: GameContext): ActionContextStack — Returns the context stack associated with a game context.
397
+ - `activeActionCodes` (function): function activeActionCodes(ctx: GameContext, base: ActionCodesMap): ActionCodesMap — Applies active contexts to a base action map for shell input tracking.
390
398
  - `playControlsActive` (function): function playControlsActive(ctx: GameContext): boolean — ⚠ undocumented
391
399
  - `setPlayControlsActive` (function): function setPlayControlsActive(ctx: GameContext, active: boolean): void — ⚠ undocumented
392
400
 
@@ -419,7 +427,7 @@
419
427
  - `LifecycleConfig` (interface): interface LifecycleConfig<TState = unknown> — Declarative start/restart run lifecycle: the state transitions a game's run phase every genre repeats (title screen → live run → live run → title screen again), expressed as pure functions over one typed {@link StoreHandle} slot instead of hand-rolled `commands.define("start"/"restart")` glue that re-derives phase after every mutation. `start`/`restart` receive the store's own value type — the store's `TState`, never `ctx.game.store.get(key) as T` — and return the next value; the runtime writes it back and derives {@link GamePhase} from it via `phaseOf` in one place, so every adopting game gets identical, correct phase-sync for free.
420
428
  - `LoopPlayer` (interface): interface LoopPlayer — Identity of a player joining or leaving a hosted world — passed to the multiplayer loop hooks.
421
429
  - `PersistConfig` (interface): interface PersistConfig — Tunes offline whole-world save (`defineGameDefinition({ persist })`). Offline games already autosave to `localStorage` by default — pass this only to tune it (or `persist: false` to opt out). Defaults: continuous `autosave` to `localStorage`, one slot, no version.
422
- - `PhysicsConfig` (interface): interface PhysicsConfig — World gravity and jump tuning, plus scene-object collision opt-ins, for the game's physics step.
430
+ - `PhysicsConfig` (interface): interface PhysicsConfig — World gravity and jump tuning, plus the optional rigid-body backend and character capsule.
423
431
  - `defineGameDefinition` (function): function defineGameDefinition<TAssetRef extends ModelAssetRef, TMultiplayer>(config: GameDefinitionConfig<TAssetRef, TMultiplayer>): GameDefinition<TAssetRef, TMultiplayer> — Host-level constructor for a bare `GameDefinition`: fills in `scene` and default `assets`, validates `name`, OR-merges `features` from installed systems, and composes `loop` from `systems` + any classic hooks. Games author through `defineGame` from `@jgengine/shell/defineGame`, which calls this and adds the presentation layer; call this directly only from headless hosts that mount no shell.
424
432
 
425
433
  ## @jgengine/core/game/defineSystem
@@ -460,6 +468,7 @@
460
468
 
461
469
  ## @jgengine/core/game/events
462
470
 
471
+ - `AnimationClipEvent` (interface): interface AnimationClipEvent — A named clip moment (`AnimGraph.events`) the rig just crossed — foot plant, hit frame, reload point. Audio and combat listen by `name`.
463
472
  - `AudioLoopSetEvent` (interface): interface AudioLoopSetEvent — Live-update the retained loop `id`: `rate` re-pitches it (1 = authored, the shell clamps to 0.25–4), `gain` rescales its volume (0–1), and `at` repositions its emitter. Emitted every tick to track a live signal (RPM, tire slip); the shell smooths rate/gain and ignores an unknown `id` (#1051).
464
473
  - `AudioLoopStartEvent` (interface): interface AudioLoopStartEvent — Start (or idempotently keep) the retained, id-keyed audio loop `id` from catalog `sound`, optionally anchored at world `at`. Restarting with the same `sound` does not restart the source (no click); a different `sound` replaces it. Drives RPM-pitched engine loops and slip-scaled tire squeal (#1051).
465
474
  - `AudioLoopStopEvent` (interface): interface AudioLoopStopEvent — Stop and dispose the retained loop `id`; an unknown `id` is ignored (#1051).
@@ -474,7 +483,7 @@
474
483
  - `CosmeticsChangedEvent` (interface): interface CosmeticsChangedEvent — ⚠ undocumented
475
484
  - `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.
476
485
  - `EmotePlayedEvent` (interface): interface EmotePlayedEvent — ⚠ undocumented
477
- - `EntityAnimationEvent` (interface): interface EntityAnimationEvent — Request that an entity's rig play a one-shot animation clip bound to `event` in its `animation.oneShots` (e.g. an "attack" swing); the shell resolves the clip and plays it once over the locomotion state.
486
+ - `EntityAnimationEvent` (interface): interface EntityAnimationEvent — Request that an entity's rig play a one-shot animation clip bound to `event` in its `animation.oneShots` (e.g. an "attack" swing); the shell resolves the clip and plays it once over the locomotion state. With an `animation.graph`, `event` arms the trigger of that name.
478
487
  - `EntityDiedEvent` (interface): interface EntityDiedEvent — ⚠ undocumented
479
488
  - `EntityFloatTextEvent` (interface): interface EntityFloatTextEvent — ⚠ undocumented
480
489
  - `FormChangedEvent` (interface): interface FormChangedEvent — ⚠ undocumented
@@ -677,6 +686,13 @@
677
686
  - `classifyPing` (function): function classifyPing(hit: PointerHit, deps: PingClassifyDeps = {}, options: PingClassifyOptions = {}): PingCategory — Classify what a pointer/aim ray hit into a ping category. Entity hits resolve by catalog role (hostile → enemy, else location); object hits by an optional catalog category tag; open ground is a location ping.
678
687
  - `createPingSystem` (function): function createPingSystem(deps: PingSystemDeps): PingSystem — Contextual ping/marker communication between teammates, classified by what was pinged.
679
688
 
689
+ ## @jgengine/core/game/pixelPerfect
690
+
691
+ - `PixelPerfectFrustum` (interface): interface PixelPerfectFrustum — The orthographic bounds and scale derived from a pixel-perfect camera config.
692
+ - `PixelPerfectViewport` (interface): interface PixelPerfectViewport — Viewport dimensions in device-independent canvas pixels.
693
+ - `pixelPerfectFrustum` (function): function pixelPerfectFrustum(viewport: PixelPerfectViewport, pixelsPerUnit: number, integerScale = false): PixelPerfectFrustum — Computes an orthographic frustum whose world units map to the requested pixel density. Integer scaling chooses the largest whole-number density multiplier available in the viewport.
694
+ - `snapPixelPerfectPosition` (function): function snapPixelPerfectPosition<T extends { x: number; y: number; z: number }>(position: T, pixelsPerUnit: number): T — Snaps a camera position to the authored pixel grid without changing its orientation.
695
+
680
696
  ## @jgengine/core/game/playableGame
681
697
 
682
698
  - `AmbientLightingConfig` (interface): interface AmbientLightingConfig — ⚠ undocumented
@@ -694,6 +710,7 @@
694
710
  - `DirectionalLightingConfig` (interface): interface DirectionalLightingConfig — ⚠ undocumented
695
711
  - `EntitySpriteConfig` (interface): interface EntitySpriteConfig — ⚠ undocumented
696
712
  - `FirstPersonCameraConfig` (interface): interface FirstPersonCameraConfig — ⚠ undocumented
713
+ - `FlightConfig` (interface): interface FlightConfig — Free-flight families moved into the walk controller so one seam covers ground + air — creative/spectator/noclip/hover are weightless, aircraft/rotorcraft stay on `flightDynamics`.
697
714
  - `GameCameraConfig` (interface): interface GameCameraConfig — Camera tuning for the shell's rig stack: pick the rig via `rig`, then tune it through its matching config block. All fields optional — the default is the third-person orbit rig.
698
715
  - `GameCaptureConfig` (interface): interface GameCaptureConfig — How a game makes itself screenshottable: the commands that reach live play or a named screen, the framings worth re-capturing, and the progress metrics a bot playtest samples. Declaring these once is what lets a capture host reproduce a view instead of re-deriving it by hand every run.
699
716
  - `GameCaptureView` (interface): interface GameCaptureView — How a screenshot host reaches live gameplay in this game — the data behind `shoot --mode play`.
@@ -707,7 +724,7 @@
707
724
  - `ModelAttachment` (interface): interface ModelAttachment — Parents a prop/weapon model to a named bone or node on the host model's rig — a sword on `handslot.r`, a spellbook offhand — following the bone's animated transform each frame.
708
725
  - `ModelConfig` (interface): interface ModelConfig — ⚠ undocumented
709
726
  - `ModelMaterialMaps` (interface): interface ModelMaterialMaps — Real PBR map URLs (e.g. `buildMaterialCatalog(...).resolve(id)!.maps` from `@jgengine/assets`) layered onto a model's material — the seam for texturing an otherwise-flat/untextured GLB. Any role may be omitted to keep the model's own map.
710
- - `ModelMaterialOverride` (interface): interface ModelMaterialOverride — Per-entity PBR material override (#151.3) applied to every `MeshStandardMaterial` in the model's cloned scene graph.
727
+ - `ModelMaterialOverride` (interface): interface ModelMaterialOverride — Per-entity PBR material override (#151.3) applied to every standard or physical material in the model's cloned scene graph.
711
728
  - `ModelPart` (interface): interface ModelPart — Static child model stacked at a fixed local offset under its parent's transform — no bone/rig resolution, unlike `ModelAttachment`. Assembles a compound entity (e.g. a modular castle wall + tower + roof) from several single-mesh kit pieces.
712
729
  - `ModelRimLight` (interface): interface ModelRimLight — Fresnel rim-light term added on top of a model's lighting — see {@link ModelMaterialOverride.rim}.
713
730
  - `MovementCommitFrame` (interface): interface MovementCommitFrame — One frame's movement resolution handed to `PlayerMovementConfig.beforeCommit`.
@@ -716,11 +733,13 @@
716
733
  - `PlayableGame` (interface): interface PlayableGame<TUi = unknown, TWorldOverlay = unknown, TRenderEntity = never, TRenderObject = never, TViewmodel = unknown, TOverlay = TWorldOverlay> — ⚠ undocumented
717
734
  - `PlayerFovConfig` (interface): interface PlayerFovConfig — Player-facing FOV preference applied across every perspective camera rig. Orthographic projections ignore it.
718
735
  - `PlayerMovementConfig` (interface): interface PlayerMovementConfig — Movement-control levers for the shell-driven local player walk controller.
736
+ - `PointLightingConfig` (interface): interface PointLightingConfig — A bounded dynamic point light in world space.
719
737
  - `PointerConfig` (interface): interface PointerConfig — ⚠ undocumented
720
738
  - `PresentationEffectsConfig` (interface): interface PresentationEffectsConfig — Per-channel combat presentation toggles for the 3D shell canvas. Missing keys default to enabled when the parent `presentationEffects` is an object.
721
739
  - `RtsCameraConfig` (interface): interface RtsCameraConfig extends TopDownCameraConfig — Free-pan / edge-scroll RTS rig (#24) — pan/rotate/zoom independent of any avatar.
722
740
  - `ShoulderCameraConfig` (interface): interface ShoulderCameraConfig — Over-the-shoulder combat rig (#25) — offset, ADS, shoulder swap, decoupled reticle.
723
741
  - `SideScrollCameraConfig` (interface): interface SideScrollCameraConfig — Fixed lateral 2.5D follow (side-on platformer cam): the camera sits perpendicular to the travel axis, tracks the followed entity, and never reads player look input.
742
+ - `SpotLightingConfig` (interface): interface SpotLightingConfig extends PointLightingConfig — A bounded dynamic spot light in world space.
724
743
  - `TopDownCameraConfig` (interface): interface TopDownCameraConfig — Fixed top-down / isometric rig (#23) — height/pitch/yaw + decoupled follow.
725
744
  - `TurntableCameraConfig` (interface): interface TurntableCameraConfig — Turntable / showcase rig — slowly auto-orbits a fixed world point (no player input), the way a museum turntable rotates an object on display. A flat, self-describing spelling of `observer`'s point-orbit mode: `target` names the point directly instead of `bind: { kind: "point", position }`. Set `camera.turntable` and the rig is inferred — you don't also write `rig`.
726
745
  - `VoxelCollisionConfig` (interface): interface VoxelCollisionConfig — Player-vs-world collision for the first-person controller. Without this the shell keeps the player on flat ground at y=0. With `voxel: true` the shell resolves the player as a box against placed scene objects (each treated as a solid unit cell), so they stand on blocks, fall into holes, and are stopped by walls — the controller a block-building/mining game needs.
@@ -1070,7 +1089,7 @@
1070
1089
  - `CropTileState` (interface): interface CropTileState — ⚠ undocumented
1071
1090
  - `CrossThresholdsOptions` (interface): interface CrossThresholdsOptions — Exact-boundary and dead-band policy for {@link crossThresholds}.
1072
1091
  - `Curve` (type): type Curve = CurveDef & CurveShape — A fully specified progression curve — a {@link CurveDef} growth shape plus optional {@link CurveShape} rounding/clamp.
1073
- - `DEFAULT_CHAT_BODY_LENGTH` (const): const DEFAULT_CHAT_BODY_LENGTH: 500 — ⚠ undocumented
1092
+ - `DEFAULT_CHAT_BODY_LENGTH` (const): const DEFAULT_CHAT_BODY_LENGTH: 240 — ⚠ undocumented
1074
1093
  - `DEFAULT_CHAT_HISTORY_LIMIT` (const): const DEFAULT_CHAT_HISTORY_LIMIT: 100 — ⚠ undocumented
1075
1094
  - `DEFAULT_CHAT_RATE_LIMIT` (const): const DEFAULT_CHAT_RATE_LIMIT: ChatRateLimit — ⚠ undocumented
1076
1095
  - `DEFAULT_FIXED_STAGES` (const): const DEFAULT_FIXED_STAGES: readonly ["input", "movement", "combat", "ai", "activities", "cleanup"] — Default fixed-sim stage order — systems pick a stage; most need only this.
@@ -1188,7 +1207,7 @@
1188
1207
  - `LootTableDef` (interface): interface LootTableDef — A named, validated loot table — its roll count, weighted-vs-independent mode, and candidate entries.
1189
1208
  - `ModelConfig` (interface): interface ModelConfig — ⚠ undocumented
1190
1209
  - `ModelMaterialMaps` (interface): interface ModelMaterialMaps — Real PBR map URLs (e.g. `buildMaterialCatalog(...).resolve(id)!.maps` from `@jgengine/assets`) layered onto a model's material — the seam for texturing an otherwise-flat/untextured GLB. Any role may be omitted to keep the model's own map.
1191
- - `ModelMaterialOverride` (interface): interface ModelMaterialOverride — Per-entity PBR material override (#151.3) applied to every `MeshStandardMaterial` in the model's cloned scene graph.
1210
+ - `ModelMaterialOverride` (interface): interface ModelMaterialOverride — Per-entity PBR material override (#151.3) applied to every standard or physical material in the model's cloned scene graph.
1192
1211
  - `ModularItemDef` (interface): interface ModularItemDef — ⚠ undocumented
1193
1212
  - `Moodle` (interface): interface Moodle — One survival moodle (status icon) — severity, source, and label for HUD chips.
1194
1213
  - `MoodleStack` (interface): interface MoodleStack — Ordered stack of active moodles derived from meters/ailments/buffs.
@@ -1216,7 +1235,7 @@
1216
1235
  - `Party` (interface): interface Party — ⚠ undocumented
1217
1236
  - `PartyInviteEntry` (interface): interface PartyInviteEntry — ⚠ undocumented
1218
1237
  - `PartyMemberEntry` (interface): interface PartyMemberEntry — ⚠ undocumented
1219
- - `PhysicsConfig` (interface): interface PhysicsConfig — World gravity and jump tuning, plus scene-object collision opt-ins, for the game's physics step.
1238
+ - `PhysicsConfig` (interface): interface PhysicsConfig — World gravity and jump tuning, plus the optional rigid-body backend and character capsule.
1220
1239
  - `PingCategory` (type): type PingCategory = string — ⚠ undocumented
1221
1240
  - `PingSystem` (interface): interface PingSystem — ⚠ undocumented
1222
1241
  - `PlayableGame` (interface): interface PlayableGame<TUi = unknown, TWorldOverlay = unknown, TRenderEntity = never, TRenderObject = never, TViewmodel = unknown, TOverlay = TWorldOverlay> — ⚠ undocumented
@@ -1401,7 +1420,7 @@
1401
1420
  - `applyBindingOverrides` (function): function applyBindingOverrides<TAction extends string, TCode extends string>(input: ActionCodesMap<TAction, TCode>, overrides: BindingOverrides): ActionCodesMap<TAction, TCode> — Merge player rebinds over a game's authored `input` map. Only actions the game already declares can be overridden; unknown override keys are ignored so a stale localStorage entry can't inject phantom actions.
1402
1421
  - `applySetBonuses` (function): function applySetBonuses(stats: Record<string, number>, bonuses: readonly SetBonus[]): Record<string, number> — Fold a set of active bonuses' additive stats onto a stat map, returning a new map (the input is not mutated).
1403
1422
  - `applyWear` (function): function applyWear(state: DurabilityState, amount: number): DurabilityState — Apply wear to an item, tracking breakage and repair eligibility.
1404
- - `balance` (function): function balance(state: WalletState, currency: string): number — ⚠ undocumented
1423
+ - `balance` (function): function balance(state: WalletState, currency: string | CurrencyDefinition): number — ⚠ undocumented
1405
1424
  - `balanceOf` (function): function balanceOf(ledger: ResourceLedger, account: string, currency: string): number — Read a single balance; unknown account/currency pairs read as `0`.
1406
1425
  - `canAfford` (function): function canAfford(state: WalletState, costs: Readonly<Record<string, number>>): boolean — True when every currency in `costs` has at least that much balance (a pure, non-mutating check).
1407
1426
  - `canCraft` (function): function canCraft(state: InventoryState, layout: InventoryLayout, traits: ItemTraits, recipe: RecipeDef, context: CraftContext = {}): CraftCheck — ⚠ undocumented
@@ -1410,7 +1429,7 @@
1410
1429
  - `candidateViolatesForbid` (function): function candidateViolatesForbid(partial: ItemIdentity, candidate: CandidatePlacement, rules: readonly CompatibilityRule[]): ForbidRule | null — The generic backtracking contract for procedural generation (see #908): given a partial identity and a candidate part, return the first forbid rule the placement would violate, or null if it stays viable. Require rules are ignored here because they may still be satisfied by a later placement.
1411
1430
  - `capAmount` (function): function capAmount(max: number | ((ctx: PolicyContext) => number)): ResourcePolicy — Clamp a transaction's amount to at most `max` (a fixed number or a function of context).
1412
1431
  - `captureProvenance` (function): function captureProvenance(identity: ItemIdentity, activeBonuses: readonly SetBonus[], seed?: number): ItemProvenance — Capture the provenance of a generated item — family, tags, per-slot parts, active bonus ids, and optional seed — as a JSON-safe record.
1413
- - `charge` (function): function charge(state: WalletState, currency: string, amount: number, options?: ChargeOptions): ChargeResult — Deduct `amount`, rejecting when it would leave the balance negative unless `options.overdraft` opts into carrying debt (`true` unlimited, `{ max }` capped) — the strict same-tick affordability check stays the default with `options` omitted.
1432
+ - `charge` (function): function charge(state: WalletState, currency: string | CurrencyDefinition, amount: number, options?: ChargeOptions): ChargeResult — Deduct `amount`, rejecting when it would leave the balance negative unless `options.overdraft` opts into carrying debt (`true` unlimited, `{ max }` capped) — the strict same-tick affordability check stays the default with `options` omitted.
1414
1433
  - `chargeAll` (function): function chargeAll(state: WalletState, costs: Readonly<Record<string, number>>, options?: ChargeOptions): ChargeResult — ⚠ undocumented
1415
1434
  - `clampValue` (function): function clampValue(value: number, bounds?: NumericBounds): number — Clamp a scalar to `bounds` (identity when `bounds` is omitted). Pure — touches no record.
1416
1435
  - `clearBindingOverride` (function): function clearBindingOverride(gameId: string, action: string, storage: Pick<WebStorageLike, "getItem" | "setItem" | "removeItem"> | null | undefined = defaultStorage()): BindingOverrides — ⚠ undocumented
@@ -1519,7 +1538,7 @@
1519
1538
  - `generate` (function): function generate(schema: GenSchema, rng: () => number, options: GenerateOptions = {}): GenOutcome — Run a caller-defined {@link GenSchema} against an injected `rng` into a deterministic, serializable {@link GenResult} with full provenance. Composes weighted/uniform choice, dependent choice, constraints with bounded backtracking, field transforms, and validation reroll over plain data — the generic seam procedural loot, affix, and modular-part rollers assemble on. Identical schema, seed, and pins reproduce an identical result across server/client and save/load.
1520
1539
  - `getRuleEffect` (function): function getRuleEffect(id: string): RuleEffectDefinition | undefined — Look up a declared rule effect, or `undefined` when the id was never registered — lets callers reject unknown effect references in authored content.
1521
1540
  - `getValue` (function): function getValue(record: Record<string, number>, key: string, fallback = 0): number — Current value for `key`, or `fallback` (default `0`) when the record has no entry.
1522
- - `grant` (function): function grant(state: WalletState, currency: string, amount: number): WalletState — ⚠ undocumented
1541
+ - `grant` (function): function grant(state: WalletState, currency: string | CurrencyDefinition, amount: number): WalletState — ⚠ undocumented
1523
1542
  - `identityOf` (function): function identityOf(family: string, tags: readonly string[], parts: readonly InstalledPart[]): ItemIdentity — Assemble an {@link ItemIdentity} from a family, tags, and installed parts.
1524
1543
  - `idleRaceSession` (function): function idleRaceSession(): RaceSessionState — The pre-race session on the grid: `idle`, both clocks at zero. Call {@link startRaceCountdown} to light the lights, or hold here until the field is ready.
1525
1544
  - `initDecayMeters` (function): function initDecayMeters(defs: readonly DecayMeterConfig[]): DecayMeterValues — Starting values for `defs` — each meter's `start ?? max`, clamped to its range. Seed a serialized state record with this instead of holding a {@link createDecayMeterSet} closure.
@@ -1528,7 +1547,7 @@
1528
1547
  - `isComplete` (function): function isComplete(def: ModularItemDef, installed: readonly InstalledPart[]): boolean — ⚠ undocumented
1529
1548
  - `isDisabled` (function): function isDisabled(spec: DurabilitySpec, state: DurabilityState): boolean — ⚠ undocumented
1530
1549
  - `isIdentityValid` (function): function isIdentityValid(identity: ItemIdentity, rules: readonly CompatibilityRule[]): boolean — Convenience predicate: true when {@link validateIdentity} finds no violations.
1531
- - `isOverdrawn` (function): function isOverdrawn(state: WalletState, currency: string): boolean — True once `balance(state, currency)` has gone negative under an overdraft-enabled charge.
1550
+ - `isOverdrawn` (function): function isOverdrawn(state: WalletState, currency: string | CurrencyDefinition): boolean — True once `balance(state, currency)` has gone negative under an overdraft-enabled charge.
1532
1551
  - `jobById` (function): function jobById<TSpec, TReserve>(state: WorkQueueState<TSpec, TReserve>, id: JobId): Job<TSpec, TReserve> | null — Look up a job by id, or `null` if absent/terminal.
1533
1552
  - `jobProgress` (function): function jobProgress<TSpec, TReserve>(job: Job<TSpec, TReserve>): number — Fractional progress of a job (0…1); a zero-duration job reads as complete.
1534
1553
  - `lapDurations` (function): function lapDurations(splits: readonly number[], gatesPerLap: number): number[] — Per-lap durations from a cumulative split book with `gatesPerLap` checkpoints per lap — each lap's time is its finish-gate split minus the previous lap's finish. Only complete laps are returned.
@@ -1585,7 +1604,7 @@
1585
1604
  - `ringSampleAt` (function): function ringSampleAt(config: RingConfig, time: number): RingSample — ⚠ undocumented
1586
1605
  - `runPipeline` (function): function runPipeline<V>(base: V, modifiers: readonly Modifier<V>[], equals: (a: V, b: V) => boolean = Object.is): PipelineResult<V> — ⚠ undocumented
1587
1606
  - `saveBindingOverride` (function): function saveBindingOverride(gameId: string, action: string, codes: ActionCodes, storage: Pick<WebStorageLike, "getItem" | "setItem" | "removeItem"> | null | undefined = defaultStorage()): BindingOverrides — ⚠ undocumented
1588
- - `seededRng` (function): function seededRng(seed: string | number): () => number — Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
1607
+ - `seededRng` (function): function seededRng(seed: string | number): SeededRng — Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
1589
1608
  - `seededStreams` (function): function seededStreams(seed: string | number): (stream: string) => () => number — Derives independent, deterministic {@link seededRng} streams from one base seed, keyed by stream name.
1590
1609
  - `selectDialogueView` (function): function selectDialogueView(graph: DialogueGraph, nodeId: string): DialogueGraphView | null — Project a {@link DialogueGraph} node id to its render-ready {@link DialogueGraphView} — the pure "current view" selector a stateless renderer reads (speaker, line, choices, done). Returns `null` when the id is not in the graph. No traversal or mutation.
1591
1610
  - `selectRules` (function): function selectRules<TPayload = unknown>(pool: readonly RuleDef<TPayload>[], config: RuleSelectionConfig): RuleSelection<TPayload> — Select up to `count` rules from `pool` deterministically from `config.seed`. Locked ids are placed first (in order), then remaining slots are filled by weighted draw from tag-filtered, still-eligible candidates, honoring `requires`/`conflicts` after each pick. Each slot draws from an independent seed stream keyed by slot index, so a later slot's outcome never shifts an earlier one. Selection stops early when no compatible candidate remains. Pure given `pool` + `config`.
@@ -1698,7 +1717,7 @@
1698
1717
  - `RolledItem` (interface): interface RolledItem — ⚠ undocumented
1699
1718
  - `RollerConfig` (interface): interface RollerConfig — ⚠ undocumented
1700
1719
  - `createAffixRoller` (function): function createAffixRoller(config: RollerConfig): AffixRoller — ⚠ undocumented
1701
- - `seededRng` (function): function seededRng(seed: string | number): () => number — Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
1720
+ - `seededRng` (function): function seededRng(seed: string | number): SeededRng — Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
1702
1721
 
1703
1722
  ## @jgengine/core/item/durability
1704
1723
 
@@ -1873,9 +1892,12 @@
1873
1892
  ## @jgengine/core/random/rng
1874
1893
 
1875
1894
  - `RandomSeed` (type): type RandomSeed = number & { readonly __randomSeed: unique symbol } — Opaque, serializable PRNG cursor for state machines that must persist their own random stream (a spawn director, a heat/pursuit meter) instead of holding a closure — the state round-trips through save/load and multiplayer sync, so it can't carry a function. Never read or do arithmetic on the raw value directly; thread it through {@link stepRandomSeed} only.
1895
+ - `SeededRng` (interface): interface SeededRng — A {@link seededRng} stream: callable like any `() => number`, with its cursor readable and restorable for snapshots and replay.
1876
1896
  - `hashString` (function): function hashString(text: string): number — Deterministic 32-bit FNV-1a hash of a string → unsigned int. Same text, same number, on every platform — the stable seed behind per-id jitter, spread offsets, and content-addressed variation.
1877
1897
  - `randomSeedFrom` (function): function randomSeedFrom(seed: number): RandomSeed — Wraps an already-integer seed (e.g. a `config.seed`) as a {@link RandomSeed} — no hashing.
1878
- - `seededRng` (function): function seededRng(seed: string | number): () => number — Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
1898
+ - `restoreRng` (function): function restoreRng(rng: () => number, cursor: number): void — Restore a seeded stream's cursor; a no-op for generators that carry no state.
1899
+ - `rngStateOf` (function): function rngStateOf(rng: () => number): number | null — The cursor of a seeded stream, or `null` for a generator that carries no state (an injected closure, `Math.random`).
1900
+ - `seededRng` (function): function seededRng(seed: string | number): SeededRng — Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
1879
1901
  - `seededStreams` (function): function seededStreams(seed: string | number): (stream: string) => () => number — Derives independent, deterministic {@link seededRng} streams from one base seed, keyed by stream name.
1880
1902
  - `stepRandomSeed` (function): function stepRandomSeed(seed: RandomSeed): readonly [value: number, next: RandomSeed] — One step of the {@link seededRng} recurrence in pure (seed in, seed out) form — the same mulberry32-style generator, shared by every state machine that persists its own PRNG cursor instead of closing over a generator.
1881
1903
 
@@ -65,6 +65,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
65
65
 
66
66
  - `breedOffspring` (function) · `import { breedOffspring } from "@jgengine/core/game/breeding"`
67
67
 
68
+ ## currency-major-units — convert safe minor-unit integers into currency amounts
69
+
70
+ - `fromMinorUnits` (function) · `import { fromMinorUnits } from "@jgengine/core/economy/currency"`
71
+
72
+ ## currency-minor-units — round decimal currency into safe integer minor units
73
+
74
+ - `toMinorUnits` (function) · `import { toMinorUnits } from "@jgengine/core/economy/currency"`
75
+
76
+ ## currency-precision-format — display a currency using its declared decimal precision
77
+
78
+ - `formatCurrency` (function) · `import { formatCurrency } from "@jgengine/core/economy/currency"`
79
+
68
80
  ## decay-meter — survival meters that drain/refill over game time (hunger, water, oxygen, stamina)
69
81
 
70
82
  - `createDecayMeterSet` (function) · `import { createDecayMeterSet } from "@jgengine/core/gameplay"`
@@ -54,3 +54,9 @@ Render seams fall back to placeholders when content is unauthored: default green
54
54
  - Generated environments still need deterministic data assertions; authored environments need scene-document assertions.
55
55
  - Audio files and licensing belong to `jgengine-assets`; spatial playback policy belongs here.
56
56
 
57
+
58
+ ### Territory and placement
59
+
60
+ Use `world/territory` for chunk-backed ownership: `planFootprintClaims` prices previews, `placeWithTerritory` commits claims and object placement atomically, and `createTerritory` binds host-owned snapshot storage to `ctx.game.territory`. Use `territoryChunkKeys` to load every footprint and gap-neighbor chunk before planning; never interpret an unloaded neighborhood as vacant. Claims persist in `RuntimeChunkRow.territory`; `RuntimePlayerRow.territoryOwnedCount` preserves the price curve and `ownedTerritoryChunkKeys` bounds owner queries. Convex hosts use `loadTerritoryState`/`persistTerritoryState` from `@jgengine/convex/territory` to commit dirty claim chunks/profiles without a shared-server write. Grant `starterBlock` before the first placement verb. Placement previews must show the claim cost and affordability on the placement surface.
61
+
62
+ Use `chunkKeysAround(position, rings)` for bounded command/presence reads; declare `commands[name].scope(input)` for every hosted command. Missing scopes load only the actor and no chunks. A whole-world command must opt in explicitly with `scope: () => ({})` and justify its read budget.