@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/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
|
+
}
|
package/dist/cards.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import type { CardPileState } from "@jgengine/core/cards/cardPile";
|
|
3
|
+
import { type DragLayer } from "./dragLayer.js";
|
|
4
|
+
/** The four French-deck suits. */
|
|
5
|
+
export type Suit = "clubs" | "diamonds" | "hearts" | "spades";
|
|
6
|
+
/** A playing-card rank, ace through king. */
|
|
7
|
+
export type Rank = "A" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "J" | "Q" | "K";
|
|
8
|
+
/** Suits in canonical order (clubs, diamonds, hearts, spades). */
|
|
9
|
+
export declare const SUITS: readonly Suit[];
|
|
10
|
+
/** Ranks in ascending order, ace low. */
|
|
11
|
+
export declare const RANKS: readonly Rank[];
|
|
12
|
+
/** True for the red suits (hearts, diamonds). */
|
|
13
|
+
export declare function isRedSuit(suit: Suit): boolean;
|
|
14
|
+
/** A single card in a stack: an id plus its face and orientation. */
|
|
15
|
+
export interface PlayingCard {
|
|
16
|
+
id: string;
|
|
17
|
+
rank: Rank;
|
|
18
|
+
suit: Suit;
|
|
19
|
+
faceDown?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** A single playing-card face: rank + suit pips, or a patterned back when `faceDown`. */
|
|
22
|
+
export declare function CardFace({ rank, suit, faceDown, width, height, className, style, children, }: {
|
|
23
|
+
rank: Rank;
|
|
24
|
+
suit: Suit;
|
|
25
|
+
faceDown?: boolean;
|
|
26
|
+
width?: number;
|
|
27
|
+
height?: number;
|
|
28
|
+
className?: string;
|
|
29
|
+
style?: CSSProperties;
|
|
30
|
+
children?: ReactNode;
|
|
31
|
+
}): import("react").JSX.Element;
|
|
32
|
+
/**
|
|
33
|
+
* An overlapping fan of `CardFace`s. Plain-props: pass `cards`. Optional drag: pass a `layer`
|
|
34
|
+
* to make each card a `DraggableCard`, and `dropId` to wrap the pile in a `DropZone`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function StackedPile({ cards, faceDown, offsetX, offsetY, cardWidth, cardHeight, layer, dropId, draggableFrom, className, cardClassName, onCardClick, renderOverlay, }: {
|
|
37
|
+
cards: readonly PlayingCard[];
|
|
38
|
+
faceDown?: boolean;
|
|
39
|
+
offsetX?: number;
|
|
40
|
+
offsetY?: number;
|
|
41
|
+
cardWidth?: number;
|
|
42
|
+
cardHeight?: number;
|
|
43
|
+
layer?: DragLayer<PlayingCard>;
|
|
44
|
+
dropId?: string;
|
|
45
|
+
draggableFrom?: number;
|
|
46
|
+
className?: string;
|
|
47
|
+
cardClassName?: string;
|
|
48
|
+
onCardClick?: (card: PlayingCard, index: number) => void;
|
|
49
|
+
renderOverlay?: (card: PlayingCard, index: number) => ReactNode;
|
|
50
|
+
}): import("react").JSX.Element;
|
|
51
|
+
/**
|
|
52
|
+
* `StackedPile` bound to a headless `CardPileState` zone: reads the ordered card ids from
|
|
53
|
+
* `pile.zones[zone]` and resolves each to a `PlayingCard` via `cardOf`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function CardStack({ pile, zone, cardOf, ...rest }: {
|
|
56
|
+
pile: CardPileState;
|
|
57
|
+
zone: string;
|
|
58
|
+
cardOf: (id: string) => PlayingCard;
|
|
59
|
+
} & Omit<Parameters<typeof StackedPile>[0], "cards">): import("react").JSX.Element;
|
package/dist/cards.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { DraggableCard, DropZone } from "./dragLayer.js";
|
|
3
|
+
/** Suits in canonical order (clubs, diamonds, hearts, spades). */
|
|
4
|
+
export const SUITS = ["clubs", "diamonds", "hearts", "spades"];
|
|
5
|
+
/** Ranks in ascending order, ace low. */
|
|
6
|
+
export const RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"];
|
|
7
|
+
/** True for the red suits (hearts, diamonds). */
|
|
8
|
+
export function isRedSuit(suit) {
|
|
9
|
+
return suit === "hearts" || suit === "diamonds";
|
|
10
|
+
}
|
|
11
|
+
const SUIT_PATHS = {
|
|
12
|
+
hearts: _jsx("path", { d: "M12 21C7 17 3 13.5 3 9.5A4.5 4.5 0 0112 7a4.5 4.5 0 019 2.5C21 13.5 17 17 12 21z" }),
|
|
13
|
+
diamonds: _jsx("path", { d: "M12 2l7.5 10L12 22 4.5 12z" }),
|
|
14
|
+
spades: (_jsx("path", { d: "M12 2C8.5 6 3.5 8.5 3.5 13A4 4 0 0011 15.7C10.7 18 9.7 19.3 8 21h8c-1.7-1.7-2.7-3-3-5.3A4 4 0 0020.5 13C20.5 8.5 15.5 6 12 2z" })),
|
|
15
|
+
clubs: (_jsx("path", { d: "M12 2a3.6 3.6 0 00-2.9 5.7A3.6 3.6 0 108.4 14.6 3.6 3.6 0 0011 15.3C10.8 18 9.8 19.4 8 21h8c-1.8-1.6-2.8-3-3-5.7a3.6 3.6 0 102.6-6.9A3.6 3.6 0 0012 2z" })),
|
|
16
|
+
};
|
|
17
|
+
function SuitGlyph({ suit, size = 16 }) {
|
|
18
|
+
return (_jsx("svg", { viewBox: "0 0 24 24", width: size, height: size, fill: "currentColor", "aria-hidden": "true", "data-suit-glyph": suit, style: { display: "block" }, children: SUIT_PATHS[suit] }));
|
|
19
|
+
}
|
|
20
|
+
const SUIT_LABEL = {
|
|
21
|
+
clubs: "clubs",
|
|
22
|
+
diamonds: "diamonds",
|
|
23
|
+
hearts: "hearts",
|
|
24
|
+
spades: "spades",
|
|
25
|
+
};
|
|
26
|
+
/** A single playing-card face: rank + suit pips, or a patterned back when `faceDown`. */
|
|
27
|
+
export function CardFace({ rank, suit, faceDown = false, width = 64, height = 90, className, style, children, }) {
|
|
28
|
+
const radius = Math.round(Math.min(width, height) * 0.11);
|
|
29
|
+
const base = {
|
|
30
|
+
width,
|
|
31
|
+
height,
|
|
32
|
+
borderRadius: radius,
|
|
33
|
+
boxSizing: "border-box",
|
|
34
|
+
position: "relative",
|
|
35
|
+
userSelect: "none",
|
|
36
|
+
...style,
|
|
37
|
+
};
|
|
38
|
+
if (faceDown) {
|
|
39
|
+
return (_jsxs("div", { className: className, "data-card-face": "", "data-face-down": "", role: "img", "aria-label": "Face-down card", style: {
|
|
40
|
+
...base,
|
|
41
|
+
border: "1px solid #1e2a52",
|
|
42
|
+
background: "repeating-linear-gradient(45deg, #2b3a72 0, #2b3a72 4px, #1b2650 4px, #1b2650 8px)",
|
|
43
|
+
}, children: [_jsx("div", { style: {
|
|
44
|
+
position: "absolute",
|
|
45
|
+
inset: Math.round(Math.min(width, height) * 0.09),
|
|
46
|
+
borderRadius: Math.max(2, radius - 3),
|
|
47
|
+
border: "1px solid rgba(255,255,255,0.35)",
|
|
48
|
+
} }), children] }));
|
|
49
|
+
}
|
|
50
|
+
const color = isRedSuit(suit) ? "#c02434" : "#16181d";
|
|
51
|
+
const cornerFont = Math.round(height * 0.16);
|
|
52
|
+
const corner = (rotated) => ({
|
|
53
|
+
position: "absolute",
|
|
54
|
+
display: "flex",
|
|
55
|
+
flexDirection: "column",
|
|
56
|
+
alignItems: "center",
|
|
57
|
+
lineHeight: 1,
|
|
58
|
+
color,
|
|
59
|
+
fontFamily: "Georgia, 'Times New Roman', serif",
|
|
60
|
+
fontWeight: 700,
|
|
61
|
+
fontSize: cornerFont,
|
|
62
|
+
...(rotated
|
|
63
|
+
? { right: Math.round(width * 0.06), bottom: Math.round(height * 0.05), transform: "rotate(180deg)" }
|
|
64
|
+
: { left: Math.round(width * 0.06), top: Math.round(height * 0.05) }),
|
|
65
|
+
});
|
|
66
|
+
return (_jsxs("div", { className: className, "data-card-face": "", "data-rank": rank, "data-suit": suit, role: "img", "aria-label": `${rank} of ${SUIT_LABEL[suit]}`, style: {
|
|
67
|
+
...base,
|
|
68
|
+
border: "1px solid #c8ccd6",
|
|
69
|
+
background: "#ffffff",
|
|
70
|
+
}, children: [_jsxs("span", { "data-card-corner": "top", style: corner(false), children: [_jsx("span", { "data-card-rank": true, children: rank }), _jsx(SuitGlyph, { suit: suit, size: cornerFont })] }), _jsx("div", { style: {
|
|
71
|
+
position: "absolute",
|
|
72
|
+
inset: 0,
|
|
73
|
+
display: "flex",
|
|
74
|
+
alignItems: "center",
|
|
75
|
+
justifyContent: "center",
|
|
76
|
+
color,
|
|
77
|
+
}, children: _jsx(SuitGlyph, { suit: suit, size: Math.round(Math.min(width, height) * 0.5) }) }), _jsxs("span", { "data-card-corner": "bottom", style: corner(true), children: [_jsx("span", { "data-card-rank": true, children: rank }), _jsx(SuitGlyph, { suit: suit, size: cornerFont })] }), children] }));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* An overlapping fan of `CardFace`s. Plain-props: pass `cards`. Optional drag: pass a `layer`
|
|
81
|
+
* to make each card a `DraggableCard`, and `dropId` to wrap the pile in a `DropZone`.
|
|
82
|
+
*/
|
|
83
|
+
export function StackedPile({ cards, faceDown, offsetX = 0, offsetY = 20, cardWidth = 64, cardHeight = 90, layer, dropId, draggableFrom = 0, className, cardClassName, onCardClick, renderOverlay, }) {
|
|
84
|
+
const last = Math.max(0, cards.length - 1);
|
|
85
|
+
const width = cardWidth + Math.abs(offsetX) * last;
|
|
86
|
+
const height = cardHeight + Math.abs(offsetY) * last;
|
|
87
|
+
const baseLeft = offsetX < 0 ? -offsetX * last : 0;
|
|
88
|
+
const baseTop = offsetY < 0 ? -offsetY * last : 0;
|
|
89
|
+
const body = (_jsx("div", { className: className, "data-card-stack": "", "data-count": cards.length, style: { position: "relative", width, height }, children: cards.map((card, index) => {
|
|
90
|
+
const positioned = {
|
|
91
|
+
position: "absolute",
|
|
92
|
+
left: baseLeft + offsetX * index,
|
|
93
|
+
top: baseTop + offsetY * index,
|
|
94
|
+
zIndex: index,
|
|
95
|
+
};
|
|
96
|
+
const face = (_jsx(CardFace, { rank: card.rank, suit: card.suit, faceDown: card.faceDown ?? faceDown, width: cardWidth, height: cardHeight, className: cardClassName, children: renderOverlay?.(card, index) }));
|
|
97
|
+
const clickable = onCardClick !== undefined;
|
|
98
|
+
if (layer !== undefined && index >= draggableFrom) {
|
|
99
|
+
return (_jsx("div", { style: positioned, children: _jsx(DraggableCard, { id: card.id, value: card, layer: layer, onRotate: false, children: face }) }, card.id));
|
|
100
|
+
}
|
|
101
|
+
return (_jsx("div", { "data-card-slot": index, style: { ...positioned, cursor: clickable ? "pointer" : undefined }, onClick: clickable ? () => onCardClick(card, index) : undefined, children: face }, card.id));
|
|
102
|
+
}) }));
|
|
103
|
+
if (layer !== undefined && dropId !== undefined) {
|
|
104
|
+
return (_jsx(DropZone, { id: dropId, layer: layer, children: body }));
|
|
105
|
+
}
|
|
106
|
+
return body;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* `StackedPile` bound to a headless `CardPileState` zone: reads the ordered card ids from
|
|
110
|
+
* `pile.zones[zone]` and resolves each to a `PlayingCard` via `cardOf`.
|
|
111
|
+
*/
|
|
112
|
+
export function CardStack({ pile, zone, cardOf, ...rest }) {
|
|
113
|
+
const ids = pile.zones[zone] ?? [];
|
|
114
|
+
const cards = ids.map(cardOf);
|
|
115
|
+
return _jsx(StackedPile, { cards: cards, ...rest });
|
|
116
|
+
}
|
package/dist/chat.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export declare function ChannelTabs({ channels, active, onSelect, className, tab
|
|
|
33
33
|
activeTabClassName?: string;
|
|
34
34
|
renderTab?: (channelId: string, isActive: boolean) => ReactNode;
|
|
35
35
|
}): import("react").JSX.Element | null;
|
|
36
|
-
export declare function ChatPanel({ channels, initialChannel, limit, className, tabsClassName, tabClassName, activeTabClassName, logClassName, messageClassName, inputClassName, inputFieldClassName, sendButtonClassName, placeholder, renderMessage, onRejected, }: {
|
|
36
|
+
export declare function ChatPanel({ channels, initialChannel, limit, className, tabsClassName, tabClassName, activeTabClassName, logClassName, messageClassName, inputClassName, inputFieldClassName, sendButtonClassName, placeholder, renderMessage, renderTab, onRejected, }: {
|
|
37
37
|
channels?: readonly string[];
|
|
38
38
|
initialChannel?: string;
|
|
39
39
|
limit?: number;
|
|
@@ -48,5 +48,7 @@ export declare function ChatPanel({ channels, initialChannel, limit, className,
|
|
|
48
48
|
sendButtonClassName?: string;
|
|
49
49
|
placeholder?: string;
|
|
50
50
|
renderMessage?: (message: ChatMessage) => ReactNode;
|
|
51
|
+
/** Overrides a tab's label — e.g. render channel id `"proximity"` as "Nearby". */
|
|
52
|
+
renderTab?: (channelId: string, isActive: boolean) => ReactNode;
|
|
51
53
|
onRejected?: (reason: string) => void;
|
|
52
54
|
}): import("react").JSX.Element | null;
|
package/dist/chat.js
CHANGED
|
@@ -66,12 +66,12 @@ export function ChannelTabs({ channels, active, onSelect, className, tabClassNam
|
|
|
66
66
|
return (_jsx("button", { type: "button", role: "tab", "aria-selected": isActive, className: classes.length > 0 ? classes : undefined, "data-channel": channelId, onClick: () => onSelect(channelId), children: renderTab !== undefined ? renderTab(channelId, isActive) : channelId }, channelId));
|
|
67
67
|
}) }));
|
|
68
68
|
}
|
|
69
|
-
export function ChatPanel({ channels, initialChannel, limit, className, tabsClassName, tabClassName, activeTabClassName, logClassName, messageClassName, inputClassName, inputFieldClassName, sendButtonClassName, placeholder, renderMessage, onRejected, }) {
|
|
69
|
+
export function ChatPanel({ channels, initialChannel, limit, className, tabsClassName, tabClassName, activeTabClassName, logClassName, messageClassName, inputClassName, inputFieldClassName, sendButtonClassName, placeholder, renderMessage, renderTab, onRejected, }) {
|
|
70
70
|
const ctx = useGameContext();
|
|
71
71
|
const chat = ctx.game.chat;
|
|
72
72
|
const ids = channels ?? chat?.channels().map((def) => def.id) ?? [];
|
|
73
73
|
const [active, setActive] = useState(initialChannel ?? ids[0] ?? "global");
|
|
74
74
|
if (chat === undefined)
|
|
75
75
|
return null;
|
|
76
|
-
return (_jsxs("section", { className: className, "data-chat-panel": true, children: [_jsx(ChannelTabs, { channels: ids, active: active, onSelect: setActive, className: tabsClassName, tabClassName: tabClassName, activeTabClassName: activeTabClassName }), _jsx(ChatLog, { channelId: active, limit: limit, className: logClassName, messageClassName: messageClassName, renderMessage: renderMessage }), _jsx(ChatInput, { channelId: active, className: inputClassName, inputClassName: inputFieldClassName, buttonClassName: sendButtonClassName, placeholder: placeholder, onRejected: onRejected })] }));
|
|
76
|
+
return (_jsxs("section", { className: className, "data-chat-panel": true, children: [_jsx(ChannelTabs, { channels: ids, active: active, onSelect: setActive, className: tabsClassName, tabClassName: tabClassName, activeTabClassName: activeTabClassName, renderTab: renderTab }), _jsx(ChatLog, { channelId: active, limit: limit, className: logClassName, messageClassName: messageClassName, renderMessage: renderMessage }), _jsx(ChatInput, { channelId: active, className: inputClassName, inputClassName: inputFieldClassName, buttonClassName: sendButtonClassName, placeholder: placeholder, onRejected: onRejected })] }));
|
|
77
77
|
}
|
package/dist/components.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
@@ -84,6 +91,20 @@ export declare function DialogueBox({ dialogue, onChoice, rng, className, lineCl
|
|
|
84
91
|
choiceClassName?: string;
|
|
85
92
|
checkClassName?: string;
|
|
86
93
|
}): import("react").JSX.Element;
|
|
94
|
+
/**
|
|
95
|
+
* The id of the dialogue `ctx.game.dialogue` (or a `talkable(id)` prompt) currently has open, or `null`.
|
|
96
|
+
* The read side of the `features.dialogue` bridge — a panel looks the id up in its own dialogue catalog and
|
|
97
|
+
* renders {@link DialogueBox}, with no per-game open/close store.
|
|
98
|
+
*/
|
|
99
|
+
export declare function useOpenDialogueId(): string | null;
|
|
100
|
+
/**
|
|
101
|
+
* Route a {@link DialogueBox} choice through the `features.dialogue` bridge: resolve the choice's invoke
|
|
102
|
+
* (honoring a skill-check `result`), run that command, and otherwise close the dialogue — the write side that
|
|
103
|
+
* replaces a per-game `onChoice` that hand-rolls `resolveDialogueInvoke` + `dialogue.close`.
|
|
104
|
+
*/
|
|
105
|
+
export declare function runDialogueChoice(commands: {
|
|
106
|
+
run(name: string, input?: unknown): unknown;
|
|
107
|
+
}, choice: DialogueChoice, result: CheckResult | null): void;
|
|
87
108
|
export declare function SkillCheckBar({ config, startedAt, className, trackClassName, zoneClassName, markerClassName, renderStatus, }: {
|
|
88
109
|
config: SkillCheckConfig;
|
|
89
110
|
startedAt: number;
|
|
@@ -106,6 +127,12 @@ export declare function CaptureOdds({ chance, className, fillClassName, }: {
|
|
|
106
127
|
className?: string;
|
|
107
128
|
fillClassName?: string;
|
|
108
129
|
}): import("react").JSX.Element;
|
|
130
|
+
/**
|
|
131
|
+
* Render `ctx.game.feed`'s entries for `action` as a newest-first toast stack — the feed-backed sibling of
|
|
132
|
+
* `@jgengine/core/game/toasts`' `createToastQueue`. Reach for this when the message source is already an engine
|
|
133
|
+
* event bound onto `ctx.game.feed` (kill feed, quest updates, loot log); reach for a `createToastQueue` when the
|
|
134
|
+
* game raises ad-hoc messages that need their own TTL independent of the feed's ring buffer.
|
|
135
|
+
*/
|
|
109
136
|
export declare function ToastStack({ action, limit, className, renderToast, }: {
|
|
110
137
|
action: string;
|
|
111
138
|
limit?: number;
|
package/dist/components.js
CHANGED
|
@@ -3,8 +3,10 @@ import { useEffect, useRef, useState } from "react";
|
|
|
3
3
|
import { evaluateSkillCheck, } from "@jgengine/core/interaction/skillCheck";
|
|
4
4
|
import { pendingQteStep } from "@jgengine/core/interaction/qte";
|
|
5
5
|
import { rollCheck } from "@jgengine/core/stats/rollCheck";
|
|
6
|
+
import { dialogueSlot } from "@jgengine/core/game/dialogue";
|
|
6
7
|
import { useGameContext } from "./provider.js";
|
|
7
8
|
import { useCurrency, useEntityStat, useFeed, useInventory, useLocalPlayerDead } from "./hooks.js";
|
|
9
|
+
import { useStore } from "./store.js";
|
|
8
10
|
import { paintQteStepDom, paintSkillCheckDom } from "./skillCheckPaint.js";
|
|
9
11
|
export { paintQteStepDom, paintSkillCheckDom } from "./skillCheckPaint.js";
|
|
10
12
|
export function SlotGrid({ inventoryId, className, renderSlot, }) {
|
|
@@ -15,7 +17,14 @@ export function SlotGrid({ inventoryId, className, renderSlot, }) {
|
|
|
15
17
|
? `${slot.itemId} x${slot.count}`
|
|
16
18
|
: null }, index))) }));
|
|
17
19
|
}
|
|
18
|
-
|
|
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, }) {
|
|
19
28
|
const stat = useEntityStat(instanceId, statId);
|
|
20
29
|
if (stat === null)
|
|
21
30
|
return null;
|
|
@@ -55,6 +64,26 @@ export function DialogueBox({ dialogue, onChoice, rng, className, lineClassName,
|
|
|
55
64
|
? ` (${choice.check.advantage})`
|
|
56
65
|
: ""] }))] }, choice.label))) }, index)) : (_jsxs("p", { className: lineClassName, children: [_jsx("span", { className: speakerClassName, "data-speaker": true, children: line.speaker }), _jsx("span", { children: line.text })] }, index))) }));
|
|
57
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The id of the dialogue `ctx.game.dialogue` (or a `talkable(id)` prompt) currently has open, or `null`.
|
|
69
|
+
* The read side of the `features.dialogue` bridge — a panel looks the id up in its own dialogue catalog and
|
|
70
|
+
* renders {@link DialogueBox}, with no per-game open/close store.
|
|
71
|
+
*/
|
|
72
|
+
export function useOpenDialogueId() {
|
|
73
|
+
return useStore(dialogueSlot, (value) => value ?? null);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Route a {@link DialogueBox} choice through the `features.dialogue` bridge: resolve the choice's invoke
|
|
77
|
+
* (honoring a skill-check `result`), run that command, and otherwise close the dialogue — the write side that
|
|
78
|
+
* replaces a per-game `onChoice` that hand-rolls `resolveDialogueInvoke` + `dialogue.close`.
|
|
79
|
+
*/
|
|
80
|
+
export function runDialogueChoice(commands, choice, result) {
|
|
81
|
+
const invoke = resolveDialogueInvoke(choice, result);
|
|
82
|
+
if (invoke !== null && invoke !== undefined)
|
|
83
|
+
commands.run(invoke.command, invoke.args ?? {});
|
|
84
|
+
else
|
|
85
|
+
commands.run("dialogue.close", {});
|
|
86
|
+
}
|
|
58
87
|
export function SkillCheckBar({ config, startedAt, className, trackClassName, zoneClassName, markerClassName, renderStatus, }) {
|
|
59
88
|
const ctx = useGameContext();
|
|
60
89
|
const rootRef = useRef(null);
|
|
@@ -119,6 +148,12 @@ function defaultToast(entry) {
|
|
|
119
148
|
}
|
|
120
149
|
return typeof entry.data === "string" ? entry.data : JSON.stringify(entry.data);
|
|
121
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Render `ctx.game.feed`'s entries for `action` as a newest-first toast stack — the feed-backed sibling of
|
|
153
|
+
* `@jgengine/core/game/toasts`' `createToastQueue`. Reach for this when the message source is already an engine
|
|
154
|
+
* event bound onto `ctx.game.feed` (kill feed, quest updates, loot log); reach for a `createToastQueue` when the
|
|
155
|
+
* game raises ad-hoc messages that need their own TTL independent of the feed's ring buffer.
|
|
156
|
+
*/
|
|
122
157
|
export function ToastStack({ action, limit = 4, className, renderToast, }) {
|
|
123
158
|
const entries = useFeed({ action, limit });
|
|
124
159
|
if (entries.length === 0)
|
package/dist/display.d.ts
CHANGED
package/dist/display.js
CHANGED
package/dist/engineStore.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ export interface ReadableEngineStore<TState> {
|
|
|
5
5
|
export interface EventfulEngineStore<TEventMap extends object> {
|
|
6
6
|
on<K extends keyof TEventMap>(eventName: K, listener: (payload: TEventMap[K]) => void): () => void;
|
|
7
7
|
}
|
|
8
|
+
/** @internal */
|
|
8
9
|
export declare function useEngineState<TState>(store: ReadableEngineStore<TState>): TState;
|
|
10
|
+
/** @internal */
|
|
9
11
|
export declare function useEngineStore<TState, TSelected>(store: ReadableEngineStore<TState>, selector: (state: TState) => TSelected, isEqual?: (previous: TSelected, next: TSelected) => boolean): TSelected;
|
|
12
|
+
/** @internal */
|
|
10
13
|
export declare function useEngineEvent<TEventMap extends object, K extends keyof TEventMap>(store: EventfulEngineStore<TEventMap>, eventName: K, handler: (payload: TEventMap[K]) => void): void;
|
package/dist/engineStore.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
2
2
|
import { createSelectCache, readSelectSnapshot } from "./selectSnapshot.js";
|
|
3
|
+
/** @internal */
|
|
3
4
|
export function useEngineState(store) {
|
|
4
5
|
const subscribe = useCallback((onChange) => store.subscribe(() => onChange()), [store]);
|
|
5
6
|
const getSnapshot = useCallback(() => store.getState(), [store]);
|
|
6
7
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
7
8
|
}
|
|
9
|
+
/** @internal */
|
|
8
10
|
export function useEngineStore(store, selector, isEqual = Object.is) {
|
|
9
11
|
const selectorRef = useRef(selector);
|
|
10
12
|
selectorRef.current = selector;
|
|
@@ -15,6 +17,7 @@ export function useEngineStore(store, selector, isEqual = Object.is) {
|
|
|
15
17
|
const getSnapshot = useCallback(() => readSelectSnapshot(cacheRef.current, store.getState(), (state) => selectorRef.current(state), (previous, next) => isEqualRef.current(previous, next)), [store]);
|
|
16
18
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
17
19
|
}
|
|
20
|
+
/** @internal */
|
|
18
21
|
export function useEngineEvent(store, eventName, handler) {
|
|
19
22
|
const handlerRef = useRef(handler);
|
|
20
23
|
handlerRef.current = handler;
|
package/dist/fogOverlay.d.ts
CHANGED
|
@@ -10,11 +10,14 @@ export interface FogPaintSurface {
|
|
|
10
10
|
fillRect(x: number, y: number, width: number, height: number): void;
|
|
11
11
|
clearRect(x: number, y: number, width: number, height: number): void;
|
|
12
12
|
}
|
|
13
|
+
/** @internal */
|
|
13
14
|
export declare function forEachUnrevealedFogCell(fog: FogCells, visit: (col: number, row: number) => void): number;
|
|
15
|
+
/** @internal */
|
|
14
16
|
export declare function paintFogOverlay(surface: FogPaintSurface, canvasSize: {
|
|
15
17
|
width: number;
|
|
16
18
|
height: number;
|
|
17
19
|
}, fog: FogCells, cellRect: (col: number, row: number) => FogCellRect | null, fill?: string): number;
|
|
20
|
+
/** @internal */
|
|
18
21
|
export declare function createFogDataUrl(fog: FogCells, canvasSize: {
|
|
19
22
|
width: number;
|
|
20
23
|
height: number;
|