@jgengine/react 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -2
- package/LICENSE +203 -661
- package/README.md +1 -1
- package/dist/actionHud.d.ts +174 -0
- package/dist/actionHud.js +294 -0
- package/dist/bars.d.ts +103 -0
- package/dist/bars.js +180 -0
- package/dist/barsPreview.d.ts +4 -0
- package/dist/barsPreview.js +47 -0
- package/dist/cards.d.ts +59 -0
- package/dist/cards.js +116 -0
- package/dist/chat.d.ts +3 -1
- package/dist/chat.js +2 -2
- package/dist/components.d.ts +28 -1
- package/dist/components.js +36 -1
- package/dist/display.d.ts +1 -0
- package/dist/display.js +1 -0
- package/dist/engineStore.d.ts +3 -0
- package/dist/engineStore.js +3 -0
- package/dist/fogOverlay.d.ts +3 -0
- package/dist/fogOverlay.js +3 -0
- package/dist/gameViewport.js +16 -0
- package/dist/hooks.d.ts +40 -0
- package/dist/hooks.js +69 -1
- package/dist/hud.d.ts +104 -0
- package/dist/hud.js +155 -0
- package/dist/hudFrame.d.ts +47 -0
- package/dist/hudFrame.js +80 -0
- package/dist/hudLayout.d.ts +31 -2
- package/dist/hudLayout.js +127 -10
- package/dist/hudLayoutPersist.d.ts +23 -0
- package/dist/hudLayoutPersist.js +19 -0
- package/dist/hudTheme.d.ts +76 -0
- package/dist/hudTheme.js +156 -0
- package/dist/hudThemePreview.d.ts +4 -0
- package/dist/hudThemePreview.js +37 -0
- package/dist/iconTreatment.d.ts +50 -0
- package/dist/iconTreatment.js +84 -0
- package/dist/iconsPreview.d.ts +4 -0
- package/dist/iconsPreview.js +29 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/keyHint.d.ts +30 -0
- package/dist/keyHint.js +31 -0
- package/dist/map.d.ts +141 -11
- package/dist/map.js +138 -16
- package/dist/save.d.ts +25 -0
- package/dist/save.js +37 -0
- package/dist/selectSnapshot.d.ts +2 -0
- package/dist/selectSnapshot.js +2 -0
- package/dist/selectionHud.d.ts +139 -0
- package/dist/selectionHud.js +188 -0
- package/dist/skillCheckPaint.d.ts +2 -0
- package/dist/skillCheckPaint.js +2 -0
- package/dist/slots.d.ts +44 -0
- package/dist/slots.js +37 -0
- package/dist/startScreen.d.ts +62 -0
- package/dist/startScreen.js +62 -0
- package/dist/store.d.ts +24 -0
- package/dist/store.js +9 -0
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +54 -0
- package/skills/jgengine/api.md +527 -0
- package/skills/jgengine/capabilities.md +131 -0
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +41 -0
- package/skills/jgengine-ui/api.md +2007 -0
- package/skills/jgengine-ui/capabilities.md +211 -0
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +513 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/llms.txt +0 -1724
package/dist/fogOverlay.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** @internal */
|
|
1
2
|
export function forEachUnrevealedFogCell(fog, visit) {
|
|
2
3
|
let count = 0;
|
|
3
4
|
for (let row = 0; row < fog.rows; row += 1) {
|
|
@@ -10,6 +11,7 @@ export function forEachUnrevealedFogCell(fog, visit) {
|
|
|
10
11
|
}
|
|
11
12
|
return count;
|
|
12
13
|
}
|
|
14
|
+
/** @internal */
|
|
13
15
|
export function paintFogOverlay(surface, canvasSize, fog, cellRect, fill = "rgba(11, 15, 20, 0.82)") {
|
|
14
16
|
surface.clearRect(0, 0, canvasSize.width, canvasSize.height);
|
|
15
17
|
surface.fillStyle = fill;
|
|
@@ -20,6 +22,7 @@ export function paintFogOverlay(surface, canvasSize, fog, cellRect, fill = "rgba
|
|
|
20
22
|
surface.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
21
23
|
});
|
|
22
24
|
}
|
|
25
|
+
/** @internal */
|
|
23
26
|
export function createFogDataUrl(fog, canvasSize, cellRect, fill = "rgba(11, 15, 20, 0.82)") {
|
|
24
27
|
if (typeof document === "undefined")
|
|
25
28
|
return null;
|
package/dist/gameViewport.js
CHANGED
|
@@ -67,6 +67,19 @@ function createRegionRegistry() {
|
|
|
67
67
|
}
|
|
68
68
|
const RegionRegistryContext = createContext(null);
|
|
69
69
|
const GameLayoutContext = createContext(null);
|
|
70
|
+
const INPUT_HINT_STYLE_ID = "jg-input-hints";
|
|
71
|
+
const INPUT_HINT_CSS = "@media (pointer: coarse){[data-jg-kbd-hint]{display:none!important}}";
|
|
72
|
+
/** Install the backstop rule that hides keyboard/mouse hints on touch, once per document. */
|
|
73
|
+
function ensureInputHintStylesheet() {
|
|
74
|
+
if (typeof document === "undefined")
|
|
75
|
+
return;
|
|
76
|
+
if (document.getElementById(INPUT_HINT_STYLE_ID) !== null)
|
|
77
|
+
return;
|
|
78
|
+
const style = document.createElement("style");
|
|
79
|
+
style.id = INPUT_HINT_STYLE_ID;
|
|
80
|
+
style.textContent = INPUT_HINT_CSS;
|
|
81
|
+
document.head.appendChild(style);
|
|
82
|
+
}
|
|
70
83
|
const SERVER_METRICS = {
|
|
71
84
|
layout: { left: 0, top: 0, right: 0, bottom: 0 },
|
|
72
85
|
visual: { left: 0, top: 0, right: 0, bottom: 0 },
|
|
@@ -156,6 +169,9 @@ export function GameViewportProvider({ platforms, className, style, children, })
|
|
|
156
169
|
const probeRef = useRef(null);
|
|
157
170
|
const [safeArea, setSafeArea] = useState({ top: 0, right: 0, bottom: 0, left: 0 });
|
|
158
171
|
const rootRef = useRef(null);
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
ensureInputHintStylesheet();
|
|
174
|
+
}, []);
|
|
159
175
|
useEffect(() => {
|
|
160
176
|
const update = () => {
|
|
161
177
|
const next = measureSafeArea(probeRef.current);
|
package/dist/hooks.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { type DependencyList } from "react";
|
|
1
2
|
import { AxisChannel, type AxisChannelConfig } from "@jgengine/core/input/axisInput";
|
|
3
|
+
import { type MarkerSet } from "@jgengine/core/world/markers";
|
|
2
4
|
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
3
5
|
import type { AbilityKit, AbilitySlotSnapshot } from "@jgengine/core/combat/abilityKit";
|
|
4
6
|
import type { EventMeter } from "@jgengine/core/stats/eventMeter";
|
|
@@ -19,6 +21,8 @@ import type { ClockSnapshot, SimClock } from "@jgengine/core/time/simClock";
|
|
|
19
21
|
import { type PositionedPrompt } from "@jgengine/core/interaction/proximityPrompt";
|
|
20
22
|
import { type GamePhase } from "@jgengine/core/game/gamePhase";
|
|
21
23
|
export declare function useGameStore<T>(selector: (ctx: GameContext) => T, isEqual?: (previous: T, next: T) => boolean): T;
|
|
24
|
+
/** Subscribe to a single keyed slice of the game store, returning `fallback` until the key is set. */
|
|
25
|
+
export declare function useGameStoreValue<T>(key: string, fallback: T): T;
|
|
22
26
|
export declare function useGame(): {
|
|
23
27
|
commands: GameContext["game"]["commands"];
|
|
24
28
|
events: GameEvents;
|
|
@@ -36,6 +40,15 @@ export declare function useGamePhase(): {
|
|
|
36
40
|
export declare function useOptionalGamePhase(): GamePhase;
|
|
37
41
|
export declare function useSceneEntities(): readonly SceneEntity[];
|
|
38
42
|
export declare function useSceneObjects(): readonly SceneObject[];
|
|
43
|
+
/**
|
|
44
|
+
* Membership-only entity id list: the returned array keeps a stable identity across per-frame pose
|
|
45
|
+
* writes and only changes when an entity spawns, despawns, or the store is hydrated (#625). A marker
|
|
46
|
+
* mapped from these ids reads its own live pose imperatively (useFrame), so the actor tree no longer
|
|
47
|
+
* re-reconciles every frame. Prefer this over {@link useSceneEntities} for large scenes.
|
|
48
|
+
*/
|
|
49
|
+
export declare function useSceneEntityIds(): readonly string[];
|
|
50
|
+
/** Membership-only object id list — the object counterpart of {@link useSceneEntityIds}; stable across move/rotate/setVisual, changes only on place/remove. */
|
|
51
|
+
export declare function useSceneObjectIds(): readonly string[];
|
|
39
52
|
export declare function useWorldItems(): readonly WorldItemRecord[];
|
|
40
53
|
/** Nearest ground item within `radius` of the local player — drives a pickup prompt/highlight. */
|
|
41
54
|
export declare function useNearestWorldItem(radius: number): WorldItemRecord | null;
|
|
@@ -90,19 +103,46 @@ export declare function useGameClock(): ClockSnapshot & {
|
|
|
90
103
|
};
|
|
91
104
|
export declare function useActivePrompt<T extends PositionedPrompt>(prompts?: readonly T[]): T | null;
|
|
92
105
|
export declare function abilityKitNeedsHeartbeat(kit: AbilityKit, resourceAvailable?: number): boolean;
|
|
106
|
+
/** True when `meter`'s value/fraction/tier/ready diverge from `previous` — the re-render check `useEventMeter` polls on its heartbeat. */
|
|
93
107
|
export declare function eventMeterNeedsHeartbeat(meter: EventMeter, previous: EventMeterView | null): boolean;
|
|
94
108
|
export interface AbilitySlotBindingOptions {
|
|
95
109
|
intervalMs?: number;
|
|
96
110
|
}
|
|
97
111
|
export declare function useAbilitySlots(kit: AbilityKit, resourceAvailable?: number, options?: AbilitySlotBindingOptions): AbilitySlotSnapshot[];
|
|
98
112
|
export declare function useAbilitySlot(kit: AbilityKit, slotId: string, resourceAvailable?: number, options?: AbilitySlotBindingOptions): AbilitySlotSnapshot | null;
|
|
113
|
+
/** A rendered snapshot of an {@link EventMeter}: current value, fill fraction, active tier, and ready-to-consume flag. */
|
|
99
114
|
export interface EventMeterView {
|
|
100
115
|
value: number;
|
|
101
116
|
fraction: number;
|
|
102
117
|
tier: string | null;
|
|
103
118
|
ready: boolean;
|
|
104
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Bind a `createEventMeter` (`@jgengine/core/stats/eventMeter`) heat/streak gauge to a component — the react-render
|
|
122
|
+
* half of the ult/adrenaline and streak/combo meters (`event-meter` capability) that lets a HUD gauge re-render
|
|
123
|
+
* on tick without the game hand-rolling a `useEffect`/`setInterval` heartbeat around `meter.value()`.
|
|
124
|
+
*
|
|
125
|
+
* @capability event-meter-hud render a core event/heat meter's live value, fraction, tier, and ready state in a HUD gauge
|
|
126
|
+
*/
|
|
105
127
|
export declare function useEventMeter(meter: EventMeter, options?: AbilitySlotBindingOptions): EventMeterView;
|
|
128
|
+
/**
|
|
129
|
+
* A self-ticking {@link MarkerSet} kept in sync with the live scene: on a
|
|
130
|
+
* heartbeat (default 100ms, and once immediately) it clears the set and calls
|
|
131
|
+
* `rebuild(markers, ctx)` to repopulate it from the current scene, so a HUD map
|
|
132
|
+
* never hand-rolls a `setInterval` + `markers.clear()` + rescan. Each heartbeat
|
|
133
|
+
* also re-renders the calling component, so player-centered props derived from
|
|
134
|
+
* the live scene each tick (`center`, `facingYaw`, zone name) stay fresh — this
|
|
135
|
+
* fully subsumes the hand-rolled ticker, not just the marker rebuild. The set is
|
|
136
|
+
* stable across renders and re-seeded whenever `ctx`, `intervalMs`, or the
|
|
137
|
+
* supplied `deps` change. SSR-safe: the heartbeat only arms when `window` is
|
|
138
|
+
* available.
|
|
139
|
+
*
|
|
140
|
+
* @capability live-markers a self-ticking MarkerSet kept in sync from the live scene each HUD tick — pass a rebuild(markers, ctx) that clears+repopulates, no hand-rolled setInterval
|
|
141
|
+
*/
|
|
142
|
+
export declare function useLiveMarkers(rebuild: (markers: MarkerSet, ctx: GameContext) => void, options?: {
|
|
143
|
+
intervalMs?: number;
|
|
144
|
+
deps?: DependencyList;
|
|
145
|
+
}): MarkerSet;
|
|
106
146
|
type HeldKeyEventTarget = Pick<Window, "addEventListener" | "removeEventListener">;
|
|
107
147
|
export declare function createHeldKeyTracker(target: HeldKeyEventTarget): {
|
|
108
148
|
isDown: (code: string) => boolean;
|
package/dist/hooks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
2
2
|
import { AxisChannel } from "@jgengine/core/input/axisInput";
|
|
3
|
+
import { createMarkerSet } from "@jgengine/core/world/markers";
|
|
3
4
|
import { browseSessions, } from "@jgengine/core/multiplayer/matchmaking";
|
|
4
5
|
import { resolveActivePrompt, } from "@jgengine/core/interaction/proximityPrompt";
|
|
5
6
|
import { gamePhase, setGamePhase } from "@jgengine/core/game/gamePhase";
|
|
@@ -16,6 +17,10 @@ export function useGameStore(selector, isEqual = Object.is) {
|
|
|
16
17
|
const getSnapshot = useCallback(() => readSelectSnapshot(cacheRef.current, ctx.version(), () => selectorRef.current(ctx), (previous, next) => isEqualRef.current(previous, next)), [ctx]);
|
|
17
18
|
return useSyncExternalStore(ctx.subscribe, getSnapshot, getSnapshot);
|
|
18
19
|
}
|
|
20
|
+
/** Subscribe to a single keyed slice of the game store, returning `fallback` until the key is set. */
|
|
21
|
+
export function useGameStoreValue(key, fallback) {
|
|
22
|
+
return useGameStore((ctx) => ctx.game.store.get(key) ?? fallback);
|
|
23
|
+
}
|
|
19
24
|
export function useGame() {
|
|
20
25
|
const ctx = useGameContext();
|
|
21
26
|
return useMemo(() => ({ commands: ctx.game.commands, events: ctx.game.events }), [ctx]);
|
|
@@ -43,6 +48,23 @@ export function useSceneEntities() {
|
|
|
43
48
|
export function useSceneObjects() {
|
|
44
49
|
return useGameStore((ctx) => ctx.scene.object.list());
|
|
45
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Membership-only entity id list: the returned array keeps a stable identity across per-frame pose
|
|
53
|
+
* writes and only changes when an entity spawns, despawns, or the store is hydrated (#625). A marker
|
|
54
|
+
* mapped from these ids reads its own live pose imperatively (useFrame), so the actor tree no longer
|
|
55
|
+
* re-reconciles every frame. Prefer this over {@link useSceneEntities} for large scenes.
|
|
56
|
+
*/
|
|
57
|
+
export function useSceneEntityIds() {
|
|
58
|
+
const ctx = useGameContext();
|
|
59
|
+
const entity = ctx.scene.entity;
|
|
60
|
+
return useSyncExternalStore(entity.subscribeMembership, entity.ids, entity.ids);
|
|
61
|
+
}
|
|
62
|
+
/** Membership-only object id list — the object counterpart of {@link useSceneEntityIds}; stable across move/rotate/setVisual, changes only on place/remove. */
|
|
63
|
+
export function useSceneObjectIds() {
|
|
64
|
+
const ctx = useGameContext();
|
|
65
|
+
const object = ctx.scene.object;
|
|
66
|
+
return useSyncExternalStore(object.subscribeMembership, object.ids, object.ids);
|
|
67
|
+
}
|
|
46
68
|
export function useWorldItems() {
|
|
47
69
|
return useGameStore((ctx) => ctx.scene.worldItem.list());
|
|
48
70
|
}
|
|
@@ -71,8 +93,9 @@ export function useCurrency(currencyId) {
|
|
|
71
93
|
export function useFeed({ action, limit }) {
|
|
72
94
|
return useGameStore((ctx) => ctx.game.feed.recent(action, limit === undefined ? undefined : { limit }));
|
|
73
95
|
}
|
|
96
|
+
const EMPTY_QUESTS = [];
|
|
74
97
|
export function useQuestJournal() {
|
|
75
|
-
return useGameStore((ctx) => ctx.game.quest
|
|
98
|
+
return useGameStore((ctx) => ctx.game.quest?.list(ctx.player.userId) ?? EMPTY_QUESTS);
|
|
76
99
|
}
|
|
77
100
|
const EMPTY_FRIENDS = [];
|
|
78
101
|
const EMPTY_PARTY = [];
|
|
@@ -191,6 +214,7 @@ export function abilityKitNeedsHeartbeat(kit, resourceAvailable) {
|
|
|
191
214
|
}
|
|
192
215
|
return false;
|
|
193
216
|
}
|
|
217
|
+
/** True when `meter`'s value/fraction/tier/ready diverge from `previous` — the re-render check `useEventMeter` polls on its heartbeat. */
|
|
194
218
|
export function eventMeterNeedsHeartbeat(meter, previous) {
|
|
195
219
|
const next = {
|
|
196
220
|
value: meter.value(),
|
|
@@ -227,6 +251,13 @@ export function useAbilitySlot(kit, slotId, resourceAvailable, options) {
|
|
|
227
251
|
useEngineHeartbeat(options?.intervalMs ?? 80, () => abilityKitNeedsHeartbeat(kit, resourceAvailable));
|
|
228
252
|
return kit.state(slotId, resourceAvailable);
|
|
229
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Bind a `createEventMeter` (`@jgengine/core/stats/eventMeter`) heat/streak gauge to a component — the react-render
|
|
256
|
+
* half of the ult/adrenaline and streak/combo meters (`event-meter` capability) that lets a HUD gauge re-render
|
|
257
|
+
* on tick without the game hand-rolling a `useEffect`/`setInterval` heartbeat around `meter.value()`.
|
|
258
|
+
*
|
|
259
|
+
* @capability event-meter-hud render a core event/heat meter's live value, fraction, tier, and ready state in a HUD gauge
|
|
260
|
+
*/
|
|
230
261
|
export function useEventMeter(meter, options) {
|
|
231
262
|
const previous = useRef(null);
|
|
232
263
|
useEngineHeartbeat(options?.intervalMs ?? 80, () => {
|
|
@@ -245,6 +276,43 @@ export function useEventMeter(meter, options) {
|
|
|
245
276
|
previous.current = view;
|
|
246
277
|
return view;
|
|
247
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* A self-ticking {@link MarkerSet} kept in sync with the live scene: on a
|
|
281
|
+
* heartbeat (default 100ms, and once immediately) it clears the set and calls
|
|
282
|
+
* `rebuild(markers, ctx)` to repopulate it from the current scene, so a HUD map
|
|
283
|
+
* never hand-rolls a `setInterval` + `markers.clear()` + rescan. Each heartbeat
|
|
284
|
+
* also re-renders the calling component, so player-centered props derived from
|
|
285
|
+
* the live scene each tick (`center`, `facingYaw`, zone name) stay fresh — this
|
|
286
|
+
* fully subsumes the hand-rolled ticker, not just the marker rebuild. The set is
|
|
287
|
+
* stable across renders and re-seeded whenever `ctx`, `intervalMs`, or the
|
|
288
|
+
* supplied `deps` change. SSR-safe: the heartbeat only arms when `window` is
|
|
289
|
+
* available.
|
|
290
|
+
*
|
|
291
|
+
* @capability live-markers a self-ticking MarkerSet kept in sync from the live scene each HUD tick — pass a rebuild(markers, ctx) that clears+repopulates, no hand-rolled setInterval
|
|
292
|
+
*/
|
|
293
|
+
export function useLiveMarkers(rebuild, options) {
|
|
294
|
+
const ctx = useGameContext();
|
|
295
|
+
const markers = useMemo(() => createMarkerSet(() => ctx.time.now()), [ctx]);
|
|
296
|
+
const rebuildRef = useRef(rebuild);
|
|
297
|
+
rebuildRef.current = rebuild;
|
|
298
|
+
const intervalMs = options?.intervalMs ?? 100;
|
|
299
|
+
const deps = options?.deps ?? [];
|
|
300
|
+
const [, forceTick] = useState(0);
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
const run = () => {
|
|
303
|
+
markers.clear();
|
|
304
|
+
rebuildRef.current(markers, ctx);
|
|
305
|
+
forceTick((value) => (value + 1) % 1_000_000);
|
|
306
|
+
};
|
|
307
|
+
run();
|
|
308
|
+
if (intervalMs <= 0 || typeof window === "undefined")
|
|
309
|
+
return undefined;
|
|
310
|
+
const id = window.setInterval(run, intervalMs);
|
|
311
|
+
return () => window.clearInterval(id);
|
|
312
|
+
// deps is spread so callers can re-seed on their own inputs.
|
|
313
|
+
}, [markers, ctx, intervalMs, ...deps]);
|
|
314
|
+
return markers;
|
|
315
|
+
}
|
|
248
316
|
export function createHeldKeyTracker(target) {
|
|
249
317
|
const held = new Set();
|
|
250
318
|
const onKeyDown = (event) => held.add(event.code);
|
package/dist/hud.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
/** Named color ramps for {@link StatBar} tones. `fn` maps fill fraction (0..1) → gradient stops. */
|
|
3
|
+
declare const TONES: Record<string, (t: number) => [string, string]>;
|
|
4
|
+
/** A stat tone — picks the bar's color ramp. */
|
|
5
|
+
export type StatTone = keyof typeof TONES;
|
|
6
|
+
/**
|
|
7
|
+
* A polished stat bar (health/mana/stamina/shield/xp) — a rounded, glassy meter with a tone-colored
|
|
8
|
+
* fill and an optional value readout. Reads `statId` off `entityId` (defaults to the local player).
|
|
9
|
+
* Renders nothing until the stat exists.
|
|
10
|
+
*
|
|
11
|
+
* @deprecated The `tone`-switched umbrella is being retired (#1033). Use the atomic, purpose-named,
|
|
12
|
+
* token-themed bars from `@jgengine/react/bars` instead — `HealthBar`, `ShieldBar`, `ManaBar`,
|
|
13
|
+
* `StaminaBar`, `ExperienceBar`, `SoulBar`, `BossBar`, `AmmoCounter` — each does one readout and is
|
|
14
|
+
* restyled globally via the shared `--jg-*` tokens (`barTokens`). Kept as a thin shim during migration.
|
|
15
|
+
*
|
|
16
|
+
* @capability hud-stat-bar optional stat-bound bar building block (health, mana, stamina) — prefer game-owned chrome
|
|
17
|
+
*/
|
|
18
|
+
export declare function StatBar({ statId, entityId, tone, label, showValue, width, icon, style, className, }: {
|
|
19
|
+
statId?: string;
|
|
20
|
+
entityId?: string;
|
|
21
|
+
tone?: StatTone;
|
|
22
|
+
label?: string;
|
|
23
|
+
showValue?: boolean;
|
|
24
|
+
width?: number;
|
|
25
|
+
icon?: ReactNode;
|
|
26
|
+
style?: CSSProperties;
|
|
27
|
+
className?: string;
|
|
28
|
+
}): import("react").JSX.Element | null;
|
|
29
|
+
/**
|
|
30
|
+
* A numbered hotbar bound to an inventory — painted iconed slots (a `GameIcon` glyph over a
|
|
31
|
+
* school-keyed gradient with a count badge, #1035), an active-slot highlight, and a keycap per slot.
|
|
32
|
+
* Place it and pass the `inventoryId`; `activeSlot` highlights the equipped one. Supply `itemIcon` to
|
|
33
|
+
* map your item ids to your own glyphs — the default resolves a `GameIcon` from the item id.
|
|
34
|
+
*
|
|
35
|
+
* @capability hud-hotbar optional inventory hotbar building block — games own slot chrome
|
|
36
|
+
*/
|
|
37
|
+
export declare function Hotbar({ inventoryId, activeSlot, keys, slotSize, itemIcon, style, className, }: {
|
|
38
|
+
inventoryId: string;
|
|
39
|
+
activeSlot?: number;
|
|
40
|
+
keys?: readonly string[];
|
|
41
|
+
slotSize?: number;
|
|
42
|
+
/** Caller-supplied item id → icon registry; return null/undefined to fall back to the default glyph. */
|
|
43
|
+
itemIcon?: (itemId: string) => ReactNode;
|
|
44
|
+
style?: CSSProperties;
|
|
45
|
+
className?: string;
|
|
46
|
+
}): import("react").JSX.Element;
|
|
47
|
+
/**
|
|
48
|
+
* A speedometer for an entity (defaults to the local player) — an SVG arc gauge + a digital readout.
|
|
49
|
+
* `scale` converts world units/second to your display unit (3.6 → km/h, 2.237 → mph); `max` sets the
|
|
50
|
+
* gauge's top of scale.
|
|
51
|
+
*/
|
|
52
|
+
export declare function Speedometer({ entityId, scale, unit, max, size, style, className, }: {
|
|
53
|
+
entityId?: string;
|
|
54
|
+
scale?: number;
|
|
55
|
+
unit?: string;
|
|
56
|
+
max?: number;
|
|
57
|
+
size?: number;
|
|
58
|
+
style?: CSSProperties;
|
|
59
|
+
className?: string;
|
|
60
|
+
}): import("react").JSX.Element;
|
|
61
|
+
/**
|
|
62
|
+
* A time-of-day clock reading the sim calendar — `Day N · HH:MM`, 24h or 12h. `controls` adds
|
|
63
|
+
* pause + the game's speed multipliers as clickable pills (the "fast-forward" bar), off by default so
|
|
64
|
+
* a game opts into letting the player scrub time.
|
|
65
|
+
*
|
|
66
|
+
* @capability hud-clock in-game time-of-day readout bound to the sim clock
|
|
67
|
+
*/
|
|
68
|
+
export declare function Clock({ format, showDay, controls, style, className, }: {
|
|
69
|
+
format?: "24h" | "12h";
|
|
70
|
+
showDay?: boolean;
|
|
71
|
+
controls?: boolean;
|
|
72
|
+
style?: CSSProperties;
|
|
73
|
+
className?: string;
|
|
74
|
+
}): import("react").JSX.Element;
|
|
75
|
+
/**
|
|
76
|
+
* A wave / round banner — a bold centered pill for "WAVE 3" style callouts, with an optional subtitle
|
|
77
|
+
* (enemies remaining, timer). Pure display: pass the current `wave` and whatever subtitle you track.
|
|
78
|
+
*/
|
|
79
|
+
export declare function WaveBanner({ wave, label, subtitle, style, className, }: {
|
|
80
|
+
wave: number | string;
|
|
81
|
+
label?: string;
|
|
82
|
+
subtitle?: ReactNode;
|
|
83
|
+
style?: CSSProperties;
|
|
84
|
+
className?: string;
|
|
85
|
+
}): import("react").JSX.Element;
|
|
86
|
+
/** A currency counter — an icon (emoji/char, default a coin) plus the live amount for `currencyId`. *
|
|
87
|
+
* @capability hud-coins currency counter widget bound to a currency id
|
|
88
|
+
*/
|
|
89
|
+
export declare function Coins({ currencyId, icon, style, className, }: {
|
|
90
|
+
currencyId: string;
|
|
91
|
+
icon?: ReactNode;
|
|
92
|
+
style?: CSSProperties;
|
|
93
|
+
className?: string;
|
|
94
|
+
}): import("react").JSX.Element;
|
|
95
|
+
/** A minimal center crosshair reticle — four ticks around a gap. Purely presentational. */
|
|
96
|
+
export declare function Crosshair({ size, gap, thickness, color, style, className, }: {
|
|
97
|
+
size?: number;
|
|
98
|
+
gap?: number;
|
|
99
|
+
thickness?: number;
|
|
100
|
+
color?: string;
|
|
101
|
+
style?: CSSProperties;
|
|
102
|
+
className?: string;
|
|
103
|
+
}): import("react").JSX.Element;
|
|
104
|
+
export {};
|
package/dist/hud.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { groundSpeed } from "@jgengine/core/scene/entityStore";
|
|
4
|
+
import { iconForItemId } from "./gameIcons.js";
|
|
5
|
+
import { hudFrameStyle } from "./hudFrame.js";
|
|
6
|
+
import { IconTreatment, schoolForItem } from "./iconTreatment.js";
|
|
7
|
+
import { useCurrency, useEntityStat, useGameClock, useGameStore, useInventory, localPlayerEntity } from "./hooks.js";
|
|
8
|
+
/**
|
|
9
|
+
* Optional HUD building blocks bound to live game state — not a finished game face. Every game
|
|
10
|
+
* owns custom presentation (see AGENTS.md); import these only as temporary scaffolding, previews,
|
|
11
|
+
* or under game-owned chrome. Prefer headless hooks + game markup for shipped UI. The engine
|
|
12
|
+
* mounts none of these by default. Styling is self-contained dark-glass for demos; pass
|
|
13
|
+
* `style`/`className` to override. Anything reading the player defaults to the local player entity.
|
|
14
|
+
*
|
|
15
|
+
* @capability hud-components optional health/hotbar/speed/clock/wave/currency building blocks — games own custom chrome
|
|
16
|
+
*/
|
|
17
|
+
const PANEL = hudFrameStyle("glass");
|
|
18
|
+
/** Named color ramps for {@link StatBar} tones. `fn` maps fill fraction (0..1) → gradient stops. */
|
|
19
|
+
const TONES = {
|
|
20
|
+
health: (t) => (t > 0.5 ? ["#22c55e", "#4ade80"] : t > 0.25 ? ["#f59e0b", "#fbbf24"] : ["#dc2626", "#f87171"]),
|
|
21
|
+
mana: () => ["#2563eb", "#60a5fa"],
|
|
22
|
+
stamina: () => ["#65a30d", "#a3e635"],
|
|
23
|
+
shield: () => ["#0891b2", "#22d3ee"],
|
|
24
|
+
xp: () => ["#7c3aed", "#a78bfa"],
|
|
25
|
+
neutral: () => ["#475569", "#94a3b8"],
|
|
26
|
+
};
|
|
27
|
+
function useLocalPlayerId(entityId) {
|
|
28
|
+
return useGameStore((ctx) => entityId ?? localPlayerEntity(ctx)?.id ?? null);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A polished stat bar (health/mana/stamina/shield/xp) — a rounded, glassy meter with a tone-colored
|
|
32
|
+
* fill and an optional value readout. Reads `statId` off `entityId` (defaults to the local player).
|
|
33
|
+
* Renders nothing until the stat exists.
|
|
34
|
+
*
|
|
35
|
+
* @deprecated The `tone`-switched umbrella is being retired (#1033). Use the atomic, purpose-named,
|
|
36
|
+
* token-themed bars from `@jgengine/react/bars` instead — `HealthBar`, `ShieldBar`, `ManaBar`,
|
|
37
|
+
* `StaminaBar`, `ExperienceBar`, `SoulBar`, `BossBar`, `AmmoCounter` — each does one readout and is
|
|
38
|
+
* restyled globally via the shared `--jg-*` tokens (`barTokens`). Kept as a thin shim during migration.
|
|
39
|
+
*
|
|
40
|
+
* @capability hud-stat-bar optional stat-bound bar building block (health, mana, stamina) — prefer game-owned chrome
|
|
41
|
+
*/
|
|
42
|
+
export function StatBar({ statId = "health", entityId, tone = "health", label, showValue = true, width = 200, icon, style, className, }) {
|
|
43
|
+
const id = useLocalPlayerId(entityId);
|
|
44
|
+
const stat = useEntityStat(id ?? "", statId);
|
|
45
|
+
if (id === null || stat === null)
|
|
46
|
+
return null;
|
|
47
|
+
const range = stat.max - stat.min;
|
|
48
|
+
const fraction = range <= 0 ? 0 : Math.max(0, Math.min(1, (stat.current - stat.min) / range));
|
|
49
|
+
const [from, to] = (TONES[tone] ?? TONES.neutral)(fraction);
|
|
50
|
+
return (_jsxs("div", { className: className, style: { ...PANEL, padding: "6px 8px", width, ...style }, "data-stat": statId, children: [(label !== undefined || icon !== undefined || showValue) && (_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 4, fontSize: 11, fontWeight: 600, letterSpacing: 0.3 }, children: [icon, _jsx("span", { style: { textTransform: "uppercase", opacity: 0.75 }, children: label ?? statId }), showValue ? (_jsxs("span", { style: { marginLeft: "auto", fontVariantNumeric: "tabular-nums", opacity: 0.9 }, children: [Math.round(stat.current), _jsxs("span", { style: { opacity: 0.5 }, children: [" / ", Math.round(stat.max)] })] })) : null] })), _jsx("div", { style: { height: 8, borderRadius: 999, background: "rgba(255,255,255,0.10)", overflow: "hidden" }, children: _jsx("div", { style: { width: `${fraction * 100}%`, height: "100%", borderRadius: 999, background: `linear-gradient(90deg, ${from}, ${to})`, transition: "width 160ms ease" } }) })] }));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A numbered hotbar bound to an inventory — painted iconed slots (a `GameIcon` glyph over a
|
|
54
|
+
* school-keyed gradient with a count badge, #1035), an active-slot highlight, and a keycap per slot.
|
|
55
|
+
* Place it and pass the `inventoryId`; `activeSlot` highlights the equipped one. Supply `itemIcon` to
|
|
56
|
+
* map your item ids to your own glyphs — the default resolves a `GameIcon` from the item id.
|
|
57
|
+
*
|
|
58
|
+
* @capability hud-hotbar optional inventory hotbar building block — games own slot chrome
|
|
59
|
+
*/
|
|
60
|
+
export function Hotbar({ inventoryId, activeSlot, keys, slotSize = 46, itemIcon, style, className, }) {
|
|
61
|
+
const slots = useInventory(inventoryId);
|
|
62
|
+
return (_jsx("div", { className: className, style: { display: "flex", gap: 6, ...style }, "data-inventory": inventoryId, children: slots.map((slot, index) => {
|
|
63
|
+
const active = index === activeSlot;
|
|
64
|
+
const keycap = keys?.[index] ?? String(index + 1);
|
|
65
|
+
const custom = slot !== null ? itemIcon?.(slot.itemId) : undefined;
|
|
66
|
+
return (_jsx("div", { "data-slot": index, style: {
|
|
67
|
+
width: slotSize,
|
|
68
|
+
height: slotSize,
|
|
69
|
+
position: "relative",
|
|
70
|
+
}, children: slot !== null ? (custom !== undefined && custom !== null ? (_jsx(IconTreatment, { glyph: custom, size: slotSize, count: slot.count, keycap: keycap, active: active })) : (_jsx(IconTreatment, { ...(iconForItemId(slot.itemId) === null ? {} : { icon: iconForItemId(slot.itemId) }), school: schoolForItem(slot.itemId), size: slotSize, count: slot.count, keycap: keycap, active: active }))) : (_jsx("div", { style: {
|
|
71
|
+
...PANEL,
|
|
72
|
+
width: slotSize,
|
|
73
|
+
height: slotSize,
|
|
74
|
+
position: "relative",
|
|
75
|
+
borderColor: active ? "rgba(56,189,248,0.9)" : "rgba(255,255,255,0.10)",
|
|
76
|
+
}, children: _jsx("span", { style: { position: "absolute", top: 2, left: 4, fontSize: 9, fontWeight: 700, opacity: 0.55 }, children: keycap }) })) }, index));
|
|
77
|
+
}) }));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A speedometer for an entity (defaults to the local player) — an SVG arc gauge + a digital readout.
|
|
81
|
+
* `scale` converts world units/second to your display unit (3.6 → km/h, 2.237 → mph); `max` sets the
|
|
82
|
+
* gauge's top of scale.
|
|
83
|
+
*/
|
|
84
|
+
export function Speedometer({ entityId, scale = 3.6, unit = "km/h", max = 60, size = 96, style, className, }) {
|
|
85
|
+
const speed = useGameStore((ctx) => {
|
|
86
|
+
const entity = entityId === undefined ? localPlayerEntity(ctx) : ctx.scene.entity.get(entityId);
|
|
87
|
+
return entity === null ? 0 : groundSpeed(entity) * scale;
|
|
88
|
+
});
|
|
89
|
+
const fraction = Math.max(0, Math.min(1, speed / max));
|
|
90
|
+
// 240° sweep from -210° to +30°.
|
|
91
|
+
const startDeg = -210;
|
|
92
|
+
const sweep = 240;
|
|
93
|
+
const radius = size / 2 - 8;
|
|
94
|
+
const cx = size / 2;
|
|
95
|
+
const cy = size / 2;
|
|
96
|
+
const arc = (fromFrac, toFrac) => {
|
|
97
|
+
const a0 = ((startDeg + sweep * fromFrac) * Math.PI) / 180;
|
|
98
|
+
const a1 = ((startDeg + sweep * toFrac) * Math.PI) / 180;
|
|
99
|
+
const large = sweep * (toFrac - fromFrac) > 180 ? 1 : 0;
|
|
100
|
+
return `M ${cx + radius * Math.cos(a0)} ${cy + radius * Math.sin(a0)} A ${radius} ${radius} 0 ${large} 1 ${cx + radius * Math.cos(a1)} ${cy + radius * Math.sin(a1)}`;
|
|
101
|
+
};
|
|
102
|
+
return (_jsxs("div", { className: className, style: { ...PANEL, width: size, padding: 6, textAlign: "center", ...style }, children: [_jsxs("svg", { width: size - 12, height: (size - 12) * 0.72, viewBox: `0 0 ${size} ${size * 0.72}`, style: { display: "block", margin: "0 auto" }, children: [_jsx("path", { d: arc(0, 1), fill: "none", stroke: "rgba(255,255,255,0.12)", strokeWidth: 7, strokeLinecap: "round" }), _jsx("path", { d: arc(0, fraction), fill: "none", stroke: fraction > 0.85 ? "#f87171" : "#38bdf8", strokeWidth: 7, strokeLinecap: "round" })] }), _jsx("div", { style: { marginTop: -8, fontVariantNumeric: "tabular-nums", fontWeight: 700, fontSize: 18, lineHeight: 1 }, children: Math.round(speed) }), _jsx("div", { style: { fontSize: 9, opacity: 0.6, letterSpacing: 0.4, textTransform: "uppercase" }, children: unit })] }));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A time-of-day clock reading the sim calendar — `Day N · HH:MM`, 24h or 12h. `controls` adds
|
|
106
|
+
* pause + the game's speed multipliers as clickable pills (the "fast-forward" bar), off by default so
|
|
107
|
+
* a game opts into letting the player scrub time.
|
|
108
|
+
*
|
|
109
|
+
* @capability hud-clock in-game time-of-day readout bound to the sim clock
|
|
110
|
+
*/
|
|
111
|
+
export function Clock({ format = "24h", showDay = true, controls = false, style, className, }) {
|
|
112
|
+
const clock = useGameClock();
|
|
113
|
+
const { hour, minute, day } = clock.calendar;
|
|
114
|
+
const label = useMemo(() => {
|
|
115
|
+
const mm = minute.toString().padStart(2, "0");
|
|
116
|
+
if (format === "12h") {
|
|
117
|
+
const h12 = hour % 12 === 0 ? 12 : hour % 12;
|
|
118
|
+
return `${h12}:${mm} ${hour < 12 ? "AM" : "PM"}`;
|
|
119
|
+
}
|
|
120
|
+
return `${hour.toString().padStart(2, "0")}:${mm}`;
|
|
121
|
+
}, [hour, minute, format]);
|
|
122
|
+
return (_jsxs("div", { className: className, style: { ...PANEL, padding: "5px 10px", display: "inline-flex", alignItems: "center", gap: 8, fontVariantNumeric: "tabular-nums", ...style }, children: [showDay ? _jsxs("span", { style: { fontSize: 11, opacity: 0.6 }, children: ["Day ", day + 1] }) : null, _jsx("span", { style: { fontWeight: 700, fontSize: 14 }, children: label }), controls ? (_jsxs("span", { style: { display: "inline-flex", gap: 3, marginLeft: 4 }, children: [_jsx(ClockPill, { label: clock.paused ? "▶" : "❚❚", active: false, onClick: () => (clock.paused ? clock.controls.play() : clock.controls.pause()) }), clock.speeds.map((s) => (_jsx(ClockPill, { label: `${s}×`, active: !clock.paused && clock.speed === s, onClick: () => clock.controls.setSpeed(s) }, s)))] })) : null] }));
|
|
123
|
+
}
|
|
124
|
+
function ClockPill({ label, active, onClick }) {
|
|
125
|
+
return (_jsx("button", { type: "button", onClick: onClick, style: {
|
|
126
|
+
pointerEvents: "auto",
|
|
127
|
+
cursor: "pointer",
|
|
128
|
+
fontSize: 10,
|
|
129
|
+
fontWeight: 600,
|
|
130
|
+
padding: "1px 6px",
|
|
131
|
+
borderRadius: 6,
|
|
132
|
+
border: "none",
|
|
133
|
+
color: active ? "#0a0c10" : "#cbd5e1",
|
|
134
|
+
background: active ? "#38bdf8" : "rgba(255,255,255,0.08)",
|
|
135
|
+
}, children: label }));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A wave / round banner — a bold centered pill for "WAVE 3" style callouts, with an optional subtitle
|
|
139
|
+
* (enemies remaining, timer). Pure display: pass the current `wave` and whatever subtitle you track.
|
|
140
|
+
*/
|
|
141
|
+
export function WaveBanner({ wave, label = "Wave", subtitle, style, className, }) {
|
|
142
|
+
return (_jsxs("div", { className: className, style: { ...PANEL, padding: "6px 16px", textAlign: "center", display: "inline-block", ...style }, children: [_jsx("div", { style: { fontSize: 10, letterSpacing: 2, textTransform: "uppercase", opacity: 0.65 }, children: label }), _jsx("div", { style: { fontSize: 24, fontWeight: 800, lineHeight: 1.1 }, children: wave }), subtitle !== undefined ? _jsx("div", { style: { fontSize: 11, opacity: 0.7, marginTop: 2 }, children: subtitle }) : null] }));
|
|
143
|
+
}
|
|
144
|
+
/** A currency counter — an icon (emoji/char, default a coin) plus the live amount for `currencyId`. *
|
|
145
|
+
* @capability hud-coins currency counter widget bound to a currency id
|
|
146
|
+
*/
|
|
147
|
+
export function Coins({ currencyId, icon = "🪙", style, className, }) {
|
|
148
|
+
const amount = useCurrency(currencyId);
|
|
149
|
+
return (_jsxs("span", { className: className, style: { ...PANEL, padding: "3px 10px", display: "inline-flex", alignItems: "center", gap: 6, fontWeight: 700, fontVariantNumeric: "tabular-nums", ...style }, "data-currency": currencyId, children: [_jsx("span", { "aria-hidden": true, children: icon }), amount] }));
|
|
150
|
+
}
|
|
151
|
+
/** A minimal center crosshair reticle — four ticks around a gap. Purely presentational. */
|
|
152
|
+
export function Crosshair({ size = 18, gap = 5, thickness = 2, color = "rgba(255,255,255,0.85)", style, className, }) {
|
|
153
|
+
const tick = (extra) => ({ position: "absolute", background: color, borderRadius: thickness, ...extra });
|
|
154
|
+
return (_jsx("div", { className: className, style: { position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "none", ...style }, children: _jsxs("div", { style: { position: "relative", width: size, height: size }, children: [_jsx("div", { style: tick({ left: "50%", top: 0, width: thickness, height: size / 2 - gap, transform: "translateX(-50%)" }) }), _jsx("div", { style: tick({ left: "50%", bottom: 0, width: thickness, height: size / 2 - gap, transform: "translateX(-50%)" }) }), _jsx("div", { style: tick({ top: "50%", left: 0, height: thickness, width: size / 2 - gap, transform: "translateY(-50%)" }) }), _jsx("div", { style: tick({ top: "50%", right: 0, height: thickness, width: size / 2 - gap, transform: "translateY(-50%)" }) })] }) }));
|
|
155
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
/** Frame skin — `glass` (the shared dark-glass panel), `plate` (a heavier opaque plate), `retro` (a hard black-outlined frame), or `themed` (driven by the `--jg-frame-*` `HudTheme` tokens). */
|
|
3
|
+
export type HudFrameVariation = "glass" | "plate" | "retro" | "themed";
|
|
4
|
+
/** Frame corner shape — `rounded` (the variation's default radius), `circle` (fully round), or `square` (no radius). */
|
|
5
|
+
export type HudFrameShape = "rounded" | "circle" | "square";
|
|
6
|
+
/**
|
|
7
|
+
* Bare style object for a HUD frame's look (no wrapper element) — spread it onto
|
|
8
|
+
* a widget that already owns its element when it just needs the chrome CSS. The
|
|
9
|
+
* `glass` variation is byte-for-byte the shared dark-glass panel used across the
|
|
10
|
+
* built-in HUD widgets; `plate` is a heavier opaque plate; `retro` is a hard
|
|
11
|
+
* black-outlined frame. `shape` overrides the corner radius.
|
|
12
|
+
*
|
|
13
|
+
* @internal helper behind {@link HudFrame} (and the built-in HUD `PANEL`); widgets normally use `HudFrame`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function hudFrameStyle(variation?: HudFrameVariation, shape?: HudFrameShape): CSSProperties;
|
|
16
|
+
/** Props for {@link HudFrame}. */
|
|
17
|
+
export interface HudFrameProps {
|
|
18
|
+
/** Frame skin (default `glass`). */
|
|
19
|
+
variation?: HudFrameVariation;
|
|
20
|
+
/** Corner shape (default `rounded`). */
|
|
21
|
+
shape?: HudFrameShape;
|
|
22
|
+
/** Small uppercase header label rendered at the top-left of the frame. */
|
|
23
|
+
title?: ReactNode;
|
|
24
|
+
/** Header content rendered at the top-right, opposite `title` (e.g. a readout or cardinal). */
|
|
25
|
+
aside?: ReactNode;
|
|
26
|
+
/** Inner padding of the frame. */
|
|
27
|
+
padding?: number | string;
|
|
28
|
+
/** Fixed frame width. */
|
|
29
|
+
width?: number | string;
|
|
30
|
+
/** Accept pointer events (default off, so the frame never eats clicks meant for the game). */
|
|
31
|
+
interactive?: boolean;
|
|
32
|
+
/** Extra class on the frame element. */
|
|
33
|
+
className?: string;
|
|
34
|
+
/** Style overrides merged last, so callers can pin exact gradients/colors. */
|
|
35
|
+
style?: CSSProperties;
|
|
36
|
+
/** Frame body. */
|
|
37
|
+
children?: ReactNode;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Optional framed panel primitive — a single `<div data-hud-frame>` with a `glass`,
|
|
41
|
+
* `plate`, `retro`, or `themed` skin, optional `title`/`aside` header, and caller
|
|
42
|
+
* `style` merged last. Shared building block for demos and token-themed widgets;
|
|
43
|
+
* games own product chrome and art direction (see AGENTS.md).
|
|
44
|
+
*
|
|
45
|
+
* @capability hud-frame optional framed panel skins (glass/plate/retro/themed) — games own product chrome
|
|
46
|
+
*/
|
|
47
|
+
export declare function HudFrame({ variation, shape, title, aside, padding, width, interactive, className, style, children, }: HudFrameProps): ReactNode;
|
package/dist/hudFrame.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Bare style object for a HUD frame's look (no wrapper element) — spread it onto
|
|
4
|
+
* a widget that already owns its element when it just needs the chrome CSS. The
|
|
5
|
+
* `glass` variation is byte-for-byte the shared dark-glass panel used across the
|
|
6
|
+
* built-in HUD widgets; `plate` is a heavier opaque plate; `retro` is a hard
|
|
7
|
+
* black-outlined frame. `shape` overrides the corner radius.
|
|
8
|
+
*
|
|
9
|
+
* @internal helper behind {@link HudFrame} (and the built-in HUD `PANEL`); widgets normally use `HudFrame`.
|
|
10
|
+
*/
|
|
11
|
+
export function hudFrameStyle(variation = "glass", shape = "rounded") {
|
|
12
|
+
const base = variation === "themed"
|
|
13
|
+
? {
|
|
14
|
+
background: "var(--jg-frame-bg, rgba(10,12,16,0.62))",
|
|
15
|
+
border: "var(--jg-frame-border, 1px solid rgba(255,255,255,0.10))",
|
|
16
|
+
borderRadius: "var(--jg-frame-radius, 10px)",
|
|
17
|
+
boxShadow: "var(--jg-frame-glow, 0 4px 16px rgba(0,0,0,0.35))",
|
|
18
|
+
color: "var(--jg-bar-text, #f4f6fb)",
|
|
19
|
+
fontFamily: "ui-sans-serif, system-ui, -apple-system, sans-serif",
|
|
20
|
+
}
|
|
21
|
+
: variation === "retro"
|
|
22
|
+
? {
|
|
23
|
+
border: "2px solid #000",
|
|
24
|
+
borderRadius: 4,
|
|
25
|
+
boxShadow: "4px 4px 0 #000",
|
|
26
|
+
color: "#fff",
|
|
27
|
+
}
|
|
28
|
+
: variation === "plate"
|
|
29
|
+
? {
|
|
30
|
+
background: "rgba(16,18,24,0.92)",
|
|
31
|
+
border: "1px solid rgba(255,255,255,0.10)",
|
|
32
|
+
borderRadius: 10,
|
|
33
|
+
color: "#f4f6fb",
|
|
34
|
+
fontFamily: "ui-sans-serif, system-ui, -apple-system, sans-serif",
|
|
35
|
+
boxShadow: "0 6px 20px rgba(0,0,0,0.45)",
|
|
36
|
+
}
|
|
37
|
+
: {
|
|
38
|
+
background: "rgba(10,12,16,0.62)",
|
|
39
|
+
backdropFilter: "blur(6px)",
|
|
40
|
+
WebkitBackdropFilter: "blur(6px)",
|
|
41
|
+
border: "1px solid rgba(255,255,255,0.10)",
|
|
42
|
+
borderRadius: 10,
|
|
43
|
+
color: "#f4f6fb",
|
|
44
|
+
fontFamily: "ui-sans-serif, system-ui, -apple-system, sans-serif",
|
|
45
|
+
boxShadow: "0 4px 16px rgba(0,0,0,0.35)",
|
|
46
|
+
};
|
|
47
|
+
if (shape === "circle")
|
|
48
|
+
return { ...base, borderRadius: 9999 };
|
|
49
|
+
if (shape === "square")
|
|
50
|
+
return { ...base, borderRadius: 0 };
|
|
51
|
+
return base;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Optional framed panel primitive — a single `<div data-hud-frame>` with a `glass`,
|
|
55
|
+
* `plate`, `retro`, or `themed` skin, optional `title`/`aside` header, and caller
|
|
56
|
+
* `style` merged last. Shared building block for demos and token-themed widgets;
|
|
57
|
+
* games own product chrome and art direction (see AGENTS.md).
|
|
58
|
+
*
|
|
59
|
+
* @capability hud-frame optional framed panel skins (glass/plate/retro/themed) — games own product chrome
|
|
60
|
+
*/
|
|
61
|
+
export function HudFrame({ variation = "glass", shape = "rounded", title, aside, padding, width, interactive, className, style, children, }) {
|
|
62
|
+
const hasHeader = title !== undefined || aside !== undefined;
|
|
63
|
+
return (_jsxs("div", { className: className, "data-hud-frame": true, style: {
|
|
64
|
+
...hudFrameStyle(variation, shape),
|
|
65
|
+
padding,
|
|
66
|
+
width,
|
|
67
|
+
pointerEvents: interactive === true ? "auto" : undefined,
|
|
68
|
+
...style,
|
|
69
|
+
}, children: [hasHeader ? (_jsxs("div", { "data-hud-frame-header": true, style: {
|
|
70
|
+
display: "flex",
|
|
71
|
+
justifyContent: "space-between",
|
|
72
|
+
alignItems: "center",
|
|
73
|
+
gap: 8,
|
|
74
|
+
marginBottom: 6,
|
|
75
|
+
fontSize: 10,
|
|
76
|
+
letterSpacing: 1.4,
|
|
77
|
+
textTransform: "uppercase",
|
|
78
|
+
opacity: 0.75,
|
|
79
|
+
}, children: [_jsx("span", { children: title }), aside !== undefined ? _jsx("span", { children: aside }) : null] })) : null, children] }));
|
|
80
|
+
}
|