@jgengine/react 0.11.0 → 0.13.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 +52 -0
- package/dist/actionHud.d.ts +5 -5
- package/dist/actionHud.js +14 -12
- 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/characterSheet.d.ts +120 -0
- package/dist/characterSheet.js +134 -0
- package/dist/components.d.ts +8 -1
- package/dist/components.js +8 -1
- package/dist/hooks.d.ts +31 -0
- package/dist/hooks.js +53 -0
- package/dist/hud.d.ts +21 -4
- package/dist/hud.js +38 -27
- package/dist/hudFrame.d.ts +47 -0
- package/dist/hudFrame.js +80 -0
- package/dist/hudLayout.d.ts +13 -1
- package/dist/hudLayout.js +18 -12
- 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 +12 -0
- package/dist/index.js +12 -0
- package/dist/inventoryGrid.d.ts +20 -0
- package/dist/inventoryGrid.js +135 -0
- package/dist/map.d.ts +72 -10
- package/dist/map.js +84 -15
- package/dist/panels.d.ts +107 -0
- package/dist/panels.js +215 -0
- package/dist/selectionHud.d.ts +11 -5
- package/dist/selectionHud.js +11 -5
- package/dist/slots.d.ts +47 -0
- package/dist/slots.js +37 -0
- package/package.json +2 -2
- package/skills/jgengine/SKILL.md +8 -4
- package/skills/jgengine/api.md +182 -81
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +17 -10
- package/skills/jgengine-ui/api.md +316 -51
- package/skills/jgengine-ui/capabilities.md +135 -2
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +59 -9
- package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
- package/skills/jgengine/reference-cartridge.md +0 -72
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,58 @@ 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.13.0
|
|
16
|
+
|
|
17
|
+
### Migrate
|
|
18
|
+
|
|
19
|
+
- **Nothing required** — this release is additive fixes, defaults, and new drop-in primitives. Bump `@jgengine/*` pins to `^0.13.0` (CLI `jgengine` is 0.10.0; `@jgengine/github` unchanged at 0.1.0) and rebuild.
|
|
20
|
+
- Two defaults changed in the player's favor, both overridable: **offline games now persist to localStorage automatically** (#1207) and **rigged catalog assets animate by default** via clip metadata + semantic clip roles (#1209). If a game relied on rigged models staying frozen or on saves being memory-only, opt out explicitly; otherwise you get better behavior for free.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **Drop-in inventory grid, window manager, and character sheet** (#1220) — `@jgengine/react` ships `InventoryGrid` (real drag/stack/split), a toggleable-window system (`panels`: bag, character sheet, spellbook-style windows with `@jgengine/core/ui/panelModel` state), and a `CharacterSheet` paperdoll, all HudTheme-skinnable — wire in a line or two instead of re-deriving from raw divs.
|
|
25
|
+
- **Serializable runtime snapshots** (#1179) — capture/restore of live runtime state, including magazine state (`@jgengine/core/combat/magazine`) and stat modifiers, for saves and replication.
|
|
26
|
+
- **Portable weapon plumbing** (#1211) — `@jgengine/core/combat/weaponFire` composes trigger → magazine → spread → projectile/hitscan as data, no game-local glue.
|
|
27
|
+
- **Portable leveling** (#1206) and **portable damage-pool access** (#1208) — progression and damage pools exposed as genre-agnostic seams on the game context.
|
|
28
|
+
- **Named combat VFX presets** (#1204) — `@jgengine/core/combat/vfxPresets`: a visible attack is one line.
|
|
29
|
+
- **Runtime-measured render-bounds hitboxes** (#1227) — colliders wrap what the shell actually mounts, not the raw asset bounds.
|
|
30
|
+
- **Character motion feel** — procedural part motion for rig-less part-composed characters (#1222) plus squash & stretch and death splat (#1229).
|
|
31
|
+
- **City fabric** (#1191) — `streetGenerator` + `cityGenerator`, editor bake verb, street-aware lots, road junctions, grounding, and a web playground.
|
|
32
|
+
- **`jgengine recipe` CLI** (#1214, #1231) — vetted wired compositions, compile-pinned against the SDK, listed and copied from the CLI.
|
|
33
|
+
- **Editor/debug modes mirror to the URL** with a proper editor exit (#1203); game template ships a dependency-free WebGL screenshot script (#1202).
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Offline games persist to localStorage by default (#1207); rigged assets animate by default (#1209).
|
|
38
|
+
- RTS camera pan inversion fixed (#1228); sprite raycast warning silenced and duplicate nameplate health bars removed (#1194); scaffolded F2+E editor Tailwind `@source` renders its theme (#1195).
|
|
39
|
+
- Outside-game DX: stat-pool no-op stability, camera-transparent decor, cleaner install/version output, headless UI-intent seam (#1223); new-game briefs direct engine bugs/gaps upstream (#1200).
|
|
40
|
+
|
|
41
|
+
## 0.12.0
|
|
42
|
+
|
|
43
|
+
### Migrate
|
|
44
|
+
|
|
45
|
+
- **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.
|
|
46
|
+
- **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.
|
|
47
|
+
- Everything else is additive — stat pools, minimap marker sources, auction house, city/path generation, editor workspaces require no changes to existing games.
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- **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`.
|
|
52
|
+
- **`@jgengine/shell/gameKit`** — happy-path authoring surface: authoring, mount, stores, systems, authored-scene helpers, HUD primitives, seeded rng in one import.
|
|
53
|
+
- **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`.
|
|
54
|
+
- **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.
|
|
55
|
+
- **Portable minimap marker sources** — core world seam feeding game minimaps; recipe in skill `jgengine-ui`.
|
|
56
|
+
- **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.
|
|
57
|
+
- **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).
|
|
58
|
+
- **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.
|
|
59
|
+
- **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).
|
|
60
|
+
- **Skills** — research-backed `game-design` and `level-design` skills (#962); every-game-owns-custom-UI invariant codified (#1148).
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- `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`.
|
|
65
|
+
- Environment studio visual-quality pass — turf tufts, depth-aware water, relief soil, weathered poles; grass fields no longer rebuild geometry per frame (#1090).
|
|
66
|
+
|
|
15
67
|
## 0.11.0
|
|
16
68
|
|
|
17
69
|
### Migrate
|
package/dist/actionHud.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
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
|
|
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,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
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import { type SlotItem } from "./slots.js";
|
|
3
|
+
import { type HudFrameVariation } from "./hudFrame.js";
|
|
4
|
+
/**
|
|
5
|
+
* Character-sheet / paperdoll composition (#1033 follow-up): a drop-in `<CharacterSheet>` that
|
|
6
|
+
* arranges caller-supplied equip slots as a paperdoll around a portrait and lists derived stats,
|
|
7
|
+
* all inside a {@link HudFrame}. It reuses the atomic slot rendering ({@link EquipmentSlots}) and the
|
|
8
|
+
* shared `--jg-slot-*` / `--jg-accent` HudTheme tokens (#1034) — the game owns which slots and which
|
|
9
|
+
* stats matter (both are props), the skin, and the window that toggles it open/closed. No genre list
|
|
10
|
+
* is baked in; {@link defaultEquipLayout} is a convenience arrangement the game can override wholesale.
|
|
11
|
+
*/
|
|
12
|
+
/** Which flank of the paperdoll a slot sits on — `left`/`right` columns, or the `bottom` row under the portrait. */
|
|
13
|
+
export type EquipSlotPosition = "left" | "right" | "bottom";
|
|
14
|
+
/** One named equip slot: a stable `id`, a display `label`, its worn `item` (or `null` = empty), and where it sits. */
|
|
15
|
+
export interface EquipSlot {
|
|
16
|
+
/** Stable slot id passed to `onSlotActivate` (e.g. `head`, `mainHand`, `ring1`). */
|
|
17
|
+
id: string;
|
|
18
|
+
/** Human label shown under/beside the slot and used as its accessible name. */
|
|
19
|
+
label: string;
|
|
20
|
+
/** The worn item, or `null` for an empty slot (renders the themed placeholder). */
|
|
21
|
+
item: SlotItem | null;
|
|
22
|
+
/** Which flank the slot sits on. Default `left`. */
|
|
23
|
+
position?: EquipSlotPosition;
|
|
24
|
+
}
|
|
25
|
+
/** An ordered set of equip slots that make up a paperdoll. Caller-supplied; see {@link defaultEquipLayout}. */
|
|
26
|
+
export type EquipSlotLayout = readonly EquipSlot[];
|
|
27
|
+
/** Slot ids used by {@link defaultEquipLayout}'s convenience arrangement. */
|
|
28
|
+
export type DefaultEquipSlotId = "head" | "chest" | "hands" | "legs" | "feet" | "mainHand" | "offHand" | "ring1" | "ring2" | "trinket";
|
|
29
|
+
/**
|
|
30
|
+
* A sensible default paperdoll arrangement — worn armor down the left flank, held gear and jewelry
|
|
31
|
+
* down the right — that the game fills by passing worn items keyed by slot id. Every id is optional;
|
|
32
|
+
* unfilled slots render empty. Games that need a different slot set pass their own {@link EquipSlotLayout}.
|
|
33
|
+
*/
|
|
34
|
+
export declare function defaultEquipLayout(items?: Partial<Record<DefaultEquipSlotId, SlotItem | null>>): EquipSlot[];
|
|
35
|
+
/** Props for {@link Paperdoll}. */
|
|
36
|
+
export interface PaperdollProps {
|
|
37
|
+
/** The equip slots to arrange. Order within a flank is preserved. */
|
|
38
|
+
slots: EquipSlotLayout;
|
|
39
|
+
/** Center node — a portrait, 3D preview, or custom art. A silhouette placeholder is drawn when omitted. */
|
|
40
|
+
portrait?: ReactNode;
|
|
41
|
+
/** Slot tile size in px. Default 44. */
|
|
42
|
+
slotSize?: number;
|
|
43
|
+
/** Fired with the slot `id` on click / Enter / Space — wire drag or an equip picker here. */
|
|
44
|
+
onSlotActivate?: (id: string) => void;
|
|
45
|
+
className?: string;
|
|
46
|
+
style?: CSSProperties;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Arranges caller-supplied equip slots as a paperdoll — worn gear in two flanking columns around a
|
|
50
|
+
* center `portrait` (or a silhouette placeholder), with any `bottom` slots in a row beneath it. Each
|
|
51
|
+
* slot reuses the atomic {@link EquipmentSlots} tile (filled icon or themed empty placeholder), carries
|
|
52
|
+
* a label, and is a focusable `role="group"` with an `aria-label`; `onSlotActivate` fires on
|
|
53
|
+
* click/Enter/Space so games can wire drag or an equip picker. Composition and skin stay the game's.
|
|
54
|
+
*
|
|
55
|
+
* @capability character-paperdoll equip-slot paperdoll — flanking columns around a portrait, activatable slots
|
|
56
|
+
*/
|
|
57
|
+
export declare function Paperdoll({ slots, portrait, slotSize, onSlotActivate, className, style }: PaperdollProps): ReactNode;
|
|
58
|
+
/** One derived-stat row. `group` optionally buckets rows under a small header. */
|
|
59
|
+
export interface StatRow {
|
|
60
|
+
/** Stable row id. */
|
|
61
|
+
id: string;
|
|
62
|
+
/** Small-caps row label (e.g. `Armor`, `Crit`). */
|
|
63
|
+
label: string;
|
|
64
|
+
/** The value to show — the game formats it (a number, `"12%"`, `"1.2/s"`). */
|
|
65
|
+
value: string | number;
|
|
66
|
+
/** Optional secondary hint shown dimmed after the value (e.g. a delta or source). */
|
|
67
|
+
hint?: string;
|
|
68
|
+
/** Optional group header this row falls under; consecutive rows sharing a group render one header. */
|
|
69
|
+
group?: string;
|
|
70
|
+
}
|
|
71
|
+
/** Props for {@link StatList}. */
|
|
72
|
+
export interface StatListProps {
|
|
73
|
+
/** Derived stat rows, in display order. The game owns which stats matter. */
|
|
74
|
+
stats: readonly StatRow[];
|
|
75
|
+
className?: string;
|
|
76
|
+
style?: CSSProperties;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Renders derived character stats as clean small-caps label→value rows, token-themed. Rows carrying a
|
|
80
|
+
* `group` field are bucketed under a small uppercase header (consecutive same-group rows share one).
|
|
81
|
+
* Pure and prop-driven — the game supplies the rows (from `useEntityStat`/derived values); the engine
|
|
82
|
+
* ships no stat list of its own.
|
|
83
|
+
*
|
|
84
|
+
* @capability character-statlist derived-stat readout — grouped small-caps label→value rows, token-themed
|
|
85
|
+
*/
|
|
86
|
+
export declare function StatList({ stats, className, style }: StatListProps): ReactNode;
|
|
87
|
+
/** Props for {@link CharacterSheet}. */
|
|
88
|
+
export interface CharacterSheetProps {
|
|
89
|
+
/** Equip slots to arrange as the paperdoll (see {@link defaultEquipLayout}). */
|
|
90
|
+
slots: EquipSlotLayout;
|
|
91
|
+
/** Derived stat rows for the {@link StatList}. */
|
|
92
|
+
stats: readonly StatRow[];
|
|
93
|
+
/** Character name shown in the header. */
|
|
94
|
+
name?: ReactNode;
|
|
95
|
+
/** Secondary header line — level, class, faction, etc. */
|
|
96
|
+
subtitle?: ReactNode;
|
|
97
|
+
/** Portrait node passed through to the {@link Paperdoll} center. */
|
|
98
|
+
portrait?: ReactNode;
|
|
99
|
+
/** Replace the whole header block (name/subtitle) with custom content. */
|
|
100
|
+
header?: ReactNode;
|
|
101
|
+
/** Frame title label. Default `Character`. */
|
|
102
|
+
title?: ReactNode;
|
|
103
|
+
/** {@link HudFrame} skin. Default `themed`. */
|
|
104
|
+
variation?: HudFrameVariation;
|
|
105
|
+
/** Slot tile size in px, forwarded to {@link Paperdoll}. */
|
|
106
|
+
slotSize?: number;
|
|
107
|
+
/** Fired with the equip slot `id` on activate. */
|
|
108
|
+
onSlotActivate?: (id: string) => void;
|
|
109
|
+
className?: string;
|
|
110
|
+
style?: CSSProperties;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Drop-in character sheet: a {@link HudFrame} wrapping an optional name/subtitle/portrait header, the
|
|
114
|
+
* equip-slot {@link Paperdoll}, and the derived-stat {@link StatList}. The game supplies slots, stats,
|
|
115
|
+
* header content, and the activation callback, and owns the window that toggles it (this component does
|
|
116
|
+
* not manage its own open/close). Pure composition over existing primitives and HudTheme tokens.
|
|
117
|
+
*
|
|
118
|
+
* @capability character-sheet drop-in paperdoll + derived-stat sheet in a HudFrame — game supplies slots/stats
|
|
119
|
+
*/
|
|
120
|
+
export declare function CharacterSheet({ slots, stats, name, subtitle, portrait, header, title, variation, slotSize, onSlotActivate, className, style, }: CharacterSheetProps): ReactNode;
|