@jgengine/react 0.9.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 +21 -1
- package/dist/chat.d.ts +3 -3
- package/dist/chat.js +14 -5
- package/dist/chatBubbles.js +2 -1
- package/dist/components.js +1 -1
- package/dist/gameViewport.d.ts +54 -0
- package/dist/gameViewport.js +340 -0
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.js +26 -10
- package/dist/hudLayout.d.ts +21 -3
- package/dist/hudLayout.js +33 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -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/settings.d.ts +3 -0
- package/dist/social.d.ts +3 -3
- package/dist/social.js +31 -10
- package/llms.txt +111 -20
- package/package.json +2 -2
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
|
|
@@ -89,6 +89,20 @@ Imports use deep paths: `@jgengine/react/<path>`.
|
|
|
89
89
|
- iconForItemId (function): function iconForItemId(itemId: string): GameIconName | null
|
|
90
90
|
- isGameIconName (function): function isGameIconName(value: string): value is GameIconName
|
|
91
91
|
|
|
92
|
+
### @jgengine/react/gameViewport
|
|
93
|
+
|
|
94
|
+
- GameViewportProvider (function): function GameViewportProvider({ platforms, className, style, children, }: { platforms?: readonly HudPlatform[]; className?: string; style?: CSSProperties; children?: ReactNode; }): React.JSX.Element — Provides the shared game viewport layout to everything it wraps. Mount it once around the whole game presentation (world, HUD, controls, system UI) so every subsystem reads one coordinated geometry and registers its rect for collision detection. Publishes live `--jg-viewport-*` / `--jg-visual-viewport-*` / `--jg-safe-*` CSS variables on its root.
|
|
95
|
+
- LayoutRegionSpec (type): type LayoutRegionSpec = Omit<LayoutRegion, "rect"> — A region descriptor without its measured rectangle — the caller supplies geometry through `useRegisterLayoutRegion`.
|
|
96
|
+
- RegionRecord (interface): interface RegionRecord extends LayoutRegion — A region registration: the full `LayoutRegion` plus the live element (dev outlining), rect measured by the shell/react side.
|
|
97
|
+
- ViewportMetrics (interface): interface ViewportMetrics — Live viewport rectangles: the layout viewport and the visible `visualViewport`.
|
|
98
|
+
- useGameLayoutMode (function): function useGameLayoutMode(): GameLayoutMode — The resolved explicit composition mode (`desktop-wide` … `mobile-portrait`).
|
|
99
|
+
- useGameOrientation (function): function useGameOrientation(): LayoutOrientation — The live device orientation.
|
|
100
|
+
- useGameViewportLayout (function): function useGameViewportLayout(): GameViewportLayout — The live shared viewport layout. Returns a neutral default outside a `GameViewportProvider` so it never throws in previews.
|
|
101
|
+
- useLayoutCollisions (function): function useLayoutCollisions(): readonly LayoutCollision[] — Live forbidden/warned region collisions (empty outside a provider).
|
|
102
|
+
- useRegisterLayoutRegion (function): function useRegisterLayoutRegion(spec: LayoutRegionSpec, ref: RefObject<HTMLElement | null>, enabled = true): void — Register the element behind `ref` as a layout region and keep its measured rectangle live (ResizeObserver + viewport changes). No-op outside a `GameViewportProvider`, so a component using it still works in isolation.
|
|
103
|
+
- useReservedControlZones (function): function useReservedControlZones(): readonly LayoutRect[] — Rectangles reserved by touch controls and system UI — HUD placement should avoid these.
|
|
104
|
+
- useViewportMetrics (function): function useViewportMetrics(): ViewportMetrics — Live visible viewport, tracking `window.visualViewport` (mobile browser chrome, pinch-zoom) with a layout-viewport fallback.
|
|
105
|
+
|
|
92
106
|
### @jgengine/react/hooks
|
|
93
107
|
|
|
94
108
|
- AbilitySlotBindingOptions (interface): interface AbilitySlotBindingOptions
|
|
@@ -119,6 +133,7 @@ Imports use deep paths: `@jgengine/react/<path>`.
|
|
|
119
133
|
- useLeaderboard (function): function useLeaderboard(stat: string, options: { scope: LeaderboardScope; limit?: number }): { userId: string; value: number }[]
|
|
120
134
|
- useLocalPlayerDead (function): function useLocalPlayerDead(healthStatId = "health"): boolean
|
|
121
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.
|
|
122
137
|
- useParty (function): function useParty(): PartyMemberEntry[]
|
|
123
138
|
- usePartyInvites (function): function usePartyInvites(): PartyInviteEntry[]
|
|
124
139
|
- usePlayer (function): function usePlayer(): { userId: string; isNew: boolean }
|
|
@@ -134,10 +149,11 @@ Imports use deep paths: `@jgengine/react/<path>`.
|
|
|
134
149
|
|
|
135
150
|
### @jgengine/react/hudLayout
|
|
136
151
|
|
|
137
|
-
- HudCanvas (function): function HudCanvas({ layout, editChord, compactScale, className, style, children, }: { layout: HudLayoutStore; editChord?: HudEditChord | false; /** Zoom applied to the whole HUD on compact displays. Default 0.85. */ compactScale?: number;
|
|
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.
|
|
138
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.
|
|
139
154
|
- HudEditChord (interface): interface HudEditChord
|
|
140
|
-
- HudPanel (function): function HudPanel({ id, anchor = "top-left", order, compact: compactMode = "keep", chip, interactive, inset, locked, className, style, children, }: { id: string; anchor?: HudAnchor; /** Stack position within the
|
|
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).
|
|
141
157
|
- useHudLayout (function): function useHudLayout(options?: { storageKey?: string; snap?: number; locked?: boolean; }): HudLayoutStore
|
|
142
158
|
|
|
143
159
|
### @jgengine/react/hudViewport
|
|
@@ -166,7 +182,7 @@ Imports use deep paths: `@jgengine/react/<path>`.
|
|
|
166
182
|
### @jgengine/react/index
|
|
167
183
|
|
|
168
184
|
- AbilitySlotBindingOptions (interface): interface AbilitySlotBindingOptions
|
|
169
|
-
- 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
|
|
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
|
|
170
186
|
- BetterAuthSessionState (interface): interface BetterAuthSessionState
|
|
171
187
|
- BetterAuthUserShape (interface): interface BetterAuthUserShape
|
|
172
188
|
- CaptureOdds (function): function CaptureOdds({ chance, className, fillClassName, }: { chance: number; className?: string; fillClassName?: string; }): React.JSX.Element
|
|
@@ -206,17 +222,19 @@ Imports use deep paths: `@jgengine/react/<path>`.
|
|
|
206
222
|
- GamePreviewProps (type): type GamePreviewProps = { className?: string; }
|
|
207
223
|
- GamePreviewStates (type): type GamePreviewStates = Record<string, GamePreviewComponent>
|
|
208
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.
|
|
209
226
|
- HealthBar (function): function HealthBar({ instanceId, statId, className, fillClassName, }: { instanceId: string; statId: string; className?: string; fillClassName?: string; }): React.JSX.Element | null
|
|
210
|
-
- HudCanvas (function): function HudCanvas({ layout, editChord, compactScale, className, style, children, }: { layout: HudLayoutStore; editChord?: HudEditChord | false; /** Zoom applied to the whole HUD on compact displays. Default 0.85. */ compactScale?: number;
|
|
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.
|
|
211
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.
|
|
212
229
|
- HudEditChord (interface): interface HudEditChord
|
|
213
|
-
- HudPanel (function): function HudPanel({ id, anchor = "top-left", order, compact: compactMode = "keep", chip, interactive, inset, locked, className, style, children, }: { id: string; anchor?: HudAnchor; /** Stack position within the
|
|
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.
|
|
214
231
|
- HudViewportContextValue (interface): interface HudViewportContextValue
|
|
215
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.
|
|
216
233
|
- IdentitySource (interface): interface IdentitySource
|
|
217
|
-
- 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
|
|
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
|
|
218
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
|
|
219
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`.
|
|
220
238
|
- LeavePartyButton (function): function LeavePartyButton({ className, children, }: { className?: string; children?: ReactNode; }): React.JSX.Element | null
|
|
221
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
|
|
222
240
|
- MapBounds (interface): interface MapBounds
|
|
@@ -232,15 +250,17 @@ Imports use deep paths: `@jgengine/react/<path>`.
|
|
|
232
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
|
|
233
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
|
|
234
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.
|
|
235
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.
|
|
236
256
|
- Screen (function): function Screen({ id, open = true, className, children, }: { id: string; open?: boolean; className?: string; children?: ReactNode; }): React.JSX.Element | null
|
|
237
257
|
- SettingsActionView (interface): interface SettingsActionView — A resolved game-state action — `run` is already bound to the game context and closes the menu.
|
|
238
|
-
- SettingsCategoryView (interface): interface SettingsCategoryView
|
|
258
|
+
- SettingsCategoryView (interface): interface SettingsCategoryView — A settings menu category with its rows and keybinds, ready to render.
|
|
239
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.
|
|
240
260
|
- SettingsControllerProvider (function): function SettingsControllerProvider({ controller, children, }: { controller: SettingsController; children: ReactNode; }): React.JSX.Element
|
|
241
|
-
- SettingsKeybindRow (interface): interface SettingsKeybindRow
|
|
261
|
+
- SettingsKeybindRow (interface): interface SettingsKeybindRow — One rebindable action row rendered in the controls settings category.
|
|
242
262
|
- SettingsProvider (function): function SettingsProvider({ store, children }: { store: SettingsStore; children: ReactNode }): React.JSX.Element
|
|
243
|
-
- SettingsRow (interface): interface SettingsRow
|
|
263
|
+
- SettingsRow (interface): interface SettingsRow — One editable setting rendered in a settings menu category.
|
|
244
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.
|
|
245
265
|
- SignOutButton (function): function SignOutButton({ className, children, }: { className?: string; children?: ReactNode; }): React.JSX.Element | null
|
|
246
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…
|
|
@@ -250,6 +270,7 @@ Imports use deep paths: `@jgengine/react/<path>`.
|
|
|
250
270
|
- UseAxisChannelResult (interface): interface UseAxisChannelResult
|
|
251
271
|
- UseVoiceOptions (interface): interface UseVoiceOptions
|
|
252
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`.
|
|
253
274
|
- VoiceRoster (function): function VoiceRoster({ voice, className, participantClassName, renderParticipant, }: { voice: VoiceState; className?: string; participantClassName?: string; renderParticipant?: (participant: VoiceParticipant, gain: number) => ReactNode; }): React.JSX.Element
|
|
254
275
|
- VoiceState (interface): interface VoiceState
|
|
255
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?:…
|
|
@@ -264,6 +285,7 @@ Imports use deep paths: `@jgengine/react/<path>`.
|
|
|
264
285
|
- createHeldKeyTracker (function): function createHeldKeyTracker(target: HeldKeyEventTarget): { isDown: (code: string) => boolean; dispose: () => void; }
|
|
265
286
|
- eventMeterNeedsHeartbeat (function): function eventMeterNeedsHeartbeat(meter: EventMeter, previous: EventMeterView | null): boolean
|
|
266
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).
|
|
267
289
|
- latestChatBubbles (function): function latestChatBubbles(messages: readonly ChatMessage[], nowMs: number, ttlMs: number): ChatBubble[]
|
|
268
290
|
- localPlayerEntity (function): function localPlayerEntity(ctx: GameContext): SceneEntity | null
|
|
269
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,22 +314,30 @@ Imports use deep paths: `@jgengine/react/<path>`.
|
|
|
292
314
|
- useGame (function): function useGame(): { commands: GameContext["game"]["commands"]; events: GameEvents }
|
|
293
315
|
- useGameClock (function): function useGameClock(): ClockSnapshot & { controls: SimClock }
|
|
294
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.
|
|
295
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.
|
|
296
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.
|
|
297
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.
|
|
298
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.
|
|
299
324
|
- useHudLayout (function): function useHudLayout(options?: { storageKey?: string; snap?: number; locked?: boolean; }): HudLayoutStore
|
|
300
325
|
- useHudViewport (function): function useHudViewport(): HudViewportContextValue | null
|
|
301
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).
|
|
302
328
|
- useLeaderboard (function): function useLeaderboard(stat: string, options: { scope: LeaderboardScope; limit?: number }): { userId: string; value: number }[]
|
|
303
329
|
- useLocalPlayerDead (function): function useLocalPlayerDead(healthStatId = "health"): boolean
|
|
304
330
|
- useMarkers (function): function useMarkers(markers: MarkerSet): readonly MapMarker[]
|
|
305
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.
|
|
306
334
|
- useParty (function): function useParty(): PartyMemberEntry[]
|
|
307
335
|
- usePartyInvites (function): function usePartyInvites(): PartyInviteEntry[]
|
|
308
336
|
- usePlayer (function): function usePlayer(): { userId: string; isNew: boolean }
|
|
309
337
|
- usePresence (function): function usePresence(userId: string): PresenceInfo
|
|
310
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.
|
|
311
341
|
- useRoster (function): function useRoster(userId?: string): readonly RosterEntry[]
|
|
312
342
|
- useSceneEntities (function): function useSceneEntities(): readonly SceneEntity[]
|
|
313
343
|
- useSceneObjects (function): function useSceneObjects(): readonly SceneObject[]
|
|
@@ -316,6 +346,7 @@ Imports use deep paths: `@jgengine/react/<path>`.
|
|
|
316
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.
|
|
317
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).
|
|
318
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.
|
|
319
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.
|
|
320
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.
|
|
321
352
|
- useWorldInvites (function): function useWorldInvites(): WorldInvite[]
|
|
@@ -350,6 +381,11 @@ Imports use deep paths: `@jgengine/react/<path>`.
|
|
|
350
381
|
|
|
351
382
|
- GameProvider (function): function GameProvider({ context, children }: { context: GameContext; children?: ReactNode }): React.JSX.Element
|
|
352
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.
|
|
353
389
|
|
|
354
390
|
### @jgengine/react/selectSnapshot
|
|
355
391
|
|
|
@@ -360,12 +396,12 @@ Imports use deep paths: `@jgengine/react/<path>`.
|
|
|
360
396
|
### @jgengine/react/settings
|
|
361
397
|
|
|
362
398
|
- SettingsActionView (interface): interface SettingsActionView — A resolved game-state action — `run` is already bound to the game context and closes the menu.
|
|
363
|
-
- SettingsCategoryView (interface): interface SettingsCategoryView
|
|
399
|
+
- SettingsCategoryView (interface): interface SettingsCategoryView — A settings menu category with its rows and keybinds, ready to render.
|
|
364
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.
|
|
365
401
|
- SettingsControllerProvider (function): function SettingsControllerProvider({ controller, children, }: { controller: SettingsController; children: ReactNode; }): React.JSX.Element
|
|
366
|
-
- SettingsKeybindRow (interface): interface SettingsKeybindRow
|
|
402
|
+
- SettingsKeybindRow (interface): interface SettingsKeybindRow — One rebindable action row rendered in the controls settings category.
|
|
367
403
|
- SettingsProvider (function): function SettingsProvider({ store, children }: { store: SettingsStore; children: ReactNode }): React.JSX.Element
|
|
368
|
-
- SettingsRow (interface): interface SettingsRow
|
|
404
|
+
- SettingsRow (interface): interface SettingsRow — One editable setting rendered in a settings menu category.
|
|
369
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.
|
|
370
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.
|
|
371
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.
|
|
@@ -379,12 +415,12 @@ Imports use deep paths: `@jgengine/react/<path>`.
|
|
|
379
415
|
|
|
380
416
|
### @jgengine/react/social
|
|
381
417
|
|
|
382
|
-
- 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
|
|
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
|
|
383
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; …
|
|
384
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…
|
|
385
421
|
- FriendRow (function): function FriendRow({ friend, className, dotClassName, children, }: { friend: FriendEntry; className?: string; dotClassName?: string; children?: ReactNode; }): React.JSX.Element
|
|
386
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
|
|
387
|
-
- 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
|
|
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
|
|
388
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
|
|
389
425
|
- LeavePartyButton (function): function LeavePartyButton({ className, children, }: { className?: string; children?: ReactNode; }): React.JSX.Element | null
|
|
390
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
|
|
@@ -505,7 +541,9 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
505
541
|
| Loadout | `game/loadout` | `LoadoutDef`, `LoadoutItemEntry`, `Loadouts` |
|
|
506
542
|
| Cosmetic loadout | `game/cosmetics` | `createCosmetics`, `Cosmetics`, `CosmeticLoadoutDef` |
|
|
507
543
|
| Quest | `game/quest` | `QuestDef`, `QuestRewards`, `QuestObjective`, `QuestJournal` |
|
|
508
|
-
| 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 |
|
|
509
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 |
|
|
510
548
|
| Terrain field | `world/terrain` | `TerrainField`, `noiseField`, `resolveTerrainField`, `rollingField`, `fractalNoise`, `valueNoise`, `withNormal`, `arenaField`, `flatField`, `resolveGroundStep`, `snapToGround`, `snapEntityToGround`, `resolveTerrainPalette`, `TERRAIN_MATERIAL_PALETTES` |
|
|
511
549
|
| Seeded RNG | `random/rng` | `seededRng`, `seededStreams` |
|
|
@@ -797,7 +835,7 @@ A voxel block is an object. A rack is an object with a slot inventory. A GPU is
|
|
|
797
835
|
|
|
798
836
|
## Game repo layout
|
|
799
837
|
|
|
800
|
-
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.
|
|
801
839
|
|
|
802
840
|
```
|
|
803
841
|
src/
|
|
@@ -828,6 +866,8 @@ src/
|
|
|
828
866
|
|
|
829
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.
|
|
830
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
|
+
|
|
831
871
|
```ts
|
|
832
872
|
// game.config.ts — imports only, nothing inline
|
|
833
873
|
import { defineGame } from "@jgengine/shell/defineGame";
|
|
@@ -889,7 +929,7 @@ export const physics: PhysicsConfig = { gravity: -32 };
|
|
|
889
929
|
|
|
890
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.
|
|
891
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.
|
|
892
|
-
- **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`).
|
|
893
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.
|
|
894
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.
|
|
895
935
|
- `server.mode` is a string your loop/commands interpret — the engine ships no gamemode presets.
|
|
@@ -974,7 +1014,7 @@ Catalog-first, no per-game audio glue. The `audio` field of `defineGame({...})`
|
|
|
974
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:
|
|
975
1015
|
| `rig` | For | Key config (`camera.<rig>`) |
|
|
976
1016
|
|-------|-----|------------------------------|
|
|
977
|
-
| `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 |
|
|
978
1018
|
| `first` | FPS mouse-look | `firstPerson: { eyeHeight, sensitivity, maxPitch, reticle, viewmodel }` |
|
|
979
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`) |
|
|
980
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 |
|
|
@@ -1033,7 +1073,7 @@ ctx.time advance, now, calendar, snapshot; pause, play, toggle,
|
|
|
1033
1073
|
ctx.subscribe / ctx.version change signal — UI layers bind via useSyncExternalStore
|
|
1034
1074
|
```
|
|
1035
1075
|
|
|
1036
|
-
`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.
|
|
1037
1077
|
|
|
1038
1078
|
### Two tiers: `ctx` runtime vs pure factories
|
|
1039
1079
|
|
|
@@ -1361,6 +1401,37 @@ Design-resolution fit is on by default for every game: each `HudCanvas` auto-sca
|
|
|
1361
1401
|
|
|
1362
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.
|
|
1363
1403
|
|
|
1404
|
+
### Phase-gated HUD visibility (`showDuring`)
|
|
1405
|
+
|
|
1406
|
+
`HudCanvas` and `HudPanel` take an opt-in `showDuring?: GamePhase[]` — the element renders only while `gamePhase` is one of the listed phases (`"menu" | "playing" | "paused" | "ended"`), so a non-cartridge game hides its HUD under menu/end overlays without hand-rolling a phase check. Omit it for the default always-visible behavior. Gate the whole HUD with `<HudCanvas showDuring={["playing"]}>`, or keep a single results panel up with a per-`HudPanel` value. The read degrades to `"playing"` when the component renders outside a `GameProvider` (component showcases, previews), so it never throws there.
|
|
1407
|
+
|
|
1408
|
+
### Shared viewport allocation (`GameViewportProvider`, region collision)
|
|
1409
|
+
|
|
1410
|
+
The shell allocates the live viewport once — visual viewport, safe-area insets, orientation, and layout mode — and hosts a **layout registry** every UI subsystem publishes its occupied rectangle to. This replaces "every subsystem independently claims an edge" (the mobile-overlap smell). It is wired automatically for every game; author against it, don't rebuild it.
|
|
1411
|
+
|
|
1412
|
+
- **The mode** is one of `desktop-wide | desktop-compact | mobile-landscape | mobile-portrait`, resolved from the live `visualViewport` + coarse-pointer + `platforms`. Read it with `useGameLayoutMode()` (or `useGameViewportLayout()` for the full geometry: `mode`, `orientation`, `safeArea`, `controlZones`, `gameplayRect`). Both degrade to a sane desktop default outside the provider, so previews never throw.
|
|
1413
|
+
- **Live CSS variables** are published on the provider root: `--jg-viewport-*`, `--jg-visual-viewport-*`, `--jg-safe-{top,right,bottom,left}`. Use them (with `100dvh` fallbacks) instead of assuming `100vh` equals the visible area on mobile Safari.
|
|
1414
|
+
- **Touch controls reserve real rectangles.** The engine measures the joystick / action-cluster / utility zones at runtime (ResizeObserver, not guessed percentages) and registers them as `control` regions. `HudCanvas` already lifts bottom-anchored panels above the dock via `--jg-hud-dock-clearance`; the registry additionally makes any residual overlap a hard error.
|
|
1415
|
+
- **Collision is detected, not hoped for.** A `HudPanel` inside a `HudCanvas` registers as a `hud` region automatically. `bun run shoot <game> --device mobile` / `mobile-landscape` reads a `data-jg-layout-collision` attribute and exits non-zero naming both colliding regions (e.g. `throttle ∩ radio (4270px²)`) — the same failure discipline as HUD overflow. In dev the colliding elements also carry `data-jg-collision` for outlining. Opt an intentional overlap out with `allowOverlapWith` / `collisionGroup`, or a soft `mobileBehavior="transient"`.
|
|
1416
|
+
|
|
1417
|
+
### HUD priority + mobile behavior (`HudPanel`)
|
|
1418
|
+
|
|
1419
|
+
Declare intent, not just CSS. `HudPanel` accepts:
|
|
1420
|
+
|
|
1421
|
+
- `priority?: "critical" | "secondary" | "tertiary"` — the Tier from §2, surfaced to tooling. Critical stays visible; tertiary folds away first.
|
|
1422
|
+
- `mobileBehavior?: "persistent" | "compact" | "icon" | "transient" | "hidden" | "sheet" | "modal"` — `"hidden"` unmounts the panel on phones (move that Tier-3 readout behind a contextual action instead); `"transient"` softens its collision policy for a fleeting line; the rest tag the element (`data-hud-mobile-behavior`) for the game's own responsive CSS. Keep these game-authored and lightly styled — this is a placement contract, not a website design system.
|
|
1423
|
+
|
|
1424
|
+
The game still owns the genre-appropriate composition; the engine owns the geometry and the failure gate.
|
|
1425
|
+
|
|
1426
|
+
### Mandatory orientation (`orientation`)
|
|
1427
|
+
|
|
1428
|
+
A game declares its phone-orientation contract on `defineGame({ orientation })`:
|
|
1429
|
+
|
|
1430
|
+
- Legacy `"landscape"` / `"portrait"` stays **advisory** — a dismissible rotate hint, never a gate.
|
|
1431
|
+
- The object form `{ mobile: <rule> }` is the strict contract, where `<rule>` is `any` (both) · `portrait` / `landscape` (advisory preference) · `portrait-required` / `landscape-required` (hard gate) · `unsupported` (no phone support).
|
|
1432
|
+
|
|
1433
|
+
For a driving/landscape game: `orientation: { mobile: "landscape-required" }`. When the device is held the wrong way the shell shows an **engine-owned `RotateDeviceScreen`** (polished, safe-area-aware, `visualViewport`-sized, reduced-motion-respecting, themeable via `--jg-*`) above every layer, **suppresses game input, freezes the simulation, and unmounts the HUD and touch controls** — gameplay never runs behind the gate. The gate is derived live from orientation, so rotating back to a valid orientation resumes automatically with no stuck-paused state. Never re-implement a "rotate for best experience" toast; declare the requirement and the engine owns the rest.
|
|
1434
|
+
|
|
1364
1435
|
## 5. Change the visual grammar
|
|
1365
1436
|
|
|
1366
1437
|
Avoid making every element the same rounded translucent rectangle.
|
|
@@ -1408,6 +1479,8 @@ Prefer small headless or lightly styled primitives over a giant universal design
|
|
|
1408
1479
|
|
|
1409
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.
|
|
1410
1481
|
|
|
1482
|
+
**Minimap** is already shipped, not hand-rolled: `Minimap` / `WorldMap` / `Compass` from `@jgengine/react/map` render a framed SVG minimap over a `MarkerSet` (`createMarkerSet`, `@jgengine/core/world/markers`) and optional `FogField`, projecting via `@jgengine/core/world/minimap` (`projectToMinimap`, `headingToBearing`, edge-clamp). A game feeds flat props (`markers`, `center: [x,z]`, `worldRadius`, `size`, `facingYaw`, `rotate`) and repopulates the marker set each HUD tick from live entities — no custom canvas painter. **Swing timer**: `swingTimerState(player, target, prevPeriod, prevTimer)` (`@jgengine/core/ui/swingTimer`) is the pure, parameter-in/next-state-out core for a melee swing bar (recovers period on the reset edge; hidden unless auto-attacking a live non-object target) — thread the returned `nextPeriod`/`nextTimer` back via refs, render a thin `Meter`.
|
|
1483
|
+
|
|
1411
1484
|
Do not create a primitive whose only value is wrapping a `div` with border radius.
|
|
1412
1485
|
|
|
1413
1486
|
## 7. Complete interaction states
|
|
@@ -1439,6 +1512,19 @@ Do not communicate all states with background-color changes alone. Use appropria
|
|
|
1439
1512
|
|
|
1440
1513
|
Focus must look authored while remaining accessible. Menus should support keyboard/controller-style focus navigation when practical.
|
|
1441
1514
|
|
|
1515
|
+
## Rendering — post-processing, lighting, shadows
|
|
1516
|
+
|
|
1517
|
+
A cinematic look is opt-in engine config, never hand-wired render passes. Set `defineGame({ postProcessing })` (`PostProcessingConfig` from `@jgengine/core/render/postProcessing`) and the shell mounts an `EffectComposer` and owns the render: RenderPass → AO → Bloom → tone-map output → Grade. Absent means the renderer draws directly (unchanged), so it never imposes a look on games that don't ask. Each stage is a config object, `false` to skip, or omitted for its tuned default:
|
|
1518
|
+
|
|
1519
|
+
- `toneMapping: "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none"` (default `aces`) + `exposure`.
|
|
1520
|
+
- `bloom: { strength, radius, threshold }` — HDR glow around bright pixels (defaults 0.32 / 0.55 / 0.85).
|
|
1521
|
+
- `grade: { lift, gain, gamma, saturation, vignette, grain }` — display-space colour grade: cool-shadow/warm-highlight split, vignette, animated film grain.
|
|
1522
|
+
- `ao: { radius, intensity, distanceFalloff, blend }` — ground-truth ambient occlusion; heavier than the rest, omit or `false` on low-end targets.
|
|
1523
|
+
|
|
1524
|
+
**Orbit camera occlusion:** the third-person orbit rig takes `camera: { collision: { enabled, padding?, minTargetDistance? } }` — a spring-arm that raycasts target→camera each frame and pulls the boom in past walls/terrain so the camera never clips inside geometry. Off by default (unchanged chase feel); enable it for any world with interiors or dense structures.
|
|
1525
|
+
|
|
1526
|
+
Lighting is `defineGame({ lighting })` (`LightingConfig`): ambient / hemisphere / directional, replacing the shell's default lights when set. A `DirectionalLightingConfig` with `castShadow` takes `shadowMapSize`, `shadowCameraSize`, `shadowBias`, `shadowNormalBias` for crisp contact shadows. Sky-lit worlds (a `sky()` world feature) get a high-res sun whose shadow camera follows the view each frame, so grounded shadows stay sharp under the player anywhere in a large world.
|
|
1527
|
+
|
|
1442
1528
|
## Settings menu
|
|
1443
1529
|
|
|
1444
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`):
|
|
@@ -1504,6 +1590,11 @@ Requirements:
|
|
|
1504
1590
|
|
|
1505
1591
|
Do not use one generic translucent controller across all games.
|
|
1506
1592
|
|
|
1593
|
+
**`presentation: "hud"` games get 3D parity.** A pure-HUD game (no camera rig) now reaches the same input/audio seams as a 3D game:
|
|
1594
|
+
- **Touch gestures** — the shell mounts a headless `TouchPlaySurface` in the hud branch too, so `touch.gestures` (swipe/tap) reach actions without the game hand-wiring pointer events on its own canvas. The visible dock stays game-authored per the rule above.
|
|
1595
|
+
- **No phantom reservations** — camera action names (`turnLeft`/`turnRight`/`interact`/…) are reserved *only* when a camera rig is active, so a hud game may bind them directly instead of renaming to `steer*`.
|
|
1596
|
+
- **Audio actually plays** — audio resumes on the first pointer gesture in hud games (not just 3D), and `playOneShot` self-resumes the suspended context. Trigger sound from anywhere holding `ctx` via `ctx.game.audio.play(soundId, at?)` / `ctx.game.audio.resume()` — the reachable seam over the shell's audio engine.
|
|
1597
|
+
|
|
1507
1598
|
## 10. Progressive instruction
|
|
1508
1599
|
|
|
1509
1600
|
Do not leave large control grids visible during gameplay.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "React UI layer for JGengine: GameProvider, hooks, and headless primitives over @jgengine/core.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"test": "bun test src"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@jgengine/core": "^0.
|
|
34
|
+
"@jgengine/core": "^0.10.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"react": "^19.0.0",
|