@jgengine/react 0.8.0 → 0.9.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 +16 -0
- package/dist/chatBubbles.d.ts +15 -0
- package/dist/chatBubbles.js +45 -0
- package/dist/components.d.ts +1 -0
- package/dist/components.js +35 -22
- package/dist/display.d.ts +2 -1
- package/dist/display.js +3 -2
- package/dist/dragLayer.d.ts +6 -1
- package/dist/dragLayer.js +64 -31
- package/dist/engineStore.d.ts +1 -1
- package/dist/engineStore.js +10 -2
- package/dist/fogOverlay.d.ts +21 -0
- package/dist/fogOverlay.js +34 -0
- package/dist/hooks.d.ts +9 -1
- package/dist/hooks.js +63 -11
- package/dist/hudLayout.d.ts +61 -0
- package/dist/hudLayout.js +488 -0
- package/dist/hudViewport.d.ts +21 -0
- package/dist/hudViewport.js +17 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/liveBind.d.ts +4 -10
- package/dist/liveBind.js +28 -16
- package/dist/map.d.ts +27 -7
- package/dist/map.js +152 -43
- package/dist/preview.d.ts +6 -0
- package/dist/preview.js +1 -0
- package/dist/selectSnapshot.d.ts +7 -0
- package/dist/selectSnapshot.js +16 -0
- package/dist/settings.d.ts +75 -0
- package/dist/settings.js +61 -0
- package/dist/skillCheckPaint.d.ts +4 -0
- package/dist/skillCheckPaint.js +28 -0
- package/llms.txt +776 -869
- package/package.json +9 -5
package/dist/liveBind.js
CHANGED
|
@@ -1,32 +1,44 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
const subscribers = new Set();
|
|
4
|
+
let sharedRaf = 0;
|
|
5
|
+
function sharedTick() {
|
|
6
|
+
for (const subscriber of subscribers)
|
|
7
|
+
subscriber();
|
|
8
|
+
if (subscribers.size > 0)
|
|
9
|
+
sharedRaf = requestAnimationFrame(sharedTick);
|
|
10
|
+
else
|
|
11
|
+
sharedRaf = 0;
|
|
12
|
+
}
|
|
13
|
+
export function subscribeFrameBind(subscriber) {
|
|
14
|
+
subscribers.add(subscriber);
|
|
15
|
+
if (sharedRaf === 0 && typeof requestAnimationFrame !== "undefined") {
|
|
16
|
+
sharedRaf = requestAnimationFrame(sharedTick);
|
|
17
|
+
}
|
|
18
|
+
return () => {
|
|
19
|
+
subscribers.delete(subscriber);
|
|
20
|
+
if (subscribers.size === 0 && sharedRaf !== 0 && typeof cancelAnimationFrame !== "undefined") {
|
|
21
|
+
cancelAnimationFrame(sharedRaf);
|
|
22
|
+
sharedRaf = 0;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function frameBindSubscriberCount() {
|
|
27
|
+
return subscribers.size;
|
|
28
|
+
}
|
|
9
29
|
export function useFrameBind(ref, get, apply) {
|
|
10
30
|
const getRef = useRef(get);
|
|
11
31
|
getRef.current = get;
|
|
12
32
|
const applyRef = useRef(apply);
|
|
13
33
|
applyRef.current = apply;
|
|
14
34
|
useEffect(() => {
|
|
15
|
-
|
|
16
|
-
const tick = () => {
|
|
35
|
+
return subscribeFrameBind(() => {
|
|
17
36
|
const element = ref.current;
|
|
18
37
|
if (element !== null)
|
|
19
38
|
applyRef.current(getRef.current(), element);
|
|
20
|
-
|
|
21
|
-
};
|
|
22
|
-
raf = requestAnimationFrame(tick);
|
|
23
|
-
return () => cancelAnimationFrame(raf);
|
|
39
|
+
});
|
|
24
40
|
}, [ref]);
|
|
25
41
|
}
|
|
26
|
-
/**
|
|
27
|
-
* A `<span>` whose text tracks a live value every frame (the 90% case of
|
|
28
|
-
* useFrameBind). `<LiveText get={() => groundSpeed(car) * KMH} format={Math.round} />`.
|
|
29
|
-
*/
|
|
30
42
|
export function LiveText({ get, format, className, style, }) {
|
|
31
43
|
const ref = useRef(null);
|
|
32
44
|
useFrameBind(ref, get, (value, element) => {
|
package/dist/map.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type ReactNode } from "react";
|
|
|
2
2
|
import type { FogField } from "@jgengine/core/world/fog";
|
|
3
3
|
import { type MapMarker, type MarkerKindStyle, type MarkerSet } from "@jgengine/core/world/markers";
|
|
4
4
|
import { type WorldXZ } from "@jgengine/core/world/minimap";
|
|
5
|
+
import { type MapCellStates, type MapRoute, type MapZone } from "@jgengine/core/world/mapLayers";
|
|
5
6
|
export declare function useMarkers(markers: MarkerSet): readonly MapMarker[];
|
|
6
7
|
export declare function useFog(fog: FogField): ReturnType<FogField["cells"]>;
|
|
7
8
|
export interface MinimapProps {
|
|
@@ -10,12 +11,21 @@ export interface MinimapProps {
|
|
|
10
11
|
worldRadius: number;
|
|
11
12
|
fog?: FogField;
|
|
12
13
|
size?: number;
|
|
13
|
-
|
|
14
|
+
/** Facing yaw (`rotationY`, forward = `(sin yaw, cos yaw)`) of the tracked entity — pass it raw, never pre-converted to a bearing. */
|
|
15
|
+
facingYaw?: number;
|
|
14
16
|
rotate?: boolean;
|
|
15
17
|
kindStyles?: Record<string, MarkerKindStyle>;
|
|
16
18
|
background?: string;
|
|
17
19
|
/** World bounds the `background` image spans, so it pans correctly under a player-centered map. */
|
|
18
20
|
mapBounds?: MapBounds;
|
|
21
|
+
/** Route/corridor polylines drawn under the markers (#285.2); `forecast` routes dash. */
|
|
22
|
+
routes?: readonly MapRoute[];
|
|
23
|
+
/** Zone/hazard overlays with live fills and dashed forecast outlines (#285.1). */
|
|
24
|
+
zones?: readonly MapZone[];
|
|
25
|
+
/** Per-cell status heatmaps (#285.1) — pair with `world/cellStates`. */
|
|
26
|
+
cellStates?: readonly MapCellStates[];
|
|
27
|
+
/** Click-to-pin seam (#285.6): receives the clicked world XZ via `unprojectFromMinimap`. */
|
|
28
|
+
onWorldClick?: (world: WorldXZ) => void;
|
|
19
29
|
className?: string;
|
|
20
30
|
title?: string;
|
|
21
31
|
children?: ReactNode;
|
|
@@ -31,9 +41,10 @@ export interface MapBounds {
|
|
|
31
41
|
* fog overlay, categorized marker icons, and a facing arrow. Reads a core
|
|
32
42
|
* `MarkerSet` / `FogField`; supply your own `kindStyles` palette to reskin.
|
|
33
43
|
*/
|
|
34
|
-
export declare function Minimap({ markers, center, worldRadius, fog, size,
|
|
44
|
+
export declare function Minimap({ markers, center, worldRadius, fog, size, facingYaw, rotate, kindStyles, background, mapBounds, routes, zones, cellStates, onWorldClick, className, title, children, }: MinimapProps): ReactNode;
|
|
35
45
|
export interface CompassProps {
|
|
36
|
-
|
|
46
|
+
/** Facing yaw (`rotationY`, forward = `(sin yaw, cos yaw)`) of the tracked entity — pass it raw, never pre-converted to a bearing. */
|
|
47
|
+
facingYaw: number;
|
|
37
48
|
center?: WorldXZ;
|
|
38
49
|
markers?: MarkerSet;
|
|
39
50
|
width?: number;
|
|
@@ -42,20 +53,29 @@ export interface CompassProps {
|
|
|
42
53
|
className?: string;
|
|
43
54
|
}
|
|
44
55
|
/**
|
|
45
|
-
* Horizontal compass strip centered on the player's facing
|
|
56
|
+
* Horizontal compass strip centered on the player's facing direction, with the
|
|
46
57
|
* eight cardinals and optional marker pips (bearing to each `MarkerSet` entry).
|
|
47
58
|
*/
|
|
48
|
-
export declare function Compass({
|
|
59
|
+
export declare function Compass({ facingYaw, center, markers, width, fov, kindStyles, className, }: CompassProps): ReactNode;
|
|
49
60
|
export interface WorldMapProps {
|
|
50
61
|
markers: MarkerSet;
|
|
51
62
|
bounds: MapBounds;
|
|
52
63
|
player?: WorldXZ;
|
|
53
|
-
|
|
64
|
+
/** Facing yaw (`rotationY`, forward = `(sin yaw, cos yaw)`) of the player — pass it raw, never pre-converted to a bearing. */
|
|
65
|
+
facingYaw?: number;
|
|
54
66
|
fog?: FogField;
|
|
55
67
|
background?: string;
|
|
56
68
|
width?: number;
|
|
57
69
|
height?: number;
|
|
58
70
|
kindStyles?: Record<string, MarkerKindStyle>;
|
|
71
|
+
/** Route/corridor polylines drawn under the markers (#285.2); `forecast` routes dash. */
|
|
72
|
+
routes?: readonly MapRoute[];
|
|
73
|
+
/** Zone/hazard overlays with live fills and dashed forecast outlines (#285.1). */
|
|
74
|
+
zones?: readonly MapZone[];
|
|
75
|
+
/** Per-cell status heatmaps (#285.1) — pair with `world/cellStates`. */
|
|
76
|
+
cellStates?: readonly MapCellStates[];
|
|
77
|
+
/** Click-to-pin seam (#285.6): receives the clicked world XZ. */
|
|
78
|
+
onWorldClick?: (world: WorldXZ) => void;
|
|
59
79
|
className?: string;
|
|
60
80
|
title?: string;
|
|
61
81
|
onClose?: () => void;
|
|
@@ -65,4 +85,4 @@ export interface WorldMapProps {
|
|
|
65
85
|
* background, reveal-on-event fog, all markers with labels, and the player.
|
|
66
86
|
* Rectangular linear projection over the supplied world `bounds`.
|
|
67
87
|
*/
|
|
68
|
-
export declare function WorldMap({ markers, bounds, player,
|
|
88
|
+
export declare function WorldMap({ markers, bounds, player, facingYaw, fog, background, width, height, kindStyles, routes, zones, cellStates, onWorldClick, className, title, onClose, }: WorldMapProps): ReactNode;
|
package/dist/map.js
CHANGED
|
@@ -1,7 +1,73 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useSyncExternalStore } from "react";
|
|
2
|
+
import { useMemo, useSyncExternalStore } from "react";
|
|
3
3
|
import { DEFAULT_MARKER_KINDS, markerKindStyle, } from "@jgengine/core/world/markers";
|
|
4
|
-
import { bearingToCardinal, clampToMinimapEdge, compassBearing, projectToMinimap, relativeBearing, } from "@jgengine/core/world/minimap";
|
|
4
|
+
import { bearingToCardinal, clampToMinimapEdge, compassBearing, headingToBearing, projectToMinimap, relativeBearing, unprojectFromMinimap, } from "@jgengine/core/world/minimap";
|
|
5
|
+
import { mapLayerColor, } from "@jgengine/core/world/mapLayers";
|
|
6
|
+
import { createFogDataUrl } from "./fogOverlay.js";
|
|
7
|
+
function routeNodes(routes, project, keyPrefix) {
|
|
8
|
+
if (routes === undefined)
|
|
9
|
+
return [];
|
|
10
|
+
return routes.map((route) => {
|
|
11
|
+
const points = route.closed === true && route.points.length > 1 ? [...route.points, route.points[0]] : route.points;
|
|
12
|
+
const path = points.map(([x, z]) => {
|
|
13
|
+
const at = project(x, z);
|
|
14
|
+
return `${at.x},${at.y}`;
|
|
15
|
+
});
|
|
16
|
+
return (_jsx("polyline", { "data-map-route": route.id, points: path.join(" "), fill: "none", stroke: mapLayerColor(route.tone), strokeWidth: route.width ?? 2, strokeLinejoin: "round", strokeLinecap: "round", opacity: 0.9, ...(route.forecast === true ? { strokeDasharray: "6 4" } : {}) }, `${keyPrefix}-route-${route.id}`));
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function zoneNodes(zones, project, scale, keyPrefix) {
|
|
20
|
+
if (zones === undefined)
|
|
21
|
+
return [];
|
|
22
|
+
return zones.map((zone) => {
|
|
23
|
+
const color = mapLayerColor(zone.tone);
|
|
24
|
+
const forecast = zone.forecast === true;
|
|
25
|
+
const fill = forecast ? "none" : color;
|
|
26
|
+
const fillOpacity = forecast ? 0 : zone.opacity ?? 0.25;
|
|
27
|
+
const stroke = { stroke: color, strokeWidth: 1.5, ...(forecast ? { strokeDasharray: "6 4" } : {}) };
|
|
28
|
+
const shape = zone.shape;
|
|
29
|
+
let node = null;
|
|
30
|
+
let labelAt = null;
|
|
31
|
+
if (shape.kind === "circle") {
|
|
32
|
+
const at = project(shape.center[0], shape.center[1]);
|
|
33
|
+
labelAt = at;
|
|
34
|
+
node = (_jsx("ellipse", { cx: at.x, cy: at.y, rx: scale.x(shape.radius), ry: scale.y(shape.radius), fill: fill, fillOpacity: fillOpacity, ...stroke }));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const corners = shape.kind === "rect"
|
|
38
|
+
? rectCorners(shape).map(([x, z]) => project(x, z))
|
|
39
|
+
: shape.points.map(([x, z]) => project(x, z));
|
|
40
|
+
labelAt = corners[0] ?? null;
|
|
41
|
+
node = (_jsx("polygon", { points: corners.map((at) => `${at.x},${at.y}`).join(" "), fill: fill, fillOpacity: fillOpacity, ...stroke }));
|
|
42
|
+
}
|
|
43
|
+
return (_jsxs("g", { "data-map-zone": zone.id, opacity: 0.9, children: [node, zone.label !== undefined && labelAt !== null ? (_jsx("text", { x: labelAt.x, y: labelAt.y - 4, textAnchor: "middle", fontSize: 9, fill: color, style: { fontWeight: 700 }, children: zone.label })) : null] }, `${keyPrefix}-zone-${zone.id}`));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function rectCorners(shape) {
|
|
47
|
+
const halfW = shape.w / 2;
|
|
48
|
+
const halfD = shape.d / 2;
|
|
49
|
+
const rotate = shape.rotate ?? 0;
|
|
50
|
+
const cos = Math.cos(rotate);
|
|
51
|
+
const sin = Math.sin(rotate);
|
|
52
|
+
return [[-halfW, -halfD], [halfW, -halfD], [halfW, halfD], [-halfW, halfD]].map(([dx, dz]) => [
|
|
53
|
+
shape.center[0] + dx * cos - dz * sin,
|
|
54
|
+
shape.center[1] + dx * sin + dz * cos,
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
function cellStateNodes(layers, project, scale, keyPrefix) {
|
|
58
|
+
if (layers === undefined)
|
|
59
|
+
return [];
|
|
60
|
+
const nodes = [];
|
|
61
|
+
for (const layer of layers) {
|
|
62
|
+
const w = scale.x(layer.cellSize);
|
|
63
|
+
const h = scale.y(layer.cellSize);
|
|
64
|
+
for (const cell of layer.cells) {
|
|
65
|
+
const at = project(layer.origin[0] + cell.col * layer.cellSize, layer.origin[1] + cell.row * layer.cellSize);
|
|
66
|
+
nodes.push(_jsx("rect", { x: at.x - w / 2, y: at.y - h / 2, width: w, height: h, fill: mapLayerColor(cell.tone), opacity: cell.opacity ?? 0.35 }, `${keyPrefix}-${layer.id}-${cell.col}-${cell.row}`));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return nodes;
|
|
70
|
+
}
|
|
5
71
|
export function useMarkers(markers) {
|
|
6
72
|
return useSyncExternalStore(markers.subscribe, markers.snapshot, markers.snapshot);
|
|
7
73
|
}
|
|
@@ -13,34 +79,37 @@ export function useFog(fog) {
|
|
|
13
79
|
* fog overlay, categorized marker icons, and a facing arrow. Reads a core
|
|
14
80
|
* `MarkerSet` / `FogField`; supply your own `kindStyles` palette to reskin.
|
|
15
81
|
*/
|
|
16
|
-
export function Minimap({ markers, center, worldRadius, fog, size = 176,
|
|
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, }) {
|
|
17
83
|
const markerList = useMarkers(markers);
|
|
18
84
|
const fogCells = useSyncExternalStore(fog?.subscribe ?? NO_SUBSCRIBE, fog?.cells ?? NULL_CELLS, fog?.cells ?? NULL_CELLS);
|
|
85
|
+
const bearing = headingToBearing(facingYaw);
|
|
19
86
|
const view = {
|
|
20
87
|
center,
|
|
21
88
|
worldRadius,
|
|
22
89
|
size,
|
|
23
|
-
...(rotate ? { rotate:
|
|
90
|
+
...(rotate ? { rotate: bearing } : {}),
|
|
24
91
|
};
|
|
25
92
|
const half = size / 2;
|
|
26
93
|
const clipId = `mm-clip-${size}`;
|
|
27
94
|
const sorted = [...markerList].sort((a, b) => markerKindStyle(a.kind, kindStyles).priority - markerKindStyle(b.kind, kindStyles).priority);
|
|
28
|
-
const
|
|
29
|
-
|
|
95
|
+
const fogImage = useMemo(() => {
|
|
96
|
+
if (fogCells === null)
|
|
97
|
+
return null;
|
|
30
98
|
const cellPx = (fogCells.cellSize / worldRadius) * half;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
99
|
+
return createFogDataUrl(fogCells, { width: size, height: size }, (col, row) => {
|
|
100
|
+
const worldX = fogCells.minX + (col + 0.5) * fogCells.cellSize;
|
|
101
|
+
const worldZ = fogCells.minZ + (row + 0.5) * fogCells.cellSize;
|
|
102
|
+
const projected = projectToMinimap([worldX, worldZ], view);
|
|
103
|
+
if (projected.distance > worldRadius + fogCells.cellSize)
|
|
104
|
+
return null;
|
|
105
|
+
return {
|
|
106
|
+
x: projected.x - cellPx / 2 - 0.5,
|
|
107
|
+
y: projected.y - cellPx / 2 - 0.5,
|
|
108
|
+
width: cellPx + 1,
|
|
109
|
+
height: cellPx + 1,
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}, [fogCells, center[0], center[1], worldRadius, size, rotate, bearing]);
|
|
44
113
|
return (_jsxs("div", { className: className, "data-minimap": true, style: {
|
|
45
114
|
width: size,
|
|
46
115
|
borderRadius: 14,
|
|
@@ -59,7 +128,17 @@ export function Minimap({ markers, center, worldRadius, fog, size = 176, heading
|
|
|
59
128
|
letterSpacing: 1.4,
|
|
60
129
|
textTransform: "uppercase",
|
|
61
130
|
color: "rgba(203,213,225,0.75)",
|
|
62
|
-
}, children: [_jsx("span", { children: title }), _jsx("span", { style: { color: "rgba(148,163,184,0.6)" }, children: bearingToCardinal(
|
|
131
|
+
}, children: [_jsx("span", { children: title }), _jsx("span", { style: { color: "rgba(148,163,184,0.6)" }, children: bearingToCardinal(bearing) })] }), _jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, "data-minimap-canvas": true, ...(onWorldClick === undefined
|
|
132
|
+
? {}
|
|
133
|
+
: {
|
|
134
|
+
onClick: (event) => {
|
|
135
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
136
|
+
const px = ((event.clientX - rect.left) / rect.width) * size;
|
|
137
|
+
const py = ((event.clientY - rect.top) / rect.height) * size;
|
|
138
|
+
onWorldClick(unprojectFromMinimap({ x: px, y: py }, view));
|
|
139
|
+
},
|
|
140
|
+
style: { cursor: "crosshair" },
|
|
141
|
+
}), children: [_jsx("defs", { children: _jsx("clipPath", { id: clipId, children: _jsx("circle", { cx: half, cy: half, r: half - 1 }) }) }), _jsxs("g", { clipPath: `url(#${clipId})`, children: [_jsx("circle", { cx: half, cy: half, r: half - 1, fill: "#1c2733" }), background !== undefined
|
|
63
142
|
? (() => {
|
|
64
143
|
if (mapBounds === undefined) {
|
|
65
144
|
return (_jsx("image", { href: background, x: 0, y: 0, width: size, height: size, preserveAspectRatio: "xMidYMid slice", opacity: 0.9 }));
|
|
@@ -67,12 +146,21 @@ export function Minimap({ markers, center, worldRadius, fog, size = 176, heading
|
|
|
67
146
|
const pxPerWorld = half / worldRadius;
|
|
68
147
|
return (_jsx("image", { href: background, x: half + (mapBounds.minX - center[0]) * pxPerWorld, y: half + (mapBounds.minZ - center[1]) * pxPerWorld, width: (mapBounds.maxX - mapBounds.minX) * pxPerWorld, height: (mapBounds.maxZ - mapBounds.minZ) * pxPerWorld, preserveAspectRatio: "none", opacity: 0.92 }));
|
|
69
148
|
})()
|
|
70
|
-
: null,
|
|
149
|
+
: null, fogImage !== null ? (_jsx("image", { href: fogImage, x: 0, y: 0, width: size, height: size, preserveAspectRatio: "none" })) : null, (() => {
|
|
150
|
+
const projectXZ = (x, z) => projectToMinimap([x, z], view);
|
|
151
|
+
const pxPerWorld = worldRadius === 0 ? 0 : half / worldRadius;
|
|
152
|
+
const layerScale = { x: (units) => units * pxPerWorld, y: (units) => units * pxPerWorld };
|
|
153
|
+
return [
|
|
154
|
+
...cellStateNodes(cellStates, projectXZ, layerScale, "mm"),
|
|
155
|
+
...zoneNodes(zones, projectXZ, layerScale, "mm"),
|
|
156
|
+
...routeNodes(routes, projectXZ, "mm"),
|
|
157
|
+
];
|
|
158
|
+
})(), _jsx("circle", { cx: half, cy: half, r: half - 1, fill: "none", stroke: "rgba(148,163,184,0.12)" }), sorted.map((marker) => {
|
|
71
159
|
const style = markerKindStyle(marker.kind, kindStyles);
|
|
72
160
|
const projected = projectToMinimap(marker.position, view);
|
|
73
161
|
const at = projected.inside ? projected : clampToMinimapEdge(projected, size);
|
|
74
162
|
return (_jsxs("g", { "data-marker-kind": marker.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));
|
|
75
|
-
}), _jsx("g", { transform: `translate(${half} ${half}) rotate(${(rotate ? 0 :
|
|
163
|
+
}), _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] }));
|
|
76
164
|
}
|
|
77
165
|
const NO_SUBSCRIBE = () => () => undefined;
|
|
78
166
|
const NULL_CELLS = () => null;
|
|
@@ -89,14 +177,15 @@ const COMPASS_TICKS = [
|
|
|
89
177
|
{ cardinal: "NW", bearing: (7 * Math.PI) / 4 },
|
|
90
178
|
];
|
|
91
179
|
/**
|
|
92
|
-
* Horizontal compass strip centered on the player's facing
|
|
180
|
+
* Horizontal compass strip centered on the player's facing direction, with the
|
|
93
181
|
* eight cardinals and optional marker pips (bearing to each `MarkerSet` entry).
|
|
94
182
|
*/
|
|
95
|
-
export function Compass({
|
|
183
|
+
export function Compass({ facingYaw, center, markers, width = 340, fov = (Math.PI * 2) / 3, kindStyles = DEFAULT_MARKER_KINDS, className, }) {
|
|
96
184
|
const markerList = useSyncExternalStore(markers?.subscribe ?? NO_SUBSCRIBE, markers?.snapshot ?? EMPTY_MARKERS, markers?.snapshot ?? EMPTY_MARKERS);
|
|
185
|
+
const facingBearing = headingToBearing(facingYaw);
|
|
97
186
|
const half = fov / 2;
|
|
98
187
|
const toX = (bearing) => {
|
|
99
|
-
const delta = relativeBearing(bearing,
|
|
188
|
+
const delta = relativeBearing(bearing, facingBearing);
|
|
100
189
|
if (Math.abs(delta) > half)
|
|
101
190
|
return null;
|
|
102
191
|
return width / 2 + (delta / half) * (width / 2);
|
|
@@ -151,12 +240,27 @@ export function Compass({ heading, center, markers, width = 340, fov = (Math.PI
|
|
|
151
240
|
})
|
|
152
241
|
: null] }));
|
|
153
242
|
}
|
|
243
|
+
function WorldMapFogImage({ fogCells, project, worldW, worldD, width, mapH, }) {
|
|
244
|
+
const fogImage = useMemo(() => {
|
|
245
|
+
if (fogCells === null)
|
|
246
|
+
return null;
|
|
247
|
+
const cellW = (fogCells.cellSize / worldW) * width;
|
|
248
|
+
const cellH = (fogCells.cellSize / worldD) * mapH;
|
|
249
|
+
return createFogDataUrl(fogCells, { width, height: mapH }, (col, row) => {
|
|
250
|
+
const at = project(fogCells.minX + col * fogCells.cellSize, fogCells.minZ + row * fogCells.cellSize);
|
|
251
|
+
return { x: at.x, y: at.y, width: cellW + 0.5, height: cellH + 0.5 };
|
|
252
|
+
}, "rgba(11, 15, 20, 0.86)");
|
|
253
|
+
}, [fogCells, project, worldW, worldD, width, mapH]);
|
|
254
|
+
if (fogImage === null)
|
|
255
|
+
return null;
|
|
256
|
+
return _jsx("image", { href: fogImage, x: 0, y: 0, width: width, height: mapH, preserveAspectRatio: "none" });
|
|
257
|
+
}
|
|
154
258
|
/**
|
|
155
259
|
* Full-bounds top-down world map (the "press M" overlay): baked terrain
|
|
156
260
|
* background, reveal-on-event fog, all markers with labels, and the player.
|
|
157
261
|
* Rectangular linear projection over the supplied world `bounds`.
|
|
158
262
|
*/
|
|
159
|
-
export function WorldMap({ markers, bounds, player,
|
|
263
|
+
export function WorldMap({ markers, bounds, player, facingYaw = 0, fog, background, width = 520, height, kindStyles = DEFAULT_MARKER_KINDS, routes, zones, cellStates, onWorldClick, className, title = "World Map", onClose, }) {
|
|
160
264
|
const markerList = useMarkers(markers);
|
|
161
265
|
const fogCells = useSyncExternalStore(fog?.subscribe ?? NO_SUBSCRIBE, fog?.cells ?? NULL_CELLS, fog?.cells ?? NULL_CELLS);
|
|
162
266
|
const worldW = bounds.maxX - bounds.minX;
|
|
@@ -182,29 +286,34 @@ export function WorldMap({ markers, bounds, player, heading = 0, fog, background
|
|
|
182
286
|
fontSize: 11,
|
|
183
287
|
padding: "3px 9px",
|
|
184
288
|
cursor: "pointer",
|
|
185
|
-
}, children: "Close" })) : null] }), _jsxs("svg", { width: width, height: mapH, viewBox: `0 0 ${width} ${mapH}`, "data-world-map-canvas": true, style: { borderRadius: 10
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
289
|
+
}, children: "Close" })) : null] }), _jsxs("svg", { width: width, height: mapH, viewBox: `0 0 ${width} ${mapH}`, "data-world-map-canvas": true, style: { borderRadius: 10, ...(onWorldClick === undefined ? {} : { cursor: "crosshair" }) }, ...(onWorldClick === undefined
|
|
290
|
+
? {}
|
|
291
|
+
: {
|
|
292
|
+
onClick: (event) => {
|
|
293
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
294
|
+
const px = ((event.clientX - rect.left) / rect.width) * width;
|
|
295
|
+
const py = ((event.clientY - rect.top) / rect.height) * mapH;
|
|
296
|
+
onWorldClick([bounds.minX + (px / width) * worldW, bounds.minZ + (py / mapH) * worldD]);
|
|
297
|
+
},
|
|
298
|
+
}), children: [_jsx("rect", { x: 0, y: 0, width: width, height: mapH, fill: "#16202b" }), background !== undefined ? (_jsx("image", { href: background, x: 0, y: 0, width: width, height: mapH, preserveAspectRatio: "none", opacity: 0.95 })) : null, _jsx(WorldMapFogImage, { fogCells: fogCells, project: project, worldW: worldW, worldD: worldD, width: width, mapH: mapH }), (() => {
|
|
299
|
+
const projectXZ = (x, z) => project(x, z);
|
|
300
|
+
const layerScale = {
|
|
301
|
+
x: (units) => (units / worldW) * width,
|
|
302
|
+
y: (units) => (units / worldD) * mapH,
|
|
303
|
+
};
|
|
304
|
+
return [
|
|
305
|
+
...cellStateNodes(cellStates, projectXZ, layerScale, "wm"),
|
|
306
|
+
...zoneNodes(zones, projectXZ, layerScale, "wm"),
|
|
307
|
+
...routeNodes(routes, projectXZ, "wm"),
|
|
308
|
+
];
|
|
309
|
+
})(), markerList.map((marker) => {
|
|
201
310
|
const at = project(marker.position[0], marker.position[2]);
|
|
202
311
|
const style = markerKindStyle(marker.kind, kindStyles);
|
|
203
312
|
return (_jsxs("g", { "data-world-marker": 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));
|
|
204
313
|
}), player !== undefined
|
|
205
314
|
? (() => {
|
|
206
315
|
const at = project(player[0], player[1]);
|
|
207
|
-
return (_jsx("g", { transform: `translate(${at.x} ${at.y}) rotate(${
|
|
316
|
+
return (_jsx("g", { transform: `translate(${at.x} ${at.y}) rotate(${headingToBearing(facingYaw) * (180 / Math.PI)})`, children: _jsx("path", { d: "M0,-11 L7,9 L0,4 L-7,9 Z", fill: "#4ade80", stroke: "#052e16", strokeWidth: 1 }) }));
|
|
208
317
|
})()
|
|
209
318
|
: null] })] }));
|
|
210
319
|
}
|
package/dist/preview.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type SelectCache<TSnapshot, TSelected> = {
|
|
2
|
+
ready: boolean;
|
|
3
|
+
snapshot: TSnapshot;
|
|
4
|
+
value: TSelected;
|
|
5
|
+
};
|
|
6
|
+
export declare function createSelectCache<TSnapshot, TSelected>(): SelectCache<TSnapshot, TSelected>;
|
|
7
|
+
export declare function readSelectSnapshot<TSnapshot, TSelected>(cache: SelectCache<TSnapshot, TSelected>, snapshot: TSnapshot, select: (snapshot: TSnapshot) => TSelected, isEqual?: (previous: TSelected, next: TSelected) => boolean): TSelected;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function createSelectCache() {
|
|
2
|
+
return { ready: false, snapshot: undefined, value: undefined };
|
|
3
|
+
}
|
|
4
|
+
export function readSelectSnapshot(cache, snapshot, select, isEqual = Object.is) {
|
|
5
|
+
if (cache.ready && Object.is(cache.snapshot, snapshot))
|
|
6
|
+
return cache.value;
|
|
7
|
+
const next = select(snapshot);
|
|
8
|
+
if (cache.ready && isEqual(cache.value, next)) {
|
|
9
|
+
cache.snapshot = snapshot;
|
|
10
|
+
return cache.value;
|
|
11
|
+
}
|
|
12
|
+
cache.ready = true;
|
|
13
|
+
cache.snapshot = snapshot;
|
|
14
|
+
cache.value = next;
|
|
15
|
+
return cache.value;
|
|
16
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type SettingCategory, type SettingKind, type SettingOption, type SettingsStore, type SettingsSurface, type SettingsVariant, type SettingValue } from "@jgengine/core/settings/settingsModel";
|
|
3
|
+
export declare function SettingsProvider({ store, children }: {
|
|
4
|
+
store: SettingsStore;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}): import("react").JSX.Element;
|
|
7
|
+
/** The shared settings store, or a standalone one when no provider is mounted (game code read outside the shell). */
|
|
8
|
+
export declare function useSettingsStore(): SettingsStore;
|
|
9
|
+
/** Read + write one persisted setting; re-renders when the value changes anywhere. */
|
|
10
|
+
export declare function useSetting<T extends SettingValue>(id: string, fallback: T): readonly [T, (value: SettingValue) => void];
|
|
11
|
+
export interface SettingsRow {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
kind: SettingKind;
|
|
15
|
+
value: SettingValue;
|
|
16
|
+
min?: number;
|
|
17
|
+
max?: number;
|
|
18
|
+
step?: number;
|
|
19
|
+
options?: readonly SettingOption[];
|
|
20
|
+
format?: (value: number) => string;
|
|
21
|
+
set: (value: SettingValue) => void;
|
|
22
|
+
}
|
|
23
|
+
export interface SettingsKeybindRow {
|
|
24
|
+
action: string;
|
|
25
|
+
label: string;
|
|
26
|
+
bindingLabel: string;
|
|
27
|
+
isDefault: boolean;
|
|
28
|
+
rebind: (code: string) => void;
|
|
29
|
+
reset: () => void;
|
|
30
|
+
}
|
|
31
|
+
export interface SettingsCategoryView {
|
|
32
|
+
id: SettingCategory;
|
|
33
|
+
label: string;
|
|
34
|
+
rows: SettingsRow[];
|
|
35
|
+
keybinds: SettingsKeybindRow[];
|
|
36
|
+
}
|
|
37
|
+
/** A resolved game-state action — `run` is already bound to the game context and closes the menu. */
|
|
38
|
+
export interface SettingsActionView {
|
|
39
|
+
id: string;
|
|
40
|
+
label: string;
|
|
41
|
+
kind: "default" | "danger";
|
|
42
|
+
description?: string;
|
|
43
|
+
run: () => void;
|
|
44
|
+
}
|
|
45
|
+
/** The live settings controller — every category/row/keybind/action plus open-state. Render it any way you like or drive the engine menu. */
|
|
46
|
+
export interface SettingsController {
|
|
47
|
+
categories: SettingsCategoryView[];
|
|
48
|
+
/** Game-state actions (Restart, Quit…) — the menu shows them as its first "Game" tab. */
|
|
49
|
+
actions: SettingsActionView[];
|
|
50
|
+
variant: SettingsVariant;
|
|
51
|
+
surface: SettingsSurface | false;
|
|
52
|
+
isOpen: boolean;
|
|
53
|
+
open: () => void;
|
|
54
|
+
close: () => void;
|
|
55
|
+
setOpen: (open: boolean) => void;
|
|
56
|
+
}
|
|
57
|
+
export declare function SettingsControllerProvider({ controller, children, }: {
|
|
58
|
+
controller: SettingsController;
|
|
59
|
+
children: ReactNode;
|
|
60
|
+
}): import("react").JSX.Element;
|
|
61
|
+
/** The engine settings controller for the current game — render your own settings UI from `categories`, or open the built-in menu with `open()`. Null-safe stub when mounted outside the shell. */
|
|
62
|
+
export declare function useSettings(): SettingsController;
|
|
63
|
+
/** True when the game has any setting or game-action to show — gate your own settings entry on it. */
|
|
64
|
+
export declare function useHasSettings(): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Inline settings entry — drop it anywhere in your game's menu or HUD; it opens
|
|
67
|
+
* the themed settings menu. Headless: pass `className` for placement/skin and
|
|
68
|
+
* `children` to replace the default gear glyph. Renders nothing when the game
|
|
69
|
+
* has no settings to show, so it never leaves a dead button behind.
|
|
70
|
+
*/
|
|
71
|
+
export declare function SettingsTrigger({ className, children, label, }: {
|
|
72
|
+
className?: string;
|
|
73
|
+
children?: ReactNode;
|
|
74
|
+
label?: string;
|
|
75
|
+
}): import("react").JSX.Element | null;
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useContext, useSyncExternalStore } from "react";
|
|
3
|
+
import { createSettingsStore, } from "@jgengine/core/settings/settingsModel";
|
|
4
|
+
const SettingsStoreContext = createContext(null);
|
|
5
|
+
export function SettingsProvider({ store, children }) {
|
|
6
|
+
return _jsx(SettingsStoreContext.Provider, { value: store, children: children });
|
|
7
|
+
}
|
|
8
|
+
/** The shared settings store, or a standalone one when no provider is mounted (game code read outside the shell). */
|
|
9
|
+
export function useSettingsStore() {
|
|
10
|
+
const store = useContext(SettingsStoreContext);
|
|
11
|
+
return store ?? fallbackStore();
|
|
12
|
+
}
|
|
13
|
+
let standalone = null;
|
|
14
|
+
function fallbackStore() {
|
|
15
|
+
if (standalone === null)
|
|
16
|
+
standalone = createSettingsStore();
|
|
17
|
+
return standalone;
|
|
18
|
+
}
|
|
19
|
+
/** Read + write one persisted setting; re-renders when the value changes anywhere. */
|
|
20
|
+
export function useSetting(id, fallback) {
|
|
21
|
+
const store = useSettingsStore();
|
|
22
|
+
const value = useSyncExternalStore(store.subscribe, () => store.get(id, fallback), () => fallback);
|
|
23
|
+
const set = useCallback((next) => store.set(id, next), [store, id]);
|
|
24
|
+
return [value, set];
|
|
25
|
+
}
|
|
26
|
+
const SettingsControllerContext = createContext(null);
|
|
27
|
+
export function SettingsControllerProvider({ controller, children, }) {
|
|
28
|
+
return _jsx(SettingsControllerContext.Provider, { value: controller, children: children });
|
|
29
|
+
}
|
|
30
|
+
/** The engine settings controller for the current game — render your own settings UI from `categories`, or open the built-in menu with `open()`. Null-safe stub when mounted outside the shell. */
|
|
31
|
+
export function useSettings() {
|
|
32
|
+
return useContext(SettingsControllerContext) ?? EMPTY_CONTROLLER;
|
|
33
|
+
}
|
|
34
|
+
const noop = () => undefined;
|
|
35
|
+
const EMPTY_CONTROLLER = {
|
|
36
|
+
categories: [],
|
|
37
|
+
actions: [],
|
|
38
|
+
variant: "panel",
|
|
39
|
+
surface: false,
|
|
40
|
+
isOpen: false,
|
|
41
|
+
open: noop,
|
|
42
|
+
close: noop,
|
|
43
|
+
setOpen: noop,
|
|
44
|
+
};
|
|
45
|
+
/** True when the game has any setting or game-action to show — gate your own settings entry on it. */
|
|
46
|
+
export function useHasSettings() {
|
|
47
|
+
const s = useSettings();
|
|
48
|
+
return s.categories.length > 0 || s.actions.length > 0;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Inline settings entry — drop it anywhere in your game's menu or HUD; it opens
|
|
52
|
+
* the themed settings menu. Headless: pass `className` for placement/skin and
|
|
53
|
+
* `children` to replace the default gear glyph. Renders nothing when the game
|
|
54
|
+
* has no settings to show, so it never leaves a dead button behind.
|
|
55
|
+
*/
|
|
56
|
+
export function SettingsTrigger({ className, children, label = "Settings", }) {
|
|
57
|
+
const settings = useSettings();
|
|
58
|
+
if (settings.categories.length === 0 && settings.actions.length === 0)
|
|
59
|
+
return null;
|
|
60
|
+
return (_jsx("button", { type: "button", "aria-label": label, onClick: settings.open, className: className, children: children ?? (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", width: "1em", height: "1em", "aria-hidden": true, children: [_jsx("circle", { cx: "12", cy: "12", r: "3" }), _jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" })] })) }));
|
|
61
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SkillCheckConfig, SkillCheckResult } from "@jgengine/core/interaction/skillCheck";
|
|
2
|
+
import type { QteStep } from "@jgengine/core/interaction/qte";
|
|
3
|
+
export declare function paintSkillCheckDom(root: HTMLElement, zone: HTMLElement, marker: HTMLElement, config: SkillCheckConfig, result: SkillCheckResult): void;
|
|
4
|
+
export declare function paintQteStepDom(elements: ReadonlyMap<string, HTMLElement>, steps: readonly QteStep[], elapsed: number, activeId: string | null, stepClassName?: string, activeClassName?: string, doneClassName?: string): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function paintSkillCheckDom(root, zone, marker, config, result) {
|
|
2
|
+
const zoneLeft = (result.zone.start / config.trackWidth) * 100;
|
|
3
|
+
const zoneWidth = ((result.zone.end - result.zone.start) / config.trackWidth) * 100;
|
|
4
|
+
const markerLeft = (result.markerPosition / config.trackWidth) * 100;
|
|
5
|
+
zone.style.left = `${zoneLeft}%`;
|
|
6
|
+
zone.style.width = `${zoneWidth}%`;
|
|
7
|
+
marker.style.left = `${markerLeft}%`;
|
|
8
|
+
root.dataset.inZone = result.success ? "true" : "false";
|
|
9
|
+
root.dataset.timedOut = result.timedOut ? "true" : "false";
|
|
10
|
+
}
|
|
11
|
+
export function paintQteStepDom(elements, steps, elapsed, activeId, stepClassName, activeClassName, doneClassName) {
|
|
12
|
+
for (const step of steps) {
|
|
13
|
+
const el = elements.get(step.id);
|
|
14
|
+
if (el === undefined)
|
|
15
|
+
continue;
|
|
16
|
+
const isActive = activeId === step.id;
|
|
17
|
+
const isDone = elapsed > step.windowEnd;
|
|
18
|
+
const classes = [stepClassName, isActive ? activeClassName : isDone ? doneClassName : undefined]
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.join(" ");
|
|
21
|
+
if (classes.length > 0)
|
|
22
|
+
el.className = classes;
|
|
23
|
+
else
|
|
24
|
+
el.removeAttribute("class");
|
|
25
|
+
el.dataset.active = isActive ? "true" : "false";
|
|
26
|
+
el.dataset.done = isDone ? "true" : "false";
|
|
27
|
+
}
|
|
28
|
+
}
|