@jgengine/shell 0.37.0 → 0.38.1
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 +82 -0
- package/dist/render/SceneModels.d.ts +13 -0
- package/dist/render/SceneModels.js +38 -2
- package/dist/render/singleVariantLod.d.ts +31 -0
- package/dist/render/singleVariantLod.js +54 -0
- package/dist/scatter/InstancedScatter.js +3 -3
- package/dist/world/StaticObjectModels.js +2 -1
- package/package.json +4 -4
- package/skills/jgengine/api.md +2 -2
- package/skills/jgengine-ui/SKILL.md +1 -1
- package/skills/jgengine-ui/api.md +68 -0
- package/skills/jgengine-ui/capabilities.md +40 -0
- package/skills/jgengine-ui/reference.md +1 -1
- package/skills/jgengine-world/api.md +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,88 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
26
26
|
`packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
|
|
27
27
|
-->
|
|
28
28
|
|
|
29
|
+
## 0.38.1
|
|
30
|
+
|
|
31
|
+
### Migrate
|
|
32
|
+
|
|
33
|
+
- **Bump lockstep SDK packages to `^0.38.1`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.35.1`; `@jgengine/github` is `0.25.1`.
|
|
34
|
+
- Nothing to migrate. `variantLevels()` was renamed to `ModelVariantConfig.shadows` before any release shipped it, so no published version ever exported it and no consumer can be holding one. A level that wants to stop paying the near level's shadow cost writes `{ url, shadows: "none" }`; leaving `shadows` out keeps the model's own answer.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- `ModelVariantConfig.shadows` (`@jgengine/core/world/modelLod`): a model variant level declares its own shadow participation as a plain field, `"inherit" | "cast" | "receive" | "both" | "none"`. `"inherit"`, the default, keeps the model's own `shadows`, so every existing model draws as before; a far level set to `"none"` leaves the shadow pass while still drawing its mesh.
|
|
39
|
+
- `ModelShadowMode` (`@jgengine/core/world/modelLod`) names the four-value shadow vocabulary, and `ModelConfig.shadows` uses it.
|
|
40
|
+
- `variantLevelModel` (`@jgengine/shell/render/singleVariantLod`) is the one place that resolves what a level draws and how it shadows; the single prop, batched prop and scatter paths all ask it, so a level's asset and its shadow answer cannot disagree.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- `SaveSlotsPane` cards are one box each: equal tracks, `grid-auto-rows: 1fr`, a card edge that reads over a bright scene, and a name that ellipsises instead of widening the card. A save with a long name and one with none produce the same card box.
|
|
45
|
+
- `GameMenu` puts the primary verbs on their own scrim, so their labels stay legible over whatever the live scene is showing instead of relying on a text shadow.
|
|
46
|
+
- The theme emits three menu tokens the slot card and the verb surface read: `--jg-menu-card-height` (`clamp(244px, 27vh, 300px)`), `--jg-menu-card-thumb` (`clamp(88px, 14vh, 160px)`) and `--jg-menu-surface-radius` (`8px`). A game overrides them in its own `HudTheme` instead of patching the menu components.
|
|
47
|
+
|
|
48
|
+
## 0.38.0
|
|
49
|
+
|
|
50
|
+
### Migrate
|
|
51
|
+
|
|
52
|
+
- **Bump lockstep SDK packages to `^0.38.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.35.0`; `@jgengine/github` is `0.25.0`.
|
|
53
|
+
- Composing a session loop directly: pass your rule as `settle`, `carries: false` when nothing is carried, and your authored options as `offers`. `createRunLoop` is removed one minor release after Beacon Bastion and Drift Foundry stop using it.
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- `GameMenu` and `GamePauseMenu` (`@jgengine/react/gameMenu`): a full-bleed menu surface over the live scene or drifting key art, with a token-sized logo, an animated navigable verb list, a generated selection tick and a detail pane per verb. The document scroll is locked while it is open, so the surface is exactly the viewport at any size.
|
|
58
|
+
- Panes for the detail slot: `ContinuePane`, `NewCharacterPane`, `SaveSlotsPane` (slot cards with a `meta.thumbnail`, playtime, progress, timestamp and a two-press delete) and `CreditsPane` (`@jgengine/react/gameMenuSaves`).
|
|
59
|
+
- `shrinkCapture` (`@jgengine/react/saveThumbnail`) turns a `captureCanvas` frame into a card-sized thumbnail data URL.
|
|
60
|
+
- `createSessionLoop` (`@jgengine/core/run/sessionLoop`) is one session loop for every game: a session opens, may carry a quantity, closes at its boundary, and the game's own `settle` callback decides what happens to what it carried. `carries: false` for a session with nothing at stake, and an optional `offers` table (with `present`/`choose`) for a game whose next session opens with an authored change; without a table nothing is drawn. `installSessionProbe` reports the live session on `window.__jgengineRun`.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- `switchSlot(slot, { flush: false })` (`@jgengine/core/game/gameSave`) loads a slot without first writing the live state over it. Opening a save from a menu uses it so Continue cannot overwrite the save it resumes.
|
|
65
|
+
- The dev runner's registry and environment modules import outside Vite, so a test or tool can read them.
|
|
66
|
+
- `createRunLoop` is now a thin composition of `createSessionLoop` and is deprecated: bank-or-lose and a per-run change table are one game type's rules, so they live in the composition instead of the base. Its API, state shape and tests are unchanged. One difference: the option draw is seeded under `:offer:` rather than `:run:` (`seededRng(\`${seed}:offer:${index}\`)`), so a career that keeps its seed draws a different change on a given session than 0.37.0 did for the same seed. Ids, payloads and saves stay valid: a saved `changeId` still restores the same change.
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- `shoot` fails instead of writing a PNG when the whole frame is one flat colour, and when a scene is still behind its loading cover. Both used to write a plausible screenshot that read as evidence of a rendered frame.
|
|
71
|
+
- The editor's play-mode pixel test waits for the warmup cover to clear before screenshotting. It shot the cover, so it read `0.9988` one-colour locally and passed in CI where the race fell the other way.
|
|
72
|
+
- `shoot` prints the WebGL2 renderer it draws with and fails the capture outright when the launched browser has none.
|
|
73
|
+
|
|
74
|
+
Root cause: capture readiness was trusted to mean "the scene has rendered", but a loading cover and a canvas that never committed both photograph as a flat frame, and nothing measured the frame after the fact. Headless Chrome on this PC was never the problem: it renders with the GPU (`ANGLE (NVIDIA GeForce RTX 3070, D3D11)`) and with SwiftShader when asked.
|
|
75
|
+
|
|
76
|
+
Before: `bun run shoot --url <flat canvas page>` exited 0 and wrote a one-colour PNG, and `packages/editor/src/CreatorApp.test.ts` failed locally at `dominantColorShare 0.9988`.
|
|
77
|
+
After: the same shoot exits 1 with `the whole frame is one flat color (100.0%) - no scene reached the capture - nothing was written`, and the editor test passes in 12s with sky and ground in the PNG.
|
|
78
|
+
|
|
79
|
+
Cost: forcing SwiftShader on the same game shot costs 2.4x the capture (16.0s vs 6.7s warm, beacon-bastion play at half size), so the launch keeps the native GPU. The two checks add about 10ms to a shot.
|
|
80
|
+
|
|
81
|
+
Verification:
|
|
82
|
+
- `bun test scripts/shot-metrics.test.ts` - pass
|
|
83
|
+
- `bun test packages/editor/src/CreatorApp.test.ts` - pass
|
|
84
|
+
- `bun run shoot beacon-bastion --mode play --size half` - exit 0, scene renders
|
|
85
|
+
- `bun run shoot --url <flat canvas page>` - exit 1, no PNG written
|
|
86
|
+
- `bun run verify:fast` - pass
|
|
87
|
+
|
|
88
|
+
The CI reliability fix this lane also carried (a browser setup hook shorter than Playwright's launch default) is #2335, so either change can be reverted without the other.
|
|
89
|
+
- The publish workflow's last step no longer reports a whole release as unpublished because it ran seconds after `npm publish`. `check:release-artifacts --wait <seconds>` re-checks while the only failures are absent packages (no metadata, version missing, tarball 404) and gives up when the budget runs out; a wrong version in the tarball, a missing subpath or a main entry that will not import still fails on the first check. The 0.37.0 publish run failed this step at 04:48 listing all 13 packages, then npm served every one of them. The step now passes `-- --wait 300`.
|
|
90
|
+
- A Playwright test file's `beforeAll` now outlives the Chrome launch it waits on, and `browser-test-budget` rejects a file that leaves the hook at or under Playwright's 30 s launch default. On `test (g)` in #2326 the hook's 30 s budget and that default expired together, so CI reported only `(unnamed) hook timed out` beside `killed 1 dangling process` and nothing named the launch.
|
|
91
|
+
- `packages/react/src/dialogBehavior.test.ts` gives its browser navigation its own 20 s deadline and one retry inside a 60 s hook.
|
|
92
|
+
|
|
93
|
+
Root cause: Bun prints a `beforeAll` timeout as an unnamed hook timeout, so a setup budget equal to Playwright's own 30 s launch default reports the anonymous timer instead of the launch that stalled.
|
|
94
|
+
|
|
95
|
+
Before: `test (g)` on #2326 reported `(fail) (unnamed) [30001.51ms] ^ a beforeEach/afterEach hook timed out` and `killed 1 dangling process`.
|
|
96
|
+
After: a stall now surfaces as Playwright's own `Timeout ... exceeded`, and `bun scripts/browser-test-budget.ts` names any file whose setup hook sits at or under the launch default.
|
|
97
|
+
|
|
98
|
+
Verification:
|
|
99
|
+
- `bun scripts/browser-test-budget.ts` - pass, and the new rule names the unraised files on the previous tip
|
|
100
|
+
- `bun test scripts/browser-test-budget.test.ts` - pass
|
|
101
|
+
- `bun test packages/react/src/dialogBehavior.test.ts packages/react/src/scrollRail.test.ts packages/shell/src/settingsBoundary.test.ts` - pass
|
|
102
|
+
- `bun run verify:fast` - pass
|
|
103
|
+
- A prop placed at runtime honours its model's `variants` levels. `StaticObjectModels` applied the level switch only inside its batched branch, so a prop placed with `scene.object.place` — Harbor Heat's palm line — drew its full asset at every camera distance: 15 palms at 9,134 triangles each, and pointing the far level at a dispatch booth changed nothing on screen. `IsolatedEntityModel` now mounts `SingleVariantModel`, which asks the same `createModelVariantSelector` and `lodBandIndex` authority the batched path uses, at the prop's own camera distance, and keeps only the active level mounted so both levels never draw at once. The switch carries the selector's hysteresis, warms every level at mount, measures only the near level, and reads the placement from the scene store through scratch vectors, so a frame allocates nothing. One palm past 60 m draws 4,110 triangles instead of 9,134 (5,024 saved, 45% of the full level).
|
|
104
|
+
|
|
105
|
+
- `singleVariantLevel` and `singleVariantModel` (`@jgengine/shell` internal, `render/singleVariantLod`) are the isolated path's level decision and the model it draws, so a fixture pins the same switch the component runs.
|
|
106
|
+
|
|
107
|
+
`bun test packages/shell/src/render/singleVariantLod.test.ts` — 6 pass. With the old render decision restored, the wiring test fails (`Expected: SingleVariantModel, Received: EntityModel`).
|
|
108
|
+
`bun --cwd=packages/shell run test` — 1,229 pass, 0 fail.
|
|
109
|
+
`bun --cwd=packages/shell run check-types` — clean.
|
|
110
|
+
|
|
29
111
|
## 0.37.0
|
|
30
112
|
|
|
31
113
|
### Migrate
|
|
@@ -45,6 +45,19 @@ export declare function IsolatedEntityModel({ model, instanceId, measure, fallba
|
|
|
45
45
|
measure?: MeasureTarget;
|
|
46
46
|
fallback?: ReactNode;
|
|
47
47
|
}): import("react").JSX.Element;
|
|
48
|
+
/**
|
|
49
|
+
* One prop drawn on its own, at its own camera distance: the same level switch the batched prop path
|
|
50
|
+
* runs, so a prop placed at runtime (`scene.object.place`) drops to its cheaper asset exactly like an
|
|
51
|
+
* authored one. Only the active level mounts, so both levels never draw at once, and the level changes
|
|
52
|
+
* only when the shared selector clears its hysteresis edge. Distance is measured from the placement the
|
|
53
|
+
* scene store holds, through memoized scratch vectors, so a frame allocates nothing.
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
export declare function SingleVariantModel({ model, instanceId, measure, }: {
|
|
57
|
+
model: ModelConfig;
|
|
58
|
+
instanceId: string;
|
|
59
|
+
measure?: MeasureTarget;
|
|
60
|
+
}): import("react").JSX.Element;
|
|
48
61
|
/** Where a measured model reports its rendered bounds: an entity kind or an object catalog id. */
|
|
49
62
|
export interface MeasureTarget {
|
|
50
63
|
target: "entity" | "object";
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useFrame, useLoader, useThree } from "@react-three/fiber";
|
|
3
|
-
import { Component, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef } from "react";
|
|
3
|
+
import { Component, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { parseModelHiddenNodes } from "@jgengine/core/editor/modelNodeVisibility";
|
|
6
6
|
import { createSpriteClipPlayer } from "@jgengine/core/render/sprite2d";
|
|
7
7
|
import { reportFallbackSeam } from "@jgengine/core/devtools/fallbackSeams";
|
|
8
|
+
import { createModelVariantSelector } from "@jgengine/core/world/modelLod";
|
|
8
9
|
import { useOptionalGameContext } from "@jgengine/react/provider";
|
|
9
10
|
import { detectKtx2Support, sharedGltfLoader } from "./modelLoad.js";
|
|
10
11
|
import { modelAssetRequests } from "./modelAssets.js";
|
|
@@ -16,6 +17,7 @@ import { useFootIk } from "./useFootIk.js";
|
|
|
16
17
|
import { resolveRigNode } from "./rigNode.js";
|
|
17
18
|
import { PartMotionRig } from "./PartMotion.js";
|
|
18
19
|
import { syncSpriteFrame } from "./spriteRender.js";
|
|
20
|
+
import { singleVariantLevel, variantLevelModel } from "./singleVariantLod.js";
|
|
19
21
|
import { applyMaterialOverride } from "../materialOverride.js";
|
|
20
22
|
import { applyPaintTextureToMaterials, cacheStandardMaterials, cloneModelScene, createPaintCanvas, disposeModelScene, ownModelMaterial, disposePaintCanvas, syncPaintCanvas, } from "./modelRender.js";
|
|
21
23
|
export function EntitySprite({ sprite }) {
|
|
@@ -90,7 +92,41 @@ export function IsolatedModelPart({ model, seam, children, }) {
|
|
|
90
92
|
return (_jsx(ModelFallbackBoundary, { fallback: null, url: model.url, seam: seam, children: _jsx(Suspense, { fallback: null, children: children }) }));
|
|
91
93
|
}
|
|
92
94
|
export function IsolatedEntityModel({ model, instanceId, measure, fallback, }) {
|
|
93
|
-
|
|
95
|
+
const levels = model.variants !== undefined && instanceId !== undefined;
|
|
96
|
+
return (_jsx(ModelFallbackBoundary, { fallback: fallback ?? null, url: model.url, seam: measure?.target, children: _jsx(Suspense, { fallback: null, children: levels ? (_jsx(SingleVariantModel, { model: model, instanceId: instanceId, measure: measure })) : (_jsx(EntityModel, { model: model, instanceId: instanceId, measure: measure })) }) }));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* One prop drawn on its own, at its own camera distance: the same level switch the batched prop path
|
|
100
|
+
* runs, so a prop placed at runtime (`scene.object.place`) drops to its cheaper asset exactly like an
|
|
101
|
+
* authored one. Only the active level mounts, so both levels never draw at once, and the level changes
|
|
102
|
+
* only when the shared selector clears its hysteresis edge. Distance is measured from the placement the
|
|
103
|
+
* scene store holds, through memoized scratch vectors, so a frame allocates nothing.
|
|
104
|
+
* @internal
|
|
105
|
+
*/
|
|
106
|
+
export function SingleVariantModel({ model, instanceId, measure, }) {
|
|
107
|
+
const ctx = useOptionalGameContext();
|
|
108
|
+
const variants = model.variants;
|
|
109
|
+
const selector = useMemo(() => (variants === undefined ? null : createModelVariantSelector(variants)), [variants]);
|
|
110
|
+
const [level, setLevel] = useState(0);
|
|
111
|
+
const cameraAt = useMemo(() => new THREE.Vector3(), []);
|
|
112
|
+
const probe = useMemo(() => new THREE.Vector3(), []);
|
|
113
|
+
// Warm every level at mount, so the switch swaps a cached clone instead of suspending on a load.
|
|
114
|
+
for (const candidate of variants?.levels ?? [])
|
|
115
|
+
useLoader.preload(sharedGltfLoader, candidate.url);
|
|
116
|
+
useFrame((state) => {
|
|
117
|
+
if (selector === null || ctx === null)
|
|
118
|
+
return;
|
|
119
|
+
const placement = ctx.scene.object.get(instanceId)?.position ?? ctx.scene.entity.get(instanceId)?.position;
|
|
120
|
+
if (placement === undefined)
|
|
121
|
+
return;
|
|
122
|
+
const next = singleVariantLevel(selector, instanceId, state.camera, placement, cameraAt, probe);
|
|
123
|
+
if (next !== level)
|
|
124
|
+
setLevel(next);
|
|
125
|
+
});
|
|
126
|
+
const drawn = useMemo(() => variantLevelModel(model, level), [model, level]);
|
|
127
|
+
// The near level owns the measured bounds and collision mesh, like the batched path: a decimated
|
|
128
|
+
// asset must not redefine what the catalog entry collides as.
|
|
129
|
+
return _jsx(EntityModel, { model: drawn, instanceId: instanceId, measure: level === 0 ? measure : undefined });
|
|
94
130
|
}
|
|
95
131
|
function BoneAttachment({ rig, model, slot, position, rotation, scale, }) {
|
|
96
132
|
const gltf = useLoader(sharedGltfLoader, model.url);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type * as THREE from "three";
|
|
2
|
+
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
import type { ModelVariantSelector } from "@jgengine/core/world/modelLod";
|
|
4
|
+
/**
|
|
5
|
+
* Shadow participation the model draws with at one level: the level's own choice when it made one,
|
|
6
|
+
* otherwise the model's `shadows` untouched.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export declare function variantLevelShadows(model: ModelConfig, level: number): ModelConfig["shadows"];
|
|
10
|
+
/**
|
|
11
|
+
* The model any path draws at one variant level: that level's asset, its shadow participation, and no
|
|
12
|
+
* nested variants, so a prop rendered on its own, a batched instance and a scatter cell all switch
|
|
13
|
+
* the same way. Out-of-range levels clamp to the nearest one, and a model without variants is
|
|
14
|
+
* returned untouched.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export declare function variantLevelModel(model: ModelConfig, level: number): ModelConfig;
|
|
18
|
+
/**
|
|
19
|
+
* Metres from the camera's world position to a placement, written through the caller's scratch vectors
|
|
20
|
+
* so a frame allocates nothing.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export declare function placementDistance(camera: THREE.Camera, position: readonly number[], cameraAt: THREE.Vector3, probe: THREE.Vector3): number;
|
|
24
|
+
/**
|
|
25
|
+
* The level one isolated prop draws this frame. The answer comes from the same
|
|
26
|
+
* {@link ModelVariantSelector} the batched prop path asks, at the prop's own camera distance, so the
|
|
27
|
+
* hysteresis and the `lodBandIndex` boundary are identical for a prop placed at runtime and one
|
|
28
|
+
* authored in the scene.
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
export declare function singleVariantLevel(selector: ModelVariantSelector, id: string, camera: THREE.Camera, position: readonly number[], cameraAt: THREE.Vector3, probe: THREE.Vector3): number;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function levelOf(model, level) {
|
|
2
|
+
const levels = model.variants?.levels;
|
|
3
|
+
if (levels === undefined)
|
|
4
|
+
return undefined;
|
|
5
|
+
return levels[Math.min(levels.length - 1, Math.max(0, level))];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Shadow participation the model draws with at one level: the level's own choice when it made one,
|
|
9
|
+
* otherwise the model's `shadows` untouched.
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export function variantLevelShadows(model, level) {
|
|
13
|
+
const chosen = levelOf(model, level)?.shadows;
|
|
14
|
+
return chosen === undefined || chosen === "inherit" ? model.shadows : chosen;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The model any path draws at one variant level: that level's asset, its shadow participation, and no
|
|
18
|
+
* nested variants, so a prop rendered on its own, a batched instance and a scatter cell all switch
|
|
19
|
+
* the same way. Out-of-range levels clamp to the nearest one, and a model without variants is
|
|
20
|
+
* returned untouched.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export function variantLevelModel(model, level) {
|
|
24
|
+
const chosen = levelOf(model, level);
|
|
25
|
+
if (chosen === undefined)
|
|
26
|
+
return model;
|
|
27
|
+
const shadows = variantLevelShadows(model, level);
|
|
28
|
+
return {
|
|
29
|
+
...model,
|
|
30
|
+
url: chosen.url,
|
|
31
|
+
variants: undefined,
|
|
32
|
+
...(shadows === undefined ? {} : { shadows }),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Metres from the camera's world position to a placement, written through the caller's scratch vectors
|
|
37
|
+
* so a frame allocates nothing.
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export function placementDistance(camera, position, cameraAt, probe) {
|
|
41
|
+
camera.getWorldPosition(cameraAt);
|
|
42
|
+
probe.set(position[0] ?? 0, position[1] ?? 0, position[2] ?? 0);
|
|
43
|
+
return cameraAt.distanceTo(probe);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The level one isolated prop draws this frame. The answer comes from the same
|
|
47
|
+
* {@link ModelVariantSelector} the batched prop path asks, at the prop's own camera distance, so the
|
|
48
|
+
* hysteresis and the `lodBandIndex` boundary are identical for a prop placed at runtime and one
|
|
49
|
+
* authored in the scene.
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
export function singleVariantLevel(selector, id, camera, position, cameraAt, probe) {
|
|
53
|
+
return selector.level(id, placementDistance(camera, position, cameraAt, probe));
|
|
54
|
+
}
|
|
@@ -8,6 +8,7 @@ import { chunkScatterInstances } from "@jgengine/core/world/scatterRegion";
|
|
|
8
8
|
import { beginFallbackSeam, reportFallbackSeam } from "@jgengine/core/devtools/fallbackSeams";
|
|
9
9
|
import { createModelVariantSelector } from "@jgengine/core/world/modelLod";
|
|
10
10
|
import { CAMERA_POST_FRAME_PRIORITY } from "../camera/cameraRigs.js";
|
|
11
|
+
import { variantLevelModel } from "../render/singleVariantLod.js";
|
|
11
12
|
import { buildScatterProxy } from "./scatterProxies.js";
|
|
12
13
|
import { buildScatterModelSources, disposeScatterModelSources } from "./scatterModels.js";
|
|
13
14
|
import { writeScatterVariantLevel } from "./scatterVariants.js";
|
|
@@ -120,9 +121,8 @@ function ScatterVariantLevel({ model, url, level, selector, batches }) {
|
|
|
120
121
|
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
121
122
|
});
|
|
122
123
|
const { sources, root } = useMemo(() => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}, [gltf, model, url]);
|
|
124
|
+
return buildScatterModelSources(gltf.scene, variantLevelModel(model, level));
|
|
125
|
+
}, [gltf, model, level]);
|
|
126
126
|
useEffect(() => () => disposeScatterModelSources(root), [root]);
|
|
127
127
|
return (_jsx(_Fragment, { children: sources.map((source, sourceIndex) => batches.map((batch) => (_jsx(ScatterVariantSourceInstances, { source: source, batch: batch, level: level, selector: selector }, `${batch.key}:${sourceIndex}`)))) }));
|
|
128
128
|
}
|
|
@@ -7,6 +7,7 @@ import { devtools } from "@jgengine/core/devtools/devtools";
|
|
|
7
7
|
import { useGameContext } from "@jgengine/react/provider";
|
|
8
8
|
import { sharedGltfLoader } from "../render/modelLoad.js";
|
|
9
9
|
import { IsolatedModelPart } from "../render/SceneModels.js";
|
|
10
|
+
import { variantLevelModel } from "../render/singleVariantLod.js";
|
|
10
11
|
import { freezeStaticTransforms } from "../render/staticTransforms.js";
|
|
11
12
|
import { measureLocalBounds, reportMeasuredBounds } from "../render/measureBounds.js";
|
|
12
13
|
import { measureLocalCollisionTriangles, reportMeasuredCollisionMesh } from "../render/measureCollisionMesh.js";
|
|
@@ -174,7 +175,7 @@ export function VariantStaticObjects({ batch, single }) {
|
|
|
174
175
|
const { objects, excluded } = useBatchMembers(batch);
|
|
175
176
|
const levels = batch.model.variants.levels;
|
|
176
177
|
const selector = useMemo(() => createModelVariantSelector(batch.model.variants), [batch.model]);
|
|
177
|
-
return _jsxs(_Fragment, { children: [levels.map((level, index) => (_jsx(VariantLevel, { url: level.url, model:
|
|
178
|
+
return _jsxs(_Fragment, { children: [levels.map((level, index) => (_jsx(VariantLevel, { url: level.url, model: variantLevelModel(batch.model, index), batch: batch, objects: objects, level: index, selector: selector, measure: index === 0, single: single }, `${level.url}:${index}`))), excluded.map((id) => { const live = ctx.scene.object.get(id); return live === null ? null : single(live); })] });
|
|
178
179
|
}
|
|
179
180
|
/** Render compatible authored or `scenery` props as shared model draws, preserving unsupported models. @internal */
|
|
180
181
|
export function StaticObjectModels({ batch, single }) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.1",
|
|
4
4
|
"description": "Game player shell for JGengine: React Three Fiber canvas, orbit camera, input tracking, HUD mounting, GameUiPreview, and a demo game. Consumers supply a GameRegistry.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"test": "bun test src"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@jgengine/core": "^0.
|
|
43
|
-
"@jgengine/react": "^0.
|
|
44
|
-
"@jgengine/ws": "^0.
|
|
42
|
+
"@jgengine/core": "^0.38.1",
|
|
43
|
+
"@jgengine/react": "^0.38.1",
|
|
44
|
+
"@jgengine/ws": "^0.38.1"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@react-three/drei": "^10.0.0",
|
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.38.1" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
10
10
|
|
|
11
11
|
## @jgengine/core/authoring
|
|
12
12
|
|
|
@@ -285,7 +285,7 @@
|
|
|
285
285
|
|
|
286
286
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
287
287
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
288
|
-
- `VERSION` (const): const VERSION: "0.
|
|
288
|
+
- `VERSION` (const): const VERSION: "0.38.1" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
289
289
|
|
|
290
290
|
## @jgengine/core/runtime/adapter
|
|
291
291
|
|
|
@@ -29,7 +29,7 @@ Existing React games keep their entity store and use the focused
|
|
|
29
29
|
|
|
30
30
|
- **Every game owns its UI end-to-end.** Custom composition, skin, placement, terminology, motion, and one main menu. The website/runner is a bare loader. Composing and reskinning the shipped blocks is correct use; a genre HUD kit, theme preset, or unarranged default stack dropped in as the game's identity is not.
|
|
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
|
-
- **Menus come from the shipped blocks, navigated by `useMenuNav`.** `TitleMenu`/`MenuScreen` (`@jgengine/react`)
|
|
32
|
+
- **Menus come from the shipped blocks, navigated by `useMenuNav`.** `GameMenu` (`@jgengine/react/gameMenu`) is the front-end surface: full-bleed over a live scene or key art, a logo sized by `--jg-menu-logo-size`, animated primary verbs with a generated selection tick, and a detail pane that follows the highlight — with `ContinuePane`, `NewCharacterPane`, `SaveSlotsPane` (cards with thumbnails), `CreditsPane` and the game's own settings surface as its panes, and `GamePauseMenu` the same system sized down. It locks the document scroll, so the surface is exactly the viewport at any size. `TitleMenu`/`MenuScreen` (`@jgengine/react`) remain for a plainer animated screen over the live scene (transform/opacity only, reduced-motion aware, unmounted after exit), `PauseMenu` the small pause shape, `SettingsSection`/`SettingsTabs` the settings surface, `SaveSlotMenu` a bare slot list, `CreditsPanel` the credits screen, `LoadingScreen` the boot screen with rotating tips. `useMenuNav` (`@jgengine/react/menuNav`) is the one keyboard + gamepad focus model for any menu: roving focus, confirm/back, disabled rows skipped, no focus trap. Games keep wording, layout, art and terminology.
|
|
33
33
|
- **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.
|
|
34
34
|
- **Route the front-end with `useMenuRouter`, and generate the credits.** `useMenuRouter(root)` is the screen stack behind title → save select → settings → credits: `open`/`back`/`replace`/`reset`, `path` for depth, and Escape-to-back bound by default (`escapeToBack: false` when the game owns Escape). `menuStackReducer` is the same transition as a pure function. For credits, `creditsForSourceIds(ids)` (`@jgengine/assets/credits`) turns the pack ids a game declares to `buildCatalog` into licensed, attributed sections, `mergeCredits` folds them into hand-authored lines, and `CreditsScreen` renders the result HudTheme-skinned.
|
|
35
35
|
- **Skin the shared chrome with one `HudTheme` token object.** `hudThemeVars(theme)` emits the whole `--jg-*` vocabulary — bar fills and troughs, `HudFrame variation="themed"`, painted slots, the minimap ring, plus surfaces, edges, text, status colors, fonts, and rarity — so one theme spread on any HUD ancestor reskins the `@jgengine/react` primitives and the `registry/jgengine` blocks together. Author your own with `deriveHudTheme({ accent, surface })` or `hudTheme({ ... })`; `HUD_THEME_PRESETS` / `resolveHudTheme("<preset>")` are demo/scaffold starting points, never a game's identity. A component that needs a token no theme emits fails `check-hud-tokens` — add it to `HudTheme` rather than inventing a second vocabulary. Caller `style`/`renderItem` overrides still win.
|
|
@@ -717,8 +717,12 @@
|
|
|
717
717
|
- `CompassProps` (interface): interface CompassProps { facingYaw: number; center?: WorldXZ; markers?: MarkerCollection; width?: number; fov?: number; kindStyles?: Record<string, MarkerKindStyle>; className?: string } — ⚠ undocumented · used by `Compass`: Horizontal compass strip centered on the player's facing direction, with the eight cardinals and optional marker pips from static views, an …
|
|
718
718
|
- `ConfirmDialog` (function): function ConfirmDialog({ title, body, confirmLabel = "Confirm", cancelLabel = "Cancel", onConfirm, onCancel, danger = false, width = 360, className, style, }: ConfirmDialogProps): ReactNode — A generic two-button confirmation dialog — title, optional body, and Cancel / Confirm buttons over a `HudTheme`-token-driven {@link HudSurface}. Presentation-only: wire `onConfirm`/`onCancel` to a modal stack's `resolve` (typically inside a {@link ModalHost} render callback) and reskin via `HudTheme` tokens or `danger` for a destructive action. It interprets nothing — the game supplies the copy.
|
|
719
719
|
- `ConfirmDialogProps` (interface): interface ConfirmDialogProps — Props for {@link ConfirmDialog}.
|
|
720
|
+
- `ContinuePane` (function): function ContinuePane({ slot, onContinue, emptyMessage = "No run saved yet. Start a new game to begin one.", now = Date.now, className, style, }: ContinuePaneProps): ReactNode — The Continue detail pane: the save being continued, read big — captured thumbnail, character, time played, the game's progress summary, when it was saved, and the resume action.
|
|
721
|
+
- `ContinuePaneProps` (interface): interface ContinuePaneProps — Props for {@link ContinuePane}.
|
|
720
722
|
- `ControlHint` (interface): interface ControlHint — One row of a control legend. Name the game action(s) whose bound key(s) to show (`action`) so the glyphs come straight from the keybind map — never re-typed — or give literal `keys` for controls that live outside the map (`"Mouse"`, `"LMB"`). `label` says what the control does.
|
|
721
723
|
- `ControlsList` (function): function ControlsList({ bindings, controls, hideOnCoarsePointer = true, separator = " / ", className, rowClassName, keysClassName, keyClassName, separatorClassName, labelClassName, renderKey, renderRow, }: { bindings?: ActionCodesMap; controls: readonly ControlHint[]; hideOnCoarsePointer?: boolean; … — Renders a controls legend whose key glyphs come from the game's keybind map, so bindings live in one place and never drift from a hand-typed table. Hides itself on coarse-pointer devices (a touchscreen has no keyboard) via the same `data-jg-kbd-hint` marker as `KeyHint`. Headless: every part carries a `className` slot and `data-*` hook; pass `renderKey`/`renderRow` to fully own the markup.
|
|
724
|
+
- `CreditsPane` (function): function CreditsPane({ sections, heading = "Credits", notice, className, style }: CreditsPaneProps): ReactNode — The Credits detail pane: a scroller that belongs to the pane. Wheel, arrows, PageUp/PageDown, Home and End scroll it, the native scrollbar is hidden, and the page behind never moves.
|
|
725
|
+
- `CreditsPaneProps` (interface): interface CreditsPaneProps — Props for {@link CreditsPane}.
|
|
722
726
|
- `CreditsPanel` (function): function CreditsPanel({ document, onBack, title = "Credits", backLabel = "Back", open = true, backdrop, className, style, contentClassName, contentStyle, }: CreditsPanelProps): ReactNode — A reachable credits screen: the generated or hand-authored {@link CreditsDocument} in a scrollable panel over the scene, with one back affordance that keyboard and gamepad both reach. The game owns the backdrop and the wording; the block keeps the lines HudTheme-skinned and the exit working.
|
|
723
727
|
- `CreditsPanelProps` (interface): interface CreditsPanelProps — Props for {@link CreditsPanel}.
|
|
724
728
|
- `Crosshair` (function): function Crosshair({ size = 18, gap = 5, thickness = 2, color = "rgba(255,255,255,0.85)", style, className, }: { size?: number; gap?: number; thickness?: number; color?: string; style?: CSSProperties; className?: string; }): React.JSX.Element — A minimal center crosshair reticle — four ticks around a gap. Purely presentational.
|
|
@@ -781,6 +785,18 @@
|
|
|
781
785
|
- `FullscreenMapProps` (interface): interface FullscreenMapProps extends Omit<WorldMapSurfaceProps, "canvasWidth" | "canvasHeight" | "viewport" | "width" | "height" | "style"> — Props for {@link FullscreenMap}.
|
|
782
786
|
- `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.
|
|
783
787
|
- `GameIdentityProvider` (function): function GameIdentityProvider({ source, children, }: { source: IdentitySource; children?: ReactNode; }): React.JSX.Element — ⚠ undocumented
|
|
788
|
+
- `GameMenu` (function): function GameMenu({ items, initialId, onSelect, onBack, onHighlight, detail, logo, eyebrow, tagline, background, art, secondary, footer, controlHint, sound = true, variant = "title", open = true, reducedMotion, label, className, style, detailClassName, detailStyle, }: GameMenuProps): ReactNode — A game front-end menu: full-bleed over a living background, with a logo, an animated primary list and a detail pane that follows the highlighted verb. One system covers the main menu and the pause menu (`variant="pause"`), keyboard and gamepad navigation come from `useMenuNav`, confirmation ticks are generated (no audio asset), and the document never scrolls while it is open.
|
|
789
|
+
- `GameMenuAvatar` (interface): interface GameMenuAvatar — One avatar/portrait choice in {@link NewCharacterPane}.
|
|
790
|
+
- `GameMenuItem` (interface): interface GameMenuItem — A game's front-end menu: a full-bleed surface over a living background, a logo, primary verbs on the left that animate as the selection moves, and a detail pane on the right that changes with the highlighted verb. It is the block a main menu, a pause menu and their sub-screens are built from, so a game composes one menu system instead of a stack of modal panels.
|
|
791
|
+
- `GameMenuLogo` (interface): interface GameMenuLogo — The logo slot. Give it `image` for art, or `text` for a styled wordmark.
|
|
792
|
+
- `GameMenuPalette` (interface): interface GameMenuPalette — One palette choice in {@link NewCharacterPane}.
|
|
793
|
+
- `GameMenuProps` (interface): interface GameMenuProps — Props for {@link GameMenu}.
|
|
794
|
+
- `GameMenuSound` (interface): interface GameMenuSound — A menu sound player. Give it to `GameMenu`'s `sound` prop, or call its methods from your own menu.
|
|
795
|
+
- `GameMenuSoundConfig` (interface): interface GameMenuSoundConfig — How {@link createGameMenuSound} is tuned.
|
|
796
|
+
- `GameMenuSoundKind` (type): type GameMenuSoundKind = "move" | "confirm" | "back" — The short, quiet click a menu makes when the selection moves or is confirmed. Generated with an oscillator instead of shipped as an audio file, so a game gets menu feedback without authoring a sound and the whole thing unmounts with the screen.
|
|
797
|
+
- `GameMenuSoundState` (interface): interface GameMenuSoundState — The sound player's readable state: on/off, gain, and whether audio is actually running.
|
|
798
|
+
- `GameMenuVariant` (type): type GameMenuVariant = "title" | "pause" — How a {@link GameMenu} is used. `pause` is the same system at a smaller scale.
|
|
799
|
+
- `GamePauseMenu` (function): function GamePauseMenu(props: Omit<GameMenuProps, "variant"> & { onResume?: () => void }): ReactNode — A pause menu built from {@link GameMenu}: the same surface and navigation at a smaller scale, so a game composes one menu system rather than a second one. Escape (or B/Start) resumes.
|
|
784
800
|
- `GamePreviewComponent` (type): type GamePreviewComponent = ComponentType<GamePreviewProps> — ⚠ undocumented
|
|
785
801
|
- `GamePreviewProps` (type): type GamePreviewProps = { className?: string; } — ⚠ undocumented
|
|
786
802
|
- `GamePreviewStates` (type): type GamePreviewStates = Record<string, GamePreviewComponent> — ⚠ undocumented
|
|
@@ -889,6 +905,9 @@
|
|
|
889
905
|
- `ModalHost` (function): function ModalHost({ stack, children, showBackdrop = true, backdrop, closeOnBackdrop = true, closeOnEsc = true, zIndexBase = 80, onInteract, className, style, }: ModalHostProps): ReactNode — Full-screen modal host: watches a stack, renders only the topmost modal centered over a dimmed backdrop, and manages accessibility — the panel is a `role="dialog" aria-modal`, focus moves into it on open and is trapped with Tab/Shift+Tab cycling, Escape and (optionally) a backdrop click cancel the top modal, and focus returns to the previously focused element on close. It renders nothing when the stack is empty. Skin the panel and backdrop via `HudTheme` tokens.
|
|
890
906
|
- `ModalHostProps` (interface): interface ModalHostProps — Props for {@link ModalHost}.
|
|
891
907
|
- `ModalStackView` (interface): interface ModalStackView — React chrome over the headless modal stack (`@jgengine/core/ui/modalStack`) — the blocking-overlay layer a game raises over its scene: a pause menu, a "quit to menu?" confirm, a reward popup. {@link useModalStack} is the DATA/HOOK layer (subscribe + the active record), {@link ModalHost} renders the top modal with a backdrop, focus trap, and Esc-to-cancel, and {@link ConfirmDialog} / {@link PauseMenu} are ready-to-wire, HudTheme-token-driven building blocks. All unskinned and game-owned: the model never interprets a modal's `kind`, so a game switches on it to pick which of these (or its own) content to show, and reskins everything via `HudTheme` tokens and the frame.
|
|
908
|
+
- `NewCharacterChoice` (interface): interface NewCharacterChoice — What {@link NewCharacterPane} reports when a run starts.
|
|
909
|
+
- `NewCharacterPane` (function): function NewCharacterPane({ name, onNameChange, palettes, paletteId, onPaletteChange, avatars, avatarId, onAvatarChange, onStart, heading = "New run", nameLabel = "Character name", namePlaceholder = "Name your character", maxLength = 24, startLabel = "Begin", className, style, }: NewCharacterPanePro… — The New Game detail pane: a character name field, an optional palette and avatar choice, and the action that starts the run. The name field is a real form field, so menu key handling leaves typing alone (Enter inside the field starts the run).
|
|
910
|
+
- `NewCharacterPaneProps` (interface): interface NewCharacterPaneProps — Props for {@link NewCharacterPane}.
|
|
892
911
|
- `NotificationBell` (function): function NotificationBell({ store, onClick, icon = "🔔", className, style }: NotificationBellProps): ReactNode — Notification bell — an icon button with an unread-count badge, bound to a notification store. Toggle a {@link NotificationCenter} panel from its click.
|
|
893
912
|
- `NotificationBellProps` (interface): interface NotificationBellProps — Props for {@link NotificationBell}.
|
|
894
913
|
- `NotificationCenter` (function): function NotificationCenter({ store, title = "Notifications", emptyLabel = "You're all caught up.", onClose, formatTime, className, style, }: NotificationCenterProps): ReactNode — Notification center panel — the scrollable log toasts don't provide: newest notifications with a kind-colored marker, title/body, relative time, and an unread highlight; a header with the unread count, mark-all-read, and clear. Clicking an entry marks it read. Bind it to a `createNotificationCenter`.
|
|
@@ -935,11 +954,17 @@
|
|
|
935
954
|
- `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.
|
|
936
955
|
- `RequireSession` (function): function RequireSession({ fallback, loading, children, }: { fallback?: ReactNode; loading?: ReactNode; children?: ReactNode; }): React.JSX.Element — ⚠ undocumented
|
|
937
956
|
- `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.
|
|
957
|
+
- `SAVE_SLOT_CHARACTER_KEY` (const): const SAVE_SLOT_CHARACTER_KEY: "character" — The character name a save card shows first, when the game supplies one as its own meta key.
|
|
958
|
+
- `SAVE_SLOT_THUMBNAIL_KEY` (const): const SAVE_SLOT_THUMBNAIL_KEY: "thumbnail" — The save slot `meta` key a card reads as its thumbnail: a data URL or image URL captured when the save was written. Every other meta key is shown as a progress chip and never interpreted.
|
|
938
959
|
- `SETTINGS_TRIGGER_CLASSNAME` (const): const SETTINGS_TRIGGER_CLASSNAME: "pointer-events-auto flex h-8 w-8 items-center justify-center rounded-md border border-[var(--jg-edge-bright)] bg-[var(--jg-surface)]/80 text-base text-[var(--jg-text-dim)] shadow-[0_1px_2px_rgba(0,0,0,0.6)] transition hover:bg-[var(--jg-surface-deep)] hover:text-[v… — Default HudTheme-token skin for {@link SettingsTrigger} — the 8×8 chrome button games re-authored seven times. Pass your own `className` only to override placement or brand chrome; the gear glyph already scales with `text-base` / `1em`.
|
|
939
960
|
- `SUITS` (const): const SUITS: readonly Suit[] — Suits in canonical order (clubs, diamonds, hearts, spades).
|
|
961
|
+
- `SaveSlotCard` (function): function SaveSlotCard({ slot, now = Date.now, recent = false, onSelect, onDelete, loadLabel, className, style, }: SaveSlotCardProps): ReactNode — One save slot as a card: a captured thumbnail, the character name, playtime, the game's progress summary and when it was saved — or, for an empty slot, an empty slot with a New Game affordance. Deleting takes two presses: the first swaps the button for a confirm pair, so a mis-click cannot erase a run.
|
|
962
|
+
- `SaveSlotCardProps` (interface): interface SaveSlotCardProps — Props for {@link SaveSlotCard}.
|
|
940
963
|
- `SaveSlotMenu` (function): function SaveSlotMenu({ model, onLoad, onNew, onDelete, onContinue, title = "Select Save", theme, now = Date.now, metaIcons, className, style, }: SaveSlotMenuProps): ReactNode — A drop-in save-select / profile menu that renders a {@link SaveSlots} index as a column of cards. A filled slot shows its name, the free-string `meta` the game supplied as chips, and a relative "saved" time, with Load and Delete actions; an empty slot shows a "New Game" affordance. A top-level Continue button targets the newest save (`model.mostRecent()`). This is a starting-point game-front-end building block — the game owns final art, wording, and layout; reskin it with {@link SaveSlotMenuTheme} or HudTheme CSS vars. Presentation only: all state lives in the core model, and `meta` keys are never interpreted.
|
|
941
964
|
- `SaveSlotMenuProps` (interface): interface SaveSlotMenuProps — Props for {@link SaveSlotMenu}.
|
|
942
965
|
- `SaveSlotMenuTheme` (interface): interface SaveSlotMenuTheme — Reskin tokens for {@link SaveSlotMenu}. All optional — each falls back to a HudTheme var then a neutral default.
|
|
966
|
+
- `SaveSlotsPane` (function): function SaveSlotsPane({ model, onContinue, onLoad, onNew, onDelete, heading = "Save slots", now = Date.now, columns = 2, className, style, }: SaveSlotsPaneProps): ReactNode — The slot grid for a {@link GameMenu} detail pane: Continue over the most recent save, then one card per slot. Arrow keys move between cards, Enter loads, and the grid scrolls inside its own pane rather than the page.
|
|
967
|
+
- `SaveSlotsPaneProps` (interface): interface SaveSlotsPaneProps — Props for {@link SaveSlotsPane}.
|
|
943
968
|
- `SaveStoreView` (interface): interface SaveStoreView<T> — The reactive slice of a {@link SaveStore} returned by {@link useSaveStore} — the live value plus bound save actions, all re-rendering on change.
|
|
944
969
|
- `Scoreboard` (function): function Scoreboard({ entries, rows, options, title, scoreLabel = "Score", nameFor, formatScore, medalIcon, theme, className, style, }: ScoreboardProps): ReactNode — A reskinnable ranked-score table for the {@link rankLeaderboard} selector: a rank column (medal icon/color for the top three via {@link medalFor}, otherwise the number), a name column, and a score column. The local player's row (`isLocal`) is washed and accented so "you" stands out. Feed it already-ranked `entries`, or raw `rows` plus `options` and it ranks them for you. All colors and spacing come from {@link ScoreboardTheme} / HudTheme `--jg-*` tokens — the component reads the model and never styles by game meaning.
|
|
945
970
|
- `ScoreboardProps` (interface): interface ScoreboardProps — Props for {@link Scoreboard}. Pass either `entries` (already ranked) or `rows` + `options`.
|
|
@@ -1039,6 +1064,7 @@
|
|
|
1039
1064
|
- `canvasCover` (const): const canvasCover: CanvasCover — The page's canvas cover. A full-screen menu (or a game's own opaque overlay) holds it; the shell stops drawing the world, its shadow passes and postfx while it is held, and draws again from the untouched scene when it lifts. A translucent scrim or a side panel must not hold it — anything the scene shows through has to keep drawing.
|
|
1040
1065
|
- `chatTransportFromSync` (function): function chatTransportFromSync(sync: ChatSync): ChatTransport — Lifts a callback-style ChatSync (e.g. createWsBackend().chatSyncFor(serverId)) into the hook-shaped ChatTransport contract. Create once per sync — outside render or inside useMemo — so subscriptions survive re-renders.
|
|
1041
1066
|
- `clerkIdentity` (function): function clerkIdentity(state: ClerkUserState, options?: { signOut?: () => void }): IdentitySource — ⚠ undocumented
|
|
1067
|
+
- `createGameMenuSound` (function): function createGameMenuSound(config: GameMenuSoundConfig = {}): GameMenuSound — A tiny WebAudio click generator for menu feedback: one oscillator burst per tick, created on the first play (so a browser autoplay policy does not log an error at mount) and reused after.
|
|
1042
1068
|
- `createHeldKeyTracker` (function): function createHeldKeyTracker(target: HeldKeyEventTarget): { isDown: (code: string) => boolean; dispose: () => void; } — ⚠ undocumented
|
|
1043
1069
|
- `defaultEquipLayout` (function): function defaultEquipLayout(items: Partial<Record<DefaultEquipSlotId, SlotItem | null>> = {}): EquipSlot[] — A sensible default paperdoll arrangement — worn armor down the left flank, held gear and jewelry down the right — that the game fills by passing worn items keyed by slot id. Every id is optional; unfilled slots render empty. Games that need a different slot set pass their own {@link EquipSlotLayout}.
|
|
1044
1070
|
- `defaultHudTheme` (const): const defaultHudTheme: HudTheme — The engine's default HUD look, applied by the shell at the HUD root of every game that does not opt out: a strong system UI font stack, layered translucent panels with a lit top rim and crisp hairline borders, a 12px type floor, and motion that honours reduced-motion. Games override any part with their own theme on a HUD ancestor.
|
|
@@ -1172,6 +1198,7 @@
|
|
|
1172
1198
|
- `useTicker` (function): function useTicker(hz = 10): number — Re-render at a steady rate. Returns a monotonically increasing tick count driven by a setInterval, for HUD elements that display wall-clock-derived values (cooldowns, cast bars, swing timers) without an engine subscription to hang off. `hz <= 0` disables the ticker.
|
|
1173
1199
|
- `useTimerRead` (function): function useTimerRead(timer: TimerSet, id: string, active = true): TimerRead | null — Subscribe to a single timer and re-read it every animation frame while mounted, so a HUD readout stays live without the game hand-rolling interval math. Reuses one read object (allocation-aware). Returns `null` for an unknown id. Pass `active={false}` to freeze the per-frame tick (e.g. an off-screen HUD).
|
|
1174
1200
|
- `useViewportMetrics` (function): function useViewportMetrics(): ViewportMetrics — Live visible viewport, tracking `window.visualViewport` (mobile browser chrome, pinch-zoom) with a layout-viewport fallback.
|
|
1201
|
+
- `useViewportScrollLock` (function): function useViewportScrollLock(active: boolean): void — Lock the page scroll while `active`, so a fixed full-bleed surface is exactly the viewport and the document behind it never gains a scrollbar. Nested and repeated locks are counted, and the previous inline styles are restored when the last one releases.
|
|
1175
1202
|
- `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.
|
|
1176
1203
|
- `useWaveRunner` (function): function useWaveRunner(runner: WaveRunner): WaveView — Subscribe to a {@link WaveRunner} and re-render whenever it notifies (wave, budget, spawns, alert change). Returns the runner's pooled {@link WaveView}. The runner is driven by the game/demo (call `runner.update(dt)`), not by this hook.
|
|
1177
1204
|
- `useWaypoints` (function): function useWaypoints(source: WaypointTracker | readonly Waypoint[]): readonly Waypoint[] — Subscribe to a {@link WaypointTracker} and re-render on every change, returning its current waypoints. Pass a plain array instead and it is returned as-is (no subscription), so a game can drive the host from a tracker or from its own derived state.
|
|
@@ -1450,6 +1477,42 @@
|
|
|
1450
1477
|
- `iconForItemId` (function): function iconForItemId(itemId: string): GameIconName | null — ⚠ undocumented
|
|
1451
1478
|
- `isGameIconName` (function): function isGameIconName(value: string): value is GameIconName — ⚠ undocumented
|
|
1452
1479
|
|
|
1480
|
+
## @jgengine/react/gameMenu
|
|
1481
|
+
|
|
1482
|
+
- `GameMenu` (function): function GameMenu({ items, initialId, onSelect, onBack, onHighlight, detail, logo, eyebrow, tagline, background, art, secondary, footer, controlHint, sound = true, variant = "title", open = true, reducedMotion, label, className, style, detailClassName, detailStyle, }: GameMenuProps): ReactNode — A game front-end menu: full-bleed over a living background, with a logo, an animated primary list and a detail pane that follows the highlighted verb. One system covers the main menu and the pause menu (`variant="pause"`), keyboard and gamepad navigation come from `useMenuNav`, confirmation ticks are generated (no audio asset), and the document never scrolls while it is open.
|
|
1483
|
+
- `GameMenuItem` (interface): interface GameMenuItem — A game's front-end menu: a full-bleed surface over a living background, a logo, primary verbs on the left that animate as the selection moves, and a detail pane on the right that changes with the highlighted verb. It is the block a main menu, a pause menu and their sub-screens are built from, so a game composes one menu system instead of a stack of modal panels.
|
|
1484
|
+
- `GameMenuLogo` (interface): interface GameMenuLogo — The logo slot. Give it `image` for art, or `text` for a styled wordmark.
|
|
1485
|
+
- `GameMenuProps` (interface): interface GameMenuProps — Props for {@link GameMenu}.
|
|
1486
|
+
- `GameMenuVariant` (type): type GameMenuVariant = "title" | "pause" — How a {@link GameMenu} is used. `pause` is the same system at a smaller scale.
|
|
1487
|
+
- `GamePauseMenu` (function): function GamePauseMenu(props: Omit<GameMenuProps, "variant"> & { onResume?: () => void }): ReactNode — A pause menu built from {@link GameMenu}: the same surface and navigation at a smaller scale, so a game composes one menu system rather than a second one. Escape (or B/Start) resumes.
|
|
1488
|
+
- `useViewportScrollLock` (function): function useViewportScrollLock(active: boolean): void — Lock the page scroll while `active`, so a fixed full-bleed surface is exactly the viewport and the document behind it never gains a scrollbar. Nested and repeated locks are counted, and the previous inline styles are restored when the last one releases.
|
|
1489
|
+
|
|
1490
|
+
## @jgengine/react/gameMenuSaves
|
|
1491
|
+
|
|
1492
|
+
- `ContinuePane` (function): function ContinuePane({ slot, onContinue, emptyMessage = "No run saved yet. Start a new game to begin one.", now = Date.now, className, style, }: ContinuePaneProps): ReactNode — The Continue detail pane: the save being continued, read big — captured thumbnail, character, time played, the game's progress summary, when it was saved, and the resume action.
|
|
1493
|
+
- `ContinuePaneProps` (interface): interface ContinuePaneProps — Props for {@link ContinuePane}.
|
|
1494
|
+
- `CreditsPane` (function): function CreditsPane({ sections, heading = "Credits", notice, className, style }: CreditsPaneProps): ReactNode — The Credits detail pane: a scroller that belongs to the pane. Wheel, arrows, PageUp/PageDown, Home and End scroll it, the native scrollbar is hidden, and the page behind never moves.
|
|
1495
|
+
- `CreditsPaneProps` (interface): interface CreditsPaneProps — Props for {@link CreditsPane}.
|
|
1496
|
+
- `GameMenuAvatar` (interface): interface GameMenuAvatar — One avatar/portrait choice in {@link NewCharacterPane}.
|
|
1497
|
+
- `GameMenuPalette` (interface): interface GameMenuPalette — One palette choice in {@link NewCharacterPane}.
|
|
1498
|
+
- `NewCharacterChoice` (interface): interface NewCharacterChoice — What {@link NewCharacterPane} reports when a run starts.
|
|
1499
|
+
- `NewCharacterPane` (function): function NewCharacterPane({ name, onNameChange, palettes, paletteId, onPaletteChange, avatars, avatarId, onAvatarChange, onStart, heading = "New run", nameLabel = "Character name", namePlaceholder = "Name your character", maxLength = 24, startLabel = "Begin", className, style, }: NewCharacterPanePro… — The New Game detail pane: a character name field, an optional palette and avatar choice, and the action that starts the run. The name field is a real form field, so menu key handling leaves typing alone (Enter inside the field starts the run).
|
|
1500
|
+
- `NewCharacterPaneProps` (interface): interface NewCharacterPaneProps — Props for {@link NewCharacterPane}.
|
|
1501
|
+
- `SAVE_SLOT_CHARACTER_KEY` (const): const SAVE_SLOT_CHARACTER_KEY: "character" — The character name a save card shows first, when the game supplies one as its own meta key.
|
|
1502
|
+
- `SAVE_SLOT_THUMBNAIL_KEY` (const): const SAVE_SLOT_THUMBNAIL_KEY: "thumbnail" — The save slot `meta` key a card reads as its thumbnail: a data URL or image URL captured when the save was written. Every other meta key is shown as a progress chip and never interpreted.
|
|
1503
|
+
- `SaveSlotCard` (function): function SaveSlotCard({ slot, now = Date.now, recent = false, onSelect, onDelete, loadLabel, className, style, }: SaveSlotCardProps): ReactNode — One save slot as a card: a captured thumbnail, the character name, playtime, the game's progress summary and when it was saved — or, for an empty slot, an empty slot with a New Game affordance. Deleting takes two presses: the first swaps the button for a confirm pair, so a mis-click cannot erase a run.
|
|
1504
|
+
- `SaveSlotCardProps` (interface): interface SaveSlotCardProps — Props for {@link SaveSlotCard}.
|
|
1505
|
+
- `SaveSlotsPane` (function): function SaveSlotsPane({ model, onContinue, onLoad, onNew, onDelete, heading = "Save slots", now = Date.now, columns = 2, className, style, }: SaveSlotsPaneProps): ReactNode — The slot grid for a {@link GameMenu} detail pane: Continue over the most recent save, then one card per slot. Arrow keys move between cards, Enter loads, and the grid scrolls inside its own pane rather than the page.
|
|
1506
|
+
- `SaveSlotsPaneProps` (interface): interface SaveSlotsPaneProps — Props for {@link SaveSlotsPane}.
|
|
1507
|
+
|
|
1508
|
+
## @jgengine/react/gameMenuSound
|
|
1509
|
+
|
|
1510
|
+
- `GameMenuSound` (interface): interface GameMenuSound — A menu sound player. Give it to `GameMenu`'s `sound` prop, or call its methods from your own menu.
|
|
1511
|
+
- `GameMenuSoundConfig` (interface): interface GameMenuSoundConfig — How {@link createGameMenuSound} is tuned.
|
|
1512
|
+
- `GameMenuSoundKind` (type): type GameMenuSoundKind = "move" | "confirm" | "back" — The short, quiet click a menu makes when the selection moves or is confirmed. Generated with an oscillator instead of shipped as an audio file, so a game gets menu feedback without authoring a sound and the whole thing unmounts with the screen.
|
|
1513
|
+
- `GameMenuSoundState` (interface): interface GameMenuSoundState — The sound player's readable state: on/off, gain, and whether audio is actually running.
|
|
1514
|
+
- `createGameMenuSound` (function): function createGameMenuSound(config: GameMenuSoundConfig = {}): GameMenuSound — A tiny WebAudio click generator for menu feedback: one oscillator burst per tick, created on the first play (so a browser autoplay policy does not log an error at mount) and reused after.
|
|
1515
|
+
|
|
1453
1516
|
## @jgengine/react/gameViewport
|
|
1454
1517
|
|
|
1455
1518
|
- `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.
|
|
@@ -1921,6 +1984,11 @@
|
|
|
1921
1984
|
- `relativeSavedAt` (function): function relativeSavedAt(savedAt: number, nowMs = Date.now()): string — Turn a `savedAt` timestamp into a compact relative label ("just now", "4m ago", "2d ago"). Presentation-only helper — the model stores raw ms.
|
|
1922
1985
|
- `useSaveSlots` (function): function useSaveSlots(model: SaveSlots): SaveSlotMeta[] — Subscribe to a {@link SaveSlots} index and re-render on any change (write, clear, rename, restore). Returns the live list of slots in menu order. Wire it into a save-select screen and mutate the model in event handlers — the menu re-renders itself.
|
|
1923
1986
|
|
|
1987
|
+
## @jgengine/react/saveThumbnail
|
|
1988
|
+
|
|
1989
|
+
- `SaveThumbnailOptions` (interface): interface SaveThumbnailOptions — Turning a rendered frame into the thumbnail a save card shows. Saves keep a small JPEG/WebP data URL (a couple of KB), not a full-resolution PNG, so a slot index still fits in local storage.
|
|
1990
|
+
- `shrinkCapture` (function): function shrinkCapture(capture: string | null, options: SaveThumbnailOptions = {}): Promise<string | null> — Downscale a captured frame (a data URL from `captureCanvas`) to a save-card thumbnail. Resolves to the thumbnail data URL, or `null` when the capture could not be decoded (a blank or tainted frame).
|
|
1991
|
+
|
|
1924
1992
|
## @jgengine/react/scoreboard
|
|
1925
1993
|
|
|
1926
1994
|
- `MedalToken` (type): type MedalToken = "gold" | "silver" | "bronze" — The medal tokens {@link medalFor} emits for the podium.
|
|
@@ -207,6 +207,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
207
207
|
|
|
208
208
|
- `ConfirmDialog` (function) · `import { ConfirmDialog } from "@jgengine/react"`
|
|
209
209
|
|
|
210
|
+
## continue-pane — continue detail pane — the save being resumed with thumbnail, character, playtime and progress
|
|
211
|
+
|
|
212
|
+
- `ContinuePane` (function) · `import { ContinuePane } from "@jgengine/react"`
|
|
213
|
+
|
|
210
214
|
## controller-camera-shake — R3F consumer that applies a core camera-shake controller's pooled per-frame offset to the active camera (translation + pitch/yaw/roll) so the view visibly shakes
|
|
211
215
|
|
|
212
216
|
- `ControllerCameraShake` (function) · `import { ControllerCameraShake } from "@jgengine/shell/camera"`
|
|
@@ -215,6 +219,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
215
219
|
|
|
216
220
|
- `ControlsList` (function) · `import { ControlsList } from "@jgengine/react"`
|
|
217
221
|
|
|
222
|
+
## credits-pane — credits detail pane — sections scrolled inside the pane
|
|
223
|
+
|
|
224
|
+
- `CreditsPane` (function) · `import { CreditsPane } from "@jgengine/react"`
|
|
225
|
+
|
|
218
226
|
## credits-panel — scrollable, navigable credits screen with a back affordance
|
|
219
227
|
|
|
220
228
|
- `CreditsPanel` (function) · `import { CreditsPanel } from "@jgengine/react"`
|
|
@@ -341,6 +349,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
341
349
|
|
|
342
350
|
- `GameContextBridge` (function) · `import { GameContextBridge } from "@jgengine/react"`
|
|
343
351
|
|
|
352
|
+
## game-menu — full-bleed game menu — living background, token-sized logo, animated navigable verbs, detail pane, selection sound, pause variant, no page scrollbar
|
|
353
|
+
|
|
354
|
+
- `GameMenu` (function) · `import { GameMenu } from "@jgengine/react"`
|
|
355
|
+
|
|
356
|
+
## game-pause-menu — pause menu from the shared game-menu system — same navigation and surface, sized down
|
|
357
|
+
|
|
358
|
+
- `GamePauseMenu` (function) · `import { GamePauseMenu } from "@jgengine/react"`
|
|
359
|
+
|
|
344
360
|
## gesture-resume — unlock the audio context on the first pointer/key/touch gesture, so pre-gesture cues play instead of vanishing
|
|
345
361
|
|
|
346
362
|
- `installGestureResume` (function) · `import { installGestureResume } from "@jgengine/shell/audio/gestureResume"`
|
|
@@ -577,6 +593,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
577
593
|
|
|
578
594
|
- `MenuScreen` (function) · `import { MenuScreen } from "@jgengine/react"`
|
|
579
595
|
|
|
596
|
+
## menu-sound — generated click for menu move/confirm/back, no audio asset
|
|
597
|
+
|
|
598
|
+
- `createGameMenuSound` (function) · `import { createGameMenuSound } from "@jgengine/react"`
|
|
599
|
+
|
|
580
600
|
## menu-stack-reducer — pure push/back/replace/reset transitions for a menu screen stack
|
|
581
601
|
|
|
582
602
|
- `menuStackReducer` (function) · `import { menuStackReducer } from "@jgengine/react/menuRouter"`
|
|
@@ -647,6 +667,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
647
667
|
|
|
648
668
|
- `WorldNameplates` (function) · `import { WorldNameplates } from "@jgengine/shell/world/WorldHud"`
|
|
649
669
|
|
|
670
|
+
## new-character-pane — character creation pane — name field, palette and avatar choice, start action
|
|
671
|
+
|
|
672
|
+
- `NewCharacterPane` (function) · `import { NewCharacterPane } from "@jgengine/react"`
|
|
673
|
+
|
|
650
674
|
## notice-feed — render ctx.game.notices as an animated toned stack (enter/exit motion, ×N stacking, HUD avoidance)
|
|
651
675
|
|
|
652
676
|
- `NoticeStack` (function) · `import { NoticeStack } from "@jgengine/react/notices"`
|
|
@@ -785,10 +809,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
785
809
|
|
|
786
810
|
- `resolveGameLook` (function) · `import { resolveGameLook } from "@jgengine/core/ui"`
|
|
787
811
|
|
|
812
|
+
## save-slot-card — save slot as a card — thumbnail, character, playtime, progress, timestamp, confirmed delete
|
|
813
|
+
|
|
814
|
+
- `SaveSlotCard` (function) · `import { SaveSlotCard } from "@jgengine/react"`
|
|
815
|
+
|
|
788
816
|
## save-slot-menu — drop-in save-select / profile menu that renders a core save-slots index as New / Continue / Load / Delete cards with free-string meta chips and relative save times, theme-skinnable
|
|
789
817
|
|
|
790
818
|
- `SaveSlotMenu` (function) · `import { SaveSlotMenu } from "@jgengine/react"`
|
|
791
819
|
|
|
820
|
+
## save-slots-pane — slot grid pane for the game menu — Continue newest, per-slot cards, keyboard navigation
|
|
821
|
+
|
|
822
|
+
- `SaveSlotsPane` (function) · `import { SaveSlotsPane } from "@jgengine/react"`
|
|
823
|
+
|
|
792
824
|
## scene-capture-binding — expose the in-Canvas scene-capture function to outside-Canvas HUD (photo mode)
|
|
793
825
|
|
|
794
826
|
- `SceneCaptureBinding` (function) · `import { SceneCaptureBinding } from "@jgengine/shell/render/sceneCapture"`
|
|
@@ -845,6 +877,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
845
877
|
|
|
846
878
|
- `ShopGrid` (function) · `import { ShopGrid } from "@jgengine/react"`
|
|
847
879
|
|
|
880
|
+
## shrink-capture — downscale a captured frame to a save-card thumbnail data URL
|
|
881
|
+
|
|
882
|
+
- `shrinkCapture` (function) · `import { shrinkCapture } from "@jgengine/react/saveThumbnail"`
|
|
883
|
+
|
|
848
884
|
## speed-format — render a m/s speed as km/h, mph, knots, or m/s for speedometers and telemetry HUDs
|
|
849
885
|
|
|
850
886
|
- `formatSpeed` (function) · `import { formatSpeed } from "@jgengine/core/ui"`
|
|
@@ -1019,6 +1055,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
1019
1055
|
|
|
1020
1056
|
- `useT` (function) · `import { useT } from "@jgengine/react"`
|
|
1021
1057
|
|
|
1058
|
+
## use-viewport-scroll-lock — count-aware document scroll lock for full-bleed surfaces
|
|
1059
|
+
|
|
1060
|
+
- `useViewportScrollLock` (function) · `import { useViewportScrollLock } from "@jgengine/react"`
|
|
1061
|
+
|
|
1022
1062
|
## use-wave-runner — React hook that subscribes to a wave runner and returns its pooled current-wave view
|
|
1023
1063
|
|
|
1024
1064
|
- `useWaveRunner` (function) · `import { useWaveRunner } from "@jgengine/react"`
|
|
@@ -243,7 +243,7 @@ Focus must look authored while remaining accessible. Menus should support keyboa
|
|
|
243
243
|
|
|
244
244
|
Compose it from the game's own markup over the headless `StartScreen` (`@jgengine/react`, `start-screen` capability) — a full-bleed `data-jg-menu` overlay that centers your content and offers an opt-in settings corner; it imposes no look. Menu rows are the game's own buttons and focus handling restyled into its chrome; the shipped pieces to compose in are `ControlsList` (`@jgengine/react`) for the control legend and `KeyHint`/`Keycap` for key badges (`KeyHint` already renders nothing on a coarse pointer) — never shipped as raw scaffold.
|
|
245
245
|
|
|
246
|
-
**The engine ships the front-end machinery; the game owns its look.** `MenuScreen` (`@jgengine/react/menuScreen`) is one animated screen over the live scene: transform/opacity enter and exit that unmount when the exit finishes, a translucent scrim (`"none"` keeps the 3D scene fully visible), and a `backdrop` slot for a poster or preview. `TitleMenu` (`@jgengine/react/titleMenu`) is the title shape
|
|
246
|
+
**The engine ships the front-end machinery; the game owns its look.** `GameMenu` (`@jgengine/react/gameMenu`) is the game menu: full-bleed `position: fixed` surface with the document scroll locked, a living background (the live scene behind it, a `background` layer, or drifting `art`), a logo sized by `--jg-menu-logo-size`, primary verbs whose highlight slides, glows and ticks (generated WebAudio, no asset), and a detail pane per verb. Ship the panes it exports: `ContinuePane` (the save being resumed), `NewCharacterPane` (name, palette, avatar), `SaveSlotsPane` (cards with a `meta.thumbnail` capture, playtime, progress, timestamp, two-press delete), `CreditsPane` (scroller inside the pane) and the game's real settings surface. `GamePauseMenu` is the same system at `variant="pause"`. Games supply the logo, art, wording, verbs and each pane's content; saves come from `createSaveSlots` + `refreshSaveSlots`, and a thumbnail from `captureCanvas` + `shrinkCapture`. `MenuScreen` (`@jgengine/react/menuScreen`) is one animated screen over the live scene: transform/opacity enter and exit that unmount when the exit finishes, a translucent scrim (`"none"` keeps the 3D scene fully visible), and a `backdrop` slot for a poster or preview. `TitleMenu` (`@jgengine/react/titleMenu`) is the plainer title shape - wordmark, eyebrow, tagline, verb list, footer, control hint. All read `useMenuNav` (`@jgengine/react/menuNav`): arrow keys/WASD/Home/End, a standard gamepad's d-pad and left stick (with hold-to-repeat), Enter/Space/A confirm, Escape/Backspace/B/Start back. Tab is never intercepted and typing in a field is never swallowed, so no menu traps focus; keys still work if the canvas kept focus. A game that composes its own markup gets the same behavior from `useMenuNav` alone.
|
|
247
247
|
|
|
248
248
|
**The front-end entries, when the game has them.** A menu is more than one Start button. Offer the real front-end verbs a player expects, gated to what the game actually supports:
|
|
249
249
|
|
|
@@ -2655,7 +2655,7 @@
|
|
|
2655
2655
|
- `ModelAssetRef` (interface): interface ModelAssetRef { url: string; anchor?: "center" | "origin"; dims?: ModelDims; collisionMesh?: CollisionMeshData; space?: AssetSpace; clips?: readonly string[] } — ⚠ undocumented · used by `createHostedWorldSessionAsync` (@jgengine/core/runtime/hostedWorldSession): Build a hosted session from an asynchronous persistence backend.
|
|
2656
2656
|
- `ModelDims` (interface): interface ModelDims — Measured horizontal footprint, footprint center, and lowest Y of a model in model space.
|
|
2657
2657
|
- `ModelNode` (interface): interface ModelNode — Generic named-socket reader for loaded 3D models. Walks a node tree (any object with `.name`, `.position`, and `.children` — structurally satisfied by `THREE.Object3D`) and collects the local offsets of nodes whose name marks an attachment point. Genre-agnostic: wire anchors on a pylon, muzzle/hand mounts on a character, hardpoints on a ship, seat/decal slots on furniture — anything an artist tags with an empty in the GLB. Pure data (no three.js import), so it lives in core.
|
|
2658
|
-
- `ModelVariantConfig` (interface): interface ModelVariantConfig — One mesh level of a {@link ModelVariantLodConfig}: a full or decimated GLB
|
|
2658
|
+
- `ModelVariantConfig` (interface): interface ModelVariantConfig — One mesh level of a {@link ModelVariantLodConfig}: a full or decimated GLB, how far it draws and how it shadows.
|
|
2659
2659
|
- `ModelVariantLodConfig` (interface): interface ModelVariantLodConfig — Ordered mesh detail levels for one model, switched per instance by camera distance. The first level is the full mesh the model's own `url` names; every later level is a cheaper asset. A tree line uses this so near trees keep their full mesh while far ones draw a decimated copy of the same species.
|
|
2660
2660
|
- `ModelVariantSelector` (interface): interface ModelVariantSelector — Per-instance distance switch over a model's detail levels: one answer per id, held across a boundary until the instance clears it.
|
|
2661
2661
|
- `ModelVariantSelectorState` (interface): interface ModelVariantSelectorState — Plain JSON state of a {@link ModelVariantSelector}: each id's current level index.
|
|
@@ -4041,7 +4041,8 @@
|
|
|
4041
4041
|
## @jgengine/core/world/modelLod
|
|
4042
4042
|
|
|
4043
4043
|
- `DEFAULT_VARIANT_HYSTERESIS` (const): const DEFAULT_VARIANT_HYSTERESIS: 6 — Metres an instance must travel past a switch boundary before it changes level unless the model declares its own.
|
|
4044
|
-
- `
|
|
4044
|
+
- `ModelShadowMode` (type): type ModelShadowMode = "cast" | "receive" | "both" | "none" — How a model, or one of its variant levels, takes part in the shadow pass.
|
|
4045
|
+
- `ModelVariantConfig` (interface): interface ModelVariantConfig — One mesh level of a {@link ModelVariantLodConfig}: a full or decimated GLB, how far it draws and how it shadows.
|
|
4045
4046
|
- `ModelVariantLodConfig` (interface): interface ModelVariantLodConfig — Ordered mesh detail levels for one model, switched per instance by camera distance. The first level is the full mesh the model's own `url` names; every later level is a cheaper asset. A tree line uses this so near trees keep their full mesh while far ones draw a decimated copy of the same species.
|
|
4046
4047
|
- `ModelVariantSelector` (interface): interface ModelVariantSelector — Per-instance distance switch over a model's detail levels: one answer per id, held across a boundary until the instance clears it.
|
|
4047
4048
|
- `ModelVariantSelectorState` (interface): interface ModelVariantSelectorState — Plain JSON state of a {@link ModelVariantSelector}: each id's current level index.
|