@jgengine/shell 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/dist/GameUiPreview.js +3 -1
  3. package/dist/Shell3dPresentation.js +6 -4
  4. package/dist/ShellChrome.js +3 -1
  5. package/dist/camera/GameCameraRig.js +2 -2
  6. package/dist/camera/GameFirstPersonCamera.js +39 -2
  7. package/dist/camera/cameraCollision.d.ts +66 -1
  8. package/dist/camera/cameraCollision.js +72 -1
  9. package/dist/camera/index.d.ts +1 -1
  10. package/dist/camera/index.js +1 -1
  11. package/dist/camera/rigResolve.d.ts +13 -0
  12. package/dist/camera/rigResolve.js +16 -0
  13. package/dist/environment/EnvironmentScene.js +69 -4
  14. package/dist/postfx/shadowPrepass.d.ts +8 -1
  15. package/dist/postfx/shadowPrepass.js +18 -2
  16. package/dist/postfx/stylizePass.js +2 -1
  17. package/dist/render/CascadedShadows.d.ts +31 -1
  18. package/dist/render/CascadedShadows.js +71 -5
  19. package/dist/render/CharacterCrowd.d.ts +20 -0
  20. package/dist/render/CharacterCrowd.js +176 -0
  21. package/dist/render/SceneLighting.js +27 -3
  22. package/dist/render/SceneWarmupGate.d.ts +20 -0
  23. package/dist/render/SceneWarmupGate.js +48 -0
  24. package/dist/render/characterLod.d.ts +62 -0
  25. package/dist/render/characterLod.js +165 -0
  26. package/dist/render/crowdRegistry.d.ts +48 -0
  27. package/dist/render/crowdRegistry.js +97 -0
  28. package/dist/render/materialAsset.js +2 -1
  29. package/dist/render/modelLoad.js +3 -0
  30. package/dist/render/modelMeshMerge.d.ts +49 -0
  31. package/dist/render/modelMeshMerge.js +269 -0
  32. package/dist/render/shaderWarmup.d.ts +69 -0
  33. package/dist/render/shaderWarmup.js +120 -0
  34. package/dist/render/shadowCasters.d.ts +28 -0
  35. package/dist/render/shadowCasters.js +95 -0
  36. package/dist/render/staticTransforms.d.ts +20 -0
  37. package/dist/render/staticTransforms.js +32 -0
  38. package/dist/render/useFootIk.d.ts +11 -3
  39. package/dist/render/useFootIk.js +35 -7
  40. package/dist/render/useModelAnimation.d.ts +16 -2
  41. package/dist/render/useModelAnimation.js +73 -4
  42. package/dist/render/useModelInstance.d.ts +1 -1
  43. package/dist/render/useModelInstance.js +23 -0
  44. package/dist/render/useRagdoll.d.ts +32 -0
  45. package/dist/render/useRagdoll.js +105 -0
  46. package/dist/settings/SettingsMenu.d.ts +1 -1
  47. package/dist/settings/SettingsMenu.js +10 -79
  48. package/dist/settings/settingsController.js +3 -0
  49. package/dist/world/StaticObjectModels.d.ts +13 -0
  50. package/dist/world/StaticObjectModels.js +28 -3
  51. package/dist/world/WorldParticles.d.ts +2 -1
  52. package/dist/world/WorldParticles.js +29 -10
  53. package/dist/world/WorldScene.js +50 -13
  54. package/dist/world/entityPose.d.ts +17 -0
  55. package/dist/world/entityPose.js +42 -0
  56. package/dist/world/staticObjectBatches.d.ts +33 -5
  57. package/dist/world/staticObjectBatches.js +40 -6
  58. package/package.json +4 -4
  59. package/skills/jgengine/api.md +2 -2
  60. package/skills/jgengine-ui/SKILL.md +1 -0
  61. package/skills/jgengine-ui/api.md +200 -9
  62. package/skills/jgengine-ui/capabilities.md +103 -1
  63. package/skills/jgengine-ui/reference-primitives.md +2 -0
  64. package/skills/jgengine-ui/reference.md +8 -0
  65. package/skills/jgengine-world/SKILL.md +4 -0
  66. package/skills/jgengine-world/api.md +128 -6
  67. package/skills/jgengine-world/capabilities.md +41 -1
  68. package/skills/jgengine-world/recipes/character-animation.md +25 -2
  69. package/skills/jgengine-world/reference.md +1 -1
@@ -8,6 +8,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
8
8
 
9
9
  - `AccessibilityProvider` (function) · `import { AccessibilityProvider } from "@jgengine/react"`
10
10
 
11
+ ## accessibility-settings — read accessibility preferences and the hold-vs-toggle choice from the shared settings store
12
+
13
+ - `readAccessibilitySettings` (function) · `import { readAccessibilitySettings } from "@jgengine/core/settings/accessibilitySettings"`
14
+
15
+ ## accessibility-settings-rows — accessibility setting rows (text scale, colorblind, reduced motion, high contrast, captions, hold-vs-toggle) backed by the shared settings store
16
+
17
+ - `accessibilitySettingRows` (function) · `import { accessibilitySettingRows } from "@jgengine/core/settings/accessibilitySettings"`
18
+
11
19
  ## accessibility-store — serializable, observable accessibility preferences (reduced motion, high contrast, text scale, colorblind mode, captions) a game binds to its settings and presentation
12
20
 
13
21
  - `createAccessibilityStore` (function) · `import { createAccessibilityStore } from "@jgengine/core/ui"`
@@ -44,6 +52,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
44
52
 
45
53
  - `ActionTooltip` (function) · `import { ActionTooltip } from "@jgengine/react"`
46
54
 
55
+ ## animation — turn a killed rigged character into a physics ragdoll on death
56
+
57
+ - `useRagdoll` (function) · `import { useRagdoll } from "@jgengine/shell/render/useRagdoll"`
58
+
47
59
  ## animation-motion-params — read resolved player movement into reusable animation graph parameters
48
60
 
49
61
  - `readModelAnimationParams` (function) · `import { readModelAnimationParams } from "@jgengine/shell/render/useModelAnimation"`
@@ -85,6 +97,11 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
85
97
 
86
98
  - `observeBrowserSuspension` (function) · `import { observeBrowserSuspension } from "@jgengine/react"`
87
99
 
100
+ ## camera-clearance — keep any camera out of world collision with a spring-arm pull-in
101
+
102
+ - `clearCameraBoom` (function) · `import { clearCameraBoom } from "@jgengine/shell/camera"`
103
+ - `sceneCameraQuery` (function) · `import { sceneCameraQuery } from "@jgengine/shell/camera"`
104
+
88
105
  ## camera-shake-meter — HUD readout for a core camera-shake controller — a trauma meter plus current impact-kind label, reskinnable and kind-labelled
89
106
 
90
107
  - `CameraShakeMeter` (function) · `import { CameraShakeMeter } from "@jgengine/react"`
@@ -97,6 +114,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
97
114
 
98
115
  - `captureCanvas` (function) · `import { captureCanvas } from "@jgengine/shell/render/sceneCapture"`
99
116
 
117
+ ## character-crowd — draw distant animated characters on the GPU instead of posing their skeletons
118
+
119
+ - `CharacterCrowd` (function) · `import { CharacterCrowd } from "@jgengine/shell/render/CharacterCrowd"`
120
+
100
121
  ## character-paperdoll — equip-slot paperdoll — flanking columns around a portrait, activatable slots
101
122
 
102
123
  - `Paperdoll` (function) · `import { Paperdoll } from "@jgengine/react"`
@@ -154,6 +175,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
154
175
 
155
176
  - `ControlsList` (function) · `import { ControlsList } from "@jgengine/react"`
156
177
 
178
+ ## credits-panel — scrollable, navigable credits screen with a back affordance
179
+
180
+ - `CreditsPanel` (function) · `import { CreditsPanel } from "@jgengine/react"`
181
+
157
182
  ## credits-screen — drop-in credits view over a CreditsDocument — headed sections, optional links, HudTheme-skinned
158
183
 
159
184
  - `CreditsScreen` (function) · `import { CreditsScreen } from "@jgengine/react/creditsScreen"`
@@ -282,6 +307,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
282
307
 
283
308
  - `displaceHeightfieldGeometry` (function) · `import { displaceHeightfieldGeometry } from "@jgengine/shell/terrain"`
284
309
 
310
+ ## hold-vs-toggle — resolve one action frame under a hold-or-toggle preference
311
+
312
+ - `holdToggleActive` (function) · `import { holdToggleActive } from "@jgengine/core/settings/accessibilitySettings"`
313
+
285
314
  ## hud-button — themed button with hover lift, press scale, focus ring, disabled state, and an interaction cue seam
286
315
 
287
316
  - `HudButton` (const) · `import { HudButton } from "@jgengine/react/hudButton"`
@@ -354,7 +383,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
354
383
 
355
384
  - `deriveHudTheme` (function) · `import { deriveHudTheme } from "@jgengine/react"`
356
385
 
357
- ## hud-theme-root — apply the default (or a game) HudTheme and the engine stylesheet at the HUD root
386
+ ## hud-theme-root — apply the default (or a game) HudTheme, the player's accessibility preferences and the engine stylesheet at the HUD root
358
387
 
359
388
  - `HudThemeRoot` (function) · `import { HudThemeRoot } from "@jgengine/react/hudThemeRoot"`
360
389
 
@@ -411,6 +440,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
411
440
 
412
441
  - `useLiveMarkers` (function) · `import { useLiveMarkers } from "@jgengine/react"`
413
442
 
443
+ ## loading-screen — loading block with progress, rotating tips, a ready gate, and a scene backdrop
444
+
445
+ - `LoadingScreen` (function) · `import { LoadingScreen } from "@jgengine/react"`
446
+
447
+ ## loading-tips — deterministic loading-screen tip rotation with a seeded shuffle and snapshot/restore
448
+
449
+ - `createTipRotation` (function) · `import { createTipRotation } from "@jgengine/core/ui/loadingTips"`
450
+
414
451
  ## map-legend — marker-kind key (glyph + color + label per kind) sharing the map's kindStyles
415
452
 
416
453
  - `MapLegend` (function) · `import { MapLegend } from "@jgengine/react"`
@@ -450,14 +487,38 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
450
487
 
451
488
  - `useTriplanarAssetMaterial` (function) · `import { useTriplanarAssetMaterial } from "@jgengine/shell/render/triplanarMaterial"`
452
489
 
490
+ ## menu-key-intent — resolve a keyboard code to a menu intent (move / confirm / back)
491
+
492
+ - `menuIntentForKey` (function) · `import { menuIntentForKey } from "@jgengine/core/ui"`
493
+
494
+ ## menu-nav-move — move menu focus by direction over a list or grid, skipping disabled items
495
+
496
+ - `menuNavMove` (function) · `import { menuNavMove } from "@jgengine/core/ui"`
497
+
498
+ ## menu-nav-repeat — gate held menu navigation into an initial press plus repeats
499
+
500
+ - `shouldRepeatMenuNav` (function) · `import { shouldRepeatMenuNav } from "@jgengine/core/ui"`
501
+
502
+ ## menu-pad-intent — resolve a standard gamepad button to a menu intent
503
+
504
+ - `menuIntentForPadButton` (function) · `import { menuIntentForPadButton } from "@jgengine/core/ui"`
505
+
453
506
  ## menu-router — screen stack for a game front-end — push/back/replace/reset with optional Escape-to-back
454
507
 
455
508
  - `useMenuRouter` (function) · `import { useMenuRouter } from "@jgengine/react/menuRouter"`
456
509
 
510
+ ## menu-screen — animated enter/exit menu screen over a live scene, with translucent scrim, backdrop slot, reduced-motion support and exit unmount
511
+
512
+ - `MenuScreen` (function) · `import { MenuScreen } from "@jgengine/react"`
513
+
457
514
  ## menu-stack-reducer — pure push/back/replace/reset transitions for a menu screen stack
458
515
 
459
516
  - `menuStackReducer` (function) · `import { menuStackReducer } from "@jgengine/react/menuRouter"`
460
517
 
518
+ ## menu-transition — enter/exit phase timing for an animated menu screen, with reduced-motion collapse and snapshot/restore
519
+
520
+ - `createMenuTransition` (function) · `import { createMenuTransition } from "@jgengine/core/ui"`
521
+
461
522
  ## merge-post-processing — layer a partial post chain over a look preset without losing its stages
462
523
 
463
524
  - `mergePostProcessing` (function) · `import { mergePostProcessing } from "@jgengine/core/render/lookPreset"`
@@ -693,6 +754,22 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
693
754
 
694
755
  - `useServerSession` (function) · `import { useServerSession } from "@jgengine/react/useServerSession"`
695
756
 
757
+ ## settings-keybind-row — one rebindable controls row — key glyph, armed state and reset
758
+
759
+ - `SettingsKeybindControl` (function) · `import { SettingsKeybindControl } from "@jgengine/react/settingsBlocks"`
760
+
761
+ ## settings-section — settings category block — rows plus click-to-capture keybind rows, HudTheme-styled, store-backed
762
+
763
+ - `SettingsSection` (function) · `import { SettingsSection } from "@jgengine/react/settingsBlocks"`
764
+
765
+ ## settings-slider — a settings slider row that mirrors locally and persists on release
766
+
767
+ - `SettingsSlider` (function) · `import { SettingsSlider } from "@jgengine/react/settingsBlocks"`
768
+
769
+ ## settings-tabs — scrollable settings category tabs with pressed state
770
+
771
+ - `SettingsTabs` (function) · `import { SettingsTabs } from "@jgengine/react/settingsBlocks"`
772
+
696
773
  ## shop-grid-host — drop-in vendor/shop grid over a caller-owned wallet — item cards with icon/price/stock, afford-aware Buy, optional Sell, and a balance readout, token-themed
697
774
 
698
775
  - `ShopGrid` (function) · `import { ShopGrid } from "@jgengine/react"`
@@ -722,6 +799,11 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
722
799
 
723
800
  - `StaticShapeInstances` (function) · `import { StaticShapeInstances } from "@jgengine/shell/render/staticShapeInstances"`
724
801
 
802
+ ## static-transforms — static-transforms
803
+
804
+ - `freezeStaticTransforms` (function) · `import { freezeStaticTransforms } from "@jgengine/shell/render/staticTransforms"`
805
+ - `thawStaticTransforms` (function) · `import { thawStaticTransforms } from "@jgengine/shell/render/staticTransforms"`
806
+
725
807
  ## status-effect-bar — timeline HUD row of active statuses — icon + radial countdown ring + stack badge, driven off the status model, HudTheme-skinned
726
808
 
727
809
  - `StatusEffectBar` (function) · `import { StatusEffectBar } from "@jgengine/react"`
@@ -754,6 +836,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
754
836
 
755
837
  - `TimerRing` (function) · `import { TimerRing } from "@jgengine/react"`
756
838
 
839
+ ## title-menu — title-screen block — wordmark, tagline, navigable verb list, footer, backdrop slot, animated over a live scene
840
+
841
+ - `TitleMenu` (function) · `import { TitleMenu } from "@jgengine/react"`
842
+
757
843
  ## trans — inline translated-message component bound to the active locale
758
844
 
759
845
  - `Trans` (function) · `import { Trans } from "@jgengine/react"`
@@ -774,6 +860,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
774
860
 
775
861
  - `useUiCue` (function) · `import { useUiCue } from "@jgengine/react/uiCues"`
776
862
 
863
+ ## use-accessibility-rows — accessibility setting rows for a game-owned settings layout
864
+
865
+ - `useAccessibilityRows` (function) · `import { useAccessibilityRows } from "@jgengine/react/accessibilitySettings"`
866
+
867
+ ## use-accessibility-settings — live accessibility preferences from the shared settings store
868
+
869
+ - `useAccessibilitySettings` (function) · `import { useAccessibilitySettings } from "@jgengine/react/accessibilitySettings"`
870
+
777
871
  ## use-camera-shake — React hook binding a camera-shake controller — subscribes for live trauma/kind and can self-drive its decay on a frame loop
778
872
 
779
873
  - `useCameraShake` (function) · `import { useCameraShake } from "@jgengine/react"`
@@ -802,10 +896,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
802
896
 
803
897
  - `useFastTravel` (function) · `import { useFastTravel } from "@jgengine/react"`
804
898
 
899
+ ## use-hold-vs-toggle — live hold-or-toggle preference from the shared settings store
900
+
901
+ - `useHoldVsToggle` (function) · `import { useHoldVsToggle } from "@jgengine/react/accessibilitySettings"`
902
+
805
903
  ## use-interaction-prompt — React hook binding a prompt registry to a moving player — resolves the nearest interactable prompt and returns the active one
806
904
 
807
905
  - `useInteractionPrompt` (function) · `import { useInteractionPrompt } from "@jgengine/react"`
808
906
 
907
+ ## use-menu-nav — keyboard and gamepad focus navigation for a menu — roving focus order, confirm/back, disabled-item skipping, no focus trap
908
+
909
+ - `useMenuNav` (function) · `import { useMenuNav } from "@jgengine/react"`
910
+
809
911
  ## use-modal-stack — React hook binding a modal stack to a component — re-renders on push/pop/resolve
810
912
 
811
913
  - `useModalStack` (function) · `import { useModalStack } from "@jgengine/react"`
@@ -96,6 +96,8 @@ AO and depth-of-field prepasses reuse the beauty pass's shadow maps; they do not
96
96
 
97
97
  **Orbit camera occlusion:** the third-person orbit rig takes `camera: { collision: { enabled, padding?, minTargetDistance? } }` — a spring-arm that raycasts target→camera each frame and pulls the boom in past walls/terrain so the camera never clips inside geometry. Off by default (unchanged chase feel); enable it for any world with interiors or dense structures.
98
98
 
99
+ **Camera clearance:** `camera: { firstPerson: { collision } }` keeps the `first` rig's eye out of geometry the body can reach. It is on by default: the boom anchors `armBack` (1.5) behind the eye along the look ray, `clearance` defaults to the near plane plus 0.15, and a blocking surface pulls the camera back toward that anchor so the near plane stays clear. `false` restores the unchecked camera. A game that drives its own camera calls the same seam: `clearCameraBoom({ pivot, desired, query: sceneCameraQuery(ctx.scene.raycast), previousArm, deltaSeconds })` returns the clear `position` and the `arm` to pass back next frame. The query reads `ctx.scene.raycast`, so world solids, scene-object colliders and walls all block it without a new collision layer; entities and terrain stay out so the followed body and the ground cannot pull the boom.
100
+
99
101
  Lighting is `defineGame({ lighting })` (`LightingConfig`): ambient / hemisphere / directional, replacing the shell's default lights when set. A `DirectionalLightingConfig` with `castShadow` takes `shadowMapSize`, `shadowCameraSize`, `shadowBias`, `shadowNormalBias` and `shadowRadius` (PCF penumbra in texels, default 1.25; the shell uses `PCFShadowMap` — in three r182 `PCFSoftShadowMap` silently compiles as unfiltered basic shadows). Every look but `"flat"` adds image-based light from a procedural sky probe (sky/horizon/ground from `hemisphere`, an HDR sun lobe from the first directional light, horizon leaning toward the ambient colour, intensity 0.45); `environmentSource` swaps in an HDRI or cube map. Under that IBL, `ambient` intensity is scaled by `iblAmbientScale` (default 0.5; set 1 to keep it), and lighting with no `directional` entry gets a soft shadowless key tinted by the ambient colour unless `fallbackKey: false` (`balanceLightingForIbl` in `@jgengine/core/render/lightingBalance`). Sky-lit worlds (a `sky()` world feature) get a high-res sun whose shadow camera follows the view each frame, so grounded shadows stay sharp under the player anywhere in a large world.
100
102
 
101
103
  ## First-person viewmodel, WorldOverlay props, render cues (#542)
@@ -243,10 +243,14 @@ Focus must look authored while remaining accessible. Menus should support keyboa
243
243
 
244
244
  Compose it from the game's own markup over the headless `StartScreen` (`@jgengine/react`, `start-screen` capability) — a full-bleed `data-jg-menu` overlay that centers your content and offers an opt-in settings corner; it imposes no look. Menu rows are the game's own buttons and focus handling restyled into its chrome; the shipped pieces to compose in are `ControlsList` (`@jgengine/react`) for the control legend and `KeyHint`/`Keycap` for key badges (`KeyHint` already renders nothing on a coarse pointer) — never shipped as raw scaffold.
245
245
 
246
+ **The engine ships the front-end machinery; the game owns its look.** `MenuScreen` (`@jgengine/react/menuScreen`) is one animated screen over the live scene: transform/opacity enter and exit that unmount when the exit finishes, a translucent scrim (`"none"` keeps the 3D scene fully visible), and a `backdrop` slot for a poster or preview. `TitleMenu` (`@jgengine/react/titleMenu`) is the title shape — wordmark, eyebrow, tagline, verb list, footer, control hint — where the game supplies the wording, verbs and art and skins it with `--jg-*` tokens. Both read `useMenuNav` (`@jgengine/react/menuNav`): arrow keys/WASD/Home/End, a standard gamepad's d-pad and left stick (with hold-to-repeat), Enter/Space/A confirm, Escape/Backspace/B/Start back. Tab is never intercepted and typing in a field is never swallowed, so no menu traps focus; keys still work if the canvas kept focus. A game that composes its own markup gets the same behavior from `useMenuNav` alone.
247
+
246
248
  **The front-end entries, when the game has them.** A menu is more than one Start button. Offer the real front-end verbs a player expects, gated to what the game actually supports:
247
249
 
248
250
  - **New Game** — begin a fresh run. When a save exists, keep it distinct from Continue rather than silently overwriting.
249
251
  - **Continue / Load** — resume the most recent save, and (when the game keeps more than one) a **Load** entry that lists save slots. Save slots are `createSaveStore` instances keyed by slot id; read them reactively with `useSaveStore` (`@jgengine/react`, offline `localSaveBackend` or cloud `remoteSaveBackend` — same hook, different backend). Disable Continue/Load with a real disabled state when there is nothing to resume; do not hide the verb entirely.
252
+
253
+ **Wire the list to the save document, not hand-typed metadata.** `SaveSlotMenu` renders a `createSaveSlots` index; `refreshSaveSlots(gameSaveSlotSource(save), index, { nameOf, metaOf })` (`@jgengine/core/game/saveSlotIndex`) fills it from the slots `createGameSave` actually wrote — name, saved time and a playtime chip from the document — and clears slots whose document was deleted. `save.peek(slot)` reads a document without applying it, so opening the menu never disturbs the live run. A game name can come from its own profile section via `nameOf`; extra chips (`chapter`, `location`) come from `metaOf`, which the index passes through untouched.
250
254
  - **Create Character / Choose Class** — when the game has characters, classes, loadouts, or profiles, the create/select flow lives here as an authored screen, not a stray dropdown. Keep the character/roster model serializable and game-owned (persist it through the game's save store); the menu is its presentation. Do not reach for a genre "class picker" preset — compose the choices this game actually offers.
251
255
  - **Load Character / Roster** — choose an existing profile or party when the game persists more than one.
252
256
  - **Settings** — reachable from the main menu, always (see Settings menu below). This is one of the two canonical places settings live; pause is the other.
@@ -275,6 +279,10 @@ HUD-only games (`presentation: "hud"` or a camera with rig `"none"`) use the sam
275
279
 
276
280
  **Present it any way you want.** `useSettings()` (`@jgengine/react`) returns the live controller — `{ categories, actions, variant, surface, isOpen, open, close, setOpen }` — so a game can drive its own pause-menu button, or render `categories`/`actions` (rows carry `value`/`set`/bounds, keybinds carry `rebind`/`reset`) entirely inside its own HUD. `useHasSettings()` gates a custom entry; `useSetting(id, fallback)` reads/writes one value. Set a slider's `min`/`max` explicitly — an omitted range collapses the thumb to 0/1.
277
281
 
282
+ **Composing your own settings layout.** `SettingsSection` (`@jgengine/react/settingsBlocks`) renders one controller category — its rows, then its click-to-capture keybind rows — and `SettingsTabs` renders the category rail (pass `tabProps` from `useMenuNav` so a gamepad can switch categories). `SettingsRowControl`, `SettingsSlider`, `SettingsToggle`, `SettingsSelect` and `SettingsKeybindControl` are the individual rows if the layout is not a list. All of them read `--jg-*` tokens and write through the controller, so the game's rebinds land in the same keybinding store as the stock menu. The stock menu uses these same blocks, so a game-composed screen and the engine dialog cannot drift.
283
+
284
+ **Accessibility is a shipped category.** The settings menu includes `accessibility` — text size, colourblind palette, reduce motion, high contrast, captions and hold-or-toggle — unless the game hides it. Text size multiplies every `--jg-type-*` token through `--jg-text-scale`, reduce motion zeroes `--jg-motion-scale`, and high contrast/colourblind/captions set `data-*` hooks on the HUD root; read them with `useAccessibilitySettings()` and `useHoldVsToggle()` for a custom layout. Apply the colourblind matrix (`ColorblindFilters`, `@jgengine/react/accessibility`) around the surface that should be daltonized — the shell sets the hook but does not wrap the 3D canvas.
285
+
278
286
  ## 8. Motion and game feel
279
287
 
280
288
  Add purposeful motion for:
@@ -67,6 +67,10 @@ Move many agents across that mesh with `createNavCrowd` (`nav/navCrowd`): it pla
67
67
 
68
68
  Vehicles on roads do not path-find: they follow authored lane spans. `createLaneGraph` (`nav/laneGraph`) resolves those spans — `traffic_lane` paths, or `road` paths from the editor's street bake, split where streets cross — into junctions, crossings with a signal program or first-come right-of-way, and parking spots; `laneGraphFromDocument` reads that straight off the scene document, so editing a road moves its traffic. `createTraffic` (`nav/traffic`) then drives cars on it: IDM car following and braking, one car at a time through each intersection box (only once the exit lane has room, which is what keeps a blocked crossing from wedging the network), signals with phases, lane changes across authored `left`/`right` neighbours, parking that reserves a spot, and `setDemand(count)` for fleet size — the time-of-day curve is the caller's, not the engine's. Cars are posed, not driven: when a player or a chase needs real handling, feed intent into `physics/kinematicVehicle` and let this seam keep the ambient city.
69
69
 
70
+ ### Heat and police response
71
+
72
+ A crime only counts when a witness holds the suspect. `createPoliceResponse` (`ai/policeResponse`) reads witness facts from `sensor/perception` memory — `memoryOf` per observer, keeping the sighting's `sourceId` and `sourceTick` — and adds heat on the shared `ai/heatSystem` curve, so there is no second sight path and no second decay. The caller's tier table decides what police do at each heat: patrol, investigate, pursue, roadblock, with a pursuer budget per tier. The search area sits on the suspect's last known position, grows while the trail is cold, and a roadblock tier cordons it with sites `policeRoadsFromLaneGraph` reads off the lane graph (`traffic_roadblock` markers projected onto the lane they block, else the graph's intersections). Breaking sight for `escapeSeconds` is what starts the decay; standing in view keeps the heat. Heat, curve, tiers and search shape are caller policy with defaults, `state()`/`restore()` round-trip mid-chase, and the injected generator is the only source of variety.
73
+
70
74
  ### Fallback-seam diagnostics
71
75
 
72
76
  Render seams fall back to placeholders when content is unauthored: default green ground (no environment), primitive capsule/box actors (no `entityModels`/`objectModels` mapping), stylized proxy foliage (no scatter `resolveItem`). These placeholders are sometimes intended, so runtime never changes — but a dev-only, opt-in probe (`@jgengine/core/devtools/fallbackSeams`) tells "unauthored/misconfigured" apart from "intended". The shell arms it via `armFallbackSeams(devtoolsEnabled)`; each seam reports at its choice site (`reportFallbackSeam`), and counts surface through the devtools `fallbacks` probe. Disarmed (production) it is a pure, allocation-free no-op.
@@ -126,6 +126,29 @@
126
126
 
127
127
  - `mobBrainGraph` (const): const mobBrainGraph: DecisionGraph — Graph-shaped description of the mob brain's idle, wander, chase, engage, and evade decisions.
128
128
 
129
+ ## @jgengine/core/ai/policeResponse
130
+
131
+ - `DEFAULT_POLICE_TIERS` (const): const DEFAULT_POLICE_TIERS: readonly PoliceTier[] — The starting tier table: patrol until heat arrives, then investigate, pursue and finally cordon.
132
+ - `DEFAULT_POLICE_TUNING` (const): const DEFAULT_POLICE_TUNING: PoliceTuning — The starting policy: linear decay, a six-second escape and a search area that grows at 2 m/s.
133
+ - `PoliceLaneHit` (interface): interface PoliceLaneHit — A lane near a world point: its id, the arc of the nearest point on it, and how far away that is.
134
+ - `PoliceResponse` (interface): interface PoliceResponse — Stateful wanted/pursuit API with retuning and save/restore support.
135
+ - `PoliceResponseConfig` (interface): interface PoliceResponseConfig extends Partial<PoliceTuning> — Everything {@link createPoliceResponse} needs: the witness seam, the roster and the caller's policy.
136
+ - `PoliceResponseKind` (type): type PoliceResponseKind = "patrol" | "investigate" | "pursue" | "roadblock" — How police answer the current heat tier — watching the streets up to cordoning them.
137
+ - `PoliceResponseState` (interface): interface PoliceResponseState — Serializable response state — the heat curve, the sight timer, the anchor and the counters.
138
+ - `PoliceRoadblockSite` (interface): interface PoliceRoadblockSite — A roadblock site on a lane, resolved from authored road data.
139
+ - `PoliceRoads` (interface): interface PoliceRoads — Road data the response reads: nearby lanes for the search area, authored sites for roadblocks.
140
+ - `PoliceRoadsOptions` (interface): interface PoliceRoadsOptions — How the document reader recognizes authored road content.
141
+ - `PoliceSearchArea` (interface): interface PoliceSearchArea — Where the search is, as the last sighting places it.
142
+ - `PoliceSighting` (interface): interface PoliceSighting — One remembered sighting of the suspect, as the perception service reports it — `PerceptionMemory` from `sensor/perception` satisfies this shape.
143
+ - `PoliceStats` (interface): interface PoliceStats — Counters a devtools probe, a HUD or a test reads.
144
+ - `PoliceStep` (interface): interface PoliceStep — One tick's answer: the heat, the tier, the search area, the cordon and the escape.
145
+ - `PoliceTickContext` (interface): interface PoliceTickContext — Per-tick world facts {@link PoliceResponse.tick} needs but cannot derive itself.
146
+ - `PoliceTier` (interface): interface PoliceTier — One response tier: the heat it begins at, what it puts on the street, and how wide it searches.
147
+ - `PoliceTuning` (interface): interface PoliceTuning — The serializable policy of a response: the tier table, the heat curve and the search shape.
148
+ - `PoliceWitness` (interface): interface PoliceWitness — The witness seam: perception memory, queried by observer id. Pass `createPerception` (`sensor/perception`) straight in — a crime is witnessed when an observer still holds the suspect in view, so there is no second sight path to keep in step.
149
+ - `createPoliceResponse` (function): function createPoliceResponse(config: PoliceResponseConfig): PoliceResponse — The wanted level a crime sandbox needs, over the shared heat curve: a crime raises heat only while a witness holds the suspect in view, the heat bleeds on a stated linear curve once the suspect has been out of every witness's sight for `escapeSeconds`, the tier table decides what police do (patrol → investigate → pursue → roadblock), the search area sits where the suspect was last seen and grows while the trail is cold, and a roadblock tier cordons it with authored sites off the lane graph. Heat, tiers and curves are caller policy; witness facts come from `sensor/perception` memory (`sourceId`/`sourceTick` kept), so there is no second sight path.
150
+ - `policeRoadsFromLaneGraph` (function): function policeRoadsFromLaneGraph(graph: LaneGraph, markers: readonly EditorMarker[] = [], options: PoliceRoadsOptions = {}): PoliceRoads — Read the police road data out of an authored lane graph: `traffic_roadblock` markers become cordon sites projected onto the lane they block, and lanes are indexed so `lanesNear` answers from the local cells rather than a scan of every lane. With no roadblock markers authored, the graph's intersections are the sites, so a cordon still sits where streets meet. Built once; the index is rebuildable from the document, so it is never serialized.
151
+
129
152
  ## @jgengine/core/ai/populationDirector
130
153
 
131
154
  - `PopulationCensus` (type): type PopulationCensus = Record<string, Record<string, number>> — Live census of alive counts keyed `region -> species -> count`, used to reconcile the director to reality.
@@ -233,6 +256,13 @@
233
256
  - `WaveView` (interface): interface WaveView — A pooled, per-frame readout of the current wave — the "brain behind WAVE 3". Reused across {@link WaveRunner.view} calls (never per-frame allocated), so read it and render it, don't retain it.
234
257
  - `createWaveRunner` (function): function createWaveRunner(config: WaveRunnerConfig): WaveRunner — A thin, stateful, observable wrapper around the seeded {@link createSpawnDirectorState} / {@link advanceSpawnDirector} model. It OWNS a {@link SpawnDirectorState}, ticks it from `update(dt, ctx?)`, keeps the new state, and forwards each emitted {@link SpawnRequest} to an optional `onSpawn` sink so the model never instantiates entities. `view()` returns a pooled per-frame readout — the "brain behind WAVE 3": the 1-based wave number, wave progress `0..1`, banked budget, per-wave and total spawn counts, alert, and done flag. `forceNextWave` / `raiseAlert` drive it, `subscribe` observes it, and `snapshot`/`restore` round-trip the plain serializable state object through a save. Nothing here is genre-specific: spawn-entry `kind`s (the director's `entryId`s) stay free strings the runner never interprets.
235
258
 
259
+ ## @jgengine/core/anim/actionLayer
260
+
261
+ - `ACTION_REST_STATE` (const): const ACTION_REST_STATE: "rest" — State id of an action layer's resting state.
262
+ - `ActionLayerInput` (interface): interface ActionLayerInput — A masked, optionally additive overlay layer: a resting clip plus one one-shot state per action.
263
+ - `actionLayer` (function): function actionLayer(input: ActionLayerInput): AnimLayer — One overlay layer from plain data: a resting state plus a one-shot state per action, each joined to the rest by a consumed-trigger transition and returning on `exitTime`. Aim, reload, wave and hit reactions are the same shape, so they are one builder instead of a hand-written state machine per game. The layer is masked by bone prefix, and `additive: true` makes it a recoil/breathing overlay on top of the base pose rather than a replacement.
264
+ - `withActionLayers` (function): function withActionLayers(graph: AnimGraph, layers: readonly ActionLayerInput[]): AnimGraph — A graph plus overlay layers: the base layers keep their ids and order, and each overlay is appended. Duplicate layer ids are rejected rather than silently shadowing, because the shell keys its mixer actions by layer id.
265
+
236
266
  ## @jgengine/core/anim/animGraph
237
267
 
238
268
  - `ANIM_PARAMS_KEY` (const): const ANIM_PARAMS_KEY: "anim.params" — Entity blackboard key the shell reads extra graph parameters from: `ctx.scene.entity.blackboard.set(id, ANIM_PARAMS_KEY, { aiming: true })`.
@@ -292,19 +322,61 @@
292
322
  - `TwoBoneOutput` (interface): interface TwoBoneOutput — Reused output buffers filled by {@link solveTwoBone}.
293
323
  - `Vec3` (type): type Vec3 = readonly [number, number, number] — A three-dimensional point or direction represented without a renderer dependency.
294
324
  - `lookAt` (function): function lookAt(input: LookAtInput): Quat — Returns a quaternion that points a +Z bone from `from` toward `target`, with optional yaw and pitch limits.
325
+ - `multiplyQuaternions` (function): function multiplyQuaternions(a: Quat, b: Quat): Quat — The product of two quaternions (`a` applied after `b`), both as `[x, y, z, w]`.
326
+ - `quaternionFromUnitVectors` (function): function quaternionFromUnitVectors(from: Vec3, to: Vec3): Quat — The shortest rotation taking one unit direction to another, as a quaternion. Parallel directions give the identity; opposite directions give a half turn about a perpendicular axis. Returns the identity for a zero-length input.
295
327
  - `solveFabrik` (function): function solveFabrik(chain: readonly Vec3[], target: Vec3, options: FabrikOptions = {}): MutableVec3[] — Solves a chain with the FABRIK forward-and-backward reaching algorithm. Returned tuples are new values; the input chain is not modified.
296
328
  - `solveTwoBone` (function): function solveTwoBone(input: TwoBoneInput, out: TwoBoneOutput): TwoBoneOutput — Solves a two-segment chain analytically, placing the middle and tip joints toward `target` and `pole`. The root remains fixed, and unreachable targets are clamped to the chain's maximum reach.
297
329
 
298
330
  ## @jgengine/core/anim/locomotionGraph
299
331
 
332
+ - `LOCOMOTION_BACKWARD_STATE` (const): const LOCOMOTION_BACKWARD_STATE: "backward" — State id of the backwards blend.
300
333
  - `LOCOMOTION_CROUCHED_PARAM` (const): const LOCOMOTION_CROUCHED_PARAM: "crouched" — Physically resolved crouch stance from the shared player motor.
334
+ - `LOCOMOTION_DIRECTION_PARAM` (const): const LOCOMOTION_DIRECTION_PARAM: "direction" — Signed lateral share of the character's motion: `-1` fully to its left, `+1` fully to its right, `0` straight ahead or straight back. A custom mover feeds it under `ANIM_PARAMS_KEY`.
335
+ - `LOCOMOTION_FACING_PARAM` (const): const LOCOMOTION_FACING_PARAM: "facing" — Dot of the character's facing with its motion direction: `1` running forward, `-1` running backwards. A custom mover feeds it under `ANIM_PARAMS_KEY`.
301
336
  - `LOCOMOTION_GROUNDED_PARAM` (const): const LOCOMOTION_GROUNDED_PARAM: "grounded" — Ground contact reported by the shared player motor; absent for custom movers without authored parameters.
302
337
  - `LOCOMOTION_LAYER` (const): const LOCOMOTION_LAYER: "base" — Layer id the locomotion graph uses; query `runtime.stateOf(LOCOMOTION_LAYER)`.
303
338
  - `LOCOMOTION_SPEED_PARAM` (const): const LOCOMOTION_SPEED_PARAM: "speed" — The parameter name the shell feeds with the entity's smoothed ground speed.
339
+ - `LOCOMOTION_STATE` (const): const LOCOMOTION_STATE: "locomotion" — State id of the forward idle/walk/run blend.
340
+ - `LOCOMOTION_STRAFE_STATE` (const): const LOCOMOTION_STRAFE_STATE: "strafe" — State id of the lateral strafe blend.
341
+ - `LOCOMOTION_TURN_RATE_PARAM` (const): const LOCOMOTION_TURN_RATE_PARAM: "turnRate" — Signed yaw rate in radians/sec; a custom mover feeds it under `ANIM_PARAMS_KEY`.
342
+ - `LOCOMOTION_TURN_STATE` (const): const LOCOMOTION_TURN_STATE: "turn" — State id of the on-the-spot turn blend.
304
343
  - `LOCOMOTION_VERTICAL_SPEED_PARAM` (const): const LOCOMOTION_VERTICAL_SPEED_PARAM: "verticalSpeed" — Resolved vertical velocity from the shared player motor, in world units/sec.
305
- - `LocomotionGraphInput` (interface): interface LocomotionGraphInput — Inputs for {@link locomotionGraph}: the idle/walk/run clip names and the one-shot table a rig config already carries.
344
+ - `LocomotionGraphInput` (interface): interface LocomotionGraphInput — Inputs for {@link locomotionGraph}: the locomotion clip names and the one-shot table a rig config already carries.
306
345
  - `animGraphFromConfig` (function): function animGraphFromConfig(config: ModelAnimationConfig): AnimGraph | undefined — The graph a model animation config plays: its `graph`, or the {@link locomotionGraph} its `states` and `oneShots` describe, retaining every one-shot variant. `undefined` for a single-clip config or incomplete locomotion roles. Idle must name a clip; an absent or blank walk role holds that idle clip at walking speeds until configured. The shell plays this and the editor inspects it, so both see the same graph.
307
- - `locomotionGraph` (function): function locomotionGraph(input: LocomotionGraphInput): AnimGraph — The engine's default locomotion as an authored graph: a speed-driven blend between idle, walk, and run, plus a state per one-shot that plays once and returns (or clamps for `death`). What `useModelAnimation` used to hardcode.
346
+ - `locomotionGraph` (function): function locomotionGraph(input: LocomotionGraphInput): AnimGraph — The engine's default locomotion as an authored graph: a speed-driven blend between idle, walk and run, plus a state per one-shot that plays once and returns (or clamps for `death`). What `useModelAnimation` used to hardcode.
347
+
348
+ ## @jgengine/core/anim/ragdollRig
349
+
350
+ - `HUMANOID_RAGDOLL_BODIES` (const): const HUMANOID_RAGDOLL_BODIES: Readonly<Partial<Record<SkeletonSlot, RagdollBodyShape>>> — Humanoid body shapes for the standard slots, sized for a roughly 1.8 m character standing on the ground. A rig of another scale multiplies them (see {@link RagdollRigOptions.scale}).
351
+ - `RagdollBodyShape` (interface): interface RagdollBodyShape — Half-extents and mass of one standard slot's physics body, in the rig's own world units.
352
+ - `RagdollPose` (interface): interface RagdollPose — One standard slot's world pose, sampled at the moment a character is killed. The orientation is the reference the ragdoll's aim rotates from, so every bone keeps the roll it died with.
353
+ - `RagdollRigOptions` (interface): interface RagdollRigOptions — Tuning for {@link ragdollConfigFromPoses}.
354
+ - `ragdollBoneOrientations` (function): function ragdollBoneOrientations(rest: readonly RagdollPose[], positions: Readonly<Partial<Record<SkeletonSlot, readonly [number, number, number]>>>): Partial<Record<SkeletonSlot, [number, number, number, number]>> — World orientations for a ragdoll's bones from its bodies' current positions: each bone rotates from its rest direction-to-child onto the current one, so the aim follows the joints while every bone keeps the roll it had in the death pose. A bone with no posed child keeps its rest orientation. The physics bodies carry translation only, so this is what gives the ragdoll a skeleton instead of a pile of boxes.
355
+ - `ragdollConfigFromPoses` (function): function ragdollConfigFromPoses(map: SkeletonMap, poses: readonly RagdollPose[], options: RagdollRigOptions = {}): RagdollConfig — A jointed ragdoll for a mapped rig from its bones' world poses at the moment of death: one box body per standard slot that has a shape, sized to the character, and one distance link per slot that has its parent present, holding the bones' own separation. The bodies start where the character actually was, so the collapse continues the death pose instead of snapping to a preset.
356
+
357
+ ## @jgengine/core/anim/skeletonStandard
358
+
359
+ - `KAYKIT_SCHEME` (const): const KAYKIT_SCHEME: RigScheme — The standard itself, as a scheme: a KayKit rig already is the standard, so its own names are the candidates. Both authored (`upperleg.l`) and runtime (`upperlegl`) spellings are accepted.
360
+ - `LOWER_BODY_MASK` (const): const LOWER_BODY_MASK: readonly string[] — Layer mask for a lower-body layer on a standard rig.
361
+ - `LOWER_BODY_SLOTS` (const): const LOWER_BODY_SLOTS: readonly SkeletonSlot[] — Standard slots a lower-body layer drives: hips, both legs and both feet.
362
+ - `MIXAMO_SCHEME` (const): const MIXAMO_SCHEME: RigScheme — Mixamo exports (`mixamorig*` names, or the same names without the prefix). Mixamo has a neck and a shoulder per side, which the standard folds into `chest` and `upperArm`; its toe is `ToeBase`, which is the standard's `toes`. Mixamo's topmost bone is `Hips`, so `root` and `hips` both resolve to it — a rig on this scheme has no bone above the pelvis.
363
+ - `QUATERNIUS_ALIEN_SCHEME` (const): const QUATERNIUS_ALIEN_SCHEME: RigScheme — Quaternius sci-fi aliens: a `Root` plus a `Spine_1..Spine_6` chain, a `Neck` and a `Head`. The pack ships no arms and no legs, so those slots stay empty and the alien can only be driven on its spine and head. That is the honest mapping: the pack's own clip set is `Alien_Idle` alone.
364
+ - `RIG_SCHEMES` (const): const RIG_SCHEMES: Readonly<Record<string, RigScheme>> — Every scheme the engine ships a mapping for, keyed by {@link RigScheme.id}.
365
+ - `RigScheme` (interface): interface RigScheme — A source naming scheme and the bone names on it that fill each standard slot. A slot lists every accepted spelling; matching is case- and punctuation-insensitive, so `mixamorigLeftUpLeg` and `LeftUpLeg` are the same candidate.
366
+ - `SKELETON_PARENT` (const): const SKELETON_PARENT: Readonly<Partial<Record<SkeletonSlot, SkeletonSlot>>> — The parent slot of each slot that has one; `root` has none.
367
+ - `SKELETON_SLOTS` (const): const SKELETON_SLOTS: readonly SkeletonSlot[] — Every standard slot, in parent-before-child order.
368
+ - `SKELETON_STANDARD` (const): const SKELETON_STANDARD: SkeletonStandard — The one humanoid skeleton standard. Bone names are the authored KayKit names; the loaded hierarchy spells them without `.` and `resolveRigNode` accepts either.
369
+ - `SkeletonMap` (interface): interface SkeletonMap — The result of mapping a rig's bones onto {@link SKELETON_STANDARD}.
370
+ - `SkeletonSlot` (type): type SkeletonSlot = | "root" | "hips" | "spine" | "chest" | "head" | "upperArm.l" | "upperArm.r" | "lowerArm.l" | "lowerArm.r" | "hand.l" | "hand.r" | "handSlot.l" | "handSlot.r" | "upperLeg.l" | "upperLeg.r" | "lowerLeg.l" | "lowerLeg.r" | "foot.l" | "foot.r" | "toes.l" | "toes.r" — The engine's one humanoid skeleton standard, and the path any other CC0 rig takes onto it.
371
+ - `SkeletonStandard` (interface): interface SkeletonStandard — A named skeleton standard: its id and the authored bone name for each slot it defines.
372
+ - `SlotLength` (interface): interface SlotLength — A standard slot and the rest-pose length of the bone chain that reaches it.
373
+ - `UPPER_BODY_MASK` (const): const UPPER_BODY_MASK: readonly string[] — Layer mask for an upper-body action layer (aim, reload, hit reaction) on a standard rig.
374
+ - `UPPER_BODY_SLOTS` (const): const UPPER_BODY_SLOTS: readonly SkeletonSlot[] — Standard slots an upper-body action layer drives: spine, chest, head and both arms.
375
+ - `mapRigToStandard` (function): function mapRigToStandard(bones: readonly string[], scheme: RigScheme | string = KAYKIT_SCHEME): SkeletonMap — Maps a rig's bone names onto {@link SKELETON_STANDARD} through one {@link RigScheme}. Each slot takes the first candidate that resolves; a candidate list is tried in order, so a scheme can name a preferred bone and fall back. Bones that fill no slot come back in `unmapped`, and slots the rig cannot fill in `missing` — the caller decides whether a partial map is usable.
376
+ - `retargetBoneScales` (function): function retargetBoneScales(source: readonly SlotLength[], target: readonly SlotLength[]): Partial<Record<SkeletonSlot, number>> — Per-slot length ratios that stretch a source rig's bone chain onto the standard's proportions, so a retargeted clip moves both rigs the same distance rather than the same angle. Slots with no counterpart on either side are omitted; a source length of zero is skipped instead of dividing.
377
+ - `retargetTracks` (function): function retargetTracks(tracks: readonly string[], map: SkeletonMap): (string | null)[] — Renames a clip's tracks from a rig's own bone names onto the standard's, so one authored clip plays on a mapped rig. The emitted node is the **runtime** bone name (see {@link runtimeBoneName}), which is what both the loaded scene graph and the loaded clips spell. A track whose node fills no slot is dropped (`null` in the result) rather than left pointing at a bone the rig does not have; a track whose property is not one of the four three writes is dropped too, since renaming it would not make it resolvable.
378
+ - `runtimeBoneName` (function): function runtimeBoneName(authored: string): string — The runtime name GLTFLoader gives an authored bone name: three's `PropertyBinding.sanitizeNodeName` strips `[ ] . : /`, so the authored `upperleg.l` loads as `upperlegl`.
379
+ - `standardBoneMask` (function): function standardBoneMask(slots: readonly SkeletonSlot[]): string[] — Bone-name prefixes for a set of standard slots, for an {@link AnimLayer.mask}. A slot on one side contributes its side-less prefix (`upperLeg.l` → `upperleg`), which matches both sides' tracks; an unsided slot contributes its own runtime name. Masking by slot means a rig mapped onto the standard is masked without hand-typing its bone names.
308
380
 
309
381
  ## @jgengine/core/area/areaEffectField
310
382
 
@@ -1324,7 +1396,7 @@
1324
1396
  - `Ragdoll` (class): class Ragdoll — A jointed multi-body character — floppy by default, or active-ragdoll when a balance motor is configured. Bones are `PhysicsWorld` bodies; links are joints on the joint API. `balance(dt, mx, mz)` drives the root toward its target height (staying upright) and pushes it by the move vector.
1325
1397
  - `RagdollBalanceConfig` (interface): interface RagdollBalanceConfig { root: string; targetHeight: number; strength?: number; moveForce?: number } — ⚠ undocumented
1326
1398
  - `RagdollBoneSpec` (interface): interface RagdollBoneSpec { name: string; position: readonly [number, number, number]; halfExtents: readonly [number, number, number]; mass?: number } — ⚠ undocumented
1327
- - `RagdollConfig` (interface): interface RagdollConfig { bones: RagdollBoneSpec[]; links: RagdollLinkSpec[]; balance?: RagdollBalanceConfig } — ⚠ undocumented
1399
+ - `RagdollConfig` (interface): interface RagdollConfig { bones: RagdollBoneSpec[]; links: RagdollLinkSpec[]; balance?: RagdollBalanceConfig } — ⚠ undocumented · used by `ragdollConfigFromPoses` (@jgengine/core/anim/ragdollRig): A jointed ragdoll for a mapped rig from its bones' world poses at the moment of death: one box body per standard slot that has a shape, size…
1328
1400
  - `RagdollLinkSpec` (interface): interface RagdollLinkSpec { a: string; b: string; kind?: "hinge" | "distance"; anchor?: readonly [number, number, number] } — ⚠ undocumented
1329
1401
  - `createRagdoll` (function): function createRagdoll(world: PhysicsWorld, config: RagdollConfig): Ragdoll — ⚠ undocumented
1330
1402
 
@@ -1754,7 +1826,7 @@
1754
1826
 
1755
1827
  - `SceneEntityQuerySource` (interface): interface SceneEntityQuerySource { list(): readonly {id: string; position: EntityPosition; rotationY: number; name?: string}[]; collidersOf?(instanceId: string): EntityColliderSet | null | undefined; inRadius?(center: EntityPosition, radius: number): readonly string[]; get… — ⚠ undocumented
1756
1828
  - `SceneObjectQuerySource` (interface): interface SceneObjectQuerySource { list(): readonly SceneObject[]; inBox?(min: EntityPosition, max: EntityPosition): readonly SceneObject[]; collidersOf?(instanceId: string): EntityColliderSet | null | undefined; halfExtentsOf?(catalogId: string): EntityPosition | null | u… — ⚠ undocumented
1757
- - `SceneRaycastApi` (interface): interface SceneRaycastApi { raycast(input: SceneRaycastInput): SceneRaycastHit | null; raycastAll(input: SceneRaycastInput): SceneRaycastHit[] } — ⚠ undocumented
1829
+ - `SceneRaycastApi` (interface): interface SceneRaycastApi { raycast(input: SceneRaycastInput): SceneRaycastHit | null; raycastAll(input: SceneRaycastInput): SceneRaycastHit[] } — ⚠ undocumented · used by `sceneCameraQuery` (@jgengine/shell/camera): `CameraCollisionQuery` over the scene's existing collision query (`ctx.scene.raycast`), which already reads world solids, scene-object colli…
1758
1830
  - `SceneRaycastDeps` (interface): interface SceneRaycastDeps { entities?: SceneEntityQuerySource; objects?: SceneObjectQuerySource; terrain?: TerrainRaycastSource; walls?: readonly WallSegment[]; solids?: Pick<WorldSolids, "inBox" | "inRay"> } — ⚠ undocumented
1759
1831
  - `SceneRaycastFilter` (interface): interface SceneRaycastFilter { entities?: boolean; objects?: boolean; terrain?: boolean; walls?: boolean } — ⚠ undocumented
1760
1832
  - `SceneRaycastHit` (interface): interface SceneRaycastHit { targetKind: SceneRaycastTargetKind; instanceId: string; catalogId?: string; colliderName: string; purpose: ColliderPurpose; damageEligible: boolean; blocks: boolean; distance: number; point: EntityPosition; normal: EntityPosition } — ⚠ undocumented · used by `firstImpact`: First impact: nearest hit that blocks, or nearest hit if none block.
@@ -2026,6 +2098,20 @@
2026
2098
  - `HitInput` (interface): interface HitInput — A single incoming hit to register on a {@link DamageDirectionTracker}. The angle is relative to the player's facing (renderer-agnostic): `0` points at the front/top of the reticle and increases clockwise, so a game passes the bearing from the player toward the attacker without knowing anything about the screen.
2027
2099
  - `createDamageDirectionTracker` (function): function createDamageDirectionTracker(options: DamageDirectionOptions = {}): DamageDirectionTracker — Create a damage-direction tracker: the classic "hit-from" feedback brain. A game calls `registerHit({ angle, intensity, kind })` with the bearing from the player toward the attacker (radians, `0` = front) and the tracker owns the fade timers and eased strength so the renderer just draws an arc per `active()` entry. It is renderer-free and genre-agnostic (the `kind` tag is never interpreted here), allocation-aware (a fixed pool, no per-frame garbage), and fully serializable via `snapshot`/`restore`. Optional angle merging collapses a burst from one direction into a single strong arc.
2028
2100
 
2101
+ ## @jgengine/core/vfx/effectLibrary
2102
+
2103
+ - `DEFAULT_EFFECT` (const): const DEFAULT_EFFECT: "sparks/weld" — The fallback effect used when a play call names an unknown effect — a typo still shows a spark.
2104
+ - `EFFECT_LIBRARY` (const): const EFFECT_LIBRARY: readonly ParticleEffect[] — The shared library of named particle effects: fire, smoke, sparks, dust, splashes, muzzle flash, explosions, magic and weather. Each entry is authored parameters only — rate, lifetime, size, colour and the sprite texture — so a game calls `ctx.particles.play("explosion/core", at)` and the editor authors the same name instead of hand-tuning an `EmitterConfig`. Names are the contract; a config retune here changes every consumer deliberately.
2105
+ - `EFFECT_PRIORITY` (const): const EFFECT_PRIORITY: { readonly weather: 20; readonly dust: 40; readonly smoke: 60; readonly magic: 70; readonly sparks: 70; readonly fire: 80; readonly splash: 80; readonly muzzle: 90; readonly explosion: 90; } — Ambient dressing drops first, then gameplay-loud effects last.
2106
+ - `EffectGroup` (type): type EffectGroup = | "fire" | "smoke" | "sparks" | "dust" | "splash" | "muzzle" | "explosion" | "magic" | "weather" — The family an effect belongs to; groups the library browser and the contact sheet.
2107
+ - `EffectOverride` (interface): interface EffectOverride — Per-call overrides merged over a library effect; each field wins over the authored default.
2108
+ - `EffectTexture` (type): type EffectTexture = "soft" | "spark" | "flake" | "flame" | "smoke" | "ribbon" | "ring" — The procedural sprite family a particle draws with on the GPU path. Each family maps to a {@link ParticleRenderHint} shape: `soft` puffs, velocity-stretched `spark` streaks, `flake` crystals, `flame` tongues, `smoke` puffs, connected `ribbon` trails and flat `ring` ripples. There is no image file to load or credit — the field is the authored name for the sprite.
2109
+ - `ParticleEffect` (interface): interface ParticleEffect — One named effect a game or the editor plays by name. Every field is data: the `config` is the same `EmitterConfig` a hand-authored emitter uses, so a library effect serializes, retunes and renders on the existing pooled GPU path without a second implementation.
2110
+ - `ResolvedEffect` (interface): interface ResolvedEffect — A library effect resolved into the exact data the director and renderer consume.
2111
+ - `effectNames` (function): function effectNames(): readonly string[] — Every library effect name, for editor pickers and runtime discovery.
2112
+ - `findEffect` (function): function findEffect(name: string): ParticleEffect | undefined — The named effect, or `undefined` when the name is not in the library.
2113
+ - `resolveEffect` (function): function resolveEffect(name: string, override?: EffectOverride): ResolvedEffect — Resolve a named effect into concrete emitter data, with any `override` winning field by field. An unknown name resolves to {@link DEFAULT_EFFECT} rather than throwing, so a typo shows *something*; call {@link findEffect} when the caller must reject an unknown name.
2114
+
2029
2115
  ## @jgengine/core/vfx/feedbackMixer
2030
2116
 
2031
2117
  - `FeedbackCurve` (type): type FeedbackCurve = readonly (readonly [number, number])[] — Piecewise-linear map from a signal value to an output value; points in ascending input order, ends clamp.
@@ -2037,8 +2123,16 @@
2037
2123
  - `createFeedbackMixer` (function): function createFeedbackMixer<TSignal extends string, TTarget extends string>(initial: FeedbackMixerConfig<TSignal, TTarget>): FeedbackMixer<TSignal, TTarget> — Creates a {@link FeedbackMixer}: the one place a game declares how sim telemetry drives presentation — rpm to engine pitch, tire saturation to squeal and rumble, speed to FOV, landing speed to a thud — instead of hand-writing that glue per vehicle, aircraft or weapon. Deterministic, allocation-free per update.
2038
2124
  - `sampleFeedbackCurve` (function): function sampleFeedbackCurve(curve: FeedbackCurve | undefined, x: number): number — Sample a {@link FeedbackCurve}; an empty curve is the identity.
2039
2125
 
2126
+ ## @jgengine/core/vfx/particleBudget
2127
+
2128
+ - `ParticleBudgetEntry` (interface): interface ParticleBudgetEntry — The graphics-tier particle budget. A tier caps the live particle count across every effect on screen; when the requested effects do not fit, whole effects are dropped in a defined order (lowest {@link ParticleBudgetEntry.priority} first) rather than every effect thinning out.
2129
+ - `ParticleBudgetGrant` (interface): interface ParticleBudgetGrant — An admitted effect and the particles it may hold.
2130
+ - `ParticleBudgetPlan` (interface): interface ParticleBudgetPlan — What the tier cap admits, and what it drops.
2131
+ - `planParticleBudget` (function): function planParticleBudget(entries: readonly ParticleBudgetEntry[], cap: number): ParticleBudgetPlan — Allocate a tier's particle cap across the effects that want it.
2132
+
2040
2133
  ## @jgengine/core/vfx/particleDirector
2041
2134
 
2135
+ - `EffectRequest` (interface): interface EffectRequest extends EffectOverride — A library effect played by name: where it plays, plus per-call overrides.
2042
2136
  - `ParticleAttachOptions` (interface): interface ParticleAttachOptions — Options for {@link ParticleDirector.attach}.
2043
2137
  - `ParticleBlendHint` (type): type ParticleBlendHint = "additive" | "normal" — How a particle effect composites on screen: `additive` for sparks/fire/glow, `normal` for smoke/dust.
2044
2138
  - `ParticleBurst` (interface): interface ParticleBurst — A one-shot burst request — consumed once by the renderer, never serialized.
@@ -2324,6 +2418,8 @@
2324
2418
  - `DEFAULT_LANDMARK_SHARE` (const): const DEFAULT_LANDMARK_SHARE: 0.04 — Default grand-plot share dial — a handful of block-scale parcels per default city.
2325
2419
  - `DEFAULT_MARKER_KINDS` (const): const DEFAULT_MARKER_KINDS: Record<string, MarkerKindStyle> — ⚠ undocumented
2326
2420
  - `DEFAULT_MINIMAP_PALETTE` (const): const DEFAULT_MINIMAP_PALETTE: MinimapBakePalette — The built-in height→color ramp and water color a bake uses when no palette override is given.
2421
+ - `DEFAULT_POLICE_TIERS` (const): const DEFAULT_POLICE_TIERS: readonly PoliceTier[] — The starting tier table: patrol until heat arrives, then investigate, pursue and finally cordon.
2422
+ - `DEFAULT_POLICE_TUNING` (const): const DEFAULT_POLICE_TUNING: PoliceTuning — The starting policy: linear decay, a six-second escape and a search area that grows at 2 m/s.
2327
2423
  - `DEFAULT_REPUTATION_TIERS` (const): const DEFAULT_REPUTATION_TIERS: readonly ReputationTier[] — ⚠ undocumented
2328
2424
  - `DamageDirectionOptions` (interface): interface DamageDirectionOptions — Options for {@link createDamageDirectionTracker}.
2329
2425
  - `DamageDirectionSnapshot` (interface): interface DamageDirectionSnapshot — Serializable state for save/restore — the clock reading plus every live indicator.
@@ -2401,6 +2497,7 @@
2401
2497
  - `HeatConfig` (interface): interface HeatConfig — Tuning for {@link createHeatState}/{@link advanceHeat} — levels, decay, and pursuit-spawn ring.
2402
2498
  - `HeatGain` (interface): interface HeatGain — One crime tick's contribution — only `witnessed` gains raise heat (unseen crimes are free, GTA-style).
2403
2499
  - `HeatLevelDef` (interface): interface HeatLevelDef — One escalation tier — the heat threshold it begins at and the pursuer count it wants active.
2500
+ - `HeatPoint` (type): type HeatPoint = readonly [number, number] — A world-space `[x, z]` used for the heat system's spawn ring and witness proximity checks.
2404
2501
  - `HeatSource` (interface): interface HeatSource — A localized warmth source — campfire, forge, geothermal vent.
2405
2502
  - `HeatState` (interface): interface HeatState — Serializable heat-system state — round-trips through `createHeatState`/`advanceHeat` each tick.
2406
2503
  - `HeightSampler` (type): type HeightSampler = (x: number, z: number) => number — A height sampler over the ground: world elevation at any `x`/`z`.
@@ -2579,6 +2676,22 @@
2579
2676
  - `PlatformCarry` (class): class PlatformCarry — Carries bodies standing on a moving platform by composing their transform with the platform's per-`step` delta — moving/rotating lifts and conveyor floors (Fall Guys, Gang Beasts). The platform is a body the game repositions each frame; riders are detected by overlap on its top face.
2580
2677
  - `Point3` (type): type Point3 = readonly [number, number, number] — Deterministic spatial sampling — draw one or many positions from a geometric region under explicit constraints, with an injected RNG, bounded attempts, and a structured pass/fail result.
2581
2678
  - `PointGravityOptions` (interface): interface PointGravityOptions — Center, strength, and falloff for radial gravity.
2679
+ - `PoliceLaneHit` (interface): interface PoliceLaneHit — A lane near a world point: its id, the arc of the nearest point on it, and how far away that is.
2680
+ - `PoliceResponse` (interface): interface PoliceResponse — Stateful wanted/pursuit API with retuning and save/restore support.
2681
+ - `PoliceResponseConfig` (interface): interface PoliceResponseConfig extends Partial<PoliceTuning> — Everything {@link createPoliceResponse} needs: the witness seam, the roster and the caller's policy.
2682
+ - `PoliceResponseKind` (type): type PoliceResponseKind = "patrol" | "investigate" | "pursue" | "roadblock" — How police answer the current heat tier — watching the streets up to cordoning them.
2683
+ - `PoliceResponseState` (interface): interface PoliceResponseState — Serializable response state — the heat curve, the sight timer, the anchor and the counters.
2684
+ - `PoliceRoadblockSite` (interface): interface PoliceRoadblockSite — A roadblock site on a lane, resolved from authored road data.
2685
+ - `PoliceRoads` (interface): interface PoliceRoads — Road data the response reads: nearby lanes for the search area, authored sites for roadblocks.
2686
+ - `PoliceRoadsOptions` (interface): interface PoliceRoadsOptions — How the document reader recognizes authored road content.
2687
+ - `PoliceSearchArea` (interface): interface PoliceSearchArea — Where the search is, as the last sighting places it.
2688
+ - `PoliceSighting` (interface): interface PoliceSighting — One remembered sighting of the suspect, as the perception service reports it — `PerceptionMemory` from `sensor/perception` satisfies this shape.
2689
+ - `PoliceStats` (interface): interface PoliceStats — Counters a devtools probe, a HUD or a test reads.
2690
+ - `PoliceStep` (interface): interface PoliceStep — One tick's answer: the heat, the tier, the search area, the cordon and the escape.
2691
+ - `PoliceTickContext` (interface): interface PoliceTickContext — Per-tick world facts {@link PoliceResponse.tick} needs but cannot derive itself.
2692
+ - `PoliceTier` (interface): interface PoliceTier — One response tier: the heat it begins at, what it puts on the street, and how wide it searches.
2693
+ - `PoliceTuning` (interface): interface PoliceTuning — The serializable policy of a response: the tier table, the heat curve and the search shape.
2694
+ - `PoliceWitness` (interface): interface PoliceWitness — The witness seam: perception memory, queried by observer id. Pass `createPerception` (`sensor/perception`) straight in — a crime is witnessed when an observer still holds the suspect in view, so there is no second sight path to keep in step.
2582
2695
  - `PositionedPrompt` (interface): interface PositionedPrompt { id: string; position: PromptPoint; priority?: number; prompt: ProximityPrompt } — ⚠ undocumented · used by `resolveActivePrompt`: Nearest prompt strictly within its radius wins; a higher-priority prompt in range beats any lower-priority one regardless of distance; equal…
2583
2696
  - `PositionedPromptPatch` (type): type PositionedPromptPatch = Partial<Omit<PositionedPrompt, "id">> — A partial edit applied to an already-registered {@link PositionedPrompt} by {@link PromptRegistry.update}. Any subset of the mutable fields — everything but the stable `id` — may be supplied; omitted fields keep their prior value.
2584
2697
  - `PromptRegistry` (interface): interface PromptRegistry — A live, observable set of interaction prompts that OWNS the registered {@link PositionedPrompt}s and tracks which one is active for a player. It is a thin stateful layer over {@link resolveActivePrompt}: registration/order lives here, the nearest-within-radius / higher-priority selection stays in the pure resolver. {@link resolve} is the per-frame hot path — call it as the player moves; subscribers fire only when the active prompt actually CHANGES.
@@ -2651,7 +2764,7 @@
2651
2764
  - `SceneKindResolveContext` (interface): interface SceneKindResolveContext — Ground sampler + options a resolver may read (terrain height/normal snap).
2652
2765
  - `SceneObject` (interface): interface SceneObject { instanceId: string; catalogId: string; position: EntityPosition; rotationY: number; parentSpace?: string; visual?: ObjectVisual; animation?: ModelAnimationConfig | "auto" | "none"; state?: Record<string, unknown>; slots?: InventoryState } — ⚠ undocumented · used by `fromRuntimeObjectRow` (@jgengine/core/runtime/objectRows): Inverse of {@link toRuntimeObjectRow}: rebuild the live placed object a host persisted.
2653
2766
  - `SceneObjectSlots` (interface): interface SceneObjectSlots — Per-instance container contents for placed objects whose catalog entry declares `slotInventory` — the engine side of "install a GPU into a rack", "put an item in a chest". Contents live on `SceneObject.slots`, so they replicate and save with the object rather than needing a game-owned table keyed by `instanceId`, and every mutation is validated against the declared `accepts` and stack limits.
2654
- - `SceneRaycastApi` (interface): interface SceneRaycastApi { raycast(input: SceneRaycastInput): SceneRaycastHit | null; raycastAll(input: SceneRaycastInput): SceneRaycastHit[] } — ⚠ undocumented
2767
+ - `SceneRaycastApi` (interface): interface SceneRaycastApi { raycast(input: SceneRaycastInput): SceneRaycastHit | null; raycastAll(input: SceneRaycastInput): SceneRaycastHit[] } — ⚠ undocumented · used by `sceneCameraQuery` (@jgengine/shell/camera): `CameraCollisionQuery` over the scene's existing collision query (`ctx.scene.raycast`), which already reads world solids, scene-object colli…
2655
2768
  - `SceneRaycastHit` (interface): interface SceneRaycastHit { targetKind: SceneRaycastTargetKind; instanceId: string; catalogId?: string; colliderName: string; purpose: ColliderPurpose; damageEligible: boolean; blocks: boolean; distance: number; point: EntityPosition; normal: EntityPosition } — ⚠ undocumented · used by `firstImpact`: First impact: nearest hit that blocks, or nearest hit if none block.
2656
2769
  - `ScreenEffect` (interface): interface ScreenEffect — A live effect in the current composite: its label, color, region, and the eased opacity `0..1` to draw *right now*. These objects are pooled and reused across {@link ScreenEffectsController.composite} calls — read them, don't retain.
2657
2770
  - `ScreenEffectEasing` (type): type ScreenEffectEasing = "linear" | "easeIn" | "easeOut" | "easeInOut" — How a transient effect's intensity curves from peak to zero over its life.
@@ -2929,6 +3042,7 @@
2929
3042
  - `createPlacedStructureStore` (function): function createPlacedStructureStore(): PlacedStructureStore — ⚠ undocumented
2930
3043
  - `createPlacementController` (function): function createPlacementController(config: PlacementControllerConfig): PlacementController — Headless placement ghost: hover → valid/invalid preview, rotate, grid/free/surface snap, commit. Pair with `@jgengine/shell/structures` `PlacementGhost` and {@link placeAssetFromCommit}.
2931
3044
  - `createPlotPermissions` (function): function createPlotPermissions(config: PlotPermissionConfig): PlotPermissions — ⚠ undocumented
3045
+ - `createPoliceResponse` (function): function createPoliceResponse(config: PoliceResponseConfig): PoliceResponse — The wanted level a crime sandbox needs, over the shared heat curve: a crime raises heat only while a witness holds the suspect in view, the heat bleeds on a stated linear curve once the suspect has been out of every witness's sight for `escapeSeconds`, the tier table decides what police do (patrol → investigate → pursue → roadblock), the search area sits where the suspect was last seen and grows while the trail is cold, and a roadblock tier cordons it with authored sites off the lane graph. Heat, tiers and curves are caller policy; witness facts come from `sensor/perception` memory (`sourceId`/`sourceTick` kept), so there is no second sight path.
2932
3046
  - `createPoseState` (function): function createPoseState(resolveAllowed: (instanceId: string) => PoseAllowedStates | null | undefined): PoseState — Stance/pose transitions — stand, crouch, prone — that change the hitbox and movement.
2933
3047
  - `createPromptRegistry` (function): function createPromptRegistry(): PromptRegistry — Create a live, observable interaction-prompt registry over the pure {@link resolveActivePrompt} resolver. Register positioned "press E" prompts, `resolve(playerPosition)` each frame as the hero moves, and read `active()` — the nearest strictly-in-range prompt (higher `priority` beats closer). The set is owned here (register/update/unregister/clear/all); selection stays pure. Subscribers are notified only when the active prompt *changes*, so a HUD does not thrash per frame, and `snapshot`/`restore` round-trip the prompts plus the active id through a save (display payloads are free strings/ids the game owns — the registry never branches on their meaning). Allocation-aware: `all()` and `resolve()` reuse a cached array instead of rebuilding per call.
2934
3048
  - `createRagdoll` (function): function createRagdoll(world: PhysicsWorld, config: RagdollConfig): Ragdoll — ⚠ undocumented
@@ -3058,6 +3172,7 @@
3058
3172
  - `pointInPolygon` (function): function pointInPolygon(point: Vec2, polygon: readonly Vec2[]): boolean — Ray-casting point-in-polygon test on the XZ plane.
3059
3173
  - `pointInVolume` (function): function pointInVolume(volume: SceneVolumeLike, point: { x: number; y: number; z: number }): boolean — True when `point` is inside an editor volume (sphere / cylinder / box). Cylinder height defaults to diameter when omitted; sphere ignores y for the common ground-plane case only when the volume radius covers the full vertical span — here y is tested for sphere and box too.
3060
3174
  - `pointSetRegion` (function): function pointSetRegion(points: readonly Vec2[], options: { weights?: readonly number[] } = {}): SampleRegion<Vec2> — A fixed set of candidate points sampled by (optionally weighted) selection — the "spawn point table" region. `contains` is exact membership. Empty when the set is empty.
3175
+ - `policeRoadsFromLaneGraph` (function): function policeRoadsFromLaneGraph(graph: LaneGraph, markers: readonly EditorMarker[] = [], options: PoliceRoadsOptions = {}): PoliceRoads — Read the police road data out of an authored lane graph: `traffic_roadblock` markers become cordon sites projected onto the lane they block, and lanes are indexed so `lanesNear` answers from the local cells rather than a scan of every lane. With no roadblock markers authored, the graph's intersections are the sites, so a cordon still sits where streets meet. Built once; the index is rebuildable from the document, so it is never serialized.
3061
3176
  - `polygonArea` (function): function polygonArea(polygon: readonly Vec2[]): number — Shoelace area of a polygon (always non-negative), in square meters.
3062
3177
  - `polygonBounds` (function): function polygonBounds(polygon: readonly Vec2[]): Aabb | null — Axis-aligned bounds of a polygon, or null if it has no points.
3063
3178
  - `polygonRegion` (function): function polygonRegion(polygon: readonly Vec2[]): SampleRegion<Vec2> — An arbitrary closed polygon on the XZ plane. Candidates are drawn uniformly from the polygon's bounding box and gated by point-in-polygon `contains`, so a run's draw count stays fixed (the sampler's attempt budget bounds the rejection, not a hidden inner loop).
@@ -3363,6 +3478,13 @@
3363
3478
  - `resolveBuildingProbabilities` (function): function resolveBuildingProbabilities(style: BuildingStyle = DEFAULT_BUILDING_STYLE, overrides?: Partial<BuildingProbabilities>): Partial<BuildingProbabilities> — Style facade probabilities with explicit overrides applied on top.
3364
3479
  - `resolveBuildingSurface` (function): function resolveBuildingSurface(surface: BuildingSurface): BuildingSurfaceMaterial — Normalises a surface to its material form so a renderer reads one shape.
3365
3480
 
3481
+ ## @jgengine/core/world/captureCamera
3482
+
3483
+ - `CaptureCameraOverride` (interface): interface CaptureCameraOverride — Per-session capture-camera override — the harness counterpart of `?cam=`/`?look=`, and the same install-style seam as `world/spawnOverride`: the runner installs one, the shell reads it, nothing changes until one is installed.
3484
+ - `clearCaptureCamera` (function): function clearCaptureCamera(): void — Clear any installed capture camera.
3485
+ - `installCaptureCamera` (function): function installCaptureCamera(override: CaptureCameraOverride | null): void — Install (or, with `null`, clear) the capture camera. Idempotent; last write wins. It outranks the game's static camera config and any runtime `ctx.camera` rig selection, and it is never saved — it is a property of this capture session, not of the game.
3486
+ - `readCaptureCamera` (function): function readCaptureCamera(): CaptureCameraOverride | null — The installed capture camera, or `null` when the game's own camera should render.
3487
+
3366
3488
  ## @jgengine/core/world/carve
3367
3489
 
3368
3490
  - `CarvableField` (class): class CarvableField implements TerrainField — A `TerrainField` you can write craters and mounds into at runtime — the height-field side of destructible terrain (Helldivers 2 explosion craters, engineer-deposited berms). Wraps a base field and layers smooth radial deformations on top, so `sampleHeight` (and therefore ground-snap, collision, and the shell's terrain mesh) all read the deformed surface. `carve` digs a bowl, `deposit` raises a mound.
@@ -3631,7 +3753,7 @@
3631
3753
  - `TerrainIslandConfig` (interface): interface TerrainIslandConfig extends TerrainEnvironmentConfig — A bounded terrain patch floating at its own altitude — sky islands, arena platforms, split landmasses with void between.
3632
3754
  - `TerrainIslandDescriptor` (type): type TerrainIslandDescriptor = Omit<TerrainEnvironmentDescriptor, "kind"> & { kind: "island"; origin: EnvironmentVec2; } — ⚠ undocumented
3633
3755
  - `TerrainMaterial` (type): type TerrainMaterial = "grass" | "sand" | "snow" | "rock" | "ash" | "highland" | "slate" — ⚠ undocumented · used by `TERRAIN_MATERIALS` (@jgengine/core/editor): The default terrain paint palette (surface id → color) shared by the panel and the mesh.
3634
- - `TerrainMaterialMaps` (interface): interface TerrainMaterialMaps — PBR map URLs for a real ground texture — the same shape `buildMaterialCatalog({ basePath }).resolve(id)!.maps` from `@jgengine/assets` returns. Kept dependency-free here so `core` never imports the assets package; any URLs (pulled maps, a CDN, a data URI) satisfy it. Omitted roughness keeps the terrain scalar; omitted AO skips occlusion sampling.
3756
+ - `TerrainMaterialMaps` (interface): interface TerrainMaterialMaps — PBR map URLs for a real ground texture — the same shape `libraryTerrainMaterial(name).maps` from `@jgengine/assets` returns. Kept dependency-free here so `core` never imports the assets package; any URLs (pulled maps, a CDN, a data URI) satisfy it. Omitted roughness keeps the terrain scalar; omitted AO skips occlusion sampling.
3635
3757
  - `TerrainMaterialRegion` (type): type TerrainMaterialRegion = TerrainCircleRegion | TerrainPolylineRegion | TerrainRectRegion — A palette zone painted over the base terrain palette. Circle (the default when no `shape` is given), `polyline` ribbons for roads/rivers, and rotatable `rect` districts all paint fully inside their core and blend back across `falloff`; later regions in the list win overlaps.
3636
3758
  - `TerrainPolylineRegion` (interface): interface TerrainPolylineRegion extends TerrainRegionStyle — A ribbon palette zone following a centerline — roads and rivers, instead of chaining overlapping circles.
3637
3759
  - `TerrainRectRegion` (interface): interface TerrainRectRegion extends TerrainRegionStyle — A rectangular palette zone, optionally rotated about the world y axis — plazas, fields, districts.