@jgengine/react 0.8.0 → 0.10.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.
- package/CHANGELOG.md +36 -0
- package/dist/chat.d.ts +3 -3
- package/dist/chat.js +14 -5
- package/dist/chatBubbles.d.ts +15 -0
- package/dist/chatBubbles.js +46 -0
- package/dist/components.d.ts +1 -0
- package/dist/components.js +36 -23
- package/dist/display.d.ts +2 -1
- package/dist/display.js +3 -2
- package/dist/dragLayer.d.ts +6 -1
- package/dist/dragLayer.js +64 -31
- package/dist/engineStore.d.ts +1 -1
- package/dist/engineStore.js +10 -2
- package/dist/fogOverlay.d.ts +21 -0
- package/dist/fogOverlay.js +34 -0
- package/dist/gameViewport.d.ts +54 -0
- package/dist/gameViewport.js +340 -0
- package/dist/hooks.d.ts +11 -1
- package/dist/hooks.js +89 -21
- package/dist/hudLayout.d.ts +79 -0
- package/dist/hudLayout.js +518 -0
- package/dist/hudViewport.d.ts +21 -0
- package/dist/hudViewport.js +17 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/liveBind.d.ts +4 -10
- package/dist/liveBind.js +28 -16
- package/dist/map.d.ts +27 -7
- package/dist/map.js +152 -43
- package/dist/preview.d.ts +6 -0
- package/dist/preview.js +1 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +4 -0
- package/dist/rotateDevice.d.ts +24 -0
- package/dist/rotateDevice.js +83 -0
- package/dist/selectSnapshot.d.ts +7 -0
- package/dist/selectSnapshot.js +16 -0
- package/dist/settings.d.ts +78 -0
- package/dist/settings.js +61 -0
- package/dist/skillCheckPaint.d.ts +4 -0
- package/dist/skillCheckPaint.js +28 -0
- package/dist/social.d.ts +3 -3
- package/dist/social.js +31 -10
- package/llms.txt +858 -860
- package/package.json +9 -5
package/llms.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @jgengine/react
|
|
2
2
|
> React UI layer for JGengine: GameProvider, hooks, and headless primitives over @jgengine/core.
|
|
3
3
|
|
|
4
|
-
Version: 0.
|
|
4
|
+
Version: 0.10.0
|
|
5
5
|
License: AGPL-3.0-only
|
|
6
6
|
Repository: https://github.com/Noisemaker111/jgengine
|
|
7
7
|
Docs: https://jgengine.com
|
|
@@ -11,305 +11,487 @@ Imports use deep paths: `@jgengine/react/<path>`.
|
|
|
11
11
|
|
|
12
12
|
### @jgengine/react/chat
|
|
13
13
|
|
|
14
|
-
- 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.
|
|
15
|
-
- ChatLog (function): function ChatLog({ channelId, limit, className, messageClassName, renderMessage, }: { channelId: string; limit?: number; className?: string; messageClassName?: string; renderMessage?: (message: ChatMessage) => ReactNode; }): React.JSX.Element
|
|
16
|
-
- 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;…
|
|
17
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
|
|
18
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
|
|
19
27
|
|
|
20
28
|
### @jgengine/react/components
|
|
21
29
|
|
|
22
|
-
- SlotGrid (function): function SlotGrid({ inventoryId, className, renderSlot, }: { inventoryId: string; className?: string; renderSlot?: (slot: InventorySlot, index: number) => ReactNode; }): React.JSX.Element
|
|
23
|
-
- HealthBar (function): function HealthBar({ instanceId, statId, className, fillClassName, }: { instanceId: string; statId: string; className?: string; fillClassName?: string; }): React.JSX.Element | null
|
|
24
|
-
- CurrencyPill (function): function CurrencyPill({ currencyId, className }: { currencyId: string; className?: string }): React.JSX.Element
|
|
25
|
-
- ProximityPrompt (function): function ProximityPrompt({ prompt, className, }: { prompt: ProximityPromptDef; className?: string; }): React.JSX.Element
|
|
26
|
-
- Screen (function): function Screen({ id, open = true, className, children, }: { id: string; open?: boolean; className?: string; children?: ReactNode; }): React.JSX.Element | null
|
|
27
|
-
- KeybindRow (function): function KeybindRow({ action, keys, className, }: { action: string; keys: readonly string[]; className?: string; }): React.JSX.Element
|
|
28
|
-
- resolveDialogueInvoke (function): function resolveDialogueInvoke(choice: DialogueChoice, result: CheckResult | null): { command: string; args?: unknown } | null
|
|
29
|
-
- 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?: …
|
|
30
|
-
- 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…
|
|
31
|
-
- 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
|
|
32
30
|
- CaptureOdds (function): function CaptureOdds({ chance, className, fillClassName, }: { chance: number; className?: string; fillClassName?: string; }): React.JSX.Element
|
|
33
|
-
-
|
|
31
|
+
- CurrencyPill (function): function CurrencyPill({ currencyId, className }: { currencyId: string; className?: string }): React.JSX.Element
|
|
34
32
|
- DeathScreen (function): function DeathScreen({ statId = "health", open, className, children, }: { statId?: string; open?: boolean; className?: string; children?: ReactNode; }): React.JSX.Element
|
|
35
|
-
-
|
|
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?: …
|
|
36
34
|
- DialogueCheck (interface): interface DialogueCheck
|
|
37
35
|
- DialogueChoice (interface): interface DialogueChoice
|
|
38
|
-
- DialogueLine (type): type DialogueLine = { speaker: string; text: string } | { choices: readonly DialogueChoice[] }
|
|
39
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
|
|
40
50
|
|
|
41
51
|
### @jgengine/react/display
|
|
42
52
|
|
|
43
|
-
- useDisplayProfile (function): function useDisplayProfile(): DisplayProfile
|
|
44
53
|
- DisplayProfile (interface): interface DisplayProfile
|
|
54
|
+
- useDisplayProfile (function): function useDisplayProfile(): DisplayProfile
|
|
45
55
|
|
|
46
56
|
### @jgengine/react/dragLayer
|
|
47
57
|
|
|
48
|
-
- useDragLayer (function): function useDragLayer<T>(options?: { onDrop?: (info: DropInfo<T>) => void; }): DragLayer<T>
|
|
49
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
|
|
50
|
-
-
|
|
51
|
-
- 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
|
|
59
|
+
- DragLayer (interface): interface DragLayer<T>
|
|
52
60
|
- DragPayload (interface): interface DragPayload<T>
|
|
53
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
|
|
54
63
|
- DropInfo (interface): interface DropInfo<T>
|
|
55
|
-
-
|
|
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>
|
|
56
66
|
|
|
57
67
|
### @jgengine/react/engineStore
|
|
58
68
|
|
|
59
|
-
- useEngineState (function): function useEngineState<TState>(store: ReadableEngineStore<TState>): TState
|
|
60
|
-
- useEngineStore (function): function useEngineStore<TState, TSelected>(store: ReadableEngineStore<TState>, selector: (state: TState) => TSelected): TSelected
|
|
61
|
-
- useEngineEvent (function): function useEngineEvent<TEventMap extends object, K extends keyof TEventMap>(store: EventfulEngineStore<TEventMap>, eventName: K, handler: (payload: TEventMap[K]) => void): void
|
|
62
|
-
- ReadableEngineStore (interface): interface ReadableEngineStore<TState>
|
|
63
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
|
|
64
82
|
|
|
65
83
|
### @jgengine/react/gameIcons
|
|
66
84
|
|
|
67
|
-
- isGameIconName (function): function isGameIconName(value: string): value is GameIconName
|
|
68
|
-
- GameIcon (function): function GameIcon({ name, size = 24, color, className, }: { name: GameIconName; size?: number; color?: string; className?: string; }): React.JSX.Element
|
|
69
|
-
- iconForItemId (function): function iconForItemId(itemId: string): GameIconName | null
|
|
70
|
-
- iconForAction (function): function iconForAction(action: string): GameIconName | null — Control glyph for a semantic action name (`hardDrop`, `sprint`, `shiftLeft`), or null when no rule matches.
|
|
71
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
|
|
72
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.
|
|
73
105
|
|
|
74
106
|
### @jgengine/react/hooks
|
|
75
107
|
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
-
|
|
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[]
|
|
86
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
|
|
87
124
|
- useFeed (function): function useFeed({ action, limit }: { action: string; limit?: number }): FeedEntry[]
|
|
88
|
-
-
|
|
125
|
+
- useFriendRequests (function): function useFriendRequests(): FriendRequestEntry[]
|
|
89
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.
|
|
90
137
|
- useParty (function): function useParty(): PartyMemberEntry[]
|
|
91
|
-
- usePresence (function): function usePresence(userId: string): PresenceInfo
|
|
92
|
-
- useWorldInvites (function): function useWorldInvites(): WorldInvite[]
|
|
93
|
-
- useChat (function): function useChat(channelId: string, options?: { limit?: number }): ChatMessage[]
|
|
94
|
-
- useFriendRequests (function): function useFriendRequests(): FriendRequestEntry[]
|
|
95
138
|
- usePartyInvites (function): function usePartyInvites(): PartyInviteEntry[]
|
|
96
|
-
-
|
|
139
|
+
- usePlayer (function): function usePlayer(): { userId: string; isNew: boolean }
|
|
140
|
+
- usePresence (function): function usePresence(userId: string): PresenceInfo
|
|
141
|
+
- useQuestJournal (function): function useQuestJournal(): QuestInstance[]
|
|
97
142
|
- useRoster (function): function useRoster(userId?: string): readonly RosterEntry[]
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
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
|
|
113
164
|
|
|
114
165
|
### @jgengine/react/identity
|
|
115
166
|
|
|
116
|
-
-
|
|
117
|
-
-
|
|
118
|
-
-
|
|
167
|
+
- BetterAuthSessionState (interface): interface BetterAuthSessionState
|
|
168
|
+
- BetterAuthUserShape (interface): interface BetterAuthUserShape
|
|
169
|
+
- ClerkUserShape (interface): interface ClerkUserShape
|
|
170
|
+
- ClerkUserState (interface): interface ClerkUserState
|
|
119
171
|
- GameIdentityProvider (function): function GameIdentityProvider({ source, children, }: { source: IdentitySource; children?: ReactNode; }): React.JSX.Element
|
|
120
|
-
-
|
|
121
|
-
- useAuthedPlayer (function): function useAuthedPlayer(options?: { guestSeed?: string }): PlayerIdentity | null
|
|
172
|
+
- IdentitySource (interface): interface IdentitySource
|
|
122
173
|
- RequireSession (function): function RequireSession({ fallback, loading, children, }: { fallback?: ReactNode; loading?: ReactNode; children?: ReactNode; }): React.JSX.Element
|
|
123
|
-
- UserBadge (function): function UserBadge({ className, avatarClassName, nameClassName, renderBadge, }: { className?: string; avatarClassName?: string; nameClassName?: string; renderBadge?: (session: AuthSession) => ReactNode; }): React.JSX.Element | null
|
|
124
174
|
- SignOutButton (function): function SignOutButton({ className, children, }: { className?: string; children?: ReactNode; }): React.JSX.Element | null
|
|
125
|
-
-
|
|
126
|
-
- ClerkUserShape (interface): interface ClerkUserShape
|
|
127
|
-
- ClerkUserState (interface): interface ClerkUserState
|
|
128
|
-
- BetterAuthUserShape (interface): interface BetterAuthUserShape
|
|
129
|
-
- BetterAuthSessionState (interface): interface BetterAuthSessionState
|
|
130
|
-
|
|
131
|
-
### @jgengine/react/index
|
|
132
|
-
|
|
133
|
-
- GameProvider (function): function GameProvider({ context, children }: { context: GameContext; children?: ReactNode }): React.JSX.Element
|
|
134
|
-
- useGameContext (function): function useGameContext(): GameContext
|
|
135
|
-
- clerkIdentity (function): function clerkIdentity(state: ClerkUserState, options?: { signOut?: () => void }): IdentitySource
|
|
175
|
+
- UserBadge (function): function UserBadge({ className, avatarClassName, nameClassName, renderBadge, }: { className?: string; avatarClassName?: string; nameClassName?: string; renderBadge?: (session: AuthSession) => ReactNode; }): React.JSX.Element | null
|
|
136
176
|
- betterAuthIdentity (function): function betterAuthIdentity(state: BetterAuthSessionState, options?: { signOut?: () => void }): IdentitySource
|
|
177
|
+
- clerkIdentity (function): function clerkIdentity(state: ClerkUserState, options?: { signOut?: () => void }): IdentitySource
|
|
137
178
|
- guestIdentity (function): function guestIdentity(seed?: string): IdentitySource
|
|
138
|
-
- GameIdentityProvider (function): function GameIdentityProvider({ source, children, }: { source: IdentitySource; children?: ReactNode; }): React.JSX.Element
|
|
139
|
-
- useSession (function): function useSession(): IdentitySource
|
|
140
179
|
- useAuthedPlayer (function): function useAuthedPlayer(options?: { guestSeed?: string }): PlayerIdentity | null
|
|
141
|
-
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
-
|
|
146
|
-
-
|
|
147
|
-
- BetterAuthUserShape (interface): interface BetterAuthUserShape
|
|
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
|
|
148
186
|
- BetterAuthSessionState (interface): interface BetterAuthSessionState
|
|
149
|
-
-
|
|
150
|
-
-
|
|
151
|
-
- 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;…
|
|
187
|
+
- BetterAuthUserShape (interface): interface BetterAuthUserShape
|
|
188
|
+
- CaptureOdds (function): function CaptureOdds({ chance, className, fillClassName, }: { chance: number; className?: string; fillClassName?: string; }): React.JSX.Element
|
|
152
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
|
|
153
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…
|
|
154
|
-
-
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
-
|
|
158
|
-
- VoiceRoster (function): function VoiceRoster({ voice, className, participantClassName, renderParticipant, }: { voice: VoiceState; className?: string; participantClassName?: string; renderParticipant?: (participant: VoiceParticipant, gain: number) => ReactNode; }): React.JSX.Element
|
|
159
|
-
- UseVoiceOptions (interface): interface UseVoiceOptions
|
|
160
|
-
- VoiceState (interface): interface VoiceState
|
|
161
|
-
- PresenceDot (function): function PresenceDot({ userId, className }: { userId: string; className?: string }): React.JSX.Element
|
|
162
|
-
- FriendRow (function): function FriendRow({ friend, className, dotClassName, children, }: { friend: FriendEntry; className?: string; dotClassName?: string; children?: ReactNode; }): React.JSX.Element
|
|
163
|
-
- FriendsList (function): function FriendsList({ className, rowClassName, dotClassName, emptyState, renderFriend, }: { className?: string; rowClassName?: string; dotClassName?: string; emptyState?: ReactNode; renderFriend?: (friend: FriendEntry) => ReactNode; }): React.JSX.Element
|
|
164
|
-
- 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
|
|
165
|
-
- FriendRequestsList (function): function FriendRequestsList({ className, rowClassName, acceptClassName, declineClassName, emptyState, renderRequest, }: { className?: string; rowClassName?: string; acceptClassName?: string; declineClassName?: string; emptyState?: ReactNode; renderRequest?: (request: FriendRequestEntry) => ReactNode…
|
|
166
|
-
- PartyMemberRow (function): function PartyMemberRow({ member, className, dotClassName, children, }: { member: PartyMemberEntry; className?: string; dotClassName?: string; children?: ReactNode; }): React.JSX.Element
|
|
167
|
-
- PartyFrame (function): function PartyFrame({ className, rowClassName, dotClassName, emptyState, renderMember, }: { className?: string; rowClassName?: string; dotClassName?: string; emptyState?: ReactNode; renderMember?: (member: PartyMemberEntry) => ReactNode; }): React.JSX.Element
|
|
168
|
-
- PartyInviteToast (function): function PartyInviteToast({ className, acceptClassName, declineClassName, renderInvite, }: { className?: string; acceptClassName?: string; declineClassName?: string; renderInvite?: (invite: PartyInviteEntry) => ReactNode; }): React.JSX.Element | null
|
|
169
|
-
- LeavePartyButton (function): function LeavePartyButton({ className, children, }: { className?: string; children?: ReactNode; }): React.JSX.Element | null
|
|
170
|
-
- 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 …
|
|
171
|
-
- 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
|
|
172
|
-
- 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?:…
|
|
173
|
-
- 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
|
|
174
|
-
- 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
|
|
175
|
-
- 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; …
|
|
176
|
-
- useGameStore (function): function useGameStore<T>(selector: (ctx: GameContext) => T): T
|
|
177
|
-
- useGame (function): function useGame(): { commands: GameContext["game"]["commands"]; events: GameEvents }
|
|
178
|
-
- usePlayer (function): function usePlayer(): { userId: string; isNew: boolean }
|
|
179
|
-
- useSceneEntities (function): function useSceneEntities(): readonly SceneEntity[]
|
|
180
|
-
- useSceneObjects (function): function useSceneObjects(): readonly SceneObject[]
|
|
181
|
-
- useWorldItems (function): function useWorldItems(): readonly WorldItemRecord[]
|
|
182
|
-
- useNearestWorldItem (function): function useNearestWorldItem(radius: number): WorldItemRecord | null — Nearest ground item within `radius` of the local player — drives a pickup prompt/highlight.
|
|
183
|
-
- useEntityStat (function): function useEntityStat(instanceId: string, statId: string): StatValue | null
|
|
184
|
-
- useTarget (function): function useTarget(fromInstanceId: string): string | null
|
|
185
|
-
- useInventory (function): function useInventory(inventoryId: string): readonly InventorySlot[]
|
|
186
|
-
- useCurrency (function): function useCurrency(currencyId: string): number
|
|
187
|
-
- useFeed (function): function useFeed({ action, limit }: { action: string; limit?: number }): FeedEntry[]
|
|
188
|
-
- useQuestJournal (function): function useQuestJournal(): QuestInstance[]
|
|
189
|
-
- useFriends (function): function useFriends(): FriendEntry[]
|
|
190
|
-
- useParty (function): function useParty(): PartyMemberEntry[]
|
|
191
|
-
- usePresence (function): function usePresence(userId: string): PresenceInfo
|
|
192
|
-
- useWorldInvites (function): function useWorldInvites(): WorldInvite[]
|
|
193
|
-
- useChat (function): function useChat(channelId: string, options?: { limit?: number }): ChatMessage[]
|
|
194
|
-
- useFriendRequests (function): function useFriendRequests(): FriendRequestEntry[]
|
|
195
|
-
- usePartyInvites (function): function usePartyInvites(): PartyInviteEntry[]
|
|
196
|
-
- 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.
|
|
197
|
-
- useRoster (function): function useRoster(userId?: string): readonly RosterEntry[]
|
|
198
|
-
- useLeaderboard (function): function useLeaderboard(stat: string, options: { scope: LeaderboardScope; limit?: number }): { userId: string; value: number }[]
|
|
199
|
-
- useLocalPlayerDead (function): function useLocalPlayerDead(healthStatId = "health"): boolean
|
|
200
|
-
- localPlayerEntity (function): function localPlayerEntity(ctx: GameContext): SceneEntity | null
|
|
201
|
-
- useGameClock (function): function useGameClock(): ClockSnapshot & { controls: SimClock }
|
|
202
|
-
- useActivePrompt (function): function useActivePrompt<T extends PositionedPrompt>(prompts?: readonly T[]): T | null
|
|
203
|
-
- useAbilitySlots (function): function useAbilitySlots(kit: AbilityKit, resourceAvailable?: number, options?: AbilitySlotBindingOptions): AbilitySlotSnapshot[]
|
|
204
|
-
- useAbilitySlot (function): function useAbilitySlot(kit: AbilityKit, slotId: string, resourceAvailable?: number, options?: AbilitySlotBindingOptions): AbilitySlotSnapshot | null
|
|
205
|
-
- useEventMeter (function): function useEventMeter(meter: EventMeter, options?: AbilitySlotBindingOptions): EventMeterView
|
|
206
|
-
- createHeldKeyTracker (function): function createHeldKeyTracker(target: HeldKeyEventTarget): { isDown: (code: string) => boolean; dispose: () => void; }
|
|
207
|
-
- 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.
|
|
208
|
-
- 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.
|
|
209
|
-
- WorldBrowserState (interface): interface WorldBrowserState
|
|
210
|
-
- AbilitySlotBindingOptions (interface): interface AbilitySlotBindingOptions
|
|
211
|
-
- EventMeterView (interface): interface EventMeterView
|
|
212
|
-
- UseAxisChannelResult (interface): interface UseAxisChannelResult
|
|
213
|
-
- SlotGrid (function): function SlotGrid({ inventoryId, className, renderSlot, }: { inventoryId: string; className?: string; renderSlot?: (slot: InventorySlot, index: number) => ReactNode; }): React.JSX.Element
|
|
214
|
-
- HealthBar (function): function HealthBar({ instanceId, statId, className, fillClassName, }: { instanceId: string; statId: string; className?: string; fillClassName?: string; }): React.JSX.Element | null
|
|
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
|
|
215
199
|
- CurrencyPill (function): function CurrencyPill({ currencyId, className }: { currencyId: string; className?: string }): React.JSX.Element
|
|
216
|
-
- ProximityPrompt (function): function ProximityPrompt({ prompt, className, }: { prompt: ProximityPromptDef; className?: string; }): React.JSX.Element
|
|
217
|
-
- Screen (function): function Screen({ id, open = true, className, children, }: { id: string; open?: boolean; className?: string; children?: ReactNode; }): React.JSX.Element | null
|
|
218
|
-
- KeybindRow (function): function KeybindRow({ action, keys, className, }: { action: string; keys: readonly string[]; className?: string; }): React.JSX.Element
|
|
219
|
-
- resolveDialogueInvoke (function): function resolveDialogueInvoke(choice: DialogueChoice, result: CheckResult | null): { command: string; args?: unknown } | null
|
|
220
|
-
- 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?: …
|
|
221
|
-
- 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…
|
|
222
|
-
- 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
|
|
223
|
-
- CaptureOdds (function): function CaptureOdds({ chance, className, fillClassName, }: { chance: number; className?: string; fillClassName?: string; }): React.JSX.Element
|
|
224
|
-
- 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
|
|
225
200
|
- DeathScreen (function): function DeathScreen({ statId = "health", open, className, children, }: { statId?: string; open?: boolean; className?: string; children?: ReactNode; }): React.JSX.Element
|
|
226
|
-
-
|
|
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?: …
|
|
227
202
|
- DialogueCheck (interface): interface DialogueCheck
|
|
228
203
|
- DialogueChoice (interface): interface DialogueChoice
|
|
229
|
-
- DialogueLine (type): type DialogueLine = { speaker: string; text: string } | { choices: readonly DialogueChoice[] }
|
|
230
204
|
- DialogueDef (interface): interface DialogueDef
|
|
231
|
-
-
|
|
232
|
-
-
|
|
233
|
-
- useEngineEvent (function): function useEngineEvent<TEventMap extends object, K extends keyof TEventMap>(store: EventfulEngineStore<TEventMap>, eventName: K, handler: (payload: TEventMap[K]) => void): void
|
|
234
|
-
- ReadableEngineStore (interface): interface ReadableEngineStore<TState>
|
|
235
|
-
- EventfulEngineStore (interface): interface EventfulEngineStore<TEventMap extends object>
|
|
236
|
-
- useDragLayer (function): function useDragLayer<T>(options?: { onDrop?: (info: DropInfo<T>) => void; }): DragLayer<T>
|
|
205
|
+
- DialogueLine (type): type DialogueLine = { speaker: string; text: string } | { choices: readonly DialogueChoice[] }
|
|
206
|
+
- DisplayProfile (interface): interface DisplayProfile
|
|
237
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
|
|
238
|
-
-
|
|
239
|
-
- 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
|
|
208
|
+
- DragLayer (interface): interface DragLayer<T>
|
|
240
209
|
- DragPayload (interface): interface DragPayload<T>
|
|
241
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
|
|
242
212
|
- DropInfo (interface): interface DropInfo<T>
|
|
243
|
-
-
|
|
244
|
-
-
|
|
245
|
-
-
|
|
246
|
-
-
|
|
247
|
-
-
|
|
248
|
-
-
|
|
249
|
-
-
|
|
250
|
-
-
|
|
251
|
-
-
|
|
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
|
|
252
240
|
- MapBounds (interface): interface MapBounds
|
|
253
|
-
-
|
|
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`.
|
|
254
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[]
|
|
255
354
|
|
|
256
355
|
### @jgengine/react/liveBind
|
|
257
356
|
|
|
258
|
-
-
|
|
259
|
-
-
|
|
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
|
|
260
361
|
|
|
261
362
|
### @jgengine/react/map
|
|
262
363
|
|
|
263
|
-
-
|
|
264
|
-
- useFog (function): function useFog(fog: FogField): ReturnType<FogField["cells"]>
|
|
265
|
-
- Minimap (function): function Minimap({ markers, center, worldRadius, fog, size = 176, heading = 0, rotate = false, kindStyles = DEFAULT_MARKER_KINDS, background, mapBounds, 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.
|
|
266
|
-
- Compass (function): function Compass({ heading, 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 bearing, with the eight cardinals and optional marker pips (bearing to each `MarkerSet` entry).
|
|
267
|
-
- WorldMap (function): function WorldMap({ markers, bounds, player, heading = 0, fog, background, width = 520, height, kindStyles = DEFAULT_MARKER_KINDS, 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`.
|
|
268
|
-
- MinimapProps (interface): interface MinimapProps
|
|
269
|
-
- MapBounds (interface): interface MapBounds
|
|
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).
|
|
270
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`.
|
|
271
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
|
|
272
391
|
|
|
273
|
-
|
|
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
|
|
274
395
|
|
|
275
|
-
|
|
276
|
-
|
|
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
|
|
277
415
|
|
|
278
416
|
### @jgengine/react/social
|
|
279
417
|
|
|
280
|
-
-
|
|
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…
|
|
281
421
|
- FriendRow (function): function FriendRow({ friend, className, dotClassName, children, }: { friend: FriendEntry; className?: string; dotClassName?: string; children?: ReactNode; }): React.JSX.Element
|
|
282
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
|
|
283
|
-
-
|
|
284
|
-
-
|
|
285
|
-
-
|
|
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
|
|
286
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
|
|
287
427
|
- PartyInviteToast (function): function PartyInviteToast({ className, acceptClassName, declineClassName, renderInvite, }: { className?: string; acceptClassName?: string; declineClassName?: string; renderInvite?: (invite: PartyInviteEntry) => ReactNode; }): React.JSX.Element | null
|
|
288
|
-
-
|
|
289
|
-
-
|
|
290
|
-
- 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
|
|
291
|
-
- 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?:…
|
|
292
|
-
- 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
|
|
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
|
|
293
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
|
|
294
|
-
-
|
|
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 …
|
|
295
433
|
|
|
296
434
|
### @jgengine/react/voice
|
|
297
435
|
|
|
298
|
-
- 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.
|
|
299
|
-
- PushToTalkButton (function): function PushToTalkButton({ voice, className, children, }: { voice: VoiceState; className?: string; children?: ReactNode; }): React.JSX.Element
|
|
300
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
|
|
301
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
|
|
302
|
-
- VoiceRoster (function): function VoiceRoster({ voice, className, participantClassName, renderParticipant, }: { voice: VoiceState; className?: string; participantClassName?: string; renderParticipant?: (participant: VoiceParticipant, gain: number) => ReactNode; }): React.JSX.Element
|
|
303
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
|
|
304
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.
|
|
305
443
|
|
|
306
444
|
## Guides
|
|
307
445
|
|
|
308
|
-
# JGengine
|
|
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
|
|
309
468
|
|
|
310
|
-
|
|
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:
|
|
311
470
|
|
|
312
|
-
|
|
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.
|
|
313
495
|
|
|
314
496
|
## Packages
|
|
315
497
|
|
|
@@ -343,7 +525,7 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
343
525
|
|---------|----------------------------------|-----------|
|
|
344
526
|
| Game boot | `game/defineGame` | `defineGame`, `GameDefinition`, `GameLoop`, `InventoryDeclaration`, `PhysicsConfig`, `GameServerConfig`, `TimeConfig` |
|
|
345
527
|
| Simulation clock | `time/simClock` | `createSimClock`, `SimClock`, `TimeConfig`, `ClockSnapshot`, `CalendarTime` |
|
|
346
|
-
| Runner contract | `game/playableGame` | `PlayableGame`, `GameCameraConfig`, `CameraRigKind`, `TopDownCameraConfig`, `RtsCameraConfig`, `ShoulderCameraConfig`, `LockOnCameraConfig`, `ChaseCameraConfig`, `ObserverCameraConfig`, `CameraShakeConfig`, `CinematicCameraConfig`, `CameraKeyframe`, `EntitySpriteConfig` |
|
|
528
|
+
| Runner contract | `game/playableGame` | `PlayableGame`, `GameCameraConfig`, `CameraRigKind`, `CameraProjection`, `SideScrollCameraConfig`, `TopDownCameraConfig`, `RtsCameraConfig`, `ShoulderCameraConfig`, `LockOnCameraConfig`, `ChaseCameraConfig`, `ObserverCameraConfig`, `CameraShakeConfig`, `CinematicCameraConfig`, `CameraKeyframe`, `EntitySpriteConfig` |
|
|
347
529
|
| Runtime ctx | `runtime/gameContext` | `createGameContext`, `GameContext`, `GameContextContent`, `GameContextItemEntry`, `GameContextEntityEntry`, `GameContextObjectEntry`, `CatalogEntityRole` |
|
|
348
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` |
|
|
349
531
|
| Reactive keyed store | `store/observableKeyedStore` | `createObservableKeyedStore`, `ObservableKeyedStore` — backs `ctx.game.store` |
|
|
@@ -359,10 +541,13 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
359
541
|
| Loadout | `game/loadout` | `LoadoutDef`, `LoadoutItemEntry`, `Loadouts` |
|
|
360
542
|
| Cosmetic loadout | `game/cosmetics` | `createCosmetics`, `Cosmetics`, `CosmeticLoadoutDef` |
|
|
361
543
|
| Quest | `game/quest` | `QuestDef`, `QuestRewards`, `QuestObjective`, `QuestJournal` |
|
|
362
|
-
| World features | `world/features` | `WorldFeature`, `biomes`, `voxel`, `plots`, `tilemap`, `flat`, `environment`, `terrain`, `rain`, `snow`, `grass`, `ocean`, `building` |
|
|
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 |
|
|
363
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 |
|
|
364
548
|
| Terrain field | `world/terrain` | `TerrainField`, `noiseField`, `resolveTerrainField`, `rollingField`, `fractalNoise`, `valueNoise`, `withNormal`, `arenaField`, `flatField`, `resolveGroundStep`, `snapToGround`, `snapEntityToGround`, `resolveTerrainPalette`, `TERRAIN_MATERIAL_PALETTES` |
|
|
365
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 |
|
|
366
551
|
| Name generator | `random/nameGen` | `createNameGenerator`, `pickFrom`, `fillTemplate`, `NameGenerator`, `NameGeneratorOptions`, `SyllableBank` |
|
|
367
552
|
| Regions | `world/regions` | `createRegionField`, `isRegionField`, `RegionDef`, `RegionField`, `RegionSample` |
|
|
368
553
|
| Wind field | `world/wind` | `windField`, `WindField`, `WindFieldConfig`, `WindVector` |
|
|
@@ -393,6 +578,7 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
393
578
|
| Persistence scopes | `runtime/persistenceScope` | `partitionScopes`, `resetRun`, `mergeScopes`, `clearRunFields`, `applyRunReset`, `planScenarioReset`, `ScopeSchema`, `ScenarioReset`, `PersistenceScope` |
|
|
394
579
|
| Inventory | `inventory/inventoryModel` | `InventoryLayout`, `InventorySet`, `ItemTraits` |
|
|
395
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 |
|
|
396
582
|
| Inventory slots | `inventory/slotModel` | `createSlots`, `placeAt`, `removeAt`, `moveSlot`, `firstEmpty`, `compactSlots`, `Slot`, `SlotGrid` |
|
|
397
583
|
| Shaped inventory | `inventory/shapedGrid` | `createShapedGrid`, `placeShaped`, `moveShaped`, `removeShaped`, `canPlace`, `rotateFootprint`, `occupiedCells`, `gridAdjacencyQuery`, `cellFromPoint`, `ShapedGrid`, `Footprint`, `Placement`, `Rotation` |
|
|
398
584
|
| Card piles | `cards/cardPile` | `createCardPile`, `createCardPileState`, `draw`, `moveCards`, `shuffleZone`, `pileRng`, `CardPile`, `CardPileState`, `CardPileConfig` |
|
|
@@ -410,6 +596,7 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
410
596
|
| Build permissions | `world/buildPermissions` | `createPlotPermissions`, `createContributionPool`, `PlotPermissions`, `ContributionPool`, `BuildRole`, `ContributionGoal` |
|
|
411
597
|
| Interiors | `world/interiors` | `createInteriors`, `Interior`, `Exterior`, `SpaceRef` |
|
|
412
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 |
|
|
413
600
|
| Scene behaviors | `scene/behaviors` | `wander`, `patrol`, `promptable`, `talkable`, `player` |
|
|
414
601
|
| Capture check | `scene/captureCheck` | `captureChance`, `rollCapture`, `CaptureCheckInput` |
|
|
415
602
|
| Owned roster | `scene/roster` | `createRoster`, `Roster`, `RosterEntry`, `RosterCaptureOptions` |
|
|
@@ -438,6 +625,7 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
438
625
|
| JSON data source | `data/jsonDataSource` | `createJsonDataSource`, `JsonDataSourceOptions` |
|
|
439
626
|
| Dev proxy routing | `data/devProxy` | `proxiedUrl`, `parseDevProxyTable`, `DevProxyTable`, `ProxiedUrlOptions`, `DEFAULT_DEV_PROXY_PREFIX` |
|
|
440
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 |
|
|
441
629
|
| Turn loop | `turn/turnLoop` | `createTurnLoop`, `TurnLoop`, `TurnLoopConfig`, `TurnState`, `PoolConfig`, `PoolState`, `TurnLoopSnapshot` |
|
|
442
630
|
| Declared-action intent board | `turn/intent` | `createIntentBoard`, `IntentBoard`, `DeclaredIntent` — `declare(participantId, intent)`, `peek`, `all`, `consume`, `clear` |
|
|
443
631
|
| Commit modes | `turn/commit` | `createCommitController`, `CommitController`, `CommitMode`, `CommitOutcome`, `SubmittedAction` |
|
|
@@ -457,8 +645,10 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
457
645
|
| Beat clock | `time/beatClock` | `createBeatClock`, `createBeatInputBuffer`, `nextBeatTime`, `BeatClock`, `BeatClockConfig`, `BeatSnapshot`, `BeatInputBuffer`, `BufferedAction` |
|
|
458
646
|
| Spawn director | `ai/spawnDirector` | `createSpawnDirectorState`, `advanceSpawnDirector`, `advanceWave`, `raiseAlert`, `pickSpawnPoint`, `SpawnDirectorConfig`, `WaveManifest`, `SpawnEntry`, `SpawnRequest`, `DirectorContext` |
|
|
459
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 |
|
|
460
649
|
| Job board | `ai/jobBoard` | `createJobBoard`, `JobBoard`, `JobDef`, `Job`, `JobPhase`, `WorkerState`, `JobReport`, `JobTickContext` |
|
|
461
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` |
|
|
462
652
|
| Physics actors | `physics/ragdoll`, `physics/carryable`, `physics/forceVolume`, `physics/spatialGrid` | `createRagdoll`, `Ragdoll`, `Carryable`, `carrySpeedMultiplier`, `ForceVolume`, `PlatformCarry`, `SpatialGrid` |
|
|
463
653
|
| Traversal (grapple/glide) | `physics/traversal` | `Grapple`, `GrappleConfig`, `Glide`, `GlideConfig` |
|
|
464
654
|
| Structural destruction | `physics/structure` | `StructureGraph`, `StructureNodeSpec`, `StructureEdgeSpec`, `StructureMaterial`, `StructureMaterialTable`, `CollapseEvent`, `DebrisConfig` |
|
|
@@ -474,6 +664,7 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
474
664
|
| Session matchmaking | `multiplayer/matchmaking` | `browseSessions`, `findByJoinCode`, `quickMatch`, `matchesFilter`, `normalizeJoinCode`, `generateJoinCode`, `SessionListing`, `MatchFilter` |
|
|
475
665
|
| Auth identity | `multiplayer/identity` | `AuthSession`, `PlayerIdentity`, `sessionPlayer`, `resolveGuestSession` |
|
|
476
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) |
|
|
477
668
|
| Voice seam | `multiplayer/voiceContract` | `VoiceTransport`, `VoiceParticipant`, `VoiceRoute`, `createLocalVoiceTransport`, `createPushToTalk`, `PushToTalkMode` |
|
|
478
669
|
| Race state | `game/race` | `raceTrack`, `RaceTrack`, `createRaceState`, `RaceState`, `RaceEvent`, `RaceWinCondition`, `firstPastPost`, `topK`, `lastStanding`, `everyoneFinishes` |
|
|
479
670
|
| Reveal query | `sensor/revealQuery` | `createRevealQuery`, `RevealQuery`, `RevealQueryOptions`, `RevealHit` |
|
|
@@ -492,6 +683,8 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
492
683
|
| Telegraph | `combat/telegraph` | `pointInTelegraph`, `telegraphProgress`, `telegraphFired`, `telegraphTurnProgress`, `telegraphFiredAtTurn`, `telegraphTurnsRemaining`, `TelegraphShape`, `TelegraphConfig` |
|
|
493
684
|
| Dash / dodge | `movement/dash` | `createDashState`, `DashState`, `DashConfig`, `DashBurst`, `iframeActive`, `dashOffset` |
|
|
494
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 |
|
|
495
688
|
| Event meter | `stats/eventMeter` | `createEventMeter`, `EventMeter`, `EventMeterConfig`, `EventMeterFeedResult` |
|
|
496
689
|
| Auto-target policy | `scene/autoTarget` | `selectAutoTarget`, `createAutoTargeter`, `AutoTargetPolicy`, `AutoTargeter`, `AutoTargetDeps` |
|
|
497
690
|
| Resistance matrix | `combat/resistance` | `resolveResistance`, `resistanceScale`, `ResistanceMatrix`, `ResistVerdict`, `ResistanceResult` |
|
|
@@ -506,6 +699,15 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
506
699
|
|
|
507
700
|
## Getting started (new project)
|
|
508
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
|
+
|
|
509
711
|
```sh
|
|
510
712
|
bun add @jgengine/core @jgengine/react @jgengine/shell react react-dom three three-stdlib @react-three/fiber @react-three/drei
|
|
511
713
|
bun add -d @tailwindcss/vite tailwindcss # HUD styling (Vite + Tailwind v4)
|
|
@@ -633,7 +835,7 @@ A voxel block is an object. A rack is an object with a slot inventory. A GPU is
|
|
|
633
835
|
|
|
634
836
|
## Game repo layout
|
|
635
837
|
|
|
636
|
-
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/`. Dense files — one `catalog.ts` per domain, never one file per entry.
|
|
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.
|
|
637
839
|
|
|
638
840
|
```
|
|
639
841
|
src/
|
|
@@ -664,6 +866,8 @@ src/
|
|
|
664
866
|
|
|
665
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.
|
|
666
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
|
+
|
|
667
871
|
```ts
|
|
668
872
|
// game.config.ts — imports only, nothing inline
|
|
669
873
|
import { defineGame } from "@jgengine/shell/defineGame";
|
|
@@ -725,7 +929,7 @@ export const physics: PhysicsConfig = { gravity: -32 };
|
|
|
725
929
|
|
|
726
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.
|
|
727
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.
|
|
728
|
-
- **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).
|
|
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`).
|
|
729
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.
|
|
730
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.
|
|
731
935
|
- `server.mode` is a string your loop/commands interpret — the engine ships no gamemode presets.
|
|
@@ -780,6 +984,8 @@ Optional render/world fields the shell also reads: `entitySprites` / `entityMode
|
|
|
780
984
|
|
|
781
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.
|
|
782
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
|
+
|
|
783
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.
|
|
784
990
|
|
|
785
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.
|
|
@@ -808,7 +1014,7 @@ Catalog-first, no per-game audio glue. The `audio` field of `defineGame({...})`
|
|
|
808
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:
|
|
809
1015
|
| `rig` | For | Key config (`camera.<rig>`) |
|
|
810
1016
|
|-------|-----|------------------------------|
|
|
811
|
-
| `orbit` (default) | Third-person chase | `initialDistance`, `targetHeight`, `min/maxPolarAngle`, drag/zoom |
|
|
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 |
|
|
812
1018
|
| `first` | FPS mouse-look | `firstPerson: { eyeHeight, sensitivity, maxPitch, reticle, viewmodel }` |
|
|
813
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`) |
|
|
814
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 |
|
|
@@ -867,7 +1073,7 @@ ctx.time advance, now, calendar, snapshot; pause, play, toggle,
|
|
|
867
1073
|
ctx.subscribe / ctx.version change signal — UI layers bind via useSyncExternalStore
|
|
868
1074
|
```
|
|
869
1075
|
|
|
870
|
-
`content.itemById(id)` supplies `{ use?, weapon?, trade? }
|
|
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.
|
|
871
1077
|
|
|
872
1078
|
### Two tiers: `ctx` runtime vs pure factories
|
|
873
1079
|
|
|
@@ -914,813 +1120,605 @@ export function onTick(ctx: GameContext, dt: number) {
|
|
|
914
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.
|
|
915
1121
|
|
|
916
1122
|
## Content catalogs
|
|
1123
|
+
## `ctx.game.store` — reactive game state
|
|
917
1124
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
| `breakable` | `false` or `{ baseBreakTime, harvest, drops, dropsWhenUnmet }` |
|
|
927
|
-
| `proximityPrompt` | Float UI + optional command invoke |
|
|
928
|
-
| `slotInventory` | Attached container `{ slots, accepts }` created at place time (`object:<instanceId>`) |
|
|
929
|
-
|
|
930
|
-
Break resolution: `duration = baseBreakTime / (tool?.breakSpeed ?? 1)`; drops per `when` (`always` / `harvestMet` / `silkTouch` / `playerKill`); then `inventory.put` + `object.remove`.
|
|
931
|
-
|
|
932
|
-
### Item catalog fields
|
|
933
|
-
|
|
934
|
-
| Field | Purpose |
|
|
935
|
-
|-------|---------|
|
|
936
|
-
| `id`, `kind`, `stack`, `model` | Basics; `stack` feeds `itemTraits.stackLimit` |
|
|
937
|
-
| `use` | Game handler name dispatched by `item.use` (`"fireGun"`, `"castBolt"`, `"drinkPotion"`) |
|
|
938
|
-
| `weapon` | Stats the handler reads via `item.weapon.getStat` — `damage`, `heal`, `reach`, `manaCost`, `projectile.{mass,gravity,fuseTime,settleOn}`, `explosion.{radius}` … |
|
|
939
|
-
| `trade` | `{ buy?: {coins: 80}, sell?, shops?: ["shop_town"] }` |
|
|
940
|
-
| `requires` | Unlock ids gating purchase/use |
|
|
941
|
-
| `placesObject` | Object id placed from hotbar |
|
|
942
|
-
| `rarity`, `baseType` | Read by the `worldItem` rarity render binding + loot filter when this item drops to the ground (#32/#33); `baseType` defaults to the item id when absent |
|
|
943
|
-
|
|
944
|
-
### Entity catalog fields
|
|
945
|
-
|
|
946
|
-
| Field | Purpose |
|
|
947
|
-
|-------|---------|
|
|
948
|
-
| `movement` | `walkSpeed` (reaches spawn automatically), `poses?: ["standing","crouch","prone","running"]`, `aim?: ["hip","ads"]`, `frozen?: boolean` — a scene-instance-level movement lock (cutscenes, stuns, mount transitions); `movedWhileFrozen(entity)` (`scene/entityStore`) flags an entity whose velocity moved anyway despite `frozen: true`, catching a system that bypassed the lock |
|
|
949
|
-
| `role` | `CatalogEntityRole` = `"player"` \| `"enemy"` \| `"hostile"` \| `"npc"` \| `"vehicle"` — catalog hostility class for targeting (`"enemy"`/`"hostile"` classify hostile in `cycleTarget`). Distinct from the scene *instance* `EntityRole` (`"player"` \| `"npc"` \| `"prop"`, in `scene/entityStore`) which drives input/camera binding — **possession** (`ctx.player.possession`) flips this instance role between `"player"`/`"npc"` on every control swap, so exactly one owned entity is ever the input/camera target |
|
|
950
|
-
| `stats` | Stat declarations — bounded values: `{ health: { max: 120, min: 0 }, level: { max: 60, min: 1, current: 1 }, … }` — `current` optional, defaults to `max` |
|
|
951
|
-
| `receive` | Per-effect absorption: `{ damage: { order: ["shield","health"], modifiers? }, heal: { order: ["health"] } }` — keyed by **game-defined effect ids**; presence = can receive |
|
|
952
|
-
| `onDeath` | `{ drops: "table_id" }` or reason-aware `{ drops: [{ table, when: { reason: "player_kill" } }], command?: { name, when? } }` |
|
|
953
|
-
| `wander`, `talkable` | AI descriptor; dialogue id sugar for a talk prompt |
|
|
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
|
+
```
|
|
954
1133
|
|
|
955
|
-
|
|
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`.
|
|
956
1135
|
|
|
957
|
-
`
|
|
1136
|
+
## `ctx.game.cards` / `ctx.game.turn` — lazily-created piles and turn loops
|
|
958
1137
|
|
|
959
|
-
|
|
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.
|
|
960
1139
|
|
|
961
|
-
##
|
|
1140
|
+
## Movement, pose, input
|
|
1141
|
+
## External data — `data/dataSource` and the dev proxy
|
|
962
1142
|
|
|
963
|
-
|
|
964
|
-
stats.get(instanceId, statId) // → { current, max, min } | null
|
|
965
|
-
stats.set(instanceId, statId, { current?, max?, min? })
|
|
966
|
-
stats.delta(instanceId, statId, n) // → null | { reason } — clamps into [min, max]
|
|
967
|
-
```
|
|
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.
|
|
968
1144
|
|
|
969
|
-
|
|
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.
|
|
970
1149
|
|
|
971
|
-
|
|
1150
|
+
## Multiplayer and the backend seam
|
|
1151
|
+
## Genre cheat sheet
|
|
972
1152
|
|
|
973
|
-
|
|
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.
|
|
974
1158
|
|
|
975
|
-
|
|
1159
|
+
## Anti-patterns
|
|
976
1160
|
|
|
977
|
-
|
|
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 |
|
|
978
1184
|
|
|
979
|
-
|
|
1185
|
+
## New-game definition of done
|
|
980
1186
|
|
|
981
|
-
|
|
982
|
-
ctx.scene.entity.setTarget(fromId, toId | null)
|
|
983
|
-
ctx.scene.entity.getTarget(fromId) // → instanceId | null
|
|
984
|
-
ctx.scene.entity.cycleTarget(fromId, { filter: "hostile" | "friendly" | "any", direction? })
|
|
985
|
-
```
|
|
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.
|
|
986
1188
|
|
|
987
|
-
|
|
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
|
|
988
1204
|
|
|
989
|
-
##
|
|
1205
|
+
## Quick reference
|
|
990
1206
|
|
|
991
|
-
```ts
|
|
992
|
-
ctx.item.use.register(handlers) // once in onInit; duplicate names throw
|
|
993
|
-
ctx.item.use.can(ctx, input) // → { reason } | null
|
|
994
|
-
ctx.item.use.use(ctx, input) // dispatches catalog `use` → your handler
|
|
995
|
-
|
|
996
|
-
type ItemUseInput = { from: string; itemId: string; inventoryId?: string; aim?: Aim };
|
|
997
|
-
type ItemUseHandler<GameContext> = {
|
|
998
|
-
can?(ctx, input): { reason: string } | null;
|
|
999
|
-
apply(ctx, input): { state: GameContext; error?: string };
|
|
1000
|
-
};
|
|
1001
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;
|
|
1002
1215
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
| Handler | Engine calls |
|
|
1006
|
-
|---------|--------------|
|
|
1007
|
-
| gun | spend ammo → `fireProjectile` → `settleProjectile` |
|
|
1008
|
-
| grenade | `fireProjectile` (ballistic) → settle → `effect({ at, radius })` |
|
|
1009
|
-
| melee | `queryArc` + reach from `getStat` → `effect` per hit |
|
|
1010
|
-
| MMO cast | `getTarget(from)` → `stats.delta(mana)` → `effect({ to })` |
|
|
1011
|
-
| consumable | `effect({ to: from, effect: "heal", via: { amount: -n } })` |
|
|
1216
|
+
# jgengine-ui
|
|
1012
1217
|
|
|
1013
|
-
|
|
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.
|
|
1014
1219
|
|
|
1015
|
-
|
|
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.
|
|
1016
1221
|
|
|
1017
|
-
|
|
1222
|
+
## Required outcome
|
|
1018
1223
|
|
|
1019
|
-
|
|
1224
|
+
A JGengine game must read as a self-contained game, not a responsive website with a canvas inside it.
|
|
1020
1225
|
|
|
1021
|
-
|
|
1226
|
+
Before shipping UI:
|
|
1022
1227
|
|
|
1023
|
-
|
|
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.
|
|
1024
1235
|
|
|
1025
|
-
|
|
1236
|
+
## Ownership boundary
|
|
1026
1237
|
|
|
1027
|
-
|
|
1238
|
+
The main `jgengine` skill owns intake, engine architecture, API routing, hooks, commands, state, and verification routing. This skill owns presentation quality.
|
|
1028
1239
|
|
|
1029
|
-
|
|
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.
|
|
1030
1241
|
|
|
1031
|
-
|
|
1242
|
+
## Non-negotiable defaults
|
|
1032
1243
|
|
|
1033
|
-
|
|
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.
|
|
1034
1251
|
|
|
1035
|
-
|
|
1036
|
-
lootTable({ id, rolls?, entries: [{ item? | currency?, count: n | [min,max], weight }] })
|
|
1037
|
-
ctx.game.loot.register(table) // in onInit
|
|
1038
|
-
ctx.game.loot.has(id) / roll(id, rng?) / grantToPlayer(userId, drops, source?)
|
|
1039
|
-
```
|
|
1252
|
+
## Preview states ship with the UI
|
|
1040
1253
|
|
|
1041
|
-
|
|
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.
|
|
1042
1255
|
|
|
1043
|
-
##
|
|
1044
|
-
Pure, renderer-free structures for card, board, and deckbuilder games — they sit **beside** the slot inventory, not in place of it. All are immutable-reducer + thin-controller pairs, mirroring the two-tier ctx/factory model: use the `create*` controller in game code, reach for the exported pure functions (`draw`, `moveCards`, `tickTimeline`, `laneAggregate`, `runPipeline`, `placeShaped`) for unit tests and headless servers.
|
|
1045
|
-
```ts
|
|
1046
|
-
// cards/cardPile — named ordered zones (deck/hand/discard/exhaust); seeded shuffle, hand limit, reshuffle-on-empty
|
|
1047
|
-
const pile = ctx.game.cards.pile("deck", { zones: ["deck","hand","discard","exhaust"], drawFrom:"deck", handZone:"hand", discardTo:"discard", handLimit:7, reshuffleFrom:"discard" });
|
|
1048
|
-
pile.reset(createCardPileState(pileConfig, { deck: ids })); // seed zone contents once, from onInit
|
|
1049
|
-
pile.shuffle("deck", seed); // seeded Fisher–Yates via pileRng — deterministic under the same seed
|
|
1050
|
-
pile.draw(5); // deck → hand, clamped to handLimit, reshuffles discard when deck runs dry
|
|
1051
|
-
pile.discard(ids); pile.exhaust(ids, "exhaust"); // Slay the Spire / Balatro lifecycle
|
|
1052
|
-
// cards/modifierPipeline — ordered { source, apply(value) → value } with an inspectable per-step trace
|
|
1053
|
-
const score = runPipeline({ chips: 10, mult: 1 }, jokers); // score.value + score.trace[i].{before,after,changed} for Balatro-style scoring readouts
|
|
1054
|
-
// board/laneBoard — N lanes, per-side power aggregate + optional per-lane LaneRule modifier (Marvel Snap / Inscryption)
|
|
1055
|
-
board.aggregate(lane, "player").total; board.outcome(lane).winner; board.lanesWon();
|
|
1056
|
-
// board/timelineBoard — N slots each on an independent cooldown, resolving in expiry order (The Bazaar auto-battlers)
|
|
1057
|
-
board.tick(dtMs); // → fires[] sorted by expiry time then slot index; multiple fires per slot per tick
|
|
1058
|
-
// inventory/shapedGrid — polyomino footprints, rotate, overlap-check, adjacency (Backpack Hero / Tetris inventory)
|
|
1059
|
-
placeShaped(grid, { id, value, footprint }, [col,row], rotation); // rotateFootprint / canPlace guard overlap + bounds
|
|
1060
|
-
gridAdjacencyQuery(grid).neighborsOf(id); // feeds synergy effects
|
|
1061
|
-
```
|
|
1062
|
-
Reuse the engine's seeded RNG (`pileRng`) for anything random — never `Math.random()` in game logic. The React drag/rotate/drop/snap gesture layer over these lives in `@jgengine/react` (see UI section).
|
|
1063
|
-
|
|
1064
|
-
`ctx.game.cards.pile(id, config?)` is the runtime-wired accessor for `createCardPile`: lazily creates the pile on first call (`config` required then) or returns the existing one for `id` on every later call, and every mutation notifies `ctx.subscribe`/bumps `ctx.version()` — so a `useEngineState`-bound hand/discard view re-renders without a game-owned store. Reach for `createCardPile` directly only for a headless test or server; game code goes through `ctx.game.cards.pile`.
|
|
1065
|
-
|
|
1066
|
-
## Puzzle primitives — cell grids and falling pieces
|
|
1067
|
-
|
|
1068
|
-
Two pure, renderer-free `@jgengine/core` primitives for cell-based puzzle games (Tetris wells, match-3 boards); tile art and the drop-cadence loop are the shell's/game's job.
|
|
1069
|
-
|
|
1070
|
-
- **`puzzle/cellGrid`** — a generic immutable `CellGrid<T>` for uniform typed-cell boards. Row 0 is the top; `y` grows downward. `createCellGrid`, `cellAt`, `withCell`/`withCells` (immutable single/batch writes), `fullRows`/`clearRows` (line-clear + compaction), `collapseColumns` (match-3 cascade gravity), `findRuns` (run detection with an optional custom matcher).
|
|
1071
|
-
- **`puzzle/fallingPiece`** — the falling-piece layer over a `CellGrid`: `ShapeTable<TShape>` maps rotation states to cell offsets; `pieceCells`/`pieceCollides`/`mergePiece` place, test, and commit a piece; `dropDistance` computes the ghost-piece landing row; `gravityInterval`/`levelForLines`/`lineScore` are the classic Tetris drop-speed/level/score curves (overridable); `createLockDelay`/`stepLockDelay` is the grounded→countdown→lock stepper (`delaySeconds: 0` locks instantly on touchdown).
|
|
1072
|
-
- **`tactics/fallingGrid`** — a generic tile-drop grid over any `TCell` payload (distinct from the `cellGrid`/`fallingPiece` row-clear pair): `createFallingGrid(config)`, `gravityIntervalMs(level, config?)` for the drop-speed curve, and a `FallingGridSnapshot`/`LockState` shape for the grounded→lock stepper.
|
|
1073
|
-
|
|
1074
|
-
## Dropped items — `worldItem` and the loot filter
|
|
1075
|
-
A `worldItem` is a scene **entity** (position + item ref + rarity), never an inventory item or object — see the three buckets. `onDeath.dropMode: "world"` (above) is the usual producer; games can also hand-place ground loot (chests, quest drops).
|
|
1076
|
-
ctx.scene.worldItem.spawn({ itemId, position, rarity?, baseType?, count?, affixTier?, source? })
|
|
1077
|
-
ctx.scene.worldItem.get(instanceId) / list() / nearestInRadius(from, radius, filter?)
|
|
1078
|
-
ctx.scene.worldItem.pickup(instanceId, userId) // grants to inventory + despawns, emits worldItem.picked_up
|
|
1079
|
-
Click-to-grab is engine-owned: setting `pointer.grabWorldItems: true` in `defineGame({...})` makes `@jgengine/shell`'s `GamePlayerShell` resolve `pointer.worldHit()` on primary click, and — when the hit entity is a `worldItem` within the `worldItem.pickupRadius` (default `DEFAULT_PICKUP_RADIUS`) configured on `defineGame({...})` of the local player — calls `pickup` directly, no game command needed. `@jgengine/react`'s `useWorldItems()` / `useNearestWorldItem(radius)` drive a HUD pickup prompt off the same store.
|
|
1080
|
-
Presentation is a two-layer render binding, both engine-owned (rendered by `@jgengine/shell`'s `WorldItems`) over **game-supplied data**:
|
|
1081
|
-
1. **Rarity baseline** — the `worldItem.rarityStyle: Record<rarity, { color?, beam?, label? }>` field of `defineGame({...})`, the game's rarity palette (Borderlands/Diablo-style beam + color coding).
|
|
1082
|
-
2. **Loot filter overlay** (#33) — the `worldItem.filter: LootFilterRule[]` field of `defineGame({...})`, built with `lootFilter([{ id, when: { rarity?, baseType?, minAffixTier?, maxAffixTier? }, hide?, color?, beam?, label? }])` from `game/lootFilter`. **First matching rule wins** (PoE/Last Epoch block semantics); a rule only overrides the fields it sets, everything else falls back to the rarity baseline. `resolveWorldItemPresentation(item, rarityStyle, rules)` composes both layers and is what the shell calls per item.
|
|
1083
|
-
## Gear systems — durability, affixes, modular items, storage tiers
|
|
1084
|
-
Four pure primitives that hang off item **instances** (not the stackable catalog id) — all catalog-first (specs are game-supplied config) and renderer-free. Item instances that carry durability/affix/modular state key off a game-assigned instance id, the same way targeting keys off entity instance ids.
|
|
1085
|
-
**Durability** (`item/durability`) — per-instance wear + repair. `DurabilitySpec` (`{ max, wearPerUse?, wearPerHit?, disableAtZero?, repair? }`) is catalog data; `createDurability(spec)` seeds a `DurabilityState`, `wear(spec, state, "use" | "hit", times?)` decrements (floors at 0), `isDisabled(spec, state)` gates use at zero, `durabilityFraction` feeds a HUD bar. Repair is quote-then-apply: `repairQuote(spec, state, { station?, to? })` returns the `{ item, count }[]` material cost (scaled by points restored) + the post-repair state (optional `qualityLossPerRepair` shrinks `max` each repair, Tarkov-style) — the game charges the materials through inventory, then commits the quote's `state`. `createDurabilityTracker()` keeps `DurabilityState` per instance id for the runtime.
|
|
1086
|
-
**Affix roller** (`item/affix`) — procgen `base × rarity → { rolled affixes, computed stats, name }`. `createAffixRoller({ pools, rarities })` over rarity-weighted `AffixPool`s. `roll(base, rarityId, rng)` draws `affixCount` distinct affixes without replacement (weighted, via the engine's `pickWeighted`), computes stats (base × `rarity.statScale`, then `op: "add"` affixes, then `op: "mul"`), and composes a name from `rarity.namePart` + prefix/suffix parts. `rollRarity(rng)` picks a weighted tier; `rollRandom(base, rng)` chains both. Pass `seededRng(seed)` for deterministic drops; any `() => number` rng works (same contract as `loot.roll`). `seededRng` lives in `random/rng` (re-exported here) alongside `seededStreams(seed)`, which derives independent named streams from one seed — `streams("worldgen")` vs `streams("history")` — so simulation draws never perturb generation (intervening in a run cannot change the map).
|
|
1087
|
-
**Modular item** (`item/modularItem`) — a whole assembled from parts in typed mount slots (guns, mechs). `ModularItemDef` has `slots: MountSlotDef[]` (`{ id, accepts, required? }`); `install(def, installed, slotId, part)` validates the slot exists, accepts the part's `category`, and is empty; `computeEffectiveStats(def, installed)` rolls part `stats` (additive) then `multipliers` over `baseStats`; `missingRequiredSlots`/`isComplete` gate a buildable whole. `createModularItem(def)` is the stateful wrapper (`install`/`uninstall`/`effectiveStats`/`partInSlot`).
|
|
1088
|
-
**Storage tiers + insurance** (`inventory/storageTier`) — the extraction-economy inventory half. Inventory containers carry a `tier: "carried" | "banked"` (`InventoryDeclaration.tier`; a Tarkov secure container is just a `banked` container on the body). `partitionOnDeath(containers)` splits a death snapshot into `{ kept, lost }` (banked survives, carried is dropped, stacks merged). `createDeliveryQueue()` is the delayed-delivery (insurance) hook: `schedule` a `ScheduledDelivery` with a game-time `deliverAt`, then `due(now)` / `claimDue(now)` drain it on the tick clock. `insureLost(lost, policy, userId, now, rng?)` filters the lost set to insured items and stamps a delayed `deliverAt` → feed straight into the queue. `resolveConsolation(policy, partition)` returns a baseline loadout id (apply via `applyLoadout`) — the death consolation grant, optionally gated on `if-carried-empty`. *(Session/round machines — extraction hold-to-leave, raid banking — consume this tier; see the objective-machine group.)*
|
|
1089
|
-
## Objective, round & session machines
|
|
1090
|
-
Content-agnostic state machines for competitive/session shapes — plant/defuse, buy/live/end rounds, downed/revive, the battle-royale ring, extraction raids, run-vs-meta persistence. All pure `core`; every timer takes a **game-time** `dt`/`now` (`ctx.time`), so pause and fast-forward apply for free. Drive them from `loop.onTick` and pipe their events into `ctx.game.feed`/`events`; render their snapshots as HUD (per the UI quality bar in [`reference/ui-react.md`](reference/ui-react.md) — the downed banner, ring warning, and extraction timer are required HUD).
|
|
1091
|
-
**Contested channel** (`session/contestedChannel`) — the interrupt-on-damage progress objective behind plant/defuse, cash-out, urn deposit, banishing, and hold-to-extract. `createContestedChannel({ duration, interruptOnDamage?, resetOnInterrupt?, favorability?, ratePerOccupant?, contested?, decayRate? })`: `start(team)` begins the channel, `tick(dt, occupants)` advances it against per-team occupancy (`Record<teamId, count>`) and emits `start`/`tick`/`contested`/`paused`/`complete` events, `damage(reason?)` interrupts (keeps or zeroes progress per `resetOnInterrupt`). `favorability[team]` scales fill rate (Deadlock deposit); `ratePerOccupant` fills faster with more owners present; `contested: "pause" | "decay"` chooses whether an opposing occupant freezes or reverses progress (The Finals contest). The owner leaving pauses it. Extraction hold-to-leave reuses this primitive verbatim.
|
|
1092
|
-
**Round state** (`session/roundState`) — the buy→live→end match machine (Valorant/CS). `createRoundState({ phases, teams, phaseOrder?, winCondition?, maxRounds?, winReward?, lossBonus? })`: `tick(dt)` runs the phase timer and auto-advances (emitting `phase.start`/`phase.end`, rolling the last phase back into the next round's first), `concludeRound(winner)` records the win on any "conclude-eligible" phase (any phase but the first/last in the cycle), settles `round.economy` (winner gets `winReward`, losers get an escalating `lossBonus` via `lossBonusFor(rule, streak)` clamped to `max`), and moves to the next phase. `onPhaseEnd(hook)` fires commerce/spawn gates on each transition; `match.end` fires at `maxRounds`. `server.mode` stays a game string — this is the timer/economy engine under it.
|
|
1093
|
-
|
|
1094
|
-
Two extras beyond the default buy/live/end cycle: `phaseOrder?: string[]` overrides the phase names/cycle entirely (a wider `Record<string, number>` `phases` shape to match) — a draft→ban→play→score cycle is the same machine with different phase names. `teams: (string | { id, role? })[]` accepts a plain id or a `{ id, role }` pair; `roleOf(team)` reads the tag back (`"attacker"`/`"defender"`, Valorant side assignment) without a parallel lookup table. `winCondition?: (snapshot: RoundSnapshot) => string | null` lets `evaluate()` (call it from `onTick` alongside `tick(dt)`) auto-conclude the round the instant a score/objective condition is met, instead of the game hand-calling `concludeRound` — return a team id to end it, `null` to keep playing; `RoundSnapshot` is `{ round, phase, timeLeft, scores, lossStreaks, roles, matchOver }`.
|
|
1095
|
-
**Role assignment** (`session/roles`) — `assignRoles(players, specs: RoleSpec[])` distributes fixed-count or proportional roles (hider/seeker, spy/operative, prop/hunter) across a player list — the allocation half of an asymmetric session mode; `RoundConfig.teams`' per-team `role` is the lighter-weight alternative when a round machine already tracks the roster.
|
|
1096
|
-
**Downed / revive** (`combat/downed`) — the 3-state alive→downed→dead chain (Apex/Helldivers). `createDownedState({ bleedoutSeconds, reviveSeconds?, reviveHealthFraction?, banner? })`: `down(id)` starts the bleedout, `tick(dt)` counts it down (→ `died`, optionally spawning a `banner`), `revive(id, dt)` accumulates an ally's hold time (→ `revived` with the health fraction the game restores), `finish(id)` executes a downed enemy, and `respawnFromBanner(id)` brings a banner-holder back at a beacon. It sits **in front of** the engine death resolution: on lethal damage call `down` instead of dying; on `died`/`bleedout` run the real `resolveDeath`. No banner ⇒ death is terminal.
|
|
1097
|
-
**Shrinking ring** (`session/ring`) — the battle-royale safe zone with out-of-bounds DoT. A catalog `RingConfig` is `{ center, phases: RingPhase[] }` where each phase is `{ startTime, shrinkDuration, fromRadius, toRadius, damagePerSecond, center? }` on the game clock. `ringSampleAt(config, t)` / `createRing(config).at(t)` returns the live `{ center, radius, damagePerSecond, shrinking }` (radius/center interpolate during each shrink window, hold between phases); `isOutside(t, pos)` / `distanceOutside(t, pos)` test a point, and `damageOutside(t, dt, positions)` returns per-entity `{ id, damage }` for everyone beyond the wall — feed those into `scene.entity.stats.delta`/`effect` each tick.
|
|
1098
|
-
**Extraction session** (`session/extraction`) — the raid-scoped "reach an extract and leave to bank what you carried" wrapper (Tarkov/DMZ/Helldivers), composed from the contested channel + `inventory/storageTier`. `createRaidSession({ extracts, insurance?, consolation? })`: `beginExtract(userId, extractId, team?)` opens a hold-to-leave channel, `tickExtract`/`damage` drive it, and on completion `resolveExtraction(userId, containers)` banks everything carried. `resolveDeath(userId, containers, now, rng?)` runs `partitionOnDeath` (banked kept, carried lost), schedules insured items through the built-in delivery queue (`claimDeliveries(now)` drains it on the clock), and yields the consolation loadout id. `playerSnapshot(userId)` feeds the extraction-timer HUD.
|
|
1099
|
-
**Persistence scopes** (`runtime/persistenceScope`) — the run-vs-meta split with explicit reset boundaries (Icarus mission wipe, Once Human season reset). `partitionScopes(state, { run })` splits a flat record into `{ meta, run }` by key; `resetRun` clears the run half while meta (talents/blueprints/account currency) survives; `clearRunFields(playerRow, runFields)` and `applyRunReset(profile, runFields, now)` do the same over `RuntimePlayerRow`/`PlayerProfileRecord`. `planScenarioReset({ gameId, serverId?, wipeChunks?, wipeServerSession?, resetPlayers?, runFields? })` normalizes a scenario/season reset that `HostPersistence.resetScenario?(reset)` applies — `@jgengine/sql` implements it (deletes the server's chunks + session, run-resets each profile in one transaction), keeping account meta intact.
|
|
1100
|
-
|
|
1101
|
-
## Trade
|
|
1102
|
-
|
|
1103
|
-
Catalog `trade` fields drive everything — no duplicate price lists.
|
|
1256
|
+
## Rejection test
|
|
1104
1257
|
|
|
1105
|
-
|
|
1106
|
-
ctx.game.trade.canBuy(itemId, shopId, count?) // → reason | null
|
|
1107
|
-
ctx.game.trade.canSell(itemId, count?)
|
|
1108
|
-
ctx.game.trade.buy(itemId, count, { shop, inventoryId }) // charge → put, rolls back on failure
|
|
1109
|
-
ctx.game.trade.sell(itemId, count, { shop, inventoryId })
|
|
1110
|
-
ctx.game.trade.tradableAt(shopId, allItemIds) // derive stock from catalogs
|
|
1111
|
-
```
|
|
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.
|
|
1112
1259
|
|
|
1113
|
-
|
|
1260
|
+
# JGengine UI — game presentation reference
|
|
1114
1261
|
|
|
1115
|
-
|
|
1116
|
-
ctx.game.economy.balance(userId, currencyId) / grant(...) / charge(...) // charge → { reason } | null
|
|
1117
|
-
ctx.game.unlocks.has(userId, id) / grant(userId, id) / list(userId) / tree(categoryId)
|
|
1118
|
-
```
|
|
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.
|
|
1119
1263
|
|
|
1120
|
-
|
|
1264
|
+
## The rule
|
|
1121
1265
|
|
|
1122
|
-
|
|
1266
|
+
A game must visually own its viewport. It must not resemble a dashboard, landing page, documentation page, or ordinary responsive web app.
|
|
1123
1267
|
|
|
1124
|
-
|
|
1268
|
+
HTML and React are valid implementation tools. Website visual grammar is not the default.
|
|
1125
1269
|
|
|
1126
|
-
|
|
1270
|
+
## 1. Start with a concise UI art direction
|
|
1127
1271
|
|
|
1128
|
-
|
|
1272
|
+
Before implementing screens, write this short block:
|
|
1129
1273
|
|
|
1130
|
-
|
|
1274
|
+
```md
|
|
1275
|
+
UI ART DIRECTION
|
|
1131
1276
|
|
|
1132
|
-
|
|
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
|
+
```
|
|
1133
1288
|
|
|
1134
|
-
|
|
1289
|
+
Keep it practical. It should directly influence layout, silhouettes, controls, timing, and materials.
|
|
1135
1290
|
|
|
1136
|
-
|
|
1137
|
-
ctx.player.loadout.register(loadouts) // onInit
|
|
1138
|
-
ctx.player.applyLoadout(userId, loadoutId) // → null | { reason }
|
|
1139
|
-
```
|
|
1291
|
+
Example forbidden patterns:
|
|
1140
1292
|
|
|
1141
|
-
|
|
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
|
|
1142
1302
|
|
|
1143
|
-
|
|
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.
|
|
1144
1304
|
|
|
1145
|
-
|
|
1146
|
-
ctx.game.quest.register(catalog) // onInit
|
|
1147
|
-
canAccept / accept / abandon / canTurnIn / turnIn / grant / revoke
|
|
1148
|
-
progress(userId, questId, objectiveId, delta)
|
|
1149
|
-
list(userId) / has(questId)
|
|
1150
|
-
bind("entity.died") // kill objectives match objective.target === catalogId
|
|
1151
|
-
bind("inventory.added") // collect objectives match objective.item
|
|
1152
|
-
```
|
|
1305
|
+
## 2. Screen inventory and hierarchy
|
|
1153
1306
|
|
|
1154
|
-
|
|
1307
|
+
Identify the screens the game actually needs:
|
|
1155
1308
|
|
|
1156
|
-
|
|
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
|
|
1157
1319
|
|
|
1158
|
-
|
|
1159
|
-
ctx.game.social.friends.canRequest / request / accept / decline / remove / block / list / requestsFor // persisted
|
|
1160
|
-
ctx.game.social.party.register({ maxMembers }) // then canInvite / invite / accept / decline / kick / leave / promote / list / membersOf / invitesFor
|
|
1161
|
-
ctx.game.social.presence.get(userId) // { online, serverId?, zoneId?, instanceId? }
|
|
1162
|
-
ctx.game.social.emotes.play(fromUserId, emoteId, radius?) // → { from, emoteId, at, recipients } | { reason }
|
|
1163
|
-
ctx.game.social.worldInvites.invite(fromUserId, toUserId, { serverId, joinCode? }) // then canInvite / accept / decline / listFor
|
|
1164
|
-
```
|
|
1320
|
+
For each screen, define:
|
|
1165
1321
|
|
|
1166
|
-
|
|
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
|
|
1167
1327
|
|
|
1168
|
-
|
|
1328
|
+
### HUD tiers
|
|
1169
1329
|
|
|
1170
|
-
|
|
1330
|
+
**Tier 1 — immediate action and survival**
|
|
1331
|
+
Health, timer, current target, ammo, danger, capture state.
|
|
1171
1332
|
|
|
1172
|
-
|
|
1333
|
+
**Tier 2 — short-term decisions**
|
|
1334
|
+
Objective progress, route progress, cooldowns, combo, pursuit distance.
|
|
1173
1335
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
ctx.game.chat.whisper(fromUserId, toUserId, body) // stable per-pair channel "whisper:<a>:<b>"
|
|
1177
|
-
ctx.game.chat.history(channelId, { limit?, viewerUserId? }) // viewer filter drops blocked senders
|
|
1178
|
-
ctx.game.chat.register({ id, kind, radius?, historyLimit?, rateLimit? }) // custom channels
|
|
1179
|
-
ctx.game.chat.channels() / snapshot() / hydrate(data)
|
|
1180
|
-
```
|
|
1336
|
+
**Tier 3 — reference information**
|
|
1337
|
+
Full map, inventory, controls, schedule, mission details, lore.
|
|
1181
1338
|
|
|
1182
|
-
|
|
1339
|
+
Tier 1 is immediately readable. Tier 2 is quieter. Tier 3 is usually hidden until requested.
|
|
1183
1340
|
|
|
1184
|
-
|
|
1341
|
+
Do not style every datum as an equally important bordered box.
|
|
1185
1342
|
|
|
1186
|
-
##
|
|
1343
|
+
## 3. Full-viewport game composition
|
|
1187
1344
|
|
|
1188
|
-
|
|
1189
|
-
ctx.player.cosmetics.register(defs) // onInit — Record<loadoutId, { slots: Record<slot, cosmeticId> }>
|
|
1190
|
-
ctx.player.cosmetics.apply(userId, loadoutId) // merges the preset's slots
|
|
1191
|
-
ctx.player.cosmetics.equip(userId, slot, cosmeticId | null) // set/clear one slot directly
|
|
1192
|
-
ctx.player.cosmetics.get(userId) // Record<slot, cosmeticId>
|
|
1193
|
-
```
|
|
1345
|
+
The active game should behave like an application mode:
|
|
1194
1346
|
|
|
1195
|
-
|
|
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
|
|
1196
1353
|
|
|
1197
|
-
|
|
1354
|
+
Recommended layer contract:
|
|
1198
1355
|
|
|
1199
|
-
```
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1356
|
+
```tsx
|
|
1357
|
+
<GamePlayer>
|
|
1358
|
+
<WorldLayer />
|
|
1359
|
+
<HudLayer />
|
|
1360
|
+
<ControlLayer />
|
|
1361
|
+
<ScreenLayer />
|
|
1362
|
+
<SystemLayer />
|
|
1363
|
+
</GamePlayer>
|
|
1203
1364
|
```
|
|
1204
1365
|
|
|
1205
|
-
|
|
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
|
|
1206
1371
|
|
|
1207
|
-
|
|
1372
|
+
Do not place unrelated interface pieces into one ordinary DOM flow.
|
|
1208
1373
|
|
|
1209
|
-
|
|
1210
|
-
ctx.scene.entity.form.register(defs) // onInit — FormDef[] = { id, movement?, abilities?, model? }
|
|
1211
|
-
ctx.scene.entity.form.shapeshift(instanceId, formId, durationSeconds?) // → null | { reason }
|
|
1212
|
-
ctx.scene.entity.form.active(instanceId) // → formId | null
|
|
1213
|
-
ctx.scene.entity.form.abilities(instanceId) // → readonly string[] | null
|
|
1214
|
-
ctx.scene.entity.form.revert(instanceId) // early revert
|
|
1215
|
-
```
|
|
1374
|
+
## 4. Explicit game layout modes
|
|
1216
1375
|
|
|
1217
|
-
|
|
1376
|
+
Do not rely on generic responsive wrapping. Compose explicit modes such as:
|
|
1218
1377
|
|
|
1219
|
-
|
|
1378
|
+
- desktop-wide
|
|
1379
|
+
- desktop-compact
|
|
1380
|
+
- mobile-landscape
|
|
1381
|
+
- mobile-portrait
|
|
1220
1382
|
|
|
1221
|
-
|
|
1222
|
-
ctx.game.events.on(name, handler) // register in onInit; typed GameEventMap
|
|
1223
|
-
ctx.game.feed.bind(action) // pipe an engine event into a ring buffer (default 20)
|
|
1224
|
-
ctx.game.feed.push(action, entry) // manual channels (chat, crafting)
|
|
1225
|
-
ctx.game.feed.recent(action, { limit? })
|
|
1226
|
-
ctx.game.leaderboard.track({ stat, scope: "global" | "server" | "profile" }) // onInit
|
|
1227
|
-
ctx.game.leaderboard.increment(userId, stat, { scope, by? }) / getTop / getProfile
|
|
1228
|
-
```
|
|
1383
|
+
A mobile layout is not a shrunken desktop HUD.
|
|
1229
1384
|
|
|
1230
|
-
|
|
1385
|
+
On mobile:
|
|
1231
1386
|
|
|
1232
|
-
|
|
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
|
|
1233
1395
|
|
|
1234
|
-
|
|
1235
|
-
ctx.game.store.set("health", 100) // any key, any value type
|
|
1236
|
-
ctx.game.store.get("health") // T | undefined
|
|
1237
|
-
ctx.game.store.has("health")
|
|
1238
|
-
ctx.game.store.delete("health")
|
|
1239
|
-
ctx.game.store.subscribe(listener) // change-signal fires on set/delete
|
|
1240
|
-
ctx.game.store.mapSnapshot() / arraySnapshot()
|
|
1241
|
-
```
|
|
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.
|
|
1242
1397
|
|
|
1243
|
-
|
|
1398
|
+
### Design-resolution fit (`platforms` + `hudFit`)
|
|
1244
1399
|
|
|
1245
|
-
|
|
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.
|
|
1246
1401
|
|
|
1247
|
-
|
|
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.
|
|
1248
1403
|
|
|
1249
|
-
|
|
1404
|
+
### Phase-gated HUD visibility (`showDuring`)
|
|
1250
1405
|
|
|
1251
|
-
|
|
1252
|
-
ctx.player.movement.getPose(id) / setPose(id, "crouch") // validates catalog movement.poses
|
|
1253
|
-
ctx.player.movement.getAim(id) / setAim(id, "ads") // ADS = aim state + zoom modifier, not a pose
|
|
1254
|
-
```
|
|
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.
|
|
1255
1407
|
|
|
1256
|
-
|
|
1408
|
+
### Shared viewport allocation (`GameViewportProvider`, region collision)
|
|
1257
1409
|
|
|
1258
|
-
|
|
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.
|
|
1259
1411
|
|
|
1260
|
-
|
|
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"`.
|
|
1261
1416
|
|
|
1262
|
-
|
|
1417
|
+
### HUD priority + mobile behavior (`HudPanel`)
|
|
1263
1418
|
|
|
1264
|
-
|
|
1419
|
+
Declare intent, not just CSS. `HudPanel` accepts:
|
|
1265
1420
|
|
|
1266
|
-
|
|
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.
|
|
1267
1423
|
|
|
1268
|
-
|
|
1424
|
+
The game still owns the genre-appropriate composition; the engine owns the geometry and the failure gate.
|
|
1269
1425
|
|
|
1270
|
-
|
|
1271
|
-
movement: {
|
|
1272
|
-
mode?: "free" | "axis" | "grid"; // "free" (default) camera-relative; "axis" locks travel to one world axis; "grid" snaps each committed position to cell centers
|
|
1273
|
-
axis?: "x" | "z"; // world axis for mode "axis". Default "x"
|
|
1274
|
-
cellSize?: number; // cell size for mode "grid". Default 1
|
|
1275
|
-
collideObjects?: boolean; // collide the walking player against placed scene objects (unit-box AABBs) even without collision.voxel
|
|
1276
|
-
beforeCommit?: (frame: MovementCommitFrame) => readonly [number, number, number] | undefined | void; // intercepts each frame's resolved position before the pose commits; return a replacement to constrain/redirect the step
|
|
1277
|
-
}
|
|
1278
|
-
```
|
|
1426
|
+
### Mandatory orientation (`orientation`)
|
|
1279
1427
|
|
|
1280
|
-
|
|
1428
|
+
A game declares its phone-orientation contract on `defineGame({ orientation })`:
|
|
1281
1429
|
|
|
1282
|
-
|
|
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).
|
|
1283
1432
|
|
|
1284
|
-
|
|
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.
|
|
1285
1434
|
|
|
1286
|
-
|
|
1435
|
+
## 5. Change the visual grammar
|
|
1287
1436
|
|
|
1288
|
-
|
|
1437
|
+
Avoid making every element the same rounded translucent rectangle.
|
|
1289
1438
|
|
|
1290
|
-
|
|
1439
|
+
Use genre-appropriate structures:
|
|
1291
1440
|
|
|
1292
|
-
|
|
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
|
|
1293
1454
|
|
|
1294
|
-
|
|
1455
|
+
Practical rule: no more than roughly 20% of a normal gameplay screen should resemble an ordinary web card or modal.
|
|
1295
1456
|
|
|
1296
|
-
|
|
1297
|
-
touch: {
|
|
1298
|
-
gestures: {
|
|
1299
|
-
tap: "rotateCw",
|
|
1300
|
-
swipeUp: "hold",
|
|
1301
|
-
swipeDown: "hardDrop",
|
|
1302
|
-
drag: { left: "shiftLeft", right: "shiftRight" },
|
|
1303
|
-
},
|
|
1304
|
-
buttons: [
|
|
1305
|
-
{ action: "rotateCcw", label: "CCW" },
|
|
1306
|
-
{ action: "softDrop", label: "Soft" },
|
|
1307
|
-
],
|
|
1308
|
-
},
|
|
1309
|
-
```
|
|
1457
|
+
Every persistent panel must justify why it exists, remains visible, has that shape, and occupies that position.
|
|
1310
1458
|
|
|
1311
|
-
|
|
1312
|
-
- **`buttons`** — curate the on-screen cluster (order preserved; bare string or `{ action, label?, icon? }`); omit to auto-derive one button per remaining bound action. Buttons render a glyph, not text: `iconForAction` (`@jgengine/react/gameIcons`) resolves the action name to a `GameIconName` (`jump`, `sprint`, `rotateCw`, `hardDrop`, `swap`, `hand`, `restart`, arrows, …), the `label` becomes the `aria-label`; set `icon: "<GameIconName>"` to pick one explicitly or `icon: false` to force the text label.
|
|
1313
|
-
- **`hidden`** — actions to drop from the derived buttons without gesture-binding them.
|
|
1314
|
-
- **`movement: false`** — suppress the virtual joystick even when movement actions are bound.
|
|
1315
|
-
- **`look` / `lookSensitivity`** — drag-to-look on the play surface; defaults to `true` for `first`-person camera rigs, `0.005` radians/px.
|
|
1316
|
-
- **`touch: false`** — opt out entirely when the game's own DOM UI is already touch-native.
|
|
1317
|
-
|
|
1318
|
-
`useDisplayProfile()` (`@jgengine/react/display`) reports `{ coarsePointer, compact, portrait }` — live media-query state, SSR-safe — for adaptive HUD layout; see the mobile/touch rules in [`reference/ui-react.md`](reference/ui-react.md).
|
|
1319
|
-
|
|
1320
|
-
## Interaction — `proximityPrompt`
|
|
1321
|
-
|
|
1322
|
-
One primitive for all float UI: `{ radius, display, invoke }` where `display` is `{ kind: "keybind", actionId }` | `{ kind: "gauge", gaugeId }` | `{ kind: "label", text }` and `invoke` is `{ command, args? }` or null (display-only). `talkable: "dialogue_id"` on an entity expands to a talk prompt. Engine picks the nearest prompt in radius (priority tie-break). Never build per-game hint resolver chains.
|
|
1323
|
-
|
|
1324
|
-
## Pointer-driven input and navigation
|
|
1325
|
-
The **pointer is a service, not per-game glue**. Opt in with `camera` plus a `pointer` config in `defineGame({...})`; the shell casts the cursor into the world and dispatches commands you define — verbs stay commands, catalogs stay data.
|
|
1326
|
-
- **`pointer.worldHit()` (shell service).** The shell raycasts the cursor to `{ point, normal, entity, object }` (a renderer-free `PointerHit` from `@jgengine/core/input/pointer`) — entity/object are the topmost instance ids under the cursor, else `null`, with a ground-plane fallback for open terrain. Consume it renderer-free: `aimToPoint(origin, point)` builds an `Aim` for `item.use`/projectiles (ground-target skillshots, twin-stick), `groundOf(hit)` drops to `[x, z]` for routing. `pointer.worldHitCenter()` is the same raycast pinned to the viewport center instead of the live cursor — the reticle-aim query a locked/hidden-cursor rig (first-person, gamepad) needs when there is no cursor position to read.
|
|
1327
|
-
- **The `pointer` field of `defineGame({...})`** (all optional): `moveCommand` (left-click ground → `run(cmd, { point, entity, object })`, click-to-move), `select` (left-drag marquee + single-click box-select of entities), `orderCommand` (right-click ground → `run(cmd, { selection, point })`, issue a command to the selection), `contextMenu` (right-click an entity/object → its catalog `verbs` menu), `secondaryCommand` (right-click ground/entity/object → `run(cmd, { point, entity, object, aim })` when neither `orderCommand` nor `contextMenu` claims the click — a generic right-click verb for games with no selection/RTS model), `aim` (route the primary ability's aim to the cursor), `grabWorldItems` (left-click a `worldItem` within pickup radius → engine-owned `ctx.scene.worldItem.pickup`, no game command). Enabling `select`/`moveCommand` frees the left button for verbs; orbit moves to middle-drag.
|
|
1328
|
-
- **`createDragCapture({ maxPull?, grabRadius? })`** (`@jgengine/core/input/pointer`) — a renderer-agnostic slingshot/drawback state machine: `begin(origin, at)` starts a pull (rejected outside `grabRadius`), `update(at)` tracks the cursor, `release()` returns the final `DragState { origin, current, pull, magnitude, fraction }` (pull clamped to `maxPull`), `cancel()` aborts. Angry Birds-style slingshots, bow drawback, throwable wind-up — pair with `aimToPoint` to fire.
|
|
1329
|
-
- **Selection math** (`scene/selection`) is pure and testable: `createSelectionSet()`, `screenRect`/`selectWithinRect`/`isMarquee` over projected screen points.
|
|
1330
|
-
- **Context menu** (`interaction/contextMenu`): a catalog entity/object carries `verbs: contextVerb(label, command, args?)[]`; the shell builds the menu with `buildContextMenu` and dispatches the chosen command via `contextVerbInput` (verb args + `target`/`point`, so one handler can walk-then-act).
|
|
1331
|
-
- **Navmesh + A\*** (`nav/navGrid`): `createNavGrid({ bounds, cellSize, diagonal? })` → mark obstacles with `blockAabb`/`setWalkable`, or `populateNavGridFromEnvironment(grid, world)` to block every generated building's footprint on an `environment()` world's `structures` in one call (returns the count blocked) instead of hand-walking the district. `findPath(grid, from, to, { clearance?, smooth?, stepCost? })` returns a string-pulled `[x, z]` polyline (blocked start/goal snap to the nearest walkable cell) feeding **both click-to-move and AI routing**; `stepCost?(from, to)` multiplies the base cost of a grid step — `slopeStepCost(terrainField, weight?)` is the ready-made factory that penalizes steep terrain (routes around cliffs instead of over them). Renderer-free — AI and gameplay consume it without the shell.
|
|
1332
|
-
- **`pathFollow`** (`nav/pathFollow`): the lighter authored-polyline mover for tower-defense creeps that needs no navmesh — `createPathFollow({ waypoints, speed, loop? })` + pure `advancePathFollow(config, state, dt)` (crosses multiple waypoints per tick, reports `done`/`heading`/`distanceTravelled`). Feed it a navmesh route with `pathFromNav(route, elevation, offset?)` and the same follower drives click-to-move — `elevation` is either a fixed `y` or a `{ sampleHeight(x, z) }` field (any `TerrainField` qualifies), so a route across relief rides the ground instead of a flat plane.
|
|
1333
|
-
- **`constrainToNavGrid(grid, { y? })`** (`nav/navConstrain`) is a standalone walkable-pass-through + wall-slide helper: it passes through walkable moves, slides along walls at the navmesh boundary instead of stopping dead, and optionally remaps `y` to the grid. Its `(proposed, entity)` shape doesn't match `PlayerMovementConfig.beforeCommit`'s `(frame) => [x,y,z]` signature, so wire it in with a small adapter closure (see "Controller kinematics" above) to wall a player/AI to the same navmesh `findPath` already routes against.
|
|
1334
|
-
## AI — director, threat, jobs, crowds (`ai/*`)
|
|
1335
|
-
Renderer-free AI over the same navmesh (`findPath`/`pathFollow`) gameplay already uses. Everything ticks on **game-time `dt`** (the `ctx.time` simClock delta), so it obeys pause and fast-forward for free. Manifests, patrol routes, job definitions, threat weights, and POIs are **game data** — the primitives own the loop, the catalog owns the content.
|
|
1336
|
-
- **Spawn director** (`ai/spawnDirector`) — budgets and escalates spawns for wave shooters and difficulty directors (Brotato, Bloons TD 6, Risk of Rain 2, Helldivers 2, Deep Rock Galactic). `createSpawnDirectorState(config)` then pure `advanceSpawnDirector(config, state, dt, { alive, players? })` → `{ state, spawns: SpawnRequest[] }`. Each `WaveManifest` grants a `budget` spent on affordable weighted `SpawnEntry`s (`cost`/`weight`/`minWave`), capped by `maxAlive`; `duration` auto-advances waves (or call `advanceWave` on "wave cleared"). Budget also trickles via `budgetPerSecond`, ramps a difficulty curve with `escalationPerSecond` (grows with sim-time), scales with `playerBudgetPerSecond`, and surges on `raiseAlert(state, amount)` decaying over time (bug-breach/dropship escalation). Seeded (`seed`) so ticks are deterministic. `pickSpawnPoint(points, players, { roll, bias })` biases placement toward (or away from) players. `config.spawnPoints?: NavPoint[]` lets the director pick a point itself: each `SpawnRequest` then also carries `point` (the chosen `[x, z]`, biased by `config.spawnPointBias`) and `laneId` (the point's index into `spawnPoints`) — feed multiple named lanes/portals and read `laneId` back to route the spawned entity down its lane instead of correlating positions by hand.
|
|
1337
|
-
- **Threat table** (`ai/threat`) — MMO/extraction aggro (Escape from Tarkov, WoW-style tanking). `createThreatTable({ decayPerSecond?, max?, forgetBelow? })`: `add(source, amount)` accumulates, `decay(dt)` bleeds off per game-second and forgets emptied sources, `highest({ current?, stickiness? })` returns the top-threat source to feed `scene/targeting` — `stickiness` (e.g. 1.1) keeps the current target until another exceeds it by that factor, so aggro doesn't jitter. `ranked()` for a threat meter.
|
|
1338
|
-
- **Patrol** (`scene/behaviors`) — `patrol({ waypoints, speed, loop? })` is a `BehaviorDescriptor` (a route is data) that layers a fixed beat on top of `wander`; drive it with `createPathFollow`/`advancePathFollow` (lane creeps, scav patrols in Deadlock/Tarkov). Route waypoints between guard posts with `findPath`.
|
|
1339
|
-
- **Job board** (`ai/jobBoard`) — colony/companion task assignment (Palworld stations, Schedule I employees, Sons of the Forest directives). `createJobBoard()`: `post(job)` a `JobDef` (`station`, `work` seconds, `priority`, `arriveRadius`, `repeat`), `claim(worker)` auto-pulls the highest-priority queued job or `assign(worker, jobId)` for a player order (steals it from its holder), `release` requeues. Per tick `advance(worker, dt, { distanceToStation })` runs the state machine `travelling → working → done` (path to `station(worker)` via `findPath`, occupy, run the loop), returning a `JobReport` on completion; `repeat` jobs re-run as a production loop and report each cycle.
|
|
1340
|
-
- **Crowd flow** (`ai/crowd`) — many agents routing to their own points of interest with congestion (Two Point Museum corridors, Dave the Diver seating). `computeFlowField(grid, goals, { clearance?, congestion? })` runs Dijkstra from the goals over the walkable grid → `direction(point)`/`next(point)` steer any agent toward the nearest goal (no per-agent A*). `createCrowdField(grid)` tracks per-cell occupancy (`enter`/`leave`/`count`); pass `crowd.penalty(weight)` as the field's `congestion` to reroute flow around crowded cells each tick. `selectPoi(pois, from, { roll, occupancy?, distanceBias?, distance? })` weights a POI by appeal and proximity, skips ones at `capacity`, and accepts a `distance` override (e.g. `findPath` length) to choose over the navmesh, not line-of-sight.
|
|
1341
|
-
## Map, fog of war & ping
|
|
1342
|
-
Minimap/world-map/fog/compass state is renderer-free core (`world/*`), the top-down terrain image bakes in the shell, and the minimap/compass/world-map are react components. Ping rides the existing party + feed — it is not a new channel.
|
|
1343
|
-
- **Markers** (`world/markers`): `createMarkerSet()` is a reactive keyed set of `MapMarker { id, kind, position, label?, owner?, expiresAt?, meta? }` — `add`/`remove`/`get`/`list`/`query({ kind, owner, near, radius })`/`prune(now)`/`subscribe`. `kind` is a game-owned catalog string; `DEFAULT_MARKER_KINDS` (objective/enemy/loot/location/danger/ping/player/ally) supplies colors + glyphs the react map reads (override with your own `MarkerKindStyle` palette). Objective/entity/loot markers all live here.
|
|
1344
|
-
- **Fog of war** (`world/fog`): `createFogField({ bounds, cellSize })` is reveal-on-event — `reveal(x, z, radius?)` (a dig/act), `revealAlong(from, to, radius?)` (a walked trail); once a cell is revealed it stays revealed. `isRevealed`/`fraction`/`cells()` (stable snapshot for rendering)/`reset`/`subscribe`.
|
|
1345
|
-
- **Minimap math** (`world/minimap`): pure projection + bearings — `projectToMinimap(worldPoint, { center, worldRadius, size, rotate? })` → pixel `{ x, y, inside, distance }` (north = −Z maps up), `clampToMinimapEdge` for off-map markers, `compassBearing(from, to)`/`headingToBearing(yaw)`/`bearingToCardinal`/`relativeBearing` for the compass strip.
|
|
1346
|
-
- **Ping** (`game/ping`): `classifyPing(hit, { roleOf, categoryOf }, options?)` turns a G1 `pointer.worldHit()` `PointerHit` into a category (hostile entity → `enemy`, tagged object → its catalog category, open ground/ally → `location`). `createPingSystem({ markers, feed, party?, ttlMs?, classify, classifyOptions? })` composes classify + broadcast: `ping(from, hit, category?)` classifies, drops a categorized marker, and pushes the `PingPayload` to the party feed under `PING_FEED_ACTION` (`"party.ping"`) — the shell's feed bridge fans it to the squad. `DEFAULT_PING_CATEGORIES` is the enemy/loot/location/danger wheel. Enable the verb with the `pointer.pingCommand` field of `defineGame({...})`: the shell binds the `ping` input action → `worldHit()` → runs your command with `{ point, entity, object, normal }`.
|
|
1347
|
-
- **Shell render** (`@jgengine/shell/map`): `bakeTerrainMap(field, bounds, { resolution? })` renders a `TerrainField`/`RegionField` to a top-down PNG data-URL for the map background; `MapMarkerBeacons({ markers })` renders world-space beacons (the visible side of a ping) — wire via the `WorldOverlay` field of `defineGame({...})`. See the `extraction-map` demo game.
|
|
1348
|
-
## Sensors, vision & observer tools (`sensor/`)
|
|
1349
|
-
Pure `@jgengine/core/sensor/*` primitives for querying and surfacing world state the player can't normally see or reach through the standard occlusion/proximity rules — reveal vision, hidden-state sensors, photo-mode framing, and session replay. Shell renderers/HUD pieces live in `@jgengine/shell/vision` and `@jgengine/shell/replay`.
|
|
1350
|
-
| Primitive | Answers |
|
|
1351
|
-
|-----------|---------|
|
|
1352
|
-
| `createRevealQuery({ resolvePosition, resolveTags, candidates })` → `RevealQuery` | `inRadius(center, radius, tags)` — occlusion-ignoring tagged-entity radius query (Dark Sight / detective-vision reveal, #115). `inRadius` already never checks occlusion (only combat's AoE `effect()` layers a LoS filter on top of it) — this is that same query shaped for a vision readout: scoped to catalog-declared tags, sorted nearest-first |
|
|
1353
|
-
| `probeHiddenState(origin, sources, { range, variableId, falloff? })` / `probeHiddenStateAll(...)` → `SensorReading \| null` | A sensor verb: reads a hidden zone/entity state variable (EMF/thermometer/geiger, #116) in range, strongest reading first; `strength` falls off linearly with distance by default |
|
|
1354
|
-
| `projectToView(camera, point)` → `FrustumProjection` | Pure camera-frustum projection (no three.js) — `inView`, `screenX/screenY` (-1..1), `distance` |
|
|
1355
|
-
| `framingScore(projection, config?)` → `number` | 0..1 framing quality from screen-center placement + distance-to-ideal (photo-mode "is this subject framed", #117) |
|
|
1356
|
-
| `createFrustumSensor(config?)` → `FrustumSensor` | `tick(camera, targets, dt)` — per-target in-view + framing + `dwellSeconds` (resets the instant a target leaves frame); a view-frustum sensor on a held camera object (Content Warning-style monster-filming scoring) |
|
|
1357
|
-
| `createRecordingBuffer(options?)` → `RecordingBuffer<T>` | `append(t, data)` / `seek(t)` / `range(fromT, toT)` — a session-recording buffer for replay/photo mode/kill-cam (#120), keyed on game-time so pause/fast-forward scrub consistently |
|
|
1358
|
-
| `colorDistance(a, b)` / `concealmentScore(entityColors, backgroundColors)` / `createConcealmentSensor(config?)` → `ConcealmentSensor` | Camouflage/blend-in scoring — how well an entity's palette matches its surroundings (hide-and-seek, stealth camo checks) against a `threshold` |
|
|
1359
|
-
| `createFreezeMonitor(config?)` → `FreezeMonitor` | Detects a tracked subject moving past a tolerance speed during a "freeze" window (red-light-green-light, statue games) and reports `FreezeViolation`s |
|
|
1360
|
-
Shell wiring: `@jgengine/shell/vision/RevealVision` (`RevealHighlights` — depth-test-disabled 3D highlight meshes for tagged entities in radius, meant for `WorldOverlay`; `RevealScreenTint` — full-screen CSS tint for "vision mode is on", meant for `GameUI`), `@jgengine/shell/vision/HiddenStateProbeHud` (`SensorReadoutMeter` — needle-strength HUD readout), `@jgengine/shell/vision/FrustumSensorHud` (`FrustumSensorReadout` — drives the sensor off the live render camera via `useThree`/`useFrame`, portals its HUD through drei's `Html fullscreen`), `@jgengine/shell/replay/useSessionRecorder` (records an entity's pose into a `RecordingBuffer` every frame; drive an observer-cam ghost, scrubber, or kill-cam export from it). The detached spectator/photo cam itself is the `observer` camera rig (see Camera rigs above) — bind it to any entity or fixed point.
|
|
1361
|
-
|
|
1362
|
-
## World features
|
|
1363
|
-
|
|
1364
|
-
Renderer-free world surface — query primitives, environment fields + weather + realm composition, survival meters/moodles, interactive building & terraform, the optional headless physics world, vehicles/mounts/racing, and spawn placement. Full surface: **[reference/world.md](reference/world.md)**.
|
|
1365
|
-
|
|
1366
|
-
## Turn-based & tactics (renderer-free)
|
|
1367
|
-
|
|
1368
|
-
Pure-`core` primitives for turn-based, grid-tactics, and card games — every one is a stateful factory with matching pure math, and every stateful piece exposes `capture()`/`restore()` so it plugs straight into the snapshot store. Overlays and tile art are the shell's/game's job; these ship the logic.
|
|
1369
|
-
|
|
1370
|
-
- **`turn/turnLoop` — `createTurnLoop(config)`.** An initiative machine over an ordered participant list with optional `phases` and per-turn action-economy `pools`. `advanceTurn()` walks the order (round++ on wrap) and **resets the entering participant's pools**; `advancePhase()` steps phases then rolls into the next turn. Pools are catalog data (`{ id, max, start? }`) — a single Slay-the-Spire energy pool or BG3's Action/Bonus/Movement/Reaction set, spent independently via `spend/canSpend/gain/refill`. `setOrder`/`addParticipant`/`removeParticipant` re-roll initiative without losing the active pointer. `config.onTurnStart?(participantId)`/`onTurnEnd?(participantId)` fire on every `advanceTurn()` transition (start also fires once for the initial participant at construction) — hang status-effect ticks, "your turn" banners, or AI-turn kickoff here instead of diffing `state()` between ticks yourself. `ctx.game.turn.loop(id, config?)` is the runtime-wired accessor: lazily creates (config required the first call) or returns the existing notify-wrapped loop for `id`, so a HUD bound to `ctx.subscribe` re-renders on every turn/phase/pool change with no separate store to wire.
|
|
1371
|
-
- **`turn/commit` — `createCommitController({ mode })`**, also hosted at `turnLoop.commit`. Three commit modes: `immediate` (submit resolves now), `simultaneous` (sealed hidden submissions → `reveal()` once `allReady()`, deterministic order — Marvel Snap), and `rewind` (visible `pending()` → `rewind()` to discard or `commit()` to finalize).
|
|
1372
|
-
- **`turn/intent` — `createIntentBoard()`.** A minimal per-participant "what will you do" board, lighter than a full commit round: `declare(participantId, { kind, magnitude?, targetId?, note? })` records one intent per participant (overwriting any prior undeclared one), `peek(participantId)` reads without clearing, `all()` lists every declared `[participantId, intent]` pair (for an enemy-intent HUD row, Slay-the-Spire style), `consume(participantId)` reads and clears in one call, `clear(participantId?)` clears one or everyone. Reach for this when you need visible declared-but-not-yet-resolved intents (telegraphed enemy actions) without the full simultaneous-reveal machinery of `turn/commit`.
|
|
1373
|
-
- **`tactics/tacticalGrid` — `createTacticalGrid({ width, height, blocked?, diagonal?, world? })`.** Tile occupancy (one unit per tile), `reachable(from, budget)` flood-fill (respects walls + occupants), `path(from, to)` shortest route, and `push(id, dir, { distance, chain })` discrete knockback-to-tile — chained collisions transfer momentum through struck units (Into the Breach), or stop with a recorded `PushCollision` against `wall`/`edge`/another unit. `world: { origin: [x, z], tileSize }` (mirroring `navGrid`'s bounds+cellSize convention) turns on `worldToTile(x, z)` (world point → `Tile | null`, null outside the grid) and `tileToWorld(tile)` (a tile's world-space center) — the render/pointer-hit round trip between the tactics grid and the 3D scene; omit `world` for a grid used purely as abstract logic (both throw if called without it).
|
|
1374
|
-
- **`tactics/predictiveQuery` — `predictAreaEffect`/`predictArcEffect`/`predictTiles`.** A "would-this-effect-hit" query for pre-commit overlays and enemy-intent telegraphs. It reuses the **exact** AoE/LoS targeting behind `ctx.scene.entity.effect` (`combat/effects` `resolveAreaTargets`) so the predicted target set matches what the effect would actually drain — without committing any state change.
|
|
1375
|
-
- **`tactics/snapshot` — `createSnapshotStore()`.** Cheap, repeatable turn-undo: `register(id, slice)` any `capture()/restore()` slice (the grid, surfaces, and turn loop all qualify), then `capture()/restore()` a deep-cloned snapshot or use the `push()/pop()` undo stack. `deepClone` handles objects/arrays/Map/Set so a held snapshot is immune to later mutation.
|
|
1376
|
-
- **`tactics/surface` — `createSurfaceLayer({ kinds, reactions })`.** A stateful tile surface layer with its own `tick(dt)` (timed surfaces decay + expire) and a **combination matrix** — `reactions` is data (`{ when: [a, b], result }`), so grease+fire→fire and water+lightning→electrified are catalog entries, not hard-coded. Distinct from terrain/water; drive its tick from `onTick`'s game-time `dt`.
|
|
1459
|
+
## 6. Game UI primitives
|
|
1377
1460
|
|
|
1378
|
-
|
|
1461
|
+
Prefer small headless or lightly styled primitives over a giant universal design system. Useful concepts include:
|
|
1379
1462
|
|
|
1380
|
-
|
|
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`
|
|
1381
1479
|
|
|
1382
|
-
|
|
1383
|
-
- **`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.
|
|
1384
|
-
- **`createJsonDataSource<T>(url, options?)`** (`data/jsonDataSource`) — sugar combining the two above: a `DataSource<T>` whose `load` calls `fetchJson(url, options)`.
|
|
1385
|
-
- **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.
|
|
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.
|
|
1386
1481
|
|
|
1387
|
-
|
|
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`.
|
|
1388
1483
|
|
|
1389
|
-
|
|
1484
|
+
Do not create a primitive whose only value is wrapping a `div` with border radius.
|
|
1390
1485
|
|
|
1391
|
-
##
|
|
1486
|
+
## 7. Complete interaction states
|
|
1392
1487
|
|
|
1393
|
-
|
|
1488
|
+
Every interactive element needs intentional states:
|
|
1394
1489
|
|
|
1395
|
-
|
|
1490
|
+
- rest
|
|
1491
|
+
- hover where applicable
|
|
1492
|
+
- keyboard/controller focus
|
|
1493
|
+
- pressed
|
|
1494
|
+
- selected
|
|
1495
|
+
- disabled
|
|
1496
|
+
- success
|
|
1497
|
+
- failure
|
|
1498
|
+
- warning
|
|
1396
1499
|
|
|
1397
|
-
|
|
1500
|
+
Do not communicate all states with background-color changes alone. Use appropriate combinations of:
|
|
1398
1501
|
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
|
1406
1512
|
|
|
1407
|
-
|
|
1513
|
+
Focus must look authored while remaining accessible. Menus should support keyboard/controller-style focus navigation when practical.
|
|
1408
1514
|
|
|
1409
|
-
|
|
1410
|
-
// loop.ts — top-level export const, nothing else
|
|
1411
|
-
export const GRAVITY = -22;
|
|
1412
|
-
export const SKY_COLOR = "#87ceeb";
|
|
1413
|
-
export const GOD_MODE = false;
|
|
1515
|
+
## Rendering — post-processing, lighting, shadows
|
|
1414
1516
|
|
|
1415
|
-
|
|
1416
|
-
export const TUNING = { reach: 6, spawnRate: 0.4, fogColor: "#334455" };
|
|
1417
|
-
```
|
|
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:
|
|
1418
1518
|
|
|
1419
|
-
|
|
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.
|
|
1420
1523
|
|
|
1421
|
-
|
|
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.
|
|
1422
1525
|
|
|
1423
|
-
|
|
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.
|
|
1424
1527
|
|
|
1425
|
-
|
|
1528
|
+
## Settings menu
|
|
1426
1529
|
|
|
1427
|
-
**
|
|
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`):
|
|
1428
1531
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
for
|
|
1433
|
-
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
entityById: (id) => entityEntries.get(id) ?? null,
|
|
1437
|
-
};
|
|
1438
|
-
```
|
|
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.
|
|
1439
1539
|
|
|
1440
|
-
|
|
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.
|
|
1441
1541
|
|
|
1442
|
-
|
|
1443
|
-
// RIGHT — map ids to the catalog def itself; build the entry fresh on every lookup
|
|
1444
|
-
const playersById = new Map(players.map((p) => [p.id, p]));
|
|
1445
|
-
function entityById(id: string): GameContextEntityEntry | null {
|
|
1446
|
-
const p = playersById.get(id);
|
|
1447
|
-
return p === undefined ? null : { stats: p.stats, movement: { poses: p.poses, walkSpeed: p.walkSpeed } };
|
|
1448
|
-
}
|
|
1449
|
-
export const content: GameContextContent = { entityById };
|
|
1450
|
-
```
|
|
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.
|
|
1451
1543
|
|
|
1452
|
-
|
|
1544
|
+
## 8. Motion and game feel
|
|
1453
1545
|
|
|
1454
|
-
|
|
1546
|
+
Add purposeful motion for:
|
|
1455
1547
|
|
|
1456
|
-
|
|
1457
|
-
|
|
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
|
|
1458
1558
|
|
|
1459
|
-
|
|
1460
|
-
```
|
|
1559
|
+
Motion should be brief, readable, interruptible when necessary, coordinated, consistent with the game’s art direction, and respectful of reduced-motion settings.
|
|
1461
1560
|
|
|
1462
|
-
|
|
1561
|
+
Do not animate everything constantly. Motion communicates hierarchy, cause and effect, urgency, and state changes.
|
|
1463
1562
|
|
|
1464
|
-
|
|
1563
|
+
## 9. Mobile controls are genre-authored
|
|
1465
1564
|
|
|
1466
|
-
|
|
1565
|
+
Shared input mechanics may remain shared. Their visual treatment and arrangement must match the game.
|
|
1467
1566
|
|
|
1468
|
-
|
|
1567
|
+
Examples:
|
|
1469
1568
|
|
|
1470
|
-
|
|
1569
|
+
**Driving**
|
|
1570
|
+
Steering region or wheel, accelerator, brake, handbrake, optional camera/map control.
|
|
1471
1571
|
|
|
1472
|
-
**
|
|
1572
|
+
**Stealth**
|
|
1573
|
+
Movement zone, sneak/crouch hold, contextual interaction, temporary map/schedule control.
|
|
1473
1574
|
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
| [Kenney.nl](https://kenney.nl) | 40,000+ CC0 assets: characters, buildings, nature, vehicles, weapons, UI, audio — the broadest single library |
|
|
1477
|
-
| [Quaternius](https://quaternius.com) / [KayKit](https://kaylousberg.itch.io) | CC0 low-poly packs incl. **rigged + animated characters**: medieval, sci-fi, dungeons, animals, adventurers |
|
|
1478
|
-
| [Poly Haven](https://polyhaven.com) / [ambientCG](https://ambientcg.com) | CC0 PBR textures, HDRIs, materials — the floor comes from here, never a flat color |
|
|
1479
|
-
| [Poly Pizza](https://poly.pizza) | Search engine over thousands of CC0 low-poly models for one specific thing |
|
|
1480
|
-
| [Game-Icons.net](https://game-icons.net) (CC BY 3.0 — credit it) / Kenney UI packs (CC0) | 4,000+ item/ability **icon silhouettes** — the registry `game-icon` item covers common HUD glyphs first |
|
|
1481
|
-
| [itch.io CC0 3D tag](https://itch.io/game-assets/assets-cc0/tag-3d) / [OpenGameArt](https://opengameart.org) | Long tail — **check the license per asset**, CC0 filter first |
|
|
1482
|
-
| [Mixamo](https://www.mixamo.com) | Free humanoid animations (Adobe license — fine for shipped games, not CC0) |
|
|
1483
|
-
| Kenney audio / [freesound CC0 filter](https://freesound.org) | Hit sounds, UI clicks, ambience |
|
|
1575
|
+
**Shooter**
|
|
1576
|
+
Movement zone, aim region, fire/action cluster, weapon or ability controls.
|
|
1484
1577
|
|
|
1485
|
-
**
|
|
1578
|
+
**Puzzle/arcade**
|
|
1579
|
+
Direct drag, tap, swipe, paddle region, or discrete directions. Do not add a joystick without a gameplay reason.
|
|
1486
1580
|
|
|
1487
|
-
|
|
1488
|
-
2. **License discipline.** CC0 needs nothing; anything else gets a line in `src/game/assets-credits.md` (source, author, license). Never ship an asset you can't name the license of.
|
|
1489
|
-
3. **Wire through the engine seams.** GLB models live in the game's `src/game/assets.ts` render catalog keyed by catalog id; billboards via `entitySprites`, real meshes via `entityModels`/`objectModels` in `defineGame({...})`; ground/skies belong to the world layer. Catalog `model` fields reference asset keys — never file paths in game logic. Source models through **`@jgengine/assets`** (`buildCatalog({ basePath })` → resolve ids/aliases → urls); `pull` packs into your app's `public/models/` (extracts Kenney's shared `Textures/` alongside the GLBs so models render textured). Network-restricted: `pull` falls back through `--mirror <baseUrl>` / `JGENGINE_ASSETS_MIRROR`, and `--offline` fails fast — see the package README for the fallback order and add/import flow.
|
|
1490
|
-
4. **Coverage follows the content budget.** Every entity family, placed object, and held item maps to a real asset *before* the catalog entry ships. If the pack lacks a model, restyle the noun to one it has — rename the fantasy, don't ship a cube.
|
|
1491
|
-
5. **Scale/pivot sanity.** `@jgengine/assets` measures each model's footprint/center/`minY` at reindex and ships them on the catalog entry (`catalog.resolve(id).dims`); with `objectModels` anchor `"center"` (the default) the shell centers the footprint on the placement point and ground-snaps the lowest vertex — so `object.place(id, cellX, y, cellZ, { rotation })` renders centered + grounded with no pivot math and no `dimensions.ts`. Anchor `"origin"` opts back into the raw GLB origin. Check the first placement of each model against its catalog `footprint`; one wrong pivot repeated 100 times is a rebuild.
|
|
1492
|
-
6. **Item and ability icons are assets too.** Every hotbar/inventory/ability slot renders a real, distinct icon — the registry `game-icon` catalog (`iconForItemId`/`iconForAction`) or a Game-Icons/Kenney silhouette — per the UI quality bar's real-icons rule.
|
|
1581
|
+
Requirements:
|
|
1493
1582
|
|
|
1494
|
-
|
|
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
|
|
1495
1590
|
|
|
1496
|
-
|
|
1497
|
-
- **Tycoon/lab**: objects + `slotInventory`, `plots()`, configure via prompt → command.
|
|
1498
|
-
- **Shooter**: `fireProjectile`/`settleProjectile`; grenades settle → `effect({ at, radius })`; `movement.poses`/`aim` + zoom modifier; `servers({ … })` + game-owned `server.mode`; loadout classes from commands.
|
|
1499
|
-
- **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"`.
|
|
1500
|
-
- **All combat games**: react to `entity.died` (feed/leaderboard/score) — never poll HP.
|
|
1591
|
+
Do not use one generic translucent controller across all games.
|
|
1501
1592
|
|
|
1502
|
-
|
|
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.
|
|
1503
1597
|
|
|
1504
|
-
|
|
1505
|
-
|-------|-------|
|
|
1506
|
-
| Player tuning in `defineGame` | Entity catalog `movement` + stats |
|
|
1507
|
-
| `behaviors: […]` on place/spawn | Catalog entry |
|
|
1508
|
-
| Engine `weapon.fire` / `consumable.use` / `combat.*` | `item.use` + catalog `use` → game handler |
|
|
1509
|
-
| `ItemUseInput.to` for targets | `getTarget(from)` in handlers |
|
|
1510
|
-
| `effect({ to })` for gunshots | `fireProjectile` + `settleProjectile` |
|
|
1511
|
-
| Polling HP in `onTick` for kills | `entity.died` event |
|
|
1512
|
-
| `combat.lootTable` / `loot.enemy` | `onDeath` on the entity that died |
|
|
1513
|
-
| Hand-rolled `Math.random()` loot in commands | `lootTable()` + `ctx.game.loot.roll` |
|
|
1514
|
-
| Hand-rolled `xpForLevel`/`levelFromXp` | `game/progression` `curve()` + `leveling()` |
|
|
1515
|
-
| Hardcoded shop arrays | `item.trade.shops` + `tradableAt` |
|
|
1516
|
-
| Kit seeding via scattered `put`/`grant` | `applyLoadout` |
|
|
1517
|
-
| Per-user quest state hand-rolled | `game.quest.register` + binds |
|
|
1518
|
-
| `useKillFeed` / per-domain feed hooks | `useFeed({ action })` |
|
|
1519
|
-
| Raw keys in game logic | `defineGame` input actions |
|
|
1520
|
-
| Positioning inside `ui/components/` or on primitives (`CurrencyPill className="absolute …"`) | Screen wrappers in `GameUI.tsx` only |
|
|
1521
|
-
| Game UI classes without `@source` in host CSS | `@source` entries for your game dirs + `node_modules/@jgengine/{shell,react}` |
|
|
1522
|
-
| One file per catalog entry / per brand | Dense `<domain>/catalog.ts` |
|
|
1523
|
-
| Convex mutations called from game code | `commands.run` through the `GameBackend` transport |
|
|
1524
|
-
| 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-newgame`) |
|
|
1525
|
-
| 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 |
|
|
1526
|
-
| Game nouns in this skill | Engine primitives + placeholder ids only |
|
|
1598
|
+
## 10. Progressive instruction
|
|
1527
1599
|
|
|
1528
|
-
|
|
1600
|
+
Do not leave large control grids visible during gameplay.
|
|
1529
1601
|
|
|
1530
|
-
|
|
1602
|
+
Prefer:
|
|
1531
1603
|
|
|
1532
|
-
-
|
|
1533
|
-
-
|
|
1534
|
-
-
|
|
1535
|
-
-
|
|
1536
|
-
-
|
|
1537
|
-
-
|
|
1538
|
-
-
|
|
1539
|
-
- [ ] Player spawns with `id === ctx.player.userId`
|
|
1540
|
-
- [ ] `game/ui/GameUI.tsx` owns layout; components use `@jgengine/react` hooks
|
|
1541
|
-
- [ ] UI passes the **quality bar** above (contrast, scale, framing, genre fit) — not just hook wiring
|
|
1542
|
-
- [ ] Camera tuned via `camera` in `defineGame({...})` — defaults untouched means the feel was never checked
|
|
1543
|
-
- [ ] 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
|
|
1544
|
-
- [ ] HUD screenshotted over a staged `GameUiPreview` scenario and **judged by looking at the image** against the UI quality bar in [`reference/ui-react.md`](reference/ui-react.md) — the final human glance, not the verification loop
|
|
1545
|
-
- [ ] Co-located bun tests for pure game math (curves, cooldowns, spawn logic)
|
|
1546
|
-
- [ ] Multiplayer via adapter config only; no direct backend calls
|
|
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
|
|
1547
1611
|
|
|
1548
|
-
|
|
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.
|
|
1549
1613
|
|
|
1550
|
-
|
|
1551
|
-
defineGame (shell) engine fields (assets, world, physics, inventories, input, server, save, time, feed, multiplayer)
|
|
1552
|
-
+ presentation fields (content, loop, GameUI, camera, environment, shadows, movement, devtools, …) in one call — smart defaults fill the rest
|
|
1553
|
-
defineGame (core) the underlying engine-only primitive: assets, world, physics, inventories, input, server, save, time, feed, multiplayer, loop
|
|
1554
|
-
PlayableGame { game, content, loop, GameUI, camera, … } — the runner contract `defineGame` (shell) returns
|
|
1555
|
-
GameContext ctx.scene / ctx.game / ctx.player / ctx.item / ctx.camera / ctx.input + subscribe/version
|
|
1556
|
-
scene.object place, remove, move, rotate, at, setVisual (per-instance ObjectVisual: scale/color/opacity override)
|
|
1557
|
-
scene.entity spawn (anchor/offset), despawn, setPose, update; stats; targeting; effects;
|
|
1558
|
-
projectiles (object-aware raycasts); spatial queries (opt-in grid broadphase)
|
|
1559
|
-
entity.stats get / set / delta — bounded stats (health, mana, xp, level) on instances
|
|
1560
|
-
progression game/progression — curve() / leveling() over bounded xp/level stats
|
|
1561
|
-
item.use catalog `use` → GameContext handler; no input.to
|
|
1562
|
-
effects drain-signed magnitudes; receive.<effect>.order; AoE = effect + at/radius/los
|
|
1563
|
-
projectiles willHit → fire → settle; ballistic via weapon.projectile
|
|
1564
|
-
death onDeath (reason-aware drops/command), entity.died, auto kill attribution + drop grant
|
|
1565
|
-
game.loot register / has / roll / grantToPlayer (lootTable() = pure factory)
|
|
1566
|
-
game.trade canBuy / canSell / buy / sell / tradableAt
|
|
1567
|
-
game.quest register, accept…turnIn, bind(entity.died | inventory.added), declarative rewards
|
|
1568
|
-
game.social friends (persisted, requests listable), party (ephemeral, invites listable), presence, emotes (nearby broadcast), worldInvites (accept → join target)
|
|
1569
|
-
game.chat send / whisper / history / register — global/party/proximity channels, rate-limited, mute via blocked set
|
|
1570
|
-
game.roster capture / release / list / setEquipped — persisted owned-creature roster
|
|
1571
|
-
game.store/cards/turn store: keyed reactive slot; cards.pile(id): lazy CardPile; turn.loop(id): lazy TurnLoop
|
|
1572
|
-
game.events/feed/leaderboard on / bind+push+recent / track+increment+getTop
|
|
1573
|
-
devtools F2 overlay (Perf/Tune/Logs/Net/Keys); zero-annotation — top-level export const number/boolean/color + exported flat tables auto-discover into Tune; tunable() is the optional low-level escape hatch; snapshotDevtools()/window.__JG_DEVTOOLS.snapshot() → DevtoolsSnapshot (+ discovered[]); probes.register(name, read) adds a Perf gauge
|
|
1574
|
-
applyLoadout all-or-nothing kit seeding per userId
|
|
1575
|
-
player.movement pose (hitboxes) + aim (zoom modifier)
|
|
1576
|
-
player.motion impulse / setVerticalVelocity / setY — vertical-motion seam into the shell's frame driver
|
|
1577
|
-
player.possession own/disown/owns/listOwned + active + possess — control-swap, rebinds shell camera
|
|
1578
|
-
player.cosmetics register + apply/equip + get — per-player appearance slots, no gameplay effect
|
|
1579
|
-
scene.entity.form register + shapeshift/revert + active/abilities — movement+ability+mesh bundle, game-time duration
|
|
1580
|
-
proximityPrompt { radius, display: {kind}, invoke } — one float-UI primitive
|
|
1581
|
-
skillCheck/qte evaluateSkillCheck (moving zone + window) / evaluateQteSequence (timed steps)
|
|
1582
|
-
captureCheck captureChance / rollCapture — hp% + catchPower → probability
|
|
1583
|
-
dialogue check DialogueChoice.check (roll vs DC + advantage/disadvantage) → onSuccess/onFailure
|
|
1584
|
-
world features biomes / voxel / plots / tilemap / flat descriptors
|
|
1585
|
-
physics/physicsWorld optional headless rigid-body sim (PhysicsWorld) — not the defineGame physics field (gravity/jumpVelocity, honored by the kinematics controller); bodies are box (halfExtents) or sphere (radius)
|
|
1586
|
-
physics/ballisticSweep createBallisticSweep(world) → arc-vs-body hit test; wire into ProjectileSystemDeps.sweepBallistic
|
|
1587
|
-
anim/easing lerp/clamp01/smoothstep/tween/timedProgress + easeIn/Out/InOut Quad/Cubic + easeOutBack/Elastic — pure 0..1 tweening math
|
|
1588
|
-
data/dataSource createDataSource/createJsonDataSource — idle/loading/ready/error async state + polling; data/fetchJson + data/devProxy for CORS-safe dev fetches
|
|
1589
|
-
audio/audioFalloff computeFalloffGain / resolveEmitterGain — pure distance→gain curve; shell plays it
|
|
1590
|
-
time/beatClock createBeatClock (BPM ticks) + createBeatInputBuffer (buffered action → next beat)
|
|
1591
|
-
ws/voiceChannel createVoiceChannelRouter — positional falloff + simultaneous non-positional channels
|
|
1592
|
-
multiplayer/identity AuthSession + sessionPlayer + resolveGuestSession — Clerk/better-auth via react structural adapters
|
|
1593
|
-
multiplayer/chatContract ChatTransport (hooks) / ChatSync (callbacks) — ws + convex bindings, local for dev
|
|
1594
|
-
multiplayer/voiceContract VoiceTransport (join/leave/publish/subscribers) + createPushToTalk — media plane host-supplied
|
|
1595
|
-
GameBackend { transport, feeds?, presence? } — Convex is one adapter (createConvexBackend)
|
|
1596
|
-
adapter kinds offline / ws / convex / socketIo / p2p / lan (+ fly({app}) ws sugar) — runtime/adapter
|
|
1597
|
-
ws/pipe TransportPipe/TransportPipeFactory — any bidirectional string channel (webSocketPipe default)
|
|
1598
|
-
ws/host, ws/hostRouter browser-safe createGameHost + createHostRouter/loopbackPipe (node re-exports both)
|
|
1599
|
-
ws/peer createPeerHost/createPeerGuest — WebRTC P2P, host tab authoritative, copy-paste signal codes
|
|
1600
|
-
ws/socketIoPipe, node/socketIoServer socketIoPipe/createSocketIoBackend + attachGameSocketIoServer
|
|
1601
|
-
@jgengine/react GameProvider + hooks + headless primitives (incl. identity/chat/voice/social kits); layout only in GameUI.tsx
|
|
1602
|
-
```
|
|
1614
|
+
## 11. Reference directions for flagship games
|
|
1603
1615
|
|
|
1604
|
-
|
|
1616
|
+
### Clockwork Heist
|
|
1605
1617
|
|
|
1606
|
-
|
|
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.
|
|
1607
1619
|
|
|
1608
|
-
|
|
1620
|
+
### Canyon Chase
|
|
1609
1621
|
|
|
1610
|
-
|
|
1611
|
-
import { GameProvider, useSceneEntities, HealthBar } from "@jgengine/react";
|
|
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.
|
|
1612
1623
|
|
|
1613
|
-
|
|
1614
|
-
```
|
|
1624
|
+
### Brick Breaker
|
|
1615
1625
|
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
All hooks bind through the ctx change signal (`ctx.subscribe`/`ctx.version`):
|
|
1619
|
-
|
|
1620
|
-
| Hook | Returns |
|
|
1621
|
-
|------|---------|
|
|
1622
|
-
| `useGame()` / `usePlayer()` | `{ commands, events }` / `{ userId, isNew }` |
|
|
1623
|
-
| `useSceneEntities()` / `useSceneObjects()` | live snapshots for rendering |
|
|
1624
|
-
| `useWorldItems()` / `useNearestWorldItem(radius)` | ground-loot snapshots / nearest pickup for a HUD prompt |
|
|
1625
|
-
| `useEntityStat(instanceId, statId)` | `StatValue \| null` |
|
|
1626
|
-
| `useTarget(fromId)` | locked instanceId \| null |
|
|
1627
|
-
| `useInventory(id)` / `useCurrency(id)` | slots / balance |
|
|
1628
|
-
| `useFeed({ action, limit? })` | recent entries — kills, loot, any action |
|
|
1629
|
-
| `useQuestJournal()` | active quests + objective progress |
|
|
1630
|
-
| `useFriends()` / `useParty()` / `usePresence(userId)` / `useWorldInvites()` | social panels |
|
|
1631
|
-
| `useFriendRequests()` / `usePartyInvites()` | pending inbound requests/invites for the local player |
|
|
1632
|
-
| `useWorldBrowser({ fetchSessions, filter?, limit?, refreshMs? })` | polls a host-supplied fetcher (e.g. `createWsBackend().browse`) through matchmaking's `browseSessions` |
|
|
1633
|
-
| `useSession()` / `useAuthedPlayer({ guestSeed? })` | auth session from `<GameIdentityProvider>` / the `{ userId, isNew }` player seam for `createGameContext` |
|
|
1634
|
-
| `useChat(channelId, { limit? })` | local-player-filtered recent messages from `ctx.game.chat` |
|
|
1635
|
-
| `useVoice({ transport?, channelId?, mode?, resolveRoutes? })` | mic capture + PTT + voice-channel roster over the `VoiceTransport` seam |
|
|
1636
|
-
| `useRoster(userId?)` | owned/captured roster entries for a user (defaults to the local player) |
|
|
1637
|
-
| `useLeaderboard(stat, { scope, limit? })` | `{ userId, value }[]` |
|
|
1638
|
-
| `useActivePrompt(prompts)` | nearest proximity prompt |
|
|
1639
|
-
| `useGameClock()` | clock snapshot (`now`, `paused`, `speed`, `calendar`) + `controls` (pause/play/setSpeed) |
|
|
1640
|
-
| `useLocalPlayerDead()` / `localPlayerEntity(entities, userId)` | death-screen gating; local player from a snapshot |
|
|
1641
|
-
| `useMarkers(markerSet)` / `useFog(fogField)` | live map-marker list / fog-cell snapshot (bind a core `MarkerSet`/`FogField`) |
|
|
1642
|
-
| `useGameStore()` | raw store handle — escape hatch under the typed hooks |
|
|
1643
|
-
| `useEngineState(store)` / `useEngineStore(store, selector)` / `useEngineEvent(store, event, handler)` | bind/select/subscribe against any `ReadableEngineStore<TState>` / `EventfulEngineStore<TEventMap>` — the escape hatch below `useGameStore()` for state that isn't wired into a typed hook yet |
|
|
1644
|
-
| `useHeldKeys()` | `(code: string) => boolean` — raw window keydown/keyup/blur-backed held-key predicate, no `ctx` needed; the primitive `useAxisChannel` and the shell's own movement sampling are built on |
|
|
1645
|
-
| `useAxisChannel(config: AxisChannelConfig)` | `{ channel: AxisChannel, isDown }` — wires `useHeldKeys` into a fresh `AxisChannel` (`@jgengine/core/input/axisInput`) for a per-frame `channel.sample(dt, isDown)`; a driving/twin-stick HUD or custom control scheme reads analog throttle/steer without touching `window` directly |
|
|
1646
|
-
|
|
1647
|
-
Import hooks from `@jgengine/react/hooks`, components from `@jgengine/react/components`, `GameProvider` from `@jgengine/react/provider` (the package uses deep paths like core). `useEngineState`, `ReadableEngineStore`, `useEngineStore`, `useEngineEvent` also ship from the main `@jgengine/react` entry point (defined in `@jgengine/react/engineStore`, re-exported at the package root) — no deep import required.
|
|
1648
|
-
|
|
1649
|
-
Headless components (className passthrough, no baked-in styling): `SlotGrid`, `HealthBar` (+ `fillClassName`), `CurrencyPill`, `ProximityPrompt`, `Screen`, `KeybindRow`, `DialogueBox` (+ `lineClassName`/`speakerClassName`/`choicesClassName`/`choiceClassName`/`checkClassName`, `rollCheck`-gated choices), `SkillCheckBar` (+ `trackClassName`/`zoneClassName`/`markerClassName`), `QteTrack` (+ `stepClassName`/`activeClassName`/`doneClassName`), `CaptureOdds` (+ `fillClassName`), `ToastStack`, `DeathScreen`, `LevelUpFlash`. Map components (bind a core `MarkerSet`/`FogField`, `kindStyles` palette overridable): `Minimap` (framed circular player-centered map — fog + markers + facing arrow, optional baked terrain `background`+`mapBounds`), `Compass` (facing strip with cardinals + marker pips), `WorldMap` (full-bounds top-down overlay). Not yet implemented: `useServer`, `useDialogue`.
|
|
1650
|
-
**Identity (`@jgengine/react/identity`)** — `<GameIdentityProvider source={…}>` + `useSession()`. Sources: `clerkIdentity({ isLoaded, isSignedIn, user })` maps Clerk's `useUser()` shape, `betterAuthIdentity({ data, isPending })` maps better-auth's `useSession()` shape (both pure structural mappers — no SDK imports, one line at the call site), `guestIdentity(seed?)` for local/dev. Gate UI with `<RequireSession fallback loading>`; `<UserBadge>` / `<SignOutButton>` are headless like everything else. `useAuthedPlayer({ guestSeed? })` returns the `{ userId, isNew }` to hand `createGameContext` — feed the player seam from the session instead of hand-picking a userId.
|
|
1651
|
-
**Chat (`@jgengine/react/chat`)** — headless `<ChatPanel>` (tabs + log + input composition with internal active-channel state), or compose `<ChannelTabs active onSelect>`, `<ChatLog channelId>` (auto-scrolls, `renderMessage` override), `<ChatInput channelId onSent onRejected>` yourself. All drive `ctx.game.chat` through `useChat`. `chatTransportFromSync(sync)` lifts a callback-style `ChatSync` (e.g. `createWsBackend(...).chatSyncFor(serverId)`) into the hook-shaped `ChatTransport` for remote chat.
|
|
1652
|
-
**Voice (`@jgengine/react/voice`)** — `useVoice()` once per channel: `getUserMedia` mic capture (`requestMic()`, tracks gated by transmission), push-to-talk via `createPushToTalk` (hold/toggle/openMic + mute), roster from `VoiceTransport.subscribers`, and per-speaker `gainFor(userId)` when you pass `resolveRoutes: () => router.resolveRoutes(myUserId)` from `@jgengine/ws/voiceChannel`. Hand the returned state to the headless `<PushToTalkButton voice>`, `<MicToggle voice>`, `<SpeakingIndicator voice userId>`, `<VoiceRoster voice>`.
|
|
1653
|
-
**Social (`@jgengine/react/social`)** — the headless social kit over `ctx.game.social`: friends (`<FriendsList>`, `<FriendRow>`, `<PresenceDot>`, `<AddFriendButton toUserId>`, `<FriendRequestsList>` with accept/decline), party (`<PartyFrame>`, `<PartyMemberRow>`, `<PartyInviteToast>`, `<LeavePartyButton>`), worlds (`<WorldBrowser listings onJoin>`, `<JoinByCode onJoin>` — normalizes codes, `<QuickMatchButton listings filter?>`, `<InviteToWorldButton toUserId target>`, `<WorldInviteToast onAccepted>` — hands you the `{ serverId, joinCode? }` join target), and `<EmoteWheel emotes>` over `emotes.play`. All className-passthrough with `data-*` hooks and `renderX` overrides; the `social-hub` demo in `apps/dev` (`?game=social-hub`) composes the whole kit.
|
|
1654
|
-
**Drag/rotate/drop/snap gesture layer** (`@jgengine/react/dragLayer`) — a 2-D UI-space gesture layer over the card/shaped-grid primitives, distinct from 3-D world drag. `useDragLayer<T>({ onDrop })` owns pointer-follow drag state (begin/rotate/setTarget/end); pair it with the headless, className-passthrough `DraggableCard` (right-click rotates), `DropZone` (reports the snapped `cellFromPoint` cell + active state), and `DragGhost` (a pointer-anchored preview). Drop resolution and overlap validation stay the game's job via `canPlace`/`placeShaped` from `inventory/shapedGrid` — Balatro hand→play drags, Backpack Hero grid placement, Slay-the-Spire card-onto-enemy targeting.
|
|
1655
|
-
|
|
1656
|
-
**Layout rule:** all **screen** positioning (`absolute`, `inset-*`, grid zones, flex regions) lives on wrappers inside `ui/GameUI.tsx` only. `ui/components/` files are content + hooks only — internal `relative`/`absolute` for bar overlays or slot badges inside a component is fine; never anchor a component to the viewport from a child file. Pass `className` to primitives for **visual** styling (colors, borders, size), not screen placement.
|
|
1657
|
-
|
|
1658
|
-
**Tailwind sources:** add `@source` entries in your CSS for your game source dirs plus `node_modules/@jgengine/shell` and `node_modules/@jgengine/react`. Without them, classes used in dynamically imported game code are **not generated** — layout wrappers in `GameUI.tsx` silently fail and every HUD cluster stacks in one corner.
|
|
1659
|
-
|
|
1660
|
-
## Visual HUD via the shadcn registry
|
|
1661
|
-
|
|
1662
|
-
Styled HUD components are **copy-in code**, not npm exports — install them from the JGengine registry with the shadcn CLI:
|
|
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.
|
|
1663
1627
|
|
|
1664
|
-
|
|
1665
|
-
npx shadcn@latest add https://jgengine.com/r/entity-vital-bar.json
|
|
1666
|
-
```
|
|
1628
|
+
These games must remain structurally distinct, not recolors of one component set.
|
|
1667
1629
|
|
|
1668
|
-
|
|
1630
|
+
## 12. Accessibility and performance
|
|
1669
1631
|
|
|
1670
|
-
|
|
1671
|
-
import { usePlayer } from "@jgengine/react/hooks";
|
|
1672
|
-
import { EntityVitalBar } from "@/components/ui/entity-vital-bar";
|
|
1632
|
+
Maintain:
|
|
1673
1633
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
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
|
|
1677
1644
|
|
|
1678
|
-
|
|
1645
|
+
Use blur, masks, textures, filters, and full-screen effects carefully, especially on mobile.
|
|
1679
1646
|
|
|
1680
|
-
|
|
1647
|
+
## 13. Screenshot verification is mandatory
|
|
1681
1648
|
|
|
1682
|
-
|
|
1649
|
+
Capture and inspect meaningful states:
|
|
1683
1650
|
|
|
1684
|
-
|
|
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
|
|
1685
1659
|
|
|
1686
|
-
|
|
1687
|
-
|-------------|---------|
|
|
1688
|
-
| **Contrast** | HUD text and borders readable on the game's scene background — never bare `text-stone-400` on near-black without a panel |
|
|
1689
|
-
| **Scale** | Primary HUD (unit frames, hotbar slots, menu buttons) ≥ 48px touch targets; body text ≥ `text-sm` (12px); key labels never below 11px |
|
|
1690
|
-
| **Distinct construction** | Unit frame, hotbar, currency, quests, and toasts must not share one card style — same `rounded border bg-stone-900/80 p-3` on everything reads identical and cheap |
|
|
1691
|
-
| **Real icons** | Every item/ability/hotbar slot shows a real, distinct silhouette or sprite (registry `game-icon`, asset-pack sprite) — never a gray box, first letter, emoji, or one generic shape reused everywhere |
|
|
1692
|
-
| **Hotbar / slots** | Icon per ability; keybind badge on the slot corner; hover/active state; empty slots visually distinct |
|
|
1693
|
-
| **Unit frames** | Name + level + labeled bars with numeric values; health/mana/resource colors genre-appropriate |
|
|
1694
|
-
| **Layout** | No overlapping anchors; reserve space for frames that appear conditionally (target, quest log) |
|
|
1695
|
-
| **Panels** | Modal/slide panels: title, close control, section headers, consistent chrome with the HUD |
|
|
1696
|
-
| **Feedback** | Errors, cooldowns, and empty actions surface to the player (toast, dim, shake) — not `console.warn` only. Error text is ephemeral floating combat text, never a bordered toast card |
|
|
1660
|
+
Inspect for:
|
|
1697
1661
|
|
|
1698
|
-
|
|
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
|
|
1699
1673
|
|
|
1700
|
-
|
|
1674
|
+
Revise after inspection. Typechecking is not visual proof.
|
|
1701
1675
|
|
|
1702
|
-
|
|
1676
|
+
## 14. Rejection criteria
|
|
1703
1677
|
|
|
1704
|
-
|
|
1678
|
+
Require revision when any of these are true:
|
|
1705
1679
|
|
|
1706
|
-
|
|
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
|
|
1707
1701
|
|
|
1708
|
-
|
|
1702
|
+
## 15. Compact implementation API appendix
|
|
1709
1703
|
|
|
1710
|
-
|
|
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.
|
|
1711
1707
|
|
|
1712
|
-
|
|
1708
|
+
Headless components are not finished design. They are behavior and accessibility seams that the game must art-direct.
|
|
1713
1709
|
|
|
1714
|
-
|
|
1710
|
+
## Definition of done
|
|
1715
1711
|
|
|
1716
|
-
|
|
1712
|
+
UI work is complete only when:
|
|
1717
1713
|
|
|
1718
|
-
-
|
|
1719
|
-
-
|
|
1720
|
-
-
|
|
1721
|
-
-
|
|
1722
|
-
-
|
|
1723
|
-
-
|
|
1724
|
-
-
|
|
1725
|
-
-
|
|
1726
|
-
-
|
|
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
|