@jgengine/react 0.11.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/actionHud.d.ts +5 -5
  3. package/dist/actionHud.js +14 -12
  4. package/dist/bars.d.ts +103 -0
  5. package/dist/bars.js +180 -0
  6. package/dist/barsPreview.d.ts +4 -0
  7. package/dist/barsPreview.js +47 -0
  8. package/dist/components.d.ts +8 -1
  9. package/dist/components.js +8 -1
  10. package/dist/hooks.d.ts +20 -0
  11. package/dist/hooks.js +38 -0
  12. package/dist/hud.d.ts +21 -4
  13. package/dist/hud.js +38 -27
  14. package/dist/hudFrame.d.ts +47 -0
  15. package/dist/hudFrame.js +80 -0
  16. package/dist/hudLayout.d.ts +13 -1
  17. package/dist/hudLayout.js +18 -12
  18. package/dist/hudLayoutPersist.d.ts +23 -0
  19. package/dist/hudLayoutPersist.js +19 -0
  20. package/dist/hudTheme.d.ts +76 -0
  21. package/dist/hudTheme.js +156 -0
  22. package/dist/hudThemePreview.d.ts +4 -0
  23. package/dist/hudThemePreview.js +37 -0
  24. package/dist/iconTreatment.d.ts +50 -0
  25. package/dist/iconTreatment.js +84 -0
  26. package/dist/iconsPreview.d.ts +4 -0
  27. package/dist/iconsPreview.js +29 -0
  28. package/dist/index.d.ts +9 -0
  29. package/dist/index.js +9 -0
  30. package/dist/map.d.ts +72 -10
  31. package/dist/map.js +84 -15
  32. package/dist/selectionHud.d.ts +11 -5
  33. package/dist/selectionHud.js +11 -5
  34. package/dist/slots.d.ts +44 -0
  35. package/dist/slots.js +37 -0
  36. package/package.json +2 -2
  37. package/skills/jgengine/SKILL.md +7 -4
  38. package/skills/jgengine/api.md +172 -79
  39. package/skills/jgengine/capabilities.md +104 -2
  40. package/skills/jgengine/recipes/minimal-game.md +88 -0
  41. package/skills/jgengine-ui/SKILL.md +15 -10
  42. package/skills/jgengine-ui/api.md +202 -47
  43. package/skills/jgengine-ui/capabilities.md +71 -2
  44. package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
  45. package/skills/jgengine-ui/reference.md +20 -7
  46. package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
  47. package/skills/jgengine/reference-cartridge.md +0 -72
package/CHANGELOG.md CHANGED
@@ -12,6 +12,32 @@ Agents building on the published SDK can also read this programmatically:
12
12
  same data as typed values, so an updater can diff its installed version against
13
13
  the latest and surface the migration steps.
14
14
 
15
+ ## 0.12.0
16
+
17
+ ### Migrate
18
+
19
+ - **Author games through `defineGame` from `@jgengine/shell`** — it is the single public authoring entry (and carries the define-game capability tag). Core's constructor was renamed `defineGame` → `defineGameDefinition` (`@jgengine/core/game/defineGame`); only hosts and tooling call it. If you imported `defineGame` from core, switch the import to `@jgengine/shell`. `GamePlayer`/`GamePlayerShell` are `@internal` now — `GameHost` is the documented mount, and it owns the editor summon (`?mode=editor`, F2+E, dev save endpoint), so delete any per-game editor bootstrap.
20
+ - **Declare your world with `world()`** (`@jgengine/core/world/place`): `world({ id, ground: { mode, size, surface? }, physics? })` with `flat` / `round` / `voxel` / `board` ground. New scaffolds emit a thin infinite flat place instead of the old 96 m meadow. `environment()` still works but is demoted to the legacy consumption path for editor-written scene data — **nothing required** for existing games; new worlds should use `world()` and author all dressing in the editor.
21
+ - Everything else is additive — stat pools, minimap marker sources, auction house, city/path generation, editor workspaces require no changes to existing games.
22
+
23
+ ### Added
24
+
25
+ - **World = place: `world()` API** (#1178) — a world is substrate + laws, not a coded diorama. `@jgengine/core/world/place` ships `world({ id, ground: { mode, size, surface?, generator? }, physics? })`; size is discriminated by mode (flat `{x,z}` with `Infinity` axes, round `{radius}`, voxel generator domain, board `{x,y}` cells — TS and runtime both reject mismatches). `ground.surface` carries matter/feel laws (metal vs slime vs felt) read by the same rule systems; per-place `physics` resolves over the game default; multiple worlds per game are first-class. The shell renders place substrates (`PlaceScene`) under the engine default sky; seeds derive from world id + save via `seedForPlace`.
26
+ - **`@jgengine/shell/gameKit`** — happy-path authoring surface: authoring, mount, stores, systems, authored-scene helpers, HUD primitives, seeded rng in one import.
27
+ - **Portable generic stat pools** — `@jgengine/core/stats/statPool`: bounded named pools (health, energy, heat, ammo…) with the entity-stats integration rebuilt on them; recipe in skill `jgengine-combat`.
28
+ - **Auction-house economy primitives** (#1086) — `@jgengine/core/economy/auctionBook` (timed bid auctions, escrowed bids, outbid refunds, optional buyout, anti-snipe close extension, seller settlement minus house cut) plus market price history.
29
+ - **Portable minimap marker sources** — core world seam feeding game minimaps; recipe in skill `jgengine-ui`.
30
+ - **Unified seed-driven path network + city generation v2** (#1103, #1106) — streets/blocks/buildings with grid-ness, curviness, branching and block-size dials; zone bands, weighted building-class mixes, massing pieces, balance presets, junction-centered clustering, road-derived block polygons and parcels. Shell renders realistic roads: real intersections, curbs, lane lines, tunnels, and roads riding bridge decks.
31
+ - **Physical fidelity** — mesh-accurate hitboxes (shots pass through holes in concave models, #1075), auto-fit entity/object colliders from measured model bounds (#1072), movement obstruction reading fitted boxes + compound-box openings (#1077).
32
+ - **Editor overhaul** (#1110) — materials / lighting / scripting / animation-timeline / multiplayer workspaces, command palette, minimap bake wired end-to-end (#1036), real offscreen GLB thumbnails in the content browser, hierarchy drag-drop + keyboard + context menus + per-object visibility/lock, multi-select inspector, console RPC, asset drag-place, ortho projection, pivot modes, play chrome.
33
+ - **Editor authoring pipeline** — author entity definitions and starter catalogs (#1012), author catalogs/schema fields in the Data tab (#1043), promote an authored scene folder into a runnable game (#1011), authored triggers promoted to a shared primitive with goal/win rules (#1013), asset import rewrites the game's `game/assets.ts` (#1042).
34
+ - **Skills** — research-backed `game-design` and `level-design` skills (#962); every-game-owns-custom-UI invariant codified (#1148).
35
+
36
+ ### Changed
37
+
38
+ - `npx jgengine create` scaffolds a thin infinite flat `world()` place; the editor `blankWorld` no longer pre-bakes seeded grass. Dressing belongs in `editor.scene.json`.
39
+ - Environment studio visual-quality pass — turf tufts, depth-aware water, relief soil, weathered poles; grass fields no longer rebuild geometry per frame (#1090).
40
+
15
41
  ## 0.11.0
16
42
 
17
43
  ### Migrate
@@ -120,12 +120,12 @@ export declare function ActionBarChrome({ model, layout, itemSize, gap, ariaLabe
120
120
  style?: CSSProperties;
121
121
  }): import("react").JSX.Element;
122
122
  /**
123
- * The batteries-included default: a skinned glass panel wrapping {@link useActionBar} +
124
- * {@link ActionBarChrome}. Pass `defs`, choose `grid`/`list`, and hand it `onActivate`. The renderer
125
- * and item chrome are still swappable via `renderItem`; a game that wants a different frame composes
126
- * the hook and chrome directly.
123
+ * Convenience composition of {@link useActionBar} + glass {@link ActionBarChrome} for demos and
124
+ * scaffolding — not a shipped game face. Pass `defs`, choose `grid`/`list`, and hand it `onActivate`.
125
+ * Games that own their UI compose the headless hook + custom chrome (or swap `renderItem`) instead
126
+ * of shipping this default panel.
127
127
  *
128
- * @capability action-bar drop-in cooldown/cost/hotkey action bar over the headless action model
128
+ * @capability action-bar optional skinned action-bar composition over the headless model — games own chrome
129
129
  */
130
130
  export declare function ActionBar({ defs, columns, layout, wrap, hotkeys, itemSize, ariaLabel, onActivate, renderItem, className, style, }: {
131
131
  defs: readonly ActionDef[];
package/dist/actionHud.js CHANGED
@@ -113,6 +113,8 @@ export function useActionBar(defs, options) {
113
113
  onKeyDown,
114
114
  };
115
115
  }
116
+ // Slot chrome reads the shared HudTheme tokens (#1034) with the built-in look as fallbacks, so a
117
+ // theme restyles action slots alongside the bars/frames and a bare game is byte-identical to before.
116
118
  const ITEM_BASE = {
117
119
  position: "relative",
118
120
  boxSizing: "border-box",
@@ -121,9 +123,9 @@ const ITEM_BASE = {
121
123
  alignItems: "center",
122
124
  justifyContent: "center",
123
125
  gap: 2,
124
- border: "1px solid rgba(255,255,255,0.14)",
125
- borderRadius: 8,
126
- background: "rgba(14,17,22,0.72)",
126
+ border: "var(--jg-slot-border, 1px solid rgba(255,255,255,0.14))",
127
+ borderRadius: "var(--jg-slot-radius, 8px)",
128
+ background: "var(--jg-slot-bg, rgba(14,17,22,0.72))",
127
129
  color: "#eef2f8",
128
130
  font: "600 12px/1.1 ui-sans-serif, system-ui, sans-serif",
129
131
  cursor: "pointer",
@@ -218,20 +220,20 @@ export function ActionBarChrome({ model, layout = "grid", itemSize = 52, gap = 6
218
220
  }
219
221
  const PANEL = {
220
222
  padding: 8,
221
- borderRadius: 12,
222
- background: "rgba(10,12,16,0.6)",
223
+ borderRadius: "var(--jg-frame-radius, 12px)",
224
+ background: "var(--jg-frame-bg, rgba(10,12,16,0.6))",
223
225
  backdropFilter: "blur(6px)",
224
226
  WebkitBackdropFilter: "blur(6px)",
225
- border: "1px solid rgba(255,255,255,0.1)",
226
- boxShadow: "0 4px 16px rgba(0,0,0,0.35)",
227
+ border: "var(--jg-frame-border, 1px solid rgba(255,255,255,0.1))",
228
+ boxShadow: "var(--jg-frame-glow, 0 4px 16px rgba(0,0,0,0.35))",
227
229
  };
228
230
  /**
229
- * The batteries-included default: a skinned glass panel wrapping {@link useActionBar} +
230
- * {@link ActionBarChrome}. Pass `defs`, choose `grid`/`list`, and hand it `onActivate`. The renderer
231
- * and item chrome are still swappable via `renderItem`; a game that wants a different frame composes
232
- * the hook and chrome directly.
231
+ * Convenience composition of {@link useActionBar} + glass {@link ActionBarChrome} for demos and
232
+ * scaffolding — not a shipped game face. Pass `defs`, choose `grid`/`list`, and hand it `onActivate`.
233
+ * Games that own their UI compose the headless hook + custom chrome (or swap `renderItem`) instead
234
+ * of shipping this default panel.
233
235
  *
234
- * @capability action-bar drop-in cooldown/cost/hotkey action bar over the headless action model
236
+ * @capability action-bar optional skinned action-bar composition over the headless model — games own chrome
235
237
  */
236
238
  export function ActionBar({ defs, columns, layout = "grid", wrap, hotkeys, itemSize, ariaLabel, onActivate, renderItem, className, style, }) {
237
239
  const model = useActionBar(defs, { columns, wrap, hotkeys, onActivate });
package/dist/bars.d.ts ADDED
@@ -0,0 +1,103 @@
1
+ import { type CSSProperties, type ReactNode } from "react";
2
+ /**
3
+ * Atomic, purpose-named vitals — one component per readout (`HealthBar`, `ShieldBar`, …), never a
4
+ * `tone`-switched umbrella and never a bundled combo. Every bar reads the same shared CSS custom
5
+ * properties (`--jg-health`, `--jg-shield`, `--jg-xp`, plus frame/shape tokens). Default token
6
+ * colors are scaffolding only: games own layout, framing, and art direction, and must set tokens
7
+ * (or replace the markup) so the HUD is unique to the pitch — the engine ships parts, not a face.
8
+ *
9
+ * Each bar accepts either an explicit `value`/`max` (pure, provider-free — for composition, tests,
10
+ * and preview fixtures) or binds to a live entity stat (`statId`/`entityId`, defaulting to the local
11
+ * player). It renders nothing until it has a value.
12
+ *
13
+ * @capability hud-vitals atomic purpose-named vitals bars (token-themed parts, not a finished HUD)
14
+ */
15
+ /** The shared vitals token contract — CSS custom properties every atomic bar reads. */
16
+ export interface BarTokens {
17
+ /** Fill color per readout. */
18
+ health?: string;
19
+ healthLow?: string;
20
+ mana?: string;
21
+ stamina?: string;
22
+ shield?: string;
23
+ xp?: string;
24
+ soul?: string;
25
+ ammo?: string;
26
+ boss?: string;
27
+ /** Frame + shape. */
28
+ track?: string;
29
+ frame?: string;
30
+ frameWidth?: string;
31
+ radius?: string;
32
+ height?: string;
33
+ text?: string;
34
+ bevel?: string;
35
+ }
36
+ /** Built-in default values for every vitals token — scaffolding/preview only, not a shipped look. */
37
+ export declare const DEFAULT_BAR_TOKENS: Required<BarTokens>;
38
+ /**
39
+ * A style object binding vitals tokens to CSS custom properties — spread onto any HUD ancestor to
40
+ * restyle every atomic bar under it at once. Only the keys you pass are set; the rest fall back to
41
+ * {@link DEFAULT_BAR_TOKENS} inside the bars. This is the single global-theming seam.
42
+ * @capability hud-vitals bind shared vitals CSS tokens onto a HUD subtree
43
+ */
44
+ export declare function barTokens(tokens: BarTokens): CSSProperties;
45
+ /** Trough shape language — `rect` (default), `pill` (fully round), `skew` (parallelogram, upright
46
+ * label), or `chamfer` (cut corners). Matches a game's skin without hand-rolling a local bar. */
47
+ export type BarShape = "rect" | "pill" | "skew" | "chamfer";
48
+ /** Shared props for every atomic bar — bound to a stat (`statId`/`entityId`) or explicit (`value`). */
49
+ export interface AtomicBarProps {
50
+ /** Trough shape language. Default `rect`. */
51
+ shape?: BarShape;
52
+ /** Explicit fill color, overriding the token (and the low-threshold swap) — for per-instance
53
+ * colors a token can't express (e.g. a class-colored resource bar). Caller overrides win. */
54
+ fill?: string;
55
+ /** Explicit current value; when set the bar is pure and needs no game provider. */
56
+ value?: number;
57
+ /** Explicit max (default 100 when `value` is set without one). */
58
+ max?: number;
59
+ /** Explicit min (default 0). */
60
+ min?: number;
61
+ /** Stat id to bind when `value` is omitted. */
62
+ statId?: string;
63
+ /** Entity to read the stat off; defaults to the local player. */
64
+ entityId?: string;
65
+ /** Uppercase label rendered at the start of the trough. */
66
+ label?: string;
67
+ /** Show the centered `current / max` readout. Default true. */
68
+ showValue?: boolean;
69
+ /** Trough width in px (or any CSS length). Default 200. */
70
+ width?: number | string;
71
+ /** Divide the trough into N segments with tick dividers (hearts/ammo/evo style). */
72
+ segments?: number;
73
+ /** A ReactNode anchored at the start — portrait / orb / icon. Composes, never bundled. */
74
+ endCap?: ReactNode;
75
+ style?: CSSProperties;
76
+ className?: string;
77
+ }
78
+ /** Player/enemy health — danger-flashes to `--jg-health-low` below a quarter full. */
79
+ export declare const HealthBar: (props: AtomicBarProps) => import("react").JSX.Element;
80
+ /** Absorb/armor shield overlay. */
81
+ export declare const ShieldBar: (props: AtomicBarProps) => import("react").JSX.Element;
82
+ /** Spell/energy resource pool. */
83
+ export declare const ManaBar: (props: AtomicBarProps) => import("react").JSX.Element;
84
+ /** Sprint/action stamina pool. */
85
+ export declare const StaminaBar: (props: AtomicBarProps) => import("react").JSX.Element;
86
+ /** Level progress / experience. */
87
+ export declare const ExperienceBar: (props: AtomicBarProps) => import("react").JSX.Element;
88
+ /** Resource-charge / soul-box meter — segmented by default. */
89
+ export declare const SoulBar: (props: AtomicBarProps) => import("react").JSX.Element;
90
+ /** A wide encounter/boss health bar with the boss name at the start. */
91
+ export declare function BossBar(props: AtomicBarProps & {
92
+ name?: string;
93
+ }): import("react").JSX.Element;
94
+ /** Loaded-vs-reserve ammunition counter — a segmented magazine plus a reserve tally. */
95
+ export declare function AmmoCounter({ loaded, reserve, magazine, width, style, className, }: {
96
+ loaded: number;
97
+ reserve?: number;
98
+ /** Magazine capacity; drives the segment count and fill fraction. Default = `loaded`. */
99
+ magazine?: number;
100
+ width?: number | string;
101
+ style?: CSSProperties;
102
+ className?: string;
103
+ }): import("react").JSX.Element;
package/dist/bars.js ADDED
@@ -0,0 +1,180 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import {} from "react";
3
+ import { useEntityStat, useGameStore, localPlayerEntity } from "./hooks.js";
4
+ const TOKEN_VAR = {
5
+ health: "--jg-health",
6
+ healthLow: "--jg-health-low",
7
+ mana: "--jg-mana",
8
+ stamina: "--jg-stamina",
9
+ shield: "--jg-shield",
10
+ xp: "--jg-xp",
11
+ soul: "--jg-soul",
12
+ ammo: "--jg-ammo",
13
+ boss: "--jg-boss",
14
+ track: "--jg-bar-track",
15
+ frame: "--jg-bar-frame",
16
+ frameWidth: "--jg-bar-frame-width",
17
+ radius: "--jg-bar-radius",
18
+ height: "--jg-bar-height",
19
+ text: "--jg-bar-text",
20
+ bevel: "--jg-bar-bevel",
21
+ };
22
+ /** Built-in default values for every vitals token — scaffolding/preview only, not a shipped look. */
23
+ export const DEFAULT_BAR_TOKENS = {
24
+ health: "#e5484d",
25
+ healthLow: "#b21e23",
26
+ mana: "#3b82f6",
27
+ stamina: "#84cc16",
28
+ shield: "#22d3ee",
29
+ xp: "#a855f7",
30
+ soul: "#c084fc",
31
+ ammo: "#f8b84e",
32
+ boss: "#ef4444",
33
+ track: "rgba(0,0,0,0.55)",
34
+ frame: "rgba(0,0,0,0.82)",
35
+ frameWidth: "2px",
36
+ radius: "4px",
37
+ height: "18px",
38
+ text: "#f5f7fa",
39
+ bevel: "inset 0 2px 4px rgba(0,0,0,0.55), inset 0 -1px 2px rgba(255,255,255,0.08)",
40
+ };
41
+ /**
42
+ * A style object binding vitals tokens to CSS custom properties — spread onto any HUD ancestor to
43
+ * restyle every atomic bar under it at once. Only the keys you pass are set; the rest fall back to
44
+ * {@link DEFAULT_BAR_TOKENS} inside the bars. This is the single global-theming seam.
45
+ * @capability hud-vitals bind shared vitals CSS tokens onto a HUD subtree
46
+ */
47
+ export function barTokens(tokens) {
48
+ const style = {};
49
+ for (const [key, value] of Object.entries(tokens)) {
50
+ if (value !== undefined)
51
+ style[TOKEN_VAR[key]] = value;
52
+ }
53
+ return style;
54
+ }
55
+ function clampFraction(current, min, max) {
56
+ const range = max - min;
57
+ if (range <= 0)
58
+ return 0;
59
+ return Math.max(0, Math.min(1, (current - min) / range));
60
+ }
61
+ function cssVar(key) {
62
+ return `var(${TOKEN_VAR[key]}, ${DEFAULT_BAR_TOKENS[key]})`;
63
+ }
64
+ const SKEW_DEG = 8;
65
+ /** Shape → trough clip/radius/transform, plus the counter-transform that keeps the label upright. */
66
+ function shapeStyles(shape) {
67
+ switch (shape) {
68
+ case "pill":
69
+ return { trough: { borderRadius: 9999 }, label: {} };
70
+ case "skew":
71
+ return { trough: { borderRadius: 2, transform: `skewX(-${SKEW_DEG}deg)` }, label: { transform: `skewX(${SKEW_DEG}deg)` } };
72
+ case "chamfer":
73
+ return { trough: { borderRadius: 0, clipPath: "polygon(7px 0, 100% 0, calc(100% - 7px) 100%, 0 100%)" }, label: {} };
74
+ default:
75
+ return { trough: { borderRadius: cssVar("radius") }, label: {} };
76
+ }
77
+ }
78
+ /** The pure, hook-free bar renderer every atomic component funnels through. */
79
+ function BarView({ fraction, valueText, config, label, width = 200, segments, endCap, shape = "rect", fillOverride, style, className, }) {
80
+ const low = fillOverride === undefined && config.lowColorKey !== undefined && fraction <= (config.lowThreshold ?? 0.25);
81
+ const fill = fillOverride ?? cssVar(low ? config.lowColorKey : config.colorKey);
82
+ const shaped = shapeStyles(shape);
83
+ const trough = (_jsxs("div", { "data-bar": config.dataBar, "data-bar-shape": shape, "data-fill-fraction": fraction.toFixed(4), ...(low ? { "data-low": "" } : {}), style: {
84
+ position: "relative",
85
+ flex: endCap === undefined ? undefined : "1 1 auto",
86
+ width: endCap === undefined ? "100%" : undefined,
87
+ height: cssVar("height"),
88
+ border: `${cssVar("frameWidth")} solid ${cssVar("frame")}`,
89
+ background: cssVar("track"),
90
+ boxShadow: cssVar("bevel"),
91
+ overflow: "hidden",
92
+ boxSizing: "border-box",
93
+ ...shaped.trough,
94
+ }, children: [_jsx("div", { style: {
95
+ position: "absolute",
96
+ inset: 0,
97
+ width: `${fraction * 100}%`,
98
+ background: fill,
99
+ transition: "width 160ms ease",
100
+ } }), _jsx("div", { style: {
101
+ position: "absolute",
102
+ inset: 0,
103
+ width: `${fraction * 100}%`,
104
+ background: "linear-gradient(180deg, rgba(255,255,255,0.28), rgba(255,255,255,0) 48%)",
105
+ pointerEvents: "none",
106
+ } }), segments !== undefined && segments > 1
107
+ ? Array.from({ length: segments - 1 }, (_, index) => (_jsx("div", { style: {
108
+ position: "absolute",
109
+ top: 0,
110
+ bottom: 0,
111
+ left: `${((index + 1) / segments) * 100}%`,
112
+ width: 1,
113
+ background: "rgba(0,0,0,0.45)",
114
+ } }, index)))
115
+ : null, _jsxs("div", { style: {
116
+ position: "absolute",
117
+ inset: 0,
118
+ display: "flex",
119
+ alignItems: "center",
120
+ justifyContent: label === undefined ? "center" : "space-between",
121
+ padding: "0 6px",
122
+ fontSize: 11,
123
+ fontWeight: 700,
124
+ letterSpacing: 0.3,
125
+ color: cssVar("text"),
126
+ textShadow: "0 1px 2px rgba(0,0,0,0.85)",
127
+ pointerEvents: "none",
128
+ }, children: [label !== undefined ? _jsx("span", { style: { textTransform: "uppercase", opacity: 0.85, ...shaped.label }, children: label }) : null, valueText !== null ? _jsx("span", { style: { fontVariantNumeric: "tabular-nums", ...shaped.label }, children: valueText }) : null] })] }));
129
+ if (endCap === undefined) {
130
+ return (_jsx("div", { className: className, style: { display: "inline-block", width, ...style }, children: trough }));
131
+ }
132
+ return (_jsxs("div", { className: className, style: { display: "flex", alignItems: "center", gap: 6, width, ...style }, children: [endCap, trough] }));
133
+ }
134
+ function valueReadout(current, max, show) {
135
+ if (!show)
136
+ return null;
137
+ return `${Math.round(current)} / ${Math.round(max)}`;
138
+ }
139
+ /** Renders an atomic bar from an explicit value — pure, no game provider needed. */
140
+ function StaticBar({ props, config }) {
141
+ const min = props.min ?? 0;
142
+ const max = props.max ?? 100;
143
+ const current = props.value ?? 0;
144
+ return (_jsx(BarView, { fraction: clampFraction(current, min, max), valueText: valueReadout(current, max, props.showValue ?? true), config: config, label: props.label, width: props.width, segments: props.segments, endCap: props.endCap, shape: props.shape, fillOverride: props.fill, style: props.style, className: props.className }));
145
+ }
146
+ /** Renders an atomic bar bound to a live entity stat. */
147
+ function BoundBar({ props, config, defaultStatId }) {
148
+ const id = useGameStore((ctx) => props.entityId ?? localPlayerEntity(ctx)?.id ?? null);
149
+ const stat = useEntityStat(id ?? "", props.statId ?? defaultStatId);
150
+ if (id === null || stat === null)
151
+ return null;
152
+ return (_jsx(BarView, { fraction: clampFraction(stat.current, stat.min, stat.max), valueText: valueReadout(stat.current, stat.max, props.showValue ?? true), config: config, label: props.label, width: props.width, segments: props.segments, endCap: props.endCap, shape: props.shape, fillOverride: props.fill, style: props.style, className: props.className }));
153
+ }
154
+ function atomicBar(defaultStatId, config) {
155
+ return function AtomicBar(props) {
156
+ return props.value !== undefined ? (_jsx(StaticBar, { props: props, config: config })) : (_jsx(BoundBar, { props: props, config: config, defaultStatId: defaultStatId }));
157
+ };
158
+ }
159
+ /** Player/enemy health — danger-flashes to `--jg-health-low` below a quarter full. */
160
+ export const HealthBar = atomicBar("health", { colorKey: "health", lowColorKey: "healthLow", lowThreshold: 0.25, dataBar: "health" });
161
+ /** Absorb/armor shield overlay. */
162
+ export const ShieldBar = atomicBar("shield", { colorKey: "shield", dataBar: "shield" });
163
+ /** Spell/energy resource pool. */
164
+ export const ManaBar = atomicBar("mana", { colorKey: "mana", dataBar: "mana" });
165
+ /** Sprint/action stamina pool. */
166
+ export const StaminaBar = atomicBar("stamina", { colorKey: "stamina", dataBar: "stamina" });
167
+ /** Level progress / experience. */
168
+ export const ExperienceBar = atomicBar("xp", { colorKey: "xp", dataBar: "xp" });
169
+ /** Resource-charge / soul-box meter — segmented by default. */
170
+ export const SoulBar = atomicBar("soul", { colorKey: "soul", dataBar: "soul" });
171
+ /** A wide encounter/boss health bar with the boss name at the start. */
172
+ export function BossBar(props) {
173
+ const merged = { width: 420, ...props, label: props.label ?? props.name };
174
+ return props.value !== undefined ? (_jsx(StaticBar, { props: merged, config: { colorKey: "boss", dataBar: "boss" } })) : (_jsx(BoundBar, { props: merged, config: { colorKey: "boss", dataBar: "boss" }, defaultStatId: "health" }));
175
+ }
176
+ /** Loaded-vs-reserve ammunition counter — a segmented magazine plus a reserve tally. */
177
+ export function AmmoCounter({ loaded, reserve, magazine, width = 160, style, className, }) {
178
+ const cap = magazine ?? Math.max(loaded, 1);
179
+ return (_jsxs("div", { className: className, "data-bar": "ammo", style: { display: "flex", alignItems: "center", gap: 8, width, color: cssVar("text"), ...style }, children: [_jsx("div", { style: { flex: "1 1 auto", minWidth: 0 }, children: _jsx(BarView, { fraction: clampFraction(loaded, 0, cap), valueText: `${loaded}`, config: { colorKey: "ammo", dataBar: "ammo-mag" }, width: "100%", segments: cap <= 20 ? cap : undefined, style: { display: "block" } }) }), reserve !== undefined ? (_jsxs("span", { style: { fontSize: 12, fontWeight: 700, fontVariantNumeric: "tabular-nums", opacity: 0.8, textShadow: "0 1px 2px rgba(0,0,0,0.8)" }, children: ["/", reserve] })) : null] }));
180
+ }
@@ -0,0 +1,4 @@
1
+ /** Renders the atomic bar matrix twice under different token blocks to prove global re-theming. */
2
+ export declare function BarsPreview({ className }: {
3
+ className?: string;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import {} from "react";
3
+ import { AmmoCounter, barTokens, BossBar, ExperienceBar, HealthBar, ManaBar, ShieldBar, SoulBar, StaminaBar, } from "./bars.js";
4
+ /**
5
+ * A deterministic preview of every atomic vitals bar plus a composed example — the fixture behind
6
+ * #1033's visual evidence. All values are static (no game provider), so it renders identically every
7
+ * time for screenshots. Rendering the same set under two different {@link BarTokens} blocks proves
8
+ * that global theming survives the umbrella's decomposition: one token change restyles every bar.
9
+ */
10
+ const PANEL = {
11
+ display: "flex",
12
+ flexDirection: "column",
13
+ gap: 8,
14
+ padding: 14,
15
+ borderRadius: 10,
16
+ background: "rgba(10,12,16,0.72)",
17
+ border: "1px solid rgba(255,255,255,0.08)",
18
+ };
19
+ function Portrait({ color }) {
20
+ return (_jsx("div", { style: {
21
+ width: 34,
22
+ height: 34,
23
+ borderRadius: 6,
24
+ background: `radial-gradient(circle at 40% 30%, ${color}, rgba(0,0,0,0.6))`,
25
+ border: "2px solid rgba(0,0,0,0.75)",
26
+ flex: "0 0 auto",
27
+ } }));
28
+ }
29
+ function AtomicMatrix({ label, tokens }) {
30
+ return (_jsxs("div", { style: { ...PANEL, ...(tokens === undefined ? {} : barTokens(tokens)) }, children: [_jsx("span", { style: { fontSize: 11, fontWeight: 700, letterSpacing: 0.6, textTransform: "uppercase", color: "rgba(255,255,255,0.6)" }, children: label }), _jsx(HealthBar, { value: 82, max: 100, label: "HP" }), _jsx(HealthBar, { value: 14, max: 100, label: "HP" }), _jsx(ShieldBar, { value: 40, max: 60, label: "SH" }), _jsx(ManaBar, { value: 55, max: 80, label: "MP" }), _jsx(StaminaBar, { value: 90, max: 100, label: "ST" }), _jsx(ExperienceBar, { value: 3, max: 10, segments: 10, showValue: false, label: "XP" }), _jsx(SoulBar, { value: 3, max: 5, segments: 5, showValue: false, label: "SOUL" }), _jsx(AmmoCounter, { loaded: 17, reserve: 90, magazine: 30 }), _jsx(BossBar, { value: 640, max: 1000, name: "Ancient Wyrm", width: "100%" }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 4 }, children: [_jsx(Portrait, { color: tokens?.health ?? "#e5484d" }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 4, flex: "1 1 auto" }, children: [_jsx(HealthBar, { value: 82, max: 100, width: "100%", showValue: false }), _jsx(ShieldBar, { value: 40, max: 60, width: "100%", showValue: false })] })] })] }));
31
+ }
32
+ /** Renders the atomic bar matrix twice under different token blocks to prove global re-theming. */
33
+ export function BarsPreview({ className }) {
34
+ return (_jsxs("div", { "data-bars-preview": true, className: className, style: { display: "flex", gap: 16, flexWrap: "wrap", padding: 20, fontFamily: "system-ui, sans-serif", color: "#f5f7fa" }, children: [_jsx(AtomicMatrix, { label: "default tokens" }), _jsx(AtomicMatrix, { label: "restyled tokens", tokens: {
35
+ health: "#2fbf71",
36
+ healthLow: "#e0a80d",
37
+ shield: "#f2c14e",
38
+ mana: "#6c5ce7",
39
+ stamina: "#00b894",
40
+ xp: "#0984e3",
41
+ soul: "#e17055",
42
+ ammo: "#fdcb6e",
43
+ frame: "#0a0a0a",
44
+ radius: "2px",
45
+ height: "20px",
46
+ } })] }));
47
+ }
@@ -12,7 +12,14 @@ export declare function SlotGrid({ inventoryId, className, renderSlot, }: {
12
12
  className?: string;
13
13
  renderSlot?: (slot: InventorySlot, index: number) => ReactNode;
14
14
  }): import("react").JSX.Element;
15
- export declare function HealthBar({ instanceId, statId, className, fillClassName, }: {
15
+ /**
16
+ * A headless, unstyled stat meter — an accessible `role="progressbar"` you paint entirely through
17
+ * `className`/`fillClassName`. This is the class-hook primitive for teams supplying their own CSS;
18
+ * for a batteries-included themed bar use the atomic, token-styled components in
19
+ * `@jgengine/react/bars` (`HealthBar`, `ShieldBar`, …). Renamed from `HealthBar` in #1033 so the
20
+ * purpose-named atomic bar owns that name and no two components share it.
21
+ */
22
+ export declare function StatMeter({ instanceId, statId, className, fillClassName, }: {
16
23
  instanceId: string;
17
24
  statId: string;
18
25
  className?: string;
@@ -17,7 +17,14 @@ export function SlotGrid({ inventoryId, className, renderSlot, }) {
17
17
  ? `${slot.itemId} x${slot.count}`
18
18
  : null }, index))) }));
19
19
  }
20
- export function HealthBar({ instanceId, statId, className, fillClassName, }) {
20
+ /**
21
+ * A headless, unstyled stat meter — an accessible `role="progressbar"` you paint entirely through
22
+ * `className`/`fillClassName`. This is the class-hook primitive for teams supplying their own CSS;
23
+ * for a batteries-included themed bar use the atomic, token-styled components in
24
+ * `@jgengine/react/bars` (`HealthBar`, `ShieldBar`, …). Renamed from `HealthBar` in #1033 so the
25
+ * purpose-named atomic bar owns that name and no two components share it.
26
+ */
27
+ export function StatMeter({ instanceId, statId, className, fillClassName, }) {
21
28
  const stat = useEntityStat(instanceId, statId);
22
29
  if (stat === null)
23
30
  return null;
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";
@@ -123,6 +125,24 @@ export interface EventMeterView {
123
125
  * @capability event-meter-hud render a core event/heat meter's live value, fraction, tier, and ready state in a HUD gauge
124
126
  */
125
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;
126
146
  type HeldKeyEventTarget = Pick<Window, "addEventListener" | "removeEventListener">;
127
147
  export declare function createHeldKeyTracker(target: HeldKeyEventTarget): {
128
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";
@@ -275,6 +276,43 @@ export function useEventMeter(meter, options) {
275
276
  previous.current = view;
276
277
  return view;
277
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
+ }
278
316
  export function createHeldKeyTracker(target) {
279
317
  const held = new Set();
280
318
  const onKeyDown = (event) => held.add(event.code);
package/dist/hud.d.ts CHANGED
@@ -7,6 +7,13 @@ export type StatTone = keyof typeof TONES;
7
7
  * A polished stat bar (health/mana/stamina/shield/xp) — a rounded, glassy meter with a tone-colored
8
8
  * fill and an optional value readout. Reads `statId` off `entityId` (defaults to the local player).
9
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
10
17
  */
11
18
  export declare function StatBar({ statId, entityId, tone, label, showValue, width, icon, style, className, }: {
12
19
  statId?: string;
@@ -20,14 +27,20 @@ export declare function StatBar({ statId, entityId, tone, label, showValue, widt
20
27
  className?: string;
21
28
  }): import("react").JSX.Element | null;
22
29
  /**
23
- * A numbered hotbar bound to an inventory — glassy slots, an active-slot highlight, item id + count,
24
- * and a keycap per slot. Place it and pass the `inventoryId`; `activeSlot` highlights the equipped one.
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
25
36
  */
26
- export declare function Hotbar({ inventoryId, activeSlot, keys, slotSize, style, className, }: {
37
+ export declare function Hotbar({ inventoryId, activeSlot, keys, slotSize, itemIcon, style, className, }: {
27
38
  inventoryId: string;
28
39
  activeSlot?: number;
29
40
  keys?: readonly string[];
30
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;
31
44
  style?: CSSProperties;
32
45
  className?: string;
33
46
  }): import("react").JSX.Element;
@@ -49,6 +62,8 @@ export declare function Speedometer({ entityId, scale, unit, max, size, style, c
49
62
  * A time-of-day clock reading the sim calendar — `Day N · HH:MM`, 24h or 12h. `controls` adds
50
63
  * pause + the game's speed multipliers as clickable pills (the "fast-forward" bar), off by default so
51
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
52
67
  */
53
68
  export declare function Clock({ format, showDay, controls, style, className, }: {
54
69
  format?: "24h" | "12h";
@@ -68,7 +83,9 @@ export declare function WaveBanner({ wave, label, subtitle, style, className, }:
68
83
  style?: CSSProperties;
69
84
  className?: string;
70
85
  }): import("react").JSX.Element;
71
- /** A currency counter — an icon (emoji/char, default a coin) plus the live amount for `currencyId`. */
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
+ */
72
89
  export declare function Coins({ currencyId, icon, style, className, }: {
73
90
  currencyId: string;
74
91
  icon?: ReactNode;