@jgengine/shell 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/dist/GamePlayerShell.d.ts +8 -0
  3. package/dist/GamePlayerShell.js +15 -1
  4. package/dist/GameUiPreview.js +3 -1
  5. package/dist/Shell3dPresentation.js +6 -4
  6. package/dist/ShellChrome.js +3 -1
  7. package/dist/camera/GameCameraRig.js +2 -2
  8. package/dist/camera/GameFirstPersonCamera.js +39 -2
  9. package/dist/camera/cameraCollision.d.ts +66 -1
  10. package/dist/camera/cameraCollision.js +72 -1
  11. package/dist/camera/index.d.ts +1 -1
  12. package/dist/camera/index.js +1 -1
  13. package/dist/camera/rigResolve.d.ts +13 -0
  14. package/dist/camera/rigResolve.js +16 -0
  15. package/dist/environment/EnvironmentScene.js +118 -10
  16. package/dist/postfx/shadowPrepass.d.ts +8 -1
  17. package/dist/postfx/shadowPrepass.js +18 -2
  18. package/dist/postfx/stylizePass.js +2 -1
  19. package/dist/render/CascadedShadows.d.ts +31 -1
  20. package/dist/render/CascadedShadows.js +71 -5
  21. package/dist/render/CharacterCrowd.d.ts +20 -0
  22. package/dist/render/CharacterCrowd.js +176 -0
  23. package/dist/render/SceneLighting.js +27 -3
  24. package/dist/render/SceneWarmupGate.d.ts +20 -0
  25. package/dist/render/SceneWarmupGate.js +48 -0
  26. package/dist/render/characterLod.d.ts +62 -0
  27. package/dist/render/characterLod.js +165 -0
  28. package/dist/render/crowdRegistry.d.ts +48 -0
  29. package/dist/render/crowdRegistry.js +97 -0
  30. package/dist/render/decalGeometry.d.ts +26 -0
  31. package/dist/render/decalGeometry.js +54 -0
  32. package/dist/render/decalTexture.d.ts +13 -0
  33. package/dist/render/decalTexture.js +114 -0
  34. package/dist/render/materialAsset.js +2 -1
  35. package/dist/render/modelLoad.js +3 -0
  36. package/dist/render/modelMeshMerge.d.ts +49 -0
  37. package/dist/render/modelMeshMerge.js +269 -0
  38. package/dist/render/modelWind.d.ts +28 -0
  39. package/dist/render/modelWind.js +103 -0
  40. package/dist/render/shaderWarmup.d.ts +69 -0
  41. package/dist/render/shaderWarmup.js +120 -0
  42. package/dist/render/shadowCasters.d.ts +28 -0
  43. package/dist/render/shadowCasters.js +95 -0
  44. package/dist/render/staticTransforms.d.ts +20 -0
  45. package/dist/render/staticTransforms.js +32 -0
  46. package/dist/render/useFootIk.d.ts +11 -3
  47. package/dist/render/useFootIk.js +35 -7
  48. package/dist/render/useModelAnimation.d.ts +16 -2
  49. package/dist/render/useModelAnimation.js +73 -4
  50. package/dist/render/useModelInstance.d.ts +1 -1
  51. package/dist/render/useModelInstance.js +26 -1
  52. package/dist/render/useRagdoll.d.ts +32 -0
  53. package/dist/render/useRagdoll.js +105 -0
  54. package/dist/scatter/scatterModels.js +2 -0
  55. package/dist/scene/DecalRenderer.d.ts +2 -0
  56. package/dist/scene/DecalRenderer.js +74 -0
  57. package/dist/scene/GrassFieldRenderer.js +3 -2
  58. package/dist/scene/builtinSceneKindRenderers.js +2 -0
  59. package/dist/settings/SettingsMenu.d.ts +1 -1
  60. package/dist/settings/SettingsMenu.js +10 -79
  61. package/dist/settings/settingsController.js +3 -0
  62. package/dist/terrain/GrassCover.d.ts +21 -0
  63. package/dist/terrain/GrassCover.js +84 -0
  64. package/dist/terrain/grassCoverGeometry.d.ts +40 -0
  65. package/dist/terrain/grassCoverGeometry.js +112 -0
  66. package/dist/terrain/terrainDetailMaterial.d.ts +12 -0
  67. package/dist/terrain/terrainDetailMaterial.js +54 -1
  68. package/dist/terrain/terrainSurfaces.d.ts +6 -0
  69. package/dist/terrain/terrainSurfaces.js +10 -0
  70. package/dist/world/StaticObjectModels.d.ts +13 -0
  71. package/dist/world/StaticObjectModels.js +28 -3
  72. package/dist/world/WorldParticles.d.ts +2 -1
  73. package/dist/world/WorldParticles.js +29 -10
  74. package/dist/world/WorldScene.js +52 -14
  75. package/dist/world/entityPose.d.ts +17 -0
  76. package/dist/world/entityPose.js +42 -0
  77. package/dist/world/staticObjectBatches.d.ts +33 -5
  78. package/dist/world/staticObjectBatches.js +40 -6
  79. package/package.json +4 -4
  80. package/skills/jgengine/api.md +2 -2
  81. package/skills/jgengine-ui/SKILL.md +1 -0
  82. package/skills/jgengine-ui/api.md +231 -9
  83. package/skills/jgengine-ui/capabilities.md +107 -1
  84. package/skills/jgengine-ui/reference-primitives.md +2 -0
  85. package/skills/jgengine-ui/reference.md +8 -0
  86. package/skills/jgengine-world/SKILL.md +8 -0
  87. package/skills/jgengine-world/api.md +243 -11
  88. package/skills/jgengine-world/capabilities.md +61 -1
  89. package/skills/jgengine-world/recipes/character-animation.md +25 -2
  90. package/skills/jgengine-world/recipes/vehicle-feel.md +5 -0
  91. package/skills/jgengine-world/reference.md +1 -1
@@ -118,6 +118,23 @@
118
118
  - `createSpriteClipPlayer` (function): function createSpriteClipPlayer(atlas: SpriteAtlas): { play(name: string): void; advance(dt: number): void; frame: () => { x: number; y: number; w: number; h: number; pivot?: [number, number] | undefined; } | undefined; snapshot: () => { animation: string; frameIndex: number; elapsed: number; done: … — Create a renderer-independent sprite animation player.
119
119
  - `sortingOrder` (function): function sortingOrder(layers: readonly string[], layer: string, offset = 0): number — Resolve a layer name and local offset into a stable render order.
120
120
 
121
+ ## @jgengine/core/settings/accessibilitySettings
122
+
123
+ - `ACCESSIBILITY_SETTING_IDS` (const): const ACCESSIBILITY_SETTING_IDS: { readonly textScale: "accessibility.textScale"; readonly colorblind: "accessibility.colorblind"; readonly reducedMotion: "accessibility.reducedMotion"; readonly highContrast: "accessibility.highContrast"; readonly captions: "accessibility.captions"; readonly holdVsT… — The persisted settings ids behind the accessibility section.
124
+ - `AccessibilitySettingRow` (interface): interface AccessibilitySettingRow — One accessibility setting as the shared settings menu renders it.
125
+ - `COLORBLIND_OPTIONS` (const): const COLORBLIND_OPTIONS: readonly SettingOption[] — Colorblind palette choices. `none` keeps the game's own colours.
126
+ - `HOLD_VS_TOGGLE_OPTIONS` (const): const HOLD_VS_TOGGLE_OPTIONS: readonly SettingOption[] — Hold-versus-toggle choices for actions that support both.
127
+ - `HoldToggleInput` (interface): interface HoldToggleInput — One frame of an action's raw input.
128
+ - `HoldVsToggle` (type): type HoldVsToggle = "hold" | "toggle" — A "hold or toggle" preference for actions that can be either (aim, sprint, crouch, block). Shared because players expect one setting to cover them all, rather than each game inventing a flag.
129
+ - `TEXT_SCALE_OPTIONS` (const): const TEXT_SCALE_OPTIONS: readonly SettingOption[] — Text-scale choices, as numeric strings the settings store persists.
130
+ - `accessibilitySettingDefs` (function): function accessibilitySettingDefs(category = "accessibility"): GameSettingDef[] — Declarative form of the accessibility rows for `defineGame({ settings })`, for a game that wants them inside its own setting categories rather than the built-in `accessibility` one.
131
+ - `accessibilitySettingRows` (function): function accessibilitySettingRows(store: SettingsStore): AccessibilitySettingRow[] — The accessibility rows, wired to the settings store: text scale, colorblind palette, reduced motion, high contrast, captions and hold-vs-toggle. The shell adds them as its `accessibility` category and any game can render them inside its own settings layout.
132
+ - `holdToggleActive` (function): function holdToggleActive(previous: boolean, input: HoldToggleInput, mode: HoldVsToggle): boolean — The action's active state for one frame under a {@link HoldVsToggle} preference: `hold` follows the button, `toggle` latches on each press edge. Call with the previous result so toggle mode can flip without the caller keeping a second flag.
133
+ - `readAccessibilitySettings` (function): function readAccessibilitySettings(store: SettingsStore): { state: AccessibilityState; holdVsToggle: HoldVsToggle; } — Read the whole accessibility state the presentation needs (reduced motion, high contrast, text scale, colorblind mode, captions) plus the hold/toggle preference, from the shared settings store.
134
+ - `readColorblind` (function): function readColorblind(store: SettingsStore): ColorblindMode — Read a stored colorblind mode, falling back to `none` for an unknown value.
135
+ - `readHoldVsToggle` (function): function readHoldVsToggle(store: SettingsStore): HoldVsToggle — Read the hold-or-toggle preference, falling back to `hold`.
136
+ - `readTextScale` (function): function readTextScale(store: SettingsStore): number — Read a stored text scale, clamped to the supported range.
137
+
121
138
  ## @jgengine/core/settings/frameRateLimit
122
139
 
123
140
  - `FRAME_RATE_LIMIT_DISPLAY` (const): const FRAME_RATE_LIMIT_DISPLAY: "display" — Stored value for "render every display refresh" — the browser's vsync-locked rate.
@@ -153,7 +170,7 @@
153
170
  ## @jgengine/core/settings/settingsModel
154
171
 
155
172
  - `BUILT_IN_SETTING_CATEGORIES` (const): const BUILT_IN_SETTING_CATEGORIES: readonly BuiltInSettingCategory[] — ⚠ undocumented
156
- - `BuiltInSettingCategory` (type): type BuiltInSettingCategory = "sound" | "graphics" | "gameplay" | "controls" — ⚠ undocumented
173
+ - `BuiltInSettingCategory` (type): type BuiltInSettingCategory = "sound" | "graphics" | "gameplay" | "controls" | "accessibility" — ⚠ undocumented
157
174
  - `DEFAULT_GRAPHICS_QUALITY` (const): const DEFAULT_GRAPHICS_QUALITY: GraphicsQuality — ⚠ undocumented
158
175
  - `DEFAULT_GRAPHICS_SHADOWS` (const): const DEFAULT_GRAPHICS_SHADOWS: true — ⚠ undocumented
159
176
  - `DEFAULT_MASTER_VOLUME` (const): const DEFAULT_MASTER_VOLUME: 1 — ⚠ undocumented
@@ -168,7 +185,7 @@
168
185
  - `SettingCategory` (type): type SettingCategory = BuiltInSettingCategory | (string & {}) — Built-in category ids keep autocomplete; any other string makes a fresh category.
169
186
  - `SettingCategoryDef` (interface): interface SettingCategoryDef — Declares or relabels/reorders a category tab; use it for a custom category or to reshape the built-ins.
170
187
  - `SettingKind` (type): type SettingKind = "slider" | "toggle" | "select" — ⚠ undocumented
171
- - `SettingOption` (interface): interface SettingOption { value: string; label: string } — ⚠ undocumented · used by `FRAME_RATE_LIMIT_OPTIONS` (@jgengine/core/settings/frameRateLimit): Frame-rate limit choices.
188
+ - `SettingOption` (interface): interface SettingOption { value: string; label: string } — ⚠ undocumented · used by `COLORBLIND_OPTIONS` (@jgengine/core/settings/accessibilitySettings): Colorblind palette choices.
172
189
  - `SettingValue` (type): type SettingValue = number | boolean | string — ⚠ undocumented · used by `useSetting` (@jgengine/react): Read + write one persisted setting; re-renders when the value changes anywhere.
173
190
  - `SettingsActionDef` (interface): interface SettingsActionDef — A game-state action (Restart, Quit to menu, …) shown as rows in the first "Game" settings tab — never a floating button or a rebindable key.
174
191
  - `SettingsStore` (interface): interface SettingsStore { get<T extends SettingValue>(id: string, fallback: T): T; set(id: string, value: SettingValue): void; subscribe(listener: () => void): () => void } — ⚠ undocumented · used by `createSettingsStore`: Reactive, localStorage-backed settings store shared by the shell wiring and React hooks.
@@ -239,8 +256,18 @@
239
256
  - `LayoutRegion` (interface): interface LayoutRegion — A physical rectangle a UI subsystem occupies, published to the shared registry.
240
257
  - `Locale` (type): type Locale = string — A locale tag (e.g. `"en"`, `"es"`, `"ja"`) — a key into a {@link Catalog}.
241
258
  - `LookPreset` (type): type LookPreset = "neutral" | "photoreal" | "toon" | "comic" | "retro" | "cinematic" | "flat" — Named look preset composing the existing lighting/sky/fog/post knobs into one field. `"neutral"` (the default when unset) keeps presentation unstyled; `"cinematic"` draws a scene lit like a shipped game — a real day sky with a view-following shadow-casting sun + hemisphere fill, a network-free image-based-lighting environment so PBR surfaces catch soft reflections, and a tuned tone-map/bloom/AO/vignette post stack. `"comic"` adds ink outlines and cel bands over a saturated grade (Borderlands-style); `"retro"` pixelates and posterizes the frame. `"flat"` opts out of the sky/IBL/post rig to the bare ambient+directional default (pre-#773). The upgraded default primitive materials — tuned roughness/metalness plus subtle procedural surface detail so un-modeled boxes/capsules stop reading as flat plastic — apply under both presets.
259
+ - `MENU_KEY_INTENTS` (const): const MENU_KEY_INTENTS: Readonly<Record<string, MenuIntent>> — Keyboard codes that drive a menu, by `KeyboardEvent.code`. Tab is deliberately absent: it stays native.
242
260
  - `MODAL_CANCEL` (const): const MODAL_CANCEL: "cancel" — Conventional cancel/dismiss result (also the default auto-dismiss and pop result).
243
261
  - `MODAL_CONFIRM` (const): const MODAL_CONFIRM: "confirm" — Conventional confirm result for a two-button dialog — a shared string, not model behavior.
262
+ - `MenuIntent` (type): type MenuIntent = MenuNavDirection | "confirm" | "back" — Everything a key or gamepad button can ask a menu to do.
263
+ - `MenuNavDirection` (type): type MenuNavDirection = "up" | "down" | "left" | "right" | "first" | "last" — Headless focus-order model for a game front-end menu: a list or grid of item ids, one focused item, and the keyboard/gamepad intents that move between them. It holds no React state, no DOM and no look — `@jgengine/react/menuNav` binds it to a container and real focus, and each game decides what the items are and where they sit.
264
+ - `MenuNavHold` (interface): interface MenuNavHold — When a held direction was pressed and when it last moved the focus.
265
+ - `MenuNavLayout` (interface): interface MenuNavLayout — The shape of the item list being navigated: how many items, how they wrap into rows.
266
+ - `MenuNavRepeat` (interface): interface MenuNavRepeat — Hold-to-repeat timing for a held d-pad/stick: one immediate move, then repeats.
267
+ - `MenuTransition` (interface): interface MenuTransition — Observable enter/exit phase machine.
268
+ - `MenuTransitionConfig` (interface): interface MenuTransitionConfig — Durations and accessibility policy for one transition.
269
+ - `MenuTransitionPhase` (type): type MenuTransitionPhase = "closed" | "enter" | "open" | "exit" — The phase timing behind an animated menu screen: closed → entering → open → exiting → closed. Pure timing over an injected clock, so a React block can animate with CSS and still know exactly when an exit finished and the screen may unmount. Reduced motion collapses every duration to zero instead of skipping the state machine, so callers never branch on it.
270
+ - `MenuTransitionSnapshot` (interface): interface MenuTransitionSnapshot — Serializable phase state.
244
271
  - `Messages` (type): type Messages = Readonly<Record<string, string>> — Flat message table for one locale: key → template string with `{param}` slots.
245
272
  - `MobileHudBehavior` (type): type MobileHudBehavior = | "persistent" | "compact" | "icon" | "transient" | "hidden" | "sheet" | "modal" — How a HUD element adapts on phones.
246
273
  - `ModalInput` (type): type ModalInput = Omit<ModalRecord, "id"> & { id?: string } — A modal to push — {@link ModalRecord} with an optional id (auto-assigned when absent).
@@ -282,7 +309,7 @@
282
309
  - `SettingCategory` (type): type SettingCategory = BuiltInSettingCategory | (string & {}) — Built-in category ids keep autocomplete; any other string makes a fresh category.
283
310
  - `SettingCategoryDef` (interface): interface SettingCategoryDef — Declares or relabels/reorders a category tab; use it for a custom category or to reshape the built-ins.
284
311
  - `SettingKind` (type): type SettingKind = "slider" | "toggle" | "select" — ⚠ undocumented
285
- - `SettingOption` (interface): interface SettingOption { value: string; label: string } — ⚠ undocumented · used by `FRAME_RATE_LIMIT_OPTIONS` (@jgengine/core/settings/frameRateLimit): Frame-rate limit choices.
312
+ - `SettingOption` (interface): interface SettingOption { value: string; label: string } — ⚠ undocumented · used by `COLORBLIND_OPTIONS` (@jgengine/core/settings/accessibilitySettings): Colorblind palette choices.
286
313
  - `SettingValue` (type): type SettingValue = number | boolean | string — ⚠ undocumented · used by `useSetting` (@jgengine/react): Read + write one persisted setting; re-renders when the value changes anywhere.
287
314
  - `SettingsActionDef` (interface): interface SettingsActionDef — A game-state action (Restart, Quit to menu, …) shown as rows in the first "Game" settings tab — never a floating button or a rebindable key.
288
315
  - `SettingsStore` (interface): interface SettingsStore { get<T extends SettingValue>(id: string, fallback: T): T; set(id: string, value: SettingValue): void; subscribe(listener: () => void): () => void } — ⚠ undocumented · used by `createSettingsStore`: Reactive, localStorage-backed settings store shared by the shell wiring and React hooks.
@@ -315,6 +342,7 @@
315
342
  - `createAccessibilityStore` (function): function createAccessibilityStore(initial: Partial<AccessibilityState> = {}): AccessibilityStore — Serializable, observable store of accessibility preferences (reduced motion, high contrast, text scale, colorblind mode, captions). Genre-agnostic plumbing like input rebinding — a game binds these to its settings UI and adapts its presentation; `snapshot`/`restore` round-trip through a save or settings blob.
316
343
  - `createCoachMarkSequence` (function): function createCoachMarkSequence(options: CoachMarkSequenceOptions): CoachMarkSequence — An onboarding coach-mark run: an ordered sequence of hint steps, each shown as a centered callout or anchored to a UI element, with a persisted "seen" set so completed onboarding stays done across sessions, and data-first condition gating so a step waits until its trigger id is satisfied. Advance/skip/dismiss drive it; `snapshot`/`restore` round-trip the seen set through a save. Purely a model — a React host renders `current()`.
317
344
  - `createI18n` (function): function createI18n(options: I18nOptions): I18n — Create a translator over a message catalog: active-locale lookup with a fallback-locale chain, `{param}` interpolation, and `Intl.PluralRules`-based pluralization. Observable (`subscribe`) so React re-renders on `setLocale`; the catalog is caller-owned static data, the locale is the only state.
345
+ - `createMenuTransition` (function): function createMenuTransition(config: MenuTransitionConfig = {}, initial?: MenuTransitionSnapshot): MenuTransition — Create a {@link MenuTransition}. `open`/`close` start a phase; `tick` advances it once its duration has elapsed. Nothing schedules timers here — the caller ticks from its animation loop or a timeout.
318
346
  - `createModalStack` (function): function createModalStack(options: ModalStackOptions = {}): ModalStack — A stack of modal dialogs: push opaque records to open blocking overlays (a pause menu, a confirm prompt, a reward popup), pop or resolve them with a free-form result to close, and query the top / open / depth to gate game input. The model is deliberately vocabulary-free — it never reads a record's `kind` or a resolution's `result`, so a game owns every dialog style — and fully serializable, with an optional injected clock driving per-modal auto-dismiss. A React host renders `top()`; `subscribe` re-renders it, `snapshot`/`restore` round-trip it through a save.
319
347
  - `createObjectiveBanner` (function): function createObjectiveBanner(options: ObjectiveBannerOptions = {}): ObjectiveBannerController — A serializable, clock-driven stage/objective banner controller: the classic transient centered title stamp ("WAVE 3", "VICTORY", "OBJECTIVE COMPLETE"). A game calls `announce({ title, subtitle?, kind? })` and the controller queues it, flies it in, holds it, and fades it out on an injected clock — one banner at a time — exposing the current banner plus its phase (in/hold/out) and `progress` `0..1` for a renderer to animate opacity/scale/translate. Nothing here is genre-specific: `kind` is a free label the game owns and styles, and all timings are parameters. Call `advance()` each frame; `current()` returns the pooled view to draw; `snapshot`/`restore` round-trips the queue through a save.
320
348
  - `createPanelState` (function): function createPanelState(defs: readonly PanelDef[]): PanelState — Build the initial {@link PanelState} from the panel defs: panels flagged `initial` start open (in declaration order, respecting group exclusivity), and every def's group/closable is captured into the state so later reducers need no defs.
@@ -332,6 +360,14 @@
332
360
  - `isOpen` (function): function isOpen(state: PanelState, id: string): boolean — Whether panel `id` is currently open.
333
361
  - `layoutScreenMarker` (function): function layoutScreenMarker(projection: ScreenProjection, viewport: ScreenMarkerViewport, options: ScreenMarkerOptions = {}): ScreenMarkerLayout — Pure, allocation-aware screen-marker math: given a projected target point, the viewport, and a `margin`, decide whether the target is on-screen and where to draw its marker. An on-screen point passes straight through (`onScreen: true`). An off-screen — or behind-camera — point is CLAMPED to the viewport edge (inset by `margin`) with `angle` set to the bearing from screen center toward the target, so a caller can pin an arrow to the frame edge and rotate it to point the way. Behind-camera projections have mirrored coordinates, so the bearing is flipped to face the true direction. This is the edge-clamp/arrow half that `layoutEntityFrames` lacks (it culls off-screen entries instead). No allocation when an `out` object is supplied — reuse one across a whole frame's waypoints.
334
362
  - `listHudPanelTypes` (function): function listHudPanelTypes(): HudPanelTypeDef[] — Every registered panel type, sorted by id — editor palette / agent listing.
363
+ - `menuIntentForKey` (function): function menuIntentForKey(code: string): MenuIntent | null — Resolve a `KeyboardEvent.code` to a menu intent, or `null` for keys the menu leaves alone (typing, Tab, shortcuts). Escape and Backspace both mean back; Space and Enter both mean confirm.
364
+ - `menuIntentForPadAxis` (function): function menuIntentForPadAxis(axis: number, value: number, deadzone = 0.55): MenuNavDirection | null — Resolve a left-stick axis sample to a direction. Axis `0` is horizontal and axis `1` vertical (standard mapping, up is negative); the larger magnitude wins so a diagonal push is one move.
365
+ - `menuIntentForPadButton` (function): function menuIntentForPadButton(button: number): MenuIntent | null — Standard-mapping gamepad buttons as menu intents: A/Cross confirms, B/Circle and Start back, the d-pad moves. Buttons with no menu meaning return `null`.
366
+ - `menuNavIdAt` (function): function menuNavIdAt(ids: readonly string[], index: number): string | null — The id at `index`, or `null` outside the list.
367
+ - `menuNavIndex` (function): function menuNavIndex(ids: readonly string[], id: string | null | undefined): number — The index of `id` in the focus order, or `-1` when it is not part of this menu.
368
+ - `menuNavMove` (function): function menuNavMove(index: number, direction: MenuNavDirection, layout: MenuNavLayout): number — The next focused index for a direction, or the same index when the move is a no-op. Moves skip `disabled` indices and stop (rather than wrap) when every item in the way is disabled, so a menu can never hand focus to a dead row.
369
+ - `menuTransitionDurations` (function): function menuTransitionDurations(config: MenuTransitionConfig = {}): { enter: number; exit: number } — The enter and exit durations after reduced-motion is applied.
370
+ - `menuTransitionPhaseMs` (function): function menuTransitionPhaseMs(phase: MenuTransitionPhase, config: MenuTransitionConfig = {}): number — How long the current phase lasts, in ms (0 for open/closed).
335
371
  - `moveGridFocus` (function): function moveGridFocus(current: number, count: number, columns: number, dir: FocusDirection, options?: GridFocusOptions): number — Pure grid focus math for keyboard and controller navigation of an action card laid out in `columns`-wide rows. Given the current index (`-1` for none) it returns the next index, clamped to `[0, count)` — or wrapped when `wrap` is set. `left/right` move within/across columns, `up/down` across rows, `next/prev` walk the flat order, `first/last` jump to the ends. Renderer-agnostic: radial and list layouts pass `columns: 1` (or `count`) to get sensible 1-D stepping.
336
372
  - `movePanel` (function): function movePanel(state: PanelState, id: string, pos: PanelPosition): PanelState — Set panel `id`'s position override — the drag commit. Does not require the panel to be open, so a caller can pre-place a window before it is shown.
337
373
  - `moveSelectionFocus` (function): function moveSelectionFocus(count: number, current: number, dir: SelectionFocusDirection): number — Pure next-focus math for the selection strip: step the focus index by one member (wrapping), or jump to an end. `-1` (no focus) steps to the first/last member. Returns `-1` for an empty selection.
@@ -356,6 +392,7 @@
356
392
  - `resolveHudPanelLayout` (function): function resolveHudPanelLayout(docPanel: EditorUiPanelLayout | undefined, fallback: HudPanelFallback): ResolvedHudPanelLayout — Resolve a panel's layout: document entry wins field-by-field over TSX fallback. TSX props are fallback-only — the scene document is the source of truth once authored.
357
393
  - `resolveOrientationRequirement` (function): function resolveOrientationRequirement(orientation: GameOrientation | undefined, platform: "mobile" | "desktop"): OrientationRequirement — Resolve the game's orientation declaration into a concrete requirement for a platform. Desktop is always unconstrained.
358
394
  - `selectionWindow` (function): function selectionWindow(members: readonly EntitySummaryDef[], scroll: number, size: number): SelectionWindow — Compute a virtualization window over a large selection: a `size`-length slice starting near `scroll`, clamped so the window never runs past the end. Render only `items`, offsetting by `start * itemSize`, to keep a thousand-unit selection from mounting a thousand portraits.
395
+ - `shouldRepeatMenuNav` (function): function shouldRepeatMenuNav(now: number, hold: MenuNavHold, repeat: MenuNavRepeat = {}): boolean — Whether a held direction should fire a move at `now`: immediately on press, then only after `delayMs`, and no more often than `intervalMs`. Pure timing, so a gamepad poll and a keydown handler share one rule.
359
396
  - `summarizeSelection` (function): function summarizeSelection(members: readonly EntitySummaryDef[], options?: SummarizeSelectionOptions): SelectionView — Summarize a selected-entity list into a {@link SelectionView}: resolve the primary focus, keep the ordered members, and bucket them by `kind`. Pure and stable — deterministic group ordering and a clamped focus index, so it is safe to call every render and to assert on in a test.
360
397
  - `swingTimerState` (function): function swingTimerState(player: SwingPlayerInput, target: SwingTargetInput | null, prevPeriod: number, prevTimer: number): SwingTimerState — Pure swing-timer bar state — no hidden state, no clock, no DOM. The caller threads `prevPeriod`/`prevTimer` back each frame. The period is recovered on the reset edge (when `swingTimer` jumps up = a new swing began) as `max(swingTimer, weapon.speed)`, so the fill is correct even without knowing the weapon's exact cadence. Hidden unless auto-attacking a live, non-object target.
361
398
  - `togglePanel` (function): function togglePanel(state: PanelState, id: string): PanelState — Toggle panel `id` — open it (with group exclusivity) if closed, close it if open. The one-key behavior behind a keybind like `B` for the bag.
@@ -465,6 +502,39 @@
465
502
  - `rectOverflow` (function): function rectOverflow(rect: HudRect, viewport: HudSize, tolerance = 1.5): Omit<HudOverflow, "id"> | null — Non-null when any edge of `rect` falls outside `viewport` by more than `tolerance` px; each field is the overflow distance past that edge (0 when inside).
466
503
  - `resolveHudFit` (function): function resolveHudFit(config: HudViewportConfig | undefined, mobile: boolean): Required<HudFitConfig> — ⚠ undocumented
467
504
 
505
+ ## @jgengine/core/ui/loadingTips
506
+
507
+ - `TipRotation` (interface): interface TipRotation — A deterministic, cycle-through tip list.
508
+ - `TipRotationConfig` (interface): interface TipRotationConfig — The tip rotation behind a loading screen: a deterministic shuffle of the game's tips, shown one at a time, cycling without repeating until the list is exhausted. Deterministic for a given seed, so a capture or a test sees the same first tip every run. Presentation-free — the loading block owns the look and the timer.
509
+ - `TipRotationSnapshot` (interface): interface TipRotationSnapshot — Serializable rotation state.
510
+ - `createTipRotation` (function): function createTipRotation(config: TipRotationConfig): TipRotation — Create a {@link TipRotation} over the game's tips. Order is a seeded shuffle; `next()` walks it and wraps, so every tip is seen once per cycle instead of a random repeat.
511
+
512
+ ## @jgengine/core/ui/menuNav
513
+
514
+ - `MENU_KEY_INTENTS` (const): const MENU_KEY_INTENTS: Readonly<Record<string, MenuIntent>> — Keyboard codes that drive a menu, by `KeyboardEvent.code`. Tab is deliberately absent: it stays native.
515
+ - `MenuIntent` (type): type MenuIntent = MenuNavDirection | "confirm" | "back" — Everything a key or gamepad button can ask a menu to do.
516
+ - `MenuNavDirection` (type): type MenuNavDirection = "up" | "down" | "left" | "right" | "first" | "last" — Headless focus-order model for a game front-end menu: a list or grid of item ids, one focused item, and the keyboard/gamepad intents that move between them. It holds no React state, no DOM and no look — `@jgengine/react/menuNav` binds it to a container and real focus, and each game decides what the items are and where they sit.
517
+ - `MenuNavHold` (interface): interface MenuNavHold — When a held direction was pressed and when it last moved the focus.
518
+ - `MenuNavLayout` (interface): interface MenuNavLayout — The shape of the item list being navigated: how many items, how they wrap into rows.
519
+ - `MenuNavRepeat` (interface): interface MenuNavRepeat — Hold-to-repeat timing for a held d-pad/stick: one immediate move, then repeats.
520
+ - `menuIntentForKey` (function): function menuIntentForKey(code: string): MenuIntent | null — Resolve a `KeyboardEvent.code` to a menu intent, or `null` for keys the menu leaves alone (typing, Tab, shortcuts). Escape and Backspace both mean back; Space and Enter both mean confirm.
521
+ - `menuIntentForPadAxis` (function): function menuIntentForPadAxis(axis: number, value: number, deadzone = 0.55): MenuNavDirection | null — Resolve a left-stick axis sample to a direction. Axis `0` is horizontal and axis `1` vertical (standard mapping, up is negative); the larger magnitude wins so a diagonal push is one move.
522
+ - `menuIntentForPadButton` (function): function menuIntentForPadButton(button: number): MenuIntent | null — Standard-mapping gamepad buttons as menu intents: A/Cross confirms, B/Circle and Start back, the d-pad moves. Buttons with no menu meaning return `null`.
523
+ - `menuNavIdAt` (function): function menuNavIdAt(ids: readonly string[], index: number): string | null — The id at `index`, or `null` outside the list.
524
+ - `menuNavIndex` (function): function menuNavIndex(ids: readonly string[], id: string | null | undefined): number — The index of `id` in the focus order, or `-1` when it is not part of this menu.
525
+ - `menuNavMove` (function): function menuNavMove(index: number, direction: MenuNavDirection, layout: MenuNavLayout): number — The next focused index for a direction, or the same index when the move is a no-op. Moves skip `disabled` indices and stop (rather than wrap) when every item in the way is disabled, so a menu can never hand focus to a dead row.
526
+ - `shouldRepeatMenuNav` (function): function shouldRepeatMenuNav(now: number, hold: MenuNavHold, repeat: MenuNavRepeat = {}): boolean — Whether a held direction should fire a move at `now`: immediately on press, then only after `delayMs`, and no more often than `intervalMs`. Pure timing, so a gamepad poll and a keydown handler share one rule.
527
+
528
+ ## @jgengine/core/ui/menuTransition
529
+
530
+ - `MenuTransition` (interface): interface MenuTransition — Observable enter/exit phase machine.
531
+ - `MenuTransitionConfig` (interface): interface MenuTransitionConfig — Durations and accessibility policy for one transition.
532
+ - `MenuTransitionPhase` (type): type MenuTransitionPhase = "closed" | "enter" | "open" | "exit" — The phase timing behind an animated menu screen: closed → entering → open → exiting → closed. Pure timing over an injected clock, so a React block can animate with CSS and still know exactly when an exit finished and the screen may unmount. Reduced motion collapses every duration to zero instead of skipping the state machine, so callers never branch on it.
533
+ - `MenuTransitionSnapshot` (interface): interface MenuTransitionSnapshot — Serializable phase state.
534
+ - `createMenuTransition` (function): function createMenuTransition(config: MenuTransitionConfig = {}, initial?: MenuTransitionSnapshot): MenuTransition — Create a {@link MenuTransition}. `open`/`close` start a phase; `tick` advances it once its duration has elapsed. Nothing schedules timers here — the caller ticks from its animation loop or a timeout.
535
+ - `menuTransitionDurations` (function): function menuTransitionDurations(config: MenuTransitionConfig = {}): { enter: number; exit: number } — The enter and exit durations after reduced-motion is applied.
536
+ - `menuTransitionPhaseMs` (function): function menuTransitionPhaseMs(phase: MenuTransitionPhase, config: MenuTransitionConfig = {}): number — How long the current phase lasts, in ms (0 for open/closed).
537
+
468
538
  ## @jgengine/core/ui/modalStack
469
539
 
470
540
  - `MODAL_CANCEL` (const): const MODAL_CANCEL: "cancel" — Conventional cancel/dismiss result (also the default auto-dismiss and pop result).
@@ -643,6 +713,8 @@
643
713
  - `ConfirmDialogProps` (interface): interface ConfirmDialogProps — Props for {@link ConfirmDialog}.
644
714
  - `ControlHint` (interface): interface ControlHint — One row of a control legend. Name the game action(s) whose bound key(s) to show (`action`) so the glyphs come straight from the keybind map — never re-typed — or give literal `keys` for controls that live outside the map (`"Mouse"`, `"LMB"`). `label` says what the control does.
645
715
  - `ControlsList` (function): function ControlsList({ bindings, controls, hideOnCoarsePointer = true, separator = " / ", className, rowClassName, keysClassName, keyClassName, separatorClassName, labelClassName, renderKey, renderRow, }: { bindings?: ActionCodesMap; controls: readonly ControlHint[]; hideOnCoarsePointer?: boolean; … — Renders a controls legend whose key glyphs come from the game's keybind map, so bindings live in one place and never drift from a hand-typed table. Hides itself on coarse-pointer devices (a touchscreen has no keyboard) via the same `data-jg-kbd-hint` marker as `KeyHint`. Headless: every part carries a `className` slot and `data-*` hook; pass `renderKey`/`renderRow` to fully own the markup.
716
+ - `CreditsPanel` (function): function CreditsPanel({ document, onBack, title = "Credits", backLabel = "Back", open = true, backdrop, className, style, contentClassName, contentStyle, }: CreditsPanelProps): ReactNode — A reachable credits screen: the generated or hand-authored {@link CreditsDocument} in a scrollable panel over the scene, with one back affordance that keyboard and gamepad both reach. The game owns the backdrop and the wording; the block keeps the lines HudTheme-skinned and the exit working.
717
+ - `CreditsPanelProps` (interface): interface CreditsPanelProps — Props for {@link CreditsPanel}.
646
718
  - `Crosshair` (function): function Crosshair({ size = 18, gap = 5, thickness = 2, color = "rgba(255,255,255,0.85)", style, className, }: { size?: number; gap?: number; thickness?: number; color?: string; style?: CSSProperties; className?: string; }): React.JSX.Element — A minimal center crosshair reticle — four ticks around a gap. Purely presentational.
647
719
  - `CurrencyPill` (function): function CurrencyPill({ currencyId, className }: { currencyId: string; className?: string }): React.JSX.Element — ⚠ undocumented
648
720
  - `CutsceneLetterbox` (function): function CutsceneLetterbox({ active, caption, onSkip, skipLabel = "Skip", progress, theme, className, style, }: CutsceneLetterboxProps): ReactNode — A drop-in cinematic letterbox overlay: black bars slide in top and bottom while a cutscene is `active`, an optional caption/dialogue line sits above the lower bar with an optional progress line, and a Skip button rides the top bar. Presentation only — it never touches the director; wire `active`/`caption`/`progress`/`onSkip` from {@link useSequenceDirector} and your `onCue` handler. Reskin via {@link CutsceneLetterboxTheme}.
@@ -772,12 +844,22 @@
772
844
  - `LayoutRegionSpec` (type): type LayoutRegionSpec = Omit<LayoutRegion, "rect"> — A region descriptor without its measured rectangle — the caller supplies geometry through `useRegisterLayoutRegion`.
773
845
  - `LeavePartyButton` (function): function LeavePartyButton({ className, children, }: { className?: string; children?: ReactNode; }): React.JSX.Element | null — ⚠ undocumented
774
846
  - `LevelUpFlash` (function): function LevelUpFlash({ stat, durationMs = 1600, className, children, renderFlash, }: { stat?: string; durationMs?: number; className?: string; children?: ReactNode; renderFlash?: (event: StatLevelUpEvent) => ReactNode; }): React.JSX.Element | null — ⚠ undocumented
847
+ - `LoadingScreen` (function): function LoadingScreen({ progress, title = "Loading", tips, tipIntervalMs = 6000, ready = false, onReady, onSkip, backdrop, scrim = "blur", children, open = true, reducedMotion, onClosed, className, style, contentClassName, contentStyle, }: LoadingScreenProps): ReactNode — A loading screen with a progress bar and a rotating tip. Progress is a real 0–1 value from the game's loader (or indeterminate), tips come from the game and cycle deterministically, and a `ready` screen accepts any key, a gamepad A/Cross or a click. It reads HudTheme tokens and keeps a live scene visible behind a blur, so loading a level looks like the game rather than a blank page.
848
+ - `LoadingScreenProps` (interface): interface LoadingScreenProps — Props for {@link LoadingScreen}.
775
849
  - `ManaBar` (const): const ManaBar: (props: AtomicBarProps) => React.JSX.Element — Spell/energy resource pool.
776
850
  - `MapBounds` (interface): interface MapBounds { minX: number; minZ: number; maxX: number; maxZ: number } — ⚠ undocumented
777
851
  - `MapLegend` (function): function MapLegend({ kinds, labels, kindStyles = DEFAULT_MARKER_KINDS, title = "Legend", className, style, }: MapLegendProps): ReactNode — Marker-kind key for a map/minimap — glyph + color swatch per kind, labelled. Reads the same `kindStyles` the map renders with, so the legend can never drift from the pins.
778
852
  - `MapLegendProps` (interface): interface MapLegendProps — Props for {@link MapLegend}.
779
853
  - `MapViewport` (interface): interface MapViewport — Pan/zoom transform applied to a {@link WorldMapSurface}'s content group.
780
854
  - `MedalToken` (type): type MedalToken = "gold" | "silver" | "bronze" — The medal tokens {@link medalFor} emits for the podium.
855
+ - `MenuNavContainerProps` (interface): interface MenuNavContainerProps — The container props that make a menu navigable.
856
+ - `MenuNavController` (interface): interface MenuNavController — The live navigation controller returned by {@link useMenuNav}.
857
+ - `MenuNavItemProps` (interface): interface MenuNavItemProps — Props a menu item needs so {@link useMenuNav} can focus it and track pointer/focus.
858
+ - `MenuNavRender` (type): type MenuNavRender = (nav: MenuNavController) => ReactNode — Render prop for a menu whose items come from {@link useMenuNav}.
859
+ - `MenuScreen` (function): function MenuScreen({ open = true, transition = "fade", scrim = "dim", enterMs = 220, exitMs = 160, reducedMotion, label, role = "dialog", backdrop, className, style, contentClassName, contentStyle, children, onClosed, }: MenuScreenProps): ReactNode — One animated screen of a game front-end: full-bleed, laid out by the game, with an enter/exit animation that runs on transform/opacity only (no layout, no frame hitch) and unmounts when the exit finishes. The scrim is translucent and the optional `backdrop` slot paints behind the content, so a live 3D scene stays visible behind a title, pause or settings screen. Durations come from the caller or collapse to zero under reduced motion.
860
+ - `MenuScreenProps` (interface): interface MenuScreenProps — Props for {@link MenuScreen}.
861
+ - `MenuScreenScrim` (type): type MenuScreenScrim = "none" | "dim" | "blur" — How much a {@link MenuScreen} dims the scene behind it. `none` keeps a live 3D scene fully visible.
862
+ - `MenuScreenTransition` (type): type MenuScreenTransition = "fade" | "slide" | "scale" | "none" — Which motion a {@link MenuScreen} uses when it enters and leaves.
781
863
  - `MicToggle` (function): function MicToggle({ voice, className, mutedLabel, unmutedLabel, }: { voice: VoiceState; className?: string; mutedLabel?: ReactNode; unmutedLabel?: ReactNode; }): React.JSX.Element — ⚠ undocumented
782
864
  - `Minimap` (function): function Minimap({ markers, center, worldRadius, fog, size = 176, facingYaw = 0, rotate = false, kindStyles = DEFAULT_MARKER_KINDS, background, mapBounds, routes, zones, cellStates, onWorldClick, className, style, title = "Map", children, }: MinimapProps): ReactNode — Framed circular minimap: optional baked terrain background, reveal-on-event fog overlay, categorized marker icons, and a facing arrow. Reads static marker views, an external marker source, or a native `MarkerSet`; supply your own `kindStyles` palette to reskin. Does not require `GameProvider`.
783
865
  - `MinimapChrome` (function): function MinimapChrome({ view, frame = false, frameColor = "rgba(148,163,184,0.4)", frameStrokeWidth = 1.5, cardinalLabel = "N", markers = [], markerColor = "#e2e8f0", markerRadius = 5, className, }: MinimapChromeProps): ReactNode — Headless minimap chrome — nests inside a game's own `<svg>`: an optional ring frame, and edge-clamped marker dots that draw as directional arrows when a marker carries a `heading` (the compass-arrow / player-blip layer ~8 games were re-drawing by hand over `projectToMinimap`/`clampToMinimapEdge`). Renders a bare `<g>` — no background, panel, or title; compose your own content layer (routes, zones, terrain) as sibling SVG nodes for full control of z-order, or call `MinimapChrome` twice (once for `frame`, again for `markers`) to sandwich custom content between the ring and the blips.
@@ -902,10 +984,14 @@
902
984
  - `TimerReadoutSource` (type): type TimerReadoutSource = "auto" | "remaining" | "elapsed" — Which value a readout leads with. `"auto"` follows the timer's direction — remaining for a countdown, elapsed for a countup charge.
903
985
  - `TimerRing` (function): function TimerRing({ timer, id, size = 72, thickness = 8, mode = "fill", color = "var(--jg-accent, #38bdf8)", trackColor = "var(--jg-bar-track, rgba(148,163,184,0.25))", children, active = true, className, style, }: TimerRingProps): ReactNode — A radial charge/cooldown ring driven off a {@link TimerSet}'s `progress01`. Re-reads every frame and sweeps an SVG arc as the timer advances — ability charge, cooldown wipe, respawn ring, capture progress, all the same widget. `mode` chooses filling vs. draining; center `children` can hold a readout. HudTheme-skinned via `--jg-accent` / `--jg-bar-track`.
904
986
  - `TimerRingProps` (interface): interface TimerRingProps — Props for {@link TimerRing}.
987
+ - `TitleMenu` (function): function TitleMenu({ title, eyebrow, tagline, items, onSelect, onBack, footer, controlHint, backdrop, children, open = true, transition = "slide", scrim = "dim", enterMs, exitMs, reducedMotion, onClosed, className, style, contentClassName, contentStyle, }: TitleMenuProps): ReactNode — A game front-end title screen: wordmark, tagline, the game's verbs in a keyboard/gamepad-navigable list, a footer and an optional backdrop. The engine owns navigation, focus and the enter/exit animation; the game owns the title markup, wording, verbs, art and every colour through HudTheme tokens. Pass the live scene (or artwork) as `backdrop` so the title renders over 3D instead of a blank page.
988
+ - `TitleMenuItem` (interface): interface TitleMenuItem — One verb on a {@link TitleMenu}: New Game, Continue, Settings, Credits, Quit.
989
+ - `TitleMenuProps` (interface): interface TitleMenuProps — Props for {@link TitleMenu}.
905
990
  - `ToastStack` (function): function ToastStack({ action, limit = 4, className, renderToast, }: { action: string; limit?: number; className?: string; renderToast?: (entry: FeedEntry, index: number) => ReactNode; }): React.JSX.Element | null — Render `ctx.game.feed`'s entries for `action` as a newest-first toast stack — the feed-backed sibling of `@jgengine/core/game/toasts`' `createToastQueue`. Reach for this when the message source is already an engine event bound onto `ctx.game.feed` (kill feed, quest updates, loot log); reach for a `createToastQueue` when the game raises ad-hoc messages that need their own TTL independent of the feed's ring buffer.
906
991
  - `Trans` (function): function Trans({ k, params }: { k: string; params?: TParams }): ReactNode — Render a translated message inline: `<Trans k="hud.score" params={{ value }} />`.
907
992
  - `UseActionBarOptions` (interface): interface UseActionBarOptions — Options for {@link useActionBar}.
908
993
  - `UseAxisChannelResult` (interface): interface UseAxisChannelResult { channel: AxisChannel; isDown: (code: string) => boolean } — ⚠ undocumented · used by `useAxisChannel`: Wires useHeldKeys into a fresh AxisChannel, ready for a per-frame `channel.sample(dt, isDown)`.
994
+ - `UseMenuNavOptions` (interface): interface UseMenuNavOptions — How {@link useMenuNav} is wired.
909
995
  - `UsePanelsOptions` (interface): interface UsePanelsOptions — Options for {@link usePanels}.
910
996
  - `UseSelectionViewOptions` (interface): interface UseSelectionViewOptions — Options for {@link useSelectionView}.
911
997
  - `UseVoiceOptions` (interface): interface UseVoiceOptions { transport?: VoiceTransport; channelId?: string; mode?: PushToTalkMode; resolveRoutes?: () => readonly VoiceRoute[]; getUserMedia?: (constraints: MediaStreamConstraints) => Promise<MediaStream> } — ⚠ undocumented · used by `useVoice`: Mic capture + push-to-talk + channel roster over the VoiceTransport signaling seam.
@@ -1024,6 +1110,7 @@
1024
1110
  - `useLocalPlayerDead` (function): function useLocalPlayerDead(healthStatId = "health"): boolean — ⚠ undocumented
1025
1111
  - `useLocale` (function): function useLocale(): Locale — Subscribe to the active locale — re-renders on `setLocale`.
1026
1112
  - `useMarkers` (function): function useMarkers<TMeta = unknown>(markers: MarkerSet<TMeta>): readonly MapMarker<TMeta>[] — Subscribe to a native marker set or external marker source, or read a static marker array.
1113
+ - `useMenuNav` (function): function useMenuNav({ ids, columns = 1, wrap = true, disabled, initialId, enabled = true, onConfirm, onBack, gamepad = true, repeat, }: UseMenuNavOptions): MenuNavController — Bind a menu's focus order to the DOM: arrow keys, WASD, Home/End and a standard gamepad move a roving focus, Enter/Space/A confirm, Escape/Backspace/B/Start go back. Tab is left to the browser, so a menu never traps focus, and typing in a text field is never intercepted. The menu is pure state + listeners — it unmounts with the screen, so it costs nothing during play.
1027
1114
  - `useModalStack` (function): function useModalStack(stack: ModalStack): ModalStackView — Subscribe to a modal stack and re-render on every change, returning the active modal and depth.
1028
1115
  - `useNearestWorldItem` (function): function useNearestWorldItem(radius: number): WorldItemRecord | null — Nearest ground item within `radius` of the local player — drives a pickup prompt/highlight.
1029
1116
  - `useNotifications` (function): function useNotifications<TMeta = unknown>(store: NotificationStore<TMeta>): readonly NotificationEntry<TMeta>[] — Subscribe to a notification store's live list.
@@ -1080,6 +1167,12 @@
1080
1167
  - `usePrefersReducedMotion` (function): function usePrefersReducedMotion(): boolean — OS "reduce motion" preference (`prefers-reduced-motion: reduce`) as a live boolean — SSR-safe (defaults false). Use it to seed an accessibility store's `reducedMotion` default.
1081
1168
  - `useReducedMotion` (function): function useReducedMotion(): boolean — True when motion should be skipped: the OS `prefers-reduced-motion` preference, or the nearest {@link AccessibilityProvider}'s `reducedMotion` setting. Works without a provider.
1082
1169
 
1170
+ ## @jgengine/react/accessibilitySettings
1171
+
1172
+ - `useAccessibilityRows` (function): function useAccessibilityRows(): AccessibilitySettingRow[] — The accessibility rows backed by the shared settings store, for a settings screen the game lays out itself: map them through `SettingsRowControl` (`@jgengine/react/settingsBlocks`). The shell's stock settings menu shows the same rows as its `accessibility` category.
1173
+ - `useAccessibilitySettings` (function): function useAccessibilitySettings(): AccessibilityState — The live accessibility preferences (reduced motion, high contrast, text scale, colorblind mode, captions) from the shared settings store. Re-renders whenever any setting changes, so a root that spreads them onto the HUD updates the moment the player picks a value.
1174
+ - `useHoldVsToggle` (function): function useHoldVsToggle(): HoldVsToggle — The live hold-versus-toggle preference for actions that support both (aim, sprint, crouch). Pair it with `holdToggleActive` (`@jgengine/core/settings/accessibilitySettings`) per frame.
1175
+
1083
1176
  ## @jgengine/react/achievements
1084
1177
 
1085
1178
  - `AchievementGallery` (function): function AchievementGallery({ achievements, title = "Achievements", maskSecrets = true, renderIcon, columns = 2, emptyLabel = "No achievements yet.", className, style, }: AchievementGalleryProps): ReactNode — Achievement/trophy gallery — a responsive grid of cards showing unlocked vs. locked state, a progress bar for counter achievements, and a header summary of completion and score. Secret+locked entries mask their name/description. Feed it `useAchievements(tracker)`.
@@ -1226,6 +1319,11 @@
1226
1319
  - `runDialogueChoice` (function): function runDialogueChoice(commands: { run(name: string, input?: unknown): unknown }, choice: DialogueChoice, result: CheckResult | null): void — Route a {@link DialogueBox} choice through the `features.dialogue` bridge: resolve the choice's invoke (honoring a skill-check `result`), run that command, and otherwise close the dialogue — the write side that replaces a per-game `onChoice` that hand-rolls `resolveDialogueInvoke` + `dialogue.close`.
1227
1320
  - `useOpenDialogueId` (function): function useOpenDialogueId(): string | null — The id of the dialogue `ctx.game.dialogue` (or a `talkable(id)` prompt) currently has open, or `null`. The read side of the `features.dialogue` bridge — a panel looks the id up in its own dialogue catalog and renders {@link DialogueBox}, with no per-game open/close store.
1228
1321
 
1322
+ ## @jgengine/react/creditsPanel
1323
+
1324
+ - `CreditsPanel` (function): function CreditsPanel({ document, onBack, title = "Credits", backLabel = "Back", open = true, backdrop, className, style, contentClassName, contentStyle, }: CreditsPanelProps): ReactNode — A reachable credits screen: the generated or hand-authored {@link CreditsDocument} in a scrollable panel over the scene, with one back affordance that keyboard and gamepad both reach. The game owns the backdrop and the wording; the block keeps the lines HudTheme-skinned and the exit working.
1325
+ - `CreditsPanelProps` (interface): interface CreditsPanelProps — Props for {@link CreditsPanel}.
1326
+
1229
1327
  ## @jgengine/react/creditsScreen
1230
1328
 
1231
1329
  - `CreditsScreen` (function): function CreditsScreen({ document, className, style, renderEntry, }: { document: CreditsDocument; className?: string; style?: CSSProperties; renderEntry?: (entry: CreditsEntry, index: number) => ReactNode; }): ReactNode — Renders a {@link CreditsDocument} as headed sections of credited lines. Skinned entirely through the `--jg-*` HudTheme tokens, so it inherits the game's look from the surrounding theme block; pass `renderEntry` to art-direct the line itself. Links render only when an entry carries `href`, and always open in a new tab with `noreferrer`.
@@ -1501,7 +1599,7 @@
1501
1599
 
1502
1600
  ## @jgengine/react/hudThemeRoot
1503
1601
 
1504
- - `HudThemeRoot` (function): function HudThemeRoot({ theme, children }: { theme?: HudTheme | false; children?: ReactNode }): ReactNode — The HUD root the shell mounts around every `GameUI`: a layout-free (`display: contents`) `[data-jg-hud]` element carrying the theme's `--jg-*` variables, plus the engine stylesheet. Unset `theme` applies {@link defaultHudTheme}; a game's own theme on any descendant still wins by inheritance. `false` opts out of the default theme and the generic HUD control defaults; the keyframes stay so engine blocks still animate.
1602
+ - `HudThemeRoot` (function): function HudThemeRoot({ theme, accessibility, children, }: { theme?: HudTheme | false; accessibility?: Partial<AccessibilityState>; children?: ReactNode; }): ReactNode — The HUD root the shell mounts around every `GameUI`: a layout-free (`display: contents`) `[data-jg-hud]` element carrying the theme's `--jg-*` variables, plus the engine stylesheet. Unset `theme` applies {@link defaultHudTheme}; a game's own theme on any descendant still wins by inheritance. `false` opts out of the default theme and the generic HUD control defaults; the keyframes stay so engine blocks still animate.
1505
1603
 
1506
1604
  ## @jgengine/react/hudTokens
1507
1605
 
@@ -1606,6 +1704,11 @@
1606
1704
  - `subscribeFrameBind` (function): function subscribeFrameBind(subscriber: FrameSubscriber): () => void — ⚠ undocumented
1607
1705
  - `useFrameBind` (function): function useFrameBind<T, E extends Element = Element>(ref: { current: E | null }, get: () => T, apply: (value: T, element: E) => void): void — ⚠ undocumented
1608
1706
 
1707
+ ## @jgengine/react/loadingScreen
1708
+
1709
+ - `LoadingScreen` (function): function LoadingScreen({ progress, title = "Loading", tips, tipIntervalMs = 6000, ready = false, onReady, onSkip, backdrop, scrim = "blur", children, open = true, reducedMotion, onClosed, className, style, contentClassName, contentStyle, }: LoadingScreenProps): ReactNode — A loading screen with a progress bar and a rotating tip. Progress is a real 0–1 value from the game's loader (or indeterminate), tips come from the game and cycle deterministically, and a `ready` screen accepts any key, a gamepad A/Cross or a click. It reads HudTheme tokens and keeps a live scene visible behind a blur, so loading a level looks like the game rather than a blank page.
1710
+ - `LoadingScreenProps` (interface): interface LoadingScreenProps — Props for {@link LoadingScreen}.
1711
+
1609
1712
  ## @jgengine/react/map
1610
1713
 
1611
1714
  - `Compass` (function): function Compass({ facingYaw, center, markers, width = 340, fov = (Math.PI * 2) / 3, kindStyles = DEFAULT_MARKER_KINDS, className, }: CompassProps): ReactNode — Horizontal compass strip centered on the player's facing direction, with the eight cardinals and optional marker pips from static views, an external source, or a native `MarkerSet`.
@@ -1636,6 +1739,15 @@
1636
1739
  - `useFog` (function): function useFog(fog: FogField): ReturnType<FogField["cells"]> — ⚠ undocumented
1637
1740
  - `useMarkers` (function): function useMarkers<TMeta = unknown>(markers: MarkerSet<TMeta>): readonly MapMarker<TMeta>[] — Subscribe to a native marker set or external marker source, or read a static marker array.
1638
1741
 
1742
+ ## @jgengine/react/menuNav
1743
+
1744
+ - `MenuNavContainerProps` (interface): interface MenuNavContainerProps — The container props that make a menu navigable.
1745
+ - `MenuNavController` (interface): interface MenuNavController — The live navigation controller returned by {@link useMenuNav}.
1746
+ - `MenuNavItemProps` (interface): interface MenuNavItemProps — Props a menu item needs so {@link useMenuNav} can focus it and track pointer/focus.
1747
+ - `MenuNavRender` (type): type MenuNavRender = (nav: MenuNavController) => ReactNode — Render prop for a menu whose items come from {@link useMenuNav}.
1748
+ - `UseMenuNavOptions` (interface): interface UseMenuNavOptions — How {@link useMenuNav} is wired.
1749
+ - `useMenuNav` (function): function useMenuNav({ ids, columns = 1, wrap = true, disabled, initialId, enabled = true, onConfirm, onBack, gamepad = true, repeat, }: UseMenuNavOptions): MenuNavController — Bind a menu's focus order to the DOM: arrow keys, WASD, Home/End and a standard gamepad move a roving focus, Enter/Space/A confirm, Escape/Backspace/B/Start go back. Tab is left to the browser, so a menu never traps focus, and typing in a text field is never intercepted. The menu is pure state + listeners — it unmounts with the screen, so it costs nothing during play.
1750
+
1639
1751
  ## @jgengine/react/menuRouter
1640
1752
 
1641
1753
  - `MenuRouter` (interface): interface MenuRouter<Id extends string> — A menu screen stack: where you are, how you got there, and how to go back.
@@ -1643,6 +1755,13 @@
1643
1755
  - `menuStackReducer` (function): function menuStackReducer<Id extends string>(path: readonly Id[], action: MenuStackAction<Id>, root: Id): readonly Id[] — The pure screen-stack transition behind {@link useMenuRouter}. Exported so a game can drive the same navigation from its own store or a replayable command log instead of React state.
1644
1756
  - `useMenuRouter` (function): function useMenuRouter<Id extends string>(root: Id, options: { escapeToBack?: boolean } = {}): MenuRouter<Id> — The screen stack behind a game front-end: title → save select → settings → credits, and Escape back out. Every game menu needs this and none of it is game-specific, so it ships here rather than being re-derived per game. Holds no content and imposes no look — the game decides what each screen id renders.
1645
1757
 
1758
+ ## @jgengine/react/menuScreen
1759
+
1760
+ - `MenuScreen` (function): function MenuScreen({ open = true, transition = "fade", scrim = "dim", enterMs = 220, exitMs = 160, reducedMotion, label, role = "dialog", backdrop, className, style, contentClassName, contentStyle, children, onClosed, }: MenuScreenProps): ReactNode — One animated screen of a game front-end: full-bleed, laid out by the game, with an enter/exit animation that runs on transform/opacity only (no layout, no frame hitch) and unmounts when the exit finishes. The scrim is translucent and the optional `backdrop` slot paints behind the content, so a live 3D scene stays visible behind a title, pause or settings screen. Durations come from the caller or collapse to zero under reduced motion.
1761
+ - `MenuScreenProps` (interface): interface MenuScreenProps — Props for {@link MenuScreen}.
1762
+ - `MenuScreenScrim` (type): type MenuScreenScrim = "none" | "dim" | "blur" — How much a {@link MenuScreen} dims the scene behind it. `none` keeps a live 3D scene fully visible.
1763
+ - `MenuScreenTransition` (type): type MenuScreenTransition = "fade" | "slide" | "scale" | "none" — Which motion a {@link MenuScreen} uses when it enters and leaves.
1764
+
1646
1765
  ## @jgengine/react/modals
1647
1766
 
1648
1767
  - `ConfirmDialog` (function): function ConfirmDialog({ title, body, confirmLabel = "Confirm", cancelLabel = "Cancel", onConfirm, onCancel, danger = false, width = 360, className, style, }: ConfirmDialogProps): ReactNode — A generic two-button confirmation dialog — title, optional body, and Cancel / Confirm buttons over a `HudTheme`-token-driven {@link HudSurface}. Presentation-only: wire `onConfirm`/`onCancel` to a modal stack's `resolve` (typically inside a {@link ModalHost} render callback) and reskin via `HudTheme` tokens or `danger` for a destructive action. It interprets nothing — the game supplies the copy.
@@ -1800,6 +1919,19 @@
1800
1919
  - `useSettings` (function): function useSettings(): SettingsController — The engine settings controller for the current game — render your own settings UI from `categories`, or open the built-in menu with `open()`. Null-safe stub when mounted outside the shell.
1801
1920
  - `useSettingsStore` (function): function useSettingsStore(): SettingsStore — The shared settings store, or a standalone one when no provider is mounted (game code read outside the shell).
1802
1921
 
1922
+ ## @jgengine/react/settingsBlocks
1923
+
1924
+ - `SettingsKeybindControl` (function): function SettingsKeybindControl({ row, armed = false, onArm, buttonProps }: SettingsKeybindControlProps): ReactNode — A controls row for one action: its label, the bound key (click to rebind), a conflict/armed state and reset. The caller owns capture; this is the row's presentation.
1925
+ - `SettingsKeybindControlProps` (interface): interface SettingsKeybindControlProps — One rebindable action row: current key glyph, armed-capture state and a reset button.
1926
+ - `SettingsRowControl` (function): function SettingsRowControl({ row }: { row: SettingsRow }): ReactNode — Render the control a {@link SettingsRow} kind asks for.
1927
+ - `SettingsSection` (function): function SettingsSection({ category, heading = true, capture = true, className, style, onCaptureChange, }: SettingsSectionProps): ReactNode — One settings category as a block: its rows, then its rebindable actions with click-to-capture. The game picks where it sits and how wide it is; the block reads HudTheme tokens and the live rows the settings controller hands it, and writes every change straight back to that controller's store.
1928
+ - `SettingsSectionProps` (interface): interface SettingsSectionProps — How a {@link SettingsSection} handles a rebind capture.
1929
+ - `SettingsSelect` (function): function SettingsSelect({ row }: { row: SettingsRow }): ReactNode — One choice row from a {@link SettingsRow}, rendered as a pressed-state option group.
1930
+ - `SettingsSlider` (function): function SettingsSlider({ row }: { row: SettingsRow }): ReactNode — One slider row from a {@link SettingsRow}: mirrors the thumb locally and commits on release, so a drag never storms the settings store or the graphics rebuild it drives.
1931
+ - `SettingsTabs` (function): function SettingsTabs({ categories, activeId, onSelect, leading, tabProps, className, style }: SettingsTabsProps): ReactNode — The category tab rail for a settings screen: a scrollable row of buttons with pressed state. The game owns the surrounding layout; this block keeps every category reachable on narrow screens.
1932
+ - `SettingsTabsProps` (interface): interface SettingsTabsProps — The live category tabs for a settings screen: a scrollable rail with pressed state.
1933
+ - `SettingsToggle` (function): function SettingsToggle({ row }: { row: SettingsRow }): ReactNode — One on/off row from a {@link SettingsRow}.
1934
+
1803
1935
  ## @jgengine/react/shopGrid
1804
1936
 
1805
1937
  - `ShopGrid` (function): function ShopGrid({ shop, wallet, onWalletChange, theme, currencyIcons, iconFor, showSell = false, walletCurrencies, title = "Shop", labelFor, minCardWidth = "140px", className, style, }: ShopGridProps): ReactNode — A drop-in vendor/shop grid: item cards (icon, name, price with a currency glyph, stock count or "∞") over a caller-owned wallet, plus a wallet balance readout. Each card's Buy button disables when the wallet cannot afford it (`shop.canAfford`) or the item is sold out; an optional Sell button appears for entries with a `sellPrice`. Buying/selling routes through the core shop model and hands the debited/credited wallet back via `onWalletChange` — the component never owns the wallet. Purely token-driven presentation (shared `--jg-*` HudTheme tokens, reskinnable via {@link ShopGridTheme}); `kind` and `currency` are free strings the game styles, never interpreted.
@@ -1882,6 +2014,12 @@
1882
2014
  - `formatTimerMs` (function): function formatTimerMs(ms: number, format: TimerFormat = "mm:ss"): string — Format a millisecond duration as digital timer text — `mm:ss` (default), `m:ss.d` (with tenths), or `ss.d` (seconds + tenths). Negative inputs clamp to zero. No genre meaning: the same helper serves a round clock, a respawn countdown, or an ability charge readout.
1883
2015
  - `useTimerRead` (function): function useTimerRead(timer: TimerSet, id: string, active = true): TimerRead | null — Subscribe to a single timer and re-read it every animation frame while mounted, so a HUD readout stays live without the game hand-rolling interval math. Reuses one read object (allocation-aware). Returns `null` for an unknown id. Pass `active={false}` to freeze the per-frame tick (e.g. an off-screen HUD).
1884
2016
 
2017
+ ## @jgengine/react/titleMenu
2018
+
2019
+ - `TitleMenu` (function): function TitleMenu({ title, eyebrow, tagline, items, onSelect, onBack, footer, controlHint, backdrop, children, open = true, transition = "slide", scrim = "dim", enterMs, exitMs, reducedMotion, onClosed, className, style, contentClassName, contentStyle, }: TitleMenuProps): ReactNode — A game front-end title screen: wordmark, tagline, the game's verbs in a keyboard/gamepad-navigable list, a footer and an optional backdrop. The engine owns navigation, focus and the enter/exit animation; the game owns the title markup, wording, verbs, art and every colour through HudTheme tokens. Pass the live scene (or artwork) as `backdrop` so the title renders over 3D instead of a blank page.
2020
+ - `TitleMenuItem` (interface): interface TitleMenuItem — One verb on a {@link TitleMenu}: New Game, Continue, Settings, Credits, Quit.
2021
+ - `TitleMenuProps` (interface): interface TitleMenuProps — Props for {@link TitleMenu}.
2022
+
1885
2023
  ## @jgengine/react/tweenNumber
1886
2024
 
1887
2025
  - `TweenDelta` (type): type TweenDelta = "up" | "down" — Direction of the last change, for the delta flash.
@@ -2008,8 +2146,12 @@
2008
2146
  ## @jgengine/shell/camera
2009
2147
 
2010
2148
  - `CAMERA_TRANSPARENT_USERDATA` (const): const CAMERA_TRANSPARENT_USERDATA: { readonly jgCameraTransparent: true } — userData applied to the group wrapping author `WorldOverlay` decor so the spring-arm ignores it by default.
2149
+ - `CameraClearanceInput` (interface): interface CameraClearanceInput — What a camera asks `clearCameraBoom` each frame: the boom it wants and the surfaces that may stop it.
2150
+ - `CameraClearanceResult` (interface): interface CameraClearanceResult — Clear camera position and the boom length to hand back next frame.
2151
+ - `CameraCollisionQuery` (interface): interface CameraCollisionQuery — One collision question the camera asks: distance along a ray to the nearest surface that should stop it.
2011
2152
  - `CameraFollowListener` (type): type CameraFollowListener = (state: CameraFollowState) => void — ⚠ undocumented
2012
- - `CameraOccluder` (interface): interface CameraOccluder — Camera spring-arm occlusion filtering.
2153
+ - `CameraOccluder` (interface): interface CameraOccluder — Minimal structural view of a raycast hit object: its own tags plus a parent link to walk. Keeps this filter free of a three.js import so it stays unit-testable.
2154
+ - `CameraPoint` (interface): interface CameraPoint — World-space point for camera boom math, matching the rigs' `Vec3`.
2013
2155
  - `CameraShakeChannel` (interface): interface CameraShakeChannel { shake(amplitude: number, decayPerSecond?: number): void; step(dt: number): void; sample(config?: Parameters<typeof shakeOffset>[1]): ShakeOffset; trauma(): number } — ⚠ undocumented · used by `defaultCameraShakeChannel`: Process-wide default channel.
2014
2156
  - `CameraShakeContext` (const): const CameraShakeContext: React.Context<CameraShakeChannel> — ⚠ undocumented
2015
2157
  - `ControllerCameraShake` (function): function ControllerCameraShake({ controller, priority = GAME_SIM_FRAME_PRIORITY, }: ControllerCameraShakeProps): ReactNode — The shell-side consumer of a core {@link CameraShakeController}: each frame it calls `controller.update(delta)` to bleed trauma, reads the pooled `controller.offset()`, and applies it additively to the active camera — a positional kick plus a pitch/yaw/roll rotation — so the view VISIBLY shakes on impacts. It runs after the camera rig (which re-poses the camera to its base every frame), so the shake composes with any rig without a manual save/restore and without fighting the built-in `shakeChannel`. Renders nothing.
@@ -2028,10 +2170,12 @@
2028
2170
  - `PlayerFovSlider` (function): function PlayerFovSlider(): React.JSX.Element | null — ⚠ undocumented
2029
2171
  - `PlayerFovState` (interface): interface PlayerFovState { fov: number; bounds: PlayerFovBounds; enabled: boolean; persist: boolean; setFov: (value: number) => void; compose: (poseFov: number, mode?: "relative" | "absolute") => number } — ⚠ undocumented
2030
2172
  - `ViewmodelProps` (interface): interface ViewmodelProps — Props handed to a custom viewmodel component (#542): a live cue ref (velocity/bob/firing/reloading/recoil/hit) for the followed entity, driven from your own `useFrame` — read `cuesRef.current` there rather than storing it as render state.
2173
+ - `clearCameraBoom` (function): function clearCameraBoom(input: CameraClearanceInput): CameraClearanceResult — Pull a camera in along its boom so it never sits inside world collision. A blocking surface rejects the desired position: the camera moves to `clearance` before it, and onto the pivot when the surface is closer than that (the pivot is inside, so no camera position is clear). Pulling in is immediate — a camera must not spend a frame inside a trunk — while releasing eases back at `releaseRate`, which is what makes the boom read as a spring arm instead of a hard stop.
2031
2174
  - `defaultCameraShakeChannel` (const): const defaultCameraShakeChannel: CameraShakeChannel — Process-wide default channel. A shell mounts its own channel via `CameraShakeContext`, but game systems that have no React context (e.g. a `loop.onTick` reacting to `entity.died`) can import `cameraShake` and feed the default channel directly.
2032
2175
  - `isCameraOccluderTransparent` (function): function isCameraOccluderTransparent(object: CameraOccluder | null | undefined): boolean — Should the camera spring-arm ignore this raycast hit? Walks the object up its `.parent` chain and honors the nearest camera tag: `jgCameraCollide === true` blocks (opt back in), `jgCameraTransparent === true` passes through. Untagged geometry blocks as before, so engine-owned ground/entities are unaffected.
2033
2176
  - `readFirstPersonMuzzle` (function): function readFirstPersonMuzzle(target: THREE.Vector3, camera?: THREE.Camera): boolean — Presentation muzzle in world space; camera-scoped when supplied, false leaves target unchanged.
2034
2177
  - `registerFirstPersonMuzzle` (function): function registerFirstPersonMuzzle(camera: Camera, reader: FirstPersonMuzzleReader): () => void — Register a custom rig's presentation muzzle for its Three.js camera. Newest available custom reader wins over that camera's stock muzzle; call the returned cleanup on unmount.
2178
+ - `sceneCameraQuery` (function): function sceneCameraQuery(raycast: SceneRaycastApi["raycast"], options: { entities?: boolean; terrain?: boolean } = {}): CameraCollisionQuery — `CameraCollisionQuery` over the scene's existing collision query (`ctx.scene.raycast`), which already reads world solids, scene-object colliders and walls. Entities and terrain stay out by default: the followed body must not push its own camera, and looking down at the ground must not pull the boom.
2035
2179
  - `usePlayerFov` (function): function usePlayerFov(): PlayerFovState — ⚠ undocumented
2036
2180
 
2037
2181
  ## @jgengine/shell/camera/ControllerCameraShake
@@ -2081,8 +2225,14 @@
2081
2225
  ## @jgengine/shell/camera/cameraCollision
2082
2226
 
2083
2227
  - `CAMERA_TRANSPARENT_USERDATA` (const): const CAMERA_TRANSPARENT_USERDATA: { readonly jgCameraTransparent: true } — userData applied to the group wrapping author `WorldOverlay` decor so the spring-arm ignores it by default.
2084
- - `CameraOccluder` (interface): interface CameraOccluder — Camera spring-arm occlusion filtering.
2228
+ - `CameraClearanceInput` (interface): interface CameraClearanceInput — What a camera asks `clearCameraBoom` each frame: the boom it wants and the surfaces that may stop it.
2229
+ - `CameraClearanceResult` (interface): interface CameraClearanceResult — Clear camera position and the boom length to hand back next frame.
2230
+ - `CameraCollisionQuery` (interface): interface CameraCollisionQuery — One collision question the camera asks: distance along a ray to the nearest surface that should stop it.
2231
+ - `CameraOccluder` (interface): interface CameraOccluder — Minimal structural view of a raycast hit object: its own tags plus a parent link to walk. Keeps this filter free of a three.js import so it stays unit-testable.
2232
+ - `CameraPoint` (interface): interface CameraPoint — World-space point for camera boom math, matching the rigs' `Vec3`.
2233
+ - `clearCameraBoom` (function): function clearCameraBoom(input: CameraClearanceInput): CameraClearanceResult — Pull a camera in along its boom so it never sits inside world collision. A blocking surface rejects the desired position: the camera moves to `clearance` before it, and onto the pivot when the surface is closer than that (the pivot is inside, so no camera position is clear). Pulling in is immediate — a camera must not spend a frame inside a trunk — while releasing eases back at `releaseRate`, which is what makes the boom read as a spring arm instead of a hard stop.
2085
2234
  - `isCameraOccluderTransparent` (function): function isCameraOccluderTransparent(object: CameraOccluder | null | undefined): boolean — Should the camera spring-arm ignore this raycast hit? Walks the object up its `.parent` chain and honors the nearest camera tag: `jgCameraCollide === true` blocks (opt back in), `jgCameraTransparent === true` passes through. Untagged geometry blocks as before, so engine-owned ground/entities are unaffected.
2235
+ - `sceneCameraQuery` (function): function sceneCameraQuery(raycast: SceneRaycastApi["raycast"], options: { entities?: boolean; terrain?: boolean } = {}): CameraCollisionQuery — `CameraCollisionQuery` over the scene's existing collision query (`ctx.scene.raycast`), which already reads world solids, scene-object colliders and walls. Entities and terrain stay out by default: the followed body must not push its own camera, and looking down at the ground must not pull the boom.
2086
2236
 
2087
2237
  ## @jgengine/shell/camera/cameraRigs
2088
2238
 
@@ -2513,6 +2663,11 @@
2513
2663
  - `CsmMaterialSetup` (interface): interface CsmMaterialSetup — Structural slice of CSM so the patcher is testable without a renderer.
2514
2664
  - `patchSceneMaterials` (function): function patchSceneMaterials(scene: THREE.Scene, csm: CsmMaterialSetup, patched: WeakSet<THREE.Material>): void — Run CSM's `setupMaterial` over every unpatched standard material in the scene, chaining rather than clobbering a material's own `onBeforeCompile`.
2515
2665
 
2666
+ ## @jgengine/shell/render/CharacterCrowd
2667
+
2668
+ - `CharacterCrowd` (function): function CharacterCrowd({ models, registry, fps = 24 }: CharacterCrowdProps): React.JSX.Element — Draws far characters through the GPU crowd path: one baked rig per character kind, one instanced draw per material, every member's clip, time offset and speed sampled in the vertex shader. The entity markers decide who is far enough (their `lod` tiers) and write poses into `registry`; this layer owns the bakes, the materials and the instance buffers. Mount it in the same canvas as the entity markers; unmount it to fall back to per-entity rigs.
2669
+ - `CharacterCrowdProps` (interface): interface CharacterCrowdProps — Props for {@link CharacterCrowd}.
2670
+
2516
2671
  ## @jgengine/shell/render/EntityPreview
2517
2672
 
2518
2673
  - `EntityPreview` (function): function EntityPreview({ children, model, instanceId, mood = "studio", backdrop, turntable = 0, faceCamera = false, forward, environment = true, cameraPosition = [0, 1, 3], fov = 35, dpr = [1, 2], className, style, }: EntityPreviewProps): React.JSX.Element — Drop-in live 3D preview of a single entity — the character-screen portrait, unit inspector, or loadout viewer. It owns all the plumbing a game would otherwise re-derive by hand: a nested `<Canvas>`, the {@link GameContextBridge} that carries the running `GameContext` across the R3F reconciler boundary (so the subject's `useEntityRenderCues` / `useGameContext` hooks resolve and it mirrors the live game), a {@link StudioStage} 3-point lighting rig with optional turntable / face-camera, and a capture-friendly GL surface. The game keeps ownership of its `renderEntity`: pass it as `children`, or hand a native `model` (+ `instanceId`) for an engine-composed body. Bound to a live `instanceId`, the portrait walks, flinches, and topples in lockstep with the world instead of a canned spin.
@@ -2565,6 +2720,32 @@
2565
2720
  - `SurfaceShape` (type): type SurfaceShape = "box" | "cylinder" — Primitive UV layout whose physical dimensions drive `repeatMetres`.
2566
2721
  - `useAuthoredSurfaceMaterial` (function): function useAuthoredSurfaceMaterial(config: AuthoredSurfaceConfig, shape: SurfaceShape = "box"): THREE.MeshStandardMaterial — Load declared surface-map roles and own the material/map clones (cached textures remain untouched). Colour, emissive, sheen-colour and specular-colour maps are sRGB; numeric maps are linear. Cylinder side metres use the RMS ellipse circumference approximation. Static batches and custom meshes share this lifecycle.
2567
2722
 
2723
+ ## @jgengine/shell/render/characterLod
2724
+
2725
+ - `CharacterLodPolicy` (interface): interface CharacterLodPolicy — Distance policy a character model's {@link ModelLodConfig} resolves to; every distance is metres.
2726
+ - `CharacterLodTier` (type): type CharacterLodTier = "full" | "half" | "quarter" | "crowd" — What a character's distance from the camera costs it this frame.
2727
+ - `DEFAULT_CHARACTER_LOD` (const): const DEFAULT_CHARACTER_LOD: CharacterLodPolicy — The engine's default character tiers: full under 15 m, half under 40 m, GPU crowd beyond 25 m.
2728
+ - `characterLodInterval` (function): function characterLodInterval(tier: CharacterLodTier): number — Frames between pose updates for a tier: 1, 2, 4, and `0` meaning "never" for the crowd tier.
2729
+ - `characterLodRate` (function): function characterLodRate(tier: CharacterLodTier): number — Share of frames a tier poses the rig: 1, 0.5, 0.25, and 0 once the GPU crowd draws it.
2730
+ - `characterLodTier` (function): function characterLodTier(distance: number, config: ModelLodConfig | undefined, previous?: CharacterLodTier): CharacterLodTier — The tier a character at `distance` falls in. Passing the tier it held last frame applies the crowd hysteresis, so a character walking along the crowd boundary does not swap representation every frame.
2731
+
2732
+ ## @jgengine/shell/render/crowdRegistry
2733
+
2734
+ - `CharacterCrowdRegistry` (class): class CharacterCrowdRegistry — The characters the crowd layer is currently drawing, written by their entity markers each frame. It holds poses rather than React state: 71 pedestrians writing matrices must not re-render the world, and the crowd batch reads the same entries back to fill its instance buffers.
2735
+ - `crowdClipForSpeed` (function): function crowdClipForSpeed(clips: readonly string[], speed: number): string | undefined — The baked clip a crowd member at `speed` plays; clips are `[idle, walk, run]` as {@link crowdClipsOf} orders them.
2736
+ - `crowdClipsOf` (function): function crowdClipsOf(model: ModelConfig, clipNames: readonly string[]): string[] — The clips a crowd batch bakes for a model: its resolved idle, walk and run, in that order. Empty when the rig's clip names do not resolve to locomotion roles, which leaves the character on its own rig instead of drawing a frozen crowd copy.
2737
+ - `crowdEligible` (function): function crowdEligible(model: ModelConfig | undefined): model is ModelConfig — Whether a model can leave its own rig for the GPU crowd path: a rigged, clip-played, plain model.
2738
+ - `crowdKindOf` (function): function crowdKindOf(model: ModelConfig): string — The identity a character model shares with every other model the GPU crowd can draw: rig url, height normalization and material tint. Two entity kinds with the same key can share one instanced draw; a different tint or height needs its own.
2739
+
2740
+ ## @jgengine/shell/render/decalGeometry
2741
+
2742
+ - `DecalDrapeGeometry` (interface): interface DecalDrapeGeometry — Baked vertex/index buffers for one draped ground decal.
2743
+ - `DecalDrapeOptions` (interface): interface DecalDrapeOptions — Inputs for {@link createDecalDrapeGeometry}.
2744
+
2745
+ ## @jgengine/shell/render/decalTexture
2746
+
2747
+ - `DecalTextureData` (interface): interface DecalTextureData — Row-major RGBA bytes plus their edge length, ready for a `THREE.DataTexture`.
2748
+
2568
2749
  ## @jgengine/shell/render/materialAppearanceSignals
2569
2750
 
2570
2751
  - `RenderMaterialAppearanceBaseline` (interface): interface RenderMaterialAppearanceBaseline extends MaterialAppearanceBaseline — Immutable resolved baseline for one caller-owned assigned material, including its original card exposure.
@@ -2624,6 +2805,14 @@
2624
2805
  - `cloneModelScene` (function): function cloneModelScene(source: THREE.Object3D, options?: { cloneMaterials?: boolean; shadows?: ModelShadowMode; surfaceDetail?: boolean }): THREE.Object3D — Clone a model with independent pose and materials, retaining shared geometry and textures.
2625
2806
  - `disposeModelScene` (function): function disposeModelScene(root: THREE.Object3D): void — Release materials and bone textures owned by `cloneModelScene`; shared assets and attached models remain owned by their callers.
2626
2807
 
2808
+ ## @jgengine/shell/render/modelWind
2809
+
2810
+ - `ResolvedModelWind` (interface): interface ResolvedModelWind — Resolved sway settings baked into one material.
2811
+ - `WindClock` (function): function WindClock(): null — Advances the shared gust clock once per frame; mount one per 3D world.
2812
+ - `applyModelWind` (function): function applyModelWind(material: THREE.MeshStandardMaterial, wind: ResolvedModelWind): THREE.MeshStandardMaterial — Compiles height-ramped wind sway into a standard material's vertex shader. Idempotent per material; the clock and direction are shared module uniforms, so the whole scene advances with one write per frame and no per-material bookkeeping.
2813
+ - `windClock` (const): const windClock: { value: number; } — Shared gust clock every foliage material reads; advanced once per frame by `WindClock`.
2814
+ - `windDirection` (const): const windDirection: Vector2 — Shared gust direction (XZ); a model may override it with its own authored direction.
2815
+
2627
2816
  ## @jgengine/shell/render/projectedOverdraw
2628
2817
 
2629
2818
  - `ProjectedOverdrawOptions` (interface): interface ProjectedOverdrawOptions — Sampling resolution; lower resolutions change thin geometry and alpha-mask raster coverage.
@@ -2648,11 +2837,24 @@
2648
2837
  - `captureCanvas` (function): function captureCanvas(gl: CaptureRenderer): string | null — Read the current frame to a PNG data URL. Requires the R3F `<Canvas>` to have been created with `gl={{ preserveDrawingBuffer: true }}` (the shell's game canvas already is); returns null if the backing canvas can't be read.
2649
2838
  - `downloadImage` (function): function downloadImage(dataUrl: string, filename = "screenshot.png"): void — Trigger a browser download of an image data URL (the photo-mode "save" action).
2650
2839
 
2840
+ ## @jgengine/shell/render/shaderWarmup
2841
+
2842
+ - `SHADER_WARMUP_DEFAULTS` (const): const SHADER_WARMUP_DEFAULTS: { readonly quietMs: 400; readonly budgetMs: 5000; readonly roundFloorMs: 1500; readonly streamWaitMs: 2000; } — Defaults sized to cover an initial burst of streamed models without holding the screen.
2843
+ - `ShaderWarmup` (interface): interface ShaderWarmup — A warmup in flight: its promise, its latest state, and a way to stop it.
2844
+ - `ShaderWarmupOptions` (interface): interface ShaderWarmupOptions — Options for {@link createShaderWarmup}; the clock, sleep and loader signal are injectable for tests.
2845
+ - `ShaderWarmupRenderer` (interface): interface ShaderWarmupRenderer — The renderer surface the warmup needs; a real `WebGLRenderer` satisfies it.
2846
+ - `ShaderWarmupState` (interface): interface ShaderWarmupState — Where the load-time shader warmup is: waiting for the scene to stop growing, compiling a round, finished, or skipped because the renderer refused. The loading screen renders it.
2847
+
2651
2848
  ## @jgengine/shell/render/staticShapeInstances
2652
2849
 
2653
2850
  - `StaticShapeInstance` (interface): interface StaticShapeInstance — One editor-derived static primitive. A box spans one unit; a cylinder has unit radius and height before scale.
2654
2851
  - `StaticShapeInstances` (function): function StaticShapeInstances({ instances }: { instances: readonly StaticShapeInstance[] }): React.JSX.Element — Batch editor-derived boxes/cylinders by shape, PBR metadata and shadows. Uploads and bounds run only when instances change; map loading stays isolated per draw group.
2655
2852
 
2853
+ ## @jgengine/shell/render/staticTransforms
2854
+
2855
+ - `freezeStaticTransforms` (function): function freezeStaticTransforms(root: THREE.Object3D): void — Bake a subtree's world matrices from its current pose and take it out of three's per-frame `updateMatrixWorld` pass. Three recomposes every node that keeps `matrixAutoUpdate` and then forces its whole subtree to rebuild the world matrix, so authored scenery — terrain, batched props, scatter, instanced ground — pays that cost every frame for a pose it never changes (#2038).
2856
+ - `thawStaticTransforms` (function): function thawStaticTransforms(root: THREE.Object3D): void — Put a subtree back on the per-frame matrix pass after {@link freezeStaticTransforms} — for the authored seam that moves on an event rather than every frame.
2857
+
2656
2858
  ## @jgengine/shell/render/testFixtures/proceduralTileLayer
2657
2859
 
2658
2860
  - `drawProceduralTileLayer` (function): function drawProceduralTileLayer(canvas: CanvasFixture): TilemapWorldConfig — Draw the deterministic tile atlas used by textured tile-layer captures and tests.
@@ -2678,7 +2880,7 @@
2678
2880
 
2679
2881
  - `FootIkRig` (interface): interface FootIkRig — Bones resolved once per loaded scene for {@link applyFootIk}.
2680
2882
  - `FootIkState` (interface): interface FootIkState — Per-model state carried between frames: the faded weight and the smoothed pelvis drop.
2681
- - `applyFootIk` (function): function applyFootIk(rig: FootIkRig, originY: number, probe: GroundProbe, state: FootIkState, delta: number, cameraTarget?: readonly [number, number, number]): boolean — Applies one frame of foot IK after the animation mixer: probes the ground under each foot, resolves targets with `placeFeet`, lowers the pelvis, solves each leg with `solveTwoBone` bending toward the animated knee, and tilts planted feet to the ground. Returns whether the feet are grounded; `state` carries the faded weight between frames. Exported for tests and custom hosts.
2883
+ - `applyFootIk` (function): function applyFootIk(rig: FootIkRig, originY: number, probe: GroundProbe, state: FootIkState, delta: number, lookAtTarget?: readonly [number, number, number]): boolean — Applies one frame of foot IK after the animation mixer: probes the ground under each foot, resolves targets with `placeFeet`, lowers the pelvis, solves each leg with `solveTwoBone` bending toward the animated knee, and tilts planted feet to the ground. Turns the look-at bone toward `lookAtTarget` when the rig has one. Returns whether the feet are grounded; `state` carries the faded weight between frames. Exported for tests and custom hosts.
2682
2884
  - `resolveFootIkRig` (function): function resolveFootIkRig(scene: THREE.Object3D, config: FootIkConfig): FootIkRig | null — Resolves foot-IK bones on a loaded rig: explicit chains, or legs found by bone name for `"auto"` and configs without `feet`. Returns `null` when no leg resolves.
2683
2885
  - `useFootIk` (function): function useFootIk(scene: THREE.Object3D, config: FootIkConfig | undefined, ctx: GameContext | null, instanceId?: string, groundOffset = 0): void — Runs foot IK after the model's animation mixer for `ModelConfig.ik`. Ground probes hit terrain and blocking physical objects, never the model's own entity. `groundOffset` is the model-space height its soles rest on (`ModelConfig.y`).
2684
2886
 
@@ -2686,19 +2888,25 @@
2686
2888
 
2687
2889
  - `GraphPose` (interface): interface GraphPose — A mixer set up to show {@link AnimGraph} output on a rig; see {@link createGraphPose}.
2688
2890
  - `ModelAnimationDiagnostic` (interface): interface ModelAnimationDiagnostic — A repairable mismatch between a model animation config and its imported rig.
2891
+ - `ModelMotionStep` (interface): interface ModelMotionStep — One visual sample's measured motion, used to derive the directional locomotion parameters. `dx`/`dz` are world horizontal displacement since the previous sample, `dyaw` the yaw change in radians and `seconds` the time the sample covers.
2689
2892
  - `createGraphPose` (function): function createGraphPose(scene: THREE.Object3D, graph: AnimGraph, clips: THREE.AnimationClip[]): GraphPose — Binds a graph's clips to a rig exactly as `useModelAnimation` does (masked layers get filtered clips, additive layers additive ones) so a host that runs its own `createAnimGraphRuntime`, such as the editor's graph preview, poses the rig from the runtime's output.
2690
2893
  - `diagnoseModelAnimation` (function): function diagnoseModelAnimation(scene: THREE.Object3D, animation: ModelAnimationConfig, clips: readonly THREE.AnimationClip[]): readonly ModelAnimationDiagnostic[] — Checks clip mappings, masked layers and root-motion assumptions without starting a mixer.
2691
- - `readModelAnimationParams` (function): function readModelAnimationParams(ctx: GameContext, instanceId: string, speed: number, out: Record<string, AnimParamValue>): Record<string, AnimParamValue> — Refreshes a caller-owned graph parameter dictionary from authored extras and known physical movement. Grounded, verticalSpeed and crouched use the shared motor's resolved state when available; custom movers retain their authored values. Speed always uses the supplied measured ground speed. Does not write game state.
2894
+ - `readModelAnimationParams` (function): function readModelAnimationParams(ctx: GameContext, instanceId: string, speed: number, out: Record<string, AnimParamValue>, step?: ModelMotionStep): Record<string, AnimParamValue> — Refreshes a caller-owned graph parameter dictionary from authored extras and known physical movement. Grounded, verticalSpeed and crouched use the shared motor's resolved state when available; custom movers retain their authored values. Speed always uses the supplied measured ground speed. Passing `step` derives the directional parameters from the character's own motion: `direction` is the motion's share along the character's right axis, `facing` its share along the facing axis, and `turnRate` its yaw change per second. Derived values override conflicting authored extras. Does not write game state.
2692
2895
  - `takeRootMotion` (function): function takeRootMotion(rootBone: THREE.Object3D, bind: THREE.Vector3, localDelta: readonly [number, number, number] | undefined, out: THREE.Vector3): THREE.Vector3 — For a frame where a `rootMotion` state is current: pins the root bone's horizontal translation to its bind pose, so the clip plays in place, and returns that step's root travel as a world-space horizontal delta (through the rig's parent transform, so the entity's facing and the model's scale apply). The root bone's vertical motion stays in the clip.
2693
2896
  - `useModelAnimation` (function): function useModelAnimation(scene: THREE.Object3D, clips: THREE.AnimationClip[], animationInput: ModelAnimationConfig | "auto" | "none" | undefined, instanceId?: string): void — The engine's model animation driver as a standalone hook — the same mixer `EntityModel` runs, for games that render a cloned scene themselves (custom materials, procedural composition). Handles `"auto"` derivation from the GLB's clip names, speed-driven idle/walk/run crossfades read from the entity's live position when `instanceId` is set, one-shots fired from `entity.animation` / `combat.hitReaction` / `entity.died`, held poses, and the death clamp. With `animation.graph` set, the headless `AnimGraph` runtime owns every clip's time and weight and the mixer only applies them; clip events surface as `animation.event`. Equivalent same-order config data retains playback across rerenders; actual data edits reset it. Entity-bound playback holds while context game time is frozen; unbound previews keep playing.
2694
2897
 
2695
2898
  ## @jgengine/shell/render/useModelInstance
2696
2899
 
2697
2900
  - `ModelInstance` (interface): interface ModelInstance — Render `scene` with `position` and uniform `scale`; animate or deform `content` inside that placement frame.
2698
- - `ModelInstanceConfig` (type): type ModelInstanceConfig = Pick<ModelConfig, "url" | "scale" | "targetHeight" | "y" | "anchor" | "dims" | "shadows" | "animation" | "hiddenNodes" | "surfaceDetail" > — Model loading, placement, shadows and animation for a custom renderer.
2901
+ - `ModelInstanceConfig` (type): type ModelInstanceConfig = Pick<ModelConfig, "url" | "scale" | "targetHeight" | "y" | "anchor" | "dims" | "shadows" | "animation" | "hiddenNodes" | "surfaceDetail" | "wind" | "lod" > — Model loading, placement, shadows and animation for a custom renderer.
2699
2902
  - `ModelInstanceOptions` (interface): interface ModelInstanceOptions — Caller-owned styling runs on the isolated clone, never the loader cache.
2700
2903
  - `useModelInstance` (function): function useModelInstance(model: ModelInstanceConfig, options: ModelInstanceOptions = {}): ModelInstance — Load and own an isolated model instance, preserving imported transforms and measuring placement at bind pose. Apply hiddenNodes to the configured clone before measurement; equivalent name lists retain the instance. Mount `scene` with the returned position/scale. Custom materials, deformation and surrounding geometry stay caller-owned. Must run inside an R3F canvas and Suspense boundary; a game provider is optional.
2701
2904
 
2905
+ ## @jgengine/shell/render/useRagdoll
2906
+
2907
+ - `RagdollOptions` (interface): interface RagdollOptions — What {@link useRagdoll} needs: the rig's bone names, the physics world and what arms it.
2908
+ - `useRagdoll` (function): function useRagdoll(scene: THREE.Object3D, options: RagdollOptions): void — Turns a rigged character into a physics ragdoll when `active` goes true: the bones' world poses at that moment become box bodies joined by distance links in `world`, and every frame after, each bone's world position and aim come from its body. The physics bodies carry translation only, so each bone is oriented by rotating its death-pose direction-to-child onto the current one, which is what gives the body a skeleton instead of a pile of boxes.
2909
+
2702
2910
  ## @jgengine/shell/replay/useSessionRecorder
2703
2911
 
2704
2912
  - `RecordedPose` (interface): interface RecordedPose { position: EntityPosition; rotationY: number } — ⚠ undocumented · used by `useSessionRecorder`: Session-recording buffer (#120) for replay / photo mode / kill-cam: records an entity's pose on game-time every frame into a `RecordingBuffe…
@@ -3029,6 +3237,11 @@
3029
3237
  - `EditableGround` (function): function EditableGround({ terrain, bounds, segments = 96, version = 0, baseColor = "#3f6b3a", surfaceColors = DEFAULT_SURFACE_COLORS, }: EditableGroundProps): React.JSX.Element — ⚠ undocumented
3030
3238
  - `EditableGroundProps` (interface): interface EditableGroundProps { terrain: Pick<EditableTerrain, "sampleHeight" | "surfaceAt">; bounds: Aabb; segments?: number; version?: number; baseColor?: string; surfaceColors?: Record<string, string> } — ⚠ undocumented
3031
3239
 
3240
+ ## @jgengine/shell/terrain/GrassCover
3241
+
3242
+ - `GrassCover` (function): function GrassCover({ area, seed, colorBase, colorTip, strength, edgeFeather, heightAt, coverageAt, segments, position, }: GrassCoverProps): React.JSX.Element | null — The turf surface under one authored `grass_field` patch: a ground-draped, edge-feathered mat that turns a bare pavement footprint into grass the blade field stands in. Bounded to one draw and a {@link GRASS_COVER_MAX_SEGMENTS}² grid, so a large park still costs a single mesh.
3243
+ - `GrassCoverProps` (interface): interface GrassCoverProps — Props for {@link GrassCover}: one authored patch's footprint, colours and drape samplers.
3244
+
3032
3245
  ## @jgengine/shell/terrain/GrassField
3033
3246
 
3034
3247
  - `DEFAULT_GRASS_COUNT` (const): const DEFAULT_GRASS_COUNT: 1500 — ⚠ undocumented
@@ -3052,6 +3265,13 @@
3052
3265
  - `DEFAULT_GRASS_COUNT` (const): const DEFAULT_GRASS_COUNT: 1500 — ⚠ undocumented
3053
3266
  - `DEFAULT_GRASS_DENSITY` (const): const DEFAULT_GRASS_DENSITY: 4 — Blades per square meter — matches `@jgengine/core/world/vegetation`'s `VEGETATION_DEFAULTS.density`, so an editor-placed grass volume and a bare `<GrassField>` agree on what "4" means.
3054
3267
 
3268
+ ## @jgengine/shell/terrain/grassCoverGeometry
3269
+
3270
+ - `GRASS_COVER_MAX_SEGMENTS` (const): const GRASS_COVER_MAX_SEGMENTS: 96 — Largest grid a cover mat bakes to, per axis — a 66 m park stays one draw at ~1 m cells.
3271
+ - `GRASS_COVER_TILE_METRES` (const): const GRASS_COVER_TILE_METRES: 3 — World metres one cover-texture tile spans; the mat's own grain is sampled in world space.
3272
+ - `GrassCoverGeometry` (interface): interface GrassCoverGeometry — Bake result for {@link createGrassCoverGeometry}; empty arrays when the mat is switched off.
3273
+ - `GrassCoverGeometryOptions` (interface): interface GrassCoverGeometryOptions — Inputs for {@link createGrassCoverGeometry}.
3274
+
3055
3275
  ## @jgengine/shell/terrain/grassGeometry
3056
3276
 
3057
3277
  - `GRASS_TUFT_BLADES` (const): const GRASS_TUFT_BLADES: 5 — Blades baked into each tuft instance — one instance reads as a clump, not a lone blade.
@@ -3108,9 +3328,11 @@
3108
3328
  ## @jgengine/shell/terrain/terrainDetailMaterial
3109
3329
 
3110
3330
  - `ALL_TERRAIN_SURFACE_CLASSES` (const): const ALL_TERRAIN_SURFACE_CLASSES: 511 — Compile every surface response, for an editor viewport whose paint can change to any surface.
3331
+ - `MAX_TERRAIN_LAYER_MAPS` (const): const MAX_TERRAIN_LAYER_MAPS: 4 — At most this many painted layers sample their own scanned map; extra layers keep procedural shading.
3111
3332
  - `TerrainDetailMaterialHandle` (interface): interface TerrainDetailMaterialHandle — The built procedural detail terrain material, ready to mount on the ground mesh.
3112
3333
  - `TerrainDetailMaterialOptions` (interface): interface TerrainDetailMaterialOptions — Surface-pass inputs beyond the resolved detail config.
3113
3334
  - `TerrainDetailMaterialTextures` (interface): interface TerrainDetailMaterialTextures — Loaded PBR textures matching a resolved `ResolvedTerrainDetailMaterial.maps`' roles. The caller (a React component, via `useTexture`/`useLoader`) owns loading and disposal; `core` never touches `THREE.Texture` and this shell function never fetches a URL itself. Omitted AO/roughness skip sampling.
3335
+ - `TerrainLayerMap` (interface): interface TerrainLayerMap — One painted layer's scanned color map plus the world metres one tile covers.
3114
3336
 
3115
3337
  ## @jgengine/shell/terrain/terrainMath
3116
3338