@jgengine/shell 0.29.0 → 0.31.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 +53 -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 +3 -3
- package/skills/jgengine-ui/api.md +51 -16
- package/skills/jgengine-ui/capabilities.md +28 -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,59 @@ 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.31.0
|
|
30
|
+
|
|
31
|
+
### Migrate
|
|
32
|
+
|
|
33
|
+
- **Bump lockstep SDK packages to `^0.31.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.28.0`; `@jgengine/github` is `0.18.0`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `createAgent` (`@jgengine/core/agents/agent`) runs one resident's whole loop on the world clock: needs decay through `sim/needs`, a utility brain picks among the targets the game offers, an authored day plan says where the agent belongs, and the agent falls back to a declared target when its pick disappears. Mood, action and the far flag snapshot and restore with the needs.
|
|
38
|
+
- `createMoodReader` / `readMood` (`@jgengine/core/agents/mood`) derive a 0-100 mood and a caller-labelled tier from weighted need satisfaction, plus named sources a queue, a litter patch or a cold snap raises and clears.
|
|
39
|
+
- `createAgentSchedule` (`@jgengine/core/agents/schedule`) resolves an authored day plan through `sim/routine` and adds a work beat: a stop names the places it patrols, so a janitor is an agent with a route rather than a special case. Off-hours behaviour is a stop without a beat.
|
|
40
|
+
- `createRelationshipBook` (`@jgengine/core/agents/relationships`) keeps undirected familiarity and opinion per pair. A social action moves both and reports the milestones crossed, friends prefer each other's company, and a bonded pair refuses to split. Pair keys come from `relation/keyedValues` and crossings from `relation/thresholds`.
|
|
41
|
+
- `GameSave.deleteSlot(slot)` and `SaveStore.deleteSlot(slot)` remove one slot's payload and drop it from the slot index; they resolve `true` when a document was removed and `false` for an unknown slot. Deleting the active slot resets the live state to the initial document. `load()` no longer lists a slot that holds nothing, so a deleted slot cannot return.
|
|
42
|
+
- `HudPanel` takes `lane` (`@jgengine/react`): blocks naming the same anchor and lane stack without overlap, lanes in one anchor sit side by side, and `resolveHudLanes` resolves the rectangles. A moved panel leaves its lane and keeps its authored placement.
|
|
43
|
+
- `hudBlockOverlaps` and `formatHudOverlapReport` (`@jgengine/react`) name every pair of rendered HUD blocks whose rectangles intersect, with both ids and the viewport; `HudCanvas` warns with that report in a dev build.
|
|
44
|
+
- `rollDrop` (`@jgengine/core/loot/drop`) rolls one deterministic item from a seed and `compareToEquipped` turns it into per-stat deltas against worn gear with a weighted verdict a HUD can render.
|
|
45
|
+
- `createRunHaul` (`@jgengine/core/loot/haul`) gates carried loot by weight through the shared encumbrance rules, banks what the persistent stash fits, and discards the run's haul when the game calls `fail`.
|
|
46
|
+
- `salvageItem` and `createItemLedger` (`@jgengine/core/loot/salvage`) break an item down for the currency its level and rarity say and claim the instance, so a salvaged item cannot be salvaged, equipped or sold again.
|
|
47
|
+
- `stockVendor` and `createVendorRotation` (`@jgengine/core/loot/vendor`) fill a `ShopStock` with rolled items priced through the economy wallet, with a run-count restock rule the game authors.
|
|
48
|
+
- `markerProximity` on `createMissionRuntime` picks the plane a marker proximity test compares in: `"horizontal"` (default) measures ground distance and allows the actor a `tolerance` (default 8 m) above or below the marker, `"spatial"` is the straight 3D check for worlds with stacked platforms. `MissionMarkerPlane`, `MissionMarkerProximity` and `DEFAULT_MARKER_TOLERANCE` are exported with it.
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- `evaluatePressure` (`@jgengine/core/ai/director/pressure`) reads pressure as player control: a healthy player further into a run is leaned on harder, a hurt player is eased off, and every effect stays inside the rule's `maxTier`.
|
|
53
|
+
- Loot bases and affixes take `minLevel`, so `rollLoot` only draws from entries at or below the item level. Loot bases take `mass`; rolled items carry it for carry checks.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- `HudCanvas` resolved its host with `canvas.parentElement`, which is `HudThemeRoot`'s `display: contents` element and measures 0x0 — the boundary overlay, the HUD-fit measurement and the new overlap report never ran under the standard HUD root. It now uses the nearest ancestor with a real box.
|
|
58
|
+
- The HUD boundary check returned early whenever nothing overflowed, so a later report in the same pass could never run.
|
|
59
|
+
- `reach`, `interact` and the `left-area` fail condition compared a marker in 3D, so a marker authored at `y=0` under rising ground was unreachable. They now read the same plane.
|
|
60
|
+
|
|
61
|
+
## 0.30.0
|
|
62
|
+
|
|
63
|
+
### Migrate
|
|
64
|
+
|
|
65
|
+
- **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`.
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
- `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.
|
|
70
|
+
- `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.
|
|
71
|
+
- `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.
|
|
72
|
+
- `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.
|
|
73
|
+
- `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.
|
|
74
|
+
- `createUpgradeTree` (`@jgengine/core/progression/upgrades`) prices each rank from the shared curve primitive and gates purchases on prerequisites and standing.
|
|
75
|
+
|
|
76
|
+
### Changed
|
|
77
|
+
|
|
78
|
+
- `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>)`.
|
|
79
|
+
- 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.
|
|
80
|
+
- 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.
|
|
81
|
+
|
|
29
82
|
## 0.29.0
|
|
30
83
|
|
|
31
84
|
### 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.31.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.31.0",
|
|
39
|
+
"@jgengine/react": "^0.31.0",
|
|
40
|
+
"@jgengine/ws": "^0.31.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.31.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.31.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
289
289
|
|
|
290
290
|
## @jgengine/core/runtime/adapter
|
|
291
291
|
|
|
@@ -728,7 +728,7 @@
|
|
|
728
728
|
- `GameLoop` (interface): interface GameLoop<TContext = unknown> — Lifecycle hooks a game implements to drive init, per-tick simulation, and player join/leave.
|
|
729
729
|
- `Hotbar` (function): function Hotbar({ inventoryId, activeSlot, keys, slotSize = 46, itemIcon, style, className, }: { inventoryId: string; activeSlot?: number; keys?: readonly string[]; slotSize?: number; /** Caller-supplied item id → icon registry; return null/undefined to fall back to the default glyph. */ itemIcon?: … — A numbered hotbar bound to an inventory — painted iconed slots (a `GameIcon` glyph over a school-keyed gradient with a count badge, #1035), an active-slot highlight, and a keycap per slot. Place it and pass the `inventoryId`; `activeSlot` highlights the equipped one. Supply `itemIcon` to map your item ids to your own glyphs — the default resolves a `GameIcon` from the item id.
|
|
730
730
|
- `HudCanvas` (function): function HudCanvas({ layout, editChord, compactScale, showDuring, className, style, children, }: { layout: HudLayoutStore; editChord?: HudEditChord | false; /** Zoom applied to the whole HUD on compact displays. Default 0.85. */ compactScale?: number; /** Opt-in play-phase gate: render the HUD only … — Full-viewport HUD surface. Panels declared with `HudPanel` flow into nine anchor regions and stack automatically with a gap — no per-panel pixel offsets, no manual clearance for sibling panels, the touch-control dock (`--jg-hud-dock-clearance`), or device safe areas. On compact displays the whole surface scales down and each panel applies its `compact` behavior.
|
|
731
|
-
- `HudPanel` (function): function HudPanel({ id, anchor = "top-left", order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, width, height, type, className, style, children, }: { id: string; anchor?: HudAnchor; /**
|
|
731
|
+
- `HudPanel` (function): function HudPanel({ id, anchor = "top-left", lane, order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, width, height, type, className, style, children, }: { id: string; anchor?: HudAnchor; /** … — A HUD block that lives in one of the nine anchor regions. Panels sharing a region stack outward from the screen edge in ascending `order`. On fine pointers panels stay draggable through the edit chord; a dragged panel leaves the flow and keeps its custom placement. On compact displays custom placements are ignored and the `compact` behavior applies.
|
|
732
732
|
- `PlayableGame` (type): type PlayableGame = EnginePlayableGame< ComponentType, ComponentType, RenderEntity, RenderObject, ComponentType<ViewmodelProps>, ComponentType<WorldOverlayProps> > & { /** HUD theme the shell applies at the `GameUI` root. Unset uses `defaultHudTheme`; `false` opts out of the default theme and the en… — The concrete React-bound playable a shell `defineGame` returns and `GameHost` mounts — the one PlayableGame games handle.
|
|
733
733
|
- `StatBar` (function): function StatBar({ statId = "health", entityId, tone = "health", label, showValue = true, width = 200, icon, style, className, }: { statId?: string; entityId?: string; tone?: StatTone; label?: string; showValue?: boolean; width?: number; icon?: ReactNode; style?: CSSProperties; className?: string; }… — A polished stat bar (health/mana/stamina/shield/xp) — a rounded, glassy meter with a tone-colored fill and an optional value readout. Reads `statId` off `entityId` (defaults to the local player). Renders nothing until the stat exists.
|
|
734
734
|
- `StoreHandle` (interface): interface StoreHandle<T> — A typed, cast-free handle onto one slot of the reactive game store (`ctx.game.store`). The single type parameter is fixed at definition; `read`/`write`/`update` never widen to `unknown`, so no call site repeats `store.get(key) as T`. Writes flow through the engine store, so they bump `ctx.version()`, serialize into a {@link WorldSnapshot}, and replay under host authority — the same guarantees a hand-rolled `store.get`/`store.set` pair gives up the moment it forks run state into a module-level singleton.
|
|
@@ -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}.
|
|
@@ -783,6 +786,9 @@
|
|
|
783
786
|
- `HUD_THEME_PRESETS` (const): const HUD_THEME_PRESETS: { "arcane-stone": HudTheme; "survival-wood": HudTheme; "military-flat": HudTheme; "sleek-hex": HudTheme; "elemental-circle": HudTheme; "sandbox-slot": HudTheme; ember: HudTheme; synthwave: HudTheme; fieldkit: HudTheme; } — Built-in theme presets for demos/scaffold only — not product identity; games author their own theme.
|
|
784
787
|
- `HealthBar` (const): const HealthBar: (props: AtomicBarProps) => React.JSX.Element — Player/enemy health — danger-flashes to `--jg-health-low` below a quarter full.
|
|
785
788
|
- `Hotbar` (function): function Hotbar({ inventoryId, activeSlot, keys, slotSize = 46, itemIcon, style, className, }: { inventoryId: string; activeSlot?: number; keys?: readonly string[]; slotSize?: number; /** Caller-supplied item id → icon registry; return null/undefined to fall back to the default glyph. */ itemIcon?: … — A numbered hotbar bound to an inventory — painted iconed slots (a `GameIcon` glyph over a school-keyed gradient with a count badge, #1035), an active-slot highlight, and a keycap per slot. Place it and pass the `inventoryId`; `activeSlot` highlights the equipped one. Supply `itemIcon` to map your item ids to your own glyphs — the default resolves a `GameIcon` from the item id.
|
|
789
|
+
- `HudBlockBounds` (interface): interface HudBlockBounds — A rendered block, as measured from the DOM.
|
|
790
|
+
- `HudBlockOverlap` (interface): interface HudBlockOverlap — One intersection between two rendered HUD blocks.
|
|
791
|
+
- `HudBlockOverlapOptions` (interface): interface HudBlockOverlapOptions — Options for {@link hudBlockOverlaps}.
|
|
786
792
|
- `HudCanvas` (function): function HudCanvas({ layout, editChord, compactScale, showDuring, className, style, children, }: { layout: HudLayoutStore; editChord?: HudEditChord | false; /** Zoom applied to the whole HUD on compact displays. Default 0.85. */ compactScale?: number; /** Opt-in play-phase gate: render the HUD only … — Full-viewport HUD surface. Panels declared with `HudPanel` flow into nine anchor regions and stack automatically with a gap — no per-panel pixel offsets, no manual clearance for sibling panels, the touch-control dock (`--jg-hud-dock-clearance`), or device safe areas. On compact displays the whole surface scales down and each panel applies its `compact` behavior.
|
|
787
793
|
- `HudCompactMode` (type): type HudCompactMode = "keep" | "chip" | "hide" — How a panel behaves on compact (phone-scale) displays. `keep` stays visible at the global compact scale, `chip` collapses to a small tap-to-expand pill, `hide` unmounts entirely.
|
|
788
794
|
- `HudEditChord` (interface): interface HudEditChord { hold: string; press: string } — ⚠ undocumented · used by `HudCanvas`: Full-viewport HUD surface.
|
|
@@ -790,9 +796,13 @@
|
|
|
790
796
|
- `HudFrameProps` (interface): interface HudFrameProps — Props for {@link HudFrame}.
|
|
791
797
|
- `HudFrameShape` (type): type HudFrameShape = "rounded" | "circle" | "square" — Frame corner shape — `rounded` (the variation's default radius), `circle` (fully round), or `square` (no radius).
|
|
792
798
|
- `HudFrameVariation` (type): type HudFrameVariation = "glass" | "plate" | "retro" | "themed" — Frame skin — `glass` (the shared dark-glass panel), `plate` (a heavier opaque plate), `retro` (a hard black-outlined frame), or `themed` (driven by the `--jg-frame-*` `HudTheme` tokens).
|
|
799
|
+
- `HudLaneConfig` (interface): interface HudLaneConfig — How the shell lays out lanes.
|
|
800
|
+
- `HudLaneLayout` (interface): interface HudLaneLayout — The resolved layout of every claimed block.
|
|
801
|
+
- `HudLanePlacement` (interface): interface HudLanePlacement — One block's resolved placement.
|
|
802
|
+
- `HudLaneSlot` (interface): interface HudLaneSlot — One HUD block's claim on a lane. Two blocks that name the same `anchor` and `lane` stack in `order` order; a lane is a column, so lanes in the same anchor sit side by side. The shell resolves every claim into a rectangle, which is what keeps blocks from arranging themselves into each other.
|
|
793
803
|
- `HudLayoutPersist` (type): type HudLayoutPersist = (id: string, panel: EditorUiPanelLayout) => void — Host-owned callback that commits one HUD panel layout into the scene document (typically an undoable `setUiPanel` dispatch). Injected by the editor so product UI never reaches for editor globals.
|
|
794
804
|
- `HudLayoutPersistProvider` (function): function HudLayoutPersistProvider({ onPanelCommit, children, }: { onPanelCommit: HudLayoutPersist; children?: ReactNode; }): React.JSX.Element — Provides {@link HudLayoutPersist} / `onPanelCommit` to descendant `useHudLayout` calls. Mount around game UI when a host can accept document UI patches (editor). Absent provider → canvas moves/resizes are no-ops for document writes.
|
|
795
|
-
- `HudPanel` (function): function HudPanel({ id, anchor = "top-left", order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, width, height, type, className, style, children, }: { id: string; anchor?: HudAnchor; /**
|
|
805
|
+
- `HudPanel` (function): function HudPanel({ id, anchor = "top-left", lane, order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, width, height, type, className, style, children, }: { id: string; anchor?: HudAnchor; /** … — A HUD block that lives in one of the nine anchor regions. Panels sharing a region stack outward from the screen edge in ascending `order`. On fine pointers panels stay draggable through the edit chord; a dragged panel leaves the flow and keeps its custom placement. On compact displays custom placements are ignored and the `compact` behavior applies.
|
|
796
806
|
- `HudSkin` (interface): interface HudSkin — A game-authored visual skin layered over a {@link HudTheme}.
|
|
797
807
|
- `HudSkinMaterial` (interface): interface HudSkinMaterial — Optional raster-backed materials for shared HUD chrome.
|
|
798
808
|
- `HudSkinPreview` (function): function HudSkinPreview({ className }: { className?: string }): React.JSX.Element — Procedural two-skin fixture for visual regression captures.
|
|
@@ -1030,8 +1040,10 @@
|
|
|
1030
1040
|
- `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.
|
|
1031
1041
|
- `deriveHudTheme` (function): function deriveHudTheme(seed: HudThemeSeed): HudTheme — Derives a coherent full theme from an accent/surface pair — shades, edges, dim text, frames, bar troughs, and slots all fall out of the seed, so a game gets one consistent look across the `packages/react` primitives and the `registry/jgengine` blocks without naming 50 tokens. Hex seeds only (`#rgb` or `#rrggbb`).
|
|
1032
1042
|
- `eventMeterNeedsHeartbeat` (function): function eventMeterNeedsHeartbeat(meter: EventMeter, previous: EventMeterView | null): boolean — True when `meter`'s value/fraction/tier/ready diverge from `previous` — the re-render check `useEventMeter` polls on its heartbeat.
|
|
1043
|
+
- `formatHudOverlapReport` (function): function formatHudOverlapReport(overlaps: readonly HudBlockOverlap[], viewport: HudSize): string — The console block for a set of overlaps — one line per pair naming both block ids, plus the viewport they collided at. Empty string when nothing intersects.
|
|
1033
1044
|
- `formatTimerMs` (function): function formatTimerMs(ms: number, format: TimerFormat = "mm:ss"): string — Format a millisecond duration as digital timer text — `mm:ss` (default), `m:ss.d` (with tenths), or `ss.d` (seconds + tenths). Negative inputs clamp to zero. No genre meaning: the same helper serves a round clock, a respawn countdown, or an ability charge readout.
|
|
1034
1045
|
- `guestIdentity` (function): function guestIdentity(seed?: string): IdentitySource — ⚠ undocumented
|
|
1046
|
+
- `hudBlockOverlaps` (function): function hudBlockOverlaps(blocks: readonly HudBlockBounds[], options: HudBlockOverlapOptions = {}): HudBlockOverlap[] — Every pair of rendered HUD blocks that intersects. This is the development-time check behind the console report: it reads what actually hit the screen rather than what a layout intended, so a block that overflows its own box or a game override that lands on a neighbour is named too.
|
|
1035
1047
|
- `hudSkinVars` (function): function hudSkinVars(skin?: HudSkin): CSSProperties — Converts a skin into the CSS variables consumed by shared HUD components.
|
|
1036
1048
|
- `hudTheme` (function): function hudTheme(overrides: HudThemeOverrides): HudTheme — Builds a full {@link HudTheme} from per-group partials, filling the rest from {@link defaultHudTheme}. Reach for this when a game wants to name exact values; use {@link deriveHudTheme} when a seed color pair is enough.
|
|
1037
1049
|
- `hudThemeVars` (function): function hudThemeVars(theme: HudTheme): CSSProperties — Emits theme CSS properties, resetting inherited bar fills when absent. Spread onto a HUD ancestor.
|
|
@@ -1046,6 +1058,7 @@
|
|
|
1046
1058
|
- `previewFixtureNames` (function): function previewFixtureNames(): string[] — Sorted list of registered fixture names, for discovery/listing.
|
|
1047
1059
|
- `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.
|
|
1048
1060
|
- `resolveDialogueInvoke` (function): function resolveDialogueInvoke(choice: DialogueChoice, result: CheckResult | null): { command: string; args?: unknown } | null — ⚠ undocumented
|
|
1061
|
+
- `resolveHudLanes` (function): function resolveHudLanes(slots: readonly HudLaneSlot[], viewport: HudSize, config: HudLaneConfig = {}): HudLaneLayout — Resolve lane claims into non-overlapping rectangles for one frame.
|
|
1049
1062
|
- `resolveHudTheme` (function): function resolveHudTheme(theme?: HudThemePreset | HudTheme): HudTheme — Resolves a preset name (or a full theme) to a `HudTheme`; falls back to the default theme.
|
|
1050
1063
|
- `resolvePreviewFixture` (function): function resolvePreviewFixture(name: string): PreviewFixture | undefined — Resolve a fixture by name, or `undefined` when it is not registered.
|
|
1051
1064
|
- `runDialogueChoice` (function): function runDialogueChoice(commands: { run(name: string, input?: unknown): unknown }, choice: DialogueChoice, result: CheckResult | null): void — Route a {@link DialogueBox} choice through the `features.dialogue` bridge: resolve the choice's invoke (honoring a skill-check `result`), run that command, and otherwise close the dialogue — the write side that replaces a per-game `onChoice` that hand-rolls `resolveDialogueInvoke` + `dialogue.close`.
|
|
@@ -1067,6 +1080,7 @@
|
|
|
1067
1080
|
- `useChat` (function): function useChat(channelId: string, options?: { limit?: number }): ChatMessage[] — ⚠ undocumented
|
|
1068
1081
|
- `useChatBubbles` (function): function useChatBubbles(options?: ChatBubblesOptions): readonly ChatBubble[] — ⚠ undocumented
|
|
1069
1082
|
- `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).
|
|
1083
|
+
- `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
1084
|
- `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
1085
|
- `useCurrency` (function): function useCurrency(currencyId: string): number — ⚠ undocumented
|
|
1072
1086
|
- `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 +1124,7 @@
|
|
|
1110
1124
|
- `useLocalPlayerDead` (function): function useLocalPlayerDead(healthStatId = "health"): boolean — ⚠ undocumented
|
|
1111
1125
|
- `useLocale` (function): function useLocale(): Locale — Subscribe to the active locale — re-renders on `setLocale`.
|
|
1112
1126
|
- `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.
|
|
1127
|
+
- `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
1128
|
- `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
1129
|
- `useNearestWorldItem` (function): function useNearestWorldItem(radius: number): WorldItemRecord | null — Nearest ground item within `radius` of the local player — drives a pickup prompt/highlight.
|
|
1116
1130
|
- `useNotifications` (function): function useNotifications<TMeta = unknown>(store: NotificationStore<TMeta>): readonly NotificationEntry<TMeta>[] — Subscribe to a notification store's live list.
|
|
@@ -1161,9 +1175,12 @@
|
|
|
1161
1175
|
|
|
1162
1176
|
## @jgengine/react/accessibility
|
|
1163
1177
|
|
|
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 `
|
|
1178
|
+
- `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`.
|
|
1179
|
+
- `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.
|
|
1180
|
+
- `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.
|
|
1181
|
+
- `ColorblindFrameProps` (interface): interface ColorblindFrameProps — Props for {@link ColorblindFrame}.
|
|
1166
1182
|
- `useAccessibility` (function): function useAccessibility(): AccessibilityContextValue — Read the live accessibility state + its store; throws without an {@link AccessibilityProvider} above.
|
|
1183
|
+
- `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
1184
|
- `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
1185
|
- `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
1186
|
|
|
@@ -1526,12 +1543,29 @@
|
|
|
1526
1543
|
- `HudInteractProvider` (function): function HudInteractProvider({ onInteract, children }: { onInteract: HudInteractListener; children?: ReactNode }): ReactNode — Routes interactions from every engine HUD primitive below it (buttons, panels, windows, action slots) to one listener — the seam a UI sound or haptics system hooks without per-button wiring. Primitives also expose `data-jg-cue` and a per-instance `onInteract`.
|
|
1527
1544
|
- `useHudInteract` (function): function useHudInteract(source: string, cue?: string, onInteract?: HudInteractListener): (kind: HudInteractKind) => void — Returns a reporter that calls the instance listener, then the nearest {@link HudInteractProvider}.
|
|
1528
1545
|
|
|
1546
|
+
## @jgengine/react/hudLanes
|
|
1547
|
+
|
|
1548
|
+
- `HudBlockBounds` (interface): interface HudBlockBounds — A rendered block, as measured from the DOM.
|
|
1549
|
+
- `HudBlockOverlap` (interface): interface HudBlockOverlap — One intersection between two rendered HUD blocks.
|
|
1550
|
+
- `HudBlockOverlapOptions` (interface): interface HudBlockOverlapOptions — Options for {@link hudBlockOverlaps}.
|
|
1551
|
+
- `HudLaneConfig` (interface): interface HudLaneConfig — How the shell lays out lanes.
|
|
1552
|
+
- `HudLaneLayout` (interface): interface HudLaneLayout — The resolved layout of every claimed block.
|
|
1553
|
+
- `HudLanePlacement` (interface): interface HudLanePlacement — One block's resolved placement.
|
|
1554
|
+
- `HudLaneSlot` (interface): interface HudLaneSlot — One HUD block's claim on a lane. Two blocks that name the same `anchor` and `lane` stack in `order` order; a lane is a column, so lanes in the same anchor sit side by side. The shell resolves every claim into a rectangle, which is what keeps blocks from arranging themselves into each other.
|
|
1555
|
+
- `formatHudOverlapReport` (function): function formatHudOverlapReport(overlaps: readonly HudBlockOverlap[], viewport: HudSize): string — The console block for a set of overlaps — one line per pair naming both block ids, plus the viewport they collided at. Empty string when nothing intersects.
|
|
1556
|
+
- `hudBlockOverlaps` (function): function hudBlockOverlaps(blocks: readonly HudBlockBounds[], options: HudBlockOverlapOptions = {}): HudBlockOverlap[] — Every pair of rendered HUD blocks that intersects. This is the development-time check behind the console report: it reads what actually hit the screen rather than what a layout intended, so a block that overflows its own box or a game override that lands on a neighbour is named too.
|
|
1557
|
+
- `resolveHudLanes` (function): function resolveHudLanes(slots: readonly HudLaneSlot[], viewport: HudSize, config: HudLaneConfig = {}): HudLaneLayout — Resolve lane claims into non-overlapping rectangles for one frame.
|
|
1558
|
+
|
|
1559
|
+
## @jgengine/react/hudLanesPreview
|
|
1560
|
+
|
|
1561
|
+
- `HudLanesPreview` (function): function HudLanesPreview({ className }: { className?: string }): React.JSX.Element — The same shape Deepward's top-left HUD uses — two blocks that used to arrange themselves into each other. Both declare `lane="status"`, so the shell stacks them; the second lane sits beside the first and the right-hand lane mirrors it. Screenshots at any viewport without a block landing on a neighbour.
|
|
1562
|
+
|
|
1529
1563
|
## @jgengine/react/hudLayout
|
|
1530
1564
|
|
|
1531
1565
|
- `HudCanvas` (function): function HudCanvas({ layout, editChord, compactScale, showDuring, className, style, children, }: { layout: HudLayoutStore; editChord?: HudEditChord | false; /** Zoom applied to the whole HUD on compact displays. Default 0.85. */ compactScale?: number; /** Opt-in play-phase gate: render the HUD only … — Full-viewport HUD surface. Panels declared with `HudPanel` flow into nine anchor regions and stack automatically with a gap — no per-panel pixel offsets, no manual clearance for sibling panels, the touch-control dock (`--jg-hud-dock-clearance`), or device safe areas. On compact displays the whole surface scales down and each panel applies its `compact` behavior.
|
|
1532
1566
|
- `HudCompactMode` (type): type HudCompactMode = "keep" | "chip" | "hide" — How a panel behaves on compact (phone-scale) displays. `keep` stays visible at the global compact scale, `chip` collapses to a small tap-to-expand pill, `hide` unmounts entirely.
|
|
1533
1567
|
- `HudEditChord` (interface): interface HudEditChord { hold: string; press: string } — ⚠ undocumented · used by `HudCanvas`: Full-viewport HUD surface.
|
|
1534
|
-
- `HudPanel` (function): function HudPanel({ id, anchor = "top-left", order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, width, height, type, className, style, children, }: { id: string; anchor?: HudAnchor; /**
|
|
1568
|
+
- `HudPanel` (function): function HudPanel({ id, anchor = "top-left", lane, order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, width, height, type, className, style, children, }: { id: string; anchor?: HudAnchor; /** … — A HUD block that lives in one of the nine anchor regions. Panels sharing a region stack outward from the screen edge in ascending `order`. On fine pointers panels stay draggable through the edit chord; a dragged panel leaves the flow and keeps its custom placement. On compact displays custom placements are ignored and the `compact` behavior applies.
|
|
1535
1569
|
- `hudVisibleInPhase` (function): function hudVisibleInPhase(showDuring: readonly GamePhase[] | undefined, phase: GamePhase): boolean — Whether a HUD element opted into `showDuring` is visible in the current phase; `undefined` = always visible (default).
|
|
1536
1570
|
- `useHudLayout` (function): function useHudLayout(options?: { storageKey?: string; snap?: number; locked?: boolean; /** * Scene-document `ui` section — source of truth for panel placement/size. * When provided, hydrates the layout store (and wins over legacy localStorage). */ documentUi?: EditorUiDocument; /** * When true (def… — Layout state for `HudCanvas` — panel placements, edit-mode drag/resize, and per-game persistence.
|
|
1537
1571
|
|
|
@@ -1746,7 +1780,7 @@
|
|
|
1746
1780
|
- `MenuNavItemProps` (interface): interface MenuNavItemProps — Props a menu item needs so {@link useMenuNav} can focus it and track pointer/focus.
|
|
1747
1781
|
- `MenuNavRender` (type): type MenuNavRender = (nav: MenuNavController) => ReactNode — Render prop for a menu whose items come from {@link useMenuNav}.
|
|
1748
1782
|
- `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.
|
|
1783
|
+
- `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
1784
|
|
|
1751
1785
|
## @jgengine/react/menuRouter
|
|
1752
1786
|
|
|
@@ -1921,16 +1955,17 @@
|
|
|
1921
1955
|
|
|
1922
1956
|
## @jgengine/react/settingsBlocks
|
|
1923
1957
|
|
|
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.
|
|
1958
|
+
- `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
1959
|
- `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.
|
|
1960
|
+
- `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}.
|
|
1961
|
+
- `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
1962
|
- `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.
|
|
1963
|
+
- `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.
|
|
1964
|
+
- `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
1965
|
- `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
1966
|
- `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}.
|
|
1967
|
+
- `SettingsToggle` (function): function SettingsToggle({ row, itemProps }: { row: SettingsRow; itemProps?: MenuNavItemProps }): ReactNode — One on/off row from a {@link SettingsRow}.
|
|
1968
|
+
- `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
1969
|
|
|
1935
1970
|
## @jgengine/react/shopGrid
|
|
1936
1971
|
|
|
@@ -3654,9 +3689,9 @@
|
|
|
3654
3689
|
- `ProjectileTracers` (function): function ProjectileTracers({ lifeMs = 130 }: { lifeMs?: number }): React.JSX.Element — ⚠ undocumented
|
|
3655
3690
|
- `Reticle` (function): function Reticle({ className }: { className?: string }): React.JSX.Element — ⚠ undocumented
|
|
3656
3691
|
- `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
|
|
3692
|
+
- `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
3693
|
- `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
|
|
3694
|
+
- `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
3695
|
- `WorldNameplatesProps` (interface): interface WorldNameplatesProps — Props for `WorldNameplates` — entity filter, refresh rate, and headless className/render hooks.
|
|
3661
3696
|
- `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
3697
|
- `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"`
|
|
@@ -343,6 +359,11 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
343
359
|
|
|
344
360
|
- `HudInteractProvider` (function) · `import { HudInteractProvider } from "@jgengine/react/hudInteract"`
|
|
345
361
|
|
|
362
|
+
## hud-lanes — two HUD blocks stacked in one lane, plus a second lane beside it
|
|
363
|
+
|
|
364
|
+
- `HudLanesPreview` (function) · `import { HudLanesPreview } from "@jgengine/react/hudLanesPreview"`
|
|
365
|
+
- `resolveHudLanes` (function) · `import { resolveHudLanes } from "@jgengine/react"`
|
|
366
|
+
|
|
346
367
|
## hud-layout — persistent, live-editable HUD panel layout state for `HudCanvas`
|
|
347
368
|
|
|
348
369
|
- `useHudLayout` (function) · `import { useHudLayout } from "@jgengine/react"`
|
|
@@ -355,6 +376,11 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
355
376
|
|
|
356
377
|
- `useHudLayoutPersist` (function) · `import { useHudLayoutPersist } from "@jgengine/react"`
|
|
357
378
|
|
|
379
|
+
## hud-overlap-report — format the intersecting HUD block pairs for a development console
|
|
380
|
+
|
|
381
|
+
- `formatHudOverlapReport` (function) · `import { formatHudOverlapReport } from "@jgengine/react"`
|
|
382
|
+
- `hudBlockOverlaps` (function) · `import { hudBlockOverlaps } from "@jgengine/react"`
|
|
383
|
+
|
|
358
384
|
## hud-panel — anchored HUD slot — drop any widget in a named, player-movable panel
|
|
359
385
|
|
|
360
386
|
- `HudPanel` (function) · `import { HudPanel } from "@jgengine/react"`
|
|
@@ -758,7 +784,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
758
784
|
|
|
759
785
|
- `SettingsKeybindControl` (function) · `import { SettingsKeybindControl } from "@jgengine/react/settingsBlocks"`
|
|
760
786
|
|
|
761
|
-
## settings-section — settings category block — rows plus click-to-capture keybind rows, HudTheme-styled, store-backed
|
|
787
|
+
## settings-section — settings category block — rows plus click-to-capture keybind rows, HudTheme-styled, store-backed, keyboard and gamepad navigable
|
|
762
788
|
|
|
763
789
|
- `SettingsSection` (function) · `import { SettingsSection } from "@jgengine/react/settingsBlocks"`
|
|
764
790
|
|
|
@@ -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"`
|