@jgengine/shell 0.28.0 → 0.30.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 +31 -0
- package/dist/Shell3dPresentation.js +28 -25
- package/dist/world/WorldHud.d.ts +2 -2
- package/dist/world/WorldHud.js +37 -6
- package/dist/world/worldBarSamples.d.ts +32 -0
- package/dist/world/worldBarSamples.js +100 -8
- package/package.json +4 -4
- package/skills/jgengine/api.md +2 -2
- package/skills/jgengine-ui/api.md +22 -14
- package/skills/jgengine-ui/capabilities.md +18 -2
- package/skills/jgengine-ui/reference.md +2 -2
- package/skills/jgengine-world/api.md +52 -0
- package/skills/jgengine-world/capabilities.md +32 -0
package/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,37 @@ 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.30.0
|
|
30
|
+
|
|
31
|
+
### Migrate
|
|
32
|
+
|
|
33
|
+
- **Bump lockstep SDK packages to `^0.30.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.27.0`; `@jgengine/github` is `0.17.0`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `ColorblindFrame` (`@jgengine/react/accessibility`) wraps the 3D canvas and applies the player's colour-vision mode to that box alone, so HUD, menu and caption text keep their own contrast. `colorblindFrameFilter` (`@jgengine/core/ui/accessibility`) resolves a mode to the CSS filter, and `useColorblindMode` reads the mode from the settings store the settings rows write; the 3D shell does both for the mounted game.
|
|
38
|
+
- `SettingsSection` (`@jgengine/react/settingsBlocks`) is one `useMenuNav` menu: a d-pad or left stick moves between rows and left/right steps the focused value — slider by its step, toggle flipped, select to the next option, confirm arms a keybind — through the same `onAdjust` path the arrow keys use. `onAdjust` (`@jgengine/react/menuNav`) is the option that makes left/right adjust instead of move.
|
|
39
|
+
- `createStanding` (`@jgengine/core/progression/standing`) holds per-faction or per-district standing over authored tracks and moves it from named outcomes, so an outcome that resolves to nothing throws at the call instead of silently moving nothing.
|
|
40
|
+
- `createGateSet` (`@jgengine/core/progression/gates`) evaluates authored milestones against standing and money, and refuses to load when a gate's unlock id resolves to nothing.
|
|
41
|
+
- `createCareerBank` (`@jgengine/core/progression/careerBank`) keeps persistent career money apart from a run's haul, so a failed run loses the haul and keeps the bank.
|
|
42
|
+
- `createUpgradeTree` (`@jgengine/core/progression/upgrades`) prices each rank from the shared curve primitive and gates purchases on prerequisites and standing.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- `AccessibilityProvider` (`@jgengine/react/accessibility`) sets its data attributes and text-scale var without filtering its own subtree. A colour matrix over the HUD and menu root recoloured the text the rest of the UI assumes, so wrap the game canvas in `ColorblindFrame` instead; a custom root can still offset it with its own `ColorblindFilters` and `filter: url(#jg-cb-<mode>)`.
|
|
47
|
+
- World health bars and nameplates derive their range from the render camera and the target height when a game sets no `maxDistance`: an overhead camera reaches the units it looks down at, and a bar is kept while the target it annotates still covers 18 px, instead of hiding every target past a fixed 60 units.
|
|
48
|
+
- A configured `worldHealthBars.maxDistance` or `nameplates.maxDistance` that hides every target in view now warns once with the nearest measured distance and the prop to raise, instead of rendering nothing.
|
|
49
|
+
|
|
50
|
+
## 0.29.0
|
|
51
|
+
|
|
52
|
+
### Migrate
|
|
53
|
+
|
|
54
|
+
- **Bump lockstep SDK packages to `^0.29.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.26.0`; `@jgengine/github` is `0.16.0`.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- `MissionView.remaining` (`@jgengine/core/gameplay`) now counts down from a `survive` objective's own `duration`, not only from a `timeout`. A defend or hold objective reports the seconds it still has to run, which is the one number its HUD needs; a completed run still reports null.
|
|
59
|
+
|
|
29
60
|
## 0.28.0
|
|
30
61
|
|
|
31
62
|
### Migrate
|
|
@@ -13,6 +13,7 @@ import { balanceLightingForIbl } from "@jgengine/core/render/lightingBalance";
|
|
|
13
13
|
import { skyNoonKeyframes } from "@jgengine/core/render/skyOwnedLighting";
|
|
14
14
|
import { GameViewportProvider } from "@jgengine/react/gameViewport";
|
|
15
15
|
import { GameProvider } from "@jgengine/react/provider";
|
|
16
|
+
import { ColorblindFrame, useColorblindMode } from "@jgengine/react/accessibility";
|
|
16
17
|
import { SettingsProvider } from "@jgengine/react/settings";
|
|
17
18
|
import { resolveWorldSky } from "./worldSky.js";
|
|
18
19
|
import { resolveCaptureCameraConfig } from "./camera/rigResolve.js";
|
|
@@ -355,6 +356,8 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
|
|
|
355
356
|
run: () => action.run(ctx),
|
|
356
357
|
}));
|
|
357
358
|
const touchScale = compact ? 0.88 : 1;
|
|
359
|
+
// The frame carries the player's colour-vision mode; the HUD and menu text above it stay unfiltered.
|
|
360
|
+
const colorblind = useColorblindMode(settingsStore);
|
|
358
361
|
const dockMounted = !poster &&
|
|
359
362
|
!orientationGate &&
|
|
360
363
|
coarsePointer &&
|
|
@@ -383,31 +386,31 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
|
|
|
383
386
|
else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
|
|
384
387
|
ctx.game.commands.run("ui.hotbarScrollPrev", {});
|
|
385
388
|
}
|
|
386
|
-
}, children: _jsxs(GameViewportProvider, { platforms: playable.platforms, children: [_jsxs(Canvas, { onCreated: ({ gl }) => {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
389
|
+
}, children: _jsxs(GameViewportProvider, { platforms: playable.platforms, children: [_jsx(ColorblindFrame, { mode: colorblind, style: { position: "absolute", inset: 0 }, children: _jsxs(Canvas, { onCreated: ({ gl }) => {
|
|
390
|
+
if (ktx2SupportDetected.current)
|
|
391
|
+
return;
|
|
392
|
+
ktx2SupportDetected.current = true;
|
|
393
|
+
detectKtx2Support(gl);
|
|
394
|
+
}, frameloop: poster && posterFrozen ? "demand" : graphics.frameRateLimit > 0 && !poster ? "never" : "always", orthographic: orthographic, camera: orthographic
|
|
395
|
+
? {
|
|
396
|
+
zoom: playable.camera?.frustum?.zoom ?? CAMERA_FRUSTUM_DEFAULTS.zoom,
|
|
397
|
+
near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
|
|
398
|
+
far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
|
|
399
|
+
}
|
|
400
|
+
: {
|
|
401
|
+
fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
|
|
402
|
+
near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
|
|
403
|
+
far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
|
|
404
|
+
}, shadows: graphics.shadows ? "percentage" : false, dpr: [Math.min(1, graphics.dpr), graphics.dpr], gl: { preserveDrawingBuffer: true, powerPreference: "high-performance" }, style: { touchAction: "none" }, children: [_jsx(TextureFiltering, { anisotropy: graphics.profile.anisotropy }), graphics.frameRateLimit > 0 && !poster ? _jsx(FrameRateLimiter, { fps: graphics.frameRateLimit }) : null, graphics.showFps ? _jsx(FpsProbe, { tally: frameTally }) : null, orthographic && playable.camera?.pixelPerfect !== undefined ? (_jsx(PixelPerfectCamera, { ...playable.camera.pixelPerfect })) : null, backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, cinematicLook ? (_jsx(EnvironmentLighting, { source: playable.environmentSource, skyColor: sceneLighting?.hemisphere?.skyColor ?? "#87b5e0", groundColor: sceneLighting?.hemisphere?.groundColor ?? "#3d4a38", horizonTint: sceneLighting?.ambient?.color, sunDirection: sceneLighting?.directional?.[0]?.position, sunColor: sceneLighting?.directional?.[0]?.color ?? "#fff2d6", ...(authoredDaylight ? { daylight: daylightModulation } : {}) })) : null, sceneLighting !== undefined ? (_jsx(ConfiguredLighting, { lighting: sceneLighting, profile: graphics.profile, ...(authoredDaylight ? { modulation: daylightModulation } : {}) })) : effectiveSky === undefined ? (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: cinematicLook ? 0.3 : 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 })] })) : null, authoredDaylight && effectiveSky !== undefined ? (_jsx(AuthoredDaylight, { sky: effectiveSky, clock: ctx.time, modulation: daylightModulation })) : null, effectiveSky !== undefined ? (effectiveSky.timeOfDay ? (_jsx(TimeOfDayDaylight, { sky: effectiveSky, clock: ctx.time, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)), fogRange: backdrop?.fog })) : (_jsx(SkyDaylight, { sky: effectiveSky, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)), fogRange: backdrop?.fog }))) : null, irradianceVolumes.length > 0 ? _jsx(IrradianceVolumeLighting, { volumes: irradianceVolumes }) : null, effectiveSky === undefined ? _jsx(BackdropFog, { fog: backdrop?.fog }) : null, _jsx(GameProvider, { context: ctx, children: _jsx(TerrainQualityContext.Provider, { value: graphics.quality, children: _jsxs(BuildingRenderBudget, { drawDistance: graphics.profile.drawDistance, visibility: playable.visibility, cullingDisabled: splitScreen, children: [_jsxs(CullingProvider, { config: playable.visibility, drawDistance: graphics.profile.drawDistance, children: [_jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, objectStyles: playable.objectStyles, environment: AutoEnvironment, assets: playable.game.assets, renderEntity: playable.renderEntity, renderObject: playable.renderObject, selectedIds: selectedIds, hideLocalActor: firstPerson, editorLayers: playable.editorLayers }), bars !== null ? (_jsx(WorldEntityBars, { statId: bars.statId, roles: bars.roles, resolveRole: resolveEntityRole, ...(bars.maxSamples === undefined ? {} : { maxSamples: bars.maxSamples }), ...(bars.tickMs === undefined ? {} : { tickMs: bars.tickMs }), ...(bars.maxDistance === undefined ? {} : { maxDistance: bars.maxDistance }), ...(bars.occlude === undefined ? {} : { occlude: bars.occlude }) })) : null, nameplates !== null ? (_jsx(WorldNameplates, { statId: nameplates.statId, roles: nameplates.roles, resolveRole: resolveEntityRole, ...(nameplates.maxSamples === undefined ? {} : { maxSamples: nameplates.maxSamples }), ...(nameplates.tickMs === undefined ? {} : { tickMs: nameplates.tickMs }), ...(nameplates.maxDistance === undefined ? {} : { maxDistance: nameplates.maxDistance }), occlude: nameplates.occlude, ...(nameplates.resolveName === undefined ? {} : { resolveName: nameplates.resolveName }), ...(nameplates.showHealth === undefined ? {} : { showHealth: nameplates.showHealth }) })) : null, WorldOverlay !== undefined ? (_jsx("group", { userData: CAMERA_TRANSPARENT_USERDATA, children: _jsx(WorldOverlay, { ctx: ctx }) })) : null] }), _jsx(WorldItems, { config: playable.worldItem }), _jsx(WorldParticles, { quality: graphics.quality, particleCap: graphics.profile.particleCap, particleBudget: graphics.profile.particleBudget, diagnostics: devtoolsEnabled }), _jsx(CombatPresentation, { effects: effects }), devtoolsEnabled ? _jsx(CollisionDebugWorld, {}) : null, _jsx(AudioListener, { engine: audioEngine, anchor: playable.camera?.rig === "rts" || playable.camera?.rig === "topDown" ? "ground" : "camera" }), _jsx(EntityAudioEmitters, { engine: audioEngine, entitySounds: playable.entitySounds }), _jsx(ObjectAudioEmitters, { engine: audioEngine, objectSounds: playable.objectSounds }), _jsx(GameCameraRig, { yawRef: yawRef, pitchRef: pitchRef, config: cameraConfig, viewmodel: playable.viewmodel, pointerControls: pointerUsesLeft, panKeysEnabled: rtsPanKeysEnabled, director: ctx.camera, onDragChange: (dragging) => {
|
|
405
|
+
cameraDraggingRef.current = dragging;
|
|
406
|
+
} }), _jsx(PointerProbe, { service: pointerService })] }) }) }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, pointerAxisRef: pointerAxisRef, analogRef: analogRef, gateRef: gateRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand, poster: poster, onPosterSettled: onPosterSettled, authoritativeFrameRef: authoritativeFrameRef }), _jsx(GamepadSource, { tracker: tracker, bindings: inputBindings ?? {}, analogRef: analogRef, input: ctx.input, feel: playable.gamepad, seats: localPlayers(ctx), onSeatJoin: (slot) => {
|
|
407
|
+
try {
|
|
408
|
+
playable.loop.onNewPlayer(ctx, { userId: slot.userId, isNew: true });
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
reportRuntimeError(error, "tick");
|
|
412
|
+
}
|
|
413
|
+
}, seatsActive: () => !gateRef.current && playControlsActive(ctx) }), _jsx(DevtoolsRendererProbe, {}), devtoolsEnabled ? (_jsx(FrameTelemetry, { game: playable.game.name, quality: graphics.quality === "low" ? "draft" : "full" })) : null, splitScreen ? (_jsx(SplitScreenRenderer, { ctx: ctx, viewports: seatViewports, config: playable.viewports, mainCamera: playable.camera })) : null, !splitScreen && resolvedLook.postProcessing !== undefined && resolvedLook.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: resolvedLook.postProcessing, quality: graphics.quality, stages: graphics.profile.postStages })) : null, poster ? null : _jsx(SceneWarmupGate, { onState: setWarmup })] }) }), _jsx(SceneWarmupCover, { state: warmup }), graphics.showFps && !poster ? _jsx(FpsCounter, { tally: frameTally }) : null, !poster &&
|
|
411
414
|
!orientationGate &&
|
|
412
415
|
coarsePointer &&
|
|
413
416
|
controlsActive &&
|
package/dist/world/WorldHud.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export declare function WorldEntityBars({ statId, height, roles, resolveRole, ma
|
|
|
10
10
|
height?: number;
|
|
11
11
|
roles?: readonly CatalogEntityRole[];
|
|
12
12
|
resolveRole?: (entity: SceneEntity) => CatalogEntityRole | undefined;
|
|
13
|
-
/** Hide bars beyond this camera distance in world units.
|
|
13
|
+
/** Hide bars beyond this camera distance in world units. Unset derives the range from the camera and the target height. */
|
|
14
14
|
maxDistance?: number;
|
|
15
15
|
/** Hide bars behind blocking geometry from the render camera. Default true; false explicitly reveals through walls. */
|
|
16
16
|
occlude?: boolean;
|
|
@@ -25,7 +25,7 @@ export interface WorldNameplatesProps {
|
|
|
25
25
|
height?: number;
|
|
26
26
|
roles?: readonly CatalogEntityRole[];
|
|
27
27
|
resolveRole?: (entity: SceneEntity) => CatalogEntityRole | undefined;
|
|
28
|
-
/** Hide nameplates beyond this camera distance in world units.
|
|
28
|
+
/** Hide nameplates beyond this camera distance in world units. Unset derives the range from the camera and the plate height. */
|
|
29
29
|
maxDistance?: number;
|
|
30
30
|
/** Hide plates behind blocking geometry from the render camera. Default true; false explicitly reveals through walls. */
|
|
31
31
|
occlude?: boolean;
|
package/dist/world/WorldHud.js
CHANGED
|
@@ -9,7 +9,7 @@ import { useCameraShake } from "../camera/shakeChannel.js";
|
|
|
9
9
|
import { CALIBRATED_TRAUMA_SHAKE_DECAY_PER_SECOND } from "../camera/rigMath.js";
|
|
10
10
|
import { readFirstPersonMuzzle } from "../camera/GameFirstPersonCamera.js";
|
|
11
11
|
import { resolveFloatTextStyle } from "./floatTextStyle.js";
|
|
12
|
-
import { collectNameplateSamples, collectWorldBarSamples, paintWorldBarSamples, refreshWorldBarSamples, } from "./worldBarSamples.js";
|
|
12
|
+
import { collectNameplateSamples, collectWorldBarSamples, createOverlayRangeWarn, deriveWorldBarRange, nearestOverlayTargetDistance, paintWorldBarSamples, refreshWorldBarSamples, worldBarTargetFilter, worldNameplateTargetFilter, } from "./worldBarSamples.js";
|
|
13
13
|
import { telegraphPulseOpacity } from "./telegraphPulse.js";
|
|
14
14
|
import { useRenderVisibility } from "../visibility/CullingProvider.js";
|
|
15
15
|
import { CAMERA_POST_FRAME_PRIORITY } from "../camera/cameraRigs.js";
|
|
@@ -18,7 +18,27 @@ export { telegraphPulseOpacity } from "./telegraphPulse.js";
|
|
|
18
18
|
function pinOverlayToViewport(_el, _camera, size) {
|
|
19
19
|
return [size.width / 2, size.height / 2];
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* A configured range that hides every overlay is a silent blank screen; measure the nearest
|
|
23
|
+
* on-screen target and name the prop to raise. Probes at most once a second and reports once.
|
|
24
|
+
*/
|
|
25
|
+
function reportHiddenRange(ref, block, configured, measure) {
|
|
26
|
+
const state = (ref.current ??= { warn: createOverlayRangeWarn(), reported: false, at: -Infinity });
|
|
27
|
+
if (state.reported)
|
|
28
|
+
return;
|
|
29
|
+
const now = performance.now();
|
|
30
|
+
if (now - state.at < 1000)
|
|
31
|
+
return;
|
|
32
|
+
state.at = now;
|
|
33
|
+
const nearest = measure();
|
|
34
|
+
if (nearest === null)
|
|
35
|
+
return;
|
|
36
|
+
state.reported = true;
|
|
37
|
+
const message = state.warn(block, configured, nearest);
|
|
38
|
+
if (message !== null)
|
|
39
|
+
console.warn(message);
|
|
40
|
+
}
|
|
41
|
+
export function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, maxDistance, occlude = true, tickMs = 80, maxSamples = 64, }) {
|
|
22
42
|
const ctx = useGameContext();
|
|
23
43
|
const camera = useThree((state) => state.camera);
|
|
24
44
|
const size = useThree((state) => state.size);
|
|
@@ -27,6 +47,7 @@ export function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, maxD
|
|
|
27
47
|
const samplesRef = useRef([]);
|
|
28
48
|
const projectRef = useRef(new THREE.Vector3());
|
|
29
49
|
const lastTickRef = useRef(-Infinity);
|
|
50
|
+
const warnRef = useRef(null);
|
|
30
51
|
const visibility = useRenderVisibility();
|
|
31
52
|
useFrame((state) => {
|
|
32
53
|
const nowMs = state.clock.elapsedTime * 1000;
|
|
@@ -43,12 +64,17 @@ export function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, maxD
|
|
|
43
64
|
canvas.height = pixelH;
|
|
44
65
|
}
|
|
45
66
|
camera.updateMatrixWorld();
|
|
67
|
+
const viewport = { width: cssW, height: cssH };
|
|
68
|
+
const range = maxDistance ?? deriveWorldBarRange(camera, viewport, height);
|
|
46
69
|
if (nowMs - lastTickRef.current >= Math.max(0, tickMs)) {
|
|
47
70
|
lastTickRef.current = nowMs;
|
|
48
|
-
collectWorldBarSamples(ctx, statId, height, roles, resolveRole, camera,
|
|
71
|
+
collectWorldBarSamples(ctx, statId, height, roles, resolveRole, camera, viewport, samplesRef.current, projectRef.current, range, occlude, { maxSamples, isVisible: visibility.current });
|
|
72
|
+
if (maxDistance !== undefined && samplesRef.current.length === 0) {
|
|
73
|
+
reportHiddenRange(warnRef, "worldHealthBars", maxDistance, () => nearestOverlayTargetDistance(ctx, camera, height, deriveWorldBarRange(camera, viewport, height), projectRef.current, { maxSamples, isVisible: visibility.current }, worldBarTargetFilter(ctx, statId, roles, resolveRole)));
|
|
74
|
+
}
|
|
49
75
|
}
|
|
50
76
|
else {
|
|
51
|
-
refreshWorldBarSamples(ctx, height, camera,
|
|
77
|
+
refreshWorldBarSamples(ctx, height, camera, viewport, samplesRef.current, projectRef.current, range, { isVisible: visibility.current });
|
|
52
78
|
}
|
|
53
79
|
paintWorldBarSamples(canvas, samplesRef.current, dpr);
|
|
54
80
|
}, CAMERA_POST_FRAME_PRIORITY + 0.01);
|
|
@@ -62,7 +88,7 @@ export function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, maxD
|
|
|
62
88
|
*
|
|
63
89
|
* @capability nameplates billboarded name + HP bar over nearby entities
|
|
64
90
|
*/
|
|
65
|
-
export function WorldNameplates({ statId = "health", height = 2.3, roles, resolveRole, maxDistance
|
|
91
|
+
export function WorldNameplates({ statId = "health", height = 2.3, roles, resolveRole, maxDistance, occlude = true, tickMs = 120, maxSamples = 64, resolveName, showHealth = true, className, nameplateClassName, nameClassName, barClassName, fillClassName, renderNameplate, }) {
|
|
66
92
|
const ctx = useGameContext();
|
|
67
93
|
const camera = useThree((state) => state.camera);
|
|
68
94
|
const size = useThree((state) => state.size);
|
|
@@ -70,6 +96,7 @@ export function WorldNameplates({ statId = "health", height = 2.3, roles, resolv
|
|
|
70
96
|
const samplesRef = useRef([]);
|
|
71
97
|
const projectRef = useRef(new THREE.Vector3());
|
|
72
98
|
const lastTickRef = useRef(-Infinity);
|
|
99
|
+
const warnRef = useRef(null);
|
|
73
100
|
const visibility = useRenderVisibility();
|
|
74
101
|
useFrame((state) => {
|
|
75
102
|
const nowMs = state.clock.elapsedTime * 1000;
|
|
@@ -77,7 +104,11 @@ export function WorldNameplates({ statId = "health", height = 2.3, roles, resolv
|
|
|
77
104
|
return;
|
|
78
105
|
lastTickRef.current = nowMs;
|
|
79
106
|
camera.updateMatrixWorld();
|
|
80
|
-
|
|
107
|
+
const viewport = { width: size.width, height: size.height };
|
|
108
|
+
collectNameplateSamples(ctx, statId, height, roles, resolveRole, camera, viewport, samplesRef.current, projectRef.current, maxDistance ?? deriveWorldBarRange(camera, viewport, height), occlude, { maxSamples, isVisible: visibility.current, ...(resolveName === undefined ? {} : { resolveName }) });
|
|
109
|
+
if (maxDistance !== undefined && samplesRef.current.length === 0) {
|
|
110
|
+
reportHiddenRange(warnRef, "nameplates", maxDistance, () => nearestOverlayTargetDistance(ctx, camera, height, deriveWorldBarRange(camera, viewport, height), projectRef.current, { maxSamples, isVisible: visibility.current, ...(resolveName === undefined ? {} : { resolveName }) }, worldNameplateTargetFilter(roles, resolveRole, resolveName)));
|
|
111
|
+
}
|
|
81
112
|
setSamples([...samplesRef.current]);
|
|
82
113
|
}, CAMERA_POST_FRAME_PRIORITY + 0.01);
|
|
83
114
|
return (_jsx(Html, { fullscreen: true, calculatePosition: pinOverlayToViewport, zIndexRange: [19, 0], style: { pointerEvents: "none" }, children: _jsx("div", { className: className, "data-nameplates": true, children: samples.map((sample) => renderNameplate !== undefined ? (_jsx("div", { style: { position: "absolute", left: sample.x, top: sample.y }, children: renderNameplate(sample) }, sample.id)) : (_jsxs("div", { className: nameplateClassName, "data-nameplate": sample.id, style: {
|
|
@@ -28,6 +28,8 @@ export interface WorldOverlayCamera {
|
|
|
28
28
|
elements: ArrayLike<number>;
|
|
29
29
|
};
|
|
30
30
|
projectionMatrix: unknown;
|
|
31
|
+
/** Clipping distance; bounds every derived overlay range. */
|
|
32
|
+
far?: number;
|
|
31
33
|
}
|
|
32
34
|
/** Sampling policy shared by health bars and nameplates. */
|
|
33
35
|
export interface WorldOverlaySampleOptions {
|
|
@@ -40,6 +42,36 @@ export interface WorldOverlaySampleOptions {
|
|
|
40
42
|
}
|
|
41
43
|
/** True when blocking scene geometry lies between the render camera and overlay anchor. @internal */
|
|
42
44
|
export declare function worldBarOccluded(ctx: GameContext, from: EntityPosition, target: EntityPosition): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Overlay range for a camera that configured none. Two measured factors replace the magic
|
|
47
|
+
* distance: the camera's own view of the ground (the frustum corner at the depth it looks down
|
|
48
|
+
* through, so an overhead camera always reaches the units below it) and the distance at which an
|
|
49
|
+
* `anchorHeight`-tall target stops covering {@link MIN_TARGET_PX} pixels. Clamped to the camera's
|
|
50
|
+
* far plane.
|
|
51
|
+
* @internal
|
|
52
|
+
*/
|
|
53
|
+
export declare function deriveWorldBarRange(camera: WorldOverlayCamera, viewport: {
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
}, anchorHeight: number): number;
|
|
57
|
+
/**
|
|
58
|
+
* Nearest on-screen target the overlay would annotate, in world units from the render camera, or
|
|
59
|
+
* `null` when nothing it covers is in view. Used to name the real distance when a configured
|
|
60
|
+
* range hides every bar. Scans one frozen list and compares squared distances — no per-target
|
|
61
|
+
* allocation — inside `searchRadius`.
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
export declare function nearestOverlayTargetDistance(ctx: GameContext, camera: WorldOverlayCamera, anchorHeight: number, searchRadius: number, project: Projectable, options: WorldOverlaySampleOptions, accept: (entity: SceneEntity) => boolean): number | null;
|
|
65
|
+
/**
|
|
66
|
+
* One-shot warning text for a configured overlay range that hides everything in view, or `null`
|
|
67
|
+
* once it has reported the same block and distance.
|
|
68
|
+
* @internal
|
|
69
|
+
*/
|
|
70
|
+
export declare function createOverlayRangeWarn(): (block: string, configured: number, nearest: number) => string | null;
|
|
71
|
+
/** Target filter mirroring the bars' own eligibility, for range diagnostics. @internal */
|
|
72
|
+
export declare function worldBarTargetFilter(ctx: GameContext, statId: string, roles: readonly CatalogEntityRole[] | undefined, resolveRole: ((entity: SceneEntity) => CatalogEntityRole | undefined) | undefined): (entity: SceneEntity) => boolean;
|
|
73
|
+
/** Target filter mirroring the nameplates' own eligibility, for range diagnostics. @internal */
|
|
74
|
+
export declare function worldNameplateTargetFilter(roles: readonly CatalogEntityRole[] | undefined, resolveRole: ((entity: SceneEntity) => CatalogEntityRole | undefined) | undefined, resolveName: ((entity: SceneEntity) => string | null) | undefined): (entity: SceneEntity) => boolean;
|
|
43
75
|
/** Project nearby live non-local entities carrying `statId`; occlusion defaults on. @internal */
|
|
44
76
|
export declare function collectWorldBarSamples(ctx: GameContext, statId: string, height: number, roles: readonly CatalogEntityRole[] | undefined, resolveRole: ((entity: SceneEntity) => CatalogEntityRole | undefined) | undefined, camera: WorldOverlayCamera, viewport: {
|
|
45
77
|
width: number;
|
|
@@ -19,6 +19,80 @@ function cameraOrigin(camera) {
|
|
|
19
19
|
const matrix = camera.matrixWorld.elements;
|
|
20
20
|
return [matrix[12], matrix[13], matrix[14]];
|
|
21
21
|
}
|
|
22
|
+
/** Screen height at which an annotated target is still plainly visible. */
|
|
23
|
+
const MIN_TARGET_PX = 18;
|
|
24
|
+
/** `1 / tan(fovY / 2)` for the engine's default 55° camera, used when the projection is not perspective. */
|
|
25
|
+
const FALLBACK_PROJECTION_SCALE = 1 / Math.tan((55 * Math.PI) / 360);
|
|
26
|
+
/**
|
|
27
|
+
* Overlay range for a camera that configured none. Two measured factors replace the magic
|
|
28
|
+
* distance: the camera's own view of the ground (the frustum corner at the depth it looks down
|
|
29
|
+
* through, so an overhead camera always reaches the units below it) and the distance at which an
|
|
30
|
+
* `anchorHeight`-tall target stops covering {@link MIN_TARGET_PX} pixels. Clamped to the camera's
|
|
31
|
+
* far plane.
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
export function deriveWorldBarRange(camera, viewport, anchorHeight) {
|
|
35
|
+
const elements = camera.matrixWorld.elements;
|
|
36
|
+
const cameraY = elements[13] ?? 0;
|
|
37
|
+
const projection = camera.projectionMatrix?.elements;
|
|
38
|
+
const perspective = projection !== undefined && projection[15] === 0 && (projection[5] ?? 0) > 0;
|
|
39
|
+
const scale = perspective ? projection[5] : FALLBACK_PROJECTION_SCALE;
|
|
40
|
+
const tanHalf = 1 / scale;
|
|
41
|
+
const height = Math.max(0.1, anchorHeight);
|
|
42
|
+
const readable = viewport.height * 0.5 * scale * height / MIN_TARGET_PX;
|
|
43
|
+
const depth = Math.max(0, cameraY - height);
|
|
44
|
+
const aspect = viewport.width / Math.max(1, viewport.height);
|
|
45
|
+
const groundReach = depth * Math.sqrt(1 + tanHalf * tanHalf * (1 + aspect * aspect));
|
|
46
|
+
const reach = Math.max(readable, groundReach);
|
|
47
|
+
const far = camera.far;
|
|
48
|
+
const clamped = Number.isFinite(far) && far !== undefined && far > 0 ? Math.min(reach, far) : reach;
|
|
49
|
+
return Math.max(1, clamped);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Nearest on-screen target the overlay would annotate, in world units from the render camera, or
|
|
53
|
+
* `null` when nothing it covers is in view. Used to name the real distance when a configured
|
|
54
|
+
* range hides every bar. Scans one frozen list and compares squared distances — no per-target
|
|
55
|
+
* allocation — inside `searchRadius`.
|
|
56
|
+
* @internal
|
|
57
|
+
*/
|
|
58
|
+
export function nearestOverlayTargetDistance(ctx, camera, anchorHeight, searchRadius, project, options, accept) {
|
|
59
|
+
if (!Number.isFinite(searchRadius) || searchRadius <= 0)
|
|
60
|
+
return null;
|
|
61
|
+
const origin = cameraOrigin(camera);
|
|
62
|
+
const limitSquared = searchRadius * searchRadius;
|
|
63
|
+
let nearest = Infinity;
|
|
64
|
+
for (const id of ctx.scene.entity.inRadius(origin, searchRadius)) {
|
|
65
|
+
const entity = eligibleEntity(ctx, id, options);
|
|
66
|
+
if (entity === null || !accept(entity))
|
|
67
|
+
continue;
|
|
68
|
+
const dx = entity.position[0] - origin[0];
|
|
69
|
+
const dy = entity.position[1] - origin[1];
|
|
70
|
+
const dz = entity.position[2] - origin[2];
|
|
71
|
+
const distanceSquared = dx * dx + dy * dy + dz * dz;
|
|
72
|
+
if (distanceSquared > limitSquared || distanceSquared >= nearest)
|
|
73
|
+
continue;
|
|
74
|
+
if (projectAnchor(entity, anchorHeight, camera, project) === null)
|
|
75
|
+
continue;
|
|
76
|
+
nearest = distanceSquared;
|
|
77
|
+
}
|
|
78
|
+
return Number.isFinite(nearest) ? Math.sqrt(nearest) : null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* One-shot warning text for a configured overlay range that hides everything in view, or `null`
|
|
82
|
+
* once it has reported the same block and distance.
|
|
83
|
+
* @internal
|
|
84
|
+
*/
|
|
85
|
+
export function createOverlayRangeWarn() {
|
|
86
|
+
let reported = "";
|
|
87
|
+
return (block, configured, nearest) => {
|
|
88
|
+
const key = `${block}:${configured}`;
|
|
89
|
+
if (reported === key)
|
|
90
|
+
return null;
|
|
91
|
+
reported = key;
|
|
92
|
+
const suggested = Math.ceil(nearest + 1);
|
|
93
|
+
return `[jgengine] ${block}.maxDistance ${configured} hides every overlay on screen: the nearest target is ${nearest.toFixed(1)} world units away. Raise ${block}.maxDistance to about ${suggested}, or drop it to derive the range from the camera.`;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
22
96
|
function projectAnchor(entity, height, camera, project) {
|
|
23
97
|
const anchor = [entity.position[0], entity.position[1] + height, entity.position[2]];
|
|
24
98
|
const inverse = camera.matrixWorldInverse.elements;
|
|
@@ -62,15 +136,25 @@ function eligibleEntity(ctx, id, options) {
|
|
|
62
136
|
function sampleLimit(options) {
|
|
63
137
|
return Math.max(0, Math.min(256, Math.floor(options.maxSamples ?? 64) || 0));
|
|
64
138
|
}
|
|
139
|
+
/** Target filter mirroring the bars' own eligibility, for range diagnostics. @internal */
|
|
140
|
+
export function worldBarTargetFilter(ctx, statId, roles, resolveRole) {
|
|
141
|
+
return (entity) => worldHealthBarAllowsRole(roles, resolveRole?.(entity)) && ctx.scene.entity.stats.get(entity.id, statId) !== null;
|
|
142
|
+
}
|
|
143
|
+
/** Target filter mirroring the nameplates' own eligibility, for range diagnostics. @internal */
|
|
144
|
+
export function worldNameplateTargetFilter(roles, resolveRole, resolveName) {
|
|
145
|
+
return (entity) => worldHealthBarAllowsRole(roles, resolveRole?.(entity)) &&
|
|
146
|
+
Boolean((resolveName ?? defaultDisplayName)(entity)?.trim());
|
|
147
|
+
}
|
|
65
148
|
/** Project nearby live non-local entities carrying `statId`; occlusion defaults on. @internal */
|
|
66
|
-
export function collectWorldBarSamples(ctx, statId, height, roles, resolveRole, camera, viewport, into, project, maxDistance
|
|
149
|
+
export function collectWorldBarSamples(ctx, statId, height, roles, resolveRole, camera, viewport, into, project, maxDistance, occlude = true, options = {}) {
|
|
67
150
|
into.length = 0;
|
|
68
151
|
const origin = cameraOrigin(camera);
|
|
69
152
|
const limit = sampleLimit(options);
|
|
70
|
-
|
|
153
|
+
const range = maxDistance ?? deriveWorldBarRange(camera, viewport, height);
|
|
154
|
+
if (limit === 0 || !Number.isFinite(range) || range <= 0)
|
|
71
155
|
return 0;
|
|
72
156
|
let checked = 0;
|
|
73
|
-
for (const id of ctx.scene.entity.inRadius(origin,
|
|
157
|
+
for (const id of ctx.scene.entity.inRadius(origin, range)) {
|
|
74
158
|
const entity = eligibleEntity(ctx, id, options);
|
|
75
159
|
if (entity === null || !worldHealthBarAllowsRole(roles, resolveRole?.(entity)))
|
|
76
160
|
continue;
|
|
@@ -97,14 +181,21 @@ export function collectWorldBarSamples(ctx, statId, height, roles, resolveRole,
|
|
|
97
181
|
return into.length;
|
|
98
182
|
}
|
|
99
183
|
/** Reproject retained bar anchors without a nearby query or visibility ray. @internal */
|
|
100
|
-
export function refreshWorldBarSamples(ctx, height, camera, viewport, samples, project, maxDistance
|
|
184
|
+
export function refreshWorldBarSamples(ctx, height, camera, viewport, samples, project, maxDistance, options = {}) {
|
|
101
185
|
const origin = cameraOrigin(camera);
|
|
186
|
+
const range = maxDistance ?? deriveWorldBarRange(camera, viewport, height);
|
|
187
|
+
const rangeSquared = range * range;
|
|
102
188
|
let kept = 0;
|
|
103
189
|
for (const sample of samples) {
|
|
104
190
|
if (sample.entityId === undefined)
|
|
105
191
|
continue;
|
|
106
192
|
const entity = eligibleEntity(ctx, sample.entityId, options);
|
|
107
|
-
if (entity === null
|
|
193
|
+
if (entity === null)
|
|
194
|
+
continue;
|
|
195
|
+
const dx = entity.position[0] - origin[0];
|
|
196
|
+
const dy = entity.position[1] - origin[1];
|
|
197
|
+
const dz = entity.position[2] - origin[2];
|
|
198
|
+
if (dx * dx + dy * dy + dz * dz > rangeSquared)
|
|
108
199
|
continue;
|
|
109
200
|
const anchor = projectAnchor(entity, height, camera, project);
|
|
110
201
|
if (anchor === null)
|
|
@@ -122,14 +213,15 @@ function defaultDisplayName(entity) {
|
|
|
122
213
|
return name === entity.id || /^[a-z0-9_-]+$/.test(name) || /[_:/]/.test(name) ? null : name || null;
|
|
123
214
|
}
|
|
124
215
|
/** Project nearby live non-local nameplates; occlusion defaults on. @internal */
|
|
125
|
-
export function collectNameplateSamples(ctx, statId, height, roles, resolveRole, camera, viewport, into, project, maxDistance
|
|
216
|
+
export function collectNameplateSamples(ctx, statId, height, roles, resolveRole, camera, viewport, into, project, maxDistance, occlude = true, options = {}) {
|
|
126
217
|
into.length = 0;
|
|
127
218
|
const origin = cameraOrigin(camera);
|
|
128
219
|
const limit = sampleLimit(options);
|
|
129
|
-
|
|
220
|
+
const range = maxDistance ?? deriveWorldBarRange(camera, viewport, height);
|
|
221
|
+
if (limit === 0 || !Number.isFinite(range) || range <= 0)
|
|
130
222
|
return 0;
|
|
131
223
|
let checked = 0;
|
|
132
|
-
for (const id of ctx.scene.entity.inRadius(origin,
|
|
224
|
+
for (const id of ctx.scene.entity.inRadius(origin, range)) {
|
|
133
225
|
const entity = eligibleEntity(ctx, id, options);
|
|
134
226
|
if (entity === null || !worldHealthBarAllowsRole(roles, resolveRole?.(entity)))
|
|
135
227
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
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",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"test": "bun test src"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@jgengine/core": "^0.
|
|
39
|
-
"@jgengine/react": "^0.
|
|
40
|
-
"@jgengine/ws": "^0.
|
|
38
|
+
"@jgengine/core": "^0.30.0",
|
|
39
|
+
"@jgengine/react": "^0.30.0",
|
|
40
|
+
"@jgengine/ws": "^0.30.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@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.30.0" — 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.30.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
289
289
|
|
|
290
290
|
## @jgengine/core/runtime/adapter
|
|
291
291
|
|
|
@@ -407,6 +407,7 @@
|
|
|
407
407
|
- `TEXT_SCALE_MAX` (const): const TEXT_SCALE_MAX: 2 — Maximum UI text scale.
|
|
408
408
|
- `TEXT_SCALE_MIN` (const): const TEXT_SCALE_MIN: 0.75 — Minimum UI text scale.
|
|
409
409
|
- `clampTextScale` (function): function clampTextScale(scale: number): number — Clamp a text-scale multiplier into the supported `[TEXT_SCALE_MIN, TEXT_SCALE_MAX]` range.
|
|
410
|
+
- `colorblindFrameFilter` (function): function colorblindFrameFilter(mode: ColorblindMode): string | null — The CSS filter that applies `mode` to one rendered frame — a `url(#jg-cb-<mode>)` reference to the `feColorMatrix` defs {@link COLORBLIND_MATRICES} drives. `"none"` maps to `null` (no filter), so a caller can spread it onto the canvas alone and leave HUD and menu text at its own contrast.
|
|
410
411
|
- `createAccessibilityStore` (function): function createAccessibilityStore(initial: Partial<AccessibilityState> = {}): AccessibilityStore — Serializable, observable store of accessibility preferences (reduced motion, high contrast, text scale, colorblind mode, captions). Genre-agnostic plumbing like input rebinding — a game binds these to its settings UI and adapts its presentation; `snapshot`/`restore` round-trip through a save or settings blob.
|
|
411
412
|
- `reducedMotionDuration` (function): function reducedMotionDuration(state: AccessibilityState, ms: number, reduced = 0): number — Return `ms` when motion is allowed, or `reduced` (default 0) when `reducedMotion` is set — the animation-duration gate.
|
|
412
413
|
|
|
@@ -652,7 +653,7 @@
|
|
|
652
653
|
## @jgengine/react
|
|
653
654
|
|
|
654
655
|
- `AbilitySlotBindingOptions` (interface): interface AbilitySlotBindingOptions { intervalMs?: number } — ⚠ undocumented · used by `useEventMeter`: Bind a `createEventMeter` (`@jgengine/core/stats/eventMeter`) heat/streak gauge to a component — the react-render half of the ult/adrenaline…
|
|
655
|
-
- `AccessibilityProvider` (function): function AccessibilityProvider({ store, children, className, style, }: { store: AccessibilityStore; children: ReactNode; className?: string; style?: CSSProperties; }): ReactNode — Apply accessibility preferences to a subtree: exposes `--jg-text-scale` for text to scale off, sets `data-reduced-motion` / `data-high-contrast` / `data-colorblind` / `data-captions` for CSS to respond to
|
|
656
|
+
- `AccessibilityProvider` (function): function AccessibilityProvider({ store, children, className, style, }: { store: AccessibilityStore; children: ReactNode; className?: string; style?: CSSProperties; }): ReactNode — Apply accessibility preferences to a subtree: exposes `--jg-text-scale` for text to scale off, and sets `data-reduced-motion` / `data-high-contrast` / `data-colorblind` / `data-captions` for CSS to respond to. Reads a `createAccessibilityStore` and re-renders on change; provides the state to `useAccessibility`.
|
|
656
657
|
- `AchievementGallery` (function): function AchievementGallery({ achievements, title = "Achievements", maskSecrets = true, renderIcon, columns = 2, emptyLabel = "No achievements yet.", className, style, }: AchievementGalleryProps): ReactNode — Achievement/trophy gallery — a responsive grid of cards showing unlocked vs. locked state, a progress bar for counter achievements, and a header summary of completion and score. Secret+locked entries mask their name/description. Feed it `useAchievements(tracker)`.
|
|
657
658
|
- `AchievementGalleryProps` (interface): interface AchievementGalleryProps — Props for {@link AchievementGallery}.
|
|
658
659
|
- `AchievementToast` (function): function AchievementToast({ name, description, icon, points, heading = "Achievement Unlocked", className, style, }: AchievementToastProps): ReactNode — The unlock-moment banner — icon, "Achievement Unlocked" heading, name, and optional points. Purely presentational; pair the game's `onUnlock` seam with a toast queue and render one of these per entry.
|
|
@@ -703,7 +704,9 @@
|
|
|
703
704
|
- `Codex` (function): function Codex({ entries, title = "Codex", columns = 3, maskSecrets = true, renderIcon, onSelect, emptyLabel = "Nothing discovered yet.", className, style, }: CodexProps): ReactNode — Codex / bestiary gallery — category tabs, a responsive grid of entry cards (discovered vs. locked, secret entries masked until found), and a header summarizing completion. Feed it `codex.list()`; wire `onSelect` to a detail pane.
|
|
704
705
|
- `CodexProps` (interface): interface CodexProps — Props for {@link Codex}.
|
|
705
706
|
- `Coins` (function): function Coins({ currencyId, icon = "🪙", style, className, }: { currencyId: string; icon?: ReactNode; style?: CSSProperties; className?: string; }): React.JSX.Element — A currency counter — an icon (emoji/char, default a coin) plus the live amount for `currencyId`. *
|
|
706
|
-
- `ColorblindFilters` (function): function ColorblindFilters(): ReactNode — Hidden SVG `<defs>` holding the `feColorMatrix` colorblind filters that `
|
|
707
|
+
- `ColorblindFilters` (function): function ColorblindFilters(): ReactNode — Hidden SVG `<defs>` holding the `feColorMatrix` colorblind filters that `filter: url(#jg-cb-<mode>)` references. Mounted by {@link ColorblindFrame}; export it standalone for a root that applies the filter itself.
|
|
708
|
+
- `ColorblindFrame` (function): function ColorblindFrame({ mode = "none", children, className, style }: ColorblindFrameProps): ReactNode — Apply the player's colour-vision mode to one rendered frame: a box carrying `filter: url(#jg-cb-<mode>)` plus the filter defs it references. Put it around the canvas of a 3D game and nothing else — the filter is one compositor pass over that box's pixels, so HUD, menu and caption text outside it keep the contrast and hues the rest of the UI assumes.
|
|
709
|
+
- `ColorblindFrameProps` (interface): interface ColorblindFrameProps — Props for {@link ColorblindFrame}.
|
|
707
710
|
- `ComboMeterHud` (function): function ComboMeterHud({ meter, tierColors, theme, animate = true, hideWhenEmpty = true, barWidth = 220, className, style, }: ComboMeterHudProps): ReactNode — A drop-in HUD for a core {@link ComboMeter}: the big live combo count ("×12"), the current free-string tier label, the derived score multiplier, and a draining window bar (reusing the shared atomic bar) that empties as the decay window runs out and re-fills on every hit. The active tier's color — supplied by the game via `tierColors`, keyed on the free-string tier id the model never interprets — drives the count glow, the label, and the bar fill, so a game reskins the escalation ("good" → "great" → "savage") without the engine hardcoding any tier meaning. Presentation only: all counting, timing, tiers, and the multiplier live in the core model.
|
|
708
711
|
- `ComboMeterHudProps` (interface): interface ComboMeterHudProps — Props for {@link ComboMeterHud}.
|
|
709
712
|
- `ComboMeterTheme` (interface): interface ComboMeterTheme — Reskin tokens for {@link ComboMeterHud}.
|
|
@@ -1067,6 +1070,7 @@
|
|
|
1067
1070
|
- `useChat` (function): function useChat(channelId: string, options?: { limit?: number }): ChatMessage[] — ⚠ undocumented
|
|
1068
1071
|
- `useChatBubbles` (function): function useChatBubbles(options?: ChatBubblesOptions): readonly ChatBubble[] — ⚠ undocumented
|
|
1069
1072
|
- `useCoachMarks` (function): function useCoachMarks(sequence: CoachMarkSequence): CoachMarkView | null — Subscribe to a coach-mark sequence and re-render on every change, returning the current step view (or `null` when the tour is complete or waiting on a gate).
|
|
1073
|
+
- `useColorblindMode` (function): function useColorblindMode(store: SettingsStore): ColorblindMode — The live colour-vision mode from a settings store — the same store the settings rows write, so the frame follows the player's choice the moment they pick it. Pass the store the shell owns; read `useAccessibilitySettings().colorblind` instead when a `SettingsProvider` is above the caller.
|
|
1070
1074
|
- `useComboMeter` (function): function useComboMeter(meter: ComboMeter, animate = true): ComboMeterView — Subscribe to a combo meter and re-render it each animation frame so the decay window bar drains live, returning the pooled view to draw right now. Reading the view each frame advances the meter's clock and fires window expiry, so a combo that times out visibly drops even with no further hits. The returned object is the model's pooled view — read its fields immediately, do not retain it.
|
|
1071
1075
|
- `useCurrency` (function): function useCurrency(currencyId: string): number — ⚠ undocumented
|
|
1072
1076
|
- `useDamageDirection` (function): function useDamageDirection(tracker: DamageDirectionTracker): readonly DamageIndicator[] — Subscribe to a damage-direction tracker and animate. Because indicators fade continuously on a clock (not just on discrete events), this drives a `requestAnimationFrame` loop while any indicator is live and stops when the screen is clear, then wakes again on the next hit. Returns the tracker's current `active()` array (reused between frames — copy if you retain it).
|
|
@@ -1110,7 +1114,7 @@
|
|
|
1110
1114
|
- `useLocalPlayerDead` (function): function useLocalPlayerDead(healthStatId = "health"): boolean — ⚠ undocumented
|
|
1111
1115
|
- `useLocale` (function): function useLocale(): Locale — Subscribe to the active locale — re-renders on `setLocale`.
|
|
1112
1116
|
- `useMarkers` (function): function useMarkers<TMeta = unknown>(markers: MarkerSet<TMeta>): readonly MapMarker<TMeta>[] — Subscribe to a native marker set or external marker source, or read a static marker array.
|
|
1113
|
-
- `useMenuNav` (function): function useMenuNav({ ids, columns = 1, wrap = true, disabled, initialId, enabled = true, onConfirm, onBack, gamepad = true, repeat, }: UseMenuNavOptions): MenuNavController — Bind a menu's focus order to the DOM: arrow keys, WASD, Home/End and a standard gamepad move a roving focus, Enter/Space/A confirm, Escape/Backspace/B/Start go back. Tab is left to the browser, so a menu never traps focus, and typing in a text field is never intercepted. The menu is pure state + listeners — it unmounts with the screen, so it costs nothing during play.
|
|
1117
|
+
- `useMenuNav` (function): function useMenuNav({ ids, columns = 1, wrap = true, disabled, initialId, enabled = true, onConfirm, onBack, onAdjust, gamepad = true, repeat, }: UseMenuNavOptions): MenuNavController — Bind a menu's focus order to the DOM: arrow keys, WASD, Home/End and a standard gamepad move a roving focus, Enter/Space/A confirm, Escape/Backspace/B/Start go back. Tab is left to the browser, so a menu never traps focus, and typing in a text field is never intercepted. The menu is pure state + listeners — it unmounts with the screen, so it costs nothing during play.
|
|
1114
1118
|
- `useModalStack` (function): function useModalStack(stack: ModalStack): ModalStackView — Subscribe to a modal stack and re-render on every change, returning the active modal and depth.
|
|
1115
1119
|
- `useNearestWorldItem` (function): function useNearestWorldItem(radius: number): WorldItemRecord | null — Nearest ground item within `radius` of the local player — drives a pickup prompt/highlight.
|
|
1116
1120
|
- `useNotifications` (function): function useNotifications<TMeta = unknown>(store: NotificationStore<TMeta>): readonly NotificationEntry<TMeta>[] — Subscribe to a notification store's live list.
|
|
@@ -1161,9 +1165,12 @@
|
|
|
1161
1165
|
|
|
1162
1166
|
## @jgengine/react/accessibility
|
|
1163
1167
|
|
|
1164
|
-
- `AccessibilityProvider` (function): function AccessibilityProvider({ store, children, className, style, }: { store: AccessibilityStore; children: ReactNode; className?: string; style?: CSSProperties; }): ReactNode — Apply accessibility preferences to a subtree: exposes `--jg-text-scale` for text to scale off, sets `data-reduced-motion` / `data-high-contrast` / `data-colorblind` / `data-captions` for CSS to respond to
|
|
1165
|
-
- `ColorblindFilters` (function): function ColorblindFilters(): ReactNode — Hidden SVG `<defs>` holding the `feColorMatrix` colorblind filters that `
|
|
1168
|
+
- `AccessibilityProvider` (function): function AccessibilityProvider({ store, children, className, style, }: { store: AccessibilityStore; children: ReactNode; className?: string; style?: CSSProperties; }): ReactNode — Apply accessibility preferences to a subtree: exposes `--jg-text-scale` for text to scale off, and sets `data-reduced-motion` / `data-high-contrast` / `data-colorblind` / `data-captions` for CSS to respond to. Reads a `createAccessibilityStore` and re-renders on change; provides the state to `useAccessibility`.
|
|
1169
|
+
- `ColorblindFilters` (function): function ColorblindFilters(): ReactNode — Hidden SVG `<defs>` holding the `feColorMatrix` colorblind filters that `filter: url(#jg-cb-<mode>)` references. Mounted by {@link ColorblindFrame}; export it standalone for a root that applies the filter itself.
|
|
1170
|
+
- `ColorblindFrame` (function): function ColorblindFrame({ mode = "none", children, className, style }: ColorblindFrameProps): ReactNode — Apply the player's colour-vision mode to one rendered frame: a box carrying `filter: url(#jg-cb-<mode>)` plus the filter defs it references. Put it around the canvas of a 3D game and nothing else — the filter is one compositor pass over that box's pixels, so HUD, menu and caption text outside it keep the contrast and hues the rest of the UI assumes.
|
|
1171
|
+
- `ColorblindFrameProps` (interface): interface ColorblindFrameProps — Props for {@link ColorblindFrame}.
|
|
1166
1172
|
- `useAccessibility` (function): function useAccessibility(): AccessibilityContextValue — Read the live accessibility state + its store; throws without an {@link AccessibilityProvider} above.
|
|
1173
|
+
- `useColorblindMode` (function): function useColorblindMode(store: SettingsStore): ColorblindMode — The live colour-vision mode from a settings store — the same store the settings rows write, so the frame follows the player's choice the moment they pick it. Pass the store the shell owns; read `useAccessibilitySettings().colorblind` instead when a `SettingsProvider` is above the caller.
|
|
1167
1174
|
- `usePrefersReducedMotion` (function): function usePrefersReducedMotion(): boolean — OS "reduce motion" preference (`prefers-reduced-motion: reduce`) as a live boolean — SSR-safe (defaults false). Use it to seed an accessibility store's `reducedMotion` default.
|
|
1168
1175
|
- `useReducedMotion` (function): function useReducedMotion(): boolean — True when motion should be skipped: the OS `prefers-reduced-motion` preference, or the nearest {@link AccessibilityProvider}'s `reducedMotion` setting. Works without a provider.
|
|
1169
1176
|
|
|
@@ -1746,7 +1753,7 @@
|
|
|
1746
1753
|
- `MenuNavItemProps` (interface): interface MenuNavItemProps — Props a menu item needs so {@link useMenuNav} can focus it and track pointer/focus.
|
|
1747
1754
|
- `MenuNavRender` (type): type MenuNavRender = (nav: MenuNavController) => ReactNode — Render prop for a menu whose items come from {@link useMenuNav}.
|
|
1748
1755
|
- `UseMenuNavOptions` (interface): interface UseMenuNavOptions — How {@link useMenuNav} is wired.
|
|
1749
|
-
- `useMenuNav` (function): function useMenuNav({ ids, columns = 1, wrap = true, disabled, initialId, enabled = true, onConfirm, onBack, gamepad = true, repeat, }: UseMenuNavOptions): MenuNavController — Bind a menu's focus order to the DOM: arrow keys, WASD, Home/End and a standard gamepad move a roving focus, Enter/Space/A confirm, Escape/Backspace/B/Start go back. Tab is left to the browser, so a menu never traps focus, and typing in a text field is never intercepted. The menu is pure state + listeners — it unmounts with the screen, so it costs nothing during play.
|
|
1756
|
+
- `useMenuNav` (function): function useMenuNav({ ids, columns = 1, wrap = true, disabled, initialId, enabled = true, onConfirm, onBack, onAdjust, gamepad = true, repeat, }: UseMenuNavOptions): MenuNavController — Bind a menu's focus order to the DOM: arrow keys, WASD, Home/End and a standard gamepad move a roving focus, Enter/Space/A confirm, Escape/Backspace/B/Start go back. Tab is left to the browser, so a menu never traps focus, and typing in a text field is never intercepted. The menu is pure state + listeners — it unmounts with the screen, so it costs nothing during play.
|
|
1750
1757
|
|
|
1751
1758
|
## @jgengine/react/menuRouter
|
|
1752
1759
|
|
|
@@ -1921,16 +1928,17 @@
|
|
|
1921
1928
|
|
|
1922
1929
|
## @jgengine/react/settingsBlocks
|
|
1923
1930
|
|
|
1924
|
-
- `SettingsKeybindControl` (function): function SettingsKeybindControl({ row, armed = false, onArm, buttonProps }: SettingsKeybindControlProps): ReactNode — A controls row for one action: its label, the bound key (click to rebind), a conflict/armed state and reset. The caller owns capture; this is the row's presentation.
|
|
1931
|
+
- `SettingsKeybindControl` (function): function SettingsKeybindControl({ row, armed = false, onArm, buttonProps, rowProps }: SettingsKeybindControlProps): ReactNode — A controls row for one action: its label, the bound key (click to rebind), a conflict/armed state and reset. The caller owns capture; this is the row's presentation.
|
|
1925
1932
|
- `SettingsKeybindControlProps` (interface): interface SettingsKeybindControlProps — One rebindable action row: current key glyph, armed-capture state and a reset button.
|
|
1926
|
-
- `SettingsRowControl` (function): function SettingsRowControl({ row }: { row: SettingsRow }): ReactNode — Render the control a {@link SettingsRow} kind asks for.
|
|
1927
|
-
- `SettingsSection` (function): function SettingsSection({ category, heading = true, capture = true, className, style, onCaptureChange, }: SettingsSectionProps): ReactNode — One settings category as a block: its rows, then its rebindable actions with click-to-capture. The game picks where it sits and how wide it is; the block reads HudTheme tokens and the live rows the settings controller hands it, and writes every change straight back to that controller's store.
|
|
1933
|
+
- `SettingsRowControl` (function): function SettingsRowControl({ row, itemProps }: { row: SettingsRow; itemProps?: MenuNavItemProps }): ReactNode — Render the control a {@link SettingsRow} kind asks for. `itemProps` carries `useMenuNav` focus tracking from {@link SettingsSection}.
|
|
1934
|
+
- `SettingsSection` (function): function SettingsSection({ category, heading = true, capture = true, enabled = true, initialId, className, style, onCaptureChange, }: SettingsSectionProps): ReactNode — One settings category as a block: its rows, then its rebindable actions with click-to-capture. The game picks where it sits and how wide it is; the block reads HudTheme tokens and the live rows the settings controller hands it, and writes every change straight back to that controller's store.
|
|
1928
1935
|
- `SettingsSectionProps` (interface): interface SettingsSectionProps — How a {@link SettingsSection} handles a rebind capture.
|
|
1929
|
-
- `SettingsSelect` (function): function SettingsSelect({ row }: { row: SettingsRow }): ReactNode — One choice row from a {@link SettingsRow}, rendered as a pressed-state option group.
|
|
1930
|
-
- `SettingsSlider` (function): function SettingsSlider({ row }: { row: SettingsRow }): ReactNode — One slider row from a {@link SettingsRow}: mirrors the thumb locally and commits on release, so a drag never storms the settings store or the graphics rebuild it drives.
|
|
1936
|
+
- `SettingsSelect` (function): function SettingsSelect({ row, itemProps }: { row: SettingsRow; itemProps?: MenuNavItemProps }): ReactNode — One choice row from a {@link SettingsRow}, rendered as a pressed-state option group.
|
|
1937
|
+
- `SettingsSlider` (function): function SettingsSlider({ row, itemProps }: { row: SettingsRow; itemProps?: MenuNavItemProps }): ReactNode — One slider row from a {@link SettingsRow}: mirrors the thumb locally and commits on release, so a drag never storms the settings store or the graphics rebuild it drives.
|
|
1931
1938
|
- `SettingsTabs` (function): function SettingsTabs({ categories, activeId, onSelect, leading, tabProps, className, style }: SettingsTabsProps): ReactNode — The category tab rail for a settings screen: a scrollable row of buttons with pressed state. The game owns the surrounding layout; this block keeps every category reachable on narrow screens.
|
|
1932
1939
|
- `SettingsTabsProps` (interface): interface SettingsTabsProps — The live category tabs for a settings screen: a scrollable rail with pressed state.
|
|
1933
|
-
- `SettingsToggle` (function): function SettingsToggle({ row }: { row: SettingsRow }): ReactNode — One on/off row from a {@link SettingsRow}.
|
|
1940
|
+
- `SettingsToggle` (function): function SettingsToggle({ row, itemProps }: { row: SettingsRow; itemProps?: MenuNavItemProps }): ReactNode — One on/off row from a {@link SettingsRow}.
|
|
1941
|
+
- `adjustSettingRow` (function): function adjustSettingRow(row: SettingsRow, delta: -1 | 1): SettingValue | undefined — One adjustment of a settings row: a slider moves by its step inside its bounds, a toggle flips, a select moves one option (wrapping at both ends). Returns the value to write, or `undefined` when the kind has nothing to step. Shared by the keyboard and the gamepad so both change a row the same way.
|
|
1934
1942
|
|
|
1935
1943
|
## @jgengine/react/shopGrid
|
|
1936
1944
|
|
|
@@ -3654,9 +3662,9 @@
|
|
|
3654
3662
|
- `ProjectileTracers` (function): function ProjectileTracers({ lifeMs = 130 }: { lifeMs?: number }): React.JSX.Element — ⚠ undocumented
|
|
3655
3663
|
- `Reticle` (function): function Reticle({ className }: { className?: string }): React.JSX.Element — ⚠ undocumented
|
|
3656
3664
|
- `WorldBarSample` (interface): interface WorldBarSample { entityId?: string; x: number; y: number; percent: number; pxPerMeter?: number } — ⚠ undocumented
|
|
3657
|
-
- `WorldEntityBars` (function): function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, maxDistance
|
|
3665
|
+
- `WorldEntityBars` (function): function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, maxDistance, occlude = true, tickMs = 80, maxSamples = 64, }: { statId: string; height?: number; roles?: readonly CatalogEntityRole[]; resolveRole?: (entity: SceneEntity) => CatalogEntityRole | undefined; /** Hide bars beyond this … — ⚠ undocumented
|
|
3658
3666
|
- `WorldFloatText` (function): function WorldFloatText({ height = 1.9, lifeMs = 950 }: { height?: number; lifeMs?: number }): React.JSX.Element — ⚠ undocumented
|
|
3659
|
-
- `WorldNameplates` (function): function WorldNameplates({ statId = "health", height = 2.3, roles, resolveRole, maxDistance
|
|
3667
|
+
- `WorldNameplates` (function): function WorldNameplates({ statId = "health", height = 2.3, roles, resolveRole, maxDistance, occlude = true, tickMs = 120, maxSamples = 64, resolveName, showHealth = true, className, nameplateClassName, nameClassName, barClassName, fillClassName, renderNameplate, }: WorldNameplatesProps): React.JSX.… — Billboarded name + 78×6px HP bar over every nearby non-local entity that passes `roles`/`maxDistance` — headless (className/data-* slots on every part, `renderNameplate` for a full swap), turned on declaratively via `defineGame({ nameplates })` rather than mounted by hand.
|
|
3660
3668
|
- `WorldNameplatesProps` (interface): interface WorldNameplatesProps — Props for `WorldNameplates` — entity filter, refresh rate, and headless className/render hooks.
|
|
3661
3669
|
- `WorldObjectHighlights` (function): function WorldObjectHighlights({ color = "#facc15", radius, y = 0.05 }: WorldObjectHighlightsProps): React.JSX.Element — Ground ring over every `ctx.scene.object.selection`-ed placed object — the object-layer counterpart to `WorldEntityBars`/`WorldNameplates`. Mount it once in the game's scene (headless: no defaults are imposed beyond a visible ring) instead of hand-rolling a selection highlight through `WorldOverlay` against external state.
|
|
3662
3670
|
- `WorldObjectHighlightsProps` (interface): interface WorldObjectHighlightsProps — Props for {@link WorldObjectHighlights}.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Reach for these before hand-rolling. Each row is *the thing you need* → *the primitive that already does it*.
|
|
6
6
|
|
|
7
|
-
## accessibility-provider — apply accessibility preferences to a subtree — text-scale CSS var
|
|
7
|
+
## accessibility-provider — apply accessibility preferences to a subtree — text-scale CSS var and reduced-motion/high-contrast/colorblind/captions data attrs
|
|
8
8
|
|
|
9
9
|
- `AccessibilityProvider` (function) · `import { AccessibilityProvider } from "@jgengine/react"`
|
|
10
10
|
|
|
@@ -52,6 +52,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
52
52
|
|
|
53
53
|
- `ActionTooltip` (function) · `import { ActionTooltip } from "@jgengine/react"`
|
|
54
54
|
|
|
55
|
+
## adjust-setting-row — step a settings row value once, for keyboard and gamepad alike
|
|
56
|
+
|
|
57
|
+
- `adjustSettingRow` (function) · `import { adjustSettingRow } from "@jgengine/react/settingsBlocks"`
|
|
58
|
+
|
|
55
59
|
## animation — turn a killed rigged character into a physics ragdoll on death
|
|
56
60
|
|
|
57
61
|
- `useRagdoll` (function) · `import { useRagdoll } from "@jgengine/shell/render/useRagdoll"`
|
|
@@ -159,6 +163,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
159
163
|
|
|
160
164
|
- `ColorblindFilters` (function) · `import { ColorblindFilters } from "@jgengine/react"`
|
|
161
165
|
|
|
166
|
+
## colorblind-frame — apply the player's colour-vision mode to the 3D frame alone, leaving HUD and menu text unfiltered
|
|
167
|
+
|
|
168
|
+
- `ColorblindFrame` (function) · `import { ColorblindFrame } from "@jgengine/react"`
|
|
169
|
+
|
|
170
|
+
## colorblind-frame-filter — resolve a colorblind mode to the CSS filter for the 3D frame alone
|
|
171
|
+
|
|
172
|
+
- `colorblindFrameFilter` (function) · `import { colorblindFrameFilter } from "@jgengine/core/ui/accessibility"`
|
|
173
|
+
|
|
174
|
+
## colorblind-frame-mode — read the live colour-vision mode from the settings store that the settings rows write
|
|
175
|
+
|
|
176
|
+
- `useColorblindMode` (function) · `import { useColorblindMode } from "@jgengine/react"`
|
|
177
|
+
|
|
162
178
|
## combo-meter-hud — drop-in HUD for a core combo meter — big live count, free-string tier label, derived multiplier, and a draining decay-window bar, per-tier colored from a caller map and HudTheme-token skinnable
|
|
163
179
|
|
|
164
180
|
- `ComboMeterHud` (function) · `import { ComboMeterHud } from "@jgengine/react"`
|
|
@@ -758,7 +774,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
758
774
|
|
|
759
775
|
- `SettingsKeybindControl` (function) · `import { SettingsKeybindControl } from "@jgengine/react/settingsBlocks"`
|
|
760
776
|
|
|
761
|
-
## settings-section — settings category block — rows plus click-to-capture keybind rows, HudTheme-styled, store-backed
|
|
777
|
+
## settings-section — settings category block — rows plus click-to-capture keybind rows, HudTheme-styled, store-backed, keyboard and gamepad navigable
|
|
762
778
|
|
|
763
779
|
- `SettingsSection` (function) · `import { SettingsSection } from "@jgengine/react/settingsBlocks"`
|
|
764
780
|
|
|
@@ -279,9 +279,9 @@ HUD-only games (`presentation: "hud"` or a camera with rig `"none"`) use the sam
|
|
|
279
279
|
|
|
280
280
|
**Present it any way you want.** `useSettings()` (`@jgengine/react`) returns the live controller — `{ categories, actions, variant, surface, isOpen, open, close, setOpen }` — so a game can drive its own pause-menu button, or render `categories`/`actions` (rows carry `value`/`set`/bounds, keybinds carry `rebind`/`reset`) entirely inside its own HUD. `useHasSettings()` gates a custom entry; `useSetting(id, fallback)` reads/writes one value. Set a slider's `min`/`max` explicitly — an omitted range collapses the thumb to 0/1.
|
|
281
281
|
|
|
282
|
-
**Composing your own settings layout.** `SettingsSection` (`@jgengine/react/settingsBlocks`) renders one controller category
|
|
282
|
+
**Composing your own settings layout.** `SettingsSection` (`@jgengine/react/settingsBlocks`) renders one controller category - its rows, then its click-to-capture keybind rows - as one `useMenuNav` menu: the d-pad and left stick move between rows, left/right step the focused value (slider by its step, toggle flipped, select to the next option), and confirm arms a keybind. Arrow keys take the same `onAdjust` path, so a keyboard and a pad cannot diverge. `SettingsTabs` renders the category rail (pass `tabProps` from `useMenuNav` so a gamepad can switch categories). `SettingsRowControl`, `SettingsSlider`, `SettingsToggle`, `SettingsSelect` and `SettingsKeybindControl` are the individual rows if the layout is not a list; pass the `itemProps` they accept so focus tracking still works. All of them read `--jg-*` tokens and write through the controller, so the game's rebinds land in the same keybinding store as the stock menu. The stock menu uses these same blocks, so a game-composed screen and the engine dialog cannot drift.
|
|
283
283
|
|
|
284
|
-
**Accessibility is a shipped category.** The settings menu includes `accessibility` — text size, colourblind palette, reduce motion, high contrast, captions and hold-or-toggle — unless the game hides it. Text size multiplies every `--jg-type-*` token through `--jg-text-scale`, reduce motion zeroes `--jg-motion-scale`, and high contrast/colourblind/captions set `data-*` hooks on the HUD root; read them with `useAccessibilitySettings()` and `useHoldVsToggle()` for a custom layout.
|
|
284
|
+
**Accessibility is a shipped category.** The settings menu includes `accessibility` — text size, colourblind palette, reduce motion, high contrast, captions and hold-or-toggle — unless the game hides it. Text size multiplies every `--jg-type-*` token through `--jg-text-scale`, reduce motion zeroes `--jg-motion-scale`, and high contrast/colourblind/captions set `data-*` hooks on the HUD root; read them with `useAccessibilitySettings()` and `useHoldVsToggle()` for a custom layout. The 3D shell applies the colourblind matrix to its canvas alone with `ColorblindFrame` (`@jgengine/react/accessibility`), so HUD and menu text keep the contrast the rest of the UI assumes. A custom presentation wraps its own canvas in `ColorblindFrame` with `useColorblindMode(store)`; wrapping the HUD or menu root instead recolours the text, which is why `AccessibilityProvider` sets `data-colorblind` and leaves the filtering to the frame.
|
|
285
285
|
|
|
286
286
|
## 8. Motion and game feel
|
|
287
287
|
|
|
@@ -46,6 +46,58 @@
|
|
|
46
46
|
- `planLookahead` (function): function planLookahead<S, M>(root: S, domain: LookaheadDomain<S, M>, profile: DifficultyProfile, rng: () => number): M | null — Pick the agent's next move by bounded lookahead: a depth-limited negamax (or single-perspective sequence search when `adversarial: false`) over the caller's {@link LookaheadDomain}, exploring at most `profile.planWidth` moves per ply to `profile.planDepth` plies, with the final root choice run through {@link pickScored} so `decisionNoise` still injects tier-appropriate blunders. Depth `1` is greedy — an easy profile literally cannot see the fork coming; an expert profile finds multi-ply tactics. Work is bounded by `planWidth ** planDepth` domain calls; run it at decision cadence (turns, ability windows, order re-evaluation), not per frame.
|
|
47
47
|
- `shouldUseAbility` (function): function shouldUseAbility(opportunity: number, profile: DifficultyProfile, rng: () => number): boolean — Should the agent spend an ability now? `opportunity` is the caller-scored quality of this moment in `0..1` (targets clumped for the AoE, player mid-animation, burn phase). The perceived opportunity is fuzzed by `decisionNoise` and compared against `abilityDiscipline`: an easy profile (low discipline, high noise) wastes cooldowns on weak moments and sometimes sits on a perfect window; an expert profile fires exactly on strong windows. One rng draw when noisy.
|
|
48
48
|
|
|
49
|
+
## @jgengine/core/ai/director/boss
|
|
50
|
+
|
|
51
|
+
- `BossBehaviour` (type): type BossBehaviour = Omit<SquadRole, "id" | "count"> — The role shape a boss fights with in one phase: the same dials a squad role uses.
|
|
52
|
+
- `BossPhase` (interface): interface BossPhase — One authored phase: the health it starts at, how the boss fights, and its tell.
|
|
53
|
+
- `BossState` (interface): interface BossState — Serializable boss state: which phase holds, how long it has held, and the tell still on screen.
|
|
54
|
+
- `BossStep` (interface): interface BossStep — One step of the boss clock.
|
|
55
|
+
- `BossTell` (interface): interface BossTell — The warning a boss gives while it changes phase, and how long the player has to read it.
|
|
56
|
+
- `advanceBoss` (function): function advanceBoss(phases: readonly BossPhase[], state: BossState, health: number, dt: number): BossStep — Advance the boss by `dt` at a health fraction and return the phase in effect.
|
|
57
|
+
- `bossPhase` (function): function bossPhase(phases: readonly BossPhase[], health: number): BossPhase — The phase a boss fights with at a health fraction. Pure read for a HUD; never mutates.
|
|
58
|
+
- `createBossState` (function): function createBossState(phases: readonly BossPhase[]): BossState — Fresh boss state for a phase list, opening in phase 0 with no tell showing.
|
|
59
|
+
|
|
60
|
+
## @jgengine/core/ai/director/encounter
|
|
61
|
+
|
|
62
|
+
- `Encounter` (interface): interface Encounter — Live encounter: opens waves, emits timed spawn orders, and round-trips through a save.
|
|
63
|
+
- `EncounterConfig` (interface): interface EncounterConfig — Construction options for {@link createEncounter}.
|
|
64
|
+
- `EncounterDefinition` (interface): interface EncounterDefinition — A whole encounter, authored as data instead of spawned in game code.
|
|
65
|
+
- `EncounterGroup` (interface): interface EncounterGroup — One authored group inside a wave: what arrives, how many, where from, and when. `unit` and `entry` are game-owned ids — the engine never resolves them.
|
|
66
|
+
- `EncounterOrder` (interface): interface EncounterOrder — One unit the director says should enter now.
|
|
67
|
+
- `EncounterPressure` (interface): interface EncounterPressure — The subset of a pressure plan the encounter layer applies.
|
|
68
|
+
- `EncounterState` (interface): interface EncounterState — Serializable encounter state.
|
|
69
|
+
- `EncounterView` (interface): interface EncounterView — Pooled per-frame readout of where the encounter stands.
|
|
70
|
+
- `EncounterWave` (interface): interface EncounterWave — One authored wave.
|
|
71
|
+
- `createEncounter` (function): function createEncounter(config: EncounterConfig): Encounter — Create a live encounter over an authored definition: open a wave, step its clock, take the orders whose time has come, and close the wave when the game says it is cleared.
|
|
72
|
+
- `expandWave` (function): function expandWave(config: EncounterConfig, waveIndex: number, pressure: EncounterPressure = {}): EncounterOrder[] — Expand one authored wave into the units it sends, at the times it sends them.
|
|
73
|
+
|
|
74
|
+
## @jgengine/core/ai/director/pressure
|
|
75
|
+
|
|
76
|
+
- `PressurePlan` (interface): interface PressurePlan — One bounded answer to "how hard now", plus the effects the encounter layer applies.
|
|
77
|
+
- `PressureReading` (interface): interface PressureReading — Encounter pressure: how hard a director leans on a player who is struggling, read from the player's own state, quantized to a tier, and bounded by the rule's cap. The plan is a value, not a callback, so a test can sweep it and a probe can print it.
|
|
78
|
+
- `PressureRule` (interface): interface PressureRule — The weights and window that turn a reading into an index, plus the hard cap.
|
|
79
|
+
- `evaluatePressure` (function): function evaluatePressure(reading: PressureReading, rule: PressureRule = {}): PressurePlan — Turn one reading into a bounded pressure plan.
|
|
80
|
+
|
|
81
|
+
## @jgengine/core/ai/director/roles
|
|
82
|
+
|
|
83
|
+
- `FightIntent` (type): type FightIntent = "retreat" | "hold" | "flank" | "cover" | "assault" — The five duties a fighter can be given in a fight. A duty is *who does what*; the squad role a game dials (range, angles, fire discipline) is *how that duty fights* and stays content.
|
|
84
|
+
- `FightOrders` (interface): interface FightOrders — What the director decided, per fighter, and why.
|
|
85
|
+
- `FightSituation` (interface): interface FightSituation — The squad as it stands and the enemy it is facing, or no enemy at all.
|
|
86
|
+
- `FighterState` (interface): interface FighterState — One fighter the director can order, with the state the decision reads.
|
|
87
|
+
- `assignFightIntents` (function): function assignFightIntents(situation: FightSituation): FightOrders — Assign each fighter a duty from the squad's own state — who is hurt, who is under fire, and who already stands on the outside of the formation — never from an authored order or a spawn index.
|
|
88
|
+
- `compositionForIntents` (function): function compositionForIntents(order: readonly string[], intents: ReadonlyMap<string, FightIntent>, roleFor: (intent: FightIntent) => SquadRole): SquadComposition — Turn duties into a squad composition `createSquadCombat` accepts: one slot per fighter, in the order the game gives its members, each mapped to the role that game dials for the duty. Build it through `composition` so squad combat executes the director's assignment instead of its own static list.
|
|
89
|
+
|
|
90
|
+
## @jgengine/core/ai/director/sound
|
|
91
|
+
|
|
92
|
+
- `Investigation` (interface): interface Investigation — A hunter still working a noise, as the squad layer reads it this tick.
|
|
93
|
+
- `NoiseEvent` (interface): interface NoiseEvent — One noise in the world, as the game produces it.
|
|
94
|
+
- `SoundHunt` (interface): interface SoundHunt — Sound hunting: raise a noise, get the bounded set of enemies that go and look.
|
|
95
|
+
- `SoundHuntConfig` (interface): interface SoundHuntConfig — Everything the hunt reads and its bounds.
|
|
96
|
+
- `SoundHuntEntry` (interface): interface SoundHuntEntry — One noise and the hunters sent to it. Serializable, so a hunt survives a save.
|
|
97
|
+
- `SoundHuntSnapshot` (interface): interface SoundHuntSnapshot — Serializable book-keeping: which noises are still being worked, and by whom.
|
|
98
|
+
- `SoundHunter` (interface): interface SoundHunter — A unit that could hear a noise: its id and where it stands when the noise lands.
|
|
99
|
+
- `createSoundHunt` (function): function createSoundHunt(config: SoundHuntConfig): SoundHunt — Make enemies investigate a noise, reusing the hearing owner.
|
|
100
|
+
|
|
49
101
|
## @jgengine/core/ai/driver
|
|
50
102
|
|
|
51
103
|
- `DrivePathPoint` (type): type DrivePathPoint = readonly [number, number] — A 2D polyline point (`[x, z]`), structurally compatible with authored road path centerlines.
|
|
@@ -192,6 +192,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
192
192
|
|
|
193
193
|
- `createBodyBind` (function) · `import { createBodyBind } from "@jgengine/core/world"`
|
|
194
194
|
|
|
195
|
+
## boss-phase-state — fresh serializable boss phase state that opens in the first phase
|
|
196
|
+
|
|
197
|
+
- `createBossState` (function) · `import { createBossState } from "@jgengine/core/ai/director/boss"`
|
|
198
|
+
|
|
199
|
+
## boss-phases — data-authored boss phases that change behaviour and fire a tell at health thresholds
|
|
200
|
+
|
|
201
|
+
- `advanceBoss` (function) · `import { advanceBoss } from "@jgengine/core/ai/director/boss"`
|
|
202
|
+
|
|
195
203
|
## bounded-grid-route — distinguish no path from exhausted grid search work
|
|
196
204
|
|
|
197
205
|
- `findPathResult` (function) · `import { findPathResult } from "@jgengine/core/nav/navGrid"`
|
|
@@ -334,6 +342,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
334
342
|
|
|
335
343
|
- `summarizeElevation` (function) · `import { summarizeElevation } from "@jgengine/core/world"`
|
|
336
344
|
|
|
345
|
+
## encounter-expand — turn an authored wave into a deterministic, pressure-adjusted list of timed spawn orders
|
|
346
|
+
|
|
347
|
+
- `expandWave` (function) · `import { expandWave } from "@jgengine/core/ai/director/encounter"`
|
|
348
|
+
|
|
349
|
+
## encounter-pressure — bounded difficulty tier read from player health, encounter progress and time
|
|
350
|
+
|
|
351
|
+
- `evaluatePressure` (function) · `import { evaluatePressure } from "@jgengine/core/ai/director/pressure"`
|
|
352
|
+
|
|
353
|
+
## encounter-runner — open authored waves, emit their timed orders, and track wave progress through a save
|
|
354
|
+
|
|
355
|
+
- `createEncounter` (function) · `import { createEncounter } from "@jgengine/core/ai/director/encounter"`
|
|
356
|
+
|
|
337
357
|
## engine-layers — layered engine audio: rpm-keyed samples crossfaded, with on-load and off-load sets picked by engine load
|
|
338
358
|
|
|
339
359
|
- `createEngineLayers` (function) · `import { createEngineLayers } from "@jgengine/core/audio/engineLayers"`
|
|
@@ -354,6 +374,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
354
374
|
|
|
355
375
|
- `createFeedbackMixer` (function) · `import { createFeedbackMixer } from "@jgengine/core/vfx/feedbackMixer"`
|
|
356
376
|
|
|
377
|
+
## fight-intent-composition — map assigned duties onto a squad combat composition in member order
|
|
378
|
+
|
|
379
|
+
- `compositionForIntents` (function) · `import { compositionForIntents } from "@jgengine/core/ai/director/roles"`
|
|
380
|
+
|
|
381
|
+
## fight-intents — assign hold, flank, cover, assault and retreat duties from live squad state
|
|
382
|
+
|
|
383
|
+
- `assignFightIntents` (function) · `import { assignFightIntents } from "@jgengine/core/ai/director/roles"`
|
|
384
|
+
|
|
357
385
|
## find-effect — look up one named particle effect in the shared library, or undefined when unknown
|
|
358
386
|
|
|
359
387
|
- `findEffect` (function) · `import { findEffect } from "@jgengine/core/vfx/effectLibrary"`
|
|
@@ -812,6 +840,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
812
840
|
|
|
813
841
|
- `planSolidRoute` (function) · `import { planSolidRoute } from "@jgengine/core/nav/solidRoute"`
|
|
814
842
|
|
|
843
|
+
## sound-hunting — send a bounded set of nearby enemies to investigate a noise with decaying memory
|
|
844
|
+
|
|
845
|
+
- `createSoundHunt` (function) · `import { createSoundHunt } from "@jgengine/core/ai/director/sound"`
|
|
846
|
+
|
|
815
847
|
## spatial-incremental — Update one entity's spatial cell after a committed write without scanning the population
|
|
816
848
|
|
|
817
849
|
- `createSpatialApi` (function) · `import { createSpatialApi } from "@jgengine/core/scene/spatial"`
|