@jgengine/react 0.13.0 → 0.14.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 +47 -1
- package/dist/hudLayout.js +138 -9
- package/dist/panels.d.ts +10 -2
- package/dist/panels.js +9 -3
- package/dist/provider.d.ts +15 -0
- package/dist/provider.js +17 -1
- package/package.json +2 -2
- package/skills/jgengine/SKILL.md +4 -2
- package/skills/jgengine/api.md +2 -2
- package/skills/jgengine-ui/SKILL.md +4 -1
- package/skills/jgengine-ui/api.md +24 -4
- package/skills/jgengine-ui/capabilities.md +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,7 +10,53 @@ let consumers pick up the better stuff, not just to list what moved.
|
|
|
10
10
|
Agents building on the published SDK can also read this programmatically:
|
|
11
11
|
`import { VERSION, CHANGELOG } from "@jgengine/core/meta/changelog"` gives the
|
|
12
12
|
same data as typed values, so an updater can diff its installed version against
|
|
13
|
-
the latest and surface the migration steps.
|
|
13
|
+
the latest and surface the migration steps. Easier still: `npx jgengine upgrade`
|
|
14
|
+
run inside a game project diffs the installed `@jgengine/*` versions against the
|
|
15
|
+
latest release and prints every Migrate step and Adopt-worthy addition in
|
|
16
|
+
between (`--json` for structured output).
|
|
17
|
+
|
|
18
|
+
## [Unreleased]
|
|
19
|
+
|
|
20
|
+
<!--
|
|
21
|
+
Every PR that changes `packages/*/src` records its consumer-facing change here, so
|
|
22
|
+
the next release's notes are complete by construction. Add a bullet under the right
|
|
23
|
+
subhead below. `bun run check-changelog` fails a source PR whose `[Unreleased]` block
|
|
24
|
+
is untouched; pure refactors/tests bypass with `[skip changelog]` in a commit message.
|
|
25
|
+
At publish, rename this heading to the new version and mirror the entries into
|
|
26
|
+
`packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
|
|
27
|
+
-->
|
|
28
|
+
|
|
29
|
+
### Migrate
|
|
30
|
+
|
|
31
|
+
- _Nothing yet._
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
### Removed
|
|
38
|
+
|
|
39
|
+
## 0.14.0
|
|
40
|
+
|
|
41
|
+
### Migrate
|
|
42
|
+
|
|
43
|
+
- **Nothing required** — this release is additive drop-in primitives, editor authoring, CLI discovery, and fixes. Bump `@jgengine/*` pins to `^0.14.0` (CLI `jgengine` is 0.11.0; `@jgengine/github` unchanged at 0.1.0) and rebuild.
|
|
44
|
+
- One behavior fix to know about: in a `rig: "chase"` game, on-foot movement and aim are now camera-relative (the chase rig reports its yaw to the shell like every other rig). If a game somehow relied on world-yaw-0 movement under a chase camera, that was the bug this fixes.
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- One authoring gesture for scatterable coverage: `@jgengine/core/world/scatterCoverage` owns shared density/budget semantics (per-kind unit, requested→count→capped, one clamp-and-warn phrasing) for `grass_field`/`scatter`/`city`; the editor inspector leads each kind with the same Area → Assets → Density coverage section, scatter truncates to the shared 250k instance budget, and city single-sources its 2,600-lot cap.
|
|
49
|
+
- **Created games ship a drive/playtest script** (#1248) — `npx jgengine create` scaffolds now include `scripts/drive.mjs` (`bun run drive`): dependency-free headless play/testing of the running game — ordered `--click`/`--key`/`--wait`/`--shot`/`--rpc` steps against the agent bridge, plus `--playtest --strict` progress/softlock verdicts off `capture.probe`. Shared Chrome/CDP machinery lives in `scripts/browser.mjs`; `scripts/shoot.mjs` is unchanged in behavior, now a thin CLI over it.
|
|
50
|
+
- Editor: viewport clip preview for rigged assets (Animation dock "Clips" mode — pick a rig or placed instance, play/scrub/loop/speed any catalog clip) and an Inspector "Animation" section that authors a placement's `ModelConfig.animation` (role→clip dropdowns, auto/none, walk/run/fade, one-shot event bindings) as undoable `marker.meta.animation` edits. New subpaths: `@jgengine/editor/shell/clipPreview`, `@jgengine/editor/modelAnimationAuthoring`, `@jgengine/editor/ClipPreviewLayer`.
|
|
51
|
+
- **`npx jgengine find <intent>`** — active capability discovery: searches every shipped domain's `capabilities.md` (staged inside the CLI tarball, so it works regardless of which `@jgengine/*` packages a project installed) and prints the drop-in primitive + its import for an intent like `"toggleable window"`, `inventory`, or `minimap`. Scaffolded games are now briefed (in `AGENTS.md` and the `jgengine` intake skill) to reach for it before hand-rolling a HUD/inventory/window/rig.
|
|
52
|
+
- **`EntityPreview`** (`@jgengine/shell/render/EntityPreview`) — drop-in live 3D entity portrait for character screens, unit inspectors, and loadout viewers. Owns the nested `<Canvas>`, the `GameContextBridge`, and a `StudioStage` lighting rig with optional turntable / face-camera; the game passes its own `renderEntity` as children (or a native `model` + `instanceId`) and, bound to a live entity, the portrait walks/flinches/topples in sync with the world.
|
|
53
|
+
- **`GameContextBridge`** (`@jgengine/react`) — re-provide the running `GameContext` across a nested React reconciler boundary (the R3F `<Canvas>`), so a game building its own preview canvas no longer re-derives the bridge by hand.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- **Chase rig reports its yaw to the shell** — `ChaseRig` now writes the camera yaw back to the shared yaw ref like every other player-facing rig, so on-foot movement and aim in a `rig: "chase"` game are camera-relative instead of frozen to world yaw 0 (WASD no longer fights the camera the moment it swings behind the player). Interior views (cockpit/hood/rear) report the vehicle heading.
|
|
58
|
+
- **Combat VFX no longer render as black squares under AO/DOF post-processing** (#1247) — GTAO/Bokeh scene prepasses skip overlay effects. Games with custom additive overlay effects opt out the same way: spread `POSTFX_OVERLAY_USERDATA` (`@jgengine/shell/postfx/postfxOverlay`) onto the overlay group's `userData`.
|
|
59
|
+
- **`PanelHost` windows stack above the HUD by default** (`@jgengine/react`) — the host establishes its own stacking context at `zIndexBase` (default 40), so open windows always paint over stat bars / nameplates / frames instead of bleeding through. Overridable per instance.
|
|
14
60
|
|
|
15
61
|
## 0.13.0
|
|
16
62
|
|
package/dist/hudLayout.js
CHANGED
|
@@ -22,6 +22,91 @@ const EDIT_BAR_Z = 100000;
|
|
|
22
22
|
const REGION_GAP = 10;
|
|
23
23
|
const COMPACT_HUD_SCALE = 0.85;
|
|
24
24
|
const HUD_ANCHORS = Object.keys(HUD_ANCHOR_FRACTIONS);
|
|
25
|
+
/**
|
|
26
|
+
* Attribute a game puts on any HUD surface it *wants* to bleed past the viewport
|
|
27
|
+
* (a full-bleed backdrop, a deliberately clipped ticker), so the boundary check
|
|
28
|
+
* skips it. Opt-out, on purpose: the default is that shown UI must fit on screen.
|
|
29
|
+
*/
|
|
30
|
+
const ALLOW_OVERFLOW_ATTR = "data-hud-allow-overflow";
|
|
31
|
+
/** Opt-in marker for a custom window mounted deeper than a direct HudCanvas child. */
|
|
32
|
+
const WINDOW_ATTR = "data-hud-window";
|
|
33
|
+
/**
|
|
34
|
+
* Dev-only, prod-safe. The on-screen boundary overlay must never reach players,
|
|
35
|
+
* so anything we cannot positively identify as a dev build resolves to `false`.
|
|
36
|
+
*/
|
|
37
|
+
function isDevBuild() {
|
|
38
|
+
try {
|
|
39
|
+
const meta = import.meta;
|
|
40
|
+
if (meta.env?.DEV === true)
|
|
41
|
+
return true;
|
|
42
|
+
if (meta.env?.PROD === true)
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* import.meta.env unavailable under this bundler */
|
|
47
|
+
}
|
|
48
|
+
const nodeEnv = globalThis.process
|
|
49
|
+
?.env?.NODE_ENV;
|
|
50
|
+
if (typeof nodeEnv === "string")
|
|
51
|
+
return nodeEnv !== "production";
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
/** True unless the element (or an ancestor) is hidden, collapsed, or removed from the a11y tree. */
|
|
55
|
+
function isSurfaceShown(el) {
|
|
56
|
+
if (el.closest('[aria-hidden="true"]') !== null)
|
|
57
|
+
return false;
|
|
58
|
+
const checkable = el;
|
|
59
|
+
if (typeof checkable.checkVisibility === "function") {
|
|
60
|
+
return checkable.checkVisibility({
|
|
61
|
+
contentVisibilityAuto: true,
|
|
62
|
+
opacityProperty: true,
|
|
63
|
+
visibilityProperty: true,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (typeof getComputedStyle !== "function")
|
|
67
|
+
return true;
|
|
68
|
+
const style = getComputedStyle(el);
|
|
69
|
+
return style.visibility !== "hidden" && style.display !== "none" && Number(style.opacity) > 0.01;
|
|
70
|
+
}
|
|
71
|
+
function surfaceId(el) {
|
|
72
|
+
return (el.getAttribute("data-hud-panel") ??
|
|
73
|
+
el.getAttribute(WINDOW_ATTR) ??
|
|
74
|
+
el.getAttribute("data-hud-id") ??
|
|
75
|
+
el.getAttribute("aria-label") ??
|
|
76
|
+
(el.id.length > 0 ? el.id : null) ??
|
|
77
|
+
"<ui>");
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Every HUD surface whose bounds we hold to the viewport: registered panels,
|
|
81
|
+
* any element tagged as a window, and the game's own direct children of the
|
|
82
|
+
* canvas (custom modals dropped straight into the HUD layer). Opted-out and
|
|
83
|
+
* hidden surfaces are dropped here so a closed drawer never reaches the check.
|
|
84
|
+
*/
|
|
85
|
+
function collectBoundarySurfaces(canvas) {
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
const out = [];
|
|
88
|
+
const consider = (el) => {
|
|
89
|
+
if (seen.has(el))
|
|
90
|
+
return;
|
|
91
|
+
seen.add(el);
|
|
92
|
+
if (el.closest(`[${ALLOW_OVERFLOW_ATTR}]`) !== null)
|
|
93
|
+
return;
|
|
94
|
+
if (!isSurfaceShown(el))
|
|
95
|
+
return;
|
|
96
|
+
out.push({ id: surfaceId(el), el });
|
|
97
|
+
};
|
|
98
|
+
for (const el of canvas.querySelectorAll(`[data-hud-panel],[${WINDOW_ATTR}]`))
|
|
99
|
+
consider(el);
|
|
100
|
+
for (const el of Array.from(canvas.children)) {
|
|
101
|
+
if (el.hasAttribute("data-hud-region") ||
|
|
102
|
+
el.hasAttribute("data-hud-edit-bar") ||
|
|
103
|
+
el.hasAttribute("data-hud-overflow-overlay")) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
consider(el);
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
25
110
|
function sizeStyle(panel) {
|
|
26
111
|
if (panel === undefined)
|
|
27
112
|
return {};
|
|
@@ -200,6 +285,7 @@ export function HudCanvas({ layout, editChord, compactScale, showDuring, classNa
|
|
|
200
285
|
const fitEnabled = hudViewport?.fitEnabled === true;
|
|
201
286
|
const [viewport, setViewport] = useState(null);
|
|
202
287
|
const [regions, setRegions] = useState({});
|
|
288
|
+
const [offenders, setOffenders] = useState([]);
|
|
203
289
|
const regionRefs = useMemo(() => {
|
|
204
290
|
const refs = {};
|
|
205
291
|
for (const anchor of HUD_ANCHORS) {
|
|
@@ -285,18 +371,20 @@ export function HudCanvas({ layout, editChord, compactScale, showDuring, classNa
|
|
|
285
371
|
return;
|
|
286
372
|
let frame = 0;
|
|
287
373
|
let lastReport = "";
|
|
374
|
+
const dev = isDevBuild();
|
|
288
375
|
const check = () => {
|
|
289
376
|
frame = 0;
|
|
290
377
|
const hostRect = host.getBoundingClientRect();
|
|
291
378
|
if (hostRect.width <= 0 || hostRect.height <= 0)
|
|
292
379
|
return;
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
380
|
+
const viewportSize = { width: hostRect.width, height: hostRect.height };
|
|
381
|
+
const surfaces = [];
|
|
382
|
+
for (const surface of collectBoundarySurfaces(canvas)) {
|
|
383
|
+
const rect = surface.el.getBoundingClientRect();
|
|
296
384
|
if (rect.width === 0 && rect.height === 0)
|
|
297
385
|
continue;
|
|
298
|
-
|
|
299
|
-
id:
|
|
386
|
+
surfaces.push({
|
|
387
|
+
id: surface.id,
|
|
300
388
|
rect: {
|
|
301
389
|
x: rect.left - hostRect.left,
|
|
302
390
|
y: rect.top - hostRect.top,
|
|
@@ -305,8 +393,10 @@ export function HudCanvas({ layout, editChord, compactScale, showDuring, classNa
|
|
|
305
393
|
},
|
|
306
394
|
});
|
|
307
395
|
}
|
|
308
|
-
|
|
309
|
-
|
|
396
|
+
// overflowingPanels reports only surfaces that are partly on screen *and*
|
|
397
|
+
// cross an edge — a closed/parked/off-screen menu is never an offender.
|
|
398
|
+
const escaped = overflowingPanels(surfaces, viewportSize);
|
|
399
|
+
const report = escaped.length === 0 ? "" : JSON.stringify(escaped);
|
|
310
400
|
if (report === lastReport)
|
|
311
401
|
return;
|
|
312
402
|
lastReport = report;
|
|
@@ -315,7 +405,13 @@ export function HudCanvas({ layout, editChord, compactScale, showDuring, classNa
|
|
|
315
405
|
}
|
|
316
406
|
else {
|
|
317
407
|
canvas.setAttribute("data-hud-overflow", report);
|
|
318
|
-
console.warn(`[jgengine] HUD
|
|
408
|
+
console.warn(`[jgengine] HUD UI escapes the ${Math.round(hostRect.width)}x${Math.round(hostRect.height)} viewport: ${report}`);
|
|
409
|
+
}
|
|
410
|
+
if (dev) {
|
|
411
|
+
setOffenders(escaped.map((overflow) => {
|
|
412
|
+
const rect = surfaces.find((s) => s.id === overflow.id)?.rect;
|
|
413
|
+
return { ...overflow, x: rect?.x ?? 0, y: rect?.y ?? 0, width: rect?.width ?? 0, height: rect?.height ?? 0 };
|
|
414
|
+
}));
|
|
319
415
|
}
|
|
320
416
|
};
|
|
321
417
|
const schedule = () => {
|
|
@@ -365,7 +461,40 @@ export function HudCanvas({ layout, editChord, compactScale, showDuring, classNa
|
|
|
365
461
|
pointerEvents: "none",
|
|
366
462
|
zIndex: 1,
|
|
367
463
|
...REGION_LAYOUT[anchor],
|
|
368
|
-
} }, anchor))), children,
|
|
464
|
+
} }, anchor))), children, offenders.length > 0 ? (_jsx("div", { "data-hud-overflow-overlay": "", style: { position: "absolute", inset: 0, pointerEvents: "none", zIndex: EDIT_BAR_Z - 1 }, children: offenders.map((o) => (_jsx("div", { style: {
|
|
465
|
+
position: "absolute",
|
|
466
|
+
left: o.x / scale,
|
|
467
|
+
top: o.y / scale,
|
|
468
|
+
width: o.width / scale,
|
|
469
|
+
height: o.height / scale,
|
|
470
|
+
border: "2px solid rgba(255, 64, 64, 0.95)",
|
|
471
|
+
boxShadow: "0 0 0 2px rgba(255, 64, 64, 0.3)",
|
|
472
|
+
boxSizing: "border-box",
|
|
473
|
+
}, children: _jsxs("span", { style: {
|
|
474
|
+
position: "absolute",
|
|
475
|
+
left: 0,
|
|
476
|
+
bottom: "100%",
|
|
477
|
+
marginBottom: 2,
|
|
478
|
+
padding: "2px 6px",
|
|
479
|
+
borderRadius: 4,
|
|
480
|
+
background: "rgba(180, 20, 20, 0.95)",
|
|
481
|
+
color: "#fff",
|
|
482
|
+
font: "600 10px/1.4 system-ui, sans-serif",
|
|
483
|
+
whiteSpace: "nowrap",
|
|
484
|
+
}, children: ["\u26A0 ", o.id, " escapes viewport (", overflowEdges(o), ")"] }) }, o.id))) })) : null, editing ? _jsx(HudEditBar, { layout: layout }) : null] }) }));
|
|
485
|
+
}
|
|
486
|
+
/** Human-readable edge list for the dev boundary badge, e.g. `right +260 · bottom +56`. */
|
|
487
|
+
function overflowEdges(overflow) {
|
|
488
|
+
const parts = [];
|
|
489
|
+
if (overflow.left > 0)
|
|
490
|
+
parts.push(`left +${overflow.left}`);
|
|
491
|
+
if (overflow.top > 0)
|
|
492
|
+
parts.push(`top +${overflow.top}`);
|
|
493
|
+
if (overflow.right > 0)
|
|
494
|
+
parts.push(`right +${overflow.right}`);
|
|
495
|
+
if (overflow.bottom > 0)
|
|
496
|
+
parts.push(`bottom +${overflow.bottom}`);
|
|
497
|
+
return parts.length > 0 ? parts.join(" · ") : "edge";
|
|
369
498
|
}
|
|
370
499
|
function chipAlignment(anchor) {
|
|
371
500
|
if (anchor.endsWith("right"))
|
package/dist/panels.d.ts
CHANGED
|
@@ -61,9 +61,15 @@ export declare function usePanels(defs: readonly PanelDef[], options?: UsePanels
|
|
|
61
61
|
* map). Accessible: every window is a `role="dialog"` with an `aria-label` and a focusable close button;
|
|
62
62
|
* ESC-close is wired by the manager. Unskinned — art-direct via `HudTheme` tokens and the `variation`.
|
|
63
63
|
*
|
|
64
|
-
*
|
|
64
|
+
* The host establishes its own stacking context at `zIndexBase` (default 40) so windows always paint
|
|
65
|
+
* *above* the HUD (stat bars, nameplates, frames typically sit at z 10–30) — no game should have to
|
|
66
|
+
* rediscover "my vitals bar bleeds through the open window" and wrap this in a `z-*` of its own. Per-
|
|
67
|
+
* window focus order still resolves inside that context, so clicking a window raises it over its peers.
|
|
68
|
+
* Raise `zIndexBase` to sit above a full-screen modal, or lower it to tuck windows beneath one.
|
|
69
|
+
*
|
|
70
|
+
* @capability panel-host render a manager's open windows as draggable, closable, z-stacked dialogs above the HUD
|
|
65
71
|
*/
|
|
66
|
-
export declare function PanelHost({ manager, render, panels, variation, shape, width, className, windowClassName, windowStyle, bodyStyle, }: {
|
|
72
|
+
export declare function PanelHost({ manager, render, panels, variation, shape, width, zIndexBase, className, windowClassName, windowStyle, bodyStyle, }: {
|
|
67
73
|
manager: PanelsManager;
|
|
68
74
|
/** Content for a panel by id. Takes precedence over `panels`. */
|
|
69
75
|
render?: (id: string) => ReactNode;
|
|
@@ -73,6 +79,8 @@ export declare function PanelHost({ manager, render, panels, variation, shape, w
|
|
|
73
79
|
shape?: HudFrameShape;
|
|
74
80
|
/** Default window width (per-panel unless overridden by the panel content). */
|
|
75
81
|
width?: number | string;
|
|
82
|
+
/** z-index of the host's stacking context — the floor every window paints above. Default 40 (above the HUD). */
|
|
83
|
+
zIndexBase?: number;
|
|
76
84
|
className?: string;
|
|
77
85
|
windowClassName?: string;
|
|
78
86
|
windowStyle?: CSSProperties;
|
package/dist/panels.js
CHANGED
|
@@ -186,12 +186,18 @@ function cascade(index) {
|
|
|
186
186
|
* map). Accessible: every window is a `role="dialog"` with an `aria-label` and a focusable close button;
|
|
187
187
|
* ESC-close is wired by the manager. Unskinned — art-direct via `HudTheme` tokens and the `variation`.
|
|
188
188
|
*
|
|
189
|
-
*
|
|
189
|
+
* The host establishes its own stacking context at `zIndexBase` (default 40) so windows always paint
|
|
190
|
+
* *above* the HUD (stat bars, nameplates, frames typically sit at z 10–30) — no game should have to
|
|
191
|
+
* rediscover "my vitals bar bleeds through the open window" and wrap this in a `z-*` of its own. Per-
|
|
192
|
+
* window focus order still resolves inside that context, so clicking a window raises it over its peers.
|
|
193
|
+
* Raise `zIndexBase` to sit above a full-screen modal, or lower it to tuck windows beneath one.
|
|
194
|
+
*
|
|
195
|
+
* @capability panel-host render a manager's open windows as draggable, closable, z-stacked dialogs above the HUD
|
|
190
196
|
*/
|
|
191
|
-
export function PanelHost({ manager, render, panels, variation, shape, width, className, windowClassName, windowStyle, bodyStyle, }) {
|
|
197
|
+
export function PanelHost({ manager, render, panels, variation, shape, width, zIndexBase = 40, className, windowClassName, windowStyle, bodyStyle, }) {
|
|
192
198
|
const open = orderedOpen(manager.state, manager.defs);
|
|
193
199
|
const content = (id) => render !== undefined ? render(id) : (panels?.[id] ?? null);
|
|
194
|
-
return (_jsx("div", { "data-jg-panel-host": "", className: className, style: { position: "absolute", inset: 0, pointerEvents: "none" }, children: open.map((def, index) => (_jsx(WindowView, { title: def.title ?? def.id, ariaLabel: def.title ?? def.id, closable: def.closable !== false, onClose: () => manager.close(def.id), onFocus: () => manager.focus(def.id), onMove: (pos) => manager.move(def.id, pos), pos: manager.state.pos[def.id] ?? cascade(index), z: manager.state.z[def.id] ?? 1, width: width, variation: variation, shape: shape, className: windowClassName, style: windowStyle, bodyStyle: bodyStyle, children: content(def.id) }, def.id))) }));
|
|
200
|
+
return (_jsx("div", { "data-jg-panel-host": "", className: className, style: { position: "absolute", inset: 0, zIndex: zIndexBase, pointerEvents: "none" }, children: open.map((def, index) => (_jsx(WindowView, { title: def.title ?? def.id, ariaLabel: def.title ?? def.id, closable: def.closable !== false, onClose: () => manager.close(def.id), onFocus: () => manager.focus(def.id), onMove: (pos) => manager.move(def.id, pos), pos: manager.state.pos[def.id] ?? cascade(index), z: manager.state.z[def.id] ?? 1, width: width, variation: variation, shape: shape, className: windowClassName, style: windowStyle, bodyStyle: bodyStyle, children: content(def.id) }, def.id))) }));
|
|
195
201
|
}
|
|
196
202
|
/**
|
|
197
203
|
* A single standalone window — title bar, close button, and title-bar drag over {@link HudFrame} chrome —
|
package/dist/provider.d.ts
CHANGED
|
@@ -4,6 +4,21 @@ export declare function GameProvider({ context, children }: {
|
|
|
4
4
|
context: GameContext;
|
|
5
5
|
children?: ReactNode;
|
|
6
6
|
}): import("react").JSX.Element;
|
|
7
|
+
/**
|
|
8
|
+
* Re-provide a captured {@link GameContext} across a nested React reconciler boundary. The `<Canvas>`
|
|
9
|
+
* of `@react-three/fiber` renders its children in a *separate* React root, so a `GameProvider` in the
|
|
10
|
+
* outer tree does NOT reach inside it — hooks like `useGameContext` / `useEntityRenderCues` throw. The
|
|
11
|
+
* fix the shell already uses internally: capture the context outside the boundary (with
|
|
12
|
+
* {@link useOptionalGameContext}) and wrap the inner tree in `GameContextBridge`. A `null` context
|
|
13
|
+
* renders children unchanged, so the same code works in showcases with no running game. This is the
|
|
14
|
+
* seam behind `EntityPreview` — reach for it directly when building a bespoke nested canvas.
|
|
15
|
+
*
|
|
16
|
+
* @capability game-context-bridge re-provide the GameContext across a nested Canvas/reconciler boundary
|
|
17
|
+
*/
|
|
18
|
+
export declare function GameContextBridge({ context, children, }: {
|
|
19
|
+
context: GameContext | null;
|
|
20
|
+
children?: ReactNode;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
7
22
|
export declare function useGameContext(): GameContext;
|
|
8
23
|
/** The game context if a `GameProvider` is present, otherwise `null` — for chrome that may render outside a running game (showcases, previews). */
|
|
9
24
|
export declare function useOptionalGameContext(): GameContext | null;
|
package/dist/provider.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useContext } from "react";
|
|
3
3
|
const GameReactContext = createContext(null);
|
|
4
4
|
export function GameProvider({ context, children }) {
|
|
5
5
|
return _jsx(GameReactContext.Provider, { value: context, children: children });
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Re-provide a captured {@link GameContext} across a nested React reconciler boundary. The `<Canvas>`
|
|
9
|
+
* of `@react-three/fiber` renders its children in a *separate* React root, so a `GameProvider` in the
|
|
10
|
+
* outer tree does NOT reach inside it — hooks like `useGameContext` / `useEntityRenderCues` throw. The
|
|
11
|
+
* fix the shell already uses internally: capture the context outside the boundary (with
|
|
12
|
+
* {@link useOptionalGameContext}) and wrap the inner tree in `GameContextBridge`. A `null` context
|
|
13
|
+
* renders children unchanged, so the same code works in showcases with no running game. This is the
|
|
14
|
+
* seam behind `EntityPreview` — reach for it directly when building a bespoke nested canvas.
|
|
15
|
+
*
|
|
16
|
+
* @capability game-context-bridge re-provide the GameContext across a nested Canvas/reconciler boundary
|
|
17
|
+
*/
|
|
18
|
+
export function GameContextBridge({ context, children, }) {
|
|
19
|
+
if (context === null)
|
|
20
|
+
return _jsx(_Fragment, { children: children });
|
|
21
|
+
return _jsx(GameReactContext.Provider, { value: context, children: children });
|
|
22
|
+
}
|
|
7
23
|
export function useGameContext() {
|
|
8
24
|
const ctx = useContext(GameReactContext);
|
|
9
25
|
if (ctx === null)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "React UI layer for JGengine: GameProvider, hooks, and headless primitives over @jgengine/core.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
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.14.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"react": "^19.0.0",
|
package/skills/jgengine/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Route game work to the smallest useful JGengine guidance set.
|
|
|
5
5
|
|
|
6
6
|
# JGengine intake and router
|
|
7
7
|
|
|
8
|
-
Read the
|
|
8
|
+
Read the JGengine README for package truth, commands, layering, and license — the repo-root [`README.md`](../../../README.md) in the monorepo, or [jgengine.com](https://jgengine.com) from a scaffolded consumer project (where the relative link does not resolve). This skill decides what to load; it is not an engine manual.
|
|
9
9
|
|
|
10
10
|
## 1. Establish the target
|
|
11
11
|
|
|
@@ -20,6 +20,8 @@ Treat the pitch as a unique composition of needs, not a genre to fill in — nev
|
|
|
20
20
|
|
|
21
21
|
For a new in-repo game, scaffold with `bun run new:game "<id-or-title>"` (thin script alias for `jgengine create`, auto-detects the in-repo variant). Outside this monorepo, start with `npx jgengine create`; never copy `Games/*`.
|
|
22
22
|
|
|
23
|
+
For an existing game asked to pick up a new engine release ("jgengine published a new version, grab it"), run `npx jgengine upgrade` in the project first: it diffs installed `@jgengine/*` versions against the latest release and prints every Migrate step and Adopt-worthy addition in between (`--json` for structured output). Bump, migrate oldest-first, then work the Adopt lists instead of reconstructing the changelog by hand.
|
|
24
|
+
|
|
23
25
|
## 2. Select domains
|
|
24
26
|
|
|
25
27
|
Load only rows the target needs. Do not preload every domain.
|
|
@@ -43,7 +45,7 @@ World content routes editor-first: any request that adds, moves, restyles, or re
|
|
|
43
45
|
Within each selected domain:
|
|
44
46
|
|
|
45
47
|
0. Fastest start for a common composition: `npx jgengine recipe <name>` prints a vetted, SDK-typechecked snippet (imports + wired code) for `combat-loop`, `boss-telegraph`, `loot`, `quest`, `coop-presence`, `third-person-camera`; `npx jgengine recipe` lists them. Copy that instead of reconning per-file.
|
|
46
|
-
1. Search `capabilities.md` by intent; it maps needs to imports.
|
|
48
|
+
1. Search `capabilities.md` by intent; it maps needs to imports. Outside the monorepo (or before a domain skill is even loaded), `npx jgengine find <intent>` greps every shipped capability index at once and prints the primitive + import — reach for it the moment you're tempted to hand-roll a HUD window, inventory grid, paperdoll, hotkey listener, minimap, stat bar, or character motion. If you're writing a `z-index`, a `keydown` for a panel, a `<div>` inventory, or a limb bob, a shipped drop-in almost certainly exists (`usePanels`/`PanelHost`, `InventoryGrid`, `CharacterSheet`/`Paperdoll`, `PartMotionRig`, `EntityPreview`); games own the *skin*, not the re-derivation.
|
|
47
49
|
2. Open `api.md` only when exact signatures or export inventory are needed.
|
|
48
50
|
3. Open the linked reference only for a deeper recipe or trap.
|
|
49
51
|
4. For how several primitives wire into a running loop, read the domain's `recipes/` — connected, genre-free walkthroughs organized by composition seam; this skill's own [recipes/minimal-game.md](recipes/minimal-game.md) is the default whole-game walkthrough. Prefer a recipe over reading a game's source; the games under `Games/*` are not templates or references, and a hybrid game is just a different composition of the same primitives.
|
package/skills/jgengine/api.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
8
8
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
9
|
-
- `VERSION` (const): const VERSION: "0.
|
|
9
|
+
- `VERSION` (const): const VERSION: "0.14.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
10
10
|
|
|
11
11
|
## @jgengine/core/authoring
|
|
12
12
|
|
|
@@ -220,7 +220,7 @@
|
|
|
220
220
|
|
|
221
221
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
222
222
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
223
|
-
- `VERSION` (const): const VERSION: "0.
|
|
223
|
+
- `VERSION` (const): const VERSION: "0.14.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
224
224
|
|
|
225
225
|
## @jgengine/core/runtime/adapter
|
|
226
226
|
|
|
@@ -16,7 +16,7 @@ Search [capabilities.md](capabilities.md), use [api.md](api.md) for signatures,
|
|
|
16
16
|
1. Write the game's UI art direction first (see [reference.md](reference.md) §1) — fantasy, shape/material language, hierarchy, forbidden patterns. Stock glass widgets are not a stand-in for this step.
|
|
17
17
|
2. Define the real UI states: attract, main menu (the game's front-end), character create/select where relevant, live play, pause, settings, credits, results, empty/error, and relevant overlays.
|
|
18
18
|
3. Read state through selectors/hooks; keep simulation mutation behind commands.
|
|
19
|
-
4. Compose this game's UI, reaching first for the shipped building blocks — `InventoryGrid`, `usePanels`/`PanelHost`/`Window`, `CharacterSheet`/`Paperdoll`, and the action/selection bars — and composing + reskinning them; only hand-roll custom markup where no building block fits. Toggleable windows (bag `B`, character `C`, etc.) come from `usePanels`, not hand-rolled open/close state. Prefer headless data + game-owned chrome. When a second presentation is plausible, split data / renderer / chrome (minimap stack example: `useLiveMarkers` → track/renderer → game frame; see reference §6).
|
|
19
|
+
4. Compose this game's UI, reaching first for the shipped building blocks — `InventoryGrid`, `usePanels`/`PanelHost`/`Window`, `CharacterSheet`/`Paperdoll`, and the action/selection bars — and composing + reskinning them; only hand-roll custom markup where no building block fits. Toggleable windows (bag `B`, character `C`, etc.) come from `usePanels`, not hand-rolled open/close state — and `PanelHost` already stacks windows above the HUD (`zIndexBase`) and wires hotkeys/ESC, so never hand-roll a `z-*` or keydown listener for them. A live 3D character-screen portrait is `EntityPreview` (`@jgengine/shell/render/EntityPreview`): it owns the nested `<Canvas>`, the `GameContextBridge` across the R3F boundary, and the `StudioStage` lighting — pass the game's own `renderEntity` as children (or a native `model`+`instanceId`) and it mirrors the live entity; don't re-derive the context bridge by hand. Prefer headless data + game-owned chrome. When a second presentation is plausible, split data / renderer / chrome (minimap stack example: `useLiveMarkers` → track/renderer → game frame; see reference §6).
|
|
20
20
|
5. Make keyboard, pointer, touch, controller, focus, and screen-reader behavior explicit.
|
|
21
21
|
6. Add preview fixtures using the real components for fast deterministic capture.
|
|
22
22
|
7. Verify desktop and mobile layouts through `jgengine-verify`. A HUD that could pass for another game's default chrome fails visual review.
|
|
@@ -31,6 +31,8 @@ Existing React games keep their entity store and use the focused
|
|
|
31
31
|
- **The main menu is a game front-end, not marketing.** The runner loads straight into the game's own menu; it offers the real front-end verbs the game supports — New Game, Continue/Load (save slots), Create/Choose character or class, Settings, Credits — each an authored screen in the game's art direction, not a landing page or raw scaffold. See [reference.md](reference.md) §"Main menu".
|
|
32
32
|
- **Settings is game-owned and reachable from a sensible place.** Every game ships settings as *its* settings — skinned and placed inline with its UI (main menu and pause), never the engine's stock face floated into a corner. Credits are a reachable in-game screen, not attribution buried in a build artifact.
|
|
33
33
|
- Engine packages ship both headless seams *and* good drop-in building blocks. The seams are layout (`HudCanvas`/`HudPanel`), data hooks, interaction models, and tokens; the building blocks are the parts almost every game needs — inventory grids (`InventoryGrid`), toggleable windows (`usePanels`/`PanelHost`/`Window`), character sheets (`CharacterSheet`/`Paperdoll`), stat/vitals bars, and action/selection bars. Composing and reskinning these is using the engine correctly, not out of policy. What stays out of scope is a whole finished game *face* or a genre theme preset shipped as the product (see [AGENTS.md](../../../AGENTS.md)).
|
|
34
|
+
- **Skin the shared chrome with one `HudTheme` token object.** `hudThemeVars(theme)` emits the `--jg-*` custom properties that the atomic bars, `HudFrame variation="themed"`, painted slots, and the minimap ring all read, so one theme spread on any HUD ancestor reskins the whole subtree — palette, frame material, corner radius, glow, and slot chrome. Author your own theme (tower-guard's `fieldkitVars` is the model); `HUD_THEME_PRESETS` / `resolveHudTheme("<preset>")` are demo/scaffold starting points, never a game's identity. Caller `style`/`renderItem` overrides still win over the theme default.
|
|
35
|
+
- **Paint slot icons with `IconTreatment`, never render raw `itemId` strings.** It draws a `GameIcon` glyph (or any node) over a school/element-keyed radial gradient with a gloss + vignette and optional count/keycap badges, and reads the `--jg-slot-*` / `--jg-accent` `HudTheme` tokens so a theme change reskins every treated icon. `schoolForItem`/`schoolForAction` infer the gradient from an id; `treatedItemIcon(itemId)` / `treatedActionIcon(action)` are ready adapters. Use it in hotbars and ability bars so slots read as painted icons, not debug text.
|
|
34
36
|
- Never reach for a genre HUD kit, theme preset, or "default RPG/FPS chrome" as the game's *identity*. Composing the shared building blocks (bag, character, spellbook, action-bar windows) is fine and expected — it's a prefab genre *look/identity* dropped in unchanged that's discouraged. Build the look this pitch needs on top of them.
|
|
35
37
|
- Layout and skin remain caller-controlled; shared primitives own reusable behavior, not product look.
|
|
36
38
|
- SSR-visible output is hydration-stable; round computed SVG values at the boundary.
|
|
@@ -41,3 +43,4 @@ Existing React games keep their entity store and use the focused
|
|
|
41
43
|
- Do not duplicate inventory, combat, quest, or selection state inside components.
|
|
42
44
|
- A static facsimile preview does not test the real UI; compose production components with fixtures.
|
|
43
45
|
- Visual quality is judged from rendered evidence, not component counts or prose.
|
|
46
|
+
- Shown HUD UI must fit the player viewport. `HudCanvas` runs a live boundary check on every registered `HudPanel`, any element tagged `data-hud-window`, and its own direct children: if a *visible* surface crosses a viewport edge it draws a red dev overlay on the offender, `console.warn`s, and sets `data-hud-overflow` (which fails the `bun run shoot` gate). A closed/parked/hidden surface (off-screen drawer, unopened menu, `visibility:hidden`, `aria-hidden`) never trips it — only surfaces partly on screen and partly past an edge. Deliberate bleed opts out with `data-hud-allow-overflow`. Mount custom windows inside `HudCanvas` (tag deeper ones `data-hud-window`) so the check can see them.
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
- `orderedOpen` (function): function orderedOpen(state: PanelState, defs: readonly PanelDef[]): PanelDef[] — The open panels' defs sorted ascending by z — the render order for a window host (map to elements so the last, highest-z window paints on top). Defs unknown to the state are skipped.
|
|
173
173
|
- `orientationGateActive` (function): function orientationGateActive(requirement: OrientationRequirement, liveOrientation: LayoutOrientation): boolean — The rotate gate blocks gameplay: a hard requirement (or `unsupported`) the live orientation doesn't satisfy.
|
|
174
174
|
- `orientationHintActive` (function): function orientationHintActive(requirement: OrientationRequirement, liveOrientation: LayoutOrientation): boolean — An advisory rotate hint applies: a preference (not a hard gate) the live orientation doesn't satisfy.
|
|
175
|
-
- `overflowingPanels` (function): function overflowingPanels(panels: readonly { id: string; rect: HudRect }[], viewport: HudSize, tolerance = 1.5): HudOverflow[] — Every panel rect that escapes the viewport — the data behind the HUD overflow gate.
|
|
175
|
+
- `overflowingPanels` (function): function overflowingPanels(panels: readonly { id: string; rect: HudRect }[], viewport: HudSize, tolerance = 1.5): HudOverflow[] — Every panel rect that escapes the viewport — the data behind the HUD overflow gate. A rect is only reported when it is partly visible *and* crosses an edge; a fully off-screen (closed/hidden) surface is skipped, so parked drawers and unopened menus never raise a false alarm.
|
|
176
176
|
- `panelByHotkey` (function): function panelByHotkey(defs: readonly PanelDef[], code: string): string | null — Find the panel whose `hotkey` matches `code`, case-insensitively — the keybind router. `code` may be a `KeyboardEvent.code` (`"KeyB"`) or key (`"b"`); it is compared verbatim to each def's `hotkey`, so author the hotkey in whichever form the caller feeds in. Returns the first match's id, or null.
|
|
177
177
|
- `placePopover` (function): function placePopover(anchor: UiRect, content: UiSize, viewport: UiSize, options?: PopoverOptions): PopoverPlacement — Position a popover/tooltip of `content` size against its `anchor` rectangle inside `viewport`: open on the `preferred` side, flip to the opposite when that side lacks room and the opposite has more, then clamp the top-left so the box stays fully on screen (respecting `margin`). Pure and deterministic — no DOM reads — so it is SSR-safe and unit-testable, and the React tooltip is a thin shell over it.
|
|
178
178
|
- `registerHudPanelType` (function): function registerHudPanelType(def: HudPanelTypeDef): void — Register a HUD panel type so the editor can list/add/configure it and canvas resize knows axes.
|
|
@@ -256,7 +256,7 @@
|
|
|
256
256
|
- `HudPlatform` (type): type HudPlatform = "web" | "mobile" — Where a game is meant to be played. `"web"` alone keeps today's desktop-first HUD; adding `"mobile"` turns on design-resolution fit scaling on compact displays.
|
|
257
257
|
- `HudViewportConfig` (interface): interface HudViewportConfig extends HudFitConfig — Per-game HUD viewport declaration carried on `PlayableGame.hudFit`; `mobile` overrides the fit on compact displays so the owner can tune the phone layout separately.
|
|
258
258
|
- `hudScaleForViewport` (function): function hudScaleForViewport(fit: Required<HudFitConfig>, viewport: HudSize): number — The one scaling rule for every display: the ratio of the live viewport to the authored design size along the limiting axis, clamped. 1 on a viewport at or above design size; smoothly below 1 down to `minScale` on phones.
|
|
259
|
-
- `overflowingPanels` (function): function overflowingPanels(panels: readonly { id: string; rect: HudRect }[], viewport: HudSize, tolerance = 1.5): HudOverflow[] — Every panel rect that escapes the viewport — the data behind the HUD overflow gate.
|
|
259
|
+
- `overflowingPanels` (function): function overflowingPanels(panels: readonly { id: string; rect: HudRect }[], viewport: HudSize, tolerance = 1.5): HudOverflow[] — Every panel rect that escapes the viewport — the data behind the HUD overflow gate. A rect is only reported when it is partly visible *and* crosses an edge; a fully off-screen (closed/hidden) surface is skipped, so parked drawers and unopened menus never raise a false alarm.
|
|
260
260
|
- `rectOverflow` (function): function rectOverflow(rect: HudRect, viewport: HudSize, tolerance = 1.5): Omit<HudOverflow, "id"> | null — Non-null when any edge of `rect` falls outside `viewport` by more than `tolerance` px; each field is the overflow distance past that edge (0 when inside).
|
|
261
261
|
- `resolveHudFit` (function): function resolveHudFit(config: HudViewportConfig | undefined, mobile: boolean): Required<HudFitConfig> — ⚠ undocumented
|
|
262
262
|
|
|
@@ -392,6 +392,7 @@
|
|
|
392
392
|
- `FriendRequestsList` (function): function FriendRequestsList({ className, rowClassName, acceptClassName, declineClassName, emptyState, renderRequest, }: { className?: string; rowClassName?: string; acceptClassName?: string; declineClassName?: string; emptyState?: ReactNode; renderRequest?: (request: FriendRequestEntry) => ReactNode… — ⚠ undocumented
|
|
393
393
|
- `FriendRow` (function): function FriendRow({ friend, className, dotClassName, children, }: { friend: FriendEntry; className?: string; dotClassName?: string; children?: ReactNode; }): React.JSX.Element — ⚠ undocumented
|
|
394
394
|
- `FriendsList` (function): function FriendsList({ className, rowClassName, dotClassName, emptyState, renderFriend, }: { className?: string; rowClassName?: string; dotClassName?: string; emptyState?: ReactNode; renderFriend?: (friend: FriendEntry) => ReactNode; }): React.JSX.Element — ⚠ undocumented
|
|
395
|
+
- `GameContextBridge` (function): function GameContextBridge({ context, children, }: { context: GameContext | null; children?: ReactNode; }): React.JSX.Element — Re-provide a captured {@link GameContext} across a nested React reconciler boundary. The `<Canvas>` of `@react-three/fiber` renders its children in a *separate* React root, so a `GameProvider` in the outer tree does NOT reach inside it — hooks like `useGameContext` / `useEntityRenderCues` throw. The fix the shell already uses internally: capture the context outside the boundary (with {@link useOptionalGameContext}) and wrap the inner tree in `GameContextBridge`. A `null` context renders children unchanged, so the same code works in showcases with no running game. This is the seam behind `EntityPreview` — reach for it directly when building a bespoke nested canvas.
|
|
395
396
|
- `GameIdentityProvider` (function): function GameIdentityProvider({ source, children, }: { source: IdentitySource; children?: ReactNode; }): React.JSX.Element — ⚠ undocumented
|
|
396
397
|
- `GamePreviewComponent` (type): type GamePreviewComponent = ComponentType<GamePreviewProps> — ⚠ undocumented
|
|
397
398
|
- `GamePreviewProps` (type): type GamePreviewProps = { className?: string; } — ⚠ undocumented
|
|
@@ -450,7 +451,7 @@
|
|
|
450
451
|
- `MinimapTrackPip` (interface): interface MinimapTrackPip — A point marker on the {@link MinimapTrack} rail at a 0..1 fraction (e.g. a gate, the exit, or the player).
|
|
451
452
|
- `MinimapTrackProps` (interface): interface MinimapTrackProps — Props for {@link MinimapTrack}.
|
|
452
453
|
- `MinimapTrackSpan` (interface): interface MinimapTrackSpan — A colored span across the {@link MinimapTrack} rail, given by 0..1 `start`/`end` fractions (e.g. from core `trackFraction`).
|
|
453
|
-
- `PanelHost` (function): function PanelHost({ manager, render, panels, variation, shape, width, className, windowClassName, windowStyle, bodyStyle, }: { manager: PanelsManager; /** Content for a panel by id. Takes precedence over `panels`. */ render?: (id: string) => ReactNode; /** Static content map by
|
|
454
|
+
- `PanelHost` (function): function PanelHost({ manager, render, panels, variation, shape, width, zIndexBase = 40, className, windowClassName, windowStyle, bodyStyle, }: { manager: PanelsManager; /** Content for a panel by id. Takes precedence over `panels`. */ render?: (id: string) => ReactNode; /** Static content map by pan… — Renders the open panels of a {@link usePanels} manager as absolutely-positioned, draggable, closable windows in z-order — the WoW window layer. Each window uses {@link HudFrame} chrome, is dragged by its title bar, raises focus on pointer-down, and gets its content from the `render` prop (or a `panels` map). Accessible: every window is a `role="dialog"` with an `aria-label` and a focusable close button; ESC-close is wired by the manager. Unskinned — art-direct via `HudTheme` tokens and the `variation`.
|
|
454
455
|
- `PanelKeyResult` (interface): interface PanelKeyResult — React chrome over the headless panel/window model (`@jgengine/core/ui/panelModel`) — the toggleable, draggable, z-stacked windows a WoW-style UI is made of (B for bag, C for character, ESC to close). `usePanels` is the DATA/HOOK layer (open/close/toggle/focus + a keybind listener), `PanelHost` renders the open windows in z-order, and `Window` is a standalone one-off window. All unskinned and token-driven: games reskin via `HudTheme` tokens and by swapping the `HudFrame` variation.
|
|
455
456
|
- `PanelsManager` (interface): interface PanelsManager — The live panel manager returned by {@link usePanels} and consumed by {@link PanelHost}.
|
|
456
457
|
- `Paperdoll` (function): function Paperdoll({ slots, portrait, slotSize = 44, onSlotActivate, className, style }: PaperdollProps): ReactNode — Arranges caller-supplied equip slots as a paperdoll — worn gear in two flanking columns around a center `portrait` (or a silhouette placeholder), with any `bottom` slots in a row beneath it. Each slot reuses the atomic {@link EquipmentSlots} tile (filled icon or themed empty placeholder), carries a label, and is a focusable `role="group"` with an `aria-label`; `onSlotActivate` fires on click/Enter/Space so games can wire drag or an equip picker. Composition and skin stay the game's.
|
|
@@ -949,7 +950,7 @@
|
|
|
949
950
|
|
|
950
951
|
## @jgengine/react/panels
|
|
951
952
|
|
|
952
|
-
- `PanelHost` (function): function PanelHost({ manager, render, panels, variation, shape, width, className, windowClassName, windowStyle, bodyStyle, }: { manager: PanelsManager; /** Content for a panel by id. Takes precedence over `panels`. */ render?: (id: string) => ReactNode; /** Static content map by
|
|
953
|
+
- `PanelHost` (function): function PanelHost({ manager, render, panels, variation, shape, width, zIndexBase = 40, className, windowClassName, windowStyle, bodyStyle, }: { manager: PanelsManager; /** Content for a panel by id. Takes precedence over `panels`. */ render?: (id: string) => ReactNode; /** Static content map by pan… — Renders the open panels of a {@link usePanels} manager as absolutely-positioned, draggable, closable windows in z-order — the WoW window layer. Each window uses {@link HudFrame} chrome, is dragged by its title bar, raises focus on pointer-down, and gets its content from the `render` prop (or a `panels` map). Accessible: every window is a `role="dialog"` with an `aria-label` and a focusable close button; ESC-close is wired by the manager. Unskinned — art-direct via `HudTheme` tokens and the `variation`.
|
|
953
954
|
- `PanelKeyResult` (interface): interface PanelKeyResult — React chrome over the headless panel/window model (`@jgengine/core/ui/panelModel`) — the toggleable, draggable, z-stacked windows a WoW-style UI is made of (B for bag, C for character, ESC to close). `usePanels` is the DATA/HOOK layer (open/close/toggle/focus + a keybind listener), `PanelHost` renders the open windows in z-order, and `Window` is a standalone one-off window. All unskinned and token-driven: games reskin via `HudTheme` tokens and by swapping the `HudFrame` variation.
|
|
954
955
|
- `PanelsManager` (interface): interface PanelsManager — The live panel manager returned by {@link usePanels} and consumed by {@link PanelHost}.
|
|
955
956
|
- `UsePanelsOptions` (interface): interface UsePanelsOptions — Options for {@link usePanels}.
|
|
@@ -965,6 +966,7 @@
|
|
|
965
966
|
|
|
966
967
|
## @jgengine/react/provider
|
|
967
968
|
|
|
969
|
+
- `GameContextBridge` (function): function GameContextBridge({ context, children, }: { context: GameContext | null; children?: ReactNode; }): React.JSX.Element — Re-provide a captured {@link GameContext} across a nested React reconciler boundary. The `<Canvas>` of `@react-three/fiber` renders its children in a *separate* React root, so a `GameProvider` in the outer tree does NOT reach inside it — hooks like `useGameContext` / `useEntityRenderCues` throw. The fix the shell already uses internally: capture the context outside the boundary (with {@link useOptionalGameContext}) and wrap the inner tree in `GameContextBridge`. A `null` context renders children unchanged, so the same code works in showcases with no running game. This is the seam behind `EntityPreview` — reach for it directly when building a bespoke nested canvas.
|
|
968
970
|
- `GameProvider` (function): function GameProvider({ context, children }: { context: GameContext; children?: ReactNode }): React.JSX.Element — ⚠ undocumented
|
|
969
971
|
- `useGameContext` (function): function useGameContext(): GameContext — ⚠ undocumented
|
|
970
972
|
- `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).
|
|
@@ -1494,6 +1496,13 @@
|
|
|
1494
1496
|
|
|
1495
1497
|
- `createGradePass` (function): function createGradePass(config: GradeConfig = {}): ShaderPass — Build the display-space colour-grade pass (lift/gain/gamma, saturation, vignette, grain). Advance `uniforms.uTime.value` each frame to animate the grain.
|
|
1496
1498
|
|
|
1499
|
+
## @jgengine/shell/postfx/postfxOverlay
|
|
1500
|
+
|
|
1501
|
+
- `POSTFX_OVERLAY_USERDATA` (const): const POSTFX_OVERLAY_USERDATA: { readonly jgPostfxOverlay: true } — Spread onto a group's `userData` to mark its subtree as a screen/world overlay effect — additive glows, telegraphs, tracers, debug gizmos — that post-processing scene prepasses (GTAO's normal/depth render, DOF's depth render) must skip. Those prepasses render the scene with `overrideMaterial`, which turns transparent overlay quads into opaque stamps in the AO/DOF buffers (visible as black squares over hit sparks). The shell marks its own combat VFX and collision-debug mounts; games mark custom overlay effects the same way.
|
|
1502
|
+
- `hidePostfxOverlays` (function): function hidePostfxOverlays(scene: THREE.Object3D, out: THREE.Object3D[]): void — Hide every currently visible marked object under `scene`, recording what was hidden into `out` (cleared first, no allocation on the hot path) so {@link restorePostfxOverlays} can undo exactly that set.
|
|
1503
|
+
- `isPostfxOverlay` (function): function isPostfxOverlay(object: { userData?: Record<string, unknown> }): boolean — True when the object opted out of postfx scene prepasses via {@link POSTFX_OVERLAY_USERDATA}.
|
|
1504
|
+
- `restorePostfxOverlays` (function): function restorePostfxOverlays(hidden: THREE.Object3D[]): void — Restore visibility for the objects hidden by {@link hidePostfxOverlays} and clear the list.
|
|
1505
|
+
|
|
1497
1506
|
## @jgengine/shell/registry
|
|
1498
1507
|
|
|
1499
1508
|
- `GameRegistry` (type): type GameRegistry = Record<string, () => Promise<PlayableGame>> — ⚠ undocumented
|
|
@@ -1502,6 +1511,11 @@
|
|
|
1502
1511
|
- `RenderObject` (type): type RenderObject = (object: SceneObject) => ReactNode — ⚠ undocumented
|
|
1503
1512
|
- `resolveGameLoader` (function): function resolveGameLoader(registry: GameRegistry, gameId: string, fallbackGameId?: string): (() => Promise<PlayableGame>) | undefined — ⚠ undocumented
|
|
1504
1513
|
|
|
1514
|
+
## @jgengine/shell/render/EntityPreview
|
|
1515
|
+
|
|
1516
|
+
- `EntityPreview` (function): function EntityPreview({ children, model, instanceId, mood = "studio", backdrop, turntable = 0, faceCamera = false, forward, environment = true, cameraPosition = [0, 1, 3], fov = 35, dpr = [1, 2], className, style, }: EntityPreviewProps): React.JSX.Element — Drop-in live 3D preview of a single entity — the character-screen portrait, unit inspector, or loadout viewer. It owns all the plumbing a game would otherwise re-derive by hand: a nested `<Canvas>`, the {@link GameContextBridge} that carries the running `GameContext` across the R3F reconciler boundary (so the subject's `useEntityRenderCues` / `useGameContext` hooks resolve and it mirrors the live game), a {@link StudioStage} 3-point lighting rig with optional turntable / face-camera, and a capture-friendly GL surface. The game keeps ownership of its `renderEntity`: pass it as `children`, or hand a native `model` (+ `instanceId`) for an engine-composed body. Bound to a live `instanceId`, the portrait walks, flinches, and topples in lockstep with the world instead of a canned spin.
|
|
1517
|
+
- `EntityPreviewProps` (interface): interface EntityPreviewProps — Props for {@link EntityPreview}.
|
|
1518
|
+
|
|
1505
1519
|
## @jgengine/shell/render/PartMotion
|
|
1506
1520
|
|
|
1507
1521
|
- `PartMotionRig` (function): function PartMotionRig({ parts, model, instanceId, renderPart, children, }: { parts: readonly ModelPart[]; model: ModelConfig; instanceId?: string; /** Renders one part's model content (the shell passes its part renderer to avoid an import cycle). */ renderPart: (part: ModelPart, index: number) => R… — Procedural motion rig for a rig-less part-composed character (`ModelPart.role` — see `@jgengine/core/game/partAnimation`). Wraps the whole composition in a root group that bobs, breathes, flinches on `combat.hitReaction`, and topples on `entity.died`, while each role-tagged part swings around its authored transform — legs/arms counter-phase from the entity's live movement speed, head counter-sway, tail wag, wing flap. Untagged parts render as static kit pieces. Children are the base model content (primitive, attachments).
|
|
@@ -1531,6 +1545,12 @@
|
|
|
1531
1545
|
- `measureLocalBounds` (function): function measureLocalBounds(root: THREE.Object3D): MeasuredLocalBounds | null — Measure the meshes under `root` in `root`'s own frame (root's transform is the measuring frame, children compose their local matrices below it). Returns `null` when nothing measurable is mounted. Skinned meshes measure at bind pose — the standard engine approximation.
|
|
1532
1546
|
- `reportMeasuredBounds` (function): function reportMeasuredBounds(ctx: GameContext, target: "entity" | "object", key: string, bounds: MeasuredLocalBounds): void — Forward a measured result into the context (entity kind or object catalog id), deduping repeat reports of the same shape so remounts and multi-instance kinds don't churn collider resolution.
|
|
1533
1547
|
|
|
1548
|
+
## @jgengine/shell/render/measureCollisionMesh
|
|
1549
|
+
|
|
1550
|
+
- `MeasuredCollisionTriangles` (interface): interface MeasuredCollisionTriangles extends CollisionMeshSource — Entity-local triangle soup of the meshes under a measured root, plus the contributing mesh count (same async fill-in detection as measured bounds). Satisfies {@link CollisionMeshSource}.
|
|
1551
|
+
- `measureLocalCollisionTriangles` (function): function measureLocalCollisionTriangles(root: THREE.Object3D, transform?: { scale: number; offset: readonly [number, number, number] }): MeasuredCollisionTriangles | null — Collect the triangle soup of the meshes under `root` in `root`'s own frame — the mesh-accurate counterpart to `measureLocalBounds`, with the same visibility/sprite/{@link MEASURE_EXCLUDE_KEY} exclusions (skinned meshes contribute their bind pose). An optional uniform `scale` + `offset` maps the result into entity-local space when the caller mounts the root scaled/translated (the model-primitive path). Returns `null` when nothing is measurable or the soup exceeds the triangle budget — callers keep their box fallback.
|
|
1552
|
+
- `reportMeasuredCollisionMesh` (function): function reportMeasuredCollisionMesh(ctx: GameContext, target: "entity" | "object", key: string, triangles: MeasuredCollisionTriangles): void — Forward measured triangles into the context (entity kind or object catalog id), deduping repeat reports of the same soup so remounts and multi-instance kinds don't rebuild the BVH.
|
|
1553
|
+
|
|
1534
1554
|
## @jgengine/shell/render/modelRender
|
|
1535
1555
|
|
|
1536
1556
|
- `MaterialCache` (interface): interface MaterialCache — ⚠ undocumented
|
|
@@ -65,6 +65,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
65
65
|
|
|
66
66
|
- `EntityPortrait` (function) · `import { EntityPortrait } from "@jgengine/react"`
|
|
67
67
|
|
|
68
|
+
## entity-preview — live 3D entity portrait — nested Canvas + context bridge + studio stage, driven by the game's own renderEntity
|
|
69
|
+
|
|
70
|
+
- `EntityPreview` (function) · `import { EntityPreview } from "@jgengine/shell/render/EntityPreview"`
|
|
71
|
+
|
|
68
72
|
## entity-render-cues — live velocity/bob/fire/reload/hit/death cues for a custom renderEntity or viewmodel component
|
|
69
73
|
|
|
70
74
|
- `useEntityRenderCues` (function) · `import { useEntityRenderCues } from "@jgengine/shell/render/useEntityRenderCues"`
|
|
@@ -77,6 +81,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
77
81
|
|
|
78
82
|
- `useEventMeter` (function) · `import { useEventMeter } from "@jgengine/react"`
|
|
79
83
|
|
|
84
|
+
## game-context-bridge — re-provide the GameContext across a nested Canvas/reconciler boundary
|
|
85
|
+
|
|
86
|
+
- `GameContextBridge` (function) · `import { GameContextBridge } from "@jgengine/react"`
|
|
87
|
+
|
|
80
88
|
## heightfield-mesh-update — in-place partial update of a displaced ground plane mesh
|
|
81
89
|
|
|
82
90
|
- `displaceHeightfieldGeometry` (function) · `import { displaceHeightfieldGeometry } from "@jgengine/shell/terrain"`
|
|
@@ -186,7 +194,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
186
194
|
|
|
187
195
|
- `focusPanel` (function) · `import { focusPanel } from "@jgengine/core/ui"`
|
|
188
196
|
|
|
189
|
-
## panel-host — render a manager's open windows as draggable, closable, z-stacked dialogs
|
|
197
|
+
## panel-host — render a manager's open windows as draggable, closable, z-stacked dialogs above the HUD
|
|
190
198
|
|
|
191
199
|
- `PanelHost` (function) · `import { PanelHost } from "@jgengine/react"`
|
|
192
200
|
|