@jgengine/react 0.10.0 → 0.12.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 (72) hide show
  1. package/CHANGELOG.md +39 -2
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/actionHud.d.ts +174 -0
  5. package/dist/actionHud.js +294 -0
  6. package/dist/bars.d.ts +103 -0
  7. package/dist/bars.js +180 -0
  8. package/dist/barsPreview.d.ts +4 -0
  9. package/dist/barsPreview.js +47 -0
  10. package/dist/cards.d.ts +59 -0
  11. package/dist/cards.js +116 -0
  12. package/dist/chat.d.ts +3 -1
  13. package/dist/chat.js +2 -2
  14. package/dist/components.d.ts +28 -1
  15. package/dist/components.js +36 -1
  16. package/dist/display.d.ts +1 -0
  17. package/dist/display.js +1 -0
  18. package/dist/engineStore.d.ts +3 -0
  19. package/dist/engineStore.js +3 -0
  20. package/dist/fogOverlay.d.ts +3 -0
  21. package/dist/fogOverlay.js +3 -0
  22. package/dist/gameViewport.js +16 -0
  23. package/dist/hooks.d.ts +40 -0
  24. package/dist/hooks.js +69 -1
  25. package/dist/hud.d.ts +104 -0
  26. package/dist/hud.js +155 -0
  27. package/dist/hudFrame.d.ts +47 -0
  28. package/dist/hudFrame.js +80 -0
  29. package/dist/hudLayout.d.ts +31 -2
  30. package/dist/hudLayout.js +127 -10
  31. package/dist/hudLayoutPersist.d.ts +23 -0
  32. package/dist/hudLayoutPersist.js +19 -0
  33. package/dist/hudTheme.d.ts +76 -0
  34. package/dist/hudTheme.js +156 -0
  35. package/dist/hudThemePreview.d.ts +4 -0
  36. package/dist/hudThemePreview.js +37 -0
  37. package/dist/iconTreatment.d.ts +50 -0
  38. package/dist/iconTreatment.js +84 -0
  39. package/dist/iconsPreview.d.ts +4 -0
  40. package/dist/iconsPreview.js +29 -0
  41. package/dist/index.d.ts +17 -0
  42. package/dist/index.js +17 -0
  43. package/dist/keyHint.d.ts +30 -0
  44. package/dist/keyHint.js +31 -0
  45. package/dist/map.d.ts +141 -11
  46. package/dist/map.js +138 -16
  47. package/dist/save.d.ts +25 -0
  48. package/dist/save.js +37 -0
  49. package/dist/selectSnapshot.d.ts +2 -0
  50. package/dist/selectSnapshot.js +2 -0
  51. package/dist/selectionHud.d.ts +139 -0
  52. package/dist/selectionHud.js +188 -0
  53. package/dist/skillCheckPaint.d.ts +2 -0
  54. package/dist/skillCheckPaint.js +2 -0
  55. package/dist/slots.d.ts +44 -0
  56. package/dist/slots.js +37 -0
  57. package/dist/startScreen.d.ts +62 -0
  58. package/dist/startScreen.js +62 -0
  59. package/dist/store.d.ts +24 -0
  60. package/dist/store.js +9 -0
  61. package/package.json +4 -4
  62. package/skills/jgengine/SKILL.md +54 -0
  63. package/skills/jgengine/api.md +527 -0
  64. package/skills/jgengine/capabilities.md +131 -0
  65. package/skills/jgengine/recipes/minimal-game.md +88 -0
  66. package/skills/jgengine-ui/SKILL.md +41 -0
  67. package/skills/jgengine-ui/api.md +2007 -0
  68. package/skills/jgengine-ui/capabilities.md +211 -0
  69. package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
  70. package/skills/jgengine-ui/reference.md +513 -0
  71. package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
  72. package/llms.txt +0 -1724
package/llms.txt DELETED
@@ -1,1724 +0,0 @@
1
- # @jgengine/react
2
- > React UI layer for JGengine: GameProvider, hooks, and headless primitives over @jgengine/core.
3
-
4
- Version: 0.10.0
5
- License: AGPL-3.0-only
6
- Repository: https://github.com/Noisemaker111/jgengine
7
- Docs: https://jgengine.com
8
- Imports use deep paths: `@jgengine/react/<path>`.
9
-
10
- ## Exported surface
11
-
12
- ### @jgengine/react/chat
13
-
14
- - ChannelTabs (function): function ChannelTabs({ channels, active, onSelect, className, tabClassName, activeTabClassName, renderTab, }: { channels?: readonly string[]; active: string; onSelect: (channelId: string) => void; className?: string; tabClassName?: string; activeTabClassName?: string; renderTab?: (channelId: string,…
15
- - ChatInput (function): function ChatInput({ channelId, className, inputClassName, buttonClassName, placeholder, sendLabel, onSent, onRejected, }: { channelId: string; className?: string; inputClassName?: string; buttonClassName?: string; placeholder?: string; sendLabel?: ReactNode; onSent?: (message: ChatMessage) => void;…
16
- - ChatLog (function): function ChatLog({ channelId, limit, className, messageClassName, renderMessage, }: { channelId: string; limit?: number; className?: string; messageClassName?: string; renderMessage?: (message: ChatMessage) => ReactNode; }): React.JSX.Element
17
- - ChatPanel (function): function ChatPanel({ channels, initialChannel, limit, className, tabsClassName, tabClassName, activeTabClassName, logClassName, messageClassName, inputClassName, inputFieldClassName, sendButtonClassName, placeholder, renderMessage, onRejected, }: { channels?: readonly string[]; initialChannel?: stri…
18
- - 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.
19
-
20
- ### @jgengine/react/chatBubbles
21
-
22
- - ChatBubble (interface): interface ChatBubble
23
- - ChatBubblesOptions (interface): interface ChatBubblesOptions
24
- - latestChatBubbles (function): function latestChatBubbles(messages: readonly ChatMessage[], nowMs: number, ttlMs: number): ChatBubble[]
25
- - useChatBubbles (function): function useChatBubbles(options?: ChatBubblesOptions): readonly ChatBubble[]
26
- - useEntityChatBubble (function): function useEntityChatBubble(instanceId: string, options?: ChatBubblesOptions): ChatBubble | null
27
-
28
- ### @jgengine/react/components
29
-
30
- - CaptureOdds (function): function CaptureOdds({ chance, className, fillClassName, }: { chance: number; className?: string; fillClassName?: string; }): React.JSX.Element
31
- - CurrencyPill (function): function CurrencyPill({ currencyId, className }: { currencyId: string; className?: string }): React.JSX.Element
32
- - DeathScreen (function): function DeathScreen({ statId = "health", open, className, children, }: { statId?: string; open?: boolean; className?: string; children?: ReactNode; }): React.JSX.Element
33
- - DialogueBox (function): function DialogueBox({ dialogue, onChoice, rng, className, lineClassName, speakerClassName, choicesClassName, choiceClassName, checkClassName, }: { dialogue: DialogueDef; onChoice?: (choice: DialogueChoice, result: CheckResult | null) => void; rng?: () => number; className?: string; lineClassName?: …
34
- - DialogueCheck (interface): interface DialogueCheck
35
- - DialogueChoice (interface): interface DialogueChoice
36
- - DialogueDef (interface): interface DialogueDef
37
- - DialogueLine (type): type DialogueLine = { speaker: string; text: string } | { choices: readonly DialogueChoice[] }
38
- - HealthBar (function): function HealthBar({ instanceId, statId, className, fillClassName, }: { instanceId: string; statId: string; className?: string; fillClassName?: string; }): React.JSX.Element | null
39
- - KeybindRow (function): function KeybindRow({ action, keys, className, }: { action: string; keys: readonly string[]; className?: string; }): React.JSX.Element
40
- - 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
41
- - ProximityPrompt (function): function ProximityPrompt({ prompt, className, }: { prompt: ProximityPromptDef; className?: string; }): React.JSX.Element
42
- - QteTrack (function): function QteTrack({ steps, startedAt, className, stepClassName, activeClassName, doneClassName, }: { steps: readonly QteStep[]; startedAt: number; className?: string; stepClassName?: string; activeClassName?: string; doneClassName?: string; }): React.JSX.Element
43
- - Screen (function): function Screen({ id, open = true, className, children, }: { id: string; open?: boolean; className?: string; children?: ReactNode; }): React.JSX.Element | null
44
- - SkillCheckBar (function): function SkillCheckBar({ config, startedAt, className, trackClassName, zoneClassName, markerClassName, renderStatus, }: { config: SkillCheckConfig; startedAt: number; className?: string; trackClassName?: string; zoneClassName?: string; markerClassName?: string; renderStatus?: (result: SkillCheckResu…
45
- - SlotGrid (function): function SlotGrid({ inventoryId, className, renderSlot, }: { inventoryId: string; className?: string; renderSlot?: (slot: InventorySlot, index: number) => ReactNode; }): React.JSX.Element
46
- - 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
47
- - paintQteStepDom (function): function paintQteStepDom(elements: ReadonlyMap<string, HTMLElement>, steps: readonly QteStep[], elapsed: number, activeId: string | null, stepClassName?: string, activeClassName?: string, doneClassName?: string): void
48
- - paintSkillCheckDom (function): function paintSkillCheckDom(root: HTMLElement, zone: HTMLElement, marker: HTMLElement, config: SkillCheckConfig, result: SkillCheckResult): void
49
- - resolveDialogueInvoke (function): function resolveDialogueInvoke(choice: DialogueChoice, result: CheckResult | null): { command: string; args?: unknown } | null
50
-
51
- ### @jgengine/react/display
52
-
53
- - DisplayProfile (interface): interface DisplayProfile
54
- - useDisplayProfile (function): function useDisplayProfile(): DisplayProfile
55
-
56
- ### @jgengine/react/dragLayer
57
-
58
- - DragGhost (function): function DragGhost<T>({ layer, className, style, children, }: { layer: DragLayer<T>; className?: string; style?: CSSProperties; children?: (payload: DragPayload<T>) => ReactNode; }): React.JSX.Element | null
59
- - DragLayer (interface): interface DragLayer<T>
60
- - DragPayload (interface): interface DragPayload<T>
61
- - DragState (interface): interface DragState<T>
62
- - DraggableCard (function): function DraggableCard<T>({ id, value, layer, className, children, onRotate, }: { id: string; value: T; layer: DragLayer<T>; className?: string; children?: ReactNode; onRotate?: boolean; }): React.JSX.Element
63
- - DropInfo (interface): interface DropInfo<T>
64
- - DropZone (function): function DropZone<T>({ id, layer, className, activeClassName, cellSize, children, }: { id: string; layer: DragLayer<T>; className?: string; activeClassName?: string; cellSize?: number; children?: ReactNode; }): React.JSX.Element
65
- - useDragLayer (function): function useDragLayer<T>(options?: { onDrop?: (info: DropInfo<T>) => void; }): DragLayer<T>
66
-
67
- ### @jgengine/react/engineStore
68
-
69
- - EventfulEngineStore (interface): interface EventfulEngineStore<TEventMap extends object>
70
- - ReadableEngineStore (interface): interface ReadableEngineStore<TState>
71
- - useEngineEvent (function): function useEngineEvent<TEventMap extends object, K extends keyof TEventMap>(store: EventfulEngineStore<TEventMap>, eventName: K, handler: (payload: TEventMap[K]) => void): void
72
- - useEngineState (function): function useEngineState<TState>(store: ReadableEngineStore<TState>): TState
73
- - useEngineStore (function): function useEngineStore<TState, TSelected>(store: ReadableEngineStore<TState>, selector: (state: TState) => TSelected, isEqual: (previous: TSelected, next: TSelected) => boolean = Object.is): TSelected
74
-
75
- ### @jgengine/react/fogOverlay
76
-
77
- - FogCellRect (interface): interface FogCellRect
78
- - FogPaintSurface (interface): interface FogPaintSurface
79
- - createFogDataUrl (function): function createFogDataUrl(fog: FogCells, canvasSize: { width: number; height: number }, cellRect: (col: number, row: number) => FogCellRect | null, fill = "rgba(11, 15, 20, 0.82)"): string | null
80
- - forEachUnrevealedFogCell (function): function forEachUnrevealedFogCell(fog: FogCells, visit: (col: number, row: number) => void): number
81
- - paintFogOverlay (function): function paintFogOverlay(surface: FogPaintSurface, canvasSize: { width: number; height: number }, fog: FogCells, cellRect: (col: number, row: number) => FogCellRect | null, fill = "rgba(11, 15, 20, 0.82)"): number
82
-
83
- ### @jgengine/react/gameIcons
84
-
85
- - GAME_ICON_NAMES (const): const GAME_ICON_NAMES: readonly ["sword", "dagger", "axe", "hammer", "bow", "arrow", "staff", "wand", "spear", "crossbow", "gun", "bomb", "shield", "helmet", "chestplate", "boots", "gauntlet", "ring", "amulet", "cloak", "backpack", "torch", "potionRed", "potionBlue", "scroll", "tome", "meat", "bread…
86
- - GameIcon (function): function GameIcon({ name, size = 24, color, className, }: { name: GameIconName; size?: number; color?: string; className?: string; }): React.JSX.Element
87
- - GameIconName (type): type GameIconName = (typeof GAME_ICON_NAMES)[number]
88
- - iconForAction (function): function iconForAction(action: string): GameIconName | null — Control glyph for a semantic action name (`hardDrop`, `sprint`, `shiftLeft`), or null when no rule matches.
89
- - iconForItemId (function): function iconForItemId(itemId: string): GameIconName | null
90
- - isGameIconName (function): function isGameIconName(value: string): value is GameIconName
91
-
92
- ### @jgengine/react/gameViewport
93
-
94
- - GameViewportProvider (function): function GameViewportProvider({ platforms, className, style, children, }: { platforms?: readonly HudPlatform[]; className?: string; style?: CSSProperties; children?: ReactNode; }): React.JSX.Element — Provides the shared game viewport layout to everything it wraps. Mount it once around the whole game presentation (world, HUD, controls, system UI) so every subsystem reads one coordinated geometry and registers its rect for collision detection. Publishes live `--jg-viewport-*` / `--jg-visual-viewport-*` / `--jg-safe-*` CSS variables on its root.
95
- - LayoutRegionSpec (type): type LayoutRegionSpec = Omit<LayoutRegion, "rect"> — A region descriptor without its measured rectangle — the caller supplies geometry through `useRegisterLayoutRegion`.
96
- - RegionRecord (interface): interface RegionRecord extends LayoutRegion — A region registration: the full `LayoutRegion` plus the live element (dev outlining), rect measured by the shell/react side.
97
- - ViewportMetrics (interface): interface ViewportMetrics — Live viewport rectangles: the layout viewport and the visible `visualViewport`.
98
- - useGameLayoutMode (function): function useGameLayoutMode(): GameLayoutMode — The resolved explicit composition mode (`desktop-wide` … `mobile-portrait`).
99
- - useGameOrientation (function): function useGameOrientation(): LayoutOrientation — The live device orientation.
100
- - useGameViewportLayout (function): function useGameViewportLayout(): GameViewportLayout — The live shared viewport layout. Returns a neutral default outside a `GameViewportProvider` so it never throws in previews.
101
- - useLayoutCollisions (function): function useLayoutCollisions(): readonly LayoutCollision[] — Live forbidden/warned region collisions (empty outside a provider).
102
- - useRegisterLayoutRegion (function): function useRegisterLayoutRegion(spec: LayoutRegionSpec, ref: RefObject<HTMLElement | null>, enabled = true): void — Register the element behind `ref` as a layout region and keep its measured rectangle live (ResizeObserver + viewport changes). No-op outside a `GameViewportProvider`, so a component using it still works in isolation.
103
- - useReservedControlZones (function): function useReservedControlZones(): readonly LayoutRect[] — Rectangles reserved by touch controls and system UI — HUD placement should avoid these.
104
- - useViewportMetrics (function): function useViewportMetrics(): ViewportMetrics — Live visible viewport, tracking `window.visualViewport` (mobile browser chrome, pinch-zoom) with a layout-viewport fallback.
105
-
106
- ### @jgengine/react/hooks
107
-
108
- - AbilitySlotBindingOptions (interface): interface AbilitySlotBindingOptions
109
- - EventMeterView (interface): interface EventMeterView
110
- - UseAxisChannelResult (interface): interface UseAxisChannelResult
111
- - WorldBrowserState (interface): interface WorldBrowserState
112
- - abilityKitNeedsHeartbeat (function): function abilityKitNeedsHeartbeat(kit: AbilityKit, resourceAvailable?: number): boolean
113
- - createHeldKeyTracker (function): function createHeldKeyTracker(target: HeldKeyEventTarget): { isDown: (code: string) => boolean; dispose: () => void; }
114
- - eventMeterNeedsHeartbeat (function): function eventMeterNeedsHeartbeat(meter: EventMeter, previous: EventMeterView | null): boolean
115
- - localPlayerEntity (function): function localPlayerEntity(ctx: GameContext): SceneEntity | null
116
- - useAbilitySlot (function): function useAbilitySlot(kit: AbilityKit, slotId: string, resourceAvailable?: number, options?: AbilitySlotBindingOptions): AbilitySlotSnapshot | null
117
- - useAbilitySlots (function): function useAbilitySlots(kit: AbilityKit, resourceAvailable?: number, options?: AbilitySlotBindingOptions): AbilitySlotSnapshot[]
118
- - useActivePrompt (function): function useActivePrompt<T extends PositionedPrompt>(prompts?: readonly T[]): T | null
119
- - 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.
120
- - useChat (function): function useChat(channelId: string, options?: { limit?: number }): ChatMessage[]
121
- - useCurrency (function): function useCurrency(currencyId: string): number
122
- - useEntityStat (function): function useEntityStat(instanceId: string, statId: string): StatValue | null
123
- - useEventMeter (function): function useEventMeter(meter: EventMeter, options?: AbilitySlotBindingOptions): EventMeterView
124
- - useFeed (function): function useFeed({ action, limit }: { action: string; limit?: number }): FeedEntry[]
125
- - useFriendRequests (function): function useFriendRequests(): FriendRequestEntry[]
126
- - useFriends (function): function useFriends(): FriendEntry[]
127
- - useGame (function): function useGame(): { commands: GameContext["game"]["commands"]; events: GameEvents }
128
- - useGameClock (function): function useGameClock(): ClockSnapshot & { controls: SimClock }
129
- - 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.
130
- - useGameStore (function): function useGameStore<T>(selector: (ctx: GameContext) => T, isEqual: (previous: T, next: T) => boolean = Object.is): T
131
- - useHeldKeys (function): function useHeldKeys(): (code: string) => boolean — Held-key predicate backed by window keydown/keyup/blur listeners (blur clears held state so a released-off-window key doesn't stick). SSR-safe: listeners attach in an effect, never at module scope. The returned predicate is stable across renders.
132
- - useInventory (function): function useInventory(inventoryId: string): readonly InventorySlot[]
133
- - useLeaderboard (function): function useLeaderboard(stat: string, options: { scope: LeaderboardScope; limit?: number }): { userId: string; value: number }[]
134
- - useLocalPlayerDead (function): function useLocalPlayerDead(healthStatId = "health"): boolean
135
- - useNearestWorldItem (function): function useNearestWorldItem(radius: number): WorldItemRecord | null — Nearest ground item within `radius` of the local player — drives a pickup prompt/highlight.
136
- - useOptionalGamePhase (function): function useOptionalGamePhase(): GamePhase — Live run phase that degrades to `"playing"` when rendered outside a `GameProvider` (component showcases, previews), so phase-gated chrome never throws.
137
- - useParty (function): function useParty(): PartyMemberEntry[]
138
- - usePartyInvites (function): function usePartyInvites(): PartyInviteEntry[]
139
- - usePlayer (function): function usePlayer(): { userId: string; isNew: boolean }
140
- - usePresence (function): function usePresence(userId: string): PresenceInfo
141
- - useQuestJournal (function): function useQuestJournal(): QuestInstance[]
142
- - useRoster (function): function useRoster(userId?: string): readonly RosterEntry[]
143
- - useSceneEntities (function): function useSceneEntities(): readonly SceneEntity[]
144
- - useSceneObjects (function): function useSceneObjects(): readonly SceneObject[]
145
- - useTarget (function): function useTarget(fromInstanceId: string): string | null
146
- - 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.
147
- - useWorldInvites (function): function useWorldInvites(): WorldInvite[]
148
- - useWorldItems (function): function useWorldItems(): readonly WorldItemRecord[]
149
-
150
- ### @jgengine/react/hudLayout
151
-
152
- - HudCanvas (function): function HudCanvas({ layout, editChord, compactScale, showDuring, className, style, children, }: { layout: HudLayoutStore; editChord?: HudEditChord | false; /** Zoom applied to the whole HUD on compact displays. Default 0.85. */ compactScale?: number; /** Opt-in play-phase gate: render the HUD only … — Full-viewport HUD surface. Panels declared with `HudPanel` flow into nine anchor regions and stack automatically with a gap — no per-panel pixel offsets, no manual clearance for sibling panels, the touch-control dock (`--jg-hud-dock-clearance`), or device safe areas. On compact displays the whole surface scales down and each panel applies its `compact` behavior.
153
- - HudCompactMode (type): type HudCompactMode = "keep" | "chip" | "hide" — How a panel behaves on compact (phone-scale) displays. `keep` stays visible at the global compact scale, `chip` collapses to a small tap-to-expand pill, `hide` unmounts entirely.
154
- - HudEditChord (interface): interface HudEditChord
155
- - HudPanel (function): function HudPanel({ id, anchor = "top-left", order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, className, style, children, }: { id: string; anchor?: HudAnchor; /** Stack position within the r… — A HUD block that lives in one of the nine anchor regions. Panels sharing a region stack outward from the screen edge in ascending `order`. On fine pointers panels stay draggable through the edit chord; a dragged panel leaves the flow and keeps its custom placement. On compact displays custom placements are ignored and the `compact` behavior applies.
156
- - hudVisibleInPhase (function): function hudVisibleInPhase(showDuring: readonly GamePhase[] | undefined, phase: GamePhase): boolean — Whether a HUD element opted into `showDuring` is visible in the current phase; `undefined` = always visible (default).
157
- - useHudLayout (function): function useHudLayout(options?: { storageKey?: string; snap?: number; locked?: boolean; }): HudLayoutStore
158
-
159
- ### @jgengine/react/hudViewport
160
-
161
- - HudViewportContextValue (interface): interface HudViewportContextValue
162
- - HudViewportProvider (function): function HudViewportProvider({ platforms, config, userScale, children, }: { platforms: readonly HudPlatform[] | undefined; config: HudViewportConfig | undefined; userScale?: number; children?: ReactNode; }): React.JSX.Element — Mounted by the shell around `GameUI` so every `HudCanvas` inside the game picks up the game's `platforms`/`hudFit` declaration and the player's UI scale setting without any game-side wiring.
163
- - useHudViewport (function): function useHudViewport(): HudViewportContextValue | null
164
-
165
- ### @jgengine/react/identity
166
-
167
- - BetterAuthSessionState (interface): interface BetterAuthSessionState
168
- - BetterAuthUserShape (interface): interface BetterAuthUserShape
169
- - ClerkUserShape (interface): interface ClerkUserShape
170
- - ClerkUserState (interface): interface ClerkUserState
171
- - GameIdentityProvider (function): function GameIdentityProvider({ source, children, }: { source: IdentitySource; children?: ReactNode; }): React.JSX.Element
172
- - IdentitySource (interface): interface IdentitySource
173
- - RequireSession (function): function RequireSession({ fallback, loading, children, }: { fallback?: ReactNode; loading?: ReactNode; children?: ReactNode; }): React.JSX.Element
174
- - SignOutButton (function): function SignOutButton({ className, children, }: { className?: string; children?: ReactNode; }): React.JSX.Element | null
175
- - UserBadge (function): function UserBadge({ className, avatarClassName, nameClassName, renderBadge, }: { className?: string; avatarClassName?: string; nameClassName?: string; renderBadge?: (session: AuthSession) => ReactNode; }): React.JSX.Element | null
176
- - betterAuthIdentity (function): function betterAuthIdentity(state: BetterAuthSessionState, options?: { signOut?: () => void }): IdentitySource
177
- - clerkIdentity (function): function clerkIdentity(state: ClerkUserState, options?: { signOut?: () => void }): IdentitySource
178
- - guestIdentity (function): function guestIdentity(seed?: string): IdentitySource
179
- - useAuthedPlayer (function): function useAuthedPlayer(options?: { guestSeed?: string }): PlayerIdentity | null
180
- - useSession (function): function useSession(): IdentitySource
181
-
182
- ### @jgengine/react/index
183
-
184
- - AbilitySlotBindingOptions (interface): interface AbilitySlotBindingOptions
185
- - AddFriendButton (function): function AddFriendButton({ toUserId, className, children, onRequested, onRejected, }: { toUserId: string; className?: string; children?: ReactNode; onRequested?: (requestId: string) => void; onRejected?: (reason: string) => void; }): React.JSX.Element | null
186
- - BetterAuthSessionState (interface): interface BetterAuthSessionState
187
- - BetterAuthUserShape (interface): interface BetterAuthUserShape
188
- - CaptureOdds (function): function CaptureOdds({ chance, className, fillClassName, }: { chance: number; className?: string; fillClassName?: string; }): React.JSX.Element
189
- - ChannelTabs (function): function ChannelTabs({ channels, active, onSelect, className, tabClassName, activeTabClassName, renderTab, }: { channels?: readonly string[]; active: string; onSelect: (channelId: string) => void; className?: string; tabClassName?: string; activeTabClassName?: string; renderTab?: (channelId: string,…
190
- - ChatBubble (interface): interface ChatBubble
191
- - ChatBubblesOptions (interface): interface ChatBubblesOptions
192
- - ChatInput (function): function ChatInput({ channelId, className, inputClassName, buttonClassName, placeholder, sendLabel, onSent, onRejected, }: { channelId: string; className?: string; inputClassName?: string; buttonClassName?: string; placeholder?: string; sendLabel?: ReactNode; onSent?: (message: ChatMessage) => void;…
193
- - ChatLog (function): function ChatLog({ channelId, limit, className, messageClassName, renderMessage, }: { channelId: string; limit?: number; className?: string; messageClassName?: string; renderMessage?: (message: ChatMessage) => ReactNode; }): React.JSX.Element
194
- - ChatPanel (function): function ChatPanel({ channels, initialChannel, limit, className, tabsClassName, tabClassName, activeTabClassName, logClassName, messageClassName, inputClassName, inputFieldClassName, sendButtonClassName, placeholder, renderMessage, onRejected, }: { channels?: readonly string[]; initialChannel?: stri…
195
- - ClerkUserShape (interface): interface ClerkUserShape
196
- - ClerkUserState (interface): interface ClerkUserState
197
- - 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 (bearing to each `MarkerSet` entry).
198
- - CompassProps (interface): interface CompassProps
199
- - CurrencyPill (function): function CurrencyPill({ currencyId, className }: { currencyId: string; className?: string }): React.JSX.Element
200
- - DeathScreen (function): function DeathScreen({ statId = "health", open, className, children, }: { statId?: string; open?: boolean; className?: string; children?: ReactNode; }): React.JSX.Element
201
- - DialogueBox (function): function DialogueBox({ dialogue, onChoice, rng, className, lineClassName, speakerClassName, choicesClassName, choiceClassName, checkClassName, }: { dialogue: DialogueDef; onChoice?: (choice: DialogueChoice, result: CheckResult | null) => void; rng?: () => number; className?: string; lineClassName?: …
202
- - DialogueCheck (interface): interface DialogueCheck
203
- - DialogueChoice (interface): interface DialogueChoice
204
- - DialogueDef (interface): interface DialogueDef
205
- - DialogueLine (type): type DialogueLine = { speaker: string; text: string } | { choices: readonly DialogueChoice[] }
206
- - DisplayProfile (interface): interface DisplayProfile
207
- - DragGhost (function): function DragGhost<T>({ layer, className, style, children, }: { layer: DragLayer<T>; className?: string; style?: CSSProperties; children?: (payload: DragPayload<T>) => ReactNode; }): React.JSX.Element | null
208
- - DragLayer (interface): interface DragLayer<T>
209
- - DragPayload (interface): interface DragPayload<T>
210
- - DragState (interface): interface DragState<T>
211
- - DraggableCard (function): function DraggableCard<T>({ id, value, layer, className, children, onRotate, }: { id: string; value: T; layer: DragLayer<T>; className?: string; children?: ReactNode; onRotate?: boolean; }): React.JSX.Element
212
- - DropInfo (interface): interface DropInfo<T>
213
- - DropZone (function): function DropZone<T>({ id, layer, className, activeClassName, cellSize, children, }: { id: string; layer: DragLayer<T>; className?: string; activeClassName?: string; cellSize?: number; children?: ReactNode; }): React.JSX.Element
214
- - EmoteWheel (function): function EmoteWheel({ emotes, radius, open = true, className, emoteClassName, onPlayed, onRejected, renderEmote, }: { emotes: readonly string[]; radius?: number; open?: boolean; className?: string; emoteClassName?: string; onPlayed?: (emoteId: string) => void; onRejected?: (reason: string) => void; …
215
- - EventMeterView (interface): interface EventMeterView
216
- - EventfulEngineStore (interface): interface EventfulEngineStore<TEventMap extends object>
217
- - FriendRequestsList (function): function FriendRequestsList({ className, rowClassName, acceptClassName, declineClassName, emptyState, renderRequest, }: { className?: string; rowClassName?: string; acceptClassName?: string; declineClassName?: string; emptyState?: ReactNode; renderRequest?: (request: FriendRequestEntry) => ReactNode…
218
- - FriendRow (function): function FriendRow({ friend, className, dotClassName, children, }: { friend: FriendEntry; className?: string; dotClassName?: string; children?: ReactNode; }): React.JSX.Element
219
- - FriendsList (function): function FriendsList({ className, rowClassName, dotClassName, emptyState, renderFriend, }: { className?: string; rowClassName?: string; dotClassName?: string; emptyState?: ReactNode; renderFriend?: (friend: FriendEntry) => ReactNode; }): React.JSX.Element
220
- - GameIdentityProvider (function): function GameIdentityProvider({ source, children, }: { source: IdentitySource; children?: ReactNode; }): React.JSX.Element
221
- - GamePreviewComponent (type): type GamePreviewComponent = ComponentType<GamePreviewProps>
222
- - GamePreviewProps (type): type GamePreviewProps = { className?: string; }
223
- - GamePreviewStates (type): type GamePreviewStates = Record<string, GamePreviewComponent>
224
- - GameProvider (function): function GameProvider({ context, children }: { context: GameContext; children?: ReactNode }): React.JSX.Element
225
- - GameViewportProvider (function): function GameViewportProvider({ platforms, className, style, children, }: { platforms?: readonly HudPlatform[]; className?: string; style?: CSSProperties; children?: ReactNode; }): React.JSX.Element — Provides the shared game viewport layout to everything it wraps. Mount it once around the whole game presentation (world, HUD, controls, system UI) so every subsystem reads one coordinated geometry and registers its rect for collision detection. Publishes live `--jg-viewport-*` / `--jg-visual-viewport-*` / `--jg-safe-*` CSS variables on its root.
226
- - HealthBar (function): function HealthBar({ instanceId, statId, className, fillClassName, }: { instanceId: string; statId: string; className?: string; fillClassName?: string; }): React.JSX.Element | null
227
- - HudCanvas (function): function HudCanvas({ layout, editChord, compactScale, showDuring, className, style, children, }: { layout: HudLayoutStore; editChord?: HudEditChord | false; /** Zoom applied to the whole HUD on compact displays. Default 0.85. */ compactScale?: number; /** Opt-in play-phase gate: render the HUD only … — Full-viewport HUD surface. Panels declared with `HudPanel` flow into nine anchor regions and stack automatically with a gap — no per-panel pixel offsets, no manual clearance for sibling panels, the touch-control dock (`--jg-hud-dock-clearance`), or device safe areas. On compact displays the whole surface scales down and each panel applies its `compact` behavior.
228
- - HudCompactMode (type): type HudCompactMode = "keep" | "chip" | "hide" — How a panel behaves on compact (phone-scale) displays. `keep` stays visible at the global compact scale, `chip` collapses to a small tap-to-expand pill, `hide` unmounts entirely.
229
- - HudEditChord (interface): interface HudEditChord
230
- - HudPanel (function): function HudPanel({ id, anchor = "top-left", order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, className, style, children, }: { id: string; anchor?: HudAnchor; /** Stack position within the r… — A HUD block that lives in one of the nine anchor regions. Panels sharing a region stack outward from the screen edge in ascending `order`. On fine pointers panels stay draggable through the edit chord; a dragged panel leaves the flow and keeps its custom placement. On compact displays custom placements are ignored and the `compact` behavior applies.
231
- - HudViewportContextValue (interface): interface HudViewportContextValue
232
- - HudViewportProvider (function): function HudViewportProvider({ platforms, config, userScale, children, }: { platforms: readonly HudPlatform[] | undefined; config: HudViewportConfig | undefined; userScale?: number; children?: ReactNode; }): React.JSX.Element — Mounted by the shell around `GameUI` so every `HudCanvas` inside the game picks up the game's `platforms`/`hudFit` declaration and the player's UI scale setting without any game-side wiring.
233
- - IdentitySource (interface): interface IdentitySource
234
- - InviteToWorldButton (function): function InviteToWorldButton({ toUserId, target, className, children, onInvited, onRejected, }: { toUserId: string; target: WorldInviteTarget; className?: string; children?: ReactNode; onInvited?: (inviteId: string) => void; onRejected?: (reason: string) => void; }): React.JSX.Element | null
235
- - JoinByCode (function): function JoinByCode({ onJoin, className, inputClassName, buttonClassName, placeholder, children, }: { onJoin: (code: string) => void; className?: string; inputClassName?: string; buttonClassName?: string; placeholder?: string; children?: ReactNode; }): React.JSX.Element
236
- - KeybindRow (function): function KeybindRow({ action, keys, className, }: { action: string; keys: readonly string[]; className?: string; }): React.JSX.Element
237
- - LayoutRegionSpec (type): type LayoutRegionSpec = Omit<LayoutRegion, "rect"> — A region descriptor without its measured rectangle — the caller supplies geometry through `useRegisterLayoutRegion`.
238
- - LeavePartyButton (function): function LeavePartyButton({ className, children, }: { className?: string; children?: ReactNode; }): React.JSX.Element | null
239
- - 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
240
- - MapBounds (interface): interface MapBounds
241
- - MicToggle (function): function MicToggle({ voice, className, mutedLabel, unmutedLabel, }: { voice: VoiceState; className?: string; mutedLabel?: ReactNode; unmutedLabel?: ReactNode; }): React.JSX.Element
242
- - 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, 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 a core `MarkerSet` / `FogField`; supply your own `kindStyles` palette to reskin.
243
- - MinimapProps (interface): interface MinimapProps
244
- - PartyFrame (function): function PartyFrame({ className, rowClassName, dotClassName, emptyState, renderMember, }: { className?: string; rowClassName?: string; dotClassName?: string; emptyState?: ReactNode; renderMember?: (member: PartyMemberEntry) => ReactNode; }): React.JSX.Element
245
- - PartyInviteToast (function): function PartyInviteToast({ className, acceptClassName, declineClassName, renderInvite, }: { className?: string; acceptClassName?: string; declineClassName?: string; renderInvite?: (invite: PartyInviteEntry) => ReactNode; }): React.JSX.Element | null
246
- - PartyMemberRow (function): function PartyMemberRow({ member, className, dotClassName, children, }: { member: PartyMemberEntry; className?: string; dotClassName?: string; children?: ReactNode; }): React.JSX.Element
247
- - PresenceDot (function): function PresenceDot({ userId, className }: { userId: string; className?: string }): React.JSX.Element
248
- - ProximityPrompt (function): function ProximityPrompt({ prompt, className, }: { prompt: ProximityPromptDef; className?: string; }): React.JSX.Element
249
- - PushToTalkButton (function): function PushToTalkButton({ voice, className, children, }: { voice: VoiceState; className?: string; children?: ReactNode; }): React.JSX.Element
250
- - QteTrack (function): function QteTrack({ steps, startedAt, className, stepClassName, activeClassName, doneClassName, }: { steps: readonly QteStep[]; startedAt: number; className?: string; stepClassName?: string; activeClassName?: string; doneClassName?: string; }): React.JSX.Element
251
- - QuickMatchButton (function): function QuickMatchButton({ listings, onJoin, onNoMatch, filter, className, children, }: { listings: readonly SessionListing[]; onJoin: (listing: SessionListing) => void; onNoMatch?: () => void; filter?: MatchFilter; className?: string; children?: ReactNode; }): React.JSX.Element
252
- - ReadableEngineStore (interface): interface ReadableEngineStore<TState>
253
- - RegionRecord (interface): interface RegionRecord extends LayoutRegion — A region registration: the full `LayoutRegion` plus the live element (dev outlining), rect measured by the shell/react side.
254
- - RequireSession (function): function RequireSession({ fallback, loading, children, }: { fallback?: ReactNode; loading?: ReactNode; children?: ReactNode; }): React.JSX.Element
255
- - RotateDeviceScreen (function): function RotateDeviceScreen({ title = "Turn your device", description, requiredOrientation = "landscape", accent = "var(--jg-accent, #8ea2ff)", icon, className, style, }: { /** Short headline. */ title?: string; /** One concise explanatory line. Defaults from `requiredOrientation`. */ description?: … — Full-viewport, non-dismissible rotate-device gate shown when a game requires an orientation the device isn't in.
256
- - Screen (function): function Screen({ id, open = true, className, children, }: { id: string; open?: boolean; className?: string; children?: ReactNode; }): React.JSX.Element | null
257
- - SettingsActionView (interface): interface SettingsActionView — A resolved game-state action — `run` is already bound to the game context and closes the menu.
258
- - SettingsCategoryView (interface): interface SettingsCategoryView — A settings menu category with its rows and keybinds, ready to render.
259
- - SettingsController (interface): interface SettingsController — The live settings controller — every category/row/keybind/action plus open-state. Render it any way you like or drive the engine menu.
260
- - SettingsControllerProvider (function): function SettingsControllerProvider({ controller, children, }: { controller: SettingsController; children: ReactNode; }): React.JSX.Element
261
- - SettingsKeybindRow (interface): interface SettingsKeybindRow — One rebindable action row rendered in the controls settings category.
262
- - SettingsProvider (function): function SettingsProvider({ store, children }: { store: SettingsStore; children: ReactNode }): React.JSX.Element
263
- - SettingsRow (interface): interface SettingsRow — One editable setting rendered in a settings menu category.
264
- - SettingsTrigger (function): function SettingsTrigger({ className, children, label = "Settings", }: { className?: string; children?: ReactNode; label?: string; }): React.JSX.Element | null — Inline settings entry — drop it anywhere in your game's menu or HUD; it opens the themed settings menu. Headless: pass `className` for placement/skin and `children` to replace the default gear glyph. Renders nothing when the game has no settings to show, so it never leaves a dead button behind.
265
- - SignOutButton (function): function SignOutButton({ className, children, }: { className?: string; children?: ReactNode; }): React.JSX.Element | null
266
- - SkillCheckBar (function): function SkillCheckBar({ config, startedAt, className, trackClassName, zoneClassName, markerClassName, renderStatus, }: { config: SkillCheckConfig; startedAt: number; className?: string; trackClassName?: string; zoneClassName?: string; markerClassName?: string; renderStatus?: (result: SkillCheckResu…
267
- - SlotGrid (function): function SlotGrid({ inventoryId, className, renderSlot, }: { inventoryId: string; className?: string; renderSlot?: (slot: InventorySlot, index: number) => ReactNode; }): React.JSX.Element
268
- - SpeakingIndicator (function): function SpeakingIndicator({ voice, userId, className, threshold = 0.01, children, }: { voice: VoiceState; userId: string; className?: string; threshold?: number; children?: ReactNode; }): React.JSX.Element
269
- - 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
270
- - UseAxisChannelResult (interface): interface UseAxisChannelResult
271
- - UseVoiceOptions (interface): interface UseVoiceOptions
272
- - UserBadge (function): function UserBadge({ className, avatarClassName, nameClassName, renderBadge, }: { className?: string; avatarClassName?: string; nameClassName?: string; renderBadge?: (session: AuthSession) => ReactNode; }): React.JSX.Element | null
273
- - ViewportMetrics (interface): interface ViewportMetrics — Live viewport rectangles: the layout viewport and the visible `visualViewport`.
274
- - VoiceRoster (function): function VoiceRoster({ voice, className, participantClassName, renderParticipant, }: { voice: VoiceState; className?: string; participantClassName?: string; renderParticipant?: (participant: VoiceParticipant, gain: number) => ReactNode; }): React.JSX.Element
275
- - VoiceState (interface): interface VoiceState
276
- - WorldBrowser (function): function WorldBrowser({ listings, onJoin, className, rowClassName, joinClassName, emptyState, renderListing, }: { listings: readonly SessionListing[]; onJoin: (listing: SessionListing) => void; className?: string; rowClassName?: string; joinClassName?: string; emptyState?: ReactNode; renderListing?:…
277
- - WorldBrowserState (interface): interface WorldBrowserState
278
- - WorldInviteToast (function): function WorldInviteToast({ className, acceptClassName, declineClassName, onAccepted, renderInvite, }: { className?: string; acceptClassName?: string; declineClassName?: string; onAccepted: (target: WorldInviteTarget) => void; renderInvite?: (invite: WorldInvite) => ReactNode; }): React.JSX.Element …
279
- - WorldMap (function): function WorldMap({ markers, bounds, player, facingYaw = 0, fog, background, width = 520, height, kindStyles = DEFAULT_MARKER_KINDS, routes, zones, cellStates, onWorldClick, className, title = "World Map", onClose, }: WorldMapProps): ReactNode — Full-bounds top-down world map (the "press M" overlay): baked terrain background, reveal-on-event fog, all markers with labels, and the player. Rectangular linear projection over the supplied world `bounds`.
280
- - WorldMapProps (interface): interface WorldMapProps
281
- - abilityKitNeedsHeartbeat (function): function abilityKitNeedsHeartbeat(kit: AbilityKit, resourceAvailable?: number): boolean
282
- - betterAuthIdentity (function): function betterAuthIdentity(state: BetterAuthSessionState, options?: { signOut?: () => void }): IdentitySource
283
- - 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.
284
- - clerkIdentity (function): function clerkIdentity(state: ClerkUserState, options?: { signOut?: () => void }): IdentitySource
285
- - createHeldKeyTracker (function): function createHeldKeyTracker(target: HeldKeyEventTarget): { isDown: (code: string) => boolean; dispose: () => void; }
286
- - eventMeterNeedsHeartbeat (function): function eventMeterNeedsHeartbeat(meter: EventMeter, previous: EventMeterView | null): boolean
287
- - guestIdentity (function): function guestIdentity(seed?: string): IdentitySource
288
- - hudVisibleInPhase (function): function hudVisibleInPhase(showDuring: readonly GamePhase[] | undefined, phase: GamePhase): boolean — Whether a HUD element opted into `showDuring` is visible in the current phase; `undefined` = always visible (default).
289
- - latestChatBubbles (function): function latestChatBubbles(messages: readonly ChatMessage[], nowMs: number, ttlMs: number): ChatBubble[]
290
- - localPlayerEntity (function): function localPlayerEntity(ctx: GameContext): SceneEntity | null
291
- - paintQteStepDom (function): function paintQteStepDom(elements: ReadonlyMap<string, HTMLElement>, steps: readonly QteStep[], elapsed: number, activeId: string | null, stepClassName?: string, activeClassName?: string, doneClassName?: string): void
292
- - paintSkillCheckDom (function): function paintSkillCheckDom(root: HTMLElement, zone: HTMLElement, marker: HTMLElement, config: SkillCheckConfig, result: SkillCheckResult): void
293
- - resolveDialogueInvoke (function): function resolveDialogueInvoke(choice: DialogueChoice, result: CheckResult | null): { command: string; args?: unknown } | null
294
- - useAbilitySlot (function): function useAbilitySlot(kit: AbilityKit, slotId: string, resourceAvailable?: number, options?: AbilitySlotBindingOptions): AbilitySlotSnapshot | null
295
- - useAbilitySlots (function): function useAbilitySlots(kit: AbilityKit, resourceAvailable?: number, options?: AbilitySlotBindingOptions): AbilitySlotSnapshot[]
296
- - useActivePrompt (function): function useActivePrompt<T extends PositionedPrompt>(prompts?: readonly T[]): T | null
297
- - useAuthedPlayer (function): function useAuthedPlayer(options?: { guestSeed?: string }): PlayerIdentity | null
298
- - 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.
299
- - useChat (function): function useChat(channelId: string, options?: { limit?: number }): ChatMessage[]
300
- - useChatBubbles (function): function useChatBubbles(options?: ChatBubblesOptions): readonly ChatBubble[]
301
- - useCurrency (function): function useCurrency(currencyId: string): number
302
- - useDisplayProfile (function): function useDisplayProfile(): DisplayProfile
303
- - useDragLayer (function): function useDragLayer<T>(options?: { onDrop?: (info: DropInfo<T>) => void; }): DragLayer<T>
304
- - useEngineEvent (function): function useEngineEvent<TEventMap extends object, K extends keyof TEventMap>(store: EventfulEngineStore<TEventMap>, eventName: K, handler: (payload: TEventMap[K]) => void): void
305
- - useEngineState (function): function useEngineState<TState>(store: ReadableEngineStore<TState>): TState
306
- - useEngineStore (function): function useEngineStore<TState, TSelected>(store: ReadableEngineStore<TState>, selector: (state: TState) => TSelected, isEqual: (previous: TSelected, next: TSelected) => boolean = Object.is): TSelected
307
- - useEntityChatBubble (function): function useEntityChatBubble(instanceId: string, options?: ChatBubblesOptions): ChatBubble | null
308
- - useEntityStat (function): function useEntityStat(instanceId: string, statId: string): StatValue | null
309
- - useEventMeter (function): function useEventMeter(meter: EventMeter, options?: AbilitySlotBindingOptions): EventMeterView
310
- - useFeed (function): function useFeed({ action, limit }: { action: string; limit?: number }): FeedEntry[]
311
- - useFog (function): function useFog(fog: FogField): ReturnType<FogField["cells"]>
312
- - useFriendRequests (function): function useFriendRequests(): FriendRequestEntry[]
313
- - useFriends (function): function useFriends(): FriendEntry[]
314
- - useGame (function): function useGame(): { commands: GameContext["game"]["commands"]; events: GameEvents }
315
- - useGameClock (function): function useGameClock(): ClockSnapshot & { controls: SimClock }
316
- - useGameContext (function): function useGameContext(): GameContext
317
- - useGameLayoutMode (function): function useGameLayoutMode(): GameLayoutMode — The resolved explicit composition mode (`desktop-wide` … `mobile-portrait`).
318
- - useGameOrientation (function): function useGameOrientation(): LayoutOrientation — The live device orientation.
319
- - 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.
320
- - useGameStore (function): function useGameStore<T>(selector: (ctx: GameContext) => T, isEqual: (previous: T, next: T) => boolean = Object.is): T
321
- - useGameViewportLayout (function): function useGameViewportLayout(): GameViewportLayout — The live shared viewport layout. Returns a neutral default outside a `GameViewportProvider` so it never throws in previews.
322
- - useHasSettings (function): function useHasSettings(): boolean — True when the game has any setting or game-action to show — gate your own settings entry on it.
323
- - useHeldKeys (function): function useHeldKeys(): (code: string) => boolean — Held-key predicate backed by window keydown/keyup/blur listeners (blur clears held state so a released-off-window key doesn't stick). SSR-safe: listeners attach in an effect, never at module scope. The returned predicate is stable across renders.
324
- - useHudLayout (function): function useHudLayout(options?: { storageKey?: string; snap?: number; locked?: boolean; }): HudLayoutStore
325
- - useHudViewport (function): function useHudViewport(): HudViewportContextValue | null
326
- - useInventory (function): function useInventory(inventoryId: string): readonly InventorySlot[]
327
- - useLayoutCollisions (function): function useLayoutCollisions(): readonly LayoutCollision[] — Live forbidden/warned region collisions (empty outside a provider).
328
- - useLeaderboard (function): function useLeaderboard(stat: string, options: { scope: LeaderboardScope; limit?: number }): { userId: string; value: number }[]
329
- - useLocalPlayerDead (function): function useLocalPlayerDead(healthStatId = "health"): boolean
330
- - useMarkers (function): function useMarkers(markers: MarkerSet): readonly MapMarker[]
331
- - useNearestWorldItem (function): function useNearestWorldItem(radius: number): WorldItemRecord | null — Nearest ground item within `radius` of the local player — drives a pickup prompt/highlight.
332
- - useOptionalGameContext (function): function useOptionalGameContext(): GameContext | null — The game context if a `GameProvider` is present, otherwise `null` — for chrome that may render outside a running game (showcases, previews).
333
- - useOptionalGamePhase (function): function useOptionalGamePhase(): GamePhase — Live run phase that degrades to `"playing"` when rendered outside a `GameProvider` (component showcases, previews), so phase-gated chrome never throws.
334
- - useParty (function): function useParty(): PartyMemberEntry[]
335
- - usePartyInvites (function): function usePartyInvites(): PartyInviteEntry[]
336
- - usePlayer (function): function usePlayer(): { userId: string; isNew: boolean }
337
- - usePresence (function): function usePresence(userId: string): PresenceInfo
338
- - useQuestJournal (function): function useQuestJournal(): QuestInstance[]
339
- - useRegisterLayoutRegion (function): function useRegisterLayoutRegion(spec: LayoutRegionSpec, ref: RefObject<HTMLElement | null>, enabled = true): void — Register the element behind `ref` as a layout region and keep its measured rectangle live (ResizeObserver + viewport changes). No-op outside a `GameViewportProvider`, so a component using it still works in isolation.
340
- - useReservedControlZones (function): function useReservedControlZones(): readonly LayoutRect[] — Rectangles reserved by touch controls and system UI — HUD placement should avoid these.
341
- - useRoster (function): function useRoster(userId?: string): readonly RosterEntry[]
342
- - useSceneEntities (function): function useSceneEntities(): readonly SceneEntity[]
343
- - useSceneObjects (function): function useSceneObjects(): readonly SceneObject[]
344
- - useSession (function): function useSession(): IdentitySource
345
- - useSetting (function): function useSetting<T extends SettingValue>(id: string, fallback: T): readonly [T, (value: SettingValue) => void] — Read + write one persisted setting; re-renders when the value changes anywhere.
346
- - 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.
347
- - useSettingsStore (function): function useSettingsStore(): SettingsStore — The shared settings store, or a standalone one when no provider is mounted (game code read outside the shell).
348
- - useTarget (function): function useTarget(fromInstanceId: string): string | null
349
- - useViewportMetrics (function): function useViewportMetrics(): ViewportMetrics — Live visible viewport, tracking `window.visualViewport` (mobile browser chrome, pinch-zoom) with a layout-viewport fallback.
350
- - useVoice (function): function useVoice(options?: UseVoiceOptions): VoiceState — Mic capture + push-to-talk + channel roster over the VoiceTransport signaling seam. Transmission gates the captured tracks' `enabled` flag; the media plane that actually moves audio bytes (WebRTC/SFU) stays behind the transport, host-supplied. Call once per voice channel and hand the returned state to the voice components.
351
- - 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.
352
- - useWorldInvites (function): function useWorldInvites(): WorldInvite[]
353
- - useWorldItems (function): function useWorldItems(): readonly WorldItemRecord[]
354
-
355
- ### @jgengine/react/liveBind
356
-
357
- - LiveText (function): function LiveText({ get, format, className, style, }: { get: () => number | string; format?: (value: number | string) => string; className?: string; style?: CSSProperties; }): React.JSX.Element
358
- - frameBindSubscriberCount (function): function frameBindSubscriberCount(): number
359
- - subscribeFrameBind (function): function subscribeFrameBind(subscriber: FrameSubscriber): () => void
360
- - useFrameBind (function): function useFrameBind<T, E extends Element = Element>(ref: { current: E | null }, get: () => T, apply: (value: T, element: E) => void): void
361
-
362
- ### @jgengine/react/map
363
-
364
- - 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 (bearing to each `MarkerSet` entry).
365
- - CompassProps (interface): interface CompassProps
366
- - MapBounds (interface): interface MapBounds
367
- - 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, 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 a core `MarkerSet` / `FogField`; supply your own `kindStyles` palette to reskin.
368
- - MinimapProps (interface): interface MinimapProps
369
- - WorldMap (function): function WorldMap({ markers, bounds, player, facingYaw = 0, fog, background, width = 520, height, kindStyles = DEFAULT_MARKER_KINDS, routes, zones, cellStates, onWorldClick, className, title = "World Map", onClose, }: WorldMapProps): ReactNode — Full-bounds top-down world map (the "press M" overlay): baked terrain background, reveal-on-event fog, all markers with labels, and the player. Rectangular linear projection over the supplied world `bounds`.
370
- - WorldMapProps (interface): interface WorldMapProps
371
- - useFog (function): function useFog(fog: FogField): ReturnType<FogField["cells"]>
372
- - useMarkers (function): function useMarkers(markers: MarkerSet): readonly MapMarker[]
373
-
374
- ### @jgengine/react/preview
375
-
376
- - GamePreviewComponent (type): type GamePreviewComponent = ComponentType<GamePreviewProps>
377
- - GamePreviewProps (type): type GamePreviewProps = { className?: string; }
378
- - GamePreviewStates (type): type GamePreviewStates = Record<string, GamePreviewComponent>
379
-
380
- ### @jgengine/react/provider
381
-
382
- - GameProvider (function): function GameProvider({ context, children }: { context: GameContext; children?: ReactNode }): React.JSX.Element
383
- - useGameContext (function): function useGameContext(): GameContext
384
- - useOptionalGameContext (function): function useOptionalGameContext(): GameContext | null — The game context if a `GameProvider` is present, otherwise `null` — for chrome that may render outside a running game (showcases, previews).
385
-
386
- ### @jgengine/react/rotateDevice
387
-
388
- - RotateDeviceScreen (function): function RotateDeviceScreen({ title = "Turn your device", description, requiredOrientation = "landscape", accent = "var(--jg-accent, #8ea2ff)", icon, className, style, }: { /** Short headline. */ title?: string; /** One concise explanatory line. Defaults from `requiredOrientation`. */ description?: … — Full-viewport, non-dismissible rotate-device gate shown when a game requires an orientation the device isn't in.
389
-
390
- ### @jgengine/react/selectSnapshot
391
-
392
- - SelectCache (type): type SelectCache<TSnapshot, TSelected> = { ready: boolean; snapshot: TSnapshot; value: TSelected; }
393
- - createSelectCache (function): function createSelectCache<TSnapshot, TSelected>(): SelectCache<TSnapshot, TSelected>
394
- - readSelectSnapshot (function): function readSelectSnapshot<TSnapshot, TSelected>(cache: SelectCache<TSnapshot, TSelected>, snapshot: TSnapshot, select: (snapshot: TSnapshot) => TSelected, isEqual: (previous: TSelected, next: TSelected) => boolean = Object.is): TSelected
395
-
396
- ### @jgengine/react/settings
397
-
398
- - SettingsActionView (interface): interface SettingsActionView — A resolved game-state action — `run` is already bound to the game context and closes the menu.
399
- - SettingsCategoryView (interface): interface SettingsCategoryView — A settings menu category with its rows and keybinds, ready to render.
400
- - SettingsController (interface): interface SettingsController — The live settings controller — every category/row/keybind/action plus open-state. Render it any way you like or drive the engine menu.
401
- - SettingsControllerProvider (function): function SettingsControllerProvider({ controller, children, }: { controller: SettingsController; children: ReactNode; }): React.JSX.Element
402
- - SettingsKeybindRow (interface): interface SettingsKeybindRow — One rebindable action row rendered in the controls settings category.
403
- - SettingsProvider (function): function SettingsProvider({ store, children }: { store: SettingsStore; children: ReactNode }): React.JSX.Element
404
- - SettingsRow (interface): interface SettingsRow — One editable setting rendered in a settings menu category.
405
- - SettingsTrigger (function): function SettingsTrigger({ className, children, label = "Settings", }: { className?: string; children?: ReactNode; label?: string; }): React.JSX.Element | null — Inline settings entry — drop it anywhere in your game's menu or HUD; it opens the themed settings menu. Headless: pass `className` for placement/skin and `children` to replace the default gear glyph. Renders nothing when the game has no settings to show, so it never leaves a dead button behind.
406
- - useHasSettings (function): function useHasSettings(): boolean — True when the game has any setting or game-action to show — gate your own settings entry on it.
407
- - useSetting (function): function useSetting<T extends SettingValue>(id: string, fallback: T): readonly [T, (value: SettingValue) => void] — Read + write one persisted setting; re-renders when the value changes anywhere.
408
- - 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.
409
- - useSettingsStore (function): function useSettingsStore(): SettingsStore — The shared settings store, or a standalone one when no provider is mounted (game code read outside the shell).
410
-
411
- ### @jgengine/react/skillCheckPaint
412
-
413
- - paintQteStepDom (function): function paintQteStepDom(elements: ReadonlyMap<string, HTMLElement>, steps: readonly QteStep[], elapsed: number, activeId: string | null, stepClassName?: string, activeClassName?: string, doneClassName?: string): void
414
- - paintSkillCheckDom (function): function paintSkillCheckDom(root: HTMLElement, zone: HTMLElement, marker: HTMLElement, config: SkillCheckConfig, result: SkillCheckResult): void
415
-
416
- ### @jgengine/react/social
417
-
418
- - AddFriendButton (function): function AddFriendButton({ toUserId, className, children, onRequested, onRejected, }: { toUserId: string; className?: string; children?: ReactNode; onRequested?: (requestId: string) => void; onRejected?: (reason: string) => void; }): React.JSX.Element | null
419
- - EmoteWheel (function): function EmoteWheel({ emotes, radius, open = true, className, emoteClassName, onPlayed, onRejected, renderEmote, }: { emotes: readonly string[]; radius?: number; open?: boolean; className?: string; emoteClassName?: string; onPlayed?: (emoteId: string) => void; onRejected?: (reason: string) => void; …
420
- - FriendRequestsList (function): function FriendRequestsList({ className, rowClassName, acceptClassName, declineClassName, emptyState, renderRequest, }: { className?: string; rowClassName?: string; acceptClassName?: string; declineClassName?: string; emptyState?: ReactNode; renderRequest?: (request: FriendRequestEntry) => ReactNode…
421
- - FriendRow (function): function FriendRow({ friend, className, dotClassName, children, }: { friend: FriendEntry; className?: string; dotClassName?: string; children?: ReactNode; }): React.JSX.Element
422
- - FriendsList (function): function FriendsList({ className, rowClassName, dotClassName, emptyState, renderFriend, }: { className?: string; rowClassName?: string; dotClassName?: string; emptyState?: ReactNode; renderFriend?: (friend: FriendEntry) => ReactNode; }): React.JSX.Element
423
- - InviteToWorldButton (function): function InviteToWorldButton({ toUserId, target, className, children, onInvited, onRejected, }: { toUserId: string; target: WorldInviteTarget; className?: string; children?: ReactNode; onInvited?: (inviteId: string) => void; onRejected?: (reason: string) => void; }): React.JSX.Element | null
424
- - JoinByCode (function): function JoinByCode({ onJoin, className, inputClassName, buttonClassName, placeholder, children, }: { onJoin: (code: string) => void; className?: string; inputClassName?: string; buttonClassName?: string; placeholder?: string; children?: ReactNode; }): React.JSX.Element
425
- - LeavePartyButton (function): function LeavePartyButton({ className, children, }: { className?: string; children?: ReactNode; }): React.JSX.Element | null
426
- - PartyFrame (function): function PartyFrame({ className, rowClassName, dotClassName, emptyState, renderMember, }: { className?: string; rowClassName?: string; dotClassName?: string; emptyState?: ReactNode; renderMember?: (member: PartyMemberEntry) => ReactNode; }): React.JSX.Element
427
- - PartyInviteToast (function): function PartyInviteToast({ className, acceptClassName, declineClassName, renderInvite, }: { className?: string; acceptClassName?: string; declineClassName?: string; renderInvite?: (invite: PartyInviteEntry) => ReactNode; }): React.JSX.Element | null
428
- - PartyMemberRow (function): function PartyMemberRow({ member, className, dotClassName, children, }: { member: PartyMemberEntry; className?: string; dotClassName?: string; children?: ReactNode; }): React.JSX.Element
429
- - PresenceDot (function): function PresenceDot({ userId, className }: { userId: string; className?: string }): React.JSX.Element
430
- - QuickMatchButton (function): function QuickMatchButton({ listings, onJoin, onNoMatch, filter, className, children, }: { listings: readonly SessionListing[]; onJoin: (listing: SessionListing) => void; onNoMatch?: () => void; filter?: MatchFilter; className?: string; children?: ReactNode; }): React.JSX.Element
431
- - WorldBrowser (function): function WorldBrowser({ listings, onJoin, className, rowClassName, joinClassName, emptyState, renderListing, }: { listings: readonly SessionListing[]; onJoin: (listing: SessionListing) => void; className?: string; rowClassName?: string; joinClassName?: string; emptyState?: ReactNode; renderListing?:…
432
- - WorldInviteToast (function): function WorldInviteToast({ className, acceptClassName, declineClassName, onAccepted, renderInvite, }: { className?: string; acceptClassName?: string; declineClassName?: string; onAccepted: (target: WorldInviteTarget) => void; renderInvite?: (invite: WorldInvite) => ReactNode; }): React.JSX.Element …
433
-
434
- ### @jgengine/react/voice
435
-
436
- - MicToggle (function): function MicToggle({ voice, className, mutedLabel, unmutedLabel, }: { voice: VoiceState; className?: string; mutedLabel?: ReactNode; unmutedLabel?: ReactNode; }): React.JSX.Element
437
- - PushToTalkButton (function): function PushToTalkButton({ voice, className, children, }: { voice: VoiceState; className?: string; children?: ReactNode; }): React.JSX.Element
438
- - SpeakingIndicator (function): function SpeakingIndicator({ voice, userId, className, threshold = 0.01, children, }: { voice: VoiceState; userId: string; className?: string; threshold?: number; children?: ReactNode; }): React.JSX.Element
439
- - UseVoiceOptions (interface): interface UseVoiceOptions
440
- - VoiceRoster (function): function VoiceRoster({ voice, className, participantClassName, renderParticipant, }: { voice: VoiceState; className?: string; participantClassName?: string; renderParticipant?: (participant: VoiceParticipant, gain: number) => ReactNode; }): React.JSX.Element
441
- - VoiceState (interface): interface VoiceState
442
- - useVoice (function): function useVoice(options?: UseVoiceOptions): VoiceState — Mic capture + push-to-talk + channel roster over the VoiceTransport signaling seam. Transmission gates the captured tracks' `enabled` flag; the media plane that actually moves audio bytes (WebRTC/SFU) stays behind the transport, host-supplied. Call once per voice channel and hand the returned state to the voice components.
443
-
444
- ## Guides
445
-
446
- # JGengine
447
-
448
- JGengine is a pure-TypeScript game-engine SDK for games built by AI coding agents. Its skills teach the agent how to use the SDK. When a user says “make X with JGengine” or “make X with jgengine.com,” treat that as sufficient intake. If the skills are not installed, run `npx jgengine skills`; then use this skill and build on the SDK. Do not make the user discover or name skills.
449
-
450
- ## Intake
451
-
452
- State the reading as a short numbered list that is easy to correct, then proceed unless the user changes it. Prefer concrete values and actions over prose:
453
-
454
- 1. **POV:** first-person
455
- 2. **World:** custom 3D wasteland with three settlements
456
- 3. **Core loop:** get quests by talking to people → defeat enemies → return to redeem quests
457
- 4. **Interaction:** collect ground items by walking over them; interact with people and doors at close range
458
- 5. **Combat:** ranged weapons, damage, death, loot
459
- 6. **Progression:** inventory, currency, quest rewards, upgrades
460
- 7. **Players:** single-player, or name the multiplayer topology and synchronized systems
461
- 8. **UI:** visible controls, objective tracker, health, inventory feedback
462
- 9. **Art direction:** one aesthetic, palette, asset family, and UI voice
463
- 10. **Done looks like:** one observable end-to-end play scenario
464
-
465
- Keep this compact—roughly one line per item. It is a build map, not a large specification or an approval gate. Infer conventional details from the named game or genre. Ask only when two plausible readings would fundamentally change the game.
466
-
467
- ## Route selectively
468
-
469
- This skill is the foundation for every task (packages, project shape, defineGame, context, catalogs). After intake, also read only the domain skills the work needs:
470
-
471
- | Need | Read |
472
- | --- | --- |
473
- | Terrain, scenes, camera, movement, physics, maps, sensors | `jgengine-world` |
474
- | Seeded generation, terrain/environment generation, grids, buildings, simulation | `jgengine-procedural` |
475
- | Damage, effects, weapons, targeting, projectiles, loot, death | `jgengine-combat` |
476
- | Items, quests, dialogue, economy, crafting, objectives, turns, social systems | `jgengine-gameplay` |
477
- | Networking adapters, authority, rooms/topology, persistence/backend seams | `jgengine-multiplayer` |
478
- | React HUD, shell affordances, controls, feedback, accessibility | `jgengine-ui` |
479
- | Models, sprites, textures, audio, catalogs, attribution | `jgengine-assets` |
480
- | Proof and screenshots | `jgengine-verify` after implementation |
481
-
482
- Do not read every domain by default. Build through documented engine surfaces; do not infer APIs from gallery games. Inspect engine source only when a documented surface appears wrong or a missing primitive blocks the work.
483
-
484
- ## Build behavior
485
-
486
- Scaffold with `npx jgengine create game-name --name "Game Name"` when needed. Build the requested game continuously from the intake, keeping systems end-to-end rather than leaving registered-but-unusable pieces. Use real assets and visible feedback early. Verify at completion with `jgengine-verify`.
487
-
488
- Cartridge-shaped games (declarative config, engine-owned loop): see [reference-cartridge.md](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine/reference-cartridge.md).
489
-
490
- ---
491
-
492
- The engine ships **verbs and primitives**; your game ships **nouns** (catalogs) and thin handlers. This skill is that foundation plus intake. Use domain skills only when needed; use `jgengine-verify` afterward. UI guidance lives in [`../jgengine-ui/reference.md`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-ui/reference.md); assets live in `jgengine-assets`.
493
-
494
- Load detailed references only for the selected domain: [`jgengine-combat`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-combat/reference.md), [`jgengine-world`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-world/reference.md), [`jgengine-multiplayer`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-multiplayer/reference.md), and [`jgengine-ui`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-ui/reference.md). Each domain skill explains when its reference is needed.
495
-
496
- ## Packages
497
-
498
- All published on npm, source at [github.com/Noisemaker111/jgengine](https://github.com/Noisemaker111/jgengine) (AGPL-3.0):
499
-
500
- | Package | Role | May import |
501
- |---------|------|------------|
502
- | `@jgengine/core` | Everything below: defineGame, GameContext, scene, combat, game systems, movement, input, world features, runtime/transport contracts | nothing platform-specific — no React, Convex, three.js, browser |
503
- | `@jgengine/react` | `GameProvider`, hooks, headless UI primitives | react + core |
504
- | `@jgengine/shell` | `GamePlayerShell` — R3F canvas, camera rig library (orbit, first-person, top-down/iso, RTS, over-the-shoulder, lock-on, chase, cinematic + shake channel), input tracking, HUD mounting, `GameUiPreview`, demo game; you supply a `GameRegistry` | react + three + core |
505
- | `@jgengine/ws` | Browser-safe `GameBackend` over a pluggable string-pipe transport (WebSocket/socket.io/WebRTC/loopback), protocol codec, HTTP reads, browser-safe authoritative host + router (`host`/`hostRouter`), and P2P (`peer`) | core |
506
- | `@jgengine/node` | Node process bindings over `@jgengine/ws`'s host/router: `ws`-package server, socket.io server attach, file persistence (re-exports `createGameHost`/`memoryPersistence` unchanged) | node + ws + core |
507
- | `@jgengine/sql` | `HostPersistence` on Postgres (structural pool, no hard `pg` dep) | core |
508
- | `@jgengine/convex` | The Convex **adapter** behind the `GameBackend` seam | react + convex + core |
509
-
510
- Import by deep path: `@jgengine/core/<domain>/<file>` (e.g. `@jgengine/core/runtime/gameContext`).
511
-
512
- ## Hit a snag? File an issue
513
-
514
- Any hiccup with JGengine — a doc that's wrong, a missing primitive, a rough edge, or a feature/improvement idea — file it fast at [github.com/Noisemaker111/jgengine/issues](https://github.com/Noisemaker111/jgengine/issues). A 30-second issue (what you were building, the glue it forced, the API you wanted) is worth more than a silent workaround — that's the fastest way gaps and doc errors get closed. Don't reverse-engineer around a broken doc in silence; report it.
515
-
516
- ## Upgrading? Read the changelog
517
-
518
- All eight packages version in lockstep. When you bump (e.g. `0.6` → `0.7`) to pick up new capabilities, read [`CHANGELOG.md`](https://github.com/Noisemaker111/jgengine/blob/main/CHANGELOG.md) — each release leads with a **Migrate** block listing the concrete steps to move a game onto the new APIs. It ships inside every package too (`node_modules/@jgengine/core/CHANGELOG.md`), and as typed values: `import { VERSION, CHANGELOG } from "@jgengine/core/meta/changelog"` to diff your installed version against the latest programmatically.
519
-
520
- ## Concept → Type Reference
521
-
522
- Exact import paths and export names — **do not invent paths**; every row below resolves to a real file under `packages/core/src`. Import the deep path form `@jgengine/core/<path>`.
523
-
524
- | Concept | Import path (`@jgengine/core/…`) | Export(s) |
525
- |---------|----------------------------------|-----------|
526
- | Game boot | `game/defineGame` | `defineGame`, `GameDefinition`, `GameLoop`, `InventoryDeclaration`, `PhysicsConfig`, `GameServerConfig`, `TimeConfig` |
527
- | Simulation clock | `time/simClock` | `createSimClock`, `SimClock`, `TimeConfig`, `ClockSnapshot`, `CalendarTime` |
528
- | Runner contract | `game/playableGame` | `PlayableGame`, `GameCameraConfig`, `CameraRigKind`, `CameraProjection`, `SideScrollCameraConfig`, `TopDownCameraConfig`, `RtsCameraConfig`, `ShoulderCameraConfig`, `LockOnCameraConfig`, `ChaseCameraConfig`, `ObserverCameraConfig`, `CameraShakeConfig`, `CinematicCameraConfig`, `CameraKeyframe`, `EntitySpriteConfig` |
529
- | Runtime ctx | `runtime/gameContext` | `createGameContext`, `GameContext`, `GameContextContent`, `GameContextItemEntry`, `GameContextEntityEntry`, `GameContextObjectEntry`, `CatalogEntityRole` |
530
- | Behaviour lifecycle | `behaviour/behaviour` | `Behaviour` (`onAwake`→`onEnable`→`onStart`→`onUpdate(dt)`→`onDisable`→`onDestroy`), `BehaviourModule`, `createBehaviourWorld`, `BehaviourWorld`, `JGEngineRegister`, `RegisterField`, `BehaviourModules` — Unity-style lifecycle over an id-keyed node tree (`setActive` cascade, lazy update dispatch); key nodes by entity instance ids. Games augment `JGEngineRegister` via `declare module "@jgengine/core/behaviour/behaviour"` for typed `world.modules`. Three.js binding: `Object3DBehaviour`, `attachObject3D`, `useBehaviourWorld` from `@jgengine/shell/behaviour` |
531
- | Reactive keyed store | `store/observableKeyedStore` | `createObservableKeyedStore`, `ObservableKeyedStore` — backs `ctx.game.store` |
532
- | Scene instance role | `scene/entityStore` | `EntityRole`, `SceneEntity`, `SpawnOptions`, `EntityPose` |
533
- | Object spatial queries | `scene/objectQuery` | `raycastObjects`, `raycastObjectsAll`, `ObjectRaycastInput`, `ObjectRaycastHit` — backs `ctx.scene.object.raycast`/`raycastAll` |
534
- | Runtime paint layer | `scene/paintLayer` | `createPaintLayer`, `PaintLayer`, `PaintStroke` — backs `ctx.scene.entity.paint` |
535
- | Possession | `scene/possession` | `createPossession`, `Possession`, `PossessionDeps`, `PossessionSwappedEvent` |
536
- | Form / shapeshift | `scene/form` | `createForms`, `Forms`, `FormDef`, `FormsDeps`, `FormChangedEvent` |
537
- | Multiplayer adapters | `runtime/adapter` | `offline`, `ws`, `convex`, `socketIo`, `p2p`, `lan`, `fly`, `servers`, `MultiplayerTopology`, `ServersPoolConfig` |
538
- | Loot | `game/lootTable` | `lootTable`, `LootTableDef`, `LootEntry`, `Drop` |
539
- | Dropped-item entity | `game/worldItem` | `WORLD_ITEM_ENTITY_NAME`, `WorldItemRecord`, `WorldItemSpawnInput`, `createWorldItemStore`, `resolveDeathDrops`, `scatterOffset`, `scatterPosition`, `selectNearestWorldItem`, `resolveWorldItemPresentation`, `RarityStyle`, `WorldItemPresentation`, `DEFAULT_RARITY`, `DEFAULT_PICKUP_RADIUS`, `DEFAULT_SCATTER` |
540
- | Loot filter | `game/lootFilter` | `lootFilter`, `evaluateLootFilter`, `LootFilterRule`, `LootFilterCondition`, `LootFilterItem`, `LootFilterOverride` |
541
- | Loadout | `game/loadout` | `LoadoutDef`, `LoadoutItemEntry`, `Loadouts` |
542
- | Cosmetic loadout | `game/cosmetics` | `createCosmetics`, `Cosmetics`, `CosmeticLoadoutDef` |
543
- | Quest | `game/quest` | `QuestDef`, `QuestRewards`, `QuestObjective`, `QuestJournal` |
544
- | World features | `world/features` | `WorldFeature`, `biomes`, `voxel`, `plots`, `tilemap`, `flat`, `environment`, `terrain`, `rain`, `snow`, `grass`, `ocean`, `building`, `road` |
545
- | Street layout | `world/streets` | `laneCenters`, `sidewalkPaths`, `furnitureSpots`, `parkingSpots`, `sidewalkPoint`, `offsetPath`, `sidewalkWidthOf`, `StreetLane`, `FurnitureSpot`, `ParkingSpot` — where things belong on a `road()`: lanes for traffic, sidewalks for peds, curb anchors for furniture |
546
- | Road ribbons | `world/roads` | `buildRoadRibbon`, `dashSegments`, `nearestOnPath`, `isOnRoad`, `pathLength`, `RoadRibbon`, `RoadSample`, `RoadPoint` — geometry + queries behind the `road()` environment feature |
547
- | Voxel field | `world/voxelField` | `createVoxelField`, `VoxelField`, `VoxelCell`, `VoxelHit`, `VoxelBounds`, `VoxelFieldSummary`, `VoxelFace`, `VOXEL_FACES`, `VOXEL_FACE_NORMALS` — a chunked block lattice, distinct from the `voxel()` `WorldFeature` descriptor |
548
- | Terrain field | `world/terrain` | `TerrainField`, `noiseField`, `resolveTerrainField`, `rollingField`, `fractalNoise`, `valueNoise`, `withNormal`, `arenaField`, `flatField`, `resolveGroundStep`, `snapToGround`, `snapEntityToGround`, `resolveTerrainPalette`, `TERRAIN_MATERIAL_PALETTES` |
549
- | Seeded RNG | `random/rng` | `seededRng`, `seededStreams` |
550
- | Seed share link | `random/seedLink` | `withSeedParam`, `seedFromUrl`, `seedFromSearch`, `dailySeed`, `DEFAULT_SEED_PARAM` — encode/decode a world seed to/from a shareable URL query param; `dailySeed` is the UTC daily-run seed |
551
- | Name generator | `random/nameGen` | `createNameGenerator`, `pickFrom`, `fillTemplate`, `NameGenerator`, `NameGeneratorOptions`, `SyllableBank` |
552
- | Regions | `world/regions` | `createRegionField`, `isRegionField`, `RegionDef`, `RegionField`, `RegionSample` |
553
- | Wind field | `world/wind` | `windField`, `WindField`, `WindFieldConfig`, `WindVector` |
554
- | Water surface | `world/water` | `waterSurface`, `waterSurfaceFromDescriptor`, `synthesizeWaves`, `WaterSurface`, `GerstnerWave` |
555
- | Scatter | `world/scatter` | `scatter`, `scatterAabb`, `ScatterConfig`, `ScatterPoint` |
556
- | Content scatter | `world/scatterItems` | `scatterItems`, `pickWeighted`, `ScatterLayer`, `ScatterInstance` |
557
- | Building generator | `world/buildings` | `generateBuilding`, `generateBuildingDistrict`, `createBuildingGrid`, `GeneratedBuilding` |
558
- | Building index | `world/buildingIndex` | `buildingIndex`, `BuildingIndex`, `BuildingHit` |
559
- | Scene summary | `world/environmentSummary` | `summarizeEnvironment`, `resolveStructureBuildings`, `EnvironmentSummary` |
560
- | Map markers | `world/markers` | `createMarkerSet`, `MarkerSet`, `MapMarker`, `MarkerInput`, `MarkerKindStyle`, `DEFAULT_MARKER_KINDS`, `markerKindStyle` |
561
- | Fog of war | `world/fog` | `createFogField`, `FogField`, `FogConfig`, `FogBounds`, `FogCells` |
562
- | Minimap math | `world/minimap` | `projectToMinimap`, `clampToMinimapEdge`, `compassBearing`, `headingToBearing`, `bearingToCardinal`, `relativeBearing`, `MinimapView` |
563
- | Ping | `game/ping` | `createPingSystem`, `classifyPing`, `PingSystem`, `PingPayload`, `PingCategory`, `PingCategoryDef`, `DEFAULT_PING_CATEGORIES`, `PING_FEED_ACTION` |
564
- | Proximity prompt | `interaction/proximityPrompt` | `proximityPrompt`, `ProximityPrompt`, `ProximityPromptDisplay`, `keybind`, `gauge`, `label`, `command` |
565
- | Skill-check minigame | `interaction/skillCheck` | `evaluateSkillCheck`, `skillCheckMarkerPosition`, `skillCheckZoneAt`, `SkillCheckConfig`, `SkillCheckZone`, `SkillCheckResult` |
566
- | QTE sequencer | `interaction/qte` | `evaluateQteSequence`, `pendingQteStep`, `qteProgress`, `QteStep`, `QteInputEvent`, `QteOutcome` |
567
- | Item use | `item/use` | `createItemUse`, `ItemUseHandler`, `ItemUseInput`, `ItemUseResult`, `ItemUseRejection` |
568
- | Durability | `item/durability` | `createDurability`, `wear`, `repairQuote`, `isDisabled`, `createDurabilityTracker`, `DurabilitySpec`, `DurabilityState`, `RepairSpec`, `RepairQuote` |
569
- | Affix roller | `item/affix` | `createAffixRoller`, `seededRng`, `AffixRoller`, `RollerConfig`, `AffixPool`, `AffixDef`, `RarityTier`, `ItemBaseDef`, `RolledItem`, `RolledAffix` |
570
- | Modular item | `item/modularItem` | `createModularItem`, `install`, `computeEffectiveStats`, `missingRequiredSlots`, `ModularItemDef`, `MountSlotDef`, `PartDef`, `InstalledPart` |
571
- | Storage tier | `inventory/storageTier` | `partitionOnDeath`, `createDeliveryQueue`, `insureLost`, `resolveConsolation`, `tierOf`, `StorageTier`, `ContainerSnapshot`, `DeathPartition`, `DeliveryQueue`, `InsurancePolicy`, `ConsolationPolicy` |
572
- | Contested channel | `session/contestedChannel` | `createContestedChannel`, `ContestedChannel`, `ContestedChannelConfig`, `ContestedEvent`, `ContestedPhase`, `ContestedSnapshot` |
573
- | Round state | `session/roundState` | `createRoundState`, `lossBonusFor`, `RoundState`, `RoundConfig`, `RoundPhase`, `RoundTeam`, `RoundEvent`, `RoundEconomy`, `RoundSnapshot`, `LossBonusRule` |
574
- | Shrinking ring | `session/ring` | `createRing`, `ringSampleAt`, `Ring`, `RingConfig`, `RingPhase`, `RingSample`, `RingHit`, `RingPoint` |
575
- | Extraction session | `session/extraction` | `createRaidSession`, `RaidSession`, `RaidSessionConfig`, `ExtractPoint`, `ExtractionResult`, `DeathResult`, `RaidStatus` |
576
- | Role assignment | `session/roles` | `assignRoles`, `RoleSpec` |
577
- | Downed / revive | `combat/downed` | `createDownedState`, `DownedState`, `DownedConfig`, `DownedPhase`, `DownedEntry`, `DownedEvent` |
578
- | Persistence scopes | `runtime/persistenceScope` | `partitionScopes`, `resetRun`, `mergeScopes`, `clearRunFields`, `applyRunReset`, `planScenarioReset`, `ScopeSchema`, `ScenarioReset`, `PersistenceScope` |
579
- | Inventory | `inventory/inventoryModel` | `InventoryLayout`, `InventorySet`, `ItemTraits` |
580
- | Progression | `game/progression` | `curve`, `evalCurve`, `leveling`, `Curve`, `LevelingTrack`, `LevelProgress` |
581
- | Talent tree | `game/talents` | `createTalentTree`, `TalentTree`, `TalentTreeConfig`, `TalentNodeDef`, `TalentRequirement`, `TalentAllocateResult`, `ResolvedTalents`, `TalentSnapshot` — point spends gated by prereqs + points-in-branch, resolved once into a cached flat `StatModifierSet` + ability grants |
582
- | Inventory slots | `inventory/slotModel` | `createSlots`, `placeAt`, `removeAt`, `moveSlot`, `firstEmpty`, `compactSlots`, `Slot`, `SlotGrid` |
583
- | Shaped inventory | `inventory/shapedGrid` | `createShapedGrid`, `placeShaped`, `moveShaped`, `removeShaped`, `canPlace`, `rotateFootprint`, `occupiedCells`, `gridAdjacencyQuery`, `cellFromPoint`, `ShapedGrid`, `Footprint`, `Placement`, `Rotation` |
584
- | Card piles | `cards/cardPile` | `createCardPile`, `createCardPileState`, `draw`, `moveCards`, `shuffleZone`, `pileRng`, `CardPile`, `CardPileState`, `CardPileConfig` |
585
- | Modifier pipeline | `cards/modifierPipeline` | `createModifierPipeline`, `runPipeline`, `Modifier`, `TraceStep`, `PipelineResult` |
586
- | Lane board | `board/laneBoard` | `createLaneBoard`, `laneAggregate`, `laneOutcome`, `boardTotals`, `lanesWon`, `LaneBoard`, `LaneRule`, `LaneBoardConfig` |
587
- | Timeline board | `board/timelineBoard` | `createTimelineBoard`, `tickTimeline`, `TimelineBoard`, `TimelineSlot`, `TimelineFire` |
588
- | World geometry | `world/geometry` | `footprintAabb`, `aabbOverlap`, `snapToGrid`, `resolveMove`, `Aabb`, `Footprint` |
589
- | Placement | `world/placement` | `validatePlacement`, `footprintObstacle`, `PlacementRules`, `PlacementResult` |
590
- | Placement ghost | `world/placementController` | `createPlacementController`, `PlacementController`, `PlacementPreview`, `PlacementCommit`, `SnapMode`, `quarterTurnsToRotationY` |
591
- | Connector sockets | `world/connectors` | `snapToNearest`, `socketsCompatible`, `worldSockets`, `socketWorldPosition`, `ConnectorSocket`, `ConnectorPieceDef`, `PlacedPiece`, `SnapResult` |
592
- | Structural support | `world/support` | `solveSupport`, `toDebrisBodies`, `SupportPiece`, `SupportLink`, `SupportResult` |
593
- | Wall/roof authoring | `world/walls` | `createWallDrawTool`, `footprintFromWalls`, `autoRoof`, `wallSegments`, `createSurfacePaint`, `WallDrawTool`, `RoofPlan`, `EnclosedFootprint` |
594
- | Placed structures | `world/placedStructureStore` | `createPlacedStructureStore`, `PlacedStructure`, `PlacedStructureStore`, `PlacedStructureSnapshot` |
595
- | Terraform | `world/terraform` | `createEditableTerrain`, `createTerraformBrush`, `brushWeight`, `EditableTerrain`, `TerraformBrush`, `TerraformEdit`, `TerraformMode` |
596
- | Build permissions | `world/buildPermissions` | `createPlotPermissions`, `createContributionPool`, `PlotPermissions`, `ContributionPool`, `BuildRole`, `ContributionGoal` |
597
- | Interiors | `world/interiors` | `createInteriors`, `Interior`, `Exterior`, `SpaceRef` |
598
- | Game clock | `time/gameClock` | `getScaledElapsedMs`, `computeGameDay`, `SECONDS_PER_GAME_DAY` |
599
- | Idle / offline catch-up | `time/idleProgress` | `idleWindow`, `linearCatchUp`, `exponentialCatchUp`, `steppedCatchUp`, `IdleWindow`, `IdleWindowConfig`, `SteppedCatchUpResult` — elapsed-real-time production/growth/decay for a game reopened after being closed |
600
- | Scene behaviors | `scene/behaviors` | `wander`, `patrol`, `promptable`, `talkable`, `player` |
601
- | Capture check | `scene/captureCheck` | `captureChance`, `rollCapture`, `CaptureCheckInput` |
602
- | Owned roster | `scene/roster` | `createRoster`, `Roster`, `RosterEntry`, `RosterCaptureOptions` |
603
- | Economy wallet | `economy/wallet` | `createEmptyWallet`, `balance`, `grant`, `charge`, `canAfford`, `chargeAll` |
604
- | Tech tree | `economy/techTree` | `createTechTree`, `TechTree`, `TechNodeDef`, `canUnlockTech`, `availableTech`, `unlockedRecipes`, `grantTech`, `techPrerequisitesMet` |
605
- | Recipe graph | `crafting/recipe` | `createRecipeGraph`, `RecipeGraph`, `RecipeDef`, `RecipeItem`, `canCraft`, `craft`, `missingInputs`, `stationSatisfied`, `craftSeconds` |
606
- | Production building | `crafting/production` | `productionBuilding`, `ProductionBuildingDef`, `createProductionState`, `tickProduction`, `feedProduction`, `drainOutput`, `advanceTransport`, `resolvePowerGrid` |
607
- | Crop tile / farming | `crafting/crop` | `createCropField`, `CropField`, `CropDef`, `CropTileState`, `tillTile`, `plantCrop`, `waterTile`, `advanceCropDay`, `harvestCrop`, `applyToolToTiles`, `squarePattern`, `diamondPattern`, `createDayTicker` |
608
- | Skill-check roll | `stats/rollCheck` | `rollCheck`, `CheckInput`, `CheckResult`, `CheckAdvantage` |
609
- | Input bindings (full) | `input/actionBindings` | `hotbarSlotBindings`, `actionLabel`, `bindingLabel`, `resolveActionCommand`, `bindingMatches`, `createActionStateTracker` |
610
- | Touch controls | `input/touchScheme` | `deriveTouchScheme`, `touchCode`, `touchActionLabel`, `withTouchCodes`, `TouchControlsConfig`, `TouchGestureBindings`, `TouchDragBinding`, `TouchButtonSpec`, `TouchScheme`, `TouchJoystick`, `TouchButton` |
611
- | Pointer hit | `input/pointer` | `PointerHit`, `PointerButton`, `aimToPoint`, `moveTargetFromHit`, `groundOf`, `PointerVec3` |
612
- | Navmesh + A* | `nav/navGrid` | `createNavGrid`, `findPath`, `smoothPath`, `NavGrid`, `NavGridConfig`, `NavPoint`, `FindPathOptions` |
613
- | Path follow | `nav/pathFollow` | `createPathFollow`, `advancePathFollow`, `pathFromNav`, `PathFollowConfig`, `PathFollowState`, `Waypoint` |
614
- | Nav-grid movement constraint | `nav/navConstrain` | `constrainToNavGrid`, `NavConstrainProposed`, `NavConstrainEntity`, `NavConstrainOptions` — a standalone walkable-pass-through + wall-slide helper; adapt its `(proposed, entity)` shape to `PlayerMovementConfig.beforeCommit`'s `(frame) => [x,y,z]` with a small closure |
615
- | Selection set | `scene/selection` | `createSelectionSet`, `SelectionSet`, `screenRect`, `selectWithinRect`, `rectContainsPoint`, `isMarquee`, `ScreenRect` |
616
- | Context menu | `interaction/contextMenu` | `contextVerb`, `buildContextMenu`, `contextVerbInput`, `ContextVerb`, `ContextMenu` |
617
- | Shared / group wallet | `economy/sharedWallet` | `createWalletBook`, `WalletBook`, `WalletScope`, `userScope`, `groupScope`, `balanceIn`, `grantTo`, `chargeFrom`, `contributionOf`, `contributorsOf` |
618
- | Analog axis input | `input/axisInput` | `AxisInput`, `AxisChannel`, `AxisBindingMap`, `DRIVE_AXIS_BINDINGS`, `clampAxis`, `rampToward`, `NEUTRAL_AXIS` |
619
- | Raw control polling | `runtime/inputSnapshot` | `createInputSnapshot`, `InputSnapshot` — backs `ctx.input` |
620
- | Physics world | `physics/physicsWorld` | `PhysicsWorld`, `PhysicsWorldConfig`, `PhysicsBounds`, `PhysicsStats`, `AddBodyOptions` (`{ shape: "box", halfExtents }` \| `{ shape: "sphere", radius }`), `JointOptions`, `JointKind`, `CollisionEvent` |
621
- | Ballistic collision sweep | `physics/ballisticSweep` | `createBallisticSweep`, `BallisticSweep`, `BallisticSweepHit`, `BallisticSweepOptions` |
622
- | Tweening / easing | `anim/easing` | `Easing`, `lerp`, `clamp01`, `smoothstep`, `easeInQuad`, `easeOutQuad`, `easeInOutQuad`, `easeInCubic`, `easeOutCubic`, `easeInOutCubic`, `easeOutBack`, `easeOutElastic`, `tween`, `timedProgress` |
623
- | Async data source | `data/dataSource` | `createDataSource`, `DataSource`, `DataSourceState`, `DataSourceStatus`, `DataSourceOptions`, `DataSourceClock`, `RefreshOptions` |
624
- | JSON fetch | `data/fetchJson` | `fetchJson`, `FetchJsonOptions`, `FetchImpl`, `HttpStatusError`, `JsonParseError` |
625
- | JSON data source | `data/jsonDataSource` | `createJsonDataSource`, `JsonDataSourceOptions` |
626
- | Dev proxy routing | `data/devProxy` | `proxiedUrl`, `parseDevProxyTable`, `DevProxyTable`, `ProxiedUrlOptions`, `DEFAULT_DEV_PROXY_PREFIX` |
627
- | Grid-cell world rendering | `world/gridInstances` | `resolveGridInstances`, `GridInstanceTransform` |
628
- | Swarm LOD scheduler | `world/lod` | `createLodScheduler`, `LodScheduler`, `LodSchedulerConfig`, `LodBand` — distance→band index for render detail, `step(id, distance, dt)` throttles per-entity updates by band interval (staggered, accumulates skipped time); pairs with `@jgengine/shell/world/SpriteBatch` for 1000+ entity swarms |
629
- | Turn loop | `turn/turnLoop` | `createTurnLoop`, `TurnLoop`, `TurnLoopConfig`, `TurnState`, `PoolConfig`, `PoolState`, `TurnLoopSnapshot` |
630
- | Declared-action intent board | `turn/intent` | `createIntentBoard`, `IntentBoard`, `DeclaredIntent` — `declare(participantId, intent)`, `peek`, `all`, `consume`, `clear` |
631
- | Commit modes | `turn/commit` | `createCommitController`, `CommitController`, `CommitMode`, `CommitOutcome`, `SubmittedAction` |
632
- | Intent board | `turn/intent` | `createIntentBoard`, `IntentBoard`, `DeclaredIntent` |
633
- | Tactical grid | `tactics/tacticalGrid` | `createTacticalGrid`, `TacticalGrid`, `TacticalGridConfig`, `Tile`, `ReachableTile`, `PushResult`, `PushCollision` |
634
- | Predictive query | `tactics/predictiveQuery` | `predictAreaEffect`, `predictArcEffect`, `predictTiles`, `PredictiveDeps`, `PredictedTarget` |
635
- | Sim snapshot | `tactics/snapshot` | `createSnapshotStore`, `SnapshotStore`, `SnapshotSlice`, `Snapshot`, `deepClone` |
636
- | Surfaces | `tactics/surface` | `createSurfaceLayer`, `SurfaceLayer`, `SurfaceLayerConfig`, `SurfaceKindDef`, `SurfaceReaction`, `SurfaceEvent` |
637
- | Area targeting | `combat/effects` | `resolveAreaTargets`, `AreaTarget`, `AreaTargetInput` (shared AoE targeting behind `effect` + the predictive query) |
638
- | Environment field | `world/envField` | `createEnvironmentField`, `EnvironmentField`, `EnvironmentSample`, `EnvironmentFieldConfig`, `OccluderRect`, `HeatSource` |
639
- | Weather + fire | `world/weather` | `resolveWeather`, `WeatherState`, `WeatherModifier`, `WeatherModifierTable`, `ResolvedWeather`, `createFireGrid`, `FireGrid`, `FireCell`, `FireGridConfig` |
640
- | Realm composition | `world/realm` | `composeRealm`, `RealmCard`, `RealmBase`, `ComposedRealm`, `RealmEnvironmentParams`, `SpawnTableOverride` |
641
- | Decay meters | `survival/decayMeter` | `createDecayMeterSet`, `DecayMeterSet`, `DecayMeterConfig`, `MeterThreshold`, `DecayMeterState` |
642
- | Status moodles | `survival/moodle` | `createMoodleStack`, `stackMoodles`, `MoodleStack`, `Moodle`, `MoodleSeverity`, `TimedMoodleInput` |
643
- | Multi-region health | `survival/regionHealth` | `createMultiRegionHealth`, `MultiRegionHealth`, `HealthRegionConfig`, `AilmentConfig`, `RegionHealthState`, `AilmentInstance` |
644
- | Audio contract | `audio/audioFalloff` | `computeFalloffGain`, `resolveEmitterGain`, `distance3`, `AudioFalloffConfig`, `FalloffCurve`, `SoundDef`, `AudioBusDef`, `AudioBusId` |
645
- | Beat clock | `time/beatClock` | `createBeatClock`, `createBeatInputBuffer`, `nextBeatTime`, `BeatClock`, `BeatClockConfig`, `BeatSnapshot`, `BeatInputBuffer`, `BufferedAction` |
646
- | Spawn director | `ai/spawnDirector` | `createSpawnDirectorState`, `advanceSpawnDirector`, `advanceWave`, `raiseAlert`, `pickSpawnPoint`, `SpawnDirectorConfig`, `WaveManifest`, `SpawnEntry`, `SpawnRequest`, `DirectorContext` |
647
- | Threat table | `ai/threat` | `createThreatTable`, `ThreatTable`, `ThreatTableConfig`, `ThreatEntry`, `HighestThreatOptions` |
648
- | Group-assist aggro | `ai/groupAssist` | `createAssistNetwork`, `AssistNetwork`, `AssistNetworkConfig`, `AssistMember` — propagates one member's threat gains to same-group members (optional radius + `distanceBetween` gating) so a single pull rallies the group |
649
- | Job board | `ai/jobBoard` | `createJobBoard`, `JobBoard`, `JobDef`, `Job`, `JobPhase`, `WorkerState`, `JobReport`, `JobTickContext` |
650
- | Crowd flow | `ai/crowd` | `computeFlowField`, `createCrowdField`, `selectPoi`, `FlowField`, `FlowFieldOptions`, `CrowdField`, `Poi`, `SelectPoiOptions` |
651
- | Factions & reputation | `faction/factions`, `faction/reputation` | `createFactionGraph`, `createFactionRoster`, `FactionRelation`, `FactionDef`, `FactionGraph`, `FactionRoster`, `createReputationLedger`, `DEFAULT_REPUTATION_TIERS`, `tierForStanding`, `effectiveRelation`, `ReputationTier`, `ReputationLedger` |
652
- | Physics actors | `physics/ragdoll`, `physics/carryable`, `physics/forceVolume`, `physics/spatialGrid` | `createRagdoll`, `Ragdoll`, `Carryable`, `carrySpeedMultiplier`, `ForceVolume`, `PlatformCarry`, `SpatialGrid` |
653
- | Traversal (grapple/glide) | `physics/traversal` | `Grapple`, `GrappleConfig`, `Glide`, `GlideConfig` |
654
- | Structural destruction | `physics/structure` | `StructureGraph`, `StructureNodeSpec`, `StructureEdgeSpec`, `StructureMaterial`, `StructureMaterialTable`, `CollapseEvent`, `DebrisConfig` |
655
- | Destructible terrain | `world/carve` | `VoxelVolume`, `VoxelMaterial`, `VoxelMaterialTable`, `CarvableField`, `carvableTerrain`, `CarveOp`, `DepositOp`, `CraterOp`, `MoundOp`, `EMPTY_VOXEL` |
656
- | Vehicle body | `physics/vehicleBody` | `createVehicleBody`, `VehicleBody`, `VehicleBodyConfig`, `WheelSpec`, `GripCurve`, `sampleGripCurve`, `DEFAULT_GRIP_CURVE` |
657
- | Buoyant boat | `physics/buoyancy` | `createBuoyantBody`, `BuoyantBody`, `BuoyantBodyConfig` |
658
- | Crash damage | `physics/damageZones` | `createDamageModel`, `DamageModel`, `DamageZoneDef`, `DamageTransition` |
659
- | Mounts / rideables | `scene/mount` | `createMountController`, `MountController`, `MountKit`, `MountSeat`, `RideableConfig` |
660
- | Shared-vehicle stations | `scene/stationClaim` | `createStationClaim`, `StationClaim`, `Station`, `SharedVehicleConfig`, `ClaimResult` |
661
- | Lag compensation | `multiplayer/lagCompensation` | `createPositionHistory`, `PositionHistory`, `rewindTimestamp`, `resolveHitscan`, `raySphereDistance`, `HitscanRay`, `HitscanTarget` |
662
- | Simultaneous commit | `multiplayer/simultaneousCommit` | `createCommitRound`, `CommitRound`, `SealedCommit`, `resolveCommits` |
663
- | Combat-snapshot replay | `multiplayer/combatSnapshot` | `serializeBoard`, `cloneSnapshot`, `replayCombat`, `BoardSnapshot`, `SnapshotUnit`, `CombatRules`, `ReplayResult` |
664
- | Session matchmaking | `multiplayer/matchmaking` | `browseSessions`, `findByJoinCode`, `quickMatch`, `matchesFilter`, `normalizeJoinCode`, `generateJoinCode`, `SessionListing`, `MatchFilter` |
665
- | Auth identity | `multiplayer/identity` | `AuthSession`, `PlayerIdentity`, `sessionPlayer`, `resolveGuestSession` |
666
- | Text chat | `game/chat`, `multiplayer/chatContract` | `createChat`, `Chat`, `ChatMessage`, `ChatChannelDef`, `whisperChannelId`, `createChatRateLimiter`, `ChatTransport`, `ChatSync`, `createLocalChatTransport` |
667
- | Chat filter | `game/chatFilter` | `createChatFilter`, `normalizeChatText`, `ChatFilter`, `ChatFilterConfig`, `ChatFilterResult` — mask/reject blocked words (leet-normalized token match); wire via `ChatDeps.filter` (word lists are game data, the engine ships the mechanism) |
668
- | Voice seam | `multiplayer/voiceContract` | `VoiceTransport`, `VoiceParticipant`, `VoiceRoute`, `createLocalVoiceTransport`, `createPushToTalk`, `PushToTalkMode` |
669
- | Race state | `game/race` | `raceTrack`, `RaceTrack`, `createRaceState`, `RaceState`, `RaceEvent`, `RaceWinCondition`, `firstPastPost`, `topK`, `lastStanding`, `everyoneFinishes` |
670
- | Reveal query | `sensor/revealQuery` | `createRevealQuery`, `RevealQuery`, `RevealQueryOptions`, `RevealHit` |
671
- | Hidden-state probe | `sensor/hiddenStateProbe` | `probeHiddenState`, `probeHiddenStateAll`, `HiddenStateSource`, `HiddenStateValue`, `SensorProbeOptions`, `SensorReading` |
672
- | View-frustum sensor | `sensor/frustumSensor` | `createFrustumSensor`, `projectToView`, `framingScore`, `FrustumCamera`, `FrustumTarget`, `FrustumProjection`, `FrustumSample`, `FrustumSensor`, `FramingConfig` |
673
- | Recording buffer | `sensor/recordingBuffer` | `createRecordingBuffer`, `RecordingBuffer`, `RecordingFrame`, `RecordingBufferOptions` |
674
- | Concealment scoring | `sensor/concealment` | `colorDistance`, `concealmentScore`, `createConcealmentSensor`, `ColorHex`, `ConcealmentTarget`, `ConcealmentSample`, `ConcealmentSensor` |
675
- | Freeze violation monitor | `sensor/freezeMonitor` | `createFreezeMonitor`, `FreezeMonitor`, `FreezeSubject`, `FreezeViolation` |
676
- | Animation SM | `combat/animationState` | `createAnimationState`, `AnimationState`, `AnimationClip`, `FramePhase`, `FrameRange`, `phasesAtFrame`, `activeRangeAtFrame`, `frameAtMs` |
677
- | Accumulator meter | `stats/accumulatorMeter` | `createAccumulatorMeter`, `AccumulatorMeter`, `AccumulatorMeterConfig`, `MeterTier`, `MeterAddResult`, `tierAt` |
678
- | Stagger / buildup | `combat/breakMeters` | `createStaggerMeter`, `createBuildupMeter`, `StaggerMeter`, `BuildupMeter`, `BuildupProc` |
679
- | Attack tags | `combat/attackTags` | `attackMeta`, `AttackTag`, `AttackMeta`, `hasTag`, `isBlockable`, `isParryable`, `isDodgeable`, `counters` |
680
- | Defensive window | `combat/defensiveWindow` | `createDefensiveWindow`, `resolveDefense`, `DefensiveWindowConfig`, `DefenseKind`, `DefenseOutcome`, `windowActiveAt`, `iframeActiveAt` |
681
- | Combo string | `combat/comboString` | `createComboRunner`, `advanceCombo`, `ComboString`, `ComboStep`, `AdvanceComboResult` |
682
- | Hit reaction | `combat/hitReaction` | `resolveHitReaction`, `HitReaction`, `HitReactionConfig`, `CameraShake`, `applyImpulse` |
683
- | Telegraph | `combat/telegraph` | `pointInTelegraph`, `telegraphProgress`, `telegraphFired`, `telegraphTurnProgress`, `telegraphFiredAtTurn`, `telegraphTurnsRemaining`, `TelegraphShape`, `TelegraphConfig` |
684
- | Dash / dodge | `movement/dash` | `createDashState`, `DashState`, `DashConfig`, `DashBurst`, `iframeActive`, `dashOffset` |
685
- | Ability kit | `combat/abilityKit` | `createAbilityKit`, `AbilityKit`, `AbilitySlotConfig`, `AbilitySlotSnapshot`, `AbilitySlotState`, `AbilityCastType`, `AbilityCastResult`, `AbilitySlotRetune` |
686
- | Resource pool | `combat/resourcePool` | `createResourcePool`, `ResourcePool`, `ResourcePoolConfig` — current/max with per-second regen/decay and spend/gain; `pool.current()` is the ability kit's `resourceAvailable` |
687
- | Combo points | `combat/comboPoints` | `createComboPoints`, `ComboPoints`, `ComboPointsConfig` — discrete points accrued on action, expiring after a timeout from the last gain, spent in bulk |
688
- | Event meter | `stats/eventMeter` | `createEventMeter`, `EventMeter`, `EventMeterConfig`, `EventMeterFeedResult` |
689
- | Auto-target policy | `scene/autoTarget` | `selectAutoTarget`, `createAutoTargeter`, `AutoTargetPolicy`, `AutoTargeter`, `AutoTargetDeps` |
690
- | Resistance matrix | `combat/resistance` | `resolveResistance`, `resistanceScale`, `ResistanceMatrix`, `ResistVerdict`, `ResistanceResult` |
691
- | Run draft | `game/runDraft` | `createRunDraft`, `createRunModifierStack`, `RunDraft`, `RunModifierStack`, `RunModifierOffer` |
692
- | Uniform-cell grid | `puzzle/cellGrid` | `CellGrid`, `CellRun`, `createCellGrid`, `cellAt`, `inGridBounds`, `withCell`, `withCells`, `fullRows`, `clearRows`, `collapseColumns`, `findRuns` |
693
- | Falling piece | `puzzle/fallingPiece` | `FallingPiece`, `ShapeTable`, `LockDelayState`, `pieceCells`, `pieceCollides`, `mergePiece`, `dropDistance`, `gravityInterval`, `levelForLines`, `lineScore`, `createLockDelay`, `stepLockDelay` |
694
- | Falling tile grid | `tactics/fallingGrid` | `createFallingGrid`, `FallingGrid`, `FallingGridConfig`, `FallingGridCell`, `FallingGridSnapshot`, `LockState`, `gravityIntervalMs`, `GravityIntervalConfig` — a generic tile-drop grid (any `TCell` payload), distinct from `puzzle/cellGrid`+`puzzle/fallingPiece`'s row-clear/shape-table pair |
695
- | Spawn/respawn points | `game/spawnPoints` | `createSpawnPoints`, `SpawnPoints`, `SpawnPointPose`, `RespawnTarget` |
696
- | Level sequence | `game/levelSequence` | `createLevelSequence`, `LevelSequence`, `LevelSequenceConfig`, `LevelDescriptor`, `CurrentLevel`, `LevelSequenceStatus`, `LevelSequenceProgress` |
697
- | Devtools overlay + tunables | `devtools/devtools` | `devtools`, `createDevtools`, `tunable`, `snapshotDevtools`, `instrumentLatency`, `Tunable`, `TunableOptions`, `TunableAccessor`, `DevtoolsControl`, `DiscoveredEntry`, `DevtoolsOverrides`, `DevtoolsSnapshot` |
698
- | Tunable auto-discovery | `devtools/transformTunables` | `transformTunableExports`, `tunableModuleTable`, `tunableDiscoveryPlugin`, `TunableTransformResult` |
699
-
700
- ## Getting started (new project)
701
-
702
- Fastest path — the `jgengine` CLI scaffolds the entire canonical shape below (harness, skeleton, stub game, verify test, AGENTS.md) as a booting game:
703
-
704
- ```sh
705
- npx jgengine create my-game # then: cd my-game && bun dev
706
- npx jgengine doctor # later, if the setup drifts (version skew, unstyled HUD, shape strays)
707
- ```
708
-
709
- Manual equivalent:
710
-
711
- ```sh
712
- bun add @jgengine/core @jgengine/react @jgengine/shell react react-dom three three-stdlib @react-three/fiber @react-three/drei
713
- bun add -d @tailwindcss/vite tailwindcss # HUD styling (Vite + Tailwind v4)
714
- ```
715
-
716
- A single game's standalone entry mounts `GameHost` (`@jgengine/shell/GameHost`) over the `game` your `game.config.ts` exports. The full standalone harness is four small files plus a script — this is exactly the shape every `Games/*` game ships, so `bun dev` plays it on its own with no host app:
717
-
718
- ```html
719
- <!-- index.html -->
720
- <!doctype html>
721
- <html lang="en">
722
- <head>
723
- <meta charset="UTF-8" />
724
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
725
- <title>My Game</title>
726
- </head>
727
- <body>
728
- <div id="root"></div>
729
- <script type="module" src="/src/main.tsx"></script>
730
- </body>
731
- </html>
732
- ```
733
-
734
- ```ts
735
- // vite.config.ts — monorepo-aware: the alias branch only fires when this folder
736
- // sits inside the engine repo checkout; copied anywhere else, @jgengine/* resolves
737
- // from npm dist and the alias list is empty
738
- import { existsSync } from "node:fs";
739
- import { fileURLToPath } from "node:url";
740
- import tailwindcss from "@tailwindcss/vite";
741
- import react from "@vitejs/plugin-react";
742
- import { defineConfig } from "vite";
743
-
744
- const engineSrc = (pkg: string) => fileURLToPath(new URL(`../../packages/${pkg}/src`, import.meta.url));
745
-
746
- export default defineConfig({
747
- plugins: [react(), tailwindcss()],
748
- resolve: {
749
- alias: existsSync(engineSrc("core"))
750
- ? [
751
- { find: /^@jgengine\/core\/(.*)$/, replacement: `${engineSrc("core")}/$1` },
752
- { find: /^@jgengine\/react\/(.*)$/, replacement: `${engineSrc("react")}/$1` },
753
- { find: /^@jgengine\/ws\/(.*)$/, replacement: `${engineSrc("ws")}/$1` },
754
- { find: /^@jgengine\/shell\/(.*)$/, replacement: `${engineSrc("shell")}/$1` },
755
- { find: /^@jgengine\/assets$/, replacement: `${engineSrc("assets")}/index.ts` },
756
- { find: /^@jgengine\/assets\/(.*)$/, replacement: `${engineSrc("assets")}/$1` },
757
- ]
758
- : [],
759
- },
760
- });
761
- ```
762
-
763
- ```css
764
- /* src/index.css */
765
- @import "tailwindcss";
766
- @source "../node_modules/@jgengine/react/dist";
767
- @source "../node_modules/@jgengine/shell/dist";
768
- ```
769
-
770
- Inside the engine repo the two `@source` lines point at `../../../packages/react/src` and `../../../packages/shell/src` instead (see any `Games/*/src/index.css`) — same file, different `@source` targets depending on where dist lives.
771
-
772
- ```tsx
773
- // main.tsx
774
- import "./index.css";
775
-
776
- import { createRoot } from "react-dom/client";
777
- import { GameHost } from "@jgengine/shell/GameHost";
778
- import { game } from "./game.config";
779
-
780
- const root = document.getElementById("root");
781
- if (root === null) throw new Error("main: missing #root mount element");
782
- createRoot(root).render(<GameHost playable={game} />);
783
- ```
784
-
785
- Add `"dev": "vite"` to `package.json`'s `scripts` — `bun dev` then launches the game standalone.
786
-
787
- `GameHost` resolves multiplayer itself from `game.multiplayer` (falling back to offline when the adapter can't resolve, with a console warning) — pass `multiplayer` (a prebuilt `ShellMultiplayer | null`, used as-is with no resolution attempted) or `resolveMultiplayer` (`(args) => ShellMultiplayer | null`, tried before the built-in resolver, falling back to it on `null`) only when the host app needs to supply its own session, e.g. trying several transports in sequence.
788
-
789
- A multi-game host (a launcher, a dev registry) wires `GamePlayer` over a `GameRegistry`:
790
-
791
- ```tsx
792
- import { GamePlayer } from "@jgengine/shell/GamePlayer";
793
- import type { GameRegistry } from "@jgengine/shell/registry";
794
-
795
- const games: GameRegistry = {
796
- "my-game": () => import("./my-game").then((m) => m.game),
797
- };
798
-
799
- function App() {
800
- return <GamePlayer gameId="my-game" registry={games} loading={<p>Loading…</p>} />;
801
- }
802
- ```
803
-
804
- `GamePlayer({ gameId, registry, fallbackGameId?, loading?, multiplayer? })` (`@jgengine/shell/GamePlayer`) is `GamePlayerShell` plus the lazy-load glue: it looks up `gameId` in `registry`, awaits the loader, renders `loading` (default `null`) until it resolves, then mounts `GamePlayerShell playable={...} multiplayer={...}`; switching `gameId` re-triggers the load, and an in-flight load is discarded if the id changes again first. `resolveGameLoader(registry, gameId, fallbackGameId?)` (`@jgengine/shell/registry`) is the underlying lookup — `registry[gameId] ?? registry[fallbackGameId]` — for hosts that want the fallback behavior without the component.
805
-
806
- HUD styling is Tailwind v4 via the `index.css` above — without its `@source` lines the HUD renders unstyled. Then build the game itself under `src/` per the layout below — `src/game.config.ts` is the single entry, defined with `defineGame` from `@jgengine/shell/defineGame`.
807
-
808
- ## Scope
809
-
810
- This file documents engine primitives and conventions only — never game domain. Example ids (`iron_block`, `mob_grunt`, `shop_town`) are placeholders, not content to copy.
811
-
812
- | Engine owns | Your game owns |
813
- |-------------|----------------|
814
- | Weighted loot RNG, trade validation, loadout application, quest journal state, social graph, stat clamp math, effect absorption, projectile geometry, death resolution, event bus, feeds, leaderboards, input capture, pose hitboxes | Catalog entries and ids, effect id names, XP curves, shop/item/quest/loadout definitions, use-handlers, AI logic, UI content |
815
-
816
- **Rules:**
817
-
818
- 1. **Catalog-first** — shape and behavior of every id lives in game-owned catalog files. Runtime calls pass ids, positions, instance keys.
819
- 2. **Three buckets** — inventory items, scene objects, scene entities. Never merge them.
820
- 3. **Dumb place/spawn** — no behaviors on `place()`/`spawn()`; the catalog owns them.
821
- 4. **Commands for verbs** — input maps to actions, actions to commands/handlers; no raw keys in game logic.
822
- 5. **Primitives over glue** — a loop several games need (loot roll, shop buy, kit seeding) belongs in the engine, not copy-pasted per game.
823
- 6. **No speculative config** — `defineGame` fields exist only with a live engine consumer.
824
- 7. **This file stays domain-free.**
825
-
826
- ## The three buckets
827
-
828
- | Bucket | What | API |
829
- |--------|------|-----|
830
- | **Inventory** | Stackable ids in containers | `ctx.player.inventory.put / take / move / has / count` |
831
- | **Scene object** | Static world content | `ctx.scene.object.place / remove / move / rotate / list` |
832
- | **Scene entity** | Movers driven per tick | `ctx.scene.entity.spawn / despawn / setPose / effect / …` |
833
-
834
- A voxel block is an object. A rack is an object with a slot inventory. A GPU is an inventory item inside it. A player, mob, or car is an entity. A dropped-item lying on the ground is also an entity — `ctx.scene.worldItem` (position + item ref + rarity, spawned under `game/worldItem`'s `WORLD_ITEM_ENTITY_NAME`) — never a fourth bucket and never merged into inventory or object.
835
-
836
- ## Game repo layout
837
-
838
- Every game is one shape, enforced by `check-game-shape` (part of `check-types`): the top of `src/` holds only the skeleton, and every game-specific module, UI component, and test lives under `src/game/`. In this repo the gate also requires a root `package.json` script `"games:<id>": "bun run --cwd Games/<id> dev"` for every `Games/*` directory — add it with the scaffold, or the very first `check-types` fails before the compiler even runs. Dense files — one `catalog.ts` per domain, never one file per entry.
839
-
840
- ```
841
- src/
842
- game.config.ts single entry — export const game = defineGame({...}) from "@jgengine/shell/defineGame"
843
- index.tsx barrel — export { game } from "./game.config" (+ any UI-preview scenario re-export)
844
- main.tsx standalone host — mounts <GameHost playable={game}/> from "@jgengine/shell/GameHost"
845
- loop.ts onInit, onNewPlayer, onTick
846
- world.ts WorldFeature + PhysicsConfig (only for games that have one)
847
- game/
848
- keybinds.ts ActionCodesMap — named actions + hotbarSlotBindings(n)
849
- inventories.ts inventory declarations
850
- assets.ts Render catalog
851
- content.ts itemById / entityById lookups over all catalogs
852
- loadouts.ts Loadout ids → items/economy/unlocks per inventory
853
- world/ zones.ts, setup.ts (place/spawn from onInit)
854
- items/ <domain>/catalog.ts + use-handlers.ts
855
- objects/ catalog.ts (+ loot tables beside their domain)
856
- entities/ players/ enemies/ npcs/ — catalog.ts per role (never actors/)
857
- quests/catalog.ts when using game.quest
858
- progression/ curves.ts — game-owned XP curve numbers fed to game/progression
859
- ui/GameUI.tsx ALL layout/positioning
860
- ui/components/ content-only pieces GameUI places
861
- ```
862
-
863
- ## `defineGame` — the single authoring entry
864
-
865
- `@jgengine/shell/defineGame` is the game-authoring entry: one call in `game.config.ts` takes both engine fields (`name`, `assets`, `world`, `physics`, `inventories`, `input`, `server`, `save`, `time`, `feed`, `multiplayer`) and presentation fields (`content`, `loop`, `GameUI`, `camera`, `environment`, `WorldOverlay`, `renderEntity`, `renderObject`, `entitySprites`, `entityModels`, `objectModels`, `hotbarSelection`, `prompts`, `pointer`, `touch`, `worldHealthBars`, `audio`, `entitySounds`, `objectSounds`, `worldItem`, `shadows`, `collision`, `movement`, `devtools`) and returns a ready `PlayableGame` — no separate object to assemble. It is a thin wrapper over the core `defineGame` primitive (below) plus the `PlayableGame` runner assembly; see `packages/shell/src/defineGame.tsx` for the exact accepted fields. Never game tuning (walk speeds, damage, prompts — those live in catalogs).
866
-
867
- **Smart defaults** — omit any of these and the call still resolves: `multiplayer` → `offline()`; `assets` → an empty asset catalog; `loop` hooks (`onInit`/`onNewPlayer`/`onTick`) → no-ops; `content` → `{}`; `GameUI` → an empty component; `camera` → third-person orbit; `feed` → 20-entry ring buffers per action; a `world` of kind `environment()` auto-renders as the backdrop with no `environment` component supplied — a non-`environment()` world (`flat()`, `voxel()`, …) still needs the game to hand it one.
868
-
869
- **Opt-in `ctx.game.*` subsystems (`features`)** — core is genre-agnostic: the always-on base is `commands` / `events` / `store` / `feed` (plus `audio`), and genre subsystems are opt-in via `defineGame({ features: { roster, cards, turn, race, leaderboard, social, chat } })`. Omit one and `ctx.game.<name>` is `undefined` — a puzzle game isn't handed a card pile, race state, or party/chat it never asked for. Declare only what the game uses (`chat` implies `social`). (The content cluster — economy/quest/loot/trade — joins this manifest in a later slim-core phase.)
870
-
871
- ```ts
872
- // game.config.ts — imports only, nothing inline
873
- import { defineGame } from "@jgengine/shell/defineGame";
874
- import { assets } from "./game/assets";
875
- import { content } from "./game/content";
876
- import { GameUI } from "./game/ui/GameUI";
877
- import { inventories } from "./game/inventories";
878
- import { keybinds } from "./game/keybinds";
879
- import { loop } from "./loop";
880
- import { physics, world } from "./world";
881
-
882
- export const game = defineGame({
883
- name: "My Game",
884
- assets,
885
- world,
886
- physics,
887
- inventories,
888
- input: keybinds,
889
- server: "persistent", // or { mode: "ffa", scoreLimit: 30 } — rules live in game code
890
- save: { auto: "5m", scope: "player+chunks" }, // or "none"
891
- multiplayer: offline(), // or ws({ topology, url? }) / fly({ app }) / convex({ topology }) / socketIo({ topology, url? }) / p2p({ room? }) / lan({ port?, path? }) / servers({ …, adapter }) — defaults to offline()
892
- content,
893
- loop, // Partial<GameLoop<GameContext>> — missing hooks default to no-ops
894
- GameUI,
895
- camera: { perspective: "third" }, // optional — this is the default
896
- });
897
- ```
898
-
899
- ```ts
900
- // game/keybinds.ts — named actions + generated hotbar slots; one key, one action
901
- import { hotbarSlotBindings, type ActionCodesMap } from "@jgengine/core/input/actionBindings";
902
-
903
- export const keybinds: ActionCodesMap = {
904
- moveForward: ["KeyW"], moveBack: ["KeyS"], moveLeft: ["KeyA"], moveRight: ["KeyD"],
905
- jump: ["Space"], sprint: ["ShiftLeft"],
906
- interact: ["KeyE"],
907
- crouch: { hold: ["KeyC"], toggle: ["KeyZ"] },
908
- aim: { hold: ["mouse2"], toggle: ["KeyV"] },
909
- tabTarget: ["Tab"], clearTarget: ["Escape"],
910
- ...hotbarSlotBindings(9), // hotbarSlot1..9 → Digit1..9 (a 10th slot gets Digit0)
911
- };
912
- ```
913
-
914
- ```ts
915
- // game/inventories.ts
916
- import type { InventoryDeclaration } from "@jgengine/core/game/defineGame";
917
- export const inventories: Record<string, InventoryDeclaration> = {
918
- hotbar: { slots: 9, hud: "hotbar" },
919
- backpack: { slots: 28, traits: itemTraits },
920
- equipment: { slots: 4, accepts: ["weapon", "armor"], applyModifiers: true },
921
- };
922
-
923
- // world.ts — top of src/, not under game/
924
- import type { PhysicsConfig } from "@jgengine/core/game/defineGame";
925
- import { biomes, type WorldFeature } from "@jgengine/core/world/features";
926
- export const world: WorldFeature = biomes({ map: "world/biomes", zones: "world/zones" });
927
- export const physics: PhysicsConfig = { gravity: -32 };
928
- ```
929
-
930
- - `PhysicsConfig.gravity`/`jumpVelocity` tune the shell's built-in walk controller's fall/jump feel (defaults ~`-24`/`7.1`) — the only two levers on gravity and jump height; everything else about movement (speed, poses) stays catalog `movement` fields.
931
- - Input bindings are string arrays (hold semantics) or `{ hold, toggle, repeatMs? }` for the same verb. `repeatMs` turns a held action into an auto-repeat fire (build-mode place-on-drag, rapid-fire without a separate `wasPressed`/interval combo in game code) — the shell refires the command every `repeatMs` while the binding stays down, on top of its normal press-edge fire.
932
- - **Keybind → command convention.** The shell fires a command for any bound action that isn't reserved: pressing an action runs a command of the **same name** if one is defined, else a `ui.<action>` fallback (so `openBackpack` → `ui.openBackpack`). Just declare the binding and a matching command — no per-game `keydown` listener. Reserved actions the shell consumes natively and never routes to a command: `moveForward/moveBack/moveLeft/moveRight`, `turnLeft/turnRight`, `sprint`, `jump`, `tabTarget`, `clearTarget`, `useAbility`, `interact`, and any `hotbarSlotN`/`slotN`. `tabTarget`/`clearTarget` run `target.cycle`/`target.clear` (native `cycleTarget`/`setTarget` fallback). Because `hotbarSlotN` never reaches a command, a game that drives selection from game code binds its own `selectSlot1..N` actions, defines matching commands that write a store key, and reads it back through `hotbarSelection: () => ...` on `defineGame` (see `loot-shooter`).
933
- - **`interact`** is special: pressing it resolves the active proximity prompt from the `prompts` field of `defineGame({...})` and runs that prompt's `invoke` command. A prompt with `invoke: null` is display-only and does nothing on the key.
934
- - UI keybind badges derive from `keybinds` via `actionLabel(keybinds, "openBackpack")` — `bindingLabel` maps codes to short labels (`Digit1`→`1`, `KeyB`→`B`, `mouse0`→`LMB`, `Escape`→`Esc`). Never hardcode label strings; they drift the moment a binding changes.
935
- - `server.mode` is a string your loop/commands interpret — the engine ships no gamemode presets.
936
- - Never in defineGame: player tuning, catalog helpers (`defineItems` etc.), game nouns, behaviors, prompts, or inline binding/inventory/world blobs. The one exception is `physics.gravity`/`physics.jumpVelocity` — global controller tuning, not a catalog value (see "Controller kinematics" below).
937
- - `assets` may be omitted for a game with no models (a HUD-only card/board game, say) — `defineGame` injects an empty catalog, so `GameDefinition.assets` is always present downstream with no per-caller `?.` checks.
938
- - `devtools` defaults to `true` — every game gets the F2-toggled debug overlay (Perf/Tune/Logs/Net/Keys) for free, and every top-level `export const` number/boolean/color and every exported flat table of them under `src/` is auto-discovered into the Tune tab with zero game code; set `false` to disable the toggle entirely. See "Devtools — F2 overlay and tunables" below.
939
-
940
- ### `@jgengine/core/game/defineGame` — the underlying primitive
941
-
942
- The low-level engine boot call the shell `defineGame` composes internally: engine fields only (`name`, `assets`, `world`, `physics`, `inventories`, `input`, `server`, `save`, `time`, `feed`, `multiplayer`, `loop`) — no `content`/`GameUI`/`camera`/render fields, those are the shell layer's job.
943
-
944
- ```ts
945
- import { defineGame as defineEngineGame } from "@jgengine/core/game/defineGame";
946
- import { offline } from "@jgengine/core/runtime/adapter";
947
-
948
- const game = defineEngineGame({
949
- name: "My Game",
950
- assets, world, physics, inventories,
951
- input: keybinds,
952
- server: "persistent",
953
- save: { auto: "5m", scope: "player+chunks" },
954
- multiplayer: offline(),
955
- loop, // GameLoop<GameContext>
956
- });
957
- ```
958
-
959
- Reach for this directly only outside a React host — a headless server, a non-shell runner; a browser game authors through `@jgengine/shell/defineGame` above, which calls this and returns the `PlayableGame` a runner needs.
960
-
961
- ## `PlayableGame` — how a game plugs into a runner
962
-
963
- The type `@jgengine/shell/defineGame` returns and every runner (`GameHost`, `GamePlayerShell`) consumes. A game never builds this object by hand — `defineGame({...})` assembles it from the merged config. Source type at `@jgengine/core/game/playableGame`:
964
-
965
- ```ts
966
- export type PlayableGame<TUi = unknown> = {
967
- game: GameDefinition;
968
- content: GameContextContent; // { itemById?, entityById?, objectById? }
969
- loop: Required<GameLoop<GameContext>>; // onInit, onNewPlayer, onTick
970
- GameUI: TUi; // React component in web runners
971
- prompts?: (ctx: GameContext) => readonly PositionedPrompt[]; // interact-key + HUD source
972
- };
973
- ```
974
-
975
- `prompts` is the **single source** of positioned proximity prompts: the shell reads it to fire the `interact` key, and the HUD should read the same list through `useActivePrompt(playable.prompts?.(ctx))` rather than building its own — one list, no drift. A prompt is only actionable if its `invoke` is non-null.
976
-
977
- Optional render/world fields the shell also reads: `entitySprites` / `entityModels` (billboards / GLBs keyed by entity kind), `objectModels` (GLBs keyed by object catalog id), `renderObject` (per-object visual override — return your own mesh for a placed object and the shell still positions it; falls back to `objectModels` → colored box), `WorldOverlay` (canvas-layer VFX), `environment` (canvas-layer scenery — ground/sky/structures; when set, replaces the default ground plane + debug grid + rock field), `camera`, `shadows` (cast/receive shadows across the R3F canvas; default true), and `worldHealthBars` (`boolean | { statId?, roles? }` — `roles` restricts bars to entities whose catalog `role` is in the given `CatalogEntityRole` list, e.g. skip friendly NPCs). A model value is a catalog id (`string`, resolved via `game.assets`) or an inline `ModelConfig { url, scale?, y?, anchor?, dims?, material?, animation? }` (`material` overrides color/metalness/roughness/emissive/emissiveIntensity on the cloned mesh, leaving shared GLTF caches untouched; `animation?: { clip?, loop?, timeScale?, paused?, time? }` plays a GLTF clip — `clip` defaults to the first clip, `loop` defaults true — and `paused: true` + `time: <seconds>` holds the rig on one fixed frame, a pose library for inventory previews or held cutscene poses). Catalog-resolved models carry measured `dims` (`catalog.resolve(id).dims = { footprint:{w,d}, center:{x,z}, minY }`); with the default `anchor: "center"` the shell centers the footprint on the placement point and ground-snaps `minY` to it, so corner-pivot kit models place correctly with no per-game pivot math. Applies through both `entityModels` and `objectModels`.
978
-
979
- `renderEntity?: (entity: SceneEntity) => ReactNode` and `renderObject?: (object: SceneObject) => ReactNode` hand you the mesh for one entity/object while the shell still positions it and keeps it tagged for picking/selection; return null/undefined to fall through to model → sprite/box. `objectStyles?: Record<catalogId, { color?, opacity?, hidden? }>` styles the default colored-box object render — `color` overrides the hash color, `opacity < 1` sets transparent, `hidden` skips the mesh but keeps the picking tag.
980
-
981
- **Presentation mode.** `PlayableGame.presentation`: `"3d"` (default) mounts the canvas, camera rig, and pointer; `"hud"` mounts none of that — the game is `GameUI` plus the command/input loop, for board/card/menu games that need no 3D camera at all.
982
-
983
- **Auto environment.** When `world` is an `environment()` descriptor and `PlayableGame.environment` is unset, the shell renders that descriptor as the backdrop automatically — no manual `environment` wiring needed for the common case. Set `environment` explicitly only to override that default (a custom canvas component always wins). The same auto-render convention covers grid-cell worlds (`biomes`/`voxel`/`plots`/`tilemap`) — see "World features" below.
984
-
985
- **Lighting and backdrop.** `PlayableGame.lighting` (`LightingConfig`, `@jgengine/core/game/playableGame`) replaces the shell's hardcoded ambient/directional default when present, regardless of world kind: `ambient?: { color?, intensity? }`, `directional?: { color?, intensity?, position, castShadow? }[]`, `hemisphere?: { skyColor?, groundColor?, intensity? }`. `PlayableGame.backdrop` (`BackdropConfig`) is a generic background/sky/fog for **any** world kind, including a custom `environment` component: `background?: string` (CSS color), `sky?: SkyEnvironmentConfig` (same descriptor `environment()`'s `sky` field takes), `fog?: { color?, near?, far?, density? }` (`density` set switches to exponential `FogExp2` and `near`/`far` are ignored). Both are optional and additive to whatever the world/`environment` renderer already draws.
986
-
987
- **Visibility & streaming (automatic).** Every 3D game gets camera frustum + distance culling for free — the shell's `CullingProvider` reads the live camera each frame, runs the engine `createVisibilitySystem` (`@jgengine/core/visibility/visibilitySystem`) over the scene's entities and placed objects, and toggles `group.visible` so off-screen objects are never submitted to the renderer (never unmounted — gameplay and simulation are untouched). Defaults are conservative (a preload margin larger than the view, hysteresis, `Infinity` default render distance) so existing games only benefit. Tune or opt out via `PlayableGame.visibility` (`VisibilityConfig`, `@jgengine/core/visibility/config`): `enabled: false` disables it; `culling`/`streaming` patch the global `CullingSettings`/`StreamingSettings` (`@jgengine/core/visibility/settings`); `scene` sets scene-wide overrides; `entities`/`objects` override by kind name / catalog id (`alwaysVisible`, `maxRenderDistance`, custom `bounds`, `pinned`, `cullingDisabled`, …). The engine layer also ships `@jgengine/core/visibility/spatialIndex` (the 3D hash the culler queries instead of scanning every object), `@jgengine/core/visibility/assetStreaming` (dedup/budget/grace-period asset loading), and `@jgengine/core/visibility/simulationCulling` (opt-in, off by default — throttles low-priority off-screen updates, never protected entities). Full reference: `packages/core/src/visibility/README.md`.
988
-
989
- **Player movement tuning** — the `movement` field (`PlayerMovementConfig`) tunes the shell's local-player walk controller beyond `physics.gravity`/`jumpVelocity`: `mode` (`"free"` camera-relative default, `"axis"` locks travel to one world `axis`, `"grid"` snaps each committed step to `cellSize` centers), `collideObjects` (collide against placed scene objects as unit-box AABBs even without `collision.voxel`), and `beforeCommit(frame)` — an escape hatch called each frame with `{ entityId, current, next, dt }` that can return a replacement `[x, y, z]` to constrain or redirect the step (rails, bounds, custom collision) before it commits and before `onTick` runs.
990
-
991
- The runner boots `createGameContext({ definition, content, player: { userId, isNew } })`, calls `loop.onInit(ctx)` then `loop.onNewPlayer(ctx)`, and drives `loop.onTick(ctx, dt)` per frame. **Convention: `onNewPlayer` spawns the player entity with `id === ctx.player.userId`** — bounded stats, targeting, and kill attribution key off that.
992
-
993
- ### Object spatial queries, entity patching, and surface sampling
994
-
995
- ```ts
996
- ctx.scene.object.at(x, y, z) // cell lookup, cell size 1, most-recent wins
997
- ctx.scene.object.inBox(min, max) // inclusive AABB query
998
- ctx.scene.object.raycast({ origin, direction, maxDistance, halfExtents?, filter? }) // → nearest hit or null
999
- ctx.scene.object.raycastAll({ origin, direction, maxDistance, halfExtents?, filter? }) // → hits, nearest-first
1000
- ctx.scene.entity.update(id, patch) // name/position/rotations/role/movement/behaviors/meta; false for unknown id
1001
- ```
1002
-
1003
- Placed objects are unit boxes (half-extents `[0.5, 0.5, 0.5]`) centered on position, matching the shell's default render, so `raycast`/`raycastAll` (`scene/objectQuery`) match what a player sees. `entity.update` notifies subscribers and bumps `ctx.version()` the same as `spawn`/`despawn`/`setPose` — it's the general-purpose patch the more specific methods (`setPose`, `form.shapeshift`, `possession.possess`) build on.
1004
-
1005
- `ctx.scene.object.place(catalogId, x, y, z, { instanceId?, parentSpace?, rotation?, visual? })` takes an optional `visual: ObjectVisual` (`@jgengine/core/scene/objectStore`) — `{ scale?: number | [x,y,z], color?, opacity? }` — a per-instance render override independent of the catalog entry; `ctx.scene.object.setVisual(instanceId, visual | undefined)` changes it after placement (`undefined` clears back to the catalog default). Distinct from `objectStyles` on `PlayableGame` (styles a catalog id for every instance); `visual`/`setVisual` targets one placed instance (a damaged crate, a dyed banner, a resized prop).
1006
-
1007
- `pointerService.worldHitCenter()` (shell) casts from the viewport center regardless of cursor presence (pointer-lock aim) — combine with `pointer.worldHit()` (cursor-driven) to support both mouse-look and free-cursor games from the same probe. `PointerHit` also carries an optional `uv?: { u, v }` on UV-mapped mesh hits (absent for the ground fallback), `material?: { color, metalness?, roughness? } | null` sampled off the hit mesh's `MeshStandardMaterial` (`null`/unset for non-standard materials, e.g. the ground plane) — combine `uv` + `material` for paint tools, decals, and material-aware interaction — and `instanceId?: number`, the hit index when the intersected mesh is a `THREE.InstancedMesh` (grid-world cells, `InstancedBodies` debris, any instanced render), absent otherwise.
1008
-
1009
- **Runtime paint layer** (`ctx.scene.entity.paint`, backed by `scene/paintLayer`) — a `PaintLayer` keyed by instance id (entity or object): `paint(instanceId, { u, v, radius, color })`, `strokes(instanceId)`, `clear(instanceId?)`, `version(instanceId)` (bumps per paint/clear), `subscribe(listener)`. The shell auto-renders painted instances through a lazily-created 512×512 canvas texture kept in sync — no per-game render wiring. Clearing refills with the material's base color; the original texture pixels are not restored.
1010
-
1011
- ### Audio — positional emitters, listener falloff, buses
1012
- Catalog-first, no per-game audio glue. The `audio` field of `defineGame({...})` — `{ sounds: Record<string, SoundDef>, buses?: Record<string, AudioBusDef> }` — declares the sound catalog (`SoundDef = { id, url, bus, gain?, loop?, positional?, falloff? }`) and mix buses (`music`/`sfx`/`ambient`/…, `AudioBusDef = { id, gain? }`) — both types from `@jgengine/core/audio/audioFalloff`. `entitySounds?: Record<string, string>` maps an entity **kind name** (same convention as `entitySprites`/`entityModels`) to a sound id: while a matching entity exists, the shell keeps a looping positional emitter on it, repositioned every frame. `objectSounds?: Record<string, string>` does the same keyed by placed-object catalog id. The pure distance→gain math (`computeFalloffGain(distance, config)`, curves `"linear" | "inverse" | "none"`) lives in core so it is unit-tested without a browser; `@jgengine/shell` (`shell/audio/audioEngine`, `shell/audio/AudioComponents`) is the only package that touches Web Audio — it owns an `AudioContext`, mounts `AudioListener` on the camera every frame, and `EntityAudioEmitters`/`ObjectAudioEmitters` drive per-instance emitter gain from the core falloff function. `GamePlayerShell` wires all of this automatically from `playable.audio`/`entitySounds`/`objectSounds` — a game never touches `AudioContext` directly.
1013
- ### Camera rigs (`camera` field of `defineGame({...})`)
1014
- The shell ships a **rig library**; a game picks and tunes one through `camera` config, never by writing camera positions from `onTick`. Select with `camera.rig` (or the `perspective: "third" | "first"` shorthand) — or by config block alone (#207.8): supplying `camera.<rig>` selects that rig with no redundant `rig` field, checked in the table's order; an explicit `rig` wins when several blocks are present:
1015
- | `rig` | For | Key config (`camera.<rig>`) |
1016
- |-------|-----|------------------------------|
1017
- | `orbit` (default) | Third-person chase | Top-level fields on `camera` itself — `initialDistance`, `minDistance`/`maxDistance`, `targetHeight`, `min/maxPolarAngle`, drag/zoom. There is **no `camera.orbit` block**; orbit is the one rig tuned at the top level |
1018
- | `first` | FPS mouse-look | `firstPerson: { eyeHeight, sensitivity, maxPitch, reticle, viewmodel }` |
1019
- | `topDown` | ARPG iso / top-down (Diablo IV, Hades II) | `topDown: { height, pitch, yaw, followSmoothing, zoom }` — decoupled follow; `pitch` is camera elevation (PI/2 = straight down, near 0 = grazing and boom-distance blows up past `frustum.far`) |
1020
- | `rts` | Free-pan / edge-scroll (The Sims, Manor Lords) | `rts: { panSpeed, edgeScroll, rotateSpeed, bounds, start, pan }` — `pan: false` turns it into a static backdrop camera: no WASD/arrow pan, no edge-scroll, no Q/E rotate, no wheel zoom, still re-centers on `followEntityId` if one resolves |
1021
- | `shoulder` | Over-the-shoulder (Helldivers 2, Remnant II) | `shoulder: { shoulderOffset, distance, ads, side }` — ADS + shoulder-swap (V) |
1022
- | `lockOn` | Souls-like strafe (Elden Ring) | `lockOn: { targetEntityId?, distance, framingBias, yawSmoothing }` — yaw binds to player→target; WASD becomes strafe |
1023
- | `chase` | Vehicle chase (Forza, Rocket League) | `chase: { distance, springDamping, fov: { base, max, speedForMax }, shakePerSpeed, view: "chase"|"cockpit"|"hood"|"rear" }` |
1024
- | `sideScroll` | Fixed lateral follow — 2.5D platformer/beat-'em-up | `sideScroll: { distance, height, lookHeight, axis: "x"\|"z", followSmoothing, fov }` — reads no player input, follows like the other follow rigs (defaults to the local player) |
1025
- | `observer` | Detached spectator/photo/kill-cam (#120) | `observer: { bind: { kind: "entity", entityId } \| { kind: "point", position }, distance, height, orbitSpeed }` — reads no player input, auto-orbits the bound subject |
1026
- | `inspection` | Model-viewer / data-viz orbit (#207.7) | `inspection: { anchor: "target"\|"cursor"\|"center", target, initialDistance, initialPosition, min/maxDistance, min/maxPolarAngle, pan, rotateSpeed, zoomSpeed, dampingFactor }` — left-drag orbit, middle/right-drag pan (pan defaults on for this rig only), scroll zoom toward the anchor (`cursor` = zoom-to-cursor); orbits a fixed `target`, never reads player/entity state |
1027
- | `none` | No camera rig mounted | HUD-only presentations or a game that manages its own camera; see `presentation: "hud"` below |
1028
- **Frustum:** `camera.frustum: { fov?, near?, far? }` overrides the canvas camera; `far` defaults to 300, so any world whose content spans more than a few hundred units must raise it or distant settlements/terrain silently clip out of view. **Every rig accepts `followEntityId: null`** so avatar-less games (city-builders, card games, auto-battlers) still mount a camera. Leave `followEntityId` unset and the shell defaults it to `ctx.player.possession.active(userId)` every frame, so a possession swap (party control-swap, BG3-style) or a form's mesh/camera-relevant change re-targets the camera automatically — set it explicitly only to override that default. **Shake / trauma (#28):** every rig reads a shake channel; feed it from anywhere with `import { cameraShake } from "@jgengine/shell/camera"` — `cameraShake(amplitude, decayPerSecond?)` (amplitude 0..1) — or from React via `useCameraShake()`. Tune with `camera.shake: { maxOffset, maxRoll, decayPerSecond, exponent, frequency }`. **Cinematic (#29):** set `camera.cinematic: { keyframes: [{ position, lookAt, fov?, duration?, ease? }], loop? }` to play a scripted path over the active rig, and `camera.transitionSeconds` cross-fades the camera when the rig changes so mode swaps don't hard-cut. The pure rig math (shake decay, spring-arm, speed→FOV, offset/strafe, keyframe lerp) is exported from `@jgengine/shell/camera` for testing.
1029
-
1030
- ## `GameContext` — the ctx surface
1031
-
1032
- `createGameContext` (in `@jgengine/core/runtime/gameContext`) wires every system:
1033
-
1034
- ```
1035
- ctx.scene.object place, remove, move, rotate, get, list, subscribe,
1036
- at, inBox, raycast, raycastAll, catalog
1037
- ctx.scene.entity spawn, despawn, setPose, update, get, list,
1038
- stats.{get,set,delta}, setTarget, getTarget, cycleTarget,
1039
- canReceive, preview, effect, paint,
1040
- willHitProjectile, fireProjectile, settleProjectile,
1041
- distance, inRadius, hasLineOfSight, queryArc, moveToward,
1042
- spawnPoseOf, resetToSpawn, resetAllToSpawn,
1043
- form.{register,get,active,abilities,shapeshift,revert}
1044
- ctx.game commands, events, feed, loot, trade, quest, social, chat,
1045
- unlocks, economy, leaderboard, roster, store, cards, turn
1046
- ctx.game.social friends, party, presence, emotes.play, worldInvites
1047
- ctx.game.store set, delete, get, has, subscribe, mapSnapshot, arraySnapshot — game-defined
1048
- keyed reactive store slot (any value type); mutations bump ctx.version()
1049
- ctx.game.cards pile(id, config?) — lazily creates (config required on first call) or returns
1050
- the existing notify-wrapped CardPile for id
1051
- ctx.game.turn loop(id, config?) — lazily creates (config required on first call) or returns
1052
- the existing notify-wrapped TurnLoop for id
1053
- ctx.player userId, isNew, inventory, stats (modifiers), loadout,
1054
- applyLoadout, movement (pose/aim), motion (impulse/setVerticalVelocity/setY/takePending),
1055
- possession, cosmetics
1056
- ctx.player.motion impulse(vy), setVerticalVelocity(vy), setY(y), takePending() — game-code
1057
- seam into the shell's vertical-motion integrator; drained once per frame
1058
- before gravity, so a jump pad or grapple release calls this from
1059
- onTick/commands instead of touching y directly
1060
- ctx.item use, weapon
1061
- ctx.input publish(held), isDown(action), held() — per-frame held-action snapshot, polled from onTick
1062
- ctx.world ground (TerrainField), groundHeightAt(x, z) — the canonical
1063
- sampler for the game's declared world; environment worlds
1064
- resolve their terrain field, every other world kind is 0.
1065
- Use it for every spawn/placement/waypoint y — never
1066
- hand-roll a noise sampler or hardcode y = 0 on relief
1067
- ctx.camera follow(entityId | null), followedEntityId(), setCinematic(config), cinematic(),
1068
- subscribe — runtime camera-follow/cinematic override; the shell reads
1069
- followedEntityId() each frame, falling back to the static
1070
- playable.camera.followEntityId when it returns undefined
1071
- ctx.time advance, now, calendar, snapshot; pause, play, toggle,
1072
- setSpeed, cycleSpeed; after, every, at (game-time timers)
1073
- ctx.subscribe / ctx.version change signal — UI layers bind via useSyncExternalStore
1074
- ```
1075
-
1076
- `content.itemById(id)` supplies `{ use?, weapon?, trade? }` — exact shapes: `use` is the handler **name string** (not an object), `weapon` is a flat `Record<string, number | Record<string, number>>` built from your catalog stats (`damage`, `projectile.{...}`, `explosion.{...}`), and every lookup returns **`null`** (not `undefined`) for an unknown id; `content.entityById(id)` supplies `{ stats?, receive?, onDeath?, movement?, role? }`; `content.objectById(id)` supplies `GameContextObjectEntry` `{ proximityPrompt?, breakable?, slotInventory? }`. Build all three from your catalogs in `content.ts`. Call-shape gotchas that cost typecheck loops: `ctx.item.use.use(input)` takes **one** argument on the ctx surface (the two-arg `use(state, input)` is the raw factory shape); `ctx.scene.entity.moveToward(id, target, { speed, dt, stopDistance? })` — the third argument is an options object, never a bare `dt`; `nav/pathFollow`'s `createPathFollow(config)` returns the initial `PathFollowState` directly and `advancePathFollow(config, state, dt)` returns the **next state** (with `position: [x,y,z]`, `heading`, `done`) — there is no wrapper object, and `Waypoint` is a `[x, y, z]` tuple. A placed object resolves its catalog entry via `ctx.scene.object.catalog(instanceId)`; `ctx.scene.object.at(x, y, z, tolerance?)` finds placed objects near a point (grid interaction, click resolution beyond the pointer service). `ctx.scene.entity.update(id, patch)` writes a shallow patch onto a spawned entity's mutable fields (e.g. `movement.walkSpeed`) without a full respawn — `scene/movementSpeed`'s `applyStatDrivenSpeed(deps, id, { baseSpeed, multiplierStat?, flatBonusStat? })` is the catalog-driven helper that recomputes and writes `movement.walkSpeed` from a stat-modifier pair each time a buff changes.
1077
-
1078
- ### Two tiers: `ctx` runtime vs pure factories
1079
-
1080
- The `ctx` surface above is the **stateful runtime** — it's what game code uses. Every subsystem it wires is *also* exported as a **pure factory** that `createGameContext` composes internally: `createTradeSystem`, `createDeathSystem`, `createEffectSystem`, `createProjectileSystem`, `createSpatialApi`, `createEntityStatsApi`, `createEntityStore`, `createObjectStore`, `createStats`, `createLoadouts`, `createLootRegistry`, `createQuestJournal`, `createSocial`, `createSlots`, `createInteriors` (plus stateless helpers beside each — `canAffordCosts`/`resolveBuy` in `game/trade`, `getStatValue`/`applyPoolDelta` in `scene/entityStats`, and so on). **Build a game through `ctx`, not these** — reach for the factories only for unit tests of pure game math, headless servers, or a custom runtime. Import the domain deep path (`@jgengine/core/combat/death`, `@jgengine/core/game/trade`, `@jgengine/core/stats/statModifiers`, …) and read the `.d.ts`; each is a real export in the published package.
1081
-
1082
- `createSpatialApi`'s optional `grid: { cellSize }` opts `inRadius`/`queryArc` into a lazily-built x/z broadphase index over `candidates()` instead of a linear scan — worth it once candidate counts run into the hundreds+. The index is reused across calls until `invalidate()` is called, so call it after any position change (move, spawn, despawn); a candidate outside the index at query time still resolves exactly (never silently skipped), only a *moved* one can be missed until invalidated.
1083
-
1084
- ## `loop` — lifecycle
1085
-
1086
- ```ts
1087
- export function onInit(ctx: GameContext) {
1088
- ctx.item.use.register(itemUseHandlers);
1089
- ctx.player.loadout.register(loadouts);
1090
- for (const table of lootTables) ctx.game.loot.register(table);
1091
- ctx.game.quest.register(quests);
1092
- ctx.game.quest.bind("entity.died");
1093
- ctx.game.feed.bind("entity.died");
1094
- ctx.game.events.on("entity.died", (evt) => onEntityDied(ctx, evt));
1095
- setupWorld(ctx);
1096
- }
1097
-
1098
- export function onNewPlayer(ctx: GameContext) {
1099
- ctx.scene.entity.spawn("player_default", { id: ctx.player.userId, position: spawnPoint });
1100
- if (ctx.player.isNew) ctx.player.applyLoadout(ctx.player.userId, "starterKit");
1101
- }
1102
-
1103
- export function onTick(ctx: GameContext, dt: number) {
1104
- // AI, regen, respawn timers — dt is GAME time (see ctx.time). Never death detection (see entity.died)
1105
- }
1106
- ```
1107
-
1108
- `onInit` runs once per boot; register everything there. Loot tables register through `ctx.game.loot.register` — `lootTable()` is a pure validating factory, there is no global side-effect registry.
1109
-
1110
- ## `ctx.time` — the simulation clock
1111
-
1112
- `onTick`'s `dt` is **game time, not real time**: the shell scales each frame's real delta by `definition.time.scale` (real→game seconds at 1×) and the live speed multiplier, so writing decay/regen/AI as `rate * dt` makes it obey pause and fast-forward for free — never read wall-clock in a tick. Configure via `defineGame({ time: { scale?, speeds?, dayLength?, start?, startPaused?, daysPerYear?, seasons? } })` (all optional; default is real-time 1:1 with speeds `[1,2,3,4]`, 365-day years).
1113
-
1114
- - **Continuous** work scales through `dt`. **Scheduled** work uses game-time timers: `ctx.time.after(sec, cb)`, `ctx.time.every(sec, cb)`, `ctx.time.at(gameSec, cb)` — measured in game-seconds, so 4× fires them 4× sooner and pause freezes them. Each returns a cancel handle.
1115
- - **Controls** (drive from a HUD or a command): `pause()`, `play()`, `toggle()`, `setSpeed(mult)` (0 pauses), `cycleSpeed()`. Read state with `ctx.time.snapshot()` / `ctx.time.calendar()` (`{ day, hour, minute, second, dayFraction, year, dayOfYear, yearFraction, season? }`), or in React with `useGameClock()` → snapshot + `controls`. Speeding to 4× or pausing affects **everything** on the tick — no per-system wiring.
1116
- - **Calendar year/season** rides the same day counter, no separate clock: `year`/`dayOfYear` fall out of `day` divided by `TimeConfig.daysPerYear` (default 365), `yearFraction` is progress through the current year (0..1). Setting `TimeConfig.seasons: string[]` (e.g. `["spring","summer","fall","winter"]`) slices the year into equal named segments and populates `calendar().season`; omit `seasons` and the field is absent — a living-world sim names its own season boundaries this way instead of a hand-rolled `dayOfYear % ...` module.
1117
-
1118
- ### Beat clock — BPM signal + input quantization
1119
-
1120
- `@jgengine/core/time/beatClock` is a separate, purpose-built signal from `simClock` — a BPM tick generator for rhythm games (Hi-Fi Rush–style quantized combat), not a day/pause clock. `createBeatClock({ bpm, beatsPerBar? }, onBeat?)` returns a `BeatClock`: call `advance(gameDt)` from `onTick` with the same **game-time** `dt` (never wall-clock) — it fires `onBeat(beatIndex)` once per newly crossed integer beat and returns a `BeatSnapshot` (`beat`, `beatIndex`, `bar`, `beatInBar`, `phase`). `createBeatInputBuffer<T>(beatDurationSec)` is the auto-correct input buffer: `buffer(action, nowSec)` quantizes an off-beat press to fire on the next beat tick (or immediately if pressed exactly on one); `advance(nowSec)` drains and returns every action whose beat has arrived. `nextBeatTime(nowSec, beatDurationSec)` is the underlying pure quantization function. Feed a music track's actual BPM in; the buffer is what makes an early/late input still land on-beat.
1121
-
1122
- ## Content catalogs
1123
- ## `ctx.game.store` — reactive game state
1124
-
1125
- ```ts
1126
- ctx.game.store.set("health", 100) // any key, any value type
1127
- ctx.game.store.get("health") // T | undefined
1128
- ctx.game.store.has("health")
1129
- ctx.game.store.delete("health")
1130
- ctx.game.store.subscribe(listener) // change-signal fires on set/delete
1131
- ctx.game.store.mapSnapshot() / arraySnapshot()
1132
- ```
1133
-
1134
- A reactive per-game keyed store (`ObservableKeyedStore<unknown>`) attached to `GameContext` — reach for it instead of a module-level singleton store for ad-hoc reactive game state (turn trackers, deck UIs, anything that doesn't already have a `ctx` surface). `set`/`delete` bump `ctx.version()` and notify `ctx.subscribe` listeners; `get`/`has` are plain reads. Unlike a per-slot handle, there is no `define`/seed step — a key simply doesn't exist until the first `set`.
1135
-
1136
- ## `ctx.game.cards` / `ctx.game.turn` — lazily-created piles and turn loops
1137
-
1138
- `ctx.game.cards.pile(id, config?)` and `ctx.game.turn.loop(id, config?)` lazily create (config required on first call) or return the existing notify-wrapped `CardPile`/`TurnLoop` for `id` — call with just the id after the first `onInit` seed to fetch the same instance; every mutating method is wrapped so it bumps `ctx.version()`/notifies `ctx.subscribe` automatically, same as every other `ctx` surface. This replaces manually constructing `createCardPile`/`createTurnLoop` and wiring notification yourself.
1139
-
1140
- ## Movement, pose, input
1141
- ## External data — `data/dataSource` and the dev proxy
1142
-
1143
- Renderer-free async-state primitives (`@jgengine/core/data`) for a game that reads a live external source (a leaderboard API, a session browser, remote config) — distinct from `ctx.game.store`/multiplayer, which are for the game's own authoritative state.
1144
-
1145
- - **`createDataSource(load, options?)`** (`data/dataSource`) → `DataSource<T>` wraps one `load(signal)` async call as `{ status: "idle"|"loading"|"ready"|"error", data, error }`. `getState()` reads the current snapshot, `subscribe(listener)` fires on every change, `refresh({ force? })` re-runs `load` (de-duplicates a call already in flight unless `force`; aborts the prior call first when forced), `startPolling(intervalMs?)`/`stopPolling()` run `refresh` on an interval (`intervalMs` falls back to the one passed at construction; throws if neither is given), `dispose()` tears down polling and in-flight requests for good. Pass `options.clock` (`{ setInterval, clearInterval }`) to swap the timer source in tests.
1146
- - **`fetchJson<T>(url, options?)`** (`data/fetchJson`) — `fetch` + JSON-parse in one call; throws `HttpStatusError` (`status`, `statusText`, `url`) on a non-OK response and `JsonParseError` (`url`, `cause`) on unparsable JSON, so a `DataSource`'s `error` is always one of these two typed shapes, never a bare `Error`. `options.fetchImpl` swaps the fetch implementation for tests/SSR.
1147
- - **`createJsonDataSource<T>(url, options?)`** (`data/jsonDataSource`) — sugar combining the two above: a `DataSource<T>` whose `load` calls `fetchJson(url, options)`.
1148
- - **Dev proxy (`data/devProxy`)** — same-origin routing for external APIs during `bun dev` so browser CORS never blocks a game's `fetchJson` call against a third-party host. `parseDevProxyTable(raw)` parses a `VITE_JGENGINE_DEV_PROXY` env value (a JSON object of `{ routeName: "https://api.example.com" }`) into a `DevProxyTable`; `proxiedUrl(target, { dev?, table?, prefix? })` rewrites a `target` URL whose prefix matches a table entry into `/proxy/<routeName>/<rest>` (default prefix `/proxy`) when `dev` is true (defaults to `import.meta.env.DEV`) — else returns `target` unchanged, so the same call hits the real host in production. `apps/dev`'s `vite.config.ts` reads the same env var and wires a matching Vite server `proxy` entry per route (`changeOrigin: true`, strips the `/proxy/<routeName>` prefix) — set `VITE_JGENGINE_DEV_PROXY` once and both sides (the URL rewrite and the actual proxy route) agree.
1149
-
1150
- ## Multiplayer and the backend seam
1151
- ## Genre cheat sheet
1152
-
1153
- - **Voxel/crafting**: objects for blocks/machines, `voxel()`, `object.break`/`object.placeFromInventory`.
1154
- - **Tycoon/lab**: objects + `slotInventory`, `plots()`, configure via prompt → command.
1155
- - **Shooter**: `fireProjectile`/`settleProjectile`; grenades settle → `effect({ at, radius })`; `movement.poses`/`aim` + zoom modifier; `servers({ … })` + game-owned `server.mode`; loadout classes from commands.
1156
- - **MMO/RPG**: bounded stats + `leveling()` over a game XP curve; `tabTarget` → `cycleTarget`; handlers read `getTarget`; quests bound to `entity.died`/`inventory.added`; social party + `partyShare`; `server: "persistent"`.
1157
- - **All combat games**: react to `entity.died` (feed/leaderboard/score) — never poll HP.
1158
-
1159
- ## Anti-patterns
1160
-
1161
- | Wrong | Right |
1162
- |-------|-------|
1163
- | Player tuning in `defineGame` | Entity catalog `movement` + stats |
1164
- | `behaviors: […]` on place/spawn | Catalog entry |
1165
- | Engine `weapon.fire` / `consumable.use` / `combat.*` | `item.use` + catalog `use` → game handler |
1166
- | `ItemUseInput.to` for targets | `getTarget(from)` in handlers |
1167
- | `effect({ to })` for gunshots | `fireProjectile` + `settleProjectile` |
1168
- | Polling HP in `onTick` for kills | `entity.died` event |
1169
- | `combat.lootTable` / `loot.enemy` | `onDeath` on the entity that died |
1170
- | Hand-rolled `Math.random()` loot in commands | `lootTable()` + `ctx.game.loot.roll` |
1171
- | Hand-rolled `xpForLevel`/`levelFromXp` | `game/progression` `curve()` + `leveling()` |
1172
- | Hardcoded shop arrays | `item.trade.shops` + `tradableAt` |
1173
- | Kit seeding via scattered `put`/`grant` | `applyLoadout` |
1174
- | Per-user quest state hand-rolled | `game.quest.register` + binds |
1175
- | `useKillFeed` / per-domain feed hooks | `useFeed({ action })` |
1176
- | Raw keys in game logic | `defineGame` input actions |
1177
- | Positioning inside `ui/components/` or on primitives (`CurrencyPill className="absolute …"`) | Screen wrappers in `GameUI.tsx` only |
1178
- | Game UI classes without `@source` in host CSS | `@source` entries for your game dirs + `node_modules/@jgengine/{shell,react}` |
1179
- | One file per catalog entry / per brand | Dense `<domain>/catalog.ts` |
1180
- | Convex mutations called from game code | `commands.run` through the `GameBackend` transport |
1181
- | Half a system: quest without tracker, cooldown without sweep, keybind never shown, stub "coming soon" modal | Finish the system end to end — or cut it whole (see `jgengine`) |
1182
- | Game-side workaround for a missing engine primitive | File the gap at github.com/Noisemaker111/jgengine/issues (or PR the primitive) and cut or scope the dependent system honestly |
1183
- | Game nouns in this skill | Engine primitives + placeholder ids only |
1184
-
1185
- ## New-game definition of done
1186
-
1187
- This is a gate, not a suggestion — every box, in one pass (workflow: **`jgengine`** skill). "Compiles and the hooks are wired" is not done; a declared system with no UI, no feedback, or no way to exercise it is not done — finish the system or cut it whole.
1188
-
1189
- - [ ] `game.config.ts` (`defineGame` from `@jgengine/shell/defineGame`) + `index.tsx` (barrel) + `main.tsx` (standalone host) + `loop.ts` + `game/content.ts`
1190
- - [ ] Catalogs: `game/entities/<role>/catalog.ts`, `game/items/<domain>/catalog.ts`, `game/objects/catalog.ts`; loot tables beside their domain
1191
- - [ ] Entity `stats` + `receive` orders aligned on the same stat ids; `role` set (drives targeting + camera)
1192
- - [ ] `game/items/use-handlers.ts` registered in `onInit`; handlers read `getTarget`/`aim`, never a target input
1193
- - [ ] `game/loadouts.ts` + `applyLoadout` in `onNewPlayer` (gated on `isNew`)
1194
- - [ ] `game/quests/catalog.ts` + binds; if using xp/level, a game-owned curve fed to `game/progression` (`curve`/`leveling`) — **with their HUD/tracker, or cut**
1195
- - [ ] `onInit`: register handlers/loadouts/loot/quests, event listeners, feed binds, leaderboard tracks; `setupWorld`
1196
- - [ ] Player spawns with `id === ctx.player.userId`
1197
- - [ ] `game/ui/GameUI.tsx` owns layout; components use `@jgengine/react` hooks
1198
- - [ ] UI passes the **quality bar** above (contrast, scale, framing, genre fit) — not just hook wiring
1199
- - [ ] Camera tuned via `camera` in `defineGame({...})` — defaults untouched means the feel was never checked
1200
- - [ ] For an `environment()` world: a `<game>.world.test.ts` asserts `summarizeEnvironment(world)` (`@jgengine/core/world/environmentSummary`) is non-empty with the expected counts — the browserless scene-correctness gate
1201
- - [ ] HUD screenshotted over a staged `GameUiPreview` scenario and **judged by looking at the image** against the UI quality bar in [`../jgengine-ui/reference.md`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-ui/reference.md) — the final human glance, not the verification loop
1202
- - [ ] Co-located bun tests for pure game math (curves, cooldowns, spawn logic)
1203
- - [ ] Multiplayer via adapter config only; no direct backend calls
1204
-
1205
- ## Quick reference
1206
-
1207
- ```
1208
- defineGame (shell) engine fields (assets, world, physics, inventories, input, server, save, time, feed, multiplayer)
1209
- + presentation fields (content, loop, GameUI, camera, environment, shadows, movement, devtools, …) in one call — smart defaults fill the rest
1210
- defineGame (core) the underlying engine-only primitive: assets, world, physics, inventories, input, server, save, time, feed, multiplayer, loop
1211
- PlayableGame { game, content, loop, GameUI, camera, … } — the runner contract `defineGame` (shell) returns
1212
- GameContext ctx.scene / ctx.game / ctx.player / ctx.item / ctx.camera / ctx.input + subscribe/version
1213
- scene.object place, remove, move, rotate, at, setVisual (per-instance ObjectVisual: scale/color/opacity override)
1214
- scene.entity spawn (anchor/offset), despawn, setPose, update; stats; targeting; effects;
1215
-
1216
- # jgengine-ui
1217
-
1218
- Use this skill for the **visual and interaction design of the game interface**: title screens, HUDs, menus, prompts, maps, inventories, dialogue, touch controls, transitions, pause/results states, accessibility, and screenshot critique.
1219
-
1220
- Do not use this skill as a React, routing, state-management, or hooks reference. The main `jgengine` skill owns routing and points to the engine APIs. When implementation needs `@jgengine/react` hooks or shell APIs, follow the links in the main skill and the compact API appendix in [reference.md](reference.md); keep this skill focused on what the player sees and feels.
1221
-
1222
- ## Required outcome
1223
-
1224
- A JGengine game must read as a self-contained game, not a responsive website with a canvas inside it.
1225
-
1226
- Before shipping UI:
1227
-
1228
- 1. Give the game a concise UI art direction.
1229
- 2. Compose explicit desktop/mobile game layouts instead of document flow.
1230
- 3. Keep persistent HUD information sparse and hierarchical.
1231
- 4. Adapt touch controls to the genre and reserve their screen zones.
1232
- 5. Implement authored focus, pressed, selected, disabled, success, failure, and warning states.
1233
- 6. Add purposeful motion and feedback.
1234
- 7. Capture screenshots and revise what actually renders.
1235
-
1236
- ## Ownership boundary
1237
-
1238
- The main `jgengine` skill owns intake, engine architecture, API routing, hooks, commands, state, and verification routing. This skill owns presentation quality.
1239
-
1240
- Read [reference.md](reference.md) when building or reviewing a game interface. It contains the implementation quality bar, layout rules, art-direction template, touch-control requirements, acceptance criteria, and the compact existing React API surface.
1241
-
1242
- ## Non-negotiable defaults
1243
-
1244
- - Active play owns the viewport; no marketing header, page title bar, document scrolling, or website container.
1245
- - Screen placement belongs in the game's `ui/GameUI.tsx` composition layer.
1246
- - Persistent gameplay information is frameless unless a physical/diegetic frame is part of the game's art direction.
1247
- - Instructions are contextual and temporary, not permanent keyboard grids.
1248
- - Mobile controls share input mechanics but not one universal visual skin.
1249
- - Themes change geometry, composition, typography roles, icons, motion, materials, sound, and density—not only colors.
1250
- - Ordinary rounded cards, pill buttons, generic dark modals, and dashboard grids are fallback failures, not defaults.
1251
-
1252
- ## Preview states ship with the UI
1253
-
1254
- Every game ships `src/preview.tsx`: a static default frame (default export, used by the website card) plus a `states` named export (`GamePreviewStates` from `@jgengine/react/preview`) keying named UI states — `stage_1`, `game_over`, `boss_intro` — to components. Build state entries from the game's **real UI components** with fixture snapshots (canned props/state), not redrawn lookalikes; that turns every key into a capturable render test. Capture any state instantly with `bun run shoot <game> --preview <stateKey>` — no sim, no three.js, no hang risk — and use it as the screenshot-critique loop for HUD/menu/overlay work before any full-shell `--mode ui`/`play` glance.
1255
-
1256
- ## Rejection test
1257
-
1258
- Reject and revise the UI when it could be mistaken for a SaaS dashboard, landing page, admin panel, documentation page, or generic emulator overlay.
1259
-
1260
- # JGengine UI — game presentation reference
1261
-
1262
- This reference defines the required visual and interaction quality for JGengine games. The main `jgengine` skill owns engine architecture, hooks, input commands, and routing. This document owns what the interface looks like, how it is composed, how it responds, and how it is verified.
1263
-
1264
- ## The rule
1265
-
1266
- A game must visually own its viewport. It must not resemble a dashboard, landing page, documentation page, or ordinary responsive web app.
1267
-
1268
- HTML and React are valid implementation tools. Website visual grammar is not the default.
1269
-
1270
- ## 1. Start with a concise UI art direction
1271
-
1272
- Before implementing screens, write this short block:
1273
-
1274
- ```md
1275
- UI ART DIRECTION
1276
-
1277
- Player fantasy:
1278
- Emotional tone:
1279
- Shape language:
1280
- Material language:
1281
- Typography roles: display / body / numerical / labels
1282
- Motion language:
1283
- Icon language:
1284
- Sound language:
1285
- Information hierarchy:
1286
- Forbidden patterns:
1287
- ```
1288
-
1289
- Keep it practical. It should directly influence layout, silhouettes, controls, timing, and materials.
1290
-
1291
- Example forbidden patterns:
1292
-
1293
- - generic rounded dashboard cards
1294
- - pill buttons
1295
- - long centered paragraphs during play
1296
- - ordinary two-column form layouts
1297
- - persistent keyboard-instruction grids
1298
- - multiple equally weighted bordered panels
1299
- - generic translucent mobile circles
1300
- - large website-style modals
1301
- - document-flow wrapping used as HUD layout
1302
-
1303
- A theme is not complete when only colors and fonts change. It must also affect composition, geometry, spacing rhythm, borders, icons, animation, sound, information density, terminology, button construction, and touch controls.
1304
-
1305
- ## 2. Screen inventory and hierarchy
1306
-
1307
- Identify the screens the game actually needs:
1308
-
1309
- - boot/loading
1310
- - title or attract screen
1311
- - mode selection
1312
- - onboarding/tutorial
1313
- - gameplay HUD
1314
- - pause
1315
- - settings
1316
- - map/inventory/dialogue where relevant
1317
- - victory/results
1318
- - failure/retry
1319
-
1320
- For each screen, define:
1321
-
1322
- - the player’s primary question
1323
- - the primary action
1324
- - the most important information
1325
- - what can be hidden
1326
- - what belongs in-world instead of in the HUD
1327
-
1328
- ### HUD tiers
1329
-
1330
- **Tier 1 — immediate action and survival**
1331
- Health, timer, current target, ammo, danger, capture state.
1332
-
1333
- **Tier 2 — short-term decisions**
1334
- Objective progress, route progress, cooldowns, combo, pursuit distance.
1335
-
1336
- **Tier 3 — reference information**
1337
- Full map, inventory, controls, schedule, mission details, lore.
1338
-
1339
- Tier 1 is immediately readable. Tier 2 is quieter. Tier 3 is usually hidden until requested.
1340
-
1341
- Do not style every datum as an equally important bordered box.
1342
-
1343
- ## 3. Full-viewport game composition
1344
-
1345
- The active game should behave like an application mode:
1346
-
1347
- - own the full viewport
1348
- - avoid document scrolling
1349
- - avoid site navigation and marketing chrome during play
1350
- - respect safe-area insets
1351
- - keep exit/settings/fullscreen controls minimal
1352
- - separate world, HUD, controls, screens, and system overlays
1353
-
1354
- Recommended layer contract:
1355
-
1356
- ```tsx
1357
- <GamePlayer>
1358
- <WorldLayer />
1359
- <HudLayer />
1360
- <ControlLayer />
1361
- <ScreenLayer />
1362
- <SystemLayer />
1363
- </GamePlayer>
1364
- ```
1365
-
1366
- - `WorldLayer`: game renderer
1367
- - `HudLayer`: non-blocking gameplay information
1368
- - `ControlLayer`: touch/input surfaces
1369
- - `ScreenLayer`: title, pause, settings, tutorial, victory, failure, transitions
1370
- - `SystemLayer`: exit, fullscreen, engine settings, devtools
1371
-
1372
- Do not place unrelated interface pieces into one ordinary DOM flow.
1373
-
1374
- ## 4. Explicit game layout modes
1375
-
1376
- Do not rely on generic responsive wrapping. Compose explicit modes such as:
1377
-
1378
- - desktop-wide
1379
- - desktop-compact
1380
- - mobile-landscape
1381
- - mobile-portrait
1382
-
1383
- A mobile layout is not a shrunken desktop HUD.
1384
-
1385
- On mobile:
1386
-
1387
- - reserve thumb-control zones
1388
- - keep critical HUD out of those zones
1389
- - hide keyboard legends
1390
- - reduce persistent information
1391
- - move Tier 3 information behind contextual panels
1392
- - respect browser and device safe areas
1393
- - support portrait only when intentionally designed
1394
- - otherwise show a polished rotate-device state
1395
-
1396
- All viewport anchoring should live in the game’s top-level UI composition file. Child components own their internal layout, not their screen position.
1397
-
1398
- ### Design-resolution fit (`platforms` + `hudFit`)
1399
-
1400
- Design-resolution fit is on by default for every game: each `HudCanvas` auto-scales from `hudFit.designSize` (default 1600×900) down to the live viewport, clamped by `hudFit.minScale`/`maxScale` (default 0.4–1), so the authored layout shrinks instead of overflowing a phone. No declaration needed. `hudFit.mobile` overrides the fit on compact displays only — tune the phone presentation there instead of hand-rolling media queries. The player's Graphics → UI scale setting multiplies the computed scale on every platform. Declaring `platforms: ["web"]` (without `"mobile"`) opts a desktop-only game out; its compact displays keep the legacy fixed 0.85 zoom.
1401
-
1402
- **Overflow is an error, not a style note.** `HudCanvas` measures every `HudPanel` against the viewport at runtime; offenders land in a `data-hud-overflow` attribute (and a console warning), and `bun run shoot <game> --device mobile` (or `both`) exits non-zero naming the escaping panels. A game is not mobile-done while shoot reports HUD OVERFLOW.
1403
-
1404
- ### Phase-gated HUD visibility (`showDuring`)
1405
-
1406
- `HudCanvas` and `HudPanel` take an opt-in `showDuring?: GamePhase[]` — the element renders only while `gamePhase` is one of the listed phases (`"menu" | "playing" | "paused" | "ended"`), so a non-cartridge game hides its HUD under menu/end overlays without hand-rolling a phase check. Omit it for the default always-visible behavior. Gate the whole HUD with `<HudCanvas showDuring={["playing"]}>`, or keep a single results panel up with a per-`HudPanel` value. The read degrades to `"playing"` when the component renders outside a `GameProvider` (component showcases, previews), so it never throws there.
1407
-
1408
- ### Shared viewport allocation (`GameViewportProvider`, region collision)
1409
-
1410
- The shell allocates the live viewport once — visual viewport, safe-area insets, orientation, and layout mode — and hosts a **layout registry** every UI subsystem publishes its occupied rectangle to. This replaces "every subsystem independently claims an edge" (the mobile-overlap smell). It is wired automatically for every game; author against it, don't rebuild it.
1411
-
1412
- - **The mode** is one of `desktop-wide | desktop-compact | mobile-landscape | mobile-portrait`, resolved from the live `visualViewport` + coarse-pointer + `platforms`. Read it with `useGameLayoutMode()` (or `useGameViewportLayout()` for the full geometry: `mode`, `orientation`, `safeArea`, `controlZones`, `gameplayRect`). Both degrade to a sane desktop default outside the provider, so previews never throw.
1413
- - **Live CSS variables** are published on the provider root: `--jg-viewport-*`, `--jg-visual-viewport-*`, `--jg-safe-{top,right,bottom,left}`. Use them (with `100dvh` fallbacks) instead of assuming `100vh` equals the visible area on mobile Safari.
1414
- - **Touch controls reserve real rectangles.** The engine measures the joystick / action-cluster / utility zones at runtime (ResizeObserver, not guessed percentages) and registers them as `control` regions. `HudCanvas` already lifts bottom-anchored panels above the dock via `--jg-hud-dock-clearance`; the registry additionally makes any residual overlap a hard error.
1415
- - **Collision is detected, not hoped for.** A `HudPanel` inside a `HudCanvas` registers as a `hud` region automatically. `bun run shoot <game> --device mobile` / `mobile-landscape` reads a `data-jg-layout-collision` attribute and exits non-zero naming both colliding regions (e.g. `throttle ∩ radio (4270px²)`) — the same failure discipline as HUD overflow. In dev the colliding elements also carry `data-jg-collision` for outlining. Opt an intentional overlap out with `allowOverlapWith` / `collisionGroup`, or a soft `mobileBehavior="transient"`.
1416
-
1417
- ### HUD priority + mobile behavior (`HudPanel`)
1418
-
1419
- Declare intent, not just CSS. `HudPanel` accepts:
1420
-
1421
- - `priority?: "critical" | "secondary" | "tertiary"` — the Tier from §2, surfaced to tooling. Critical stays visible; tertiary folds away first.
1422
- - `mobileBehavior?: "persistent" | "compact" | "icon" | "transient" | "hidden" | "sheet" | "modal"` — `"hidden"` unmounts the panel on phones (move that Tier-3 readout behind a contextual action instead); `"transient"` softens its collision policy for a fleeting line; the rest tag the element (`data-hud-mobile-behavior`) for the game's own responsive CSS. Keep these game-authored and lightly styled — this is a placement contract, not a website design system.
1423
-
1424
- The game still owns the genre-appropriate composition; the engine owns the geometry and the failure gate.
1425
-
1426
- ### Mandatory orientation (`orientation`)
1427
-
1428
- A game declares its phone-orientation contract on `defineGame({ orientation })`:
1429
-
1430
- - Legacy `"landscape"` / `"portrait"` stays **advisory** — a dismissible rotate hint, never a gate.
1431
- - The object form `{ mobile: <rule> }` is the strict contract, where `<rule>` is `any` (both) · `portrait` / `landscape` (advisory preference) · `portrait-required` / `landscape-required` (hard gate) · `unsupported` (no phone support).
1432
-
1433
- For a driving/landscape game: `orientation: { mobile: "landscape-required" }`. When the device is held the wrong way the shell shows an **engine-owned `RotateDeviceScreen`** (polished, safe-area-aware, `visualViewport`-sized, reduced-motion-respecting, themeable via `--jg-*`) above every layer, **suppresses game input, freezes the simulation, and unmounts the HUD and touch controls** — gameplay never runs behind the gate. The gate is derived live from orientation, so rotating back to a valid orientation resumes automatically with no stuck-paused state. Never re-implement a "rotate for best experience" toast; declare the requirement and the engine owns the rest.
1434
-
1435
- ## 5. Change the visual grammar
1436
-
1437
- Avoid making every element the same rounded translucent rectangle.
1438
-
1439
- Use genre-appropriate structures:
1440
-
1441
- - clipped corners
1442
- - irregular silhouettes
1443
- - image-backed frames
1444
- - mechanical plates
1445
- - radial interfaces
1446
- - ribbons and tabs
1447
- - gauges and meters
1448
- - emblems and decorative corners
1449
- - notches and edge anchors
1450
- - diegetic objects
1451
- - world-space prompts
1452
- - asymmetrical compositions
1453
- - masks, textures, layered borders, and strong focal elements
1454
-
1455
- Practical rule: no more than roughly 20% of a normal gameplay screen should resemble an ordinary web card or modal.
1456
-
1457
- Every persistent panel must justify why it exists, remains visible, has that shape, and occupies that position.
1458
-
1459
- ## 6. Game UI primitives
1460
-
1461
- Prefer small headless or lightly styled primitives over a giant universal design system. Useful concepts include:
1462
-
1463
- - `HudAnchor`
1464
- - `StatReadout`
1465
- - `Meter`
1466
- - `ObjectiveTracker`
1467
- - `ActionPrompt`
1468
- - `Reticle`
1469
- - `MinimapFrame`
1470
- - `DialoguePlate`
1471
- - `Countdown`
1472
- - `BossBar`
1473
- - `ItemPickup`
1474
- - `DamageIndicator`
1475
- - `PauseScreen`
1476
- - `ResultsScreen`
1477
- - `VirtualControlZone`
1478
- - `ScreenTransition`
1479
-
1480
- A primitive should expose game-oriented choices such as shape, material, urgency, placement, hierarchy, entry motion, icon treatment, compactness, and diegetic-versus-overlay presentation.
1481
-
1482
- **Minimap** is already shipped, not hand-rolled: `Minimap` / `WorldMap` / `Compass` from `@jgengine/react/map` render a framed SVG minimap over a `MarkerSet` (`createMarkerSet`, `@jgengine/core/world/markers`) and optional `FogField`, projecting via `@jgengine/core/world/minimap` (`projectToMinimap`, `headingToBearing`, edge-clamp). A game feeds flat props (`markers`, `center: [x,z]`, `worldRadius`, `size`, `facingYaw`, `rotate`) and repopulates the marker set each HUD tick from live entities — no custom canvas painter. **Swing timer**: `swingTimerState(player, target, prevPeriod, prevTimer)` (`@jgengine/core/ui/swingTimer`) is the pure, parameter-in/next-state-out core for a melee swing bar (recovers period on the reset edge; hidden unless auto-attacking a live non-object target) — thread the returned `nextPeriod`/`nextTimer` back via refs, render a thin `Meter`.
1483
-
1484
- Do not create a primitive whose only value is wrapping a `div` with border radius.
1485
-
1486
- ## 7. Complete interaction states
1487
-
1488
- Every interactive element needs intentional states:
1489
-
1490
- - rest
1491
- - hover where applicable
1492
- - keyboard/controller focus
1493
- - pressed
1494
- - selected
1495
- - disabled
1496
- - success
1497
- - failure
1498
- - warning
1499
-
1500
- Do not communicate all states with background-color changes alone. Use appropriate combinations of:
1501
-
1502
- - scale compression
1503
- - position shift
1504
- - edge or glow response
1505
- - mask movement
1506
- - icon movement
1507
- - text response
1508
- - brief particles
1509
- - sound
1510
- - haptics when supported
1511
- - controlled shake only when appropriate
1512
-
1513
- Focus must look authored while remaining accessible. Menus should support keyboard/controller-style focus navigation when practical.
1514
-
1515
- ## Rendering — post-processing, lighting, shadows
1516
-
1517
- A cinematic look is opt-in engine config, never hand-wired render passes. Set `defineGame({ postProcessing })` (`PostProcessingConfig` from `@jgengine/core/render/postProcessing`) and the shell mounts an `EffectComposer` and owns the render: RenderPass → AO → Bloom → tone-map output → Grade. Absent means the renderer draws directly (unchanged), so it never imposes a look on games that don't ask. Each stage is a config object, `false` to skip, or omitted for its tuned default:
1518
-
1519
- - `toneMapping: "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none"` (default `aces`) + `exposure`.
1520
- - `bloom: { strength, radius, threshold }` — HDR glow around bright pixels (defaults 0.32 / 0.55 / 0.85).
1521
- - `grade: { lift, gain, gamma, saturation, vignette, grain }` — display-space colour grade: cool-shadow/warm-highlight split, vignette, animated film grain.
1522
- - `ao: { radius, intensity, distanceFalloff, blend }` — ground-truth ambient occlusion; heavier than the rest, omit or `false` on low-end targets.
1523
-
1524
- **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.
1525
-
1526
- Lighting is `defineGame({ lighting })` (`LightingConfig`): ambient / hemisphere / directional, replacing the shell's default lights when set. A `DirectionalLightingConfig` with `castShadow` takes `shadowMapSize`, `shadowCameraSize`, `shadowBias`, `shadowNormalBias` for crisp contact shadows. Sky-lit worlds (a `sky()` world feature) get a high-res sun whose shadow camera follows the view each frame, so grounded shadows stay sharp under the player anywhere in a large world.
1527
-
1528
- ## Settings menu
1529
-
1530
- **Settings menu (themed, four layouts, no forced chrome).** The engine builds the whole menu for free — Sound (master + per-bus volume), Graphics (quality/dpr + shadows), Gameplay (FOV slider, default 40–120), Controls (per-action key rebinding, inline click-to-rebind, persisted) — from the game's `audio.buses` and `input` map. What it does **not** do is bolt a fixed gear onto every game: **there is no auto trigger.** You place the entry yourself so it lives *inline with your game's own UI*, never a stray corner overlay. Drop `<SettingsTrigger className=…>` (from `@jgengine/react`) anywhere in your HUD or menu — headless button, `className` for skin/placement, optional `children` to replace the default gear glyph, renders nothing when there's nothing to show. Or call `useSettings().open()` from your own control. Tune the menu via `defineGame({ settings })` (`GameSettingsConfig` from `@jgengine/core/settings/settingsModel`):
1531
-
1532
- - `variant: "panel" | "sheet" | "sidebar" | "fullscreen"` — the layout + skin (default `panel`; `sheet` is the mobile bottom-sheet). All four are fixed-size (no shrink-to-content jitter) and read the game's `--jg-*` theme tokens, falling back to a neutral dark skin.
1533
- - `actions: SettingsActionDef[]` — game-state actions (Restart, Quit to menu, …). They become the **first "Game" tab, shown before anything else** — the home for buttons that used to float over the HUD. Each: `{ id, label, kind?: "default"|"danger", description?, run(ctx) }`; the menu closes right after `run`.
1534
- - `hideBindings: string[]` — input actions to drop from the rebindable Controls list. A game-state key like `restart` belongs in `actions`, not the rebind grid — hide it here so it stops showing up as a "rebindable" control.
1535
- - `surface: "quick"` — additionally mount compact on-screen volume/graphics buttons. Omit for none. `settings: false` — off entirely.
1536
- - `extra: GameSettingDef[]` — append rows to any category, built-in *or a brand-new one* named by `category` (any string). Each row: `{ id, label, category, kind: "slider"|"toggle"|"select", default, min?, max?, step?, options?, onChange?(value, ctx) }`.
1537
- - `categories: SettingCategoryDef[]` — declare custom category tabs, or relabel/reorder built-ins (`{ id, label, order? }`).
1538
- - `hide: SettingCategory[]` — drop built-in categories.
1539
-
1540
- **Game-state controls go in `actions`, never a floating button.** Restart/quit/new-game buttons stapled to the bottom of the HUD are the anti-pattern — declare them as `actions` (first Game tab) and place a `<SettingsTrigger>` inline. A contextual button on a win/lose *results* card is fine; a persistent game-state button pinned over live play is not.
1541
-
1542
- **Present it any way you want.** `useSettings()` (`@jgengine/react`) returns the live controller — `{ categories, actions, variant, surface, isOpen, open, close, setOpen }` — so a game can drive its own pause-menu button, or render `categories`/`actions` (rows carry `value`/`set`/bounds, keybinds carry `rebind`/`reset`) entirely inside its own HUD. `useHasSettings()` gates a custom entry; `useSetting(id, fallback)` reads/writes one value. Set a slider's `min`/`max` explicitly — an omitted range collapses the thumb to 0/1.
1543
-
1544
- ## 8. Motion and game feel
1545
-
1546
- Add purposeful motion for:
1547
-
1548
- - screen entry and exit
1549
- - confirm and cancel
1550
- - warnings
1551
- - score increases
1552
- - objective updates
1553
- - damage
1554
- - victory and failure
1555
- - countdowns
1556
- - pause
1557
- - item pickup
1558
-
1559
- Motion should be brief, readable, interruptible when necessary, coordinated, consistent with the game’s art direction, and respectful of reduced-motion settings.
1560
-
1561
- Do not animate everything constantly. Motion communicates hierarchy, cause and effect, urgency, and state changes.
1562
-
1563
- ## 9. Mobile controls are genre-authored
1564
-
1565
- Shared input mechanics may remain shared. Their visual treatment and arrangement must match the game.
1566
-
1567
- Examples:
1568
-
1569
- **Driving**
1570
- Steering region or wheel, accelerator, brake, handbrake, optional camera/map control.
1571
-
1572
- **Stealth**
1573
- Movement zone, sneak/crouch hold, contextual interaction, temporary map/schedule control.
1574
-
1575
- **Shooter**
1576
- Movement zone, aim region, fire/action cluster, weapon or ability controls.
1577
-
1578
- **Puzzle/arcade**
1579
- Direct drag, tap, swipe, paddle region, or discrete directions. Do not add a joystick without a gameplay reason.
1580
-
1581
- Requirements:
1582
-
1583
- - never cover critical HUD information
1584
- - use the game’s shape and material language
1585
- - fade training labels after learning
1586
- - consider thumb reach
1587
- - preserve accessible target sizes
1588
- - visually respond to activation
1589
- - support optional scaling where appropriate
1590
-
1591
- Do not use one generic translucent controller across all games.
1592
-
1593
- **`presentation: "hud"` games get 3D parity.** A pure-HUD game (no camera rig) now reaches the same input/audio seams as a 3D game:
1594
- - **Touch gestures** — the shell mounts a headless `TouchPlaySurface` in the hud branch too, so `touch.gestures` (swipe/tap) reach actions without the game hand-wiring pointer events on its own canvas. The visible dock stays game-authored per the rule above.
1595
- - **No phantom reservations** — camera action names (`turnLeft`/`turnRight`/`interact`/…) are reserved *only* when a camera rig is active, so a hud game may bind them directly instead of renaming to `steer*`.
1596
- - **Audio actually plays** — audio resumes on the first pointer gesture in hud games (not just 3D), and `playOneShot` self-resumes the suspended context. Trigger sound from anywhere holding `ctx` via `ctx.game.audio.play(soundId, at?)` / `ctx.game.audio.resume()` — the reachable seam over the shell's audio engine.
1597
-
1598
- ## 10. Progressive instruction
1599
-
1600
- Do not leave large control grids visible during gameplay.
1601
-
1602
- Prefer:
1603
-
1604
- - contextual prompts
1605
- - brief onboarding
1606
- - first-use hints
1607
- - a controls screen
1608
- - pause-menu reference
1609
- - icons attached to actions
1610
- - progressive disclosure
1611
-
1612
- Desktop keyboard legends must not appear on touch devices. Prompts should appear near the relevant action, object, or HUD region and clear when no longer useful.
1613
-
1614
- ## 11. Reference directions for flagship games
1615
-
1616
- ### Clockwork Heist
1617
-
1618
- Use gentleman-thief mechanical field-kit language: watch geometry, midnight enamel, aged brass, ivory paper, engraved labels, mechanical shutters, and an authored schedule timeline. The timer should feel like a clock. The schedule should be an on-demand pocket-watch or dossier panel. Touch controls should use brass/enamel construction. Restart belongs in pause, not permanently over the world.
1619
-
1620
- ### Canyon Chase
1621
-
1622
- Use desert pursuit language: battered dashboard, analog instruments, radio display, route strip, warning lamps, and road-sign typography. Target gap should read as a pursuit gauge. Border progress should resemble an odometer, route strip, or mile marker. Driving controls should feel like steering, throttle, brake, and handbrake—not generic circles.
1623
-
1624
- ### Brick Breaker
1625
-
1626
- Use arcade-cabinet language: bezel framing, CRT/vector treatment, arcade numerical readouts, attract mode, launch feedback, and brief level overlays that clear before play. Pause and results should feel like cabinet states, not web dialogs.
1627
-
1628
- These games must remain structurally distinct, not recolors of one component set.
1629
-
1630
- ## 12. Accessibility and performance
1631
-
1632
- Maintain:
1633
-
1634
- - sufficient contrast
1635
- - readable text size
1636
- - keyboard navigation
1637
- - controller navigation where available
1638
- - reduced-motion support
1639
- - visible authored focus
1640
- - touch target sizing
1641
- - semantic labels where practical
1642
- - responsive scaling
1643
- - reasonable DOM and animation performance
1644
-
1645
- Use blur, masks, textures, filters, and full-screen effects carefully, especially on mobile.
1646
-
1647
- ## 13. Screenshot verification is mandatory
1648
-
1649
- Capture and inspect meaningful states:
1650
-
1651
- - desktop title screen
1652
- - desktop gameplay
1653
- - mobile landscape
1654
- - mobile portrait where supported
1655
- - pause
1656
- - victory or failure
1657
- - an interaction prompt
1658
- - touch controls in active use
1659
-
1660
- Inspect for:
1661
-
1662
- - overlap and clipping
1663
- - weak contrast
1664
- - unreadable scale
1665
- - excessive cards
1666
- - website-like composition
1667
- - broken safe areas
1668
- - conflicting hierarchy
1669
- - browser chrome interference
1670
- - poor thumb reach
1671
- - keyboard instructions on touch devices
1672
- - inconsistent art direction
1673
-
1674
- Revise after inspection. Typechecking is not visual proof.
1675
-
1676
- ## 14. Rejection criteria
1677
-
1678
- Require revision when any of these are true:
1679
-
1680
- - normal site navigation remains visible during active gameplay
1681
- - the player must scroll the page to use the game
1682
- - the game is presented inside a normal content card
1683
- - essential HUD is covered by touch controls
1684
- - controls overlap each other
1685
- - keyboard instructions appear on touch devices
1686
- - large instruction panels remain visible during gameplay
1687
- - more than three ordinary card-like panels are persistently visible
1688
- - the main action looks like a standard website button
1689
- - pause resembles a generic website modal
1690
- - victory/failure is only text plus restart
1691
- - restart is permanently visible without a gameplay reason
1692
- - menu elements lack pressed, focused, selected, or disabled states
1693
- - UI changes have no transition or feedback
1694
- - generic virtual controls are used without genre adaptation
1695
- - the theme only changes colors and fonts
1696
- - unrelated UI elements have equal visual weight
1697
- - mobile portrait technically fits but is not intentionally composed
1698
- - important UI sits beneath safe areas
1699
- - ordinary document flow determines the HUD layout
1700
- - generic default styling is used because no art direction was written
1701
-
1702
- ## 15. Compact implementation API appendix
1703
-
1704
- Use the main `jgengine` skill for the authoritative engine API routing. The React package exposes `GameProvider`, hooks, and headless primitives from `@jgengine/react` and its documented subpaths. Common hooks include player/game state, entities, stats, inventory, quests, prompts, clocks, markers, fog, and engine stores. The shell provides `GamePlayerShell`, input integration, devtools, and `GameUiPreview`.
1705
-
1706
- Use these APIs to bind state; do not let API wiring dictate visual composition. Keybind labels should derive from the game’s binding table, and UI actions should dispatch through game commands rather than existing only as click handlers.
1707
-
1708
- Headless components are not finished design. They are behavior and accessibility seams that the game must art-direct.
1709
-
1710
- ## Definition of done
1711
-
1712
- UI work is complete only when:
1713
-
1714
- - the game owns the viewport
1715
- - site chrome is absent during active play
1716
- - no document scrolling is required
1717
- - HUD and control layers have clear responsibilities
1718
- - mobile controls do not cover critical content
1719
- - touch controls match the genre and game identity
1720
- - title, pause, and results screens feel authored
1721
- - interaction states and transitions are present
1722
- - screenshots have been reviewed and revised
1723
- - the implementation remains accessible and performant
1724
- - future generated UI is explicitly prevented from falling back to generic website-card styling