@jgengine/shell 0.32.0 → 0.33.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 (41) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/GamePlayerShell.js +7 -0
  3. package/dist/Shell3dPresentation.js +5 -3
  4. package/dist/audio/AudioComponents.js +61 -51
  5. package/dist/audio/FeedbackHost.js +8 -4
  6. package/dist/audio/audioEngine.js +101 -24
  7. package/dist/audio/emitterTracking.d.ts +40 -0
  8. package/dist/audio/emitterTracking.js +153 -0
  9. package/dist/audio/eventFeedback.d.ts +15 -5
  10. package/dist/audio/eventFeedback.js +12 -7
  11. package/dist/audio/hapticsRoute.d.ts +28 -0
  12. package/dist/audio/hapticsRoute.js +59 -0
  13. package/dist/audio/loopParams.js +20 -10
  14. package/dist/devtools/FrameTelemetry.d.ts +1 -1
  15. package/dist/devtools/FrameTelemetry.js +4 -2
  16. package/dist/devtools/agentBridge.js +13 -0
  17. package/dist/devtools/frameSampler.js +4 -0
  18. package/dist/drivers/CanvasCoverFrameloop.d.ts +13 -0
  19. package/dist/drivers/CanvasCoverFrameloop.js +22 -0
  20. package/dist/drivers/FrameRateLimiter.d.ts +2 -1
  21. package/dist/drivers/FrameRateLimiter.js +6 -3
  22. package/dist/postfx/PostProcessing.d.ts +78 -2
  23. package/dist/postfx/PostProcessing.js +149 -76
  24. package/dist/postfx/postfxOverlay.d.ts +14 -4
  25. package/dist/postfx/postfxOverlay.js +90 -10
  26. package/dist/postfx/shadowPrepass.d.ts +6 -2
  27. package/dist/postfx/shadowPrepass.js +7 -3
  28. package/dist/render/characterLod.js +6 -0
  29. package/dist/render/useFootIk.js +54 -15
  30. package/dist/render/useModelAnimation.js +22 -5
  31. package/dist/settings/settingsController.d.ts +10 -1
  32. package/dist/settings/settingsController.js +81 -69
  33. package/dist/vision/RevealVision.d.ts +3 -1
  34. package/dist/vision/RevealVision.js +34 -15
  35. package/dist/world/WorldItems.js +11 -3
  36. package/package.json +4 -4
  37. package/skills/jgengine/api.md +2 -2
  38. package/skills/jgengine-ui/api.md +45 -24
  39. package/skills/jgengine-ui/capabilities.md +8 -0
  40. package/skills/jgengine-ui/reference-primitives.md +2 -0
  41. package/skills/jgengine-world/api.md +4 -4
@@ -186,16 +186,18 @@
186
186
  - `SettingCategoryDef` (interface): interface SettingCategoryDef — Declares or relabels/reorders a category tab; use it for a custom category or to reshape the built-ins.
187
187
  - `SettingKind` (type): type SettingKind = "slider" | "toggle" | "select" — ⚠ undocumented
188
188
  - `SettingOption` (interface): interface SettingOption { value: string; label: string } — ⚠ undocumented · used by `COLORBLIND_OPTIONS` (@jgengine/core/settings/accessibilitySettings): Colorblind palette choices.
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.
189
+ - `SettingValue` (type): type SettingValue = number | boolean | string — ⚠ undocumented · used by `asSettingType`: Read a stored value as the type the caller asked for.
190
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.
191
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.
192
192
  - `SettingsSurface` (type): type SettingsSurface = "quick" — `quick` shows compact on-screen volume/graphics buttons; `false` (default) mounts no engine trigger — open the menu from your own UI with `<SettingsTrigger>` or `useSettings().open()`.
193
+ - `SettingsTypeError` (class): class SettingsTypeError extends Error — A stored setting holds a value the read asked for as another type, so no honest value exists.
193
194
  - `SettingsVariant` (type): type SettingsVariant = "panel" | "sheet" | "sidebar" | "fullscreen" — The four themed settings layouts, chosen with `defineGame({ settings: { variant } })`. All read the game's `--jg-*` theme tokens.
194
195
  - `UI_SCALE_MAX` (const): const UI_SCALE_MAX: 1.5 — ⚠ undocumented
195
196
  - `UI_SCALE_MIN` (const): const UI_SCALE_MIN: 0.5 — ⚠ undocumented
197
+ - `asSettingType` (function): function asSettingType<T extends SettingValue>(id: string, stored: unknown, fallback: T): T — Read a stored value as the type the caller asked for. A value of another primitive type converts when the result is the same value in the asked-for type (a number as its digits, a boolean as `"true"`/`"false"`); otherwise it throws {@link SettingsTypeError}, naming the key and both types, instead of handing back the fallback as if nothing were stored.
196
198
  - `busVolumeSettingId` (function): function busVolumeSettingId(busId: string): string — ⚠ undocumented
197
199
  - `createSettingsStore` (function): function createSettingsStore(storage: Pick<WebStorageLike, "getItem" | "setItem"> | null | undefined = defaultStorage()): SettingsStore — Reactive, localStorage-backed settings store shared by the shell wiring and React hooks.
198
- - `loadSettingValue` (function): function loadSettingValue<T extends SettingValue>(id: string, fallback: T, storage: Pick<WebStorageLike, "getItem"> | null | undefined = defaultStorage()): T — ⚠ undocumented
200
+ - `loadSettingValue` (function): function loadSettingValue<T extends SettingValue>(id: string, fallback: T, storage: Pick<WebStorageLike, "getItem"> | null | undefined = defaultStorage()): T — Read one persisted setting as `fallback`'s type. An absent or unreadable cell yields the fallback; a stored value of an incompatible type throws {@link SettingsTypeError}.
199
201
  - `saveSettingValue` (function): function saveSettingValue(id: string, value: SettingValue, storage: Pick<WebStorageLike, "setItem"> | null | undefined = defaultStorage()): void — ⚠ undocumented
200
202
  - `settingStorageKey` (function): function settingStorageKey(id: string): string — ⚠ undocumented
201
203
 
@@ -310,7 +312,7 @@
310
312
  - `SettingCategoryDef` (interface): interface SettingCategoryDef — Declares or relabels/reorders a category tab; use it for a custom category or to reshape the built-ins.
311
313
  - `SettingKind` (type): type SettingKind = "slider" | "toggle" | "select" — ⚠ undocumented
312
314
  - `SettingOption` (interface): interface SettingOption { value: string; label: string } — ⚠ undocumented · used by `COLORBLIND_OPTIONS` (@jgengine/core/settings/accessibilitySettings): Colorblind palette choices.
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.
315
+ - `SettingValue` (type): type SettingValue = number | boolean | string — ⚠ undocumented · used by `asSettingType` (@jgengine/core/settings/settingsModel): Read a stored value as the type the caller asked for.
314
316
  - `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.
315
317
  - `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.
316
318
  - `SettingsSurface` (type): type SettingsSurface = "quick" — `quick` shows compact on-screen volume/graphics buttons; `false` (default) mounts no engine trigger — open the menu from your own UI with `<SettingsTrigger>` or `useSettings().open()`.
@@ -682,6 +684,7 @@
682
684
  - `CameraShakeMeter` (function): function CameraShakeMeter({ controller, animate = false, title = "CAMERA SHAKE", kindLabels, variation = "glass", className, style, }: CameraShakeMeterProps): ReactNode — A drop-in HUD panel that visualizes a core {@link CameraShakeController}: a trauma meter (reusing the shared {@link StaminaBar} building block) plus a live percentage and the current impact `kind` label. Presentation only — it reads the controller and never interprets `kind`, mapping it through `kindLabels` for display. A shell component applies the same controller's `offset()` to the camera; this panel is the on-screen readout of how much shake is in flight.
683
685
  - `CameraShakeMeterProps` (interface): interface CameraShakeMeterProps — Props for {@link CameraShakeMeter}.
684
686
  - `CameraShakeReadout` (interface): interface CameraShakeReadout — A live read of a camera-shake controller: current trauma `0..1` and the last game-owned `kind`.
687
+ - `CanvasCover` (interface): interface CanvasCover — A page-wide declaration that some surface paints over the whole game canvas.
685
688
  - `CaptureOdds` (function): function CaptureOdds({ chance, className, fillClassName, }: { chance: number; className?: string; fillClassName?: string; }): React.JSX.Element — ⚠ undocumented
686
689
  - `CardFace` (function): function CardFace({ rank, suit, faceDown = false, width = 64, height = 90, className, style, children, }: { rank: Rank; suit: Suit; faceDown?: boolean; width?: number; height?: number; className?: string; style?: CSSProperties; children?: ReactNode; }): React.JSX.Element — A single playing-card face: rank + suit pips, or a patterned back when `faceDown`.
687
690
  - `CardStack` (function): function CardStack({ pile, zone, cardOf, ...rest }: { pile: CardPileState; zone: string; cardOf: (id: string) => PlayingCard; } & Omit<Parameters<typeof StackedPile>[0], "cards">): React.JSX.Element — `StackedPile` bound to a headless `CardPileState` zone: reads the ordered card ids from `pile.zones[zone]` and resolves each to a `PlayingCard` via `cardOf`.
@@ -866,7 +869,7 @@
866
869
  - `MenuNavController` (interface): interface MenuNavController — The live navigation controller returned by {@link useMenuNav}.
867
870
  - `MenuNavItemProps` (interface): interface MenuNavItemProps — Props a menu item needs so {@link useMenuNav} can focus it and track pointer/focus.
868
871
  - `MenuNavRender` (type): type MenuNavRender = (nav: MenuNavController) => ReactNode — Render prop for a menu whose items come from {@link useMenuNav}.
869
- - `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.
872
+ - `MenuScreen` (function): function MenuScreen({ open = true, transition = "fade", scrim = "dim", coversCanvas = false, 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.
870
873
  - `MenuScreenProps` (interface): interface MenuScreenProps — Props for {@link MenuScreen}.
871
874
  - `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.
872
875
  - `MenuScreenTransition` (type): type MenuScreenTransition = "fade" | "slide" | "scale" | "none" — Which motion a {@link MenuScreen} uses when it enters and leaves.
@@ -994,7 +997,7 @@
994
997
  - `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.
995
998
  - `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`.
996
999
  - `TimerRingProps` (interface): interface TimerRingProps — Props for {@link TimerRing}.
997
- - `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.
1000
+ - `TitleMenu` (function): function TitleMenu({ title, eyebrow, tagline, items, onSelect, onBack, footer, controlHint, backdrop, children, open = true, transition = "slide", scrim = "dim", coversCanvas, 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.
998
1001
  - `TitleMenuItem` (interface): interface TitleMenuItem — One verb on a {@link TitleMenu}: New Game, Continue, Settings, Credits, Quit.
999
1002
  - `TitleMenuProps` (interface): interface TitleMenuProps — Props for {@link TitleMenu}.
1000
1003
  - `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.
@@ -1033,6 +1036,7 @@
1033
1036
  - `actionFromAbilitySlot` (function): function actionFromAbilitySlot(slot: AbilitySlotSnapshot, extra?: Partial<ActionDef> & { resourceId?: string }): ActionDef — Composable React renderers over the headless action view model (`@jgengine/core/ui/actionModel`) — the data/renderer/chrome split for an RTS command card, an ability action bar, a build menu, or a radial. `useActionBar` is the DATA/HOOK layer (resolution + focus/hover/keyboard/hotkey), `ActionBarChrome` is the composable RENDERER (swap `renderItem` for custom chrome, or ignore it and consume the model directly for a radial), and `ActionBar` is the batteries-included default. A game swaps renderer or chrome without forking the logic. Layout, dimensions, hotkeys, and catalog-id mapping stay caller-owned.
1034
1037
  - `barTokens` (function): function barTokens(tokens: BarTokens): CSSProperties — A style object binding vitals tokens to CSS custom properties — spread onto any HUD ancestor to restyle every atomic bar under it at once. Only the keys you pass are set; the rest fall back to {@link DEFAULT_BAR_TOKENS} inside the bars. This is the single global-theming seam.
1035
1038
  - `betterAuthIdentity` (function): function betterAuthIdentity(state: BetterAuthSessionState, options?: { signOut?: () => void }): IdentitySource — ⚠ undocumented
1039
+ - `canvasCover` (const): const canvasCover: CanvasCover — The page's canvas cover. A full-screen menu (or a game's own opaque overlay) holds it; the shell stops drawing the world, its shadow passes and postfx while it is held, and draws again from the untouched scene when it lifts. A translucent scrim or a side panel must not hold it — anything the scene shows through has to keep drawing.
1036
1040
  - `chatTransportFromSync` (function): function chatTransportFromSync(sync: ChatSync): ChatTransport — Lifts a callback-style ChatSync (e.g. createWsBackend().chatSyncFor(serverId)) into the hook-shaped ChatTransport contract. Create once per sync — outside render or inside useMemo — so subscriptions survive re-renders.
1037
1041
  - `clerkIdentity` (function): function clerkIdentity(state: ClerkUserState, options?: { signOut?: () => void }): IdentitySource — ⚠ undocumented
1038
1042
  - `createHeldKeyTracker` (function): function createHeldKeyTracker(target: HeldKeyEventTarget): { isDown: (code: string) => boolean; dispose: () => void; } — ⚠ undocumented
@@ -1077,6 +1081,8 @@
1077
1081
  - `useAxisChannel` (function): function useAxisChannel(config: AxisChannelConfig): UseAxisChannelResult — Wires useHeldKeys into a fresh AxisChannel, ready for a per-frame `channel.sample(dt, isDown)`. The channel is recreated when `config` identity changes, so pass a stable config (useMemo/module constant at the call site) unless a rebind is intended.
1078
1082
  - `useBrowserSuspension` (function): function useBrowserSuspension(onSuspend: (reason: BrowserSuspensionReason) => void, options: BrowserSuspensionOptions = {}): void — Observe browser interruptions while mounted, invoking the latest callback without replacing listeners.
1079
1083
  - `useCameraShake` (function): function useCameraShake(controller: CameraShakeController, animate = true): CameraShakeReadout — Subscribe to a camera-shake controller and re-render on every change (add / decay / clear). Optionally drives `update()` on an animation frame so the trauma meter bleeds down live even when no game loop ticks the controller. Returns the current trauma + kind for a HUD readout.
1084
+ - `useCanvasCover` (function): function useCanvasCover(declared: boolean): void — Declare the canvas fully covered while `declared` is true, and release on unmount.
1085
+ - `useCanvasCovered` (function): function useCanvasCovered(): boolean — True while a surface covers the canvas.
1080
1086
  - `useChat` (function): function useChat(channelId: string, options?: { limit?: number }): ChatMessage[] — ⚠ undocumented
1081
1087
  - `useChatBubbles` (function): function useChatBubbles(options?: ChatBubblesOptions): readonly ChatBubble[] — ⚠ undocumented
1082
1088
  - `useCoachMarks` (function): function useCoachMarks(sequence: CoachMarkSequence): CoachMarkView | null — Subscribe to a coach-mark sequence and re-render on every change, returning the current step view (or `null` when the tour is complete or waiting on a gate).
@@ -1094,10 +1100,10 @@
1094
1100
  - `useEventMeter` (function): function useEventMeter(meter: EventMeter, options?: AbilitySlotBindingOptions): EventMeterView — Bind a `createEventMeter` (`@jgengine/core/stats/eventMeter`) heat/streak gauge to a component — the react-render half of the ult/adrenaline and streak/combo meters (`event-meter` capability) that lets a HUD gauge re-render on tick without the game hand-rolling a `useEffect`/`setInterval` heartbeat around `meter.value()`.
1095
1101
  - `useEventTicker` (function): function useEventTicker(ticker: EventTicker, animate = true): EventTickerView[] — Subscribe to an event ticker and re-render it on an animation frame (unless the game already ticks it), returning the live entries newest-first with `fade` applied. Driving the frame loop is what makes entries visibly fade and drop as they age out.
1096
1102
  - `useFastTravel` (function): function useFastTravel<TMeta = unknown>(network: FastTravelNetwork<TMeta>): FastTravelNetwork<TMeta> — Subscribe a component to a fast-travel network's discovery changes. Returns the network unchanged; the hook only forces a re-render when a point is discovered or the network is restored, so callers can read `destinations`/`list` in render.
1097
- - `useFeed` (function): function useFeed({ action, limit }: { action: string; limit?: number }): FeedEntry[] — ⚠ undocumented
1103
+ - `useFeed` (function): function useFeed({ action, limit }: { action: string; limit?: number }): FeedEntry[] — Recent feed entries for `action`, newest last: the list keeps one identity until an entry is pushed, so a HUD ticker or kill feed no longer re-renders on every pose write (#2216).
1098
1104
  - `useFog` (function): function useFog(fog: FogField): ReturnType<FogField["cells"]> — ⚠ undocumented
1099
1105
  - `useFriendRequests` (function): function useFriendRequests(): FriendRequestEntry[] — ⚠ undocumented
1100
- - `useFriends` (function): function useFriends(): FriendEntry[] — ⚠ undocumented
1106
+ - `useFriends` (function): function useFriends(): FriendEntry[] — The local player's friends with their presence: the list keeps one identity until a request is accepted, a friend is removed or a friend's presence flips, so a friends HUD no longer re-renders on every pose write (#2216).
1101
1107
  - `useGame` (function): function useGame(): { commands: Omit<GameContext["game"]["commands"], "run"> & { run(name: string, input: unknown): DispatchedCommandResult }; events: GameEvents } — UI command surface. Hosted commands return a promise; await it to display acknowledgement or rejection. Registration and runAs remain trusted local APIs.
1102
1108
  - `useGameClock` (function): function useGameClock(): ClockSnapshot & { controls: SimClock } — ⚠ undocumented
1103
1109
  - `useGameContext` (function): function useGameContext(): GameContext — ⚠ undocumented
@@ -1140,7 +1146,7 @@
1140
1146
  - `usePlural` (function): function usePlural(): (key: string, count: number, params?: TParams) => string — Return the plural-aware translator bound to the current locale.
1141
1147
  - `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.
1142
1148
  - `usePresence` (function): function usePresence(userId: string): PresenceInfo — ⚠ undocumented
1143
- - `useQuestJournal` (function): function useQuestJournal(): QuestInstance[] — ⚠ undocumented
1149
+ - `useQuestJournal` (function): function useQuestJournal(): QuestInstance[] — The local player's journal: the list keeps one identity while the accepted quests, their status and their objective progress are unchanged, so a tracker HUD no longer re-renders on every pose write (#2216). Accepting, progressing, completing or revoking a quest re-renders once.
1144
1150
  - `useRafLoop` (function): function useRafLoop(onFrame: (deltaSeconds: number) => void, active = true): void — Run a requestAnimationFrame loop while `active`, calling `onFrame(deltaSeconds)` each frame. The callback is kept in a ref so re-renders never restart the loop; cleanup cancels the pending frame. For scene-graph work inside a canvas prefer R3F's useFrame — this is for DOM-side animation outside the renderer.
1145
1151
  - `useRebindSession` (function): function useRebindSession(session: RebindSession): RebindSession — Subscribe to a rebind session and re-render on every change (capture, reset, arm/disarm, restore). Returns the session unchanged for convenient inline use.
1146
1152
  - `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.
@@ -1149,10 +1155,10 @@
1149
1155
  - `useRoster` (function): function useRoster(userId?: string): readonly RosterEntry[] — ⚠ undocumented
1150
1156
  - `useSaveSlots` (function): function useSaveSlots(model: SaveSlots): SaveSlotMeta[] — Subscribe to a {@link SaveSlots} index and re-render on any change (write, clear, rename, restore). Returns the live list of slots in menu order. Wire it into a save-select screen and mutate the model in event handlers — the menu re-renders itself.
1151
1157
  - `useSaveStore` (function): function useSaveStore<T>(store: SaveStore<T>, options?: { load?: boolean }): SaveStoreView<T> — Bind a {@link SaveStore} to React: returns the live `value` and `status` and re-renders whenever either changes. By default it calls `load()` once on mount (pass `{ load: false }` to hydrate yourself). The same hook works for offline (localStorage) and cloud (Convex/DB) saves — only the store's backend differs.
1152
- - `useSceneEntities` (function): function useSceneEntities(): readonly SceneEntity[] — ⚠ undocumented
1158
+ - `useSceneEntities` (function): function useSceneEntities(): readonly SceneEntity[] — Live entities: the returned list keeps one identity until an entity spawns, despawns, is replaced or the store is hydrated — a pose write does not re-render its readers (#2212). `entity.list()` allocates a new array per write against an `Object.is` comparison, so every pose write re-rendered every HUD reading this hook. The records themselves are mutated in place, so a reader that needs a pose right now can still take `entity.position`; render-loop work reads it in `useFrame`.
1153
1159
  - `useSceneEntityIds` (function): function useSceneEntityIds(): readonly string[] — Membership-only entity id list: the returned array keeps a stable identity across per-frame pose writes and only changes when an entity spawns, despawns, or the store is hydrated (#625). A marker mapped from these ids reads its own live pose imperatively (useFrame), so the actor tree no longer re-reconciles every frame. Prefer this over {@link useSceneEntities} for large scenes.
1154
1160
  - `useSceneObjectIds` (function): function useSceneObjectIds(): readonly string[] — Membership-only object id list — the object counterpart of {@link useSceneEntityIds}; stable across move/rotate/setVisual, changes only on place/remove.
1155
- - `useSceneObjects` (function): function useSceneObjects(): readonly SceneObject[] — ⚠ undocumented
1161
+ - `useSceneObjects` (function): function useSceneObjects(): readonly SceneObject[] — Placed objects: the returned list keeps one identity across move and rotate writes, so a HUD reading it no longer re-renders on every pose write (#2212). Place, remove and the non-pose fields (catalog id, parent space, animation, visual, state, slots) still re-render. `move`/`rotate` replace the record, so take live placement from `ctx.scene.object.get(instanceId)` rather than the record's `position`.
1156
1162
  - `useSelectionView` (function): function useSelectionView(members: readonly EntitySummaryDef[], options?: UseSelectionViewOptions): SelectionModel — The DATA/HOOK layer: summarize a selected-entity list into a live model with a focused primary, ordered members, same-kind groups, keyboard focus stepping, and a virtualization window. Rendering-agnostic — feed it to {@link SelectionPanel} or read the `view` directly.
1157
1163
  - `useSequenceDirector` (function): function useSequenceDirector(director: SequenceDirector): SequenceDirectorBinding — Bind a {@link SequenceDirector} to a component: subscribe to its state, and while it is playing run a `requestAnimationFrame` loop that ticks the director each frame off the same injected clock — so a game drops in one hook and the cutscene advances itself. Returns the current playback state (playhead, progress, playing/done) plus bound `play`/`pause`/`skip`/`seek`/`stop`. Cue side effects belong on `director.onCue`, not here — the hook never interprets a cue.
1158
1164
  - `useSession` (function): function useSession(): IdentitySource — ⚠ undocumented
@@ -1171,7 +1177,7 @@
1171
1177
  - `useWaypoints` (function): function useWaypoints(source: WaypointTracker | readonly Waypoint[]): readonly Waypoint[] — Subscribe to a {@link WaypointTracker} and re-render on every change, returning its current waypoints. Pass a plain array instead and it is returned as-is (no subscription), so a game can drive the host from a tracker or from its own derived state.
1172
1178
  - `useWorldBrowser` (function): function useWorldBrowser(options: { fetchSessions: () => Promise<readonly SessionListing[]>; filter?: MatchFilter; limit?: number; refreshMs?: number; }): WorldBrowserState — Polls a host-supplied session fetcher (e.g. createWsBackend().browse) and filters through matchmaking's browseSessions. fetchSessions must be identity-stable (wrap in useCallback at the call site) or every render refetches.
1173
1179
  - `useWorldInvites` (function): function useWorldInvites(): WorldInvite[] — ⚠ undocumented
1174
- - `useWorldItems` (function): function useWorldItems(): readonly WorldItemRecord[] — ⚠ undocumented
1180
+ - `useWorldItems` (function): function useWorldItems(): readonly WorldItemRecord[] — Live dropped-item records: the returned list keeps one identity until an item spawns, is picked up or is removed. `worldItem.list()` allocates a new array per call and the shared store signal fires on every pose write, so a raw read re-rendered every marker each frame (#2175). A marker reads its own live pose imperatively (useFrame).
1175
1181
 
1176
1182
  ## @jgengine/react/accessibility
1177
1183
 
@@ -1249,6 +1255,13 @@
1249
1255
  - `CameraShakeReadout` (interface): interface CameraShakeReadout — A live read of a camera-shake controller: current trauma `0..1` and the last game-owned `kind`.
1250
1256
  - `useCameraShake` (function): function useCameraShake(controller: CameraShakeController, animate = true): CameraShakeReadout — Subscribe to a camera-shake controller and re-render on every change (add / decay / clear). Optionally drives `update()` on an animation frame so the trauma meter bleeds down live even when no game loop ticks the controller. Returns the current trauma + kind for a HUD readout.
1251
1257
 
1258
+ ## @jgengine/react/canvasCover
1259
+
1260
+ - `CanvasCover` (interface): interface CanvasCover — A page-wide declaration that some surface paints over the whole game canvas.
1261
+ - `canvasCover` (const): const canvasCover: CanvasCover — The page's canvas cover. A full-screen menu (or a game's own opaque overlay) holds it; the shell stops drawing the world, its shadow passes and postfx while it is held, and draws again from the untouched scene when it lifts. A translucent scrim or a side panel must not hold it — anything the scene shows through has to keep drawing.
1262
+ - `useCanvasCover` (function): function useCanvasCover(declared: boolean): void — Declare the canvas fully covered while `declared` is true, and release on unmount.
1263
+ - `useCanvasCovered` (function): function useCanvasCovered(): boolean — True while a surface covers the canvas.
1264
+
1252
1265
  ## @jgengine/react/cards
1253
1266
 
1254
1267
  - `CardFace` (function): function CardFace({ rank, suit, faceDown = false, width = 64, height = 90, className, style, children, }: { rank: Rank; suit: Suit; faceDown?: boolean; width?: number; height?: number; className?: string; style?: CSSProperties; children?: ReactNode; }): React.JSX.Element — A single playing-card face: rank + suit pips, or a patterned back when `faceDown`.
@@ -1473,9 +1486,9 @@
1473
1486
  - `useDomEvent` (function): function useDomEvent<E>(resolveTarget: () => DomEventTarget | null, type: string, handler: (event: E) => void, options?: { capture?: boolean; passive?: boolean }): void — Attach a DOM event listener with automatic cleanup. `resolveTarget` runs inside the effect, so `() => window` and ref-reading resolvers are SSR-safe; return null to skip attaching. The handler is kept in a ref, so a fresh closure per render never re-binds the listener. Re-binds only when `type` or `options.capture`/`options.passive` change.
1474
1487
  - `useEntityStat` (function): function useEntityStat(instanceId: string, statId: string): StatValue | null — ⚠ undocumented
1475
1488
  - `useEventMeter` (function): function useEventMeter(meter: EventMeter, options?: AbilitySlotBindingOptions): EventMeterView — Bind a `createEventMeter` (`@jgengine/core/stats/eventMeter`) heat/streak gauge to a component — the react-render half of the ult/adrenaline and streak/combo meters (`event-meter` capability) that lets a HUD gauge re-render on tick without the game hand-rolling a `useEffect`/`setInterval` heartbeat around `meter.value()`.
1476
- - `useFeed` (function): function useFeed({ action, limit }: { action: string; limit?: number }): FeedEntry[] — ⚠ undocumented
1489
+ - `useFeed` (function): function useFeed({ action, limit }: { action: string; limit?: number }): FeedEntry[] — Recent feed entries for `action`, newest last: the list keeps one identity until an entry is pushed, so a HUD ticker or kill feed no longer re-renders on every pose write (#2216).
1477
1490
  - `useFriendRequests` (function): function useFriendRequests(): FriendRequestEntry[] — ⚠ undocumented
1478
- - `useFriends` (function): function useFriends(): FriendEntry[] — ⚠ undocumented
1491
+ - `useFriends` (function): function useFriends(): FriendEntry[] — The local player's friends with their presence: the list keeps one identity until a request is accepted, a friend is removed or a friend's presence flips, so a friends HUD no longer re-renders on every pose write (#2216).
1479
1492
  - `useGame` (function): function useGame(): { commands: Omit<GameContext["game"]["commands"], "run"> & { run(name: string, input: unknown): DispatchedCommandResult }; events: GameEvents } — UI command surface. Hosted commands return a promise; await it to display acknowledgement or rejection. Registration and runAs remain trusted local APIs.
1480
1493
  - `useGameClock` (function): function useGameClock(): ClockSnapshot & { controls: SimClock } — ⚠ undocumented
1481
1494
  - `useGamePhase` (function): function useGamePhase(): { phase: GamePhase; setPhase: (phase: GamePhase) => void } — Live run phase + a setter that also gates the shell's touch controls. `menu`/`paused`/`ended` hide the touch dock; `playing` shows it.
@@ -1493,18 +1506,18 @@
1493
1506
  - `usePartyInvites` (function): function usePartyInvites(): PartyInviteEntry[] — ⚠ undocumented
1494
1507
  - `usePlayer` (function): function usePlayer(): { userId: string; isNew: boolean } — ⚠ undocumented
1495
1508
  - `usePresence` (function): function usePresence(userId: string): PresenceInfo — ⚠ undocumented
1496
- - `useQuestJournal` (function): function useQuestJournal(): QuestInstance[] — ⚠ undocumented
1509
+ - `useQuestJournal` (function): function useQuestJournal(): QuestInstance[] — The local player's journal: the list keeps one identity while the accepted quests, their status and their objective progress are unchanged, so a tracker HUD no longer re-renders on every pose write (#2216). Accepting, progressing, completing or revoking a quest re-renders once.
1497
1510
  - `useRafLoop` (function): function useRafLoop(onFrame: (deltaSeconds: number) => void, active = true): void — Run a requestAnimationFrame loop while `active`, calling `onFrame(deltaSeconds)` each frame. The callback is kept in a ref so re-renders never restart the loop; cleanup cancels the pending frame. For scene-graph work inside a canvas prefer R3F's useFrame — this is for DOM-side animation outside the renderer.
1498
1511
  - `useRoster` (function): function useRoster(userId?: string): readonly RosterEntry[] — ⚠ undocumented
1499
- - `useSceneEntities` (function): function useSceneEntities(): readonly SceneEntity[] — ⚠ undocumented
1512
+ - `useSceneEntities` (function): function useSceneEntities(): readonly SceneEntity[] — Live entities: the returned list keeps one identity until an entity spawns, despawns, is replaced or the store is hydrated — a pose write does not re-render its readers (#2212). `entity.list()` allocates a new array per write against an `Object.is` comparison, so every pose write re-rendered every HUD reading this hook. The records themselves are mutated in place, so a reader that needs a pose right now can still take `entity.position`; render-loop work reads it in `useFrame`.
1500
1513
  - `useSceneEntityIds` (function): function useSceneEntityIds(): readonly string[] — Membership-only entity id list: the returned array keeps a stable identity across per-frame pose writes and only changes when an entity spawns, despawns, or the store is hydrated (#625). A marker mapped from these ids reads its own live pose imperatively (useFrame), so the actor tree no longer re-reconciles every frame. Prefer this over {@link useSceneEntities} for large scenes.
1501
1514
  - `useSceneObjectIds` (function): function useSceneObjectIds(): readonly string[] — Membership-only object id list — the object counterpart of {@link useSceneEntityIds}; stable across move/rotate/setVisual, changes only on place/remove.
1502
- - `useSceneObjects` (function): function useSceneObjects(): readonly SceneObject[] — ⚠ undocumented
1515
+ - `useSceneObjects` (function): function useSceneObjects(): readonly SceneObject[] — Placed objects: the returned list keeps one identity across move and rotate writes, so a HUD reading it no longer re-renders on every pose write (#2212). Place, remove and the non-pose fields (catalog id, parent space, animation, visual, state, slots) still re-render. `move`/`rotate` replace the record, so take live placement from `ctx.scene.object.get(instanceId)` rather than the record's `position`.
1503
1516
  - `useTarget` (function): function useTarget(fromInstanceId: string): string | null — The entity `fromInstanceId` currently targets (set by `ctx.scene.entity.setTarget` or `cycleTarget`), for a target frame.
1504
1517
  - `useTicker` (function): function useTicker(hz = 10): number — Re-render at a steady rate. Returns a monotonically increasing tick count driven by a setInterval, for HUD elements that display wall-clock-derived values (cooldowns, cast bars, swing timers) without an engine subscription to hang off. `hz <= 0` disables the ticker.
1505
1518
  - `useWorldBrowser` (function): function useWorldBrowser(options: { fetchSessions: () => Promise<readonly SessionListing[]>; filter?: MatchFilter; limit?: number; refreshMs?: number; }): WorldBrowserState — Polls a host-supplied session fetcher (e.g. createWsBackend().browse) and filters through matchmaking's browseSessions. fetchSessions must be identity-stable (wrap in useCallback at the call site) or every render refetches.
1506
1519
  - `useWorldInvites` (function): function useWorldInvites(): WorldInvite[] — ⚠ undocumented
1507
- - `useWorldItems` (function): function useWorldItems(): readonly WorldItemRecord[] — ⚠ undocumented
1520
+ - `useWorldItems` (function): function useWorldItems(): readonly WorldItemRecord[] — Live dropped-item records: the returned list keeps one identity until an item spawns, is picked up or is removed. `worldItem.list()` allocates a new array per call and the shared store signal fires on every pose write, so a raw read re-rendered every marker each frame (#2175). A marker reads its own live pose imperatively (useFrame).
1508
1521
 
1509
1522
  ## @jgengine/react/hud
1510
1523
 
@@ -1791,7 +1804,7 @@
1791
1804
 
1792
1805
  ## @jgengine/react/menuScreen
1793
1806
 
1794
- - `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.
1807
+ - `MenuScreen` (function): function MenuScreen({ open = true, transition = "fade", scrim = "dim", coversCanvas = false, 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.
1795
1808
  - `MenuScreenProps` (interface): interface MenuScreenProps — Props for {@link MenuScreen}.
1796
1809
  - `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.
1797
1810
  - `MenuScreenTransition` (type): type MenuScreenTransition = "fade" | "slide" | "scale" | "none" — Which motion a {@link MenuScreen} uses when it enters and leaves.
@@ -2051,7 +2064,7 @@
2051
2064
 
2052
2065
  ## @jgengine/react/titleMenu
2053
2066
 
2054
- - `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.
2067
+ - `TitleMenu` (function): function TitleMenu({ title, eyebrow, tagline, items, onSelect, onBack, footer, controlHint, backdrop, children, open = true, transition = "slide", scrim = "dim", coversCanvas, 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.
2055
2068
  - `TitleMenuItem` (interface): interface TitleMenuItem — One verb on a {@link TitleMenu}: New Game, Continue, Settings, Credits, Quit.
2056
2069
  - `TitleMenuProps` (interface): interface TitleMenuProps — Props for {@link TitleMenu}.
2057
2070
 
@@ -2178,7 +2191,7 @@
2178
2191
  ## @jgengine/shell/audio/eventFeedback
2179
2192
 
2180
2193
  - `AnyFeedbackBinding` (type): type AnyFeedbackBinding = { [Name in FeedbackEventName]: FeedbackBinding<Name> }[FeedbackEventName] — Any row of the event mapping, whichever event it binds.
2181
- - `DEFAULT_FEEDBACK_BINDINGS` (const): const DEFAULT_FEEDBACK_BINDINGS: readonly AnyFeedbackBinding[] — The default mapping: each engine-announced gameplay event to the cue it raises. Damage to the local player reads as hurt, damage anywhere else as a hit; a wave start has no event of its own, so a game announces it through `notify("wave")` or `attachWaveFeedback`.
2194
+ - `DEFAULT_FEEDBACK_BINDINGS` (const): const DEFAULT_FEEDBACK_BINDINGS: readonly AnyFeedbackBinding[] — The default mapping: each engine-announced gameplay event to the cue it raises. Damage to the local player reads as hurt, damage anywhere else as a hit; an event that carries a `position` (damage, death) pans and attenuates its cue, and one without still plays centred. A wave start has no event of its own, so a game announces it through `notify("wave")` or `attachWaveFeedback`.
2182
2195
  - `DEFAULT_FEEDBACK_REACTIONS` (const): const DEFAULT_FEEDBACK_REACTIONS: Readonly<Record<FeedbackCue, FeedbackReaction>> — The default reactions. They are data: a game overrides one through `audio.feedbackCues` (sound) or `createEventFeedback({ reactions })` (haptics, shake, caption), and replaces the mapping through `bindings` without touching the shell.
2183
2196
  - `EventFeedback` (interface): interface EventFeedback — The event-to-reaction seam. One instance per running world.
2184
2197
  - `EventFeedbackOptions` (interface): interface EventFeedbackOptions — Options a game or host may override.
@@ -2197,6 +2210,13 @@
2197
2210
 
2198
2211
  - `installGestureResume` (function): function installGestureResume(engine: Pick<AudioEngine, "resume">, target: Pick<Document, "addEventListener" | "removeEventListener"> | null = typeof document === "undefined" ? null : document): () => void — Resume the audio context on the first real user gesture. Chrome keeps a freshly created `AudioContext` suspended until the page sees a trusted input, so pressing a key, tapping or clicking is the only moment sound may start; the engine then flushes any cue it held. One listener set per game, removed on unmount.
2199
2212
 
2213
+ ## @jgengine/shell/audio/hapticsRoute
2214
+
2215
+ - `HapticsRoute` (interface): interface HapticsRoute — Route haptic pulses to whatever the player can feel them on.
2216
+ - `HapticsRouteOptions` (interface): interface HapticsRouteOptions — The browser reach of {@link createHapticsRoute}; injected so the route runs headless.
2217
+ - `HapticsSink` (type): type HapticsSink = "gamepad" | "vibration" | "none" — Where a pulse actually landed: a gamepad, the device's own motor, or nowhere.
2218
+ - `createHapticsRoute` (function): function createHapticsRoute(pad: (pulse: HapticPulse) => void, options: HapticsRouteOptions = {}): HapticsRoute — One pulse in, whichever motor the player has: a connected gamepad takes it through the haptic channels the gamepad loop already drives, and a phone with no pad falls back to `navigator.vibrate`. Pad presence is cached and refreshed on connection events, so a hit never asks the browser for its pad list. Reduced motion does not silence this: see `eventFeedback`.
2219
+
2200
2220
  ## @jgengine/shell/audio/loopParams
2201
2221
 
2202
2222
  - `MAX_LOOP_CUTOFF` (const): const MAX_LOOP_CUTOFF: 22050 — Highest live filter cutoff, Hz; the engine also caps it at the context's Nyquist frequency.
@@ -2726,8 +2746,9 @@
2726
2746
  ## @jgengine/shell/postfx/postfxOverlay
2727
2747
 
2728
2748
  - `POSTFX_OVERLAY_USERDATA` (const): const POSTFX_OVERLAY_USERDATA: { readonly jgPostfxOverlay: true } — Spread onto a group's `userData` to mark its subtree as a screen/world overlay effect — additive glows, telegraphs, tracers, debug gizmos — that post-processing scene prepasses (GTAO's normal/depth render, DOF's depth render) must skip. Those prepasses render the scene with `overrideMaterial`, which turns transparent overlay quads into opaque stamps in the AO/DOF buffers (visible as black squares over hit sparks). The shell marks its own combat VFX and collision-debug mounts; games mark custom overlay effects the same way.
2729
- - `hidePostfxOverlays` (function): function hidePostfxOverlays(scene: THREE.Object3D, out: THREE.Object3D[]): void — Hide every currently visible marked object under `scene`, recording what was hidden into `out` (cleared first, no allocation on the hot path) so {@link restorePostfxOverlays} can undo exactly that set.
2749
+ - `hidePostfxOverlays` (function): function hidePostfxOverlays(scene: THREE.Object3D, out: THREE.Object3D[], withUnmodelled = false): void — Hide every currently visible object `scene`'s prepasses must skip, recording what was hidden into `out` (cleared first, no allocation on the hot path) so {@link restorePostfxOverlays} can undo exactly that set. `withUnmodelled` also hides points and lines, which three's GTAO pass excludes from its normal buffer but a depth prepass keeps.
2730
2750
  - `isPostfxOverlay` (function): function isPostfxOverlay(object: { userData?: Record<string, unknown> }): boolean — True when the object opted out of postfx scene prepasses via {@link POSTFX_OVERLAY_USERDATA}.
2751
+ - `releasePostfxOverlayTracking` (function): function releasePostfxOverlayTracking(scene: THREE.Object3D): void — Drop the cached set and its listeners for a scene that is going away, so a torn-down world does not keep its node graph reachable through this module.
2731
2752
  - `restorePostfxOverlays` (function): function restorePostfxOverlays(hidden: THREE.Object3D[]): void — Restore visibility for the objects hidden by {@link hidePostfxOverlays} and clear the list.
2732
2753
 
2733
2754
  ## @jgengine/shell/postfx/stylizePass
@@ -3508,12 +3529,12 @@
3508
3529
 
3509
3530
  ## @jgengine/shell/vision/RevealVision
3510
3531
 
3511
- - `RevealHighlights` (function): function RevealHighlights(props: RevealHighlightsProps): React.JSX.Element | null — Screen-space reveal effect (#115) — highlights tagged entities through occluders (Dark Sight / detective-vision / wallhack style). Renders with `depthTest: false` so the highlight draws over any wall standing between the origin and the revealed entity, rather than the usual depth-sorted scene.
3532
+ - `RevealHighlights` (function): function RevealHighlights(props: RevealHighlightsProps): React.JSX.Element | null — Screen-space reveal effect (#115) — highlights tagged entities through occluders (Dark Sight / detective-vision / wallhack style). Renders with `depthTest: false` so the highlight draws over any wall standing between the origin and the revealed entity, rather than the usual depth-sorted scene. Highlights track the origin and their targets every frame while enabled, so a disabled mode costs no renders.
3512
3533
  - `RevealHighlightsProps` (interface): interface RevealHighlightsProps extends RevealVisionOptions { enabled: boolean; color?: string } — ⚠ undocumented · used by `RevealHighlights`: Screen-space reveal effect (#115) — highlights tagged entities through occluders (Dark Sight / detective-vision / wallhack style).
3513
3534
  - `RevealScreenTint` (function): function RevealScreenTint({ enabled, color = "rgba(56, 189, 248, 0.16)", className }: RevealScreenTintProps): React.JSX.Element | null — Full-screen desaturating tint that reads as "vision mode is on" (Dark Sight / thermal / detective vision).
3514
3535
  - `RevealScreenTintProps` (interface): interface RevealScreenTintProps { enabled: boolean; color?: string; className?: string } — ⚠ undocumented · used by `RevealScreenTint`: Full-screen desaturating tint that reads as "vision mode is on" (Dark Sight / thermal / detective vision).
3515
- - `RevealVisionOptions` (interface): interface RevealVisionOptions { originEntityId?: string; radius: number; tags: readonly string[]; resolveTags: (entity: SceneEntity) => readonly string[] } — ⚠ undocumented · used by `useRevealHits`: Occlusion-ignoring tagged-entity radius query (#115), bound to the live scene.
3516
- - `useRevealHits` (function): function useRevealHits(options: RevealVisionOptions): readonly RevealHit[] — Occlusion-ignoring tagged-entity radius query (#115), bound to the live scene.
3536
+ - `RevealVisionOptions` (interface): interface RevealVisionOptions { originEntityId?: string; radius: number; tags: readonly string[]; resolveTags: (entity: SceneEntity) => readonly string[] } — ⚠ undocumented · used by `useRevealHits`: Occlusion-ignoring tagged-entity radius query (#115), bound to the live scene and refreshed on render.
3537
+ - `useRevealHits` (function): function useRevealHits(options: RevealVisionOptions): readonly RevealHit[] — Occlusion-ignoring tagged-entity radius query (#115), bound to the live scene and refreshed on render.
3517
3538
 
3518
3539
  ## @jgengine/shell/vision/frustumSampleEqual
3519
3540
 
@@ -126,6 +126,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
126
126
 
127
127
  - `isCameraOccluderTransparent` (function) · `import { isCameraOccluderTransparent } from "@jgengine/shell/camera"`
128
128
 
129
+ ## canvas-cover — stop world rendering behind a full-screen surface while input, audio, the HUD and the menu keep running
130
+
131
+ - `canvasCover` (const) · `import { canvasCover } from "@jgengine/react"`
132
+
129
133
  ## caption-feed — bounded, self-expiring, deduped caption lines for the accessibility captions overlay
130
134
 
131
135
  - `createCaptionFeed` (function) · `import { createCaptionFeed } from "@jgengine/shell/audio/captions"`
@@ -347,6 +351,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
347
351
  - `buildGroundCoverageMask` (function) · `import { buildGroundCoverageMask } from "@jgengine/shell/terrain"`
348
352
  - `createGroundEdgeFade` (function) · `import { createGroundEdgeFade } from "@jgengine/shell/terrain"`
349
353
 
354
+ ## haptics-route — send a feedback pulse to the gamepad when one is connected, else to the phone's vibration motor
355
+
356
+ - `createHapticsRoute` (function) · `import { createHapticsRoute } from "@jgengine/shell/audio/hapticsRoute"`
357
+
350
358
  ## heavy-models — models over the per-model triangle budget, for perf probes and smoke assertions
351
359
 
352
360
  - `heavyModels` (function) · `import { heavyModels } from "@jgengine/shell/render/modelLoad"`
@@ -94,6 +94,8 @@ The post chain itself is never hand-wired render passes. Set `defineGame({ postP
94
94
 
95
95
  AO and depth-of-field prepasses reuse the beauty pass's shadow maps; they do not redraw shadow casters. Prepass failure restores shadow update policy and marked overlay visibility. The composer resizes passes through `setSize`, and graph cleanup calls each pass's `dispose` once; avoid reading a pass's internal targets or materials.
96
96
 
97
+ The Graphics tab offers a `postStages` toggle only for a stage the resolved chain can build: an `ao` or `bloom` authored `false`, `aa: "msaa"`/`false` (no SMAA pass), or an unauthored `dof` drops that row, because a toggle over a pass the composer never creates changes nothing. A game with no post chain gets none of the stage rows.
98
+
97
99
  **Orbit camera occlusion:** the third-person orbit rig takes `camera: { collision: { enabled, padding?, minTargetDistance? } }` — a spring-arm that raycasts target→camera each frame and pulls the boom in past walls/terrain so the camera never clips inside geometry. Off by default (unchanged chase feel); enable it for any world with interiors or dense structures.
98
100
 
99
101
  **Camera clearance:** `camera: { firstPerson: { collision } }` keeps the `first` rig's eye out of geometry the body can reach. It is on by default: the boom anchors `armBack` (1.5) behind the eye along the look ray, `clearance` defaults to the near plane plus 0.15, and a blocking surface pulls the camera back toward that anchor so the near plane stays clear. `false` restores the unchecked camera. A game that drives its own camera calls the same seam: `clearCameraBoom({ pivot, desired, query: sceneCameraQuery(ctx.scene.raycast), previousArm, deltaSeconds })` returns the clear `position` and the `arm` to pass back next frame. The query reads `ctx.scene.raycast`, so world solids, scene-object colliders and walls all block it without a new collision layer; entities and terrain stay out so the followed body and the ground cannot pull the boom.
@@ -1001,7 +1001,7 @@
1001
1001
  ## @jgengine/core/movement/solidObstacles
1002
1002
 
1003
1003
  - `ObstacleReach` (interface): interface ObstacleReach — Largest blocking-collider extent in the scene, split so tower height cannot inflate the XZ query.
1004
- - `ObstacleReachCache` (interface): interface ObstacleReachCache — Mutable reach cache; one per source, invalidated by object count.
1004
+ - `ObstacleReachCache` (interface): interface ObstacleReachCache — Mutable reach cache; one per source, invalidated by object count or by the source's version.
1005
1005
  - `SolidObstacleSource` (interface): interface SolidObstacleSource — The slice of the scene's object store this query needs. Narrow on purpose: the scene context wires its own store here while it is still being constructed, `ctx.scene.object` satisfies it as-is, and a pure rules package with no `GameContext` to import can satisfy it from its own object list.
1006
1006
  - `createObstacleReachCache` (function): function createObstacleReachCache(): ObstacleReachCache — A fresh {@link ObstacleReachCache} for a caller that owns its own source.
1007
1007
  - `obstacleFromSolid` (function): function obstacleFromSolid(solid: WorldSolid): CollisionObstacle — A {@link WorldSolid} as a {@link CollisionObstacle}. Axis-aligned solids are one box; a yawed solid is cut into strips along its long axis, each fitted with an AABB, so a turned building does not block the street beside it the way a single enclosing box would.
@@ -1010,7 +1010,7 @@
1010
1010
  - `slideStep` (function): function slideStep(position: EntityPosition, stepX: number, stepZ: number, obstacles: readonly CollisionObstacle[], radius = DEFAULT_OBSTACLE_PLAYER_RADIUS, stepUpHeight = 0): { stepX: number; stepZ: number } — {@link resolveWalkerStep} against an already-gathered obstacle set.
1011
1011
  - `solidObstaclesNear` (function): function solidObstaclesNear(ctx: GameContext, position: EntityPosition, reachX: number, reachZ: number, height = WALKER_HEIGHT): CollisionObstacle[] — Every blocking physical obstacle overlapping the box `position` ± `reachX`/`reachZ`, read from the scene's resolved colliders and `ctx.world.solids`.
1012
1012
  - `sourceObstacleReach` (function): function sourceObstacleReach(source: SolidObstacleSource, cache: ObstacleReachCache): ObstacleReach — {@link solidObstacleReach} against a bare source and caller-owned cache.
1013
- - `sourceObstaclesNear` (function): function sourceObstaclesNear(source: SolidObstacleSource, reach: ObstacleReach, position: EntityPosition, reachX: number, reachZ: number, height = WALKER_HEIGHT): CollisionObstacle[] — {@link solidObstaclesNear} against a bare source and an already-resolved reach.
1013
+ - `sourceObstaclesNear` (function): function sourceObstaclesNear(source: SolidObstacleSource, reach: ObstacleReach, position: EntityPosition, reachX: number, reachZ: number, height = WALKER_HEIGHT, cache?: ObstacleReachCache): CollisionObstacle[] — {@link solidObstaclesNear} against a bare source and an already-resolved reach. Pass the source's {@link ObstacleReachCache} to use the version-keyed obstacle index; a source that reports no `version()` keeps the per-query `inBox` gather.
1014
1014
 
1015
1015
  ## @jgengine/core/movement/steering
1016
1016
 
@@ -2037,7 +2037,7 @@
2037
2037
 
2038
2038
  ## @jgengine/core/sensor/revealQuery
2039
2039
 
2040
- - `RevealHit` (interface): interface RevealHit { instanceId: string; distance: number; tags: readonly string[] } — ⚠ undocumented · used by `useRevealHits` (@jgengine/shell/vision/RevealVision): Occlusion-ignoring tagged-entity radius query (#115), bound to the live scene.
2040
+ - `RevealHit` (interface): interface RevealHit { instanceId: string; distance: number; tags: readonly string[] } — ⚠ undocumented · used by `useRevealHits` (@jgengine/shell/vision/RevealVision): Occlusion-ignoring tagged-entity radius query (#115), bound to the live scene and refreshed on render.
2041
2041
  - `RevealQuery` (interface): interface RevealQuery { inRadius(center: EntityPosition | string, radius: number, tags: readonly string[]): RevealHit[] } — ⚠ undocumented
2042
2042
  - `RevealQueryOptions` (interface): interface RevealQueryOptions { resolvePosition: (instanceId: string) => EntityPosition | undefined; resolveTags: (instanceId: string) => readonly string[]; candidates: () => string[] } — ⚠ undocumented
2043
2043
 
@@ -2782,7 +2782,7 @@
2782
2782
  - `ResolvedTerrainDetail` (type): type ResolvedTerrainDetail = Required<Omit<TerrainDetailConfig, "waterLevel" | "material" | "sweeps">> & { waterLevel: number; material?: ResolvedTerrainDetailMaterial; sweeps: { dry: readonly [number, number, number]; wet: readonly [number, number, number] }; } — A {@link TerrainDetailConfig} with every field resolved to a concrete value — the shape the shell's detail material consumes.
2783
2783
  - `ResolvedWeather` (interface): interface ResolvedWeather { grip: number; visibility: number; structureDamage: number; chill: number; ignition: number; spread: number } — ⚠ undocumented
2784
2784
  - `ResponseCurve` (type): type ResponseCurve = | { readonly kind: "linear"; readonly from: number; readonly to: number } /** Same shape as `linear`, raised to `exponent` (default `2`), so appeal stays flat until it matters. */ | { readonly kind: "quadratic"; readonly from: number; readonly to: number; readonly exponent?: num… — How one need's satisfaction maps to an option's appeal: `1` at `from`, `0` at `to`, clamped outside. Read satisfaction `1` as a full meter; pair with {@link UtilityTerm.invert} when a full meter is what makes the option attractive.
2785
- - `RevealHit` (interface): interface RevealHit { instanceId: string; distance: number; tags: readonly string[] } — ⚠ undocumented · used by `useRevealHits` (@jgengine/shell/vision/RevealVision): Occlusion-ignoring tagged-entity radius query (#115), bound to the live scene.
2785
+ - `RevealHit` (interface): interface RevealHit { instanceId: string; distance: number; tags: readonly string[] } — ⚠ undocumented · used by `useRevealHits` (@jgengine/shell/vision/RevealVision): Occlusion-ignoring tagged-entity radius query (#115), bound to the live scene and refreshed on render.
2786
2786
  - `RevealQuery` (interface): interface RevealQuery { inRadius(center: EntityPosition | string, radius: number, tags: readonly string[]): RevealHit[] } — ⚠ undocumented
2787
2787
  - `RoadCut` (interface): interface RoadCut — One trim of a ribbon end back to a junction boundary — the seam the welded surface attaches to.
2788
2788
  - `RoadEnvironmentDescriptor` (type): type RoadEnvironmentDescriptor = { kind: "road" } & Required< Pick<RoadEnvironmentConfig, "path" | "width" | "color" | "markings" | "markingColor" | "elevation"> > & { /** Resolved sidewalk band, or `false` when the road has none. */ sidewalk: { width: number; color: string } | false; /** Procedural… — Resolved road descriptor produced by {@link road} and rendered by the shell environment scene.