@jgengine/react 0.10.0 → 0.12.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 +39 -2
- package/LICENSE +203 -661
- package/README.md +1 -1
- package/dist/actionHud.d.ts +174 -0
- package/dist/actionHud.js +294 -0
- package/dist/bars.d.ts +103 -0
- package/dist/bars.js +180 -0
- package/dist/barsPreview.d.ts +4 -0
- package/dist/barsPreview.js +47 -0
- package/dist/cards.d.ts +59 -0
- package/dist/cards.js +116 -0
- package/dist/chat.d.ts +3 -1
- package/dist/chat.js +2 -2
- package/dist/components.d.ts +28 -1
- package/dist/components.js +36 -1
- package/dist/display.d.ts +1 -0
- package/dist/display.js +1 -0
- package/dist/engineStore.d.ts +3 -0
- package/dist/engineStore.js +3 -0
- package/dist/fogOverlay.d.ts +3 -0
- package/dist/fogOverlay.js +3 -0
- package/dist/gameViewport.js +16 -0
- package/dist/hooks.d.ts +40 -0
- package/dist/hooks.js +69 -1
- package/dist/hud.d.ts +104 -0
- package/dist/hud.js +155 -0
- package/dist/hudFrame.d.ts +47 -0
- package/dist/hudFrame.js +80 -0
- package/dist/hudLayout.d.ts +31 -2
- package/dist/hudLayout.js +127 -10
- package/dist/hudLayoutPersist.d.ts +23 -0
- package/dist/hudLayoutPersist.js +19 -0
- package/dist/hudTheme.d.ts +76 -0
- package/dist/hudTheme.js +156 -0
- package/dist/hudThemePreview.d.ts +4 -0
- package/dist/hudThemePreview.js +37 -0
- package/dist/iconTreatment.d.ts +50 -0
- package/dist/iconTreatment.js +84 -0
- package/dist/iconsPreview.d.ts +4 -0
- package/dist/iconsPreview.js +29 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/keyHint.d.ts +30 -0
- package/dist/keyHint.js +31 -0
- package/dist/map.d.ts +141 -11
- package/dist/map.js +138 -16
- package/dist/save.d.ts +25 -0
- package/dist/save.js +37 -0
- package/dist/selectSnapshot.d.ts +2 -0
- package/dist/selectSnapshot.js +2 -0
- package/dist/selectionHud.d.ts +139 -0
- package/dist/selectionHud.js +188 -0
- package/dist/skillCheckPaint.d.ts +2 -0
- package/dist/skillCheckPaint.js +2 -0
- package/dist/slots.d.ts +44 -0
- package/dist/slots.js +37 -0
- package/dist/startScreen.d.ts +62 -0
- package/dist/startScreen.js +62 -0
- package/dist/store.d.ts +24 -0
- package/dist/store.js +9 -0
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +54 -0
- package/skills/jgengine/api.md +527 -0
- package/skills/jgengine/capabilities.md +131 -0
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +41 -0
- package/skills/jgengine-ui/api.md +2007 -0
- package/skills/jgengine-ui/capabilities.md +211 -0
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +513 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/llms.txt +0 -1724
package/dist/map.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useSyncExternalStore } from "react";
|
|
3
3
|
import { DEFAULT_MARKER_KINDS, markerKindStyle, } from "@jgengine/core/world/markers";
|
|
4
4
|
import { bearingToCardinal, clampToMinimapEdge, compassBearing, headingToBearing, projectToMinimap, relativeBearing, unprojectFromMinimap, } from "@jgengine/core/world/minimap";
|
|
@@ -68,18 +68,42 @@ function cellStateNodes(layers, project, scale, keyPrefix) {
|
|
|
68
68
|
}
|
|
69
69
|
return nodes;
|
|
70
70
|
}
|
|
71
|
+
function markerAccess(markers) {
|
|
72
|
+
if (Array.isArray(markers)) {
|
|
73
|
+
const snapshot = markers;
|
|
74
|
+
return { subscribe: NO_SUBSCRIBE, getSnapshot: () => snapshot, getServerSnapshot: () => snapshot };
|
|
75
|
+
}
|
|
76
|
+
if ("getSnapshot" in markers) {
|
|
77
|
+
const source = markers;
|
|
78
|
+
return {
|
|
79
|
+
subscribe: (listener) => source.subscribe(listener),
|
|
80
|
+
getSnapshot: () => source.getSnapshot(),
|
|
81
|
+
getServerSnapshot: () => (source.getServerSnapshot ?? source.getSnapshot).call(source),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const markerSet = markers;
|
|
85
|
+
return {
|
|
86
|
+
subscribe: (listener) => markerSet.subscribe(listener),
|
|
87
|
+
getSnapshot: () => markerSet.snapshot(),
|
|
88
|
+
getServerSnapshot: () => markerSet.snapshot(),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
71
91
|
export function useMarkers(markers) {
|
|
72
|
-
|
|
92
|
+
const access = useMemo(() => markerAccess(markers), [markers]);
|
|
93
|
+
return useSyncExternalStore(access.subscribe, access.getSnapshot, access.getServerSnapshot);
|
|
73
94
|
}
|
|
74
95
|
export function useFog(fog) {
|
|
75
96
|
return useSyncExternalStore(fog.subscribe, fog.cells, fog.cells);
|
|
76
97
|
}
|
|
77
98
|
/**
|
|
78
99
|
* Framed circular minimap: optional baked terrain background, reveal-on-event
|
|
79
|
-
* fog overlay, categorized marker icons, and a facing arrow. Reads
|
|
80
|
-
*
|
|
100
|
+
* fog overlay, categorized marker icons, and a facing arrow. Reads static
|
|
101
|
+
* marker views, an external marker source, or a native `MarkerSet`; supply
|
|
102
|
+
* your own `kindStyles` palette to reskin. Does not require `GameProvider`.
|
|
103
|
+
*
|
|
104
|
+
* @capability minimap framed circular minimap with terrain bake, fog, markers, and facing arrow
|
|
81
105
|
*/
|
|
82
|
-
export function Minimap({ markers, center, worldRadius, fog, size = 176, facingYaw = 0, rotate = false, kindStyles = DEFAULT_MARKER_KINDS, background, mapBounds, routes, zones, cellStates, onWorldClick, className, title = "Map", children, }) {
|
|
106
|
+
export function Minimap({ markers, center, worldRadius, fog, size = 176, facingYaw = 0, rotate = false, kindStyles = DEFAULT_MARKER_KINDS, background, mapBounds, routes, zones, cellStates, onWorldClick, className, style, title = "Map", children, }) {
|
|
83
107
|
const markerList = useMarkers(markers);
|
|
84
108
|
const fogCells = useSyncExternalStore(fog?.subscribe ?? NO_SUBSCRIBE, fog?.cells ?? NULL_CELLS, fog?.cells ?? NULL_CELLS);
|
|
85
109
|
const bearing = headingToBearing(facingYaw);
|
|
@@ -91,7 +115,7 @@ export function Minimap({ markers, center, worldRadius, fog, size = 176, facingY
|
|
|
91
115
|
};
|
|
92
116
|
const half = size / 2;
|
|
93
117
|
const clipId = `mm-clip-${size}`;
|
|
94
|
-
const sorted = [...markerList].sort((a, b) => markerKindStyle(a.kind, kindStyles).priority - markerKindStyle(b.kind, kindStyles).priority);
|
|
118
|
+
const sorted = useMemo(() => [...markerList].sort((a, b) => markerKindStyle(a.kind, kindStyles).priority - markerKindStyle(b.kind, kindStyles).priority), [markerList, kindStyles]);
|
|
95
119
|
const fogImage = useMemo(() => {
|
|
96
120
|
if (fogCells === null)
|
|
97
121
|
return null;
|
|
@@ -115,10 +139,12 @@ export function Minimap({ markers, center, worldRadius, fog, size = 176, facingY
|
|
|
115
139
|
borderRadius: 14,
|
|
116
140
|
padding: 8,
|
|
117
141
|
background: "linear-gradient(160deg, rgba(24,28,36,0.94), rgba(12,15,20,0.94))",
|
|
118
|
-
|
|
142
|
+
// Ring color reads the shared HudTheme `--jg-ring` token (#1034); fallback = the built-in ring.
|
|
143
|
+
border: "1px solid var(--jg-ring, rgba(148,163,184,0.28))",
|
|
119
144
|
boxShadow: "0 10px 30px rgba(0,0,0,0.45)",
|
|
120
145
|
color: "#e2e8f0",
|
|
121
146
|
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
147
|
+
...style,
|
|
122
148
|
}, children: [_jsxs("div", { style: {
|
|
123
149
|
display: "flex",
|
|
124
150
|
justifyContent: "space-between",
|
|
@@ -156,16 +182,46 @@ export function Minimap({ markers, center, worldRadius, fog, size = 176, facingY
|
|
|
156
182
|
...routeNodes(routes, projectXZ, "mm"),
|
|
157
183
|
];
|
|
158
184
|
})(), _jsx("circle", { cx: half, cy: half, r: half - 1, fill: "none", stroke: "rgba(148,163,184,0.12)" }), sorted.map((marker) => {
|
|
159
|
-
const
|
|
185
|
+
const kind = marker.kind ?? "marker";
|
|
186
|
+
const style = markerKindStyle(kind, kindStyles);
|
|
160
187
|
const projected = projectToMinimap(marker.position, view);
|
|
161
188
|
const at = projected.inside ? projected : clampToMinimapEdge(projected, size);
|
|
162
|
-
return (_jsxs("g", { "data-marker-kind":
|
|
189
|
+
return (_jsxs("g", { "data-marker-kind": kind, children: [_jsx("circle", { cx: at.x, cy: at.y, r: 7, fill: "rgba(2,6,12,0.55)" }), _jsx("text", { x: at.x, y: at.y + 3.5, textAnchor: "middle", fontSize: 10, fill: style.color, style: { fontWeight: 700 }, children: style.glyph })] }, marker.id));
|
|
163
190
|
}), _jsx("g", { transform: `translate(${half} ${half}) rotate(${(rotate ? 0 : bearing) * (180 / Math.PI)})`, children: _jsx("path", { d: "M0,-9 L6,7 L0,3 L-6,7 Z", fill: "#4ade80", stroke: "#052e16", strokeWidth: 0.75 }) })] }), _jsx("circle", { cx: half, cy: half, r: half - 1, fill: "none", stroke: "rgba(148,163,184,0.4)", strokeWidth: 1.5 }), _jsx("text", { x: half, y: 13, textAnchor: "middle", fontSize: 11, fill: "rgba(226,232,240,0.9)", style: { fontWeight: 700 }, children: "N" })] }), children] }));
|
|
164
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Headless minimap chrome — nests inside a game's own `<svg>`: an optional
|
|
194
|
+
* ring frame, and edge-clamped marker dots that draw as directional arrows
|
|
195
|
+
* when a marker carries a `heading` (the compass-arrow / player-blip layer
|
|
196
|
+
* ~8 games were re-drawing by hand over `projectToMinimap`/`clampToMinimapEdge`).
|
|
197
|
+
* Renders a bare `<g>` — no background, panel, or title; compose your own
|
|
198
|
+
* content layer (routes, zones, terrain) as sibling SVG nodes for full control
|
|
199
|
+
* of z-order, or call `MinimapChrome` twice (once for `frame`, again for
|
|
200
|
+
* `markers`) to sandwich custom content between the ring and the blips.
|
|
201
|
+
*
|
|
202
|
+
* @capability minimap-chrome ring frame plus edge-clamped, heading-aware marker dots to nest inside a game's own `<svg>`
|
|
203
|
+
*/
|
|
204
|
+
export function MinimapChrome({ view, frame = false, frameColor = "rgba(148,163,184,0.4)", frameStrokeWidth = 1.5, cardinalLabel = "N", markers = [], markerColor = "#e2e8f0", markerRadius = 5, className, }) {
|
|
205
|
+
const half = view.size / 2;
|
|
206
|
+
const rotate = view.rotate ?? 0;
|
|
207
|
+
return (_jsxs("g", { className: className, "data-minimap-chrome": true, children: [frame ? (_jsxs(_Fragment, { children: [_jsx("circle", { cx: half, cy: half, r: half - 1, fill: "none", stroke: frameColor, strokeWidth: frameStrokeWidth }), cardinalLabel !== false ? (_jsx("text", { x: half, y: 13, textAnchor: "middle", fontSize: 11, fill: frameColor, style: { fontWeight: 700 }, children: cardinalLabel })) : null] })) : null, markers.map((marker) => {
|
|
208
|
+
const projected = projectToMinimap(marker.position, view);
|
|
209
|
+
const clamp = marker.clampToEdge ?? true;
|
|
210
|
+
if (!projected.inside && !clamp)
|
|
211
|
+
return null;
|
|
212
|
+
const at = projected.inside ? projected : clampToMinimapEdge(projected, view.size);
|
|
213
|
+
const color = marker.color ?? markerColor;
|
|
214
|
+
const radius = marker.radius ?? markerRadius;
|
|
215
|
+
if (marker.heading !== undefined) {
|
|
216
|
+
const deg = (marker.heading - rotate) * (180 / Math.PI);
|
|
217
|
+
return (_jsx("g", { "data-minimap-marker": marker.id, transform: `translate(${at.x} ${at.y}) rotate(${deg})`, children: _jsx("path", { d: `M0,${-radius} L${radius * 0.7},${radius * 0.8} L0,${radius * 0.35} L${-radius * 0.7},${radius * 0.8} Z`, fill: color, stroke: marker.strokeColor ?? "rgba(0,0,0,0.6)", strokeWidth: marker.strokeWidth ?? 0.75 }) }, marker.id));
|
|
218
|
+
}
|
|
219
|
+
return (_jsxs("g", { "data-minimap-marker": marker.id, children: [_jsx("circle", { cx: at.x, cy: at.y, r: radius, fill: color, stroke: marker.strokeColor, strokeWidth: marker.strokeWidth }), marker.glyph !== undefined ? (_jsx("text", { x: at.x, y: at.y + 3.5, textAnchor: "middle", fontSize: 10, fill: marker.glyphColor ?? "#0f172a", style: { fontWeight: 700 }, children: marker.glyph })) : null] }, marker.id));
|
|
220
|
+
})] }));
|
|
221
|
+
}
|
|
165
222
|
const NO_SUBSCRIBE = () => () => undefined;
|
|
166
223
|
const NULL_CELLS = () => null;
|
|
167
224
|
const EMPTY_MARKERS_VALUE = [];
|
|
168
|
-
const EMPTY_MARKERS = () => EMPTY_MARKERS_VALUE;
|
|
169
225
|
const COMPASS_TICKS = [
|
|
170
226
|
{ cardinal: "N", bearing: 0 },
|
|
171
227
|
{ cardinal: "NE", bearing: Math.PI / 4 },
|
|
@@ -178,10 +234,11 @@ const COMPASS_TICKS = [
|
|
|
178
234
|
];
|
|
179
235
|
/**
|
|
180
236
|
* Horizontal compass strip centered on the player's facing direction, with the
|
|
181
|
-
* eight cardinals and optional marker pips
|
|
237
|
+
* eight cardinals and optional marker pips from static views, an external
|
|
238
|
+
* source, or a native `MarkerSet`.
|
|
182
239
|
*/
|
|
183
240
|
export function Compass({ facingYaw, center, markers, width = 340, fov = (Math.PI * 2) / 3, kindStyles = DEFAULT_MARKER_KINDS, className, }) {
|
|
184
|
-
const markerList =
|
|
241
|
+
const markerList = useMarkers(markers ?? EMPTY_MARKERS_VALUE);
|
|
185
242
|
const facingBearing = headingToBearing(facingYaw);
|
|
186
243
|
const half = fov / 2;
|
|
187
244
|
const toX = (bearing) => {
|
|
@@ -227,8 +284,9 @@ export function Compass({ facingYaw, center, markers, width = 340, fov = (Math.P
|
|
|
227
284
|
const x = toX(bearing);
|
|
228
285
|
if (x === null)
|
|
229
286
|
return null;
|
|
230
|
-
const
|
|
231
|
-
|
|
287
|
+
const kind = marker.kind ?? "marker";
|
|
288
|
+
const style = markerKindStyle(kind, kindStyles);
|
|
289
|
+
return (_jsx("span", { "data-compass-marker": kind, style: {
|
|
232
290
|
position: "absolute",
|
|
233
291
|
left: x,
|
|
234
292
|
bottom: 2,
|
|
@@ -240,6 +298,69 @@ export function Compass({ facingYaw, center, markers, width = 340, fov = (Math.P
|
|
|
240
298
|
})
|
|
241
299
|
: null] }));
|
|
242
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* Composed circular-minimap chrome: optional zone-label + clock header above
|
|
303
|
+
* the `Minimap`, optional `Compass` strip below. Purely a wiring layer over
|
|
304
|
+
* the existing primitives — the game supplies the zone name and clock text,
|
|
305
|
+
* this only places them; omit either slot and the header disappears.
|
|
306
|
+
*
|
|
307
|
+
* @capability minimap-panel circular minimap with zone label, clock, and compass composed together
|
|
308
|
+
*/
|
|
309
|
+
export function MinimapPanel({ zoneLabel, clock, showCompass = true, compassProps, headerClassName, zoneLabelClassName, clockClassName, compassClassName, children, ...minimapProps }) {
|
|
310
|
+
const hasHeader = zoneLabel !== undefined || clock !== undefined;
|
|
311
|
+
return (_jsxs("div", { "data-minimap-chrome": true, children: [hasHeader ? (_jsxs("div", { className: headerClassName, "data-minimap-chrome-header": true, style: {
|
|
312
|
+
display: "flex",
|
|
313
|
+
justifyContent: "space-between",
|
|
314
|
+
alignItems: "baseline",
|
|
315
|
+
gap: 8,
|
|
316
|
+
marginBottom: 4,
|
|
317
|
+
width: minimapProps.size ?? 176,
|
|
318
|
+
color: "#e2e8f0",
|
|
319
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
320
|
+
}, children: [zoneLabel !== undefined ? (_jsx("span", { className: zoneLabelClassName, "data-minimap-chrome-zone": true, style: { fontSize: 12, fontWeight: 700 }, children: zoneLabel })) : (_jsx("span", {})), clock !== undefined ? (_jsx("span", { className: clockClassName, "data-minimap-chrome-clock": true, style: { fontSize: 11, color: "rgba(226,232,240,0.75)" }, children: clock })) : null] })) : null, _jsx(Minimap, { ...minimapProps, children: children }), showCompass ? (_jsx("div", { className: compassClassName, "data-minimap-chrome-compass": true, style: { marginTop: 6 }, children: _jsx(Compass, { facingYaw: minimapProps.facingYaw ?? 0, center: minimapProps.center, markers: minimapProps.markers, width: minimapProps.size ?? 176, ...compassProps }) })) : null] }));
|
|
321
|
+
}
|
|
322
|
+
const TRACK_PIP_DIAMETER = {
|
|
323
|
+
dot: 6,
|
|
324
|
+
exit: 8,
|
|
325
|
+
player: 12,
|
|
326
|
+
};
|
|
327
|
+
/**
|
|
328
|
+
* Horizontal linear track minimap — a rounded progress rail with colored zone
|
|
329
|
+
* `spans` and gate/exit/player `pips` positioned by 0..1 fraction. The
|
|
330
|
+
* structural counterpart to the radial {@link Minimap} for corridor/route
|
|
331
|
+
* games: fractions are supplied by the caller (via core `trackFraction`), so it
|
|
332
|
+
* reads no store and stays presentation-only, sharing chrome with the radial
|
|
333
|
+
* minimap.
|
|
334
|
+
*
|
|
335
|
+
* @capability minimap-track-hud horizontal linear track minimap (corridor/route progress) — colored zone spans and gate/player pips positioned by 0..1 fraction, sharing chrome with the radial Minimap
|
|
336
|
+
*/
|
|
337
|
+
export function MinimapTrack({ spans = [], pips = [], width = "100%", height = 12, railColor = "rgba(255,255,255,0.10)", className, style, children, }) {
|
|
338
|
+
return (_jsxs("div", { className: className, "data-minimap-track": true, style: { position: "relative", width, height, borderRadius: 9999, background: railColor, ...style }, children: [spans.map((span) => (_jsx("div", { "data-track-span": span.id, title: span.label, style: {
|
|
339
|
+
position: "absolute",
|
|
340
|
+
top: 0,
|
|
341
|
+
bottom: 0,
|
|
342
|
+
left: `${span.start * 100}%`,
|
|
343
|
+
width: `${(span.end - span.start) * 100}%`,
|
|
344
|
+
background: span.color ?? "rgba(255,255,255,0.25)",
|
|
345
|
+
opacity: 0.4,
|
|
346
|
+
borderRadius: 9999,
|
|
347
|
+
} }, span.id))), pips.map((pip) => {
|
|
348
|
+
const shape = pip.shape ?? "dot";
|
|
349
|
+
const color = pip.color ?? "#f4f6fb";
|
|
350
|
+
const common = {
|
|
351
|
+
position: "absolute",
|
|
352
|
+
top: "50%",
|
|
353
|
+
left: `${pip.at * 100}%`,
|
|
354
|
+
transform: "translate(-50%, -50%)",
|
|
355
|
+
background: color,
|
|
356
|
+
};
|
|
357
|
+
if (shape === "gate") {
|
|
358
|
+
return (_jsx("div", { "data-track-pip": pip.id, "data-pip-shape": "gate", title: pip.title ?? pip.label, style: { ...common, width: 3, height: height + 6, borderRadius: 2 } }, pip.id));
|
|
359
|
+
}
|
|
360
|
+
const diameter = TRACK_PIP_DIAMETER[shape];
|
|
361
|
+
return (_jsx("div", { "data-track-pip": pip.id, "data-pip-shape": shape, title: pip.title ?? pip.label, style: { ...common, width: diameter, height: diameter, borderRadius: 9999, border: "1px solid rgba(0,0,0,0.6)" } }, pip.id));
|
|
362
|
+
}), children] }));
|
|
363
|
+
}
|
|
243
364
|
function WorldMapFogImage({ fogCells, project, worldW, worldD, width, mapH, }) {
|
|
244
365
|
const fogImage = useMemo(() => {
|
|
245
366
|
if (fogCells === null)
|
|
@@ -308,8 +429,9 @@ export function WorldMap({ markers, bounds, player, facingYaw = 0, fog, backgrou
|
|
|
308
429
|
];
|
|
309
430
|
})(), markerList.map((marker) => {
|
|
310
431
|
const at = project(marker.position[0], marker.position[2]);
|
|
311
|
-
const
|
|
312
|
-
|
|
432
|
+
const kind = marker.kind ?? "marker";
|
|
433
|
+
const style = markerKindStyle(kind, kindStyles);
|
|
434
|
+
return (_jsxs("g", { "data-world-marker": kind, children: [_jsx("circle", { cx: at.x, cy: at.y, r: 9, fill: "rgba(2,6,12,0.6)", stroke: style.color, strokeWidth: 1.2 }), _jsx("text", { x: at.x, y: at.y + 4, textAnchor: "middle", fontSize: 12, fill: style.color, style: { fontWeight: 700 }, children: style.glyph }), marker.label !== undefined ? (_jsx("text", { x: at.x + 12, y: at.y + 4, fontSize: 11, fill: "rgba(226,232,240,0.85)", children: marker.label })) : null] }, marker.id));
|
|
313
435
|
}), player !== undefined
|
|
314
436
|
? (() => {
|
|
315
437
|
const at = project(player[0], player[1]);
|
package/dist/save.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SaveStatus, SaveStore } from "@jgengine/core/game/saveStore";
|
|
2
|
+
/** The reactive slice of a {@link SaveStore} returned by {@link useSaveStore} — the live value plus bound save actions, all re-rendering on change. */
|
|
3
|
+
export interface SaveStoreView<T> {
|
|
4
|
+
value: T;
|
|
5
|
+
status: SaveStatus;
|
|
6
|
+
slot: string;
|
|
7
|
+
/** True while the initial `load()` is in flight — gate a loading screen on it. */
|
|
8
|
+
loading: boolean;
|
|
9
|
+
/** True while a write is in flight — show a "Saving…" indicator. */
|
|
10
|
+
saving: boolean;
|
|
11
|
+
set: (value: T) => void;
|
|
12
|
+
patch: (mutate: (previous: T) => T) => void;
|
|
13
|
+
save: () => Promise<void>;
|
|
14
|
+
clear: () => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Bind a {@link SaveStore} to React: returns the live `value` and `status` and
|
|
18
|
+
* re-renders whenever either changes. By default it calls `load()` once on
|
|
19
|
+
* mount (pass `{ load: false }` to hydrate yourself). The same hook works for
|
|
20
|
+
* offline (localStorage) and cloud (Convex/DB) saves — only the store's backend
|
|
21
|
+
* differs.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useSaveStore<T>(store: SaveStore<T>, options?: {
|
|
24
|
+
load?: boolean;
|
|
25
|
+
}): SaveStoreView<T>;
|
package/dist/save.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Bind a {@link SaveStore} to React: returns the live `value` and `status` and
|
|
4
|
+
* re-renders whenever either changes. By default it calls `load()` once on
|
|
5
|
+
* mount (pass `{ load: false }` to hydrate yourself). The same hook works for
|
|
6
|
+
* offline (localStorage) and cloud (Convex/DB) saves — only the store's backend
|
|
7
|
+
* differs.
|
|
8
|
+
*/
|
|
9
|
+
export function useSaveStore(store, options) {
|
|
10
|
+
useSyncExternalStore(store.subscribe, store.revision, store.revision);
|
|
11
|
+
const shouldLoad = options?.load !== false;
|
|
12
|
+
const loadedFor = useRef(null);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!shouldLoad)
|
|
15
|
+
return;
|
|
16
|
+
if (loadedFor.current === store)
|
|
17
|
+
return;
|
|
18
|
+
loadedFor.current = store;
|
|
19
|
+
void store.load();
|
|
20
|
+
}, [store, shouldLoad]);
|
|
21
|
+
const set = useCallback((value) => store.set(value), [store]);
|
|
22
|
+
const patch = useCallback((mutate) => void store.patch(mutate), [store]);
|
|
23
|
+
const save = useCallback(() => store.save(), [store]);
|
|
24
|
+
const clear = useCallback(() => store.clear(), [store]);
|
|
25
|
+
const status = store.status();
|
|
26
|
+
return {
|
|
27
|
+
value: store.value(),
|
|
28
|
+
status,
|
|
29
|
+
slot: store.slot(),
|
|
30
|
+
loading: status === "loading",
|
|
31
|
+
saving: status === "saving",
|
|
32
|
+
set,
|
|
33
|
+
patch,
|
|
34
|
+
save,
|
|
35
|
+
clear,
|
|
36
|
+
};
|
|
37
|
+
}
|
package/dist/selectSnapshot.d.ts
CHANGED
|
@@ -3,5 +3,7 @@ export type SelectCache<TSnapshot, TSelected> = {
|
|
|
3
3
|
snapshot: TSnapshot;
|
|
4
4
|
value: TSelected;
|
|
5
5
|
};
|
|
6
|
+
/** @internal */
|
|
6
7
|
export declare function createSelectCache<TSnapshot, TSelected>(): SelectCache<TSnapshot, TSelected>;
|
|
8
|
+
/** @internal */
|
|
7
9
|
export declare function readSelectSnapshot<TSnapshot, TSelected>(cache: SelectCache<TSnapshot, TSelected>, snapshot: TSnapshot, select: (snapshot: TSnapshot) => TSelected, isEqual?: (previous: TSelected, next: TSelected) => boolean): TSelected;
|
package/dist/selectSnapshot.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
/** @internal */
|
|
1
2
|
export function createSelectCache() {
|
|
2
3
|
return { ready: false, snapshot: undefined, value: undefined };
|
|
3
4
|
}
|
|
5
|
+
/** @internal */
|
|
4
6
|
export function readSelectSnapshot(cache, snapshot, select, isEqual = Object.is) {
|
|
5
7
|
if (cache.ready && Object.is(cache.snapshot, snapshot))
|
|
6
8
|
return cache.value;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import { type EntitySummaryDef, type EntityVital, type SelectionGroup, type SelectionView, type SelectionWindow } from "@jgengine/core/ui/selectionModel";
|
|
3
|
+
/** A minimal keyboard-event shape — a real `KeyboardEvent`/React synthetic satisfies it. */
|
|
4
|
+
export interface SelectionKeyEvent {
|
|
5
|
+
key: string;
|
|
6
|
+
preventDefault(): void;
|
|
7
|
+
}
|
|
8
|
+
/** Options for {@link useSelectionView}. */
|
|
9
|
+
export interface UseSelectionViewOptions {
|
|
10
|
+
/** Member count above which `grouped` flips true (default 12). */
|
|
11
|
+
groupThreshold?: number;
|
|
12
|
+
/** Map a member `kind` to a display label for its group. */
|
|
13
|
+
labelOf?: (kind: string) => string;
|
|
14
|
+
/** Fired when the focused (primary) member changes. */
|
|
15
|
+
onFocusChange?: (id: string) => void;
|
|
16
|
+
}
|
|
17
|
+
/** The live selection model returned by {@link useSelectionView}. */
|
|
18
|
+
export interface SelectionModel {
|
|
19
|
+
view: SelectionView;
|
|
20
|
+
focusId: string | null;
|
|
21
|
+
setFocus: (id: string) => void;
|
|
22
|
+
onKeyDown: (event: SelectionKeyEvent) => void;
|
|
23
|
+
/** A virtualization window over the members, for a large selection strip. */
|
|
24
|
+
window: (scroll: number, size: number) => SelectionWindow;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The DATA/HOOK layer: summarize a selected-entity list into a live model with a focused primary,
|
|
28
|
+
* ordered members, same-kind groups, keyboard focus stepping, and a virtualization window.
|
|
29
|
+
* Rendering-agnostic — feed it to {@link SelectionPanel} or read the `view` directly.
|
|
30
|
+
*
|
|
31
|
+
* @capability selection-model headless multi-entity selection model with primary focus, grouping, and virtualization
|
|
32
|
+
*/
|
|
33
|
+
export declare function useSelectionView(members: readonly EntitySummaryDef[], options?: UseSelectionViewOptions): SelectionModel;
|
|
34
|
+
/**
|
|
35
|
+
* A compact current/max vital meter for an entity summary; tone maps to a colour ramp. Reuse it as
|
|
36
|
+
* the vital row inside {@link EntitySummary} or any custom entity panel.
|
|
37
|
+
*
|
|
38
|
+
* @deprecated The `tone`/`vital`-API umbrella is retired (#1033). Use the atomic, purpose-named,
|
|
39
|
+
* token-themed bars from `@jgengine/react/bars` (`HealthBar`, `ShieldBar`, `ManaBar`, …) and compose
|
|
40
|
+
* your own panel — each does one readout and restyles globally via the shared `--jg-*` tokens.
|
|
41
|
+
* @capability vital-bar compact current/max vital meter for an entity summary
|
|
42
|
+
*/
|
|
43
|
+
export declare function VitalBar({ vital, width, showLabel, className, style, }: {
|
|
44
|
+
vital: EntityVital;
|
|
45
|
+
width?: number;
|
|
46
|
+
showLabel?: boolean;
|
|
47
|
+
className?: string;
|
|
48
|
+
style?: CSSProperties;
|
|
49
|
+
}): import("react").JSX.Element;
|
|
50
|
+
/**
|
|
51
|
+
* A selectable entity portrait tile — icon/initials, an optional thin health strip, and complete
|
|
52
|
+
* selected/hover/focus states as a real `<button>` (`aria-pressed`). The unit-of-a-selection chrome;
|
|
53
|
+
* swap the face via `renderIcon`. Accessible and keyboard-focusable.
|
|
54
|
+
*
|
|
55
|
+
* @capability entity-portrait selectable entity portrait tile with vitals and selected/focus states
|
|
56
|
+
*/
|
|
57
|
+
export declare function EntityPortrait({ entity, selected, size, vitalId, tabIndex, onSelect, renderIcon, className, style, }: {
|
|
58
|
+
entity: EntitySummaryDef;
|
|
59
|
+
selected?: boolean;
|
|
60
|
+
size?: number;
|
|
61
|
+
vitalId?: string;
|
|
62
|
+
tabIndex?: number;
|
|
63
|
+
onSelect?: (id: string) => void;
|
|
64
|
+
renderIcon?: (entity: EntitySummaryDef) => ReactNode;
|
|
65
|
+
className?: string;
|
|
66
|
+
style?: CSSProperties;
|
|
67
|
+
}): import("react").JSX.Element;
|
|
68
|
+
/**
|
|
69
|
+
* The primary-entity detail chrome — name, tags, and full vital bars for the focused member. The
|
|
70
|
+
* "who is selected" summary a single-selection or the head of a multi-selection shows. Swap the vital
|
|
71
|
+
* rows via `renderVital`.
|
|
72
|
+
*
|
|
73
|
+
* @deprecated Bundling a whole vitals array in one component is the combo anti-pattern #1033 retires.
|
|
74
|
+
* Compose your own summary — name/tags plus the atomic `@jgengine/react/bars` (`HealthBar`, …) you
|
|
75
|
+
* need — so no engine component packages multiple readouts by default. Kept during migration.
|
|
76
|
+
* @capability entity-summary primary selected-entity detail panel (name, tags, vitals)
|
|
77
|
+
*/
|
|
78
|
+
export declare function EntitySummary({ entity, renderVital, className, style, }: {
|
|
79
|
+
entity: EntitySummaryDef;
|
|
80
|
+
renderVital?: (vital: EntityVital) => ReactNode;
|
|
81
|
+
className?: string;
|
|
82
|
+
style?: CSSProperties;
|
|
83
|
+
}): import("react").JSX.Element;
|
|
84
|
+
/**
|
|
85
|
+
* A same-kind selection chip — icon plus count — for the grouped large-selection view. The default
|
|
86
|
+
* chrome {@link SelectionCollectionChrome} renders per group; swap it via `renderGroup`.
|
|
87
|
+
*
|
|
88
|
+
* @capability selection-group-chip same-kind selection chip (icon + count) for a grouped selection
|
|
89
|
+
*/
|
|
90
|
+
export declare function SelectionGroupChip({ group, onSelect, renderIcon, className, style, }: {
|
|
91
|
+
group: SelectionGroup;
|
|
92
|
+
onSelect?: (group: SelectionGroup) => void;
|
|
93
|
+
renderIcon?: (group: SelectionGroup) => ReactNode;
|
|
94
|
+
className?: string;
|
|
95
|
+
style?: CSSProperties;
|
|
96
|
+
}): import("react").JSX.Element;
|
|
97
|
+
/**
|
|
98
|
+
* The composable RENDERER for the member collection: a portrait strip for a small selection, or the
|
|
99
|
+
* same-kind group chips once the model is `grouped`. Roving tabindex + arrow-key focus. Swap
|
|
100
|
+
* `renderMember`/`renderGroup` for custom chrome, or read the model and lay it out yourself. No panel
|
|
101
|
+
* skin — a behavior/accessibility layer only.
|
|
102
|
+
*
|
|
103
|
+
* @capability selection-collection accessible portrait-strip / group-chip renderer over a selection model
|
|
104
|
+
*/
|
|
105
|
+
export declare function SelectionCollectionChrome({ model, itemSize, gap, windowSize, scroll, ariaLabel, renderMember, renderGroup, onGroupSelect, className, style, }: {
|
|
106
|
+
model: SelectionModel;
|
|
107
|
+
itemSize?: number;
|
|
108
|
+
gap?: number;
|
|
109
|
+
/** Cap the mounted portraits; the model virtualizes past this many members. */
|
|
110
|
+
windowSize?: number;
|
|
111
|
+
scroll?: number;
|
|
112
|
+
ariaLabel?: string;
|
|
113
|
+
renderMember?: (entity: EntitySummaryDef, selected: boolean) => ReactNode;
|
|
114
|
+
renderGroup?: (group: SelectionGroup) => ReactNode;
|
|
115
|
+
onGroupSelect?: (group: SelectionGroup) => void;
|
|
116
|
+
className?: string;
|
|
117
|
+
style?: CSSProperties;
|
|
118
|
+
}): import("react").JSX.Element;
|
|
119
|
+
/**
|
|
120
|
+
* Convenience composition: skinned glass panel with {@link EntitySummary} for the primary member
|
|
121
|
+
* beside {@link SelectionCollectionChrome} for the rest — demos/scaffold only, not a finished game
|
|
122
|
+
* face. Pass `members` (or a prebuilt `model`); swap sub-renderers or compose the headless pieces
|
|
123
|
+
* under game-owned chrome for product UI.
|
|
124
|
+
*
|
|
125
|
+
* @capability selection-panel optional selection-panel composition: summary + portrait strip / group chips
|
|
126
|
+
*/
|
|
127
|
+
export declare function SelectionPanel({ members, model: providedModel, groupThreshold, labelOf, windowSize, renderPrimary, renderMember, renderVital, onFocusChange, className, style, }: {
|
|
128
|
+
members?: readonly EntitySummaryDef[];
|
|
129
|
+
model?: SelectionModel;
|
|
130
|
+
groupThreshold?: number;
|
|
131
|
+
labelOf?: (kind: string) => string;
|
|
132
|
+
windowSize?: number;
|
|
133
|
+
renderPrimary?: (entity: EntitySummaryDef) => ReactNode;
|
|
134
|
+
renderMember?: (entity: EntitySummaryDef, selected: boolean) => ReactNode;
|
|
135
|
+
renderVital?: (vital: EntityVital) => ReactNode;
|
|
136
|
+
onFocusChange?: (id: string) => void;
|
|
137
|
+
className?: string;
|
|
138
|
+
style?: CSSProperties;
|
|
139
|
+
}): import("react").JSX.Element | null;
|