@jgengine/react 0.7.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 +63 -0
- package/dist/chat.d.ts +52 -0
- package/dist/chat.js +68 -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 +14 -0
- package/dist/display.js +45 -0
- 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/gameIcons.d.ts +12 -0
- package/dist/gameIcons.js +282 -0
- package/dist/hooks.d.ts +59 -2
- package/dist/hooks.js +170 -12
- 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/identity.d.ts +65 -0
- package/dist/identity.js +94 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -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/dist/social.d.ts +108 -0
- package/dist/social.js +119 -0
- package/dist/voice.d.ts +58 -0
- package/dist/voice.js +130 -0
- package/llms.txt +1633 -0
- package/package.json +11 -6
package/dist/identity.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useMemo } from "react";
|
|
3
|
+
import { resolveGuestSession, sessionPlayer, } from "@jgengine/core/multiplayer/identity";
|
|
4
|
+
export function clerkIdentity(state, options) {
|
|
5
|
+
if (!state.isLoaded)
|
|
6
|
+
return { session: null, isLoading: true };
|
|
7
|
+
const user = state.user;
|
|
8
|
+
if (state.isSignedIn !== true || user === null || user === undefined) {
|
|
9
|
+
return { session: null, isLoading: false };
|
|
10
|
+
}
|
|
11
|
+
const displayName = user.fullName ?? user.username ?? undefined;
|
|
12
|
+
const session = { userId: user.id };
|
|
13
|
+
if (displayName !== undefined)
|
|
14
|
+
session.displayName = displayName;
|
|
15
|
+
if (user.imageUrl !== null && user.imageUrl !== undefined)
|
|
16
|
+
session.avatarUrl = user.imageUrl;
|
|
17
|
+
const email = user.primaryEmailAddress?.emailAddress;
|
|
18
|
+
if (email !== undefined)
|
|
19
|
+
session.email = email;
|
|
20
|
+
if (user.createdAt !== null && user.createdAt !== undefined) {
|
|
21
|
+
session.isNew =
|
|
22
|
+
user.lastSignInAt === null ||
|
|
23
|
+
user.lastSignInAt === undefined ||
|
|
24
|
+
user.lastSignInAt.getTime() <= user.createdAt.getTime();
|
|
25
|
+
}
|
|
26
|
+
const source = { session, isLoading: false };
|
|
27
|
+
if (options?.signOut !== undefined)
|
|
28
|
+
source.signOut = options.signOut;
|
|
29
|
+
return source;
|
|
30
|
+
}
|
|
31
|
+
export function betterAuthIdentity(state, options) {
|
|
32
|
+
if (state.isPending)
|
|
33
|
+
return { session: null, isLoading: true };
|
|
34
|
+
const user = state.data?.user;
|
|
35
|
+
if (user === undefined)
|
|
36
|
+
return { session: null, isLoading: false };
|
|
37
|
+
const session = { userId: user.id };
|
|
38
|
+
if (user.name !== null && user.name !== undefined)
|
|
39
|
+
session.displayName = user.name;
|
|
40
|
+
if (user.image !== null && user.image !== undefined)
|
|
41
|
+
session.avatarUrl = user.image;
|
|
42
|
+
if (user.email !== null && user.email !== undefined)
|
|
43
|
+
session.email = user.email;
|
|
44
|
+
const source = { session, isLoading: false };
|
|
45
|
+
if (options?.signOut !== undefined)
|
|
46
|
+
source.signOut = options.signOut;
|
|
47
|
+
return source;
|
|
48
|
+
}
|
|
49
|
+
export function guestIdentity(seed) {
|
|
50
|
+
return { session: resolveGuestSession(seed), isLoading: false };
|
|
51
|
+
}
|
|
52
|
+
const IdentityReactContext = createContext(null);
|
|
53
|
+
export function GameIdentityProvider({ source, children, }) {
|
|
54
|
+
return _jsx(IdentityReactContext.Provider, { value: source, children: children });
|
|
55
|
+
}
|
|
56
|
+
export function useSession() {
|
|
57
|
+
const source = useContext(IdentityReactContext);
|
|
58
|
+
if (source === null)
|
|
59
|
+
throw new Error("useSession must be used within <GameIdentityProvider>");
|
|
60
|
+
return source;
|
|
61
|
+
}
|
|
62
|
+
export function useAuthedPlayer(options) {
|
|
63
|
+
const { session, isLoading } = useSession();
|
|
64
|
+
const guestSeed = options?.guestSeed;
|
|
65
|
+
return useMemo(() => {
|
|
66
|
+
if (session !== null)
|
|
67
|
+
return sessionPlayer(session);
|
|
68
|
+
if (isLoading || guestSeed === undefined)
|
|
69
|
+
return null;
|
|
70
|
+
return sessionPlayer(resolveGuestSession(guestSeed));
|
|
71
|
+
}, [session, isLoading, guestSeed]);
|
|
72
|
+
}
|
|
73
|
+
export function RequireSession({ fallback, loading, children, }) {
|
|
74
|
+
const { session, isLoading } = useSession();
|
|
75
|
+
if (isLoading)
|
|
76
|
+
return _jsx(_Fragment, { children: loading ?? null });
|
|
77
|
+
if (session === null)
|
|
78
|
+
return _jsx(_Fragment, { children: fallback ?? null });
|
|
79
|
+
return _jsx(_Fragment, { children: children });
|
|
80
|
+
}
|
|
81
|
+
export function UserBadge({ className, avatarClassName, nameClassName, renderBadge, }) {
|
|
82
|
+
const { session } = useSession();
|
|
83
|
+
if (session === null)
|
|
84
|
+
return null;
|
|
85
|
+
if (renderBadge !== undefined)
|
|
86
|
+
return _jsx(_Fragment, { children: renderBadge(session) });
|
|
87
|
+
return (_jsxs("span", { className: className, "data-user": session.userId, children: [session.avatarUrl !== undefined && (_jsx("img", { className: avatarClassName, src: session.avatarUrl, alt: "", "data-avatar": true })), _jsx("span", { className: nameClassName, "data-display-name": true, children: session.displayName ?? session.userId })] }));
|
|
88
|
+
}
|
|
89
|
+
export function SignOutButton({ className, children, }) {
|
|
90
|
+
const { session, signOut } = useSession();
|
|
91
|
+
if (session === null || signOut === undefined)
|
|
92
|
+
return null;
|
|
93
|
+
return (_jsx("button", { type: "button", className: className, "data-sign-out": true, onClick: () => signOut(), children: children ?? "Sign out" }));
|
|
94
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
export * from "./provider.js";
|
|
2
|
+
export * from "./identity.js";
|
|
3
|
+
export * from "./chat.js";
|
|
4
|
+
export * from "./chatBubbles.js";
|
|
5
|
+
export * from "./voice.js";
|
|
6
|
+
export * from "./social.js";
|
|
2
7
|
export * from "./hooks.js";
|
|
8
|
+
export * from "./settings.js";
|
|
3
9
|
export * from "./components.js";
|
|
4
10
|
export * from "./engineStore.js";
|
|
5
11
|
export * from "./dragLayer.js";
|
|
12
|
+
export * from "./hudLayout.js";
|
|
13
|
+
export * from "./hudViewport.js";
|
|
14
|
+
export * from "./display.js";
|
|
6
15
|
export * from "./map.js";
|
|
16
|
+
export * from "./preview.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
export * from "./provider.js";
|
|
2
|
+
export * from "./identity.js";
|
|
3
|
+
export * from "./chat.js";
|
|
4
|
+
export * from "./chatBubbles.js";
|
|
5
|
+
export * from "./voice.js";
|
|
6
|
+
export * from "./social.js";
|
|
2
7
|
export * from "./hooks.js";
|
|
8
|
+
export * from "./settings.js";
|
|
3
9
|
export * from "./components.js";
|
|
4
10
|
export * from "./engineStore.js";
|
|
5
11
|
export * from "./dragLayer.js";
|
|
12
|
+
export * from "./hudLayout.js";
|
|
13
|
+
export * from "./hudViewport.js";
|
|
14
|
+
export * from "./display.js";
|
|
6
15
|
export * from "./map.js";
|
|
16
|
+
export * from "./preview.js";
|
package/dist/liveBind.d.ts
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
import { type CSSProperties } from "react";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* `apply(value, element)` so HUDs bound to live engine state (speed, pose)
|
|
6
|
-
* never re-render and never lag. `get`/`apply` may change without restarting.
|
|
7
|
-
*/
|
|
2
|
+
type FrameSubscriber = () => void;
|
|
3
|
+
export declare function subscribeFrameBind(subscriber: FrameSubscriber): () => void;
|
|
4
|
+
export declare function frameBindSubscriberCount(): number;
|
|
8
5
|
export declare function useFrameBind<T, E extends Element = Element>(ref: {
|
|
9
6
|
current: E | null;
|
|
10
7
|
}, get: () => T, apply: (value: T, element: E) => void): void;
|
|
11
|
-
/**
|
|
12
|
-
* A `<span>` whose text tracks a live value every frame (the 90% case of
|
|
13
|
-
* useFrameBind). `<LiveText get={() => groundSpeed(car) * KMH} format={Math.round} />`.
|
|
14
|
-
*/
|
|
15
8
|
export declare function LiveText({ get, format, className, style, }: {
|
|
16
9
|
get: () => number | string;
|
|
17
10
|
format?: (value: number | string) => string;
|
|
18
11
|
className?: string;
|
|
19
12
|
style?: CSSProperties;
|
|
20
13
|
}): import("react").JSX.Element;
|
|
14
|
+
export {};
|
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
|
+
}
|