@jgengine/react 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -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 +3 -3
- package/skills/jgengine-ui/api.md +29 -2
- package/skills/jgengine-ui/capabilities.md +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,38 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
26
26
|
`packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
|
|
27
27
|
-->
|
|
28
28
|
|
|
29
|
+
## 0.31.0
|
|
30
|
+
|
|
31
|
+
### Migrate
|
|
32
|
+
|
|
33
|
+
- **Bump lockstep SDK packages to `^0.31.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.28.0`; `@jgengine/github` is `0.18.0`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `createAgent` (`@jgengine/core/agents/agent`) runs one resident's whole loop on the world clock: needs decay through `sim/needs`, a utility brain picks among the targets the game offers, an authored day plan says where the agent belongs, and the agent falls back to a declared target when its pick disappears. Mood, action and the far flag snapshot and restore with the needs.
|
|
38
|
+
- `createMoodReader` / `readMood` (`@jgengine/core/agents/mood`) derive a 0-100 mood and a caller-labelled tier from weighted need satisfaction, plus named sources a queue, a litter patch or a cold snap raises and clears.
|
|
39
|
+
- `createAgentSchedule` (`@jgengine/core/agents/schedule`) resolves an authored day plan through `sim/routine` and adds a work beat: a stop names the places it patrols, so a janitor is an agent with a route rather than a special case. Off-hours behaviour is a stop without a beat.
|
|
40
|
+
- `createRelationshipBook` (`@jgengine/core/agents/relationships`) keeps undirected familiarity and opinion per pair. A social action moves both and reports the milestones crossed, friends prefer each other's company, and a bonded pair refuses to split. Pair keys come from `relation/keyedValues` and crossings from `relation/thresholds`.
|
|
41
|
+
- `GameSave.deleteSlot(slot)` and `SaveStore.deleteSlot(slot)` remove one slot's payload and drop it from the slot index; they resolve `true` when a document was removed and `false` for an unknown slot. Deleting the active slot resets the live state to the initial document. `load()` no longer lists a slot that holds nothing, so a deleted slot cannot return.
|
|
42
|
+
- `HudPanel` takes `lane` (`@jgengine/react`): blocks naming the same anchor and lane stack without overlap, lanes in one anchor sit side by side, and `resolveHudLanes` resolves the rectangles. A moved panel leaves its lane and keeps its authored placement.
|
|
43
|
+
- `hudBlockOverlaps` and `formatHudOverlapReport` (`@jgengine/react`) name every pair of rendered HUD blocks whose rectangles intersect, with both ids and the viewport; `HudCanvas` warns with that report in a dev build.
|
|
44
|
+
- `rollDrop` (`@jgengine/core/loot/drop`) rolls one deterministic item from a seed and `compareToEquipped` turns it into per-stat deltas against worn gear with a weighted verdict a HUD can render.
|
|
45
|
+
- `createRunHaul` (`@jgengine/core/loot/haul`) gates carried loot by weight through the shared encumbrance rules, banks what the persistent stash fits, and discards the run's haul when the game calls `fail`.
|
|
46
|
+
- `salvageItem` and `createItemLedger` (`@jgengine/core/loot/salvage`) break an item down for the currency its level and rarity say and claim the instance, so a salvaged item cannot be salvaged, equipped or sold again.
|
|
47
|
+
- `stockVendor` and `createVendorRotation` (`@jgengine/core/loot/vendor`) fill a `ShopStock` with rolled items priced through the economy wallet, with a run-count restock rule the game authors.
|
|
48
|
+
- `markerProximity` on `createMissionRuntime` picks the plane a marker proximity test compares in: `"horizontal"` (default) measures ground distance and allows the actor a `tolerance` (default 8 m) above or below the marker, `"spatial"` is the straight 3D check for worlds with stacked platforms. `MissionMarkerPlane`, `MissionMarkerProximity` and `DEFAULT_MARKER_TOLERANCE` are exported with it.
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- `evaluatePressure` (`@jgengine/core/ai/director/pressure`) reads pressure as player control: a healthy player further into a run is leaned on harder, a hurt player is eased off, and every effect stays inside the rule's `maxTier`.
|
|
53
|
+
- Loot bases and affixes take `minLevel`, so `rollLoot` only draws from entries at or below the item level. Loot bases take `mass`; rolled items carry it for carry checks.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- `HudCanvas` resolved its host with `canvas.parentElement`, which is `HudThemeRoot`'s `display: contents` element and measures 0x0 — the boundary overlay, the HUD-fit measurement and the new overlap report never ran under the standard HUD root. It now uses the nearest ancestor with a real box.
|
|
58
|
+
- The HUD boundary check returned early whenever nothing overflowed, so a later report in the same pass could never run.
|
|
59
|
+
- `reach`, `interact` and the `left-area` fail condition compared a marker in 3D, so a marker authored at `y=0` under rising ground was unreachable. They now read the same plane.
|
|
60
|
+
|
|
29
61
|
## 0.30.0
|
|
30
62
|
|
|
31
63
|
### 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.31.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.31.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.31.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
10
10
|
|
|
11
11
|
## @jgengine/core/authoring
|
|
12
12
|
|
|
@@ -285,7 +285,7 @@
|
|
|
285
285
|
|
|
286
286
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
287
287
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
288
|
-
- `VERSION` (const): const VERSION: "0.
|
|
288
|
+
- `VERSION` (const): const VERSION: "0.31.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
289
289
|
|
|
290
290
|
## @jgengine/core/runtime/adapter
|
|
291
291
|
|
|
@@ -728,7 +728,7 @@
|
|
|
728
728
|
- `GameLoop` (interface): interface GameLoop<TContext = unknown> — Lifecycle hooks a game implements to drive init, per-tick simulation, and player join/leave.
|
|
729
729
|
- `Hotbar` (function): function Hotbar({ inventoryId, activeSlot, keys, slotSize = 46, itemIcon, style, className, }: { inventoryId: string; activeSlot?: number; keys?: readonly string[]; slotSize?: number; /** Caller-supplied item id → icon registry; return null/undefined to fall back to the default glyph. */ itemIcon?: … — A numbered hotbar bound to an inventory — painted iconed slots (a `GameIcon` glyph over a school-keyed gradient with a count badge, #1035), an active-slot highlight, and a keycap per slot. Place it and pass the `inventoryId`; `activeSlot` highlights the equipped one. Supply `itemIcon` to map your item ids to your own glyphs — the default resolves a `GameIcon` from the item id.
|
|
730
730
|
- `HudCanvas` (function): function HudCanvas({ layout, editChord, compactScale, showDuring, className, style, children, }: { layout: HudLayoutStore; editChord?: HudEditChord | false; /** Zoom applied to the whole HUD on compact displays. Default 0.85. */ compactScale?: number; /** Opt-in play-phase gate: render the HUD only … — Full-viewport HUD surface. Panels declared with `HudPanel` flow into nine anchor regions and stack automatically with a gap — no per-panel pixel offsets, no manual clearance for sibling panels, the touch-control dock (`--jg-hud-dock-clearance`), or device safe areas. On compact displays the whole surface scales down and each panel applies its `compact` behavior.
|
|
731
|
-
- `HudPanel` (function): function HudPanel({ id, anchor = "top-left", order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, width, height, type, className, style, children, }: { id: string; anchor?: HudAnchor; /**
|
|
731
|
+
- `HudPanel` (function): function HudPanel({ id, anchor = "top-left", lane, order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, width, height, type, className, style, children, }: { id: string; anchor?: HudAnchor; /** … — A HUD block that lives in one of the nine anchor regions. Panels sharing a region stack outward from the screen edge in ascending `order`. On fine pointers panels stay draggable through the edit chord; a dragged panel leaves the flow and keeps its custom placement. On compact displays custom placements are ignored and the `compact` behavior applies.
|
|
732
732
|
- `PlayableGame` (type): type PlayableGame = EnginePlayableGame< ComponentType, ComponentType, RenderEntity, RenderObject, ComponentType<ViewmodelProps>, ComponentType<WorldOverlayProps> > & { /** HUD theme the shell applies at the `GameUI` root. Unset uses `defaultHudTheme`; `false` opts out of the default theme and the en… — The concrete React-bound playable a shell `defineGame` returns and `GameHost` mounts — the one PlayableGame games handle.
|
|
733
733
|
- `StatBar` (function): function StatBar({ statId = "health", entityId, tone = "health", label, showValue = true, width = 200, icon, style, className, }: { statId?: string; entityId?: string; tone?: StatTone; label?: string; showValue?: boolean; width?: number; icon?: ReactNode; style?: CSSProperties; className?: string; }… — A polished stat bar (health/mana/stamina/shield/xp) — a rounded, glassy meter with a tone-colored fill and an optional value readout. Reads `statId` off `entityId` (defaults to the local player). Renders nothing until the stat exists.
|
|
734
734
|
- `StoreHandle` (interface): interface StoreHandle<T> — A typed, cast-free handle onto one slot of the reactive game store (`ctx.game.store`). The single type parameter is fixed at definition; `read`/`write`/`update` never widen to `unknown`, so no call site repeats `store.get(key) as T`. Writes flow through the engine store, so they bump `ctx.version()`, serialize into a {@link WorldSnapshot}, and replay under host authority — the same guarantees a hand-rolled `store.get`/`store.set` pair gives up the moment it forks run state into a module-level singleton.
|
|
@@ -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
|
|
|
@@ -359,6 +359,11 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
359
359
|
|
|
360
360
|
- `HudInteractProvider` (function) · `import { HudInteractProvider } from "@jgengine/react/hudInteract"`
|
|
361
361
|
|
|
362
|
+
## hud-lanes — two HUD blocks stacked in one lane, plus a second lane beside it
|
|
363
|
+
|
|
364
|
+
- `HudLanesPreview` (function) · `import { HudLanesPreview } from "@jgengine/react/hudLanesPreview"`
|
|
365
|
+
- `resolveHudLanes` (function) · `import { resolveHudLanes } from "@jgengine/react"`
|
|
366
|
+
|
|
362
367
|
## hud-layout — persistent, live-editable HUD panel layout state for `HudCanvas`
|
|
363
368
|
|
|
364
369
|
- `useHudLayout` (function) · `import { useHudLayout } from "@jgengine/react"`
|
|
@@ -371,6 +376,11 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
371
376
|
|
|
372
377
|
- `useHudLayoutPersist` (function) · `import { useHudLayoutPersist } from "@jgengine/react"`
|
|
373
378
|
|
|
379
|
+
## hud-overlap-report — format the intersecting HUD block pairs for a development console
|
|
380
|
+
|
|
381
|
+
- `formatHudOverlapReport` (function) · `import { formatHudOverlapReport } from "@jgengine/react"`
|
|
382
|
+
- `hudBlockOverlaps` (function) · `import { hudBlockOverlaps } from "@jgengine/react"`
|
|
383
|
+
|
|
374
384
|
## hud-panel — anchored HUD slot — drop any widget in a named, player-movable panel
|
|
375
385
|
|
|
376
386
|
- `HudPanel` (function) · `import { HudPanel } from "@jgengine/react"`
|