@jgengine/react 0.13.0 → 0.15.0-next.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 +529 -1
- package/dist/accessibility.d.ts +39 -0
- package/dist/accessibility.js +61 -0
- package/dist/achievements.d.ts +50 -0
- package/dist/achievements.js +94 -0
- package/dist/cameraShake.d.ts +52 -0
- package/dist/cameraShake.js +53 -0
- package/dist/coachMarks.d.ts +72 -0
- package/dist/coachMarks.js +136 -0
- package/dist/codex.d.ts +25 -0
- package/dist/codex.js +62 -0
- package/dist/comboMeter.d.ts +63 -0
- package/dist/comboMeter.js +93 -0
- package/dist/components.js +4 -3
- package/dist/creditsScreen.d.ts +19 -0
- package/dist/creditsScreen.js +31 -0
- package/dist/cutscene.d.ts +66 -0
- package/dist/cutscene.js +132 -0
- package/dist/damageDirection.d.ts +51 -0
- package/dist/damageDirection.js +103 -0
- package/dist/dialogueView.d.ts +69 -0
- package/dist/dialogueView.js +111 -0
- package/dist/entityFrames.d.ts +114 -0
- package/dist/entityFrames.js +55 -0
- package/dist/fastTravel.d.ts +36 -0
- package/dist/fastTravel.js +103 -0
- package/dist/floatingText.d.ts +84 -0
- package/dist/floatingText.js +61 -0
- package/dist/gameViewport.js +8 -12
- package/dist/hooks.d.ts +42 -0
- package/dist/hooks.js +85 -0
- package/dist/hudLayout.js +138 -9
- package/dist/hudTheme.d.ts +104 -7
- package/dist/hudTheme.js +300 -2
- package/dist/i18n.d.ts +29 -0
- package/dist/i18n.js +44 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +34 -0
- package/dist/interactionPrompt.d.ts +80 -0
- package/dist/interactionPrompt.js +112 -0
- package/dist/keybindingMenu.d.ts +59 -0
- package/dist/keybindingMenu.js +146 -0
- package/dist/killFeed.d.ts +63 -0
- package/dist/killFeed.js +88 -0
- package/dist/map.d.ts +123 -2
- package/dist/map.js +239 -36
- package/dist/menuRouter.d.ts +51 -0
- package/dist/menuRouter.js +49 -0
- package/dist/modals.d.ts +131 -0
- package/dist/modals.js +168 -0
- package/dist/notifications.d.ts +39 -0
- package/dist/notifications.js +111 -0
- package/dist/objectiveBanner.d.ts +62 -0
- package/dist/objectiveBanner.js +129 -0
- package/dist/panels.d.ts +10 -2
- package/dist/panels.js +9 -3
- package/dist/photoMode.d.ts +21 -0
- package/dist/photoMode.js +39 -0
- package/dist/previewFixtures.d.ts +30 -0
- package/dist/previewFixtures.js +46 -0
- package/dist/provider.d.ts +15 -0
- package/dist/provider.js +17 -1
- package/dist/questTracker.d.ts +24 -0
- package/dist/questTracker.js +31 -0
- package/dist/quickMenu.d.ts +39 -0
- package/dist/quickMenu.js +79 -0
- package/dist/radialMenu.d.ts +50 -0
- package/dist/radialMenu.js +69 -0
- package/dist/saveSlots.d.ts +81 -0
- package/dist/saveSlots.js +138 -0
- package/dist/scoreboard.d.ts +67 -0
- package/dist/scoreboard.js +71 -0
- package/dist/settings.d.ts +9 -3
- package/dist/settings.js +10 -4
- package/dist/shopGrid.d.ts +70 -0
- package/dist/shopGrid.js +129 -0
- package/dist/statusEffectBar.d.ts +52 -0
- package/dist/statusEffectBar.js +57 -0
- package/dist/streetGeometryPreview.d.ts +7 -0
- package/dist/streetGeometryPreview.js +119 -0
- package/dist/talentTree.d.ts +60 -0
- package/dist/talentTree.js +217 -0
- package/dist/timerReadout.d.ts +90 -0
- package/dist/timerReadout.js +111 -0
- package/dist/useDebouncedCommit.d.ts +54 -0
- package/dist/useDebouncedCommit.js +81 -0
- package/dist/waveHud.d.ts +49 -0
- package/dist/waveHud.js +53 -0
- package/dist/waypointMarkers.d.ts +80 -0
- package/dist/waypointMarkers.js +116 -0
- package/package.json +3 -3
- package/skills/ce-handoff/SKILL.md +149 -0
- package/skills/jgengine/SKILL.md +7 -3
- package/skills/jgengine/api.md +17 -7
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine/recipes/minimal-game.md +7 -4
- package/skills/jgengine-ui/SKILL.md +8 -3
- package/skills/jgengine-ui/api.md +732 -23
- package/skills/jgengine-ui/capabilities.md +371 -2
- package/skills/jgengine-ui/recipes/overhead-entity-frames.md +136 -0
- package/skills/jgengine-ui/reference.md +7 -4
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useReducer } from "react";
|
|
3
|
+
import { StaminaBar } from "./bars.js";
|
|
4
|
+
import { KeyHint, Keycap } from "./keyHint.js";
|
|
5
|
+
/**
|
|
6
|
+
* Subscribe to a {@link PromptRegistry} and re-`resolve` it whenever the player
|
|
7
|
+
* moves, returning the prompt to draw right now — or `null` when the player is
|
|
8
|
+
* out of range of every interactable. The registry notifies only on active-prompt
|
|
9
|
+
* *changes*, so this re-renders on transitions rather than every frame the hero
|
|
10
|
+
* moves. Feed it the player's `{ x, z }` position each frame; identical positions
|
|
11
|
+
* do no work.
|
|
12
|
+
*
|
|
13
|
+
* @capability use-interaction-prompt React hook binding a prompt registry to a moving player — resolves the nearest interactable prompt and returns the active one
|
|
14
|
+
*/
|
|
15
|
+
export function useInteractionPrompt(registry, playerPosition) {
|
|
16
|
+
const [, bump] = useReducer((n) => n + 1, 0);
|
|
17
|
+
useEffect(() => registry.subscribe(bump), [registry]);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
registry.resolve(playerPosition);
|
|
20
|
+
}, [registry, playerPosition.x, playerPosition.z]);
|
|
21
|
+
return registry.active();
|
|
22
|
+
}
|
|
23
|
+
function resolveTheme(theme) {
|
|
24
|
+
return {
|
|
25
|
+
accent: theme?.accent ?? "var(--jg-accent, #38bdf8)",
|
|
26
|
+
background: theme?.background ?? "rgba(15,20,28,0.86)",
|
|
27
|
+
border: theme?.border ?? "rgba(148,163,184,0.28)",
|
|
28
|
+
text: theme?.text ?? "#e2e8f0",
|
|
29
|
+
fontFamily: theme?.fontFamily ?? "ui-sans-serif, system-ui, sans-serif",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const ANCHOR_STYLE = {
|
|
33
|
+
"bottom-center": { bottom: "12%", left: "50%", transform: "translateX(-50%)" },
|
|
34
|
+
"top-center": { top: "12%", left: "50%", transform: "translateX(-50%)" },
|
|
35
|
+
center: { top: "50%", left: "50%", transform: "translate(-50%, -50%)" },
|
|
36
|
+
};
|
|
37
|
+
/** Bind to the registry either through the moving player, or passively on the active id. */
|
|
38
|
+
function useActive(registry, playerPosition) {
|
|
39
|
+
const [, bump] = useReducer((n) => n + 1, 0);
|
|
40
|
+
useEffect(() => registry.subscribe(bump), [registry]);
|
|
41
|
+
const px = playerPosition?.x;
|
|
42
|
+
const pz = playerPosition?.z;
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (px !== undefined && pz !== undefined)
|
|
45
|
+
registry.resolve({ x: px, z: pz });
|
|
46
|
+
}, [registry, px, pz]);
|
|
47
|
+
return registry.active();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A screen-anchored "Press E to …" interaction callout that renders the active
|
|
51
|
+
* prompt of a {@link PromptRegistry}. It subscribes to the registry and — when
|
|
52
|
+
* given `playerPosition` — re-resolves as the hero moves, showing the nearest
|
|
53
|
+
* in-range interactable and switching to a closer/higher-priority one as they
|
|
54
|
+
* cross radii. Each display kind renders as itself: a `keybind` shows a key cap
|
|
55
|
+
* plus its label ("Press [E] Open"), a `gauge` shows a hold bar filling with
|
|
56
|
+
* `gaugeProgress`, and a `label` shows plain text. Presentation only — the
|
|
57
|
+
* registry never branches on a prompt's meaning; the game colors each via
|
|
58
|
+
* `accentFor` and maps action ids to key glyphs via `keyFor`. HudTheme-skinnable
|
|
59
|
+
* through `--jg-accent` and {@link InteractionPromptTheme}.
|
|
60
|
+
*
|
|
61
|
+
* @capability interaction-prompt-host screen-anchored "press E to…" callout rendering a prompt registry's active interactable — keybind cap+label, gauge hold bar, or plain label, theme- and per-prompt-accent skinnable
|
|
62
|
+
*/
|
|
63
|
+
export function InteractionPrompt({ registry, playerPosition, theme, accentFor, keyFor, gaugeProgress, anchor = "bottom-center", zIndex = 40, className, style, }) {
|
|
64
|
+
const prompt = useActive(registry, playerPosition);
|
|
65
|
+
if (prompt === null)
|
|
66
|
+
return null;
|
|
67
|
+
const t = resolveTheme(theme);
|
|
68
|
+
const accent = accentFor?.(prompt) ?? t.accent;
|
|
69
|
+
const display = prompt.prompt.display;
|
|
70
|
+
let body;
|
|
71
|
+
if (display.kind === "keybind") {
|
|
72
|
+
const glyph = keyFor?.(display.actionId) ?? "E";
|
|
73
|
+
body = (_jsxs(KeyHint, { style: { display: "inline-flex", alignItems: "center", gap: 10 }, children: [_jsx("span", { style: { fontSize: 13, fontWeight: 600, letterSpacing: 0.4, opacity: 0.85 }, children: "Press" }), _jsx(Keycap, { style: {
|
|
74
|
+
display: "inline-flex",
|
|
75
|
+
minWidth: 26,
|
|
76
|
+
justifyContent: "center",
|
|
77
|
+
padding: "3px 8px",
|
|
78
|
+
borderRadius: 6,
|
|
79
|
+
border: `1px solid ${accent}`,
|
|
80
|
+
background: "rgba(0,0,0,0.35)",
|
|
81
|
+
color: accent,
|
|
82
|
+
fontWeight: 800,
|
|
83
|
+
fontSize: 14,
|
|
84
|
+
boxShadow: `0 0 10px -2px ${accent}`,
|
|
85
|
+
}, children: glyph }), display.label !== undefined ? (_jsx("span", { style: { fontSize: 15, fontWeight: 700 }, children: display.label })) : null] }));
|
|
86
|
+
}
|
|
87
|
+
else if (display.kind === "gauge") {
|
|
88
|
+
const progress = Math.max(0, Math.min(1, gaugeProgress?.(display.gaugeId) ?? 0));
|
|
89
|
+
body = (_jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 10 }, children: [_jsx("span", { style: { fontSize: 13, fontWeight: 600, letterSpacing: 0.4, opacity: 0.85 }, children: "Hold" }), _jsx(StaminaBar, { value: progress * 100, showValue: false, width: 140, fill: accent })] }));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
body = _jsx("span", { style: { fontSize: 15, fontWeight: 700 }, children: display.text });
|
|
93
|
+
}
|
|
94
|
+
return (_jsx("div", { className: className, "data-interaction-prompt": prompt.id, "data-prompt-kind": display.kind, style: {
|
|
95
|
+
position: "absolute",
|
|
96
|
+
...ANCHOR_STYLE[anchor],
|
|
97
|
+
display: "flex",
|
|
98
|
+
alignItems: "center",
|
|
99
|
+
pointerEvents: "none",
|
|
100
|
+
padding: "9px 16px",
|
|
101
|
+
borderRadius: 12,
|
|
102
|
+
border: `1px solid ${t.border}`,
|
|
103
|
+
background: t.background,
|
|
104
|
+
color: t.text,
|
|
105
|
+
fontFamily: t.fontFamily,
|
|
106
|
+
boxShadow: `0 8px 24px rgba(0,0,0,0.4), 0 0 0 1px ${accent}22`,
|
|
107
|
+
backdropFilter: "blur(4px)",
|
|
108
|
+
whiteSpace: "nowrap",
|
|
109
|
+
zIndex,
|
|
110
|
+
...style,
|
|
111
|
+
}, children: body }));
|
|
112
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import type { RebindSession } from "@jgengine/core/input/rebindSession";
|
|
3
|
+
/**
|
|
4
|
+
* Subscribe to a rebind session and re-render on every change (capture, reset,
|
|
5
|
+
* arm/disarm, restore). Returns the session unchanged for convenient inline use.
|
|
6
|
+
*
|
|
7
|
+
* @capability use-rebind-session React hook that re-renders a component whenever a rebind session changes
|
|
8
|
+
*/
|
|
9
|
+
export declare function useRebindSession(session: RebindSession): RebindSession;
|
|
10
|
+
/** Reskin tokens for {@link KeybindingMenu}. All fall back to HudTheme `--jg-*` vars. */
|
|
11
|
+
export interface KeybindingMenuTheme {
|
|
12
|
+
/** Panel background. Default reads `--jg-frame-bg`. */
|
|
13
|
+
bg?: string;
|
|
14
|
+
/** Panel border. Default reads `--jg-frame-border`. */
|
|
15
|
+
border?: string;
|
|
16
|
+
/** Corner radius. Default reads `--jg-frame-radius`. */
|
|
17
|
+
radius?: string;
|
|
18
|
+
/** Row label text color. */
|
|
19
|
+
text?: string;
|
|
20
|
+
/** Muted text (hints, secondary). */
|
|
21
|
+
muted?: string;
|
|
22
|
+
/** Accent for the armed row + focus glow. Default reads `--jg-accent`. */
|
|
23
|
+
accent?: string;
|
|
24
|
+
/** Conflict highlight color. */
|
|
25
|
+
conflict?: string;
|
|
26
|
+
/** Font family. */
|
|
27
|
+
fontFamily?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Props for {@link KeybindingMenu}. */
|
|
30
|
+
export interface KeybindingMenuProps {
|
|
31
|
+
/** The rebind session to edit. */
|
|
32
|
+
session: RebindSession;
|
|
33
|
+
/** Reskin tokens (fall back to HudTheme). */
|
|
34
|
+
theme?: KeybindingMenuTheme;
|
|
35
|
+
/** Heading shown above the list. Default `"Controls"`. */
|
|
36
|
+
title?: ReactNode;
|
|
37
|
+
/** Label for the reset-all button. Default `"Reset All"`. */
|
|
38
|
+
resetAllLabel?: string;
|
|
39
|
+
/** Prompt shown in a row that is armed for capture. Default `"Press a key…"`. */
|
|
40
|
+
capturePrompt?: string;
|
|
41
|
+
/** Called after a successful rebind or reset — e.g. persist `session.overrides()`. */
|
|
42
|
+
onChange?: (session: RebindSession) => void;
|
|
43
|
+
className?: string;
|
|
44
|
+
style?: CSSProperties;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A drop-in controls-settings surface for a {@link RebindSession}: one row per
|
|
48
|
+
* action showing its label and current key glyph. Clicking a row arms capture
|
|
49
|
+
* ("Press a key…") and a window `keydown` listener feeds the pressed
|
|
50
|
+
* `event.code` to `session.capture`; rows that collide with another action show
|
|
51
|
+
* a conflict badge; each row has a Reset button and the panel has a Reset-all
|
|
52
|
+
* button. Presentation only — all conflict/capture/reset logic lives in the core
|
|
53
|
+
* session, and action ids/labels are the game's free strings. Reskin via
|
|
54
|
+
* {@link KeybindingMenuTheme} or ambient HudTheme tokens. A game owns its overall
|
|
55
|
+
* settings layout; this is the reusable controls block it drops in.
|
|
56
|
+
*
|
|
57
|
+
* @capability keybinding-menu drop-in controls-settings surface over a rebind session — click-to-capture rows with key glyphs, conflict badges, per-row and reset-all, HudTheme-skinnable
|
|
58
|
+
*/
|
|
59
|
+
export declare function KeybindingMenu({ session, theme, title, resetAllLabel, capturePrompt, onChange, className, style, }: KeybindingMenuProps): ReactNode;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useReducer } from "react";
|
|
3
|
+
import { Keycap } from "./keyHint.js";
|
|
4
|
+
/**
|
|
5
|
+
* Subscribe to a rebind session and re-render on every change (capture, reset,
|
|
6
|
+
* arm/disarm, restore). Returns the session unchanged for convenient inline use.
|
|
7
|
+
*
|
|
8
|
+
* @capability use-rebind-session React hook that re-renders a component whenever a rebind session changes
|
|
9
|
+
*/
|
|
10
|
+
export function useRebindSession(session) {
|
|
11
|
+
const [, bump] = useReducer((n) => n + 1, 0);
|
|
12
|
+
useEffect(() => session.subscribe(bump), [session]);
|
|
13
|
+
return session;
|
|
14
|
+
}
|
|
15
|
+
function resolveTheme(theme) {
|
|
16
|
+
return {
|
|
17
|
+
bg: theme?.bg ?? "var(--jg-frame-bg, linear-gradient(180deg, rgba(20,24,32,0.92), rgba(10,12,16,0.95)))",
|
|
18
|
+
border: theme?.border ?? "var(--jg-frame-border, 1px solid rgba(255,255,255,0.12))",
|
|
19
|
+
radius: theme?.radius ?? "var(--jg-frame-radius, 12px)",
|
|
20
|
+
text: theme?.text ?? "#e8edf5",
|
|
21
|
+
muted: theme?.muted ?? "rgba(148,163,184,0.85)",
|
|
22
|
+
accent: theme?.accent ?? "var(--jg-accent, #38bdf8)",
|
|
23
|
+
conflict: theme?.conflict ?? "#f87171",
|
|
24
|
+
fontFamily: theme?.fontFamily ?? "ui-sans-serif, system-ui, sans-serif",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A drop-in controls-settings surface for a {@link RebindSession}: one row per
|
|
29
|
+
* action showing its label and current key glyph. Clicking a row arms capture
|
|
30
|
+
* ("Press a key…") and a window `keydown` listener feeds the pressed
|
|
31
|
+
* `event.code` to `session.capture`; rows that collide with another action show
|
|
32
|
+
* a conflict badge; each row has a Reset button and the panel has a Reset-all
|
|
33
|
+
* button. Presentation only — all conflict/capture/reset logic lives in the core
|
|
34
|
+
* session, and action ids/labels are the game's free strings. Reskin via
|
|
35
|
+
* {@link KeybindingMenuTheme} or ambient HudTheme tokens. A game owns its overall
|
|
36
|
+
* settings layout; this is the reusable controls block it drops in.
|
|
37
|
+
*
|
|
38
|
+
* @capability keybinding-menu drop-in controls-settings surface over a rebind session — click-to-capture rows with key glyphs, conflict badges, per-row and reset-all, HudTheme-skinnable
|
|
39
|
+
*/
|
|
40
|
+
export function KeybindingMenu({ session, theme, title = "Controls", resetAllLabel = "Reset All", capturePrompt = "Press a key…", onChange, className, style, }) {
|
|
41
|
+
useRebindSession(session);
|
|
42
|
+
const t = resolveTheme(theme);
|
|
43
|
+
const rows = session.rows();
|
|
44
|
+
const capturing = session.capturingActionId();
|
|
45
|
+
// While armed, the next keydown anywhere reassigns the row (Escape cancels).
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (capturing === null)
|
|
48
|
+
return;
|
|
49
|
+
const onKeyDown = (event) => {
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
event.stopPropagation();
|
|
52
|
+
if (event.code === "Escape") {
|
|
53
|
+
session.cancelCapture();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
session.capture(event.code);
|
|
57
|
+
onChange?.(session);
|
|
58
|
+
};
|
|
59
|
+
window.addEventListener("keydown", onKeyDown, { capture: true });
|
|
60
|
+
return () => window.removeEventListener("keydown", onKeyDown, { capture: true });
|
|
61
|
+
}, [capturing, session, onChange]);
|
|
62
|
+
const anyConflict = session.hasConflicts();
|
|
63
|
+
return (_jsxs("div", { "data-keybinding-menu": "", className: className, style: {
|
|
64
|
+
pointerEvents: "auto",
|
|
65
|
+
display: "flex",
|
|
66
|
+
flexDirection: "column",
|
|
67
|
+
gap: 10,
|
|
68
|
+
width: "min(420px, 92vw)",
|
|
69
|
+
padding: "16px 18px",
|
|
70
|
+
background: t.bg,
|
|
71
|
+
border: t.border,
|
|
72
|
+
borderRadius: t.radius,
|
|
73
|
+
color: t.text,
|
|
74
|
+
fontFamily: t.fontFamily,
|
|
75
|
+
boxShadow: "0 12px 40px rgba(0,0,0,0.5)",
|
|
76
|
+
...style,
|
|
77
|
+
}, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }, children: [_jsx("div", { style: { fontSize: 15, fontWeight: 800, letterSpacing: "0.08em", textTransform: "uppercase" }, children: title }), _jsx("button", { type: "button", "data-keybinding-reset-all": "", onClick: () => {
|
|
78
|
+
session.resetAll();
|
|
79
|
+
onChange?.(session);
|
|
80
|
+
}, style: buttonStyle(t.muted, t.border), children: resetAllLabel })] }), anyConflict ? (_jsx("div", { style: { fontSize: 12, color: t.conflict, fontWeight: 600 }, children: "\u26A0 Some keys are bound to more than one action." })) : null, _jsx("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: rows.map((row) => (_jsx(KeybindingRow, { row: row, armed: capturing === row.actionId, capturePrompt: capturePrompt, theme: t, onArm: () => session.beginCapture(row.actionId), onReset: () => {
|
|
81
|
+
session.reset(row.actionId);
|
|
82
|
+
onChange?.(session);
|
|
83
|
+
} }, row.actionId))) })] }));
|
|
84
|
+
}
|
|
85
|
+
function KeybindingRow({ row, armed, capturePrompt, theme, onArm, onReset, }) {
|
|
86
|
+
const conflicted = row.conflictWith.length > 0;
|
|
87
|
+
return (_jsxs("div", { "data-keybinding-row": row.actionId, "data-armed": armed ? "" : undefined, "data-conflict": conflicted ? "" : undefined, style: {
|
|
88
|
+
display: "flex",
|
|
89
|
+
alignItems: "center",
|
|
90
|
+
gap: 10,
|
|
91
|
+
padding: "8px 10px",
|
|
92
|
+
borderRadius: 8,
|
|
93
|
+
background: armed ? "rgba(56,189,248,0.14)" : "rgba(255,255,255,0.03)",
|
|
94
|
+
border: `1px solid ${armed ? theme.accent : conflicted ? theme.conflict : "transparent"}`,
|
|
95
|
+
boxShadow: armed ? `0 0 0 1px ${theme.accent}, 0 0 14px rgba(56,189,248,0.35)` : "none",
|
|
96
|
+
transition: "background 120ms, border-color 120ms",
|
|
97
|
+
}, children: [_jsxs("div", { style: { flex: 1, minWidth: 0, fontSize: 14, fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: [row.label, conflicted ? (_jsx("span", { "data-keybinding-conflict-badge": "", style: {
|
|
98
|
+
marginLeft: 8,
|
|
99
|
+
padding: "1px 7px",
|
|
100
|
+
borderRadius: 999,
|
|
101
|
+
fontSize: 10,
|
|
102
|
+
fontWeight: 800,
|
|
103
|
+
letterSpacing: "0.06em",
|
|
104
|
+
textTransform: "uppercase",
|
|
105
|
+
color: "#1a0808",
|
|
106
|
+
background: theme.conflict,
|
|
107
|
+
}, children: "Conflict" })) : null] }), _jsx("button", { type: "button", "data-keybinding-capture": "", onClick: onArm, style: {
|
|
108
|
+
minWidth: 96,
|
|
109
|
+
display: "flex",
|
|
110
|
+
justifyContent: "center",
|
|
111
|
+
cursor: "pointer",
|
|
112
|
+
background: "transparent",
|
|
113
|
+
border: "none",
|
|
114
|
+
padding: 0,
|
|
115
|
+
}, children: armed ? (_jsx("span", { style: { fontSize: 12, fontWeight: 700, color: theme.accent }, children: capturePrompt })) : (_jsx(Keycap, { style: {
|
|
116
|
+
minWidth: 34,
|
|
117
|
+
padding: "3px 9px",
|
|
118
|
+
borderRadius: 6,
|
|
119
|
+
textAlign: "center",
|
|
120
|
+
fontSize: 13,
|
|
121
|
+
fontWeight: 700,
|
|
122
|
+
color: conflicted ? theme.conflict : theme.text,
|
|
123
|
+
background: "rgba(0,0,0,0.4)",
|
|
124
|
+
border: `1px solid ${conflicted ? theme.conflict : "rgba(255,255,255,0.18)"}`,
|
|
125
|
+
boxShadow: "inset 0 -2px 0 rgba(0,0,0,0.4)",
|
|
126
|
+
}, children: row.bindingLabel || "—" })) }), _jsx("button", { type: "button", "data-keybinding-reset": "", onClick: onReset, disabled: row.isDefault, title: "Reset to default", style: {
|
|
127
|
+
...buttonStyle(theme.muted, theme.border),
|
|
128
|
+
opacity: row.isDefault ? 0.35 : 1,
|
|
129
|
+
cursor: row.isDefault ? "default" : "pointer",
|
|
130
|
+
padding: "4px 8px",
|
|
131
|
+
fontSize: 11,
|
|
132
|
+
}, children: "Reset" })] }));
|
|
133
|
+
}
|
|
134
|
+
function buttonStyle(color, border) {
|
|
135
|
+
return {
|
|
136
|
+
pointerEvents: "auto",
|
|
137
|
+
background: "rgba(255,255,255,0.05)",
|
|
138
|
+
border,
|
|
139
|
+
borderRadius: 8,
|
|
140
|
+
color,
|
|
141
|
+
fontSize: 12,
|
|
142
|
+
fontWeight: 600,
|
|
143
|
+
padding: "5px 10px",
|
|
144
|
+
cursor: "pointer",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import type { EventTicker, EventTickerView } from "@jgengine/core/game/eventTicker";
|
|
3
|
+
/**
|
|
4
|
+
* Subscribe to an event ticker and re-render it on an animation frame (unless the game
|
|
5
|
+
* already ticks it), returning the live entries newest-first with `fade` applied. Driving
|
|
6
|
+
* the frame loop is what makes entries visibly fade and drop as they age out.
|
|
7
|
+
*
|
|
8
|
+
* @capability use-event-ticker React hook binding an event-ticker — re-renders per frame so entries fade and drop live, returning the current newest-first views
|
|
9
|
+
*/
|
|
10
|
+
export declare function useEventTicker(ticker: EventTicker, animate?: boolean): EventTickerView[];
|
|
11
|
+
/** Per-`kind` styling for a kill-feed row — accent color and an optional icon. */
|
|
12
|
+
export interface KillFeedKindStyle {
|
|
13
|
+
/** Accent color for the row's icon and left rule. */
|
|
14
|
+
accent?: string;
|
|
15
|
+
/** Free-string {@link GameIcon} name to show for this kind (overridden by an entry's own `icon`). */
|
|
16
|
+
icon?: string;
|
|
17
|
+
/** Text color for the row. Defaults to the theme text color. */
|
|
18
|
+
color?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Reskin tokens for {@link KillFeed}. */
|
|
21
|
+
export interface KillFeedTheme {
|
|
22
|
+
/** Row background. Default reads `--jg-frame-bg`. */
|
|
23
|
+
rowBg?: string;
|
|
24
|
+
/** Row text color. Default near-white. */
|
|
25
|
+
text?: string;
|
|
26
|
+
/** Fallback accent when a kind has none. Default reads `--jg-accent`. */
|
|
27
|
+
accent?: string;
|
|
28
|
+
/** Font family. */
|
|
29
|
+
fontFamily?: string;
|
|
30
|
+
/** Row corner radius. Default reads `--jg-frame-radius`. */
|
|
31
|
+
radius?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Props for {@link KillFeed}. */
|
|
34
|
+
export interface KillFeedProps {
|
|
35
|
+
/** The ticker to render. */
|
|
36
|
+
ticker: EventTicker;
|
|
37
|
+
/**
|
|
38
|
+
* Per-`kind` accent + icon, keyed by the free-string `kind` a game pushes with ("kill",
|
|
39
|
+
* "assist", "info", …). The matching entry styles that row; the model never interprets `kind`.
|
|
40
|
+
*/
|
|
41
|
+
kindStyles?: Record<string, KillFeedKindStyle>;
|
|
42
|
+
/** Base reskin tokens. */
|
|
43
|
+
theme?: KillFeedTheme;
|
|
44
|
+
/** Icon size in px. Default `18`. */
|
|
45
|
+
iconSize?: number;
|
|
46
|
+
/** Drive re-render on an animation frame. Default `true`. Turn off if the game ticks it. */
|
|
47
|
+
animate?: boolean;
|
|
48
|
+
/** Extra `z-index` for the stack. Default `40`. */
|
|
49
|
+
zIndex?: number;
|
|
50
|
+
className?: string;
|
|
51
|
+
style?: CSSProperties;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A vertical event/kill-feed ticker: renders an {@link EventTicker}'s recent entries as a
|
|
55
|
+
* stack of rows, newest on top, each with its `fade` `0..1` mapped to opacity so entries dim
|
|
56
|
+
* and drop as they age out. Each row shows an optional per-kind (or per-entry) {@link GameIcon}
|
|
57
|
+
* and a per-kind accent color from `kindStyles`, styled with HudTheme tokens. Presentation
|
|
58
|
+
* only: all capping/timing lives in the core ticker, and the free-string `kind`/`text`/`icon`
|
|
59
|
+
* are never interpreted here — a game colors and icons each variant via `kindStyles`.
|
|
60
|
+
*
|
|
61
|
+
* @capability kill-feed vertical event/kill-feed ticker rendering a core event-ticker as a fading, newest-on-top stack of per-kind iconned, accent-colored rows over HudTheme tokens
|
|
62
|
+
*/
|
|
63
|
+
export declare function KillFeed({ ticker, kindStyles, theme, iconSize, animate, zIndex, className, style, }: KillFeedProps): ReactNode;
|
package/dist/killFeed.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useReducer, useRef } from "react";
|
|
3
|
+
import { GameIcon, isGameIconName } from "./gameIcons.js";
|
|
4
|
+
/**
|
|
5
|
+
* Subscribe to an event ticker and re-render it on an animation frame (unless the game
|
|
6
|
+
* already ticks it), returning the live entries newest-first with `fade` applied. Driving
|
|
7
|
+
* the frame loop is what makes entries visibly fade and drop as they age out.
|
|
8
|
+
*
|
|
9
|
+
* @capability use-event-ticker React hook binding an event-ticker — re-renders per frame so entries fade and drop live, returning the current newest-first views
|
|
10
|
+
*/
|
|
11
|
+
export function useEventTicker(ticker, animate = true) {
|
|
12
|
+
const [, bump] = useReducer((n) => n + 1, 0);
|
|
13
|
+
const rafRef = useRef(0);
|
|
14
|
+
useEffect(() => ticker.subscribe(bump), [ticker]);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!animate)
|
|
17
|
+
return;
|
|
18
|
+
let running = true;
|
|
19
|
+
const loop = () => {
|
|
20
|
+
if (!running)
|
|
21
|
+
return;
|
|
22
|
+
bump();
|
|
23
|
+
rafRef.current = requestAnimationFrame(loop);
|
|
24
|
+
};
|
|
25
|
+
rafRef.current = requestAnimationFrame(loop);
|
|
26
|
+
return () => {
|
|
27
|
+
running = false;
|
|
28
|
+
cancelAnimationFrame(rafRef.current);
|
|
29
|
+
};
|
|
30
|
+
}, [ticker, animate]);
|
|
31
|
+
return ticker.recent();
|
|
32
|
+
}
|
|
33
|
+
function resolveTheme(theme) {
|
|
34
|
+
return {
|
|
35
|
+
rowBg: theme?.rowBg ?? "var(--jg-frame-bg, rgba(12,16,22,0.82))",
|
|
36
|
+
text: theme?.text ?? "#f1f5f9",
|
|
37
|
+
accent: theme?.accent ?? "var(--jg-accent, #38bdf8)",
|
|
38
|
+
fontFamily: theme?.fontFamily ?? "ui-sans-serif, system-ui, sans-serif",
|
|
39
|
+
radius: theme?.radius ?? "var(--jg-frame-radius, 8px)",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A vertical event/kill-feed ticker: renders an {@link EventTicker}'s recent entries as a
|
|
44
|
+
* stack of rows, newest on top, each with its `fade` `0..1` mapped to opacity so entries dim
|
|
45
|
+
* and drop as they age out. Each row shows an optional per-kind (or per-entry) {@link GameIcon}
|
|
46
|
+
* and a per-kind accent color from `kindStyles`, styled with HudTheme tokens. Presentation
|
|
47
|
+
* only: all capping/timing lives in the core ticker, and the free-string `kind`/`text`/`icon`
|
|
48
|
+
* are never interpreted here — a game colors and icons each variant via `kindStyles`.
|
|
49
|
+
*
|
|
50
|
+
* @capability kill-feed vertical event/kill-feed ticker rendering a core event-ticker as a fading, newest-on-top stack of per-kind iconned, accent-colored rows over HudTheme tokens
|
|
51
|
+
*/
|
|
52
|
+
export function KillFeed({ ticker, kindStyles, theme, iconSize = 18, animate = true, zIndex = 40, className, style, }) {
|
|
53
|
+
const entries = useEventTicker(ticker, animate);
|
|
54
|
+
const t = resolveTheme(theme);
|
|
55
|
+
return (_jsx("div", { className: className, "data-kill-feed": "", style: {
|
|
56
|
+
display: "flex",
|
|
57
|
+
flexDirection: "column",
|
|
58
|
+
alignItems: "stretch",
|
|
59
|
+
gap: 6,
|
|
60
|
+
pointerEvents: "none",
|
|
61
|
+
fontFamily: t.fontFamily,
|
|
62
|
+
zIndex,
|
|
63
|
+
...style,
|
|
64
|
+
}, children: entries.map((entry) => {
|
|
65
|
+
const kindStyle = kindStyles?.[entry.kind];
|
|
66
|
+
const accent = kindStyle?.accent ?? t.accent;
|
|
67
|
+
const iconName = entry.icon ?? kindStyle?.icon;
|
|
68
|
+
const opacity = 1 - entry.fade * 0.85;
|
|
69
|
+
return (_jsxs("div", { "data-kill-feed-row": entry.id, "data-kind": entry.kind, style: {
|
|
70
|
+
display: "flex",
|
|
71
|
+
alignItems: "center",
|
|
72
|
+
gap: 8,
|
|
73
|
+
padding: "5px 10px",
|
|
74
|
+
background: t.rowBg,
|
|
75
|
+
borderLeft: `3px solid ${accent}`,
|
|
76
|
+
borderRadius: t.radius,
|
|
77
|
+
color: kindStyle?.color ?? t.text,
|
|
78
|
+
fontSize: 13,
|
|
79
|
+
fontWeight: 600,
|
|
80
|
+
lineHeight: 1.2,
|
|
81
|
+
whiteSpace: "nowrap",
|
|
82
|
+
opacity,
|
|
83
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.4)",
|
|
84
|
+
transform: `translateX(${entry.fade * 8}px)`,
|
|
85
|
+
willChange: "opacity, transform",
|
|
86
|
+
}, children: [iconName !== undefined && isGameIconName(iconName) ? (_jsx("span", { style: { color: accent, display: "flex", flexShrink: 0 }, children: _jsx(GameIcon, { name: iconName, size: iconSize }) })) : null, _jsx("span", { "data-kill-feed-text": "", children: entry.text })] }, entry.id));
|
|
87
|
+
}) }));
|
|
88
|
+
}
|
package/dist/map.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type CSSProperties, type ReactNode } from "react";
|
|
|
2
2
|
import type { FogField } from "@jgengine/core/world/fog";
|
|
3
3
|
import { type MapMarker, type MarkerCollection, type MarkerKindStyle, type MarkerSource, type MarkerSet, type MarkerView } from "@jgengine/core/world/markers";
|
|
4
4
|
import { type MinimapView, type WorldXZ } from "@jgengine/core/world/minimap";
|
|
5
|
-
import { type MapCellStates, type MapRoute, type MapZone } from "@jgengine/core/world/mapLayers";
|
|
5
|
+
import { type MapCellStates, type MapLayerTone, type MapRoute, type MapZone } from "@jgengine/core/world/mapLayers";
|
|
6
6
|
/** Subscribe to a native marker set or external marker source, or read a static marker array. */
|
|
7
7
|
export declare function useMarkers<TMeta = unknown>(markers: MarkerSet<TMeta>): readonly MapMarker<TMeta>[];
|
|
8
8
|
export declare function useMarkers<TMarker extends MarkerView>(markers: readonly TMarker[] | MarkerSource<TMarker>): readonly TMarker[];
|
|
@@ -210,9 +210,130 @@ export interface WorldMapProps {
|
|
|
210
210
|
title?: string;
|
|
211
211
|
onClose?: () => void;
|
|
212
212
|
}
|
|
213
|
+
/** Pan/zoom transform applied to a {@link WorldMapSurface}'s content group. */
|
|
214
|
+
export interface MapViewport {
|
|
215
|
+
/** Uniform zoom factor; 1 = fit. */
|
|
216
|
+
scale: number;
|
|
217
|
+
/** Content-group translation in canvas px. */
|
|
218
|
+
tx: number;
|
|
219
|
+
ty: number;
|
|
220
|
+
}
|
|
221
|
+
/** Props for {@link WorldMapSurface}. */
|
|
222
|
+
export interface WorldMapSurfaceProps {
|
|
223
|
+
markers: MarkerCollection;
|
|
224
|
+
bounds: MapBounds;
|
|
225
|
+
player?: WorldXZ;
|
|
226
|
+
facingYaw?: number;
|
|
227
|
+
fog?: FogField;
|
|
228
|
+
background?: string;
|
|
229
|
+
/** Content layout width in px (world bounds map into this). */
|
|
230
|
+
width?: number;
|
|
231
|
+
/** Content layout height; defaults to the bounds aspect ratio. */
|
|
232
|
+
height?: number;
|
|
233
|
+
kindStyles?: Record<string, MarkerKindStyle>;
|
|
234
|
+
routes?: readonly MapRoute[];
|
|
235
|
+
zones?: readonly MapZone[];
|
|
236
|
+
cellStates?: readonly MapCellStates[];
|
|
237
|
+
onWorldClick?: (world: WorldXZ) => void;
|
|
238
|
+
/** Outer `<svg>` pixel size; defaults to the content `width`/`height` (no viewport). */
|
|
239
|
+
canvasWidth?: number;
|
|
240
|
+
canvasHeight?: number;
|
|
241
|
+
/** Pan/zoom transform; identity when omitted. Click math inverts it before unprojecting. */
|
|
242
|
+
viewport?: MapViewport;
|
|
243
|
+
style?: CSSProperties;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* The bare top-down map `<svg>` shared by {@link WorldMap} (framed panel) and
|
|
247
|
+
* {@link FullscreenMap} (pan/zoom overlay): baked terrain background, fog, map
|
|
248
|
+
* layers, markers with labels, and the player arrow, drawn under an optional
|
|
249
|
+
* viewport transform. Rectangular linear projection over the world `bounds`.
|
|
250
|
+
*/
|
|
251
|
+
export declare function WorldMapSurface({ markers, bounds, player, facingYaw, fog, background, width, height, kindStyles, routes, zones, cellStates, onWorldClick, canvasWidth, canvasHeight, viewport, style, }: WorldMapSurfaceProps): ReactNode;
|
|
213
252
|
/**
|
|
214
253
|
* Full-bounds top-down world map (the "press M" overlay): baked terrain
|
|
215
254
|
* background, reveal-on-event fog, all markers with labels, and the player.
|
|
216
|
-
* Rectangular linear projection over the supplied world `bounds`.
|
|
255
|
+
* Rectangular linear projection over the supplied world `bounds`. Framed panel
|
|
256
|
+
* wrapper over {@link WorldMapSurface}.
|
|
217
257
|
*/
|
|
218
258
|
export declare function WorldMap({ markers, bounds, player, facingYaw, fog, background, width, height, kindStyles, routes, zones, cellStates, onWorldClick, className, title, onClose, }: WorldMapProps): ReactNode;
|
|
259
|
+
/** Props for {@link FullscreenMap}. */
|
|
260
|
+
export interface FullscreenMapProps extends Omit<WorldMapSurfaceProps, "canvasWidth" | "canvasHeight" | "viewport" | "width" | "height" | "style"> {
|
|
261
|
+
/** Render nothing when false. Default true. */
|
|
262
|
+
open?: boolean;
|
|
263
|
+
/** Content layout width in px before zoom; height derives from the bounds aspect. Default 900. */
|
|
264
|
+
contentWidth?: number;
|
|
265
|
+
/** Zoom bounds. Default 0.5 / 8. */
|
|
266
|
+
minScale?: number;
|
|
267
|
+
maxScale?: number;
|
|
268
|
+
title?: string;
|
|
269
|
+
onClose?: () => void;
|
|
270
|
+
/**
|
|
271
|
+
* Interaction mode. `"pan"` (default) drags to pan and click-to-place fires
|
|
272
|
+
* `onWorldClick`; `"draw"` drags to paint a freehand stroke, committed to
|
|
273
|
+
* `onStrokeComplete` (panning and click-to-place are suppressed).
|
|
274
|
+
*/
|
|
275
|
+
tool?: "pan" | "draw";
|
|
276
|
+
/** Called with a finished freehand stroke (world-XZ points) in `"draw"` mode — wire to an annotation layer's `addStroke`. */
|
|
277
|
+
onStrokeComplete?: (points: readonly WorldXZ[]) => void;
|
|
278
|
+
/** Tone of the in-progress stroke while drawing. Default `"info"`. */
|
|
279
|
+
drawTone?: MapLayerTone;
|
|
280
|
+
/** Width (map px) of the in-progress stroke. Default 3. */
|
|
281
|
+
drawWidth?: number;
|
|
282
|
+
/** Overlay chrome drawn above the map (legend, tool palette, hints). */
|
|
283
|
+
children?: ReactNode;
|
|
284
|
+
overlayClassName?: string;
|
|
285
|
+
overlayStyle?: CSSProperties;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Fullscreen pan/zoom world-map overlay — the enlarged "press M" map. Wheel to
|
|
289
|
+
* zoom (toward the cursor), drag to pan, and click to place (wire `onWorldClick`
|
|
290
|
+
* to a waypoint/annotation store). Switch `tool` to `"draw"` to freehand-draw
|
|
291
|
+
* strokes committed to `onStrokeComplete` (wire to `createAnnotationLayer`).
|
|
292
|
+
* Reuses {@link WorldMapSurface} for rendering, so terrain bake, fog, routes,
|
|
293
|
+
* zones, markers, and the player arrow all appear; a drag never fires
|
|
294
|
+
* `onWorldClick`. Compose a {@link MapLegend} or tool palette via `children`.
|
|
295
|
+
*
|
|
296
|
+
* @capability fullscreen-map fullscreen pan/zoom world-map overlay over WorldMapSurface — wheel-zoom, drag-pan, click-to-place, and a freehand draw tool (onStrokeComplete)
|
|
297
|
+
*/
|
|
298
|
+
export declare function FullscreenMap({ open, bounds, contentWidth, minScale, maxScale, title, onClose, onWorldClick, tool, onStrokeComplete, drawTone, drawWidth, children, overlayClassName, overlayStyle, ...surfaceProps }: FullscreenMapProps): ReactNode;
|
|
299
|
+
/** Props for {@link MapLegend}. */
|
|
300
|
+
export interface MapLegendProps {
|
|
301
|
+
/** Marker kinds to key, in order. */
|
|
302
|
+
kinds: readonly string[];
|
|
303
|
+
/** Human labels per kind; falls back to the kind id. */
|
|
304
|
+
labels?: Record<string, string>;
|
|
305
|
+
kindStyles?: Record<string, MarkerKindStyle>;
|
|
306
|
+
title?: string;
|
|
307
|
+
className?: string;
|
|
308
|
+
style?: CSSProperties;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Marker-kind key for a map/minimap — glyph + color swatch per kind, labelled.
|
|
312
|
+
* Reads the same `kindStyles` the map renders with, so the legend can never
|
|
313
|
+
* drift from the pins.
|
|
314
|
+
*
|
|
315
|
+
* @capability map-legend marker-kind key (glyph + color + label per kind) sharing the map's kindStyles
|
|
316
|
+
*/
|
|
317
|
+
export declare function MapLegend({ kinds, labels, kindStyles, title, className, style, }: MapLegendProps): ReactNode;
|
|
318
|
+
/** Props for {@link WaypointArrow}. */
|
|
319
|
+
export interface WaypointArrowProps {
|
|
320
|
+
/** Bearing to the target relative to facing (radians); 0 = dead ahead. From `WaypointStore.guidance().relative`. */
|
|
321
|
+
relative: number;
|
|
322
|
+
/** XZ distance in world units; renders a readout when supplied. */
|
|
323
|
+
distance?: number;
|
|
324
|
+
label?: string;
|
|
325
|
+
/** Format the distance readout; defaults to whole units + "m". */
|
|
326
|
+
formatDistance?: (distance: number) => string;
|
|
327
|
+
size?: number;
|
|
328
|
+
color?: string;
|
|
329
|
+
className?: string;
|
|
330
|
+
style?: CSSProperties;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* On-screen guide arrow to the tracked waypoint — a HUD compass needle rotated
|
|
334
|
+
* by the facing-relative bearing, with an optional label and distance readout.
|
|
335
|
+
* Pair with `WaypointStore.guidance(playerXZ, facingYaw)`.
|
|
336
|
+
*
|
|
337
|
+
* @capability waypoint-arrow on-screen HUD guide arrow to a tracked waypoint, rotated by facing-relative bearing with a distance readout
|
|
338
|
+
*/
|
|
339
|
+
export declare function WaypointArrow({ relative, distance, label, formatDistance, size, color, className, style, }: WaypointArrowProps): ReactNode;
|