@jgengine/react 0.30.0 → 0.32.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 +51 -0
- package/dist/hudLanes.d.ts +96 -0
- package/dist/hudLanes.js +148 -0
- package/dist/hudLanesPreview.d.ts +11 -0
- package/dist/hudLanesPreview.js +31 -0
- package/dist/hudLayout.d.ts +8 -1
- package/dist/hudLayout.js +236 -25
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/previewFixtures.js +6 -0
- package/package.json +2 -2
- package/skills/jgengine/api.md +4 -3
- package/skills/jgengine-ui/SKILL.md +1 -0
- package/skills/jgengine-ui/api.md +79 -3
- package/skills/jgengine-ui/capabilities.md +42 -0
- package/skills/jgengine-ui/reference.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,57 @@ 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.32.0
|
|
30
|
+
|
|
31
|
+
### Migrate
|
|
32
|
+
|
|
33
|
+
- **Bump lockstep SDK packages to `^0.32.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.29.0`; `@jgengine/github` is `0.19.0`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Audio mixing grew a mute that composes with gain: `AudioEngine.setBusMute` / `setMasterMute` keep the player's volume while silencing a bus, `masterLevel()` / `busLevel()` report the composed value, and Settings carries a mute row per bus plus "Mute all" (`sound.muted`, `sound.bus.<id>.muted`). `AudioEngine.play` and `playOneShot` hold cues in a bounded 16-slot queue while the browser keeps the context suspended and play them on the first gesture instead of dropping them; `installGestureResume` wires that gesture for both presentation paths.
|
|
38
|
+
- `AudioEngine.measure()` reads master RMS and peak from a lazily built analyser, and `installAudioProbe` publishes `window.__jgengineAudio` so a headless capture can prove sound with a measured level rather than a screenshot.
|
|
39
|
+
- `@jgengine/core/audio/feedbackCues` ships seven procedural reactions - `hit`, `hurt`, `kill`, `pickup`, `objective`, `wave`, `deny` - on an `sfx` bus, merged into every game's catalog as `feel.<cue>`. A game sound with that id replaces one; `audio.feedbackCues` replaces or silences them.
|
|
40
|
+
- `@jgengine/shell/audio/eventFeedback` maps the events the engine already announces to those reactions: `combat.damage` (local player reads as hurt, anything else as a hit), `entity.died`, `worldItem.picked_up`, `quest.completed`, `stat.levelUp` and `command.refused`. Each row carries a sound, a gamepad pulse, camera trauma and a caption; the mapping and reactions are data (`DEFAULT_FEEDBACK_BINDINGS`, `DEFAULT_FEEDBACK_REACTIONS`) a game can replace, cooldowns fold a burst into one impact, reduced motion keeps sound and rumble but drops the camera kick, and the resolved reaction is pooled so handling an event allocates nothing per frame. `attachWaveFeedback(runner, ctx.game.feedback)` announces each new wave.
|
|
41
|
+
- `ctx.game.feedback.cue(cue, caption?)` announces a moment no engine event covers and emits `feedback.cue`.
|
|
42
|
+
- `@jgengine/shell/audio/captions` and `CaptionOverlay` render a bounded, deduping, self-expiring caption line under the accessibility captions setting; `FeedbackHost` mounts the whole layer - feedback mapping, gesture resume, audio probe and captions - for both the 3D and HUD presentation paths.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- The CI gate runs nine test shards instead of ten, and a `types` job whose stages overlap, so a docs-only pull request fits the account's 20-job runner cap with room for another run beside it. `scripts/test-shard.ts` owns which files each shard runs and what it prepares first; `bun scripts/type-shard.ts` runs the `check-types` plan two stages at a time and then the tsc fan-out alone.
|
|
47
|
+
|
|
48
|
+
## 0.31.0
|
|
49
|
+
|
|
50
|
+
### Migrate
|
|
51
|
+
|
|
52
|
+
- **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`.
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- `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.
|
|
57
|
+
- `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.
|
|
58
|
+
- `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.
|
|
59
|
+
- `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`.
|
|
60
|
+
- `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.
|
|
61
|
+
- `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.
|
|
62
|
+
- `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.
|
|
63
|
+
- `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.
|
|
64
|
+
- `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`.
|
|
65
|
+
- `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.
|
|
66
|
+
- `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.
|
|
67
|
+
- `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.
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- `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`.
|
|
72
|
+
- 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.
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
|
|
76
|
+
- `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.
|
|
77
|
+
- The HUD boundary check returned early whenever nothing overflowed, so a later report in the same pass could never run.
|
|
78
|
+
- `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.
|
|
79
|
+
|
|
29
80
|
## 0.30.0
|
|
30
81
|
|
|
31
82
|
### Migrate
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type HudAnchor, type HudRect, type HudSize } from "@jgengine/core/ui/hudLayout";
|
|
2
|
+
/**
|
|
3
|
+
* One HUD block's claim on a lane. Two blocks that name the same `anchor` and
|
|
4
|
+
* `lane` stack in `order` order; a lane is a column, so lanes in the same anchor
|
|
5
|
+
* sit side by side. The shell resolves every claim into a rectangle, which is
|
|
6
|
+
* what keeps blocks from arranging themselves into each other.
|
|
7
|
+
*/
|
|
8
|
+
export interface HudLaneSlot {
|
|
9
|
+
id: string;
|
|
10
|
+
/** The anchored corner/edge the lane hangs from. */
|
|
11
|
+
anchor: HudAnchor;
|
|
12
|
+
/** Lane name inside the anchor. Default `"main"`. */
|
|
13
|
+
lane?: string;
|
|
14
|
+
/** Stack position within the lane, ascending away from the anchored edge. Default `0`. */
|
|
15
|
+
order?: number;
|
|
16
|
+
/** Declared width in px. Falls back to the size the block measured. */
|
|
17
|
+
width?: number;
|
|
18
|
+
/** Declared height in px. Falls back to the size the block measured. */
|
|
19
|
+
height?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Game override: an exact rectangle in HUD space. The block is placed there
|
|
22
|
+
* and takes no part in lane stacking, so a game keeps full control of its own
|
|
23
|
+
* placement.
|
|
24
|
+
*/
|
|
25
|
+
rect?: HudRect;
|
|
26
|
+
}
|
|
27
|
+
/** One block's resolved placement. */
|
|
28
|
+
export interface HudLanePlacement {
|
|
29
|
+
id: string;
|
|
30
|
+
anchor: HudAnchor;
|
|
31
|
+
lane: string;
|
|
32
|
+
rect: HudRect;
|
|
33
|
+
}
|
|
34
|
+
/** How the shell lays out lanes. */
|
|
35
|
+
export interface HudLaneConfig {
|
|
36
|
+
/** Gap between stacked blocks and between lanes, in px. Default `10`. */
|
|
37
|
+
gap?: number;
|
|
38
|
+
/** Inset from the frame edge, in px. Default `0` — the HUD layer already carries its padding. */
|
|
39
|
+
pad?: number;
|
|
40
|
+
/** Size used for a block that declares and has not yet measured one. */
|
|
41
|
+
fallback?: HudSize;
|
|
42
|
+
}
|
|
43
|
+
/** The resolved layout of every claimed block. */
|
|
44
|
+
export interface HudLaneLayout {
|
|
45
|
+
placements: HudLanePlacement[];
|
|
46
|
+
/** Rectangle by block id, for renderers that only lookup. */
|
|
47
|
+
rects: Record<string, HudRect>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve lane claims into non-overlapping rectangles for one frame.
|
|
51
|
+
*
|
|
52
|
+
* Blocks are grouped by anchor, then by lane (first-declared lane first), then
|
|
53
|
+
* sorted by `order`. A lane is one column: its blocks stack top-to-bottom with
|
|
54
|
+
* `gap` between them, and the anchor's lanes sit side by side with the same gap.
|
|
55
|
+
* The whole group is aligned to its anchored edge and the block is aligned
|
|
56
|
+
* within its lane, so a block added at any viewport can only take free space —
|
|
57
|
+
* never a neighbour's. A block carrying `rect` is placed there untouched.
|
|
58
|
+
*
|
|
59
|
+
* @capability hud-lanes lay HUD blocks out in non-overlapping anchored lanes
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolveHudLanes(slots: readonly HudLaneSlot[], viewport: HudSize, config?: HudLaneConfig): HudLaneLayout;
|
|
62
|
+
/** One intersection between two rendered HUD blocks. */
|
|
63
|
+
export interface HudBlockOverlap {
|
|
64
|
+
a: string;
|
|
65
|
+
b: string;
|
|
66
|
+
/** Overlapping area in px². */
|
|
67
|
+
area: number;
|
|
68
|
+
/** The intersection itself, in the same space as the inputs. */
|
|
69
|
+
rect: HudRect;
|
|
70
|
+
}
|
|
71
|
+
/** A rendered block, as measured from the DOM. */
|
|
72
|
+
export interface HudBlockBounds {
|
|
73
|
+
id: string;
|
|
74
|
+
rect: HudRect;
|
|
75
|
+
}
|
|
76
|
+
/** Options for {@link hudBlockOverlaps}. */
|
|
77
|
+
export interface HudBlockOverlapOptions {
|
|
78
|
+
/** Ignore intersections smaller than this many px². Default `1`. */
|
|
79
|
+
minArea?: number;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Every pair of rendered HUD blocks that intersects. This is the development-time
|
|
83
|
+
* check behind the console report: it reads what actually hit the screen rather
|
|
84
|
+
* than what a layout intended, so a block that overflows its own box or a game
|
|
85
|
+
* override that lands on a neighbour is named too.
|
|
86
|
+
*
|
|
87
|
+
* @capability hud-overlap-report name every intersecting pair of rendered HUD blocks
|
|
88
|
+
*/
|
|
89
|
+
export declare function hudBlockOverlaps(blocks: readonly HudBlockBounds[], options?: HudBlockOverlapOptions): HudBlockOverlap[];
|
|
90
|
+
/**
|
|
91
|
+
* The console block for a set of overlaps — one line per pair naming both block
|
|
92
|
+
* ids, plus the viewport they collided at. Empty string when nothing intersects.
|
|
93
|
+
*
|
|
94
|
+
* @capability hud-overlap-report format the intersecting HUD block pairs for a development console
|
|
95
|
+
*/
|
|
96
|
+
export declare function formatHudOverlapReport(overlaps: readonly HudBlockOverlap[], viewport: HudSize): string;
|
package/dist/hudLanes.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { HUD_ANCHOR_FRACTIONS, } from "@jgengine/core/ui/hudLayout";
|
|
2
|
+
const DEFAULT_GAP = 10;
|
|
3
|
+
const DEFAULT_FALLBACK = { width: 240, height: 44 };
|
|
4
|
+
/**
|
|
5
|
+
* Resolve lane claims into non-overlapping rectangles for one frame.
|
|
6
|
+
*
|
|
7
|
+
* Blocks are grouped by anchor, then by lane (first-declared lane first), then
|
|
8
|
+
* sorted by `order`. A lane is one column: its blocks stack top-to-bottom with
|
|
9
|
+
* `gap` between them, and the anchor's lanes sit side by side with the same gap.
|
|
10
|
+
* The whole group is aligned to its anchored edge and the block is aligned
|
|
11
|
+
* within its lane, so a block added at any viewport can only take free space —
|
|
12
|
+
* never a neighbour's. A block carrying `rect` is placed there untouched.
|
|
13
|
+
*
|
|
14
|
+
* @capability hud-lanes lay HUD blocks out in non-overlapping anchored lanes
|
|
15
|
+
*/
|
|
16
|
+
export function resolveHudLanes(slots, viewport, config = {}) {
|
|
17
|
+
const gap = config.gap ?? DEFAULT_GAP;
|
|
18
|
+
const pad = config.pad ?? 0;
|
|
19
|
+
const fallback = config.fallback ?? DEFAULT_FALLBACK;
|
|
20
|
+
const placements = [];
|
|
21
|
+
const rects = {};
|
|
22
|
+
const byAnchor = new Map();
|
|
23
|
+
slots.forEach((slot, index) => {
|
|
24
|
+
const claims = byAnchor.get(slot.anchor) ?? [];
|
|
25
|
+
claims.push({ slot, index });
|
|
26
|
+
byAnchor.set(slot.anchor, claims);
|
|
27
|
+
});
|
|
28
|
+
const place = (id, anchor, lane, rect) => {
|
|
29
|
+
placements.push({ id, anchor, lane, rect });
|
|
30
|
+
rects[id] = rect;
|
|
31
|
+
};
|
|
32
|
+
for (const [anchor, claims] of byAnchor) {
|
|
33
|
+
const { fx, fy } = HUD_ANCHOR_FRACTIONS[anchor];
|
|
34
|
+
for (const claim of claims) {
|
|
35
|
+
if (claim.slot.rect === undefined)
|
|
36
|
+
continue;
|
|
37
|
+
place(claim.slot.id, anchor, claim.slot.lane ?? "main", claim.slot.rect);
|
|
38
|
+
}
|
|
39
|
+
const flowing = claims.filter((claim) => claim.slot.rect === undefined);
|
|
40
|
+
if (flowing.length === 0)
|
|
41
|
+
continue;
|
|
42
|
+
const lanes = new Map();
|
|
43
|
+
for (const claim of flowing) {
|
|
44
|
+
const name = claim.slot.lane ?? "main";
|
|
45
|
+
const members = lanes.get(name) ?? [];
|
|
46
|
+
members.push(claim);
|
|
47
|
+
lanes.set(name, members);
|
|
48
|
+
}
|
|
49
|
+
const sizeOf = (claim) => ({
|
|
50
|
+
width: claim.slot.width ?? fallback.width,
|
|
51
|
+
height: claim.slot.height ?? fallback.height,
|
|
52
|
+
});
|
|
53
|
+
const laneSize = new Map();
|
|
54
|
+
for (const [name, members] of lanes) {
|
|
55
|
+
members.sort((a, b) => (a.slot.order ?? 0) - (b.slot.order ?? 0) || a.index - b.index);
|
|
56
|
+
let width = 0;
|
|
57
|
+
let height = 0;
|
|
58
|
+
members.forEach((claim, position) => {
|
|
59
|
+
const size = sizeOf(claim);
|
|
60
|
+
width = Math.max(width, size.width);
|
|
61
|
+
height += size.height + (position === 0 ? 0 : gap);
|
|
62
|
+
});
|
|
63
|
+
laneSize.set(name, { width, height });
|
|
64
|
+
}
|
|
65
|
+
const laneWidths = [...laneSize.values()].map((size) => size.width);
|
|
66
|
+
const groupWidth = laneWidths.reduce((sum, width) => sum + width, 0) + gap * Math.max(0, laneWidths.length - 1);
|
|
67
|
+
const groupHeight = Math.max(...[...laneSize.values()].map((size) => size.height));
|
|
68
|
+
let groupLeft = pad;
|
|
69
|
+
if (fx === 1)
|
|
70
|
+
groupLeft = viewport.width - pad - groupWidth;
|
|
71
|
+
else if (fx === 0.5)
|
|
72
|
+
groupLeft = (viewport.width - groupWidth) / 2;
|
|
73
|
+
let groupTop = pad;
|
|
74
|
+
if (fy === 1)
|
|
75
|
+
groupTop = viewport.height - pad - groupHeight;
|
|
76
|
+
else if (fy === 0.5)
|
|
77
|
+
groupTop = (viewport.height - groupHeight) / 2;
|
|
78
|
+
let columnX = groupLeft;
|
|
79
|
+
for (const [name, members] of lanes) {
|
|
80
|
+
const lane = laneSize.get(name);
|
|
81
|
+
const columnY = fy === 0 ? groupTop : fy === 1 ? groupTop + (groupHeight - lane.height) : groupTop + (groupHeight - lane.height) / 2;
|
|
82
|
+
let cursor = columnY;
|
|
83
|
+
for (const claim of members) {
|
|
84
|
+
const size = sizeOf(claim);
|
|
85
|
+
const alignedX = fx === 0 ? columnX : fx === 1 ? columnX + (lane.width - size.width) : columnX + (lane.width - size.width) / 2;
|
|
86
|
+
place(claim.slot.id, anchor, name, {
|
|
87
|
+
x: alignedX,
|
|
88
|
+
y: cursor,
|
|
89
|
+
width: size.width,
|
|
90
|
+
height: size.height,
|
|
91
|
+
});
|
|
92
|
+
cursor += size.height + gap;
|
|
93
|
+
}
|
|
94
|
+
columnX += lane.width + gap;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { placements, rects };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Every pair of rendered HUD blocks that intersects. This is the development-time
|
|
101
|
+
* check behind the console report: it reads what actually hit the screen rather
|
|
102
|
+
* than what a layout intended, so a block that overflows its own box or a game
|
|
103
|
+
* override that lands on a neighbour is named too.
|
|
104
|
+
*
|
|
105
|
+
* @capability hud-overlap-report name every intersecting pair of rendered HUD blocks
|
|
106
|
+
*/
|
|
107
|
+
export function hudBlockOverlaps(blocks, options = {}) {
|
|
108
|
+
const minArea = options.minArea ?? 1;
|
|
109
|
+
const out = [];
|
|
110
|
+
for (let i = 0; i < blocks.length; i += 1) {
|
|
111
|
+
for (let j = i + 1; j < blocks.length; j += 1) {
|
|
112
|
+
const a = blocks[i];
|
|
113
|
+
const b = blocks[j];
|
|
114
|
+
const width = Math.min(a.rect.x + a.rect.width, b.rect.x + b.rect.width) - Math.max(a.rect.x, b.rect.x);
|
|
115
|
+
const height = Math.min(a.rect.y + a.rect.height, b.rect.y + b.rect.height) - Math.max(a.rect.y, b.rect.y);
|
|
116
|
+
if (width <= 0 || height <= 0)
|
|
117
|
+
continue;
|
|
118
|
+
const area = width * height;
|
|
119
|
+
if (area < minArea)
|
|
120
|
+
continue;
|
|
121
|
+
out.push({
|
|
122
|
+
a: a.id,
|
|
123
|
+
b: b.id,
|
|
124
|
+
area,
|
|
125
|
+
rect: {
|
|
126
|
+
x: Math.max(a.rect.x, b.rect.x),
|
|
127
|
+
y: Math.max(a.rect.y, b.rect.y),
|
|
128
|
+
width,
|
|
129
|
+
height,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The console block for a set of overlaps — one line per pair naming both block
|
|
138
|
+
* ids, plus the viewport they collided at. Empty string when nothing intersects.
|
|
139
|
+
*
|
|
140
|
+
* @capability hud-overlap-report format the intersecting HUD block pairs for a development console
|
|
141
|
+
*/
|
|
142
|
+
export function formatHudOverlapReport(overlaps, viewport) {
|
|
143
|
+
if (overlaps.length === 0)
|
|
144
|
+
return "";
|
|
145
|
+
const header = `HUD BLOCK OVERLAP at ${Math.round(viewport.width)}x${Math.round(viewport.height)}`;
|
|
146
|
+
const lines = overlaps.map((overlap) => `${overlap.a} intersects ${overlap.b} by ${Math.round(overlap.area)} px² (${Math.round(overlap.rect.x)},${Math.round(overlap.rect.y)} ${Math.round(overlap.rect.width)}x${Math.round(overlap.rect.height)})`);
|
|
147
|
+
return [header, ...lines].join("\n");
|
|
148
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The same shape Deepward's top-left HUD uses — two blocks that used to arrange
|
|
3
|
+
* themselves into each other. Both declare `lane="status"`, so the shell stacks
|
|
4
|
+
* them; the second lane sits beside the first and the right-hand lane mirrors it.
|
|
5
|
+
* Screenshots at any viewport without a block landing on a neighbour.
|
|
6
|
+
*
|
|
7
|
+
* @capability hud-lanes two HUD blocks stacked in one lane, plus a second lane beside it
|
|
8
|
+
*/
|
|
9
|
+
export declare function HudLanesPreview({ className }: {
|
|
10
|
+
className?: string;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { HudFrame } from "./hudFrame.js";
|
|
3
|
+
import { HudSurface } from "./hudSurface.js";
|
|
4
|
+
import { HudThemeRoot } from "./hudThemeRoot.js";
|
|
5
|
+
import { HudCanvas, HudPanel, useHudLayout } from "./hudLayout.js";
|
|
6
|
+
function Chip({ label, value }) {
|
|
7
|
+
return (_jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10 }, children: [_jsx("span", { style: { color: "var(--jg-text-dim, rgba(255,255,255,0.62))", fontSize: 12 }, children: label }), _jsx("span", { style: { fontSize: 12, fontVariantNumeric: "tabular-nums" }, children: value })] }));
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The same shape Deepward's top-left HUD uses — two blocks that used to arrange
|
|
11
|
+
* themselves into each other. Both declare `lane="status"`, so the shell stacks
|
|
12
|
+
* them; the second lane sits beside the first and the right-hand lane mirrors it.
|
|
13
|
+
* Screenshots at any viewport without a block landing on a neighbour.
|
|
14
|
+
*
|
|
15
|
+
* @capability hud-lanes two HUD blocks stacked in one lane, plus a second lane beside it
|
|
16
|
+
*/
|
|
17
|
+
export function HudLanesPreview({ className }) {
|
|
18
|
+
const layout = useHudLayout();
|
|
19
|
+
return (_jsx("div", { className: className, style: {
|
|
20
|
+
position: "relative",
|
|
21
|
+
width: "100%",
|
|
22
|
+
height: "100%",
|
|
23
|
+
overflow: "hidden",
|
|
24
|
+
background: "radial-gradient(ellipse at 30% 20%, #3b5a46, #1b2a24 55%, #0f1714)",
|
|
25
|
+
}, children: _jsx(HudThemeRoot, { children: _jsxs(HudCanvas, { layout: layout, editChord: false, children: [_jsx(HudPanel, { id: "vitals", lane: "status", anchor: "top-left", width: 260, height: 104, children: _jsx(HudFrame, { title: "Warden \u00B7 Lv 12", padding: 10, children: _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [_jsx(Chip, { label: "HP", value: "78 / 100" }), _jsx(Chip, { label: "MP", value: "50 / 80" }), _jsx(Chip, { label: "Lantern", value: "burning" })] }) }) }), _jsx(HudPanel, { id: "quest-log", lane: "status", anchor: "top-left", order: 1, width: 260, height: 132, children: _jsx(HudSurface, { title: "Expedition", padding: 10, children: _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [_jsx(Chip, { label: "Reach the drowned keep", value: "1 / 3" }), _jsx(Chip, { label: "Recover the tide sigil", value: "0 / 1" }), _jsx(Chip, { label: "Return to camp", value: "\u2014" })] }) }) }), _jsx(HudPanel, { id: "party", lane: "roster", anchor: "top-left", width: 190, height: 248, children: _jsx(HudSurface, { title: "Party", padding: 10, children: _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [_jsx(Chip, { label: "Mara", value: "Stable" }), _jsx(Chip, { label: "Osk", value: "Wounded" }), _jsx(Chip, { label: "Bel", value: "Stable" }), _jsx(Chip, { label: "Tallow", value: "Missing" })] }) }) }), _jsx(HudPanel, { id: "radar", lane: "radar", anchor: "top-right", width: 180, height: 180, children: _jsx(HudSurface, { title: "Survey", padding: 10, children: _jsx("div", { style: {
|
|
26
|
+
height: 122,
|
|
27
|
+
borderRadius: 999,
|
|
28
|
+
border: "1px solid var(--jg-slot-border, rgba(255,255,255,0.2))",
|
|
29
|
+
background: "radial-gradient(circle at 50% 50%, rgba(120,200,160,0.24), rgba(0,0,0,0.35))",
|
|
30
|
+
} }) }) }), _jsx(HudPanel, { id: "hint", lane: "radar", anchor: "top-right", order: 1, width: 180, height: 64, children: _jsx(HudSurface, { padding: 10, children: _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: [_jsx(Chip, { label: "Hold", value: "Lantern up" }), _jsx(Chip, { label: "Tide", value: "Rising" })] }) }) })] }) }) }));
|
|
31
|
+
}
|
package/dist/hudLayout.d.ts
CHANGED
|
@@ -64,9 +64,16 @@ export declare function HudCanvas({ layout, editChord, compactScale, showDuring,
|
|
|
64
64
|
*
|
|
65
65
|
* @capability hud-panel anchored HUD slot — drop any widget in a named, player-movable panel
|
|
66
66
|
*/
|
|
67
|
-
export declare function HudPanel({ id, anchor, order, compact: compactMode, chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region, width, height, type, className, style, children, }: {
|
|
67
|
+
export declare function HudPanel({ id, anchor, lane, order, compact: compactMode, chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region, width, height, type, className, style, children, }: {
|
|
68
68
|
id: string;
|
|
69
69
|
anchor?: HudAnchor;
|
|
70
|
+
/**
|
|
71
|
+
* Placement lane inside the anchor. Blocks naming the same lane stack in
|
|
72
|
+
* `order` and can never overlap; lanes in one anchor sit side by side. A block
|
|
73
|
+
* that has been moved (drag/edit, or a scene-document `ui.panels` entry) leaves
|
|
74
|
+
* the lane and keeps its authored position, so a game override always wins.
|
|
75
|
+
*/
|
|
76
|
+
lane?: string;
|
|
70
77
|
/** Stack position within the region, ascending outward from the screen edge. Default 0. */
|
|
71
78
|
order?: number;
|
|
72
79
|
/** Behavior on compact displays. Default `"keep"`. */
|
package/dist/hudLayout.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import { HUD_ANCHOR_FRACTIONS, anchoredPlacement, createHudLayout, isPanelDraggable, registerActiveHudLayout, } from "@jgengine/core/ui/hudLayout";
|
|
5
5
|
import { resizePanelSize, resolvePanelResize, } from "@jgengine/core/ui/hudDocument";
|
|
@@ -9,6 +9,7 @@ import { useDisplayProfile } from "./display.js";
|
|
|
9
9
|
import { useEngineState } from "./engineStore.js";
|
|
10
10
|
import { useGameLayoutMode, useRegisterLayoutRegion } from "./gameViewport.js";
|
|
11
11
|
import { useOptionalGamePhase } from "./hooks.js";
|
|
12
|
+
import { formatHudOverlapReport, hudBlockOverlaps, resolveHudLanes } from "./hudLanes.js";
|
|
12
13
|
import { useHudLayoutPersist } from "./hudLayoutPersist.js";
|
|
13
14
|
import { useHudViewport } from "./hudViewport.js";
|
|
14
15
|
import { hudFont, hudType } from "./hudTokens.js";
|
|
@@ -34,13 +35,21 @@ const WINDOW_ATTR = "data-hud-window";
|
|
|
34
35
|
/**
|
|
35
36
|
* Dev-only, prod-safe. The on-screen boundary overlay must never reach players,
|
|
36
37
|
* so anything we cannot positively identify as a dev build resolves to `false`.
|
|
38
|
+
*
|
|
39
|
+
* `import.meta.env.DEV` is read as a literal member chain because Vite replaces
|
|
40
|
+
* exactly those words at transform time; reading `env` off a cast `import.meta`
|
|
41
|
+
* survives to the browser as `undefined` and every dev-only report stays dark.
|
|
37
42
|
*/
|
|
38
43
|
function isDevBuild() {
|
|
39
44
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
// Literal members on purpose: Vite replaces exactly `import.meta.env.DEV`.
|
|
46
|
+
// Inlined as plain `@ts-ignore` because a workspace-wide type program that
|
|
47
|
+
// includes `vite/client` would otherwise fail an unused-directive check.
|
|
48
|
+
// @ts-ignore -- `vite/client` types are not part of this package's own tsconfig
|
|
49
|
+
if (import.meta.env.DEV === true)
|
|
42
50
|
return true;
|
|
43
|
-
|
|
51
|
+
// @ts-ignore -- `vite/client` types are not part of this package's own tsconfig
|
|
52
|
+
if (import.meta.env.PROD === true)
|
|
44
53
|
return false;
|
|
45
54
|
}
|
|
46
55
|
catch {
|
|
@@ -108,6 +117,21 @@ function collectBoundarySurfaces(canvas) {
|
|
|
108
117
|
}
|
|
109
118
|
return out;
|
|
110
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* The nearest ancestor that actually has a box. `HudThemeRoot` is
|
|
122
|
+
* `display: contents`, so `canvas.parentElement` can be a layout-free element
|
|
123
|
+
* whose rect is 0x0 — measuring it would silently disable every check below.
|
|
124
|
+
*/
|
|
125
|
+
function measurableHost(canvas) {
|
|
126
|
+
let element = canvas.parentElement;
|
|
127
|
+
while (element !== null) {
|
|
128
|
+
const rect = element.getBoundingClientRect();
|
|
129
|
+
if (rect.width > 0 && rect.height > 0)
|
|
130
|
+
return element;
|
|
131
|
+
element = element.parentElement;
|
|
132
|
+
}
|
|
133
|
+
return canvas;
|
|
134
|
+
}
|
|
111
135
|
function sizeStyle(panel) {
|
|
112
136
|
if (panel === undefined)
|
|
113
137
|
return {};
|
|
@@ -197,6 +221,7 @@ export function useHudLayout(options) {
|
|
|
197
221
|
}, [layout, storageKey, documentUi]);
|
|
198
222
|
return layout;
|
|
199
223
|
}
|
|
224
|
+
const EMPTY_LANE_RECTS = {};
|
|
200
225
|
const HudCanvasContext = createContext(null);
|
|
201
226
|
function HudEditBar({ layout }) {
|
|
202
227
|
return (_jsxs("div", { "data-hud-edit-bar": "", style: {
|
|
@@ -305,7 +330,82 @@ export function HudCanvas({ layout, editChord, compactScale, showDuring, classNa
|
|
|
305
330
|
}
|
|
306
331
|
return refs;
|
|
307
332
|
}, []);
|
|
308
|
-
const
|
|
333
|
+
const [laneLayer, setLaneLayer] = useState(null);
|
|
334
|
+
const [laneFrame, setLaneFrame] = useState(null);
|
|
335
|
+
const [laneRevision, setLaneRevision] = useState(0);
|
|
336
|
+
const laneSlots = useRef(new Map());
|
|
337
|
+
const laneSizes = useRef(new Map());
|
|
338
|
+
const declareLane = useCallback((slot) => {
|
|
339
|
+
const previous = laneSlots.current.get(slot.id);
|
|
340
|
+
if (previous !== undefined &&
|
|
341
|
+
previous.anchor === slot.anchor &&
|
|
342
|
+
previous.lane === slot.lane &&
|
|
343
|
+
previous.order === slot.order &&
|
|
344
|
+
previous.width === slot.width &&
|
|
345
|
+
previous.height === slot.height &&
|
|
346
|
+
previous.rect === slot.rect) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
laneSlots.current.set(slot.id, slot);
|
|
350
|
+
setLaneRevision((value) => value + 1);
|
|
351
|
+
}, []);
|
|
352
|
+
const removeLane = useCallback((id) => {
|
|
353
|
+
if (!laneSlots.current.delete(id))
|
|
354
|
+
return;
|
|
355
|
+
laneSizes.current.delete(id);
|
|
356
|
+
setLaneRevision((value) => value + 1);
|
|
357
|
+
}, []);
|
|
358
|
+
const reportLaneSize = useCallback((id, size) => {
|
|
359
|
+
const previous = laneSizes.current.get(id);
|
|
360
|
+
if (previous !== undefined &&
|
|
361
|
+
Math.abs(previous.width - size.width) < 0.5 &&
|
|
362
|
+
Math.abs(previous.height - size.height) < 0.5) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
laneSizes.current.set(id, size);
|
|
366
|
+
setLaneRevision((value) => value + 1);
|
|
367
|
+
}, []);
|
|
368
|
+
const laneRects = useMemo(() => {
|
|
369
|
+
if (laneFrame === null || laneSlots.current.size === 0)
|
|
370
|
+
return EMPTY_LANE_RECTS;
|
|
371
|
+
const slots = [];
|
|
372
|
+
for (const slot of laneSlots.current.values()) {
|
|
373
|
+
const measured = laneSizes.current.get(slot.id);
|
|
374
|
+
slots.push(measured === undefined
|
|
375
|
+
? slot
|
|
376
|
+
: { ...slot, width: slot.width ?? measured.width, height: slot.height ?? measured.height });
|
|
377
|
+
}
|
|
378
|
+
return resolveHudLanes(slots, laneFrame, { gap: REGION_GAP }).rects;
|
|
379
|
+
}, [laneFrame, laneRevision]);
|
|
380
|
+
useEffect(() => {
|
|
381
|
+
if (laneLayer === null)
|
|
382
|
+
return;
|
|
383
|
+
const measure = () => {
|
|
384
|
+
const rect = laneLayer.getBoundingClientRect();
|
|
385
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
386
|
+
return;
|
|
387
|
+
setLaneFrame((prev) => prev !== null && Math.abs(prev.width - rect.width) < 0.5 && Math.abs(prev.height - rect.height) < 0.5
|
|
388
|
+
? prev
|
|
389
|
+
: { width: rect.width, height: rect.height });
|
|
390
|
+
};
|
|
391
|
+
measure();
|
|
392
|
+
if (typeof ResizeObserver === "undefined")
|
|
393
|
+
return;
|
|
394
|
+
const observer = new ResizeObserver(measure);
|
|
395
|
+
observer.observe(laneLayer);
|
|
396
|
+
return () => observer.disconnect();
|
|
397
|
+
}, [laneLayer]);
|
|
398
|
+
const value = useMemo(() => ({
|
|
399
|
+
layout,
|
|
400
|
+
canvasRef,
|
|
401
|
+
regions,
|
|
402
|
+
compact,
|
|
403
|
+
laneLayer,
|
|
404
|
+
laneRects,
|
|
405
|
+
declareLane,
|
|
406
|
+
removeLane,
|
|
407
|
+
reportLaneSize,
|
|
408
|
+
}), [layout, regions, compact, laneLayer, laneRects, declareLane, removeLane, reportLaneSize]);
|
|
309
409
|
const editing = useEngineState(layout).editing;
|
|
310
410
|
useEffect(() => registerActiveHudLayout(layout), [layout]);
|
|
311
411
|
const chordEnabled = editChord !== false;
|
|
@@ -350,7 +450,7 @@ export function HudCanvas({ layout, editChord, compactScale, showDuring, classNa
|
|
|
350
450
|
useEffect(() => {
|
|
351
451
|
if (!fitEnabled)
|
|
352
452
|
return;
|
|
353
|
-
const host = canvasRef.current
|
|
453
|
+
const host = canvasRef.current === null ? null : measurableHost(canvasRef.current);
|
|
354
454
|
if (host === undefined || host === null || typeof ResizeObserver === "undefined")
|
|
355
455
|
return;
|
|
356
456
|
const measure = () => {
|
|
@@ -365,13 +465,14 @@ export function HudCanvas({ layout, editChord, compactScale, showDuring, classNa
|
|
|
365
465
|
}, [fitEnabled]);
|
|
366
466
|
useEffect(() => {
|
|
367
467
|
const canvas = canvasRef.current;
|
|
368
|
-
|
|
369
|
-
if (canvas === null || canvas === undefined || host === undefined || host === null)
|
|
468
|
+
if (canvas === null)
|
|
370
469
|
return;
|
|
470
|
+
const host = measurableHost(canvas);
|
|
371
471
|
if (typeof requestAnimationFrame !== "function")
|
|
372
472
|
return;
|
|
373
473
|
let frame = 0;
|
|
374
474
|
let lastReport = "";
|
|
475
|
+
let lastOverlapReport = "";
|
|
375
476
|
const dev = isDevBuild();
|
|
376
477
|
const check = () => {
|
|
377
478
|
frame = 0;
|
|
@@ -398,21 +499,46 @@ export function HudCanvas({ layout, editChord, compactScale, showDuring, classNa
|
|
|
398
499
|
// cross an edge — a closed/parked/off-screen menu is never an offender.
|
|
399
500
|
const escaped = overflowingPanels(surfaces, viewportSize);
|
|
400
501
|
const report = escaped.length === 0 ? "" : JSON.stringify(escaped);
|
|
401
|
-
if (report
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
502
|
+
if (report !== lastReport) {
|
|
503
|
+
lastReport = report;
|
|
504
|
+
if (report === "") {
|
|
505
|
+
canvas.removeAttribute("data-hud-overflow");
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
canvas.setAttribute("data-hud-overflow", report);
|
|
509
|
+
console.warn(`[jgengine] HUD UI escapes the ${Math.round(hostRect.width)}x${Math.round(hostRect.height)} viewport: ${report}`);
|
|
510
|
+
}
|
|
511
|
+
if (dev) {
|
|
512
|
+
setOffenders(escaped.map((overflow) => {
|
|
513
|
+
const rect = surfaces.find((s) => s.id === overflow.id)?.rect;
|
|
514
|
+
return { ...overflow, x: rect?.x ?? 0, y: rect?.y ?? 0, width: rect?.width ?? 0, height: rect?.height ?? 0 };
|
|
515
|
+
}));
|
|
516
|
+
}
|
|
410
517
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
518
|
+
const blocks = [...canvas.querySelectorAll("[data-hud-panel]")]
|
|
519
|
+
.filter((el) => isSurfaceShown(el))
|
|
520
|
+
.map((el) => {
|
|
521
|
+
const rect = el.getBoundingClientRect();
|
|
522
|
+
return {
|
|
523
|
+
id: surfaceId(el),
|
|
524
|
+
rect: {
|
|
525
|
+
x: rect.left - hostRect.left,
|
|
526
|
+
y: rect.top - hostRect.top,
|
|
527
|
+
width: rect.width,
|
|
528
|
+
height: rect.height,
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
});
|
|
532
|
+
const overlapReport = dev ? formatHudOverlapReport(hudBlockOverlaps(blocks), viewportSize) : "";
|
|
533
|
+
if (overlapReport !== lastOverlapReport) {
|
|
534
|
+
lastOverlapReport = overlapReport;
|
|
535
|
+
if (overlapReport === "") {
|
|
536
|
+
canvas.removeAttribute("data-hud-overlap");
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
canvas.setAttribute("data-hud-overlap", overlapReport);
|
|
540
|
+
console.warn(`[jgengine] ${overlapReport}`);
|
|
541
|
+
}
|
|
416
542
|
}
|
|
417
543
|
};
|
|
418
544
|
const schedule = () => {
|
|
@@ -462,7 +588,14 @@ export function HudCanvas({ layout, editChord, compactScale, showDuring, classNa
|
|
|
462
588
|
pointerEvents: "none",
|
|
463
589
|
zIndex: 1,
|
|
464
590
|
...REGION_LAYOUT[anchor],
|
|
465
|
-
} }, anchor))),
|
|
591
|
+
} }, anchor))), _jsx("div", { "data-hud-lane-layer": "", style: {
|
|
592
|
+
position: "absolute",
|
|
593
|
+
inset: 0,
|
|
594
|
+
padding: `${PAD_TOP} ${PAD_RIGHT} ${PAD_BOTTOM} ${PAD_LEFT}`,
|
|
595
|
+
boxSizing: "border-box",
|
|
596
|
+
pointerEvents: "none",
|
|
597
|
+
zIndex: 1,
|
|
598
|
+
}, children: _jsx("div", { ref: setLaneLayer, style: { position: "relative", width: "100%", height: "100%" } }) }), children, offenders.length > 0 ? (_jsx("div", { "data-hud-overflow-overlay": "", style: { position: "absolute", inset: 0, pointerEvents: "none", zIndex: EDIT_BAR_Z - 1 }, children: offenders.map((o) => (_jsx("div", { style: {
|
|
466
599
|
position: "absolute",
|
|
467
600
|
left: o.x / scale,
|
|
468
601
|
top: o.y / scale,
|
|
@@ -537,13 +670,13 @@ function HudChip({ label, anchor, children, }) {
|
|
|
537
670
|
*
|
|
538
671
|
* @capability hud-panel anchored HUD slot — drop any widget in a named, player-movable panel
|
|
539
672
|
*/
|
|
540
|
-
export 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, }) {
|
|
673
|
+
export 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, }) {
|
|
541
674
|
const ctx = useContext(HudCanvasContext);
|
|
542
675
|
const phase = useOptionalGamePhase();
|
|
543
676
|
const layoutMode = useGameLayoutMode();
|
|
544
677
|
if (ctx === null)
|
|
545
678
|
throw new Error("HudPanel must be rendered inside a HudCanvas");
|
|
546
|
-
const { layout, canvasRef, regions, compact } = ctx;
|
|
679
|
+
const { layout, canvasRef, regions, compact, laneLayer, laneRects, declareLane, removeLane, reportLaneSize } = ctx;
|
|
547
680
|
const onMobile = isMobileMode(layoutMode);
|
|
548
681
|
const registeredOnRef = useRef(null);
|
|
549
682
|
if (registeredOnRef.current !== layout) {
|
|
@@ -582,6 +715,59 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
582
715
|
// A flowing panel portals into its anchor region, which HudCanvas only provides after its first commit.
|
|
583
716
|
const awaitingRegion = panel !== undefined && (compact || !panel.moved) && regions[compact ? anchor : panel.placement.anchor] === undefined;
|
|
584
717
|
useRegisterLayoutRegion(regionSpec, rootRef, region && !regionHidden && !awaitingRegion);
|
|
718
|
+
const laneActive = lane !== undefined && !regionHidden && !(panel !== undefined && panel.moved);
|
|
719
|
+
const laneRect = lane === undefined ? undefined : laneRects[id];
|
|
720
|
+
const [laneSize, setLaneSize] = useState(null);
|
|
721
|
+
useEffect(() => {
|
|
722
|
+
if (!laneActive)
|
|
723
|
+
return;
|
|
724
|
+
declareLane({
|
|
725
|
+
id,
|
|
726
|
+
anchor: panel?.placement.anchor ?? anchor,
|
|
727
|
+
lane,
|
|
728
|
+
order,
|
|
729
|
+
width: panel?.width ?? width,
|
|
730
|
+
height: panel?.height ?? height,
|
|
731
|
+
});
|
|
732
|
+
return () => removeLane(id);
|
|
733
|
+
}, [
|
|
734
|
+
laneActive,
|
|
735
|
+
declareLane,
|
|
736
|
+
removeLane,
|
|
737
|
+
id,
|
|
738
|
+
panel?.placement.anchor,
|
|
739
|
+
lane,
|
|
740
|
+
order,
|
|
741
|
+
panel?.width,
|
|
742
|
+
width,
|
|
743
|
+
panel?.height,
|
|
744
|
+
height,
|
|
745
|
+
]);
|
|
746
|
+
const laneMounted = laneRect !== undefined;
|
|
747
|
+
useEffect(() => {
|
|
748
|
+
if (!laneActive || !laneMounted)
|
|
749
|
+
return;
|
|
750
|
+
const element = rootRef.current;
|
|
751
|
+
if (element === null)
|
|
752
|
+
return;
|
|
753
|
+
const measure = () => {
|
|
754
|
+
const rect = element.getBoundingClientRect();
|
|
755
|
+
if (rect.width <= 0 && rect.height <= 0)
|
|
756
|
+
return;
|
|
757
|
+
setLaneSize((previous) => previous !== null &&
|
|
758
|
+
Math.abs(previous.width - rect.width) < 0.5 &&
|
|
759
|
+
Math.abs(previous.height - rect.height) < 0.5
|
|
760
|
+
? previous
|
|
761
|
+
: { width: rect.width, height: rect.height });
|
|
762
|
+
reportLaneSize(id, { width: rect.width, height: rect.height });
|
|
763
|
+
};
|
|
764
|
+
measure();
|
|
765
|
+
if (typeof ResizeObserver === "undefined")
|
|
766
|
+
return;
|
|
767
|
+
const observer = new ResizeObserver(measure);
|
|
768
|
+
observer.observe(element);
|
|
769
|
+
return () => observer.disconnect();
|
|
770
|
+
}, [laneActive, laneMounted, id, reportLaneSize]);
|
|
585
771
|
const onPointerDown = (event) => {
|
|
586
772
|
if (!draggable || event.button !== 0)
|
|
587
773
|
return;
|
|
@@ -712,6 +898,31 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
712
898
|
if (!hudVisibleInPhase(showDuring, phase))
|
|
713
899
|
return null;
|
|
714
900
|
const authoredSize = sizeStyle(panel);
|
|
901
|
+
if (laneActive) {
|
|
902
|
+
if (laneRect === undefined || laneLayer === null)
|
|
903
|
+
return null;
|
|
904
|
+
const declaredWidth = panel.width ?? width;
|
|
905
|
+
const declaredHeight = panel.height ?? height;
|
|
906
|
+
const settled = laneSize !== null || declaredWidth !== undefined || declaredHeight !== undefined;
|
|
907
|
+
const chipped = compact && compactMode === "chip";
|
|
908
|
+
const passThrough = interactive === false && !editing && !chipped;
|
|
909
|
+
return createPortal(_jsxs("div", { ref: rootRef, "data-hud-panel": id, "data-hud-lane": lane, "data-dragging": dragging ? "" : undefined, "data-hud-priority": priority, "data-hud-mobile-behavior": mobileBehavior, "data-hud-type": panel.type ?? type, className: className, onPointerDown: onPointerDown, onClickCapture: onClickCapture, style: {
|
|
910
|
+
position: "absolute",
|
|
911
|
+
left: laneRect.x,
|
|
912
|
+
top: laneRect.y,
|
|
913
|
+
...(declaredWidth === undefined ? {} : { width: declaredWidth }),
|
|
914
|
+
...(declaredHeight === undefined ? {} : { height: declaredHeight }),
|
|
915
|
+
visibility: settled ? undefined : "hidden",
|
|
916
|
+
pointerEvents: passThrough ? "none" : "auto",
|
|
917
|
+
touchAction: draggable ? "none" : undefined,
|
|
918
|
+
userSelect: editing || dragging ? "none" : undefined,
|
|
919
|
+
cursor: dragging ? "grabbing" : editing ? "grab" : undefined,
|
|
920
|
+
outline: editing ? "1px dashed rgba(255, 255, 255, 0.6)" : undefined,
|
|
921
|
+
outlineOffset: editing ? 2 : undefined,
|
|
922
|
+
boxSizing: "border-box",
|
|
923
|
+
...style,
|
|
924
|
+
}, children: [chipped ? (_jsx(HudChip, { label: chip ?? id, anchor: anchor, children: children })) : (children), editingChrome] }), laneLayer);
|
|
925
|
+
}
|
|
715
926
|
const flow = compact || !panel.moved;
|
|
716
927
|
if (flow) {
|
|
717
928
|
const regionEl = regions[compact ? anchor : panel.placement.anchor];
|
package/dist/index.d.ts
CHANGED
|
@@ -67,6 +67,7 @@ export * from "./cards.js";
|
|
|
67
67
|
export * from "./actionHud.js";
|
|
68
68
|
export * from "./selectionHud.js";
|
|
69
69
|
export * from "./hudLayout.js";
|
|
70
|
+
export * from "./hudLanes.js";
|
|
70
71
|
export * from "./hudLayoutPersist.js";
|
|
71
72
|
export * from "./hudViewport.js";
|
|
72
73
|
export * from "./gameViewport.js";
|
package/dist/index.js
CHANGED
|
@@ -67,6 +67,7 @@ export * from "./cards.js";
|
|
|
67
67
|
export * from "./actionHud.js";
|
|
68
68
|
export * from "./selectionHud.js";
|
|
69
69
|
export * from "./hudLayout.js";
|
|
70
|
+
export * from "./hudLanes.js";
|
|
70
71
|
export * from "./hudLayoutPersist.js";
|
|
71
72
|
export * from "./hudViewport.js";
|
|
72
73
|
export * from "./gameViewport.js";
|
package/dist/previewFixtures.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BarsPreview } from "./barsPreview.js";
|
|
2
2
|
import { HudChromePreview } from "./hudChromePreview.js";
|
|
3
|
+
import { HudLanesPreview } from "./hudLanesPreview.js";
|
|
3
4
|
import { HudThemePreview } from "./hudThemePreview.js";
|
|
4
5
|
import { IconsPreview } from "./iconsPreview.js";
|
|
5
6
|
import { StreetGeometryPreview } from "./streetGeometryPreview.js";
|
|
@@ -20,6 +21,11 @@ import { ScrollRailPreview } from "./scrollRailPreview.js";
|
|
|
20
21
|
* declares.
|
|
21
22
|
*/
|
|
22
23
|
export const PREVIEW_FIXTURES = {
|
|
24
|
+
HudLanesPreview: {
|
|
25
|
+
name: "HudLanesPreview",
|
|
26
|
+
description: "Two HUD blocks stacked in one lane, a second lane beside it, and the mirrored right-hand lane.",
|
|
27
|
+
component: HudLanesPreview,
|
|
28
|
+
},
|
|
23
29
|
ScrollRailPreview: { name: "ScrollRailPreview", description: "Reachable caller-owned settings categories in shared scrolling chrome.", component: ScrollRailPreview },
|
|
24
30
|
HudChromePreview: {
|
|
25
31
|
name: "HudChromePreview",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "React UI layer for JGengine: GameProvider, hooks, and headless primitives over @jgengine/core.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"styles": "bun scripts/styles.ts"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@jgengine/core": "^0.
|
|
38
|
+
"@jgengine/core": "^0.32.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"react": "^19.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.32.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.32.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
289
289
|
|
|
290
290
|
## @jgengine/core/runtime/adapter
|
|
291
291
|
|
|
@@ -410,6 +410,7 @@
|
|
|
410
410
|
- `GameContextRace` (interface): interface GameContextRace { state(id: string, config?: RaceStateConfig): RaceState } — ⚠ undocumented
|
|
411
411
|
- `GameContextTurn` (interface): interface GameContextTurn { loop(id: string, config?: TurnLoopConfig): TurnLoop } — ⚠ undocumented
|
|
412
412
|
- `GameContextWorld` (interface): interface GameContextWorld { ground: TerrainField; groundHeightAt(x: number, z: number): number; solids: WorldSolids } — ⚠ undocumented
|
|
413
|
+
- `GameFeedback` (interface): interface GameFeedback — The game-announced feel seam on `ctx.game`: name a moment the engine has no event for and the shell plays its cue's sound, rumble, camera kick and caption. The reaction table lives in the shell, so a game never encodes what a hit or a wave should sound like.
|
|
413
414
|
- `HitReactionInput` (interface): interface HitReactionInput { from: string; to: string; config: HitReactionConfig | ImpactPresetName; power?: number } — ⚠ undocumented · used by `resolveHitReaction` (@jgengine/core/combat): Resolves hit feel (hitstop, knockback impulse, camera shake) from either a named `impactPresets` event (`resolveHitReaction("explosion", inp…
|
|
414
415
|
- `MoveTowardCommitOptions` (interface): interface MoveTowardCommitOptions extends MoveTowardOptions — Options for {@link SceneEntityContext.moveTowardCommit}: {@link MoveTowardOptions} plus an optional facing turn.
|
|
415
416
|
- `SceneEntityContext` (interface): interface SceneEntityContext { spawn(name: string, options?: SpawnOptions): string; despawn(instanceId: string): boolean; update: EntityStore["update"]; setPose(instanceId: string, pose: EntityPose): boolean; setVelocity: EntityStore["setVelocity"]; setPoseConstraint: … — ⚠ undocumented
|
|
@@ -728,7 +729,7 @@
|
|
|
728
729
|
- `GameLoop` (interface): interface GameLoop<TContext = unknown> — Lifecycle hooks a game implements to drive init, per-tick simulation, and player join/leave.
|
|
729
730
|
- `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
731
|
- `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; /**
|
|
732
|
+
- `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
733
|
- `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
734
|
- `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
735
|
- `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.
|
|
@@ -46,6 +46,7 @@ Existing React games keep their entity store and use the focused
|
|
|
46
46
|
|
|
47
47
|
- **Refusals and notices are engine feedback, not per-game toasts.** Return `{ reason: "insufficient-gold", message: "Needs 40 more gold" }` from a command's `validate`: the shell shows `message` in the shared notice feed with the deny cue and stacks repeats. Push other player messages with `ctx.game.notices.push({ text, tone })` (info/success/warn/deny) from game logic instead of a hand-built toast list. The stack renders inside the HUD theme root, so it takes the game's `hudTheme` colours and font; a top or bottom stack moves clear of `HudCanvas` panels and touch controls in its column and sits above `ModalHost` (z 85). Tune or hide it with `notices: { placement, offset, avoidHud, zIndex, cues, hidden }` (`hidden` keeps cues and lets the game render `useNoticeList(ctx.game.notices)` itself); `push({ text, silent: true })` skips one notice's sound.
|
|
48
48
|
- **UI sounds come from `useUiCue()`.** Engine primitives already sound through the shell; for game-owned markup call `cue("click" | "hover" | "open" | "close" | "confirm" | "deny" | "notify")` from buttons and panels; replace a default by declaring `audio.sounds["ui.click"]`, or silence with `audio.uiCues: { hover: false }` / `false`.
|
|
49
|
+
- **A hit, kill, pickup, objective, wave and refusal react without any sound authored.** The shell maps the events the engine already announces (`combat.damage`, `entity.died`, `worldItem.picked_up`, `quest.completed`, `stat.levelUp`, `command.refused`) to a `feel.<cue>` reaction - sound, gamepad pulse, camera kick and caption - and merges procedural defaults on an `sfx` bus. Replace a sound by declaring `audio.sounds["feel.hit"]`, tune or silence with `audio.feedbackCues`, and announce a wave through `attachWaveFeedback(runner, ctx.game.feedback)` or any custom moment with `ctx.game.feedback.cue("wave", "WAVE 3")`. Captions follow the accessibility setting; sound follows the `sfx` bus volume and mute rows in Settings.
|
|
49
50
|
- Every rejected verb displays its reason on the triggering surface. Use `useServerSession(transport, gameId)` for join lifecycle. Join failure blocks play with `JoinGate` and retry; returning `{ ok: false }` without feedback is incomplete.
|
|
50
51
|
- Placement previews display claim cost and affordability inline with `TerritoryOverlay`.
|
|
51
52
|
- `ChatPanel` owns collapsed/expanded state, own-message styling, Enter focus and Escape blur; standalone hosts pass `messages`, `userId`, and `onSend` (typed `{ ok, reason }`), while context-backed games use channel props. Use native inputs so shell typing-target suspension applies.
|
|
@@ -786,6 +786,9 @@
|
|
|
786
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.
|
|
787
787
|
- `HealthBar` (const): const HealthBar: (props: AtomicBarProps) => React.JSX.Element — Player/enemy health — danger-flashes to `--jg-health-low` below a quarter full.
|
|
788
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}.
|
|
789
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.
|
|
790
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.
|
|
791
794
|
- `HudEditChord` (interface): interface HudEditChord { hold: string; press: string } — ⚠ undocumented · used by `HudCanvas`: Full-viewport HUD surface.
|
|
@@ -793,9 +796,13 @@
|
|
|
793
796
|
- `HudFrameProps` (interface): interface HudFrameProps — Props for {@link HudFrame}.
|
|
794
797
|
- `HudFrameShape` (type): type HudFrameShape = "rounded" | "circle" | "square" — Frame corner shape — `rounded` (the variation's default radius), `circle` (fully round), or `square` (no radius).
|
|
795
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.
|
|
796
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.
|
|
797
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.
|
|
798
|
-
- `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.
|
|
799
806
|
- `HudSkin` (interface): interface HudSkin — A game-authored visual skin layered over a {@link HudTheme}.
|
|
800
807
|
- `HudSkinMaterial` (interface): interface HudSkinMaterial — Optional raster-backed materials for shared HUD chrome.
|
|
801
808
|
- `HudSkinPreview` (function): function HudSkinPreview({ className }: { className?: string }): React.JSX.Element — Procedural two-skin fixture for visual regression captures.
|
|
@@ -1033,8 +1040,10 @@
|
|
|
1033
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.
|
|
1034
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`).
|
|
1035
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.
|
|
1036
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.
|
|
1037
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.
|
|
1038
1047
|
- `hudSkinVars` (function): function hudSkinVars(skin?: HudSkin): CSSProperties — Converts a skin into the CSS variables consumed by shared HUD components.
|
|
1039
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.
|
|
1040
1049
|
- `hudThemeVars` (function): function hudThemeVars(theme: HudTheme): CSSProperties — Emits theme CSS properties, resetting inherited bar fills when absent. Spread onto a HUD ancestor.
|
|
@@ -1049,6 +1058,7 @@
|
|
|
1049
1058
|
- `previewFixtureNames` (function): function previewFixtureNames(): string[] — Sorted list of registered fixture names, for discovery/listing.
|
|
1050
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.
|
|
1051
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.
|
|
1052
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.
|
|
1053
1063
|
- `resolvePreviewFixture` (function): function resolvePreviewFixture(name: string): PreviewFixture | undefined — Resolve a fixture by name, or `undefined` when it is not registered.
|
|
1054
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`.
|
|
@@ -1533,12 +1543,29 @@
|
|
|
1533
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`.
|
|
1534
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}.
|
|
1535
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
|
+
|
|
1536
1563
|
## @jgengine/react/hudLayout
|
|
1537
1564
|
|
|
1538
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.
|
|
1539
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.
|
|
1540
1567
|
- `HudEditChord` (interface): interface HudEditChord { hold: string; press: string } — ⚠ undocumented · used by `HudCanvas`: Full-viewport HUD surface.
|
|
1541
|
-
- `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.
|
|
1542
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).
|
|
1543
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.
|
|
1544
1571
|
|
|
@@ -2112,15 +2139,64 @@
|
|
|
2112
2139
|
- `EntityAudioEmitters` (function): function EntityAudioEmitters({ engine, entitySounds, }: { engine: AudioEngine; entitySounds: Record<string, string> | undefined; }): null — ⚠ undocumented
|
|
2113
2140
|
- `ObjectAudioEmitters` (function): function ObjectAudioEmitters({ engine, objectSounds, }: { engine: AudioEngine; objectSounds: Record<string, string> | undefined; }): null — ⚠ undocumented
|
|
2114
2141
|
|
|
2142
|
+
## @jgengine/shell/audio/CaptionOverlay
|
|
2143
|
+
|
|
2144
|
+
- `CaptionOverlay` (function): function CaptionOverlay({ feed, max = 2, zIndex = 84, }: { feed: CaptionFeed; /** How many lines to show at once. Default 2. */ max?: number; zIndex?: number; }): ReactNode — The captions line: a bounded, bottom-centred stack that renders only while the accessibility captions setting is on. Reads HudTheme tokens, so a game's theme reskins it, and lives in the same overlay layer as the notice stack.
|
|
2145
|
+
|
|
2146
|
+
## @jgengine/shell/audio/FeedbackHost
|
|
2147
|
+
|
|
2148
|
+
- `FeedbackHost` (function): function FeedbackHost({ ctx, engine }: { ctx: GameContext; engine: AudioEngine }): ReactNode — Mounts the engine's feel layer for one game: announced events become a sound, a gamepad pulse, a camera kick and a caption line; the audio context unlocks on the first gesture; and the caption stack follows the accessibility setting. Games supply sounds, never the rules.
|
|
2149
|
+
|
|
2115
2150
|
## @jgengine/shell/audio/audioEngine
|
|
2116
2151
|
|
|
2117
2152
|
- `AudioEmitterHandle` (interface): interface AudioEmitterHandle { isPlaying?(): boolean; setPosition(position: Vec3): void; setRate(rate: number): void; setGain(gain: number): void; setLowpass(hz: number): void; setHighpass(hz: number): void; setVelocity(velocity: Vec3): void; stop(): void } — ⚠ undocumented
|
|
2118
|
-
- `AudioEngine` (interface): interface AudioEngine { setListenerPose(pose: ListenerPose | Vec3): void; setVoiceLimit(maxTotal: number, overflow?: VoiceAllocatorConfig["overflow"]): void;
|
|
2153
|
+
- `AudioEngine` (interface): interface AudioEngine { setListenerPose(pose: ListenerPose | Vec3): void; setVoiceLimit(maxTotal: number, overflow?: VoiceAllocatorConfig["overflow"]): void; play(soundId: string, position?: Vec3): void; playOneShot(soundId: string, position?: Vec3): void; playL… — ⚠ undocumented · used by `createAudioEngine`: Game-owned sound catalog playback with bounded SFX reservations, spatial panning, and shared buses.
|
|
2154
|
+
- `AudioLevel` (interface): interface AudioLevel — Master-output level, `0..1`. Pooled by {@link AudioEngine.measure} - read it, don't retain it.
|
|
2119
2155
|
- `AudioSceneConfig` (interface): interface AudioSceneConfig { sounds?: Record<string, SoundDef>; maxVoices?: number; voiceOverflow?: VoiceAllocatorConfig["overflow"]; voiceAllocator?: VoiceAllocator; buses?: Record<string, AudioBusDef>; music?: Record<string, MusicTheme>; musicBus?: string } — ⚠ undocumented · used by `createAudioEngine`: Game-owned sound catalog playback with bounded SFX reservations, spatial panning, and shared buses.
|
|
2120
2156
|
- `ListenerPose` (interface): interface ListenerPose — Position and orientation for a spatial-audio listener.
|
|
2157
|
+
- `MAX_PENDING_CUES` (const): const MAX_PENDING_CUES: 16 — Cues held while the browser keeps the audio context suspended, before any user gesture.
|
|
2121
2158
|
- `Vec3` (interface): interface Vec3 { x: number; y: number; z: number } — ⚠ undocumented · used by `coverPoints` (@jgengine/core/ai/tacticalQueries): Returns boundary locations that are hidden from a threat.
|
|
2122
2159
|
- `createAudioEngine` (function): function createAudioEngine(config: AudioSceneConfig = {}): AudioEngine — Game-owned sound catalog playback with bounded SFX reservations, spatial panning, and shared buses.
|
|
2123
2160
|
|
|
2161
|
+
## @jgengine/shell/audio/audioProbe
|
|
2162
|
+
|
|
2163
|
+
- `AudioProbe` (interface): interface AudioProbe — The read-only handle a capture script or devtools uses to prove the mix is producing sound.
|
|
2164
|
+
- `installAudioProbe` (function): function installAudioProbe(engine: AudioEngine): () => void — Publish the audio engine's own level readout on `window.__jgengineAudio`, so a headless capture can state a measured RMS instead of guessing from a screenshot. Reads only; it never plays or changes a bus.
|
|
2165
|
+
|
|
2166
|
+
## @jgengine/shell/audio/busMix
|
|
2167
|
+
|
|
2168
|
+
- `MASTER_MUTE_SETTING_ID` (const): const MASTER_MUTE_SETTING_ID: "sound.muted" — Persisted mute for the whole mix, next to `sound.master`.
|
|
2169
|
+
- `busMuteSettingId` (function): function busMuteSettingId(busId: string): string — Persisted mute for one bus, next to its `busVolumeSettingId(busId)` volume.
|
|
2170
|
+
|
|
2171
|
+
## @jgengine/shell/audio/captions
|
|
2172
|
+
|
|
2173
|
+
- `Caption` (interface): interface Caption — One caption line as the overlay renders it.
|
|
2174
|
+
- `CaptionFeed` (interface): interface CaptionFeed — A capped, self-expiring list of caption lines that the accessibility captions setting turns on.
|
|
2175
|
+
- `CaptionFeedConfig` (interface): interface CaptionFeedConfig — Tunables for {@link createCaptionFeed}; all retunable at runtime.
|
|
2176
|
+
- `createCaptionFeed` (function): function createCaptionFeed(config: CaptionFeedConfig = {}): CaptionFeed — The caption list the feedback seam writes to and the HUD overlay reads: bounded, self-expiring, and deduped so three hits read as "Hit x3" rather than three copies. Pure state with an injected clock, so it runs headless.
|
|
2177
|
+
|
|
2178
|
+
## @jgengine/shell/audio/eventFeedback
|
|
2179
|
+
|
|
2180
|
+
- `AnyFeedbackBinding` (type): type AnyFeedbackBinding = { [Name in FeedbackEventName]: FeedbackBinding<Name> }[FeedbackEventName] — Any row of the event mapping, whichever event it binds.
|
|
2181
|
+
- `DEFAULT_FEEDBACK_BINDINGS` (const): const DEFAULT_FEEDBACK_BINDINGS: readonly AnyFeedbackBinding[] — The default mapping: each engine-announced gameplay event to the cue it raises. Damage to the local player reads as hurt, damage anywhere else as a hit; a wave start has no event of its own, so a game announces it through `notify("wave")` or `attachWaveFeedback`.
|
|
2182
|
+
- `DEFAULT_FEEDBACK_REACTIONS` (const): const DEFAULT_FEEDBACK_REACTIONS: Readonly<Record<FeedbackCue, FeedbackReaction>> — The default reactions. They are data: a game overrides one through `audio.feedbackCues` (sound) or `createEventFeedback({ reactions })` (haptics, shake, caption), and replaces the mapping through `bindings` without touching the shell.
|
|
2183
|
+
- `EventFeedback` (interface): interface EventFeedback — The event-to-reaction seam. One instance per running world.
|
|
2184
|
+
- `EventFeedbackOptions` (interface): interface EventFeedbackOptions — Options a game or host may override.
|
|
2185
|
+
- `FeedbackAnnouncer` (interface): interface FeedbackAnnouncer — The slice of `ctx.game.feedback` a wave runner needs, so this runs headless in a test.
|
|
2186
|
+
- `FeedbackBinding` (type): type FeedbackBinding<Name extends FeedbackEventName = FeedbackEventName> = { event: Name; cue: FeedbackCue; /** Default `"any"`. `"local"` only fires for the local player's own target/user/refusal. */ scope?: FeedbackScope; /** Overrides the cue's caption for this event, e.g. "You died". */ caption?… — One row of the event mapping: `event` announces, `cue` reacts.
|
|
2187
|
+
- `FeedbackDeps` (interface): interface FeedbackDeps — Everything the feedback seam reaches outside itself. Injected so it runs headless.
|
|
2188
|
+
- `FeedbackEventName` (type): type FeedbackEventName = | "combat.damage" | "entity.died" | "worldItem.picked_up" | "quest.completed" | "stat.levelUp" | "command.refused" — Event names the default mapping listens to; a game may add its own through `bindings`.
|
|
2189
|
+
- `FeedbackReaction` (interface): interface FeedbackReaction — One cue's reaction: what plays, what the hand feels, what the screen does, and what a caption says.
|
|
2190
|
+
- `FeedbackScope` (type): type FeedbackScope = "local" | "remote" | "any" — Which side of an event a binding reacts to: the local player's, someone else's, or either.
|
|
2191
|
+
- `ResolvedFeedback` (interface): interface ResolvedFeedback — A resolved trigger: pooled per binding, so a busy event stream allocates nothing to decide.
|
|
2192
|
+
- `attachEventFeedback` (function): function attachEventFeedback(events: Pick<GameEvents, "on">, feedback: EventFeedback, eventsToBind: readonly FeedbackEventName[] = Object.keys( DEFAULT_FEEDBACK_BINDINGS.reduce<Record<string, true>>((names, binding) => { names[binding.event] = true; return names; }, {}), ) as FeedbackEventName[]): (… — Subscribe the feedback seam to a game's event bus, including `feedback.cue` for moments the game announces itself. Returns the detach function; the mapping and reactions stay override-able through the seam's `retune`.
|
|
2193
|
+
- `attachWaveFeedback` (function): function attachWaveFeedback(runner: Pick<WaveRunner, "state" | "subscribe">, announce: FeedbackAnnouncer): () => void — Announce each new wave a {@link WaveRunner} starts as the `wave` cue, once per boundary. A wave has no event of its own, so this is the engine-side hookup for tower defense and wave survival games: `attachWaveFeedback(runner, ctx.game.feedback)`. Nothing is polled, the runner's own notification drives it, and one `update` that crosses two waves still fires once - the cue has a cooldown and the next boundary is announced by the next crossing.
|
|
2194
|
+
- `createEventFeedback` (function): function createEventFeedback(deps: FeedbackDeps, options: EventFeedbackOptions = {}): EventFeedback — Build the event-to-sound/haptic/shake/caption seam. The mapping is data ({@link DEFAULT_FEEDBACK_BINDINGS}), reactions are data ({@link DEFAULT_FEEDBACK_REACTIONS}), and the resolved trigger for each binding is allocated once - handling an event allocates only the caption string it actually shows, never per frame.
|
|
2195
|
+
|
|
2196
|
+
## @jgengine/shell/audio/gestureResume
|
|
2197
|
+
|
|
2198
|
+
- `installGestureResume` (function): function installGestureResume(engine: Pick<AudioEngine, "resume">, target: Pick<Document, "addEventListener" | "removeEventListener"> | null = typeof document === "undefined" ? null : document): () => void — Resume the audio context on the first real user gesture. Chrome keeps a freshly created `AudioContext` suspended until the page sees a trusted input, so pressing a key, tapping or clicking is the only moment sound may start; the engine then flushes any cue it held. One listener set per game, removed on unmount.
|
|
2199
|
+
|
|
2124
2200
|
## @jgengine/shell/audio/loopParams
|
|
2125
2201
|
|
|
2126
2202
|
- `MAX_LOOP_CUTOFF` (const): const MAX_LOOP_CUTOFF: 22050 — Highest live filter cutoff, Hz; the engine also caps it at the context's Nyquist frequency.
|
|
@@ -64,6 +64,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
64
64
|
|
|
65
65
|
- `readModelAnimationParams` (function) · `import { readModelAnimationParams } from "@jgengine/shell/render/useModelAnimation"`
|
|
66
66
|
|
|
67
|
+
## attach-event-feedback — one call wires every announced combat/loot/objective/refusal event to its reaction
|
|
68
|
+
|
|
69
|
+
- `attachEventFeedback` (function) · `import { attachEventFeedback } from "@jgengine/shell/audio/eventFeedback"`
|
|
70
|
+
|
|
71
|
+
## attach-wave-feedback — fire the wave cue once whenever a wave runner crosses into a new wave
|
|
72
|
+
|
|
73
|
+
- `attachWaveFeedback` (function) · `import { attachWaveFeedback } from "@jgengine/shell/audio/eventFeedback"`
|
|
74
|
+
|
|
75
|
+
## audio-probe — read the live master RMS/peak and bus gains from a running page for capture and diagnostics
|
|
76
|
+
|
|
77
|
+
- `installAudioProbe` (function) · `import { installAudioProbe } from "@jgengine/shell/audio/audioProbe"`
|
|
78
|
+
|
|
67
79
|
## authored-flocks — render authored habitat agents through caller-selected animated species models with velocity orientation and bounded model lifetime
|
|
68
80
|
|
|
69
81
|
- `AuthoredFlocks` (function) · `import { AuthoredFlocks } from "@jgengine/shell/world/AuthoredFlocks"`
|
|
@@ -114,6 +126,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
114
126
|
|
|
115
127
|
- `isCameraOccluderTransparent` (function) · `import { isCameraOccluderTransparent } from "@jgengine/shell/camera"`
|
|
116
128
|
|
|
129
|
+
## caption-feed — bounded, self-expiring, deduped caption lines for the accessibility captions overlay
|
|
130
|
+
|
|
131
|
+
- `createCaptionFeed` (function) · `import { createCaptionFeed } from "@jgengine/shell/audio/captions"`
|
|
132
|
+
|
|
133
|
+
## caption-overlay — render the caption feed as a themed, screen-reader-announced line stack gated by the captions accessibility setting
|
|
134
|
+
|
|
135
|
+
- `CaptionOverlay` (function) · `import { CaptionOverlay } from "@jgengine/shell/audio/CaptionOverlay"`
|
|
136
|
+
|
|
117
137
|
## capture-canvas — read the live R3F frame to a PNG data URL (needs preserveDrawingBuffer)
|
|
118
138
|
|
|
119
139
|
- `captureCanvas` (function) · `import { captureCanvas } from "@jgengine/shell/render/sceneCapture"`
|
|
@@ -285,6 +305,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
285
305
|
|
|
286
306
|
- `EntitySummary` (function) · `import { EntitySummary } from "@jgengine/react"`
|
|
287
307
|
|
|
308
|
+
## event-feedback — map announced gameplay events (damage, kill, pickup, objective, wave, refusal) to a sound, gamepad pulse, camera shake and caption through one data mapping
|
|
309
|
+
|
|
310
|
+
- `createEventFeedback` (function) · `import { createEventFeedback } from "@jgengine/shell/audio/eventFeedback"`
|
|
311
|
+
|
|
288
312
|
## event-meter-hud — render a core event/heat meter's live value, fraction, tier, and ready state in a HUD gauge
|
|
289
313
|
|
|
290
314
|
- `useEventMeter` (function) · `import { useEventMeter } from "@jgengine/react"`
|
|
@@ -293,6 +317,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
293
317
|
|
|
294
318
|
- `FastTravelMenu` (function) · `import { FastTravelMenu } from "@jgengine/react"`
|
|
295
319
|
|
|
320
|
+
## feedback-host — mount event feedback, gesture-gated audio and captions for a running game in one component
|
|
321
|
+
|
|
322
|
+
- `FeedbackHost` (function) · `import { FeedbackHost } from "@jgengine/shell/audio/FeedbackHost"`
|
|
323
|
+
|
|
296
324
|
## floating-text — overlay that renders on-screen floating combat text with per-kind color, alpha, and pop-in scale
|
|
297
325
|
|
|
298
326
|
- `FloatingText` (function) · `import { FloatingText } from "@jgengine/react"`
|
|
@@ -309,6 +337,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
309
337
|
|
|
310
338
|
- `GameContextBridge` (function) · `import { GameContextBridge } from "@jgengine/react"`
|
|
311
339
|
|
|
340
|
+
## gesture-resume — unlock the audio context on the first pointer/key/touch gesture, so pre-gesture cues play instead of vanishing
|
|
341
|
+
|
|
342
|
+
- `installGestureResume` (function) · `import { installGestureResume } from "@jgengine/shell/audio/gestureResume"`
|
|
343
|
+
|
|
312
344
|
## ground-edge-fade — fade merged ground layers into the terrain at their outer edge, keeping inner seams crisp
|
|
313
345
|
|
|
314
346
|
- `applyGroundEdgeFade` (function) · `import { applyGroundEdgeFade } from "@jgengine/shell/terrain"`
|
|
@@ -359,6 +391,11 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
359
391
|
|
|
360
392
|
- `HudInteractProvider` (function) · `import { HudInteractProvider } from "@jgengine/react/hudInteract"`
|
|
361
393
|
|
|
394
|
+
## hud-lanes — two HUD blocks stacked in one lane, plus a second lane beside it
|
|
395
|
+
|
|
396
|
+
- `HudLanesPreview` (function) · `import { HudLanesPreview } from "@jgengine/react/hudLanesPreview"`
|
|
397
|
+
- `resolveHudLanes` (function) · `import { resolveHudLanes } from "@jgengine/react"`
|
|
398
|
+
|
|
362
399
|
## hud-layout — persistent, live-editable HUD panel layout state for `HudCanvas`
|
|
363
400
|
|
|
364
401
|
- `useHudLayout` (function) · `import { useHudLayout } from "@jgengine/react"`
|
|
@@ -371,6 +408,11 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
371
408
|
|
|
372
409
|
- `useHudLayoutPersist` (function) · `import { useHudLayoutPersist } from "@jgengine/react"`
|
|
373
410
|
|
|
411
|
+
## hud-overlap-report — format the intersecting HUD block pairs for a development console
|
|
412
|
+
|
|
413
|
+
- `formatHudOverlapReport` (function) · `import { formatHudOverlapReport } from "@jgengine/react"`
|
|
414
|
+
- `hudBlockOverlaps` (function) · `import { hudBlockOverlaps } from "@jgengine/react"`
|
|
415
|
+
|
|
374
416
|
## hud-panel — anchored HUD slot — drop any widget in a named, player-movable panel
|
|
375
417
|
|
|
376
418
|
- `HudPanel` (function) · `import { HudPanel } from "@jgengine/react"`
|
|
@@ -351,7 +351,7 @@ Canyon Chase is the worked example: `movement: { axis: "horizontal" }` steering,
|
|
|
351
351
|
**`presentation: "hud"` games get 3D parity.** A pure-HUD game (no camera rig) now reaches the same input/audio seams as a 3D game:
|
|
352
352
|
- **Touch gestures** — the shell mounts a headless `TouchPlaySurface` in the hud branch too, so `touch.gestures` (swipe/tap) reach actions without the game hand-wiring pointer events on its own canvas. The visible dock stays game-authored per the rule above.
|
|
353
353
|
- **No phantom reservations** — camera action names (`turnLeft`/`turnRight`/`interact`/…) are reserved *only* when a camera rig is active, so a hud game may bind them directly instead of renaming to `steer*`.
|
|
354
|
-
- **Audio actually plays**
|
|
354
|
+
- **Audio actually plays** - audio resumes on the first pointer gesture in hud games (not just 3D), and `playOneShot` self-resumes the suspended context. Cues played before any gesture wait in a bounded queue and play on the first real input instead of vanishing. Trigger sound from anywhere holding `ctx` via `ctx.game.audio.play(soundId, at?)` / `ctx.game.audio.resume()` - the reachable seam over the shell's audio engine. Bus gain and mute compose (`engine.setBusMute`), so a muted bus keeps the player's chosen volume for unmute, and Settings carries per-bus and master mute. `ctx.game.feedback.cue("wave", "WAVE 3")` raises the engine's own reaction for a moment no event covers.
|
|
355
355
|
|
|
356
356
|
## 10. Progressive instruction
|
|
357
357
|
|