@jgengine/react 0.21.0 → 0.23.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 +114 -0
- package/dist/accessibility.d.ts +5 -0
- package/dist/accessibility.js +9 -0
- package/dist/achievements.js +4 -3
- package/dist/actionHud.d.ts +6 -1
- package/dist/actionHud.js +37 -17
- package/dist/bars.js +5 -2
- package/dist/barsPreview.js +2 -1
- package/dist/characterSheet.js +5 -4
- package/dist/coachMarks.js +3 -2
- package/dist/codex.js +4 -3
- package/dist/components.js +9 -2
- package/dist/creditsScreen.js +2 -1
- package/dist/engineStyles.d.ts +18 -0
- package/dist/engineStyles.js +119 -0
- package/dist/fastTravel.js +4 -3
- package/dist/hud.js +7 -6
- package/dist/hudButton.d.ts +29 -0
- package/dist/hudButton.js +31 -0
- package/dist/hudChromePreview.d.ts +8 -0
- package/dist/hudChromePreview.js +27 -0
- package/dist/hudFrame.js +15 -11
- package/dist/hudInteract.d.ts +26 -0
- package/dist/hudInteract.js +28 -0
- package/dist/hudLayout.js +4 -3
- package/dist/hudPanel.d.ts +31 -0
- package/dist/hudPanel.js +32 -0
- package/dist/hudTheme.d.ts +88 -1
- package/dist/hudTheme.js +123 -8
- package/dist/hudThemePreview.js +2 -1
- package/dist/hudThemeRoot.d.ts +15 -0
- package/dist/hudThemeRoot.js +20 -0
- package/dist/hudTokens.d.ts +39 -0
- package/dist/hudTokens.js +56 -0
- package/dist/iconTreatment.js +3 -2
- package/dist/iconsPreview.js +2 -1
- package/dist/keybindingMenu.js +3 -2
- package/dist/map.js +9 -8
- package/dist/modals.d.ts +6 -3
- package/dist/modals.js +42 -52
- package/dist/notices.d.ts +27 -0
- package/dist/notices.js +145 -0
- package/dist/notifications.js +4 -3
- package/dist/panels.d.ts +9 -2
- package/dist/panels.js +78 -35
- package/dist/photoMode.js +2 -1
- package/dist/presence.d.ts +9 -0
- package/dist/presence.js +25 -0
- package/dist/previewFixtures.js +6 -0
- package/dist/questTracker.js +3 -2
- package/dist/quickMenu.js +6 -5
- package/dist/saveSlots.js +4 -3
- package/dist/selectionHud.js +4 -3
- package/dist/shopGrid.js +2 -1
- package/dist/statusEffectBar.js +2 -1
- package/dist/streetGeometryPreview.js +3 -2
- package/dist/styles.css +72 -0
- package/dist/talentTree.js +3 -2
- package/dist/tweenNumber.d.ts +37 -0
- package/dist/tweenNumber.js +66 -0
- package/dist/uiCues.d.ts +20 -0
- package/dist/uiCues.js +37 -0
- package/dist/waveHud.js +2 -1
- package/dist/waypointMarkers.js +2 -1
- package/package.json +10 -6
- package/skills/jgengine/SKILL.md +1 -0
- package/skills/jgengine/api.md +18 -18
- package/skills/jgengine-ui/SKILL.md +4 -0
- package/skills/jgengine-ui/api.md +242 -34
- package/skills/jgengine-ui/capabilities.md +56 -0
- package/skills/jgengine-ui/reference-primitives.md +4 -4
package/dist/questTracker.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { hudType } from "./hudTokens.js";
|
|
2
3
|
function ObjectiveRow({ label, progress, count, complete, accent }) {
|
|
3
4
|
const fraction = count > 0 ? Math.min(1, progress / count) : complete ? 1 : 0;
|
|
4
5
|
return (_jsxs("li", { "data-quest-objective": true, "data-complete": complete, style: { display: "flex", flexDirection: "column", gap: 3, listStyle: "none" }, children: [_jsxs("div", { style: { display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 8 }, children: [_jsxs("span", { style: { display: "flex", alignItems: "baseline", gap: 6, opacity: complete ? 0.65 : 1 }, children: [_jsx("span", { "aria-hidden": true, style: { color: complete ? accent : "rgba(148,163,184,0.7)" }, children: complete ? "✓" : "○" }), _jsx("span", { style: { textDecoration: complete ? "line-through" : "none" }, children: label })] }), count > 1 ? (_jsxs("span", { style: { fontVariantNumeric: "tabular-nums", color: "rgba(203,213,225,0.7)", fontSize: "0.9em" }, children: [Math.min(progress, count), "/", count] })) : null] }), count > 1 && !complete ? (_jsx("div", { style: { height: 3, borderRadius: 9999, background: "rgba(148,163,184,0.2)", overflow: "hidden" }, children: _jsx("div", { style: { width: `${Math.round(fraction * 100)}%`, height: "100%", background: accent } }) })) : null] }));
|
|
@@ -22,10 +23,10 @@ export function QuestTracker({ quests, title = "Quests", emptyLabel = "No active
|
|
|
22
23
|
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
23
24
|
fontSize: 12,
|
|
24
25
|
...style,
|
|
25
|
-
}, children: [title !== "" ? (_jsx("div", { style: { fontSize:
|
|
26
|
+
}, children: [title !== "" ? (_jsx("div", { style: { fontSize: hudType("xs"), letterSpacing: 1.4, textTransform: "uppercase", color: "rgba(203,213,225,0.7)", marginBottom: 8 }, children: title })) : null, quests.length === 0 ? (_jsx("p", { style: { margin: 0, color: "rgba(148,163,184,0.8)" }, children: emptyLabel })) : (_jsx("ul", { style: { margin: 0, padding: 0, listStyle: "none", display: "flex", flexDirection: "column", gap: 10 }, children: quests.map((quest) => {
|
|
26
27
|
const shown = maxObjectives === undefined ? quest.objectives : quest.objectives.slice(0, maxObjectives);
|
|
27
28
|
const hidden = quest.objectives.length - shown.length;
|
|
28
29
|
const done = quest.status === "completed";
|
|
29
|
-
return (_jsxs("li", { "data-quest": quest.id, "data-status": quest.status, style: { listStyle: "none" }, children: [_jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: 6, marginBottom: 4 }, children: [_jsx("span", { style: { fontWeight: 700, color: done ? accent : "#f8fafc" }, children: quest.title }), done ? _jsx("span", { style: { fontSize:
|
|
30
|
+
return (_jsxs("li", { "data-quest": quest.id, "data-status": quest.status, style: { listStyle: "none" }, children: [_jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: 6, marginBottom: 4 }, children: [_jsx("span", { style: { fontWeight: 700, color: done ? accent : "#f8fafc" }, children: quest.title }), done ? _jsx("span", { style: { fontSize: hudType("xs"), color: accent }, children: "DONE" }) : null] }), _jsx("ul", { style: { margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 5 }, children: shown.map((objective) => (_jsx(ObjectiveRow, { label: objective.label, progress: objective.progress, count: objective.count, complete: objective.complete, accent: accent }, objective.id))) }), hidden > 0 ? (_jsxs("div", { style: { marginTop: 4, fontSize: hudType("xs"), color: "rgba(148,163,184,0.75)" }, children: ["+", hidden, " more"] })) : null] }, quest.id));
|
|
30
31
|
}) }))] }));
|
|
31
32
|
}
|
package/dist/quickMenu.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useState } from "react";
|
|
3
3
|
import { RadialMenu } from "./radialMenu.js";
|
|
4
|
+
import { hudType } from "./hudTokens.js";
|
|
4
5
|
const PANEL = {
|
|
5
6
|
borderRadius: 14,
|
|
6
7
|
padding: 12,
|
|
@@ -21,7 +22,7 @@ function groupBySection(items) {
|
|
|
21
22
|
return groups;
|
|
22
23
|
}
|
|
23
24
|
function Badges({ item, accent }) {
|
|
24
|
-
return (_jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 6 }, children: [item.cooldown !== undefined && item.cooldown > 0 ? (_jsxs("span", { style: { fontSize:
|
|
25
|
+
return (_jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 6 }, children: [item.cooldown !== undefined && item.cooldown > 0 ? (_jsxs("span", { style: { fontSize: hudType("xs"), color: "rgba(148,163,184,0.85)" }, children: [Math.ceil(item.cooldown * 100), "%"] })) : null, item.badge !== undefined ? (_jsx("span", { style: { fontSize: hudType("xs"), fontWeight: 700, color: accent, fontVariantNumeric: "tabular-nums" }, children: item.badge })) : null, item.hotkey !== undefined ? (_jsx("kbd", { style: { fontSize: hudType("xs"), padding: "1px 5px", borderRadius: 4, border: "1px solid rgba(148,163,184,0.4)", color: "rgba(226,232,240,0.85)" }, children: item.hotkey })) : null, item.children !== undefined ? _jsx("span", { style: { color: "rgba(226,232,240,0.7)" }, children: "\u25B8" }) : null] }));
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
28
|
* Versatile quick menu that takes many forms — a `radial` wheel, a partial
|
|
@@ -53,7 +54,7 @@ export function QuickMenu({ items, layout = "list", onSelect, onClose, title, co
|
|
|
53
54
|
onSelect(item.id);
|
|
54
55
|
};
|
|
55
56
|
const back = () => setPath((p) => p.slice(0, -1));
|
|
56
|
-
const header = title !== undefined || path.length > 0 ? (_jsxs("div", { "data-quick-header": true, style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }, children: [path.length > 0 ? (_jsx("button", { type: "button", "data-quick-back": true, onClick: back, style: { border: "1px solid rgba(148,163,184,0.4)", borderRadius: 6, background: "transparent", color: "#e2e8f0", fontSize:
|
|
57
|
+
const header = title !== undefined || path.length > 0 ? (_jsxs("div", { "data-quick-header": true, style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }, children: [path.length > 0 ? (_jsx("button", { type: "button", "data-quick-back": true, onClick: back, style: { border: "1px solid rgba(148,163,184,0.4)", borderRadius: 6, background: "transparent", color: "#e2e8f0", fontSize: hudType("xs"), padding: "2px 8px", cursor: "pointer" }, children: "\u2039 Back" })) : null, title !== undefined ? (_jsx("span", { style: { fontSize: hudType("xs"), letterSpacing: 1.2, textTransform: "uppercase", color: "rgba(203,213,225,0.75)" }, children: title })) : null] })) : null;
|
|
57
58
|
if (layout === "radial" || layout === "arc") {
|
|
58
59
|
const options = current.map((item) => ({ ...item, hasSubmenu: item.children !== undefined && item.children.length > 0 }));
|
|
59
60
|
return (_jsxs("div", { className: className, "data-quick-menu": true, "data-layout": layout, style: { position: "relative", ...style }, children: [header, _jsx(RadialMenu, { options: options, size: radialSize, accent: accent, ...(layout === "arc" ? { arc } : {}), onSelect: (id) => {
|
|
@@ -64,14 +65,14 @@ export function QuickMenu({ items, layout = "list", onSelect, onClose, title, co
|
|
|
64
65
|
}
|
|
65
66
|
const groups = groupBySection(current);
|
|
66
67
|
if (layout === "grid") {
|
|
67
|
-
return (_jsxs("div", { className: className, "data-quick-menu": true, "data-layout": "grid", style: { ...PANEL, width: columns * 92 + 24, ...style }, children: [header, groups.map((group, gi) => (_jsxs("div", { "data-section": group.section, children: [group.section !== undefined ? (_jsx("div", { style: { fontSize:
|
|
68
|
+
return (_jsxs("div", { className: className, "data-quick-menu": true, "data-layout": "grid", style: { ...PANEL, width: columns * 92 + 24, ...style }, children: [header, groups.map((group, gi) => (_jsxs("div", { "data-section": group.section, children: [group.section !== undefined ? (_jsx("div", { style: { fontSize: hudType("xs"), textTransform: "uppercase", letterSpacing: 1.2, color: "rgba(148,163,184,0.75)", margin: "6px 0 4px" }, children: group.section })) : null, _jsx("div", { style: { display: "grid", gridTemplateColumns: `repeat(${columns}, 1fr)`, gap: 6 }, children: group.items.map((item) => (_jsxs("button", { type: "button", "data-quick-item": item.id, "data-disabled": item.disabled === true, onClick: () => activate(item), disabled: item.disabled === true, style: {
|
|
68
69
|
position: "relative", display: "flex", flexDirection: "column", alignItems: "center", gap: 3, padding: "10px 4px",
|
|
69
70
|
borderRadius: 10, border: "1px solid rgba(148,163,184,0.25)", background: "rgba(17,22,30,0.7)", color: "#e2e8f0",
|
|
70
71
|
cursor: item.disabled === true ? "default" : "pointer", opacity: item.disabled === true ? 0.4 : 1,
|
|
71
|
-
}, children: [_jsx("span", { style: { fontSize: 22 }, children: item.icon }), item.label !== undefined ? _jsx("span", { style: { fontSize:
|
|
72
|
+
}, children: [_jsx("span", { style: { fontSize: 22 }, children: item.icon }), item.label !== undefined ? _jsx("span", { style: { fontSize: hudType("xs") }, children: item.label }) : null, item.badge !== undefined ? (_jsx("span", { style: { position: "absolute", top: 3, right: 5, fontSize: hudType("xs"), fontWeight: 700, color: accent }, children: item.badge })) : null, item.children !== undefined ? _jsx("span", { style: { position: "absolute", bottom: 3, right: 5, fontSize: hudType("xs") }, children: "\u25B8" }) : null] }, item.id))) })] }, group.section ?? gi)))] }));
|
|
72
73
|
}
|
|
73
74
|
// list
|
|
74
|
-
return (_jsxs("div", { className: className, "data-quick-menu": true, "data-layout": "list", style: { ...PANEL, width: 240, ...style }, children: [header, groups.map((group, gi) => (_jsxs("div", { "data-section": group.section, children: [group.section !== undefined ? (_jsx("div", { style: { fontSize:
|
|
75
|
+
return (_jsxs("div", { className: className, "data-quick-menu": true, "data-layout": "list", style: { ...PANEL, width: 240, ...style }, children: [header, groups.map((group, gi) => (_jsxs("div", { "data-section": group.section, children: [group.section !== undefined ? (_jsx("div", { style: { fontSize: hudType("xs"), textTransform: "uppercase", letterSpacing: 1.2, color: "rgba(148,163,184,0.75)", margin: "6px 0 4px" }, children: group.section })) : null, _jsx("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 2 }, children: group.items.map((item) => (_jsx("li", { children: _jsxs("button", { type: "button", "data-quick-item": item.id, "data-disabled": item.disabled === true, onClick: () => activate(item), disabled: item.disabled === true, style: {
|
|
75
76
|
width: "100%", display: "flex", alignItems: "center", gap: 8, padding: "7px 9px", borderRadius: 8,
|
|
76
77
|
border: "1px solid transparent", background: "transparent", color: "#e2e8f0", fontSize: 13, textAlign: "left",
|
|
77
78
|
cursor: item.disabled === true ? "default" : "pointer", opacity: item.disabled === true ? 0.45 : 1,
|
package/dist/saveSlots.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useReducer, useEffect } from "react";
|
|
3
3
|
import { GameIcon } from "./gameIcons.js";
|
|
4
4
|
import { HudFrame } from "./hudFrame.js";
|
|
5
|
+
import { hudType } from "./hudTokens.js";
|
|
5
6
|
/**
|
|
6
7
|
* Subscribe to a {@link SaveSlots} index and re-render on any change (write,
|
|
7
8
|
* clear, rename, restore). Returns the live list of slots in menu order. Wire it
|
|
@@ -75,10 +76,10 @@ function MetaChips({ slot, t, metaIcons, }) {
|
|
|
75
76
|
color: t.muted,
|
|
76
77
|
borderRadius: 999,
|
|
77
78
|
padding: "3px 9px",
|
|
78
|
-
fontSize:
|
|
79
|
+
fontSize: hudType("xs"),
|
|
79
80
|
fontWeight: 600,
|
|
80
81
|
lineHeight: 1.3,
|
|
81
|
-
}, children: [icon !== undefined ? _jsx(GameIcon, { name: icon, size: 12, color: t.accent }) : null, _jsx("span", { style: { opacity: 0.7, textTransform: "uppercase", letterSpacing: 0.6, fontSize:
|
|
82
|
+
}, children: [icon !== undefined ? _jsx(GameIcon, { name: icon, size: 12, color: t.accent }) : null, _jsx("span", { style: { opacity: 0.7, textTransform: "uppercase", letterSpacing: 0.6, fontSize: hudType("xs") }, children: key }), _jsx("span", { style: { color: t.text }, children: String(value) })] }, key));
|
|
82
83
|
}) }));
|
|
83
84
|
}
|
|
84
85
|
/**
|
|
@@ -133,6 +134,6 @@ export function SaveSlotMenu({ model, onLoad, onNew, onDelete, onContinue, title
|
|
|
133
134
|
height: 40,
|
|
134
135
|
borderRadius: 10,
|
|
135
136
|
border: `1px dashed ${t.muted}`,
|
|
136
|
-
}, children: _jsx(GameIcon, { name: "star", size: 18, color: t.muted }) }), _jsxs("span", { children: [_jsx("span", { style: { display: "block", fontSize: 15, fontWeight: 700, color: t.text }, children: "New Game" }), _jsxs("span", { style: { fontSize:
|
|
137
|
+
}, children: _jsx(GameIcon, { name: "star", size: 18, color: t.muted }) }), _jsxs("span", { children: [_jsx("span", { style: { display: "block", fontSize: 15, fontWeight: 700, color: t.text }, children: "New Game" }), _jsxs("span", { style: { fontSize: hudType("xs") }, children: ["Empty slot \u00B7 ", slot.id] })] })] })) : (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: [_jsxs("div", { style: { display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 10 }, children: [_jsx("span", { "data-save-slot-name": "", style: { fontSize: 16, fontWeight: 800 }, children: slot.name ?? slot.id }), slot.savedAt !== undefined ? (_jsx("span", { style: { fontSize: hudType("xs"), fontWeight: 600, color: t.accent, whiteSpace: "nowrap" }, children: relativeSavedAt(slot.savedAt, nowMs) })) : null] }), _jsx(MetaChips, { slot: slot, t: t, metaIcons: metaIcons }), _jsxs("div", { style: { display: "flex", gap: 8, marginTop: 12 }, children: [_jsxs("button", { type: "button", "data-save-slot-load": "", style: { ...rowButton(t.accent, true), flex: 1, justifyContent: "center" }, onClick: () => onLoad?.(slot), children: [_jsx(GameIcon, { name: "chest", size: 14, color: "#0b1220" }), "Load"] }), _jsxs("button", { type: "button", "data-save-slot-delete": "", style: rowButton(t.accent, false), onClick: () => onDelete?.(slot), "aria-label": `Delete ${slot.name ?? slot.id}`, children: [_jsx(GameIcon, { name: "cross", size: 14 }), "Delete"] })] })] })) }, slot.id));
|
|
137
138
|
})] }));
|
|
138
139
|
}
|
package/dist/selectionHud.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useMemo, useState, } from "react";
|
|
3
3
|
import { moveSelectionFocus, selectionWindow, summarizeSelection, } from "@jgengine/core/ui/selectionModel";
|
|
4
|
+
import { hudType } from "./hudTokens.js";
|
|
4
5
|
/**
|
|
5
6
|
* Composable React renderers over the headless selection view model
|
|
6
7
|
* (`@jgengine/core/ui/selectionModel`) — the data/renderer/chrome split for an RTS selection panel, a
|
|
@@ -74,7 +75,7 @@ export function VitalBar({ vital, width = 120, showLabel = true, className, styl
|
|
|
74
75
|
const denom = vital.max <= 0 ? 1 : vital.max;
|
|
75
76
|
const fraction = Math.max(0, Math.min(1, vital.current / denom));
|
|
76
77
|
const [from, to] = VITAL_TONES[vital.tone ?? "neutral"] ?? VITAL_TONES.neutral;
|
|
77
|
-
return (_jsxs("div", { className: className, "data-vital": vital.id, style: { width, ...style }, children: [showLabel ? (_jsxs("div", { style: { display: "flex", fontSize:
|
|
78
|
+
return (_jsxs("div", { className: className, "data-vital": vital.id, style: { width, ...style }, children: [showLabel ? (_jsxs("div", { style: { display: "flex", fontSize: hudType("xs"), opacity: 0.75, marginBottom: 2 }, children: [_jsx("span", { children: vital.label ?? vital.id }), _jsxs("span", { style: { marginLeft: "auto", fontVariantNumeric: "tabular-nums" }, children: [Math.round(vital.current), "/", Math.round(vital.max)] })] })) : null, _jsx("div", { style: { height: 6, borderRadius: 999, background: "rgba(255,255,255,0.12)", overflow: "hidden" }, children: _jsx("div", { style: { width: `${fraction * 100}%`, height: "100%", background: `linear-gradient(90deg, ${from}, ${to})` } }) })] }));
|
|
78
79
|
}
|
|
79
80
|
const PORTRAIT_BASE = {
|
|
80
81
|
position: "relative",
|
|
@@ -124,7 +125,7 @@ export function EntityPortrait({ entity, selected = false, size = 44, vitalId =
|
|
|
124
125
|
* @capability entity-summary primary selected-entity detail panel (name, tags, vitals)
|
|
125
126
|
*/
|
|
126
127
|
export function EntitySummary({ entity, renderVital, className, style, }) {
|
|
127
|
-
return (_jsxs("div", { className: className, "data-entity-summary": entity.id, style: { display: "flex", flexDirection: "column", gap: 4, minWidth: 140, ...style }, children: [_jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: 6 }, children: [_jsx("strong", { style: { fontSize: 14 }, children: entity.name ?? entity.id }), entity.kind !== undefined ? (_jsx("span", { style: { fontSize:
|
|
128
|
+
return (_jsxs("div", { className: className, "data-entity-summary": entity.id, style: { display: "flex", flexDirection: "column", gap: 4, minWidth: 140, ...style }, children: [_jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: 6 }, children: [_jsx("strong", { style: { fontSize: 14 }, children: entity.name ?? entity.id }), entity.kind !== undefined ? (_jsx("span", { style: { fontSize: hudType("xs"), textTransform: "uppercase", letterSpacing: 0.4, opacity: 0.6 }, children: entity.kind })) : null] }), entity.tags !== undefined && entity.tags.length > 0 ? (_jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4 }, children: entity.tags.map((tag) => (_jsx("span", { style: { fontSize: hudType("xs"), padding: "1px 5px", borderRadius: 999, background: "rgba(255,255,255,0.1)" }, children: tag }, tag))) })) : null, (entity.vitals ?? []).map((vital) => (_jsx("div", { children: renderVital ? renderVital(vital) : _jsx(VitalBar, { vital: vital }) }, vital.id)))] }));
|
|
128
129
|
}
|
|
129
130
|
/**
|
|
130
131
|
* A same-kind selection chip — icon plus count — for the grouped large-selection view. The default
|
|
@@ -133,7 +134,7 @@ export function EntitySummary({ entity, renderVital, className, style, }) {
|
|
|
133
134
|
* @capability selection-group-chip same-kind selection chip (icon + count) for a grouped selection
|
|
134
135
|
*/
|
|
135
136
|
export function SelectionGroupChip({ group, onSelect, renderIcon, className, style, }) {
|
|
136
|
-
return (_jsxs("button", { type: "button", className: className, "data-selection-group": group.kind, "aria-label": `${group.count} ${group.label}`, title: `${group.count} ${group.label}`, onClick: () => onSelect?.(group), style: { ...PORTRAIT_BASE, width: 44, height: 44, ...style }, children: [_jsx("span", { "aria-hidden": true, style: { fontSize: 16 }, children: renderIcon ? renderIcon(group) : (group.icon ?? group.label.slice(0, 2)) }), _jsx("span", { style: { position: "absolute", right: 2, bottom: 1, fontSize:
|
|
137
|
+
return (_jsxs("button", { type: "button", className: className, "data-selection-group": group.kind, "aria-label": `${group.count} ${group.label}`, title: `${group.count} ${group.label}`, onClick: () => onSelect?.(group), style: { ...PORTRAIT_BASE, width: 44, height: 44, ...style }, children: [_jsx("span", { "aria-hidden": true, style: { fontSize: 16 }, children: renderIcon ? renderIcon(group) : (group.icon ?? group.label.slice(0, 2)) }), _jsx("span", { style: { position: "absolute", right: 2, bottom: 1, fontSize: hudType("xs"), fontWeight: 700 }, children: group.count })] }));
|
|
137
138
|
}
|
|
138
139
|
/**
|
|
139
140
|
* The composable RENDERER for the member collection: a portrait strip for a small selection, or the
|
package/dist/shopGrid.js
CHANGED
|
@@ -3,6 +3,7 @@ import { useEffect, useReducer } from "react";
|
|
|
3
3
|
import { balance } from "@jgengine/core/economy/wallet";
|
|
4
4
|
import { GameIcon, iconForItemId } from "./gameIcons.js";
|
|
5
5
|
import { HudFrame } from "./hudFrame.js";
|
|
6
|
+
import { hudType } from "./hudTokens.js";
|
|
6
7
|
/**
|
|
7
8
|
* Subscribe to a shop's stock and re-render whenever it changes (buy, sell, restock, price, add,
|
|
8
9
|
* remove, restore). Returns the current entry list — a detached snapshot safe to map over.
|
|
@@ -104,7 +105,7 @@ export function ShopGrid({ shop, wallet, onWalletChange, theme, currencyIcons, i
|
|
|
104
105
|
height: 40,
|
|
105
106
|
borderRadius: 8,
|
|
106
107
|
background: "rgba(0,0,0,0.28)",
|
|
107
|
-
}, children: _jsx(GameIcon, { name: entryIcon(entry, iconFor), size: 28, color: t.accent }) }), _jsx("span", { "data-shop-item-qty": true, style: { fontSize: 12, fontWeight: 700, color: t.muted, fontVariantNumeric: "tabular-nums" }, children: entry.qty === null ? "∞" : `x${entry.qty}` })] }), _jsx("div", { style: { fontSize: 13, fontWeight: 700, color: t.text, lineHeight: 1.2 }, children: labelFor?.(entry) ?? capitalize(entry.kind) }), _jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 6 }, children: [_jsx(PriceTag, { price: entry.price, icon: currencyIcon(entry.price.currency, currencyIcons), accent: t.accent, muted: t.text }), entry.sellPrice !== undefined ? (_jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 3, fontSize:
|
|
108
|
+
}, children: _jsx(GameIcon, { name: entryIcon(entry, iconFor), size: 28, color: t.accent }) }), _jsx("span", { "data-shop-item-qty": true, style: { fontSize: 12, fontWeight: 700, color: t.muted, fontVariantNumeric: "tabular-nums" }, children: entry.qty === null ? "∞" : `x${entry.qty}` })] }), _jsx("div", { style: { fontSize: 13, fontWeight: 700, color: t.text, lineHeight: 1.2 }, children: labelFor?.(entry) ?? capitalize(entry.kind) }), _jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 6 }, children: [_jsx(PriceTag, { price: entry.price, icon: currencyIcon(entry.price.currency, currencyIcons), accent: t.accent, muted: t.text }), entry.sellPrice !== undefined ? (_jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 3, fontSize: hudType("xs"), color: t.muted }, children: ["sell", _jsx(PriceTag, { price: entry.sellPrice, icon: currencyIcon(entry.sellPrice.currency, currencyIcons), accent: t.muted, muted: t.muted })] })) : null] }), _jsxs("div", { style: { display: "flex", gap: 6 }, children: [_jsx("button", { type: "button", "data-shop-buy": entry.id, disabled: buyDisabled, onClick: () => onBuy(entry), style: {
|
|
108
109
|
flex: 1,
|
|
109
110
|
padding: "7px 8px",
|
|
110
111
|
borderRadius: 8,
|
package/dist/statusEffectBar.js
CHANGED
|
@@ -3,6 +3,7 @@ import {} from "react";
|
|
|
3
3
|
import { statusEffectRemainingFraction } from "@jgengine/core/combat";
|
|
4
4
|
import { iconForItemId, isGameIconName } from "./gameIcons.js";
|
|
5
5
|
import { IconTreatment, schoolForItem } from "./iconTreatment.js";
|
|
6
|
+
import { hudType } from "./hudTokens.js";
|
|
6
7
|
const FALLBACK_ICON = "hourglass";
|
|
7
8
|
function defaultIcon(kind) {
|
|
8
9
|
return iconForItemId(kind) ?? FALLBACK_ICON;
|
|
@@ -33,7 +34,7 @@ function StatusEffectIcon({ effect, size, ringColor, school, glyph, label, showT
|
|
|
33
34
|
const circumference = 2 * Math.PI * radius;
|
|
34
35
|
const dashOffset = circumference * (1 - fraction);
|
|
35
36
|
return (_jsxs("div", { "data-status-effect": effect.kind, "data-remaining-fraction": fraction.toFixed(4), title: label, "aria-label": label, style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 3 }, children: [_jsxs("div", { style: { position: "relative", width: size, height: size }, children: [_jsx(IconTreatment, { ...(glyph.node !== undefined ? { glyph: glyph.node } : glyph.icon !== undefined ? { icon: glyph.icon } : {}), school: school, size: size, count: effect.stacks }), _jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, "aria-hidden": "true", style: { position: "absolute", inset: 0, transform: "rotate(-90deg)", pointerEvents: "none", overflow: "visible" }, children: [_jsx("circle", { cx: size / 2, cy: size / 2, r: radius, fill: "none", stroke: "rgba(0,0,0,0.55)", strokeWidth: ringWidth }), _jsx("circle", { "data-countdown-ring": "", cx: size / 2, cy: size / 2, r: radius, fill: "none", stroke: ringColor, strokeWidth: ringWidth, strokeLinecap: "round", strokeDasharray: circumference, strokeDashoffset: dashOffset, style: { transition: "stroke-dashoffset 200ms linear" } })] })] }), showTimer ? (_jsx("span", { "data-status-timer": "", style: {
|
|
36
|
-
fontSize:
|
|
37
|
+
fontSize: hudType("xs"),
|
|
37
38
|
fontWeight: 700,
|
|
38
39
|
fontVariantNumeric: "tabular-nums",
|
|
39
40
|
color: "var(--jg-bar-text, #f5f7fa)",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { buildIntersectionMarkings, buildTrimmedIntersections, } from "@jgengine/core/world/roads";
|
|
3
|
+
import { hudType } from "./hudTokens.js";
|
|
3
4
|
const armRoad = (angle, width) => ({
|
|
4
5
|
path: [
|
|
5
6
|
[0, 0],
|
|
@@ -108,12 +109,12 @@ export function StreetGeometryPreview({ className }) {
|
|
|
108
109
|
for (let i = 3; i < surface.positions.length; i += 3) {
|
|
109
110
|
extent = Math.max(extent, Math.hypot(surface.positions[i], surface.positions[i + 2]));
|
|
110
111
|
}
|
|
111
|
-
return (_jsxs("section", { style: { background: "#10263a", border: "1px solid #285174", borderRadius: 10, overflow: "hidden" }, children: [_jsxs("div", { style: { padding: "8px 12px 3px", display: "flex", justifyContent: "space-between", gap: 8 }, children: [_jsx("strong", { style: { fontSize: 14 }, children: entry.name }), _jsxs("span", { style: { color: "#67e8f9", fontSize:
|
|
112
|
+
return (_jsxs("section", { style: { background: "#10263a", border: "1px solid #285174", borderRadius: 10, overflow: "hidden" }, children: [_jsxs("div", { style: { padding: "8px 12px 3px", display: "flex", justifyContent: "space-between", gap: 8 }, children: [_jsx("strong", { style: { fontSize: 14 }, children: entry.name }), _jsxs("span", { style: { color: "#67e8f9", fontSize: hudType("xs") }, children: ["extent ", extent.toFixed(1)] })] }), _jsxs("svg", { viewBox: "0 0 280 205", role: "img", "aria-label": `${entry.name} road mesh`, style: { display: "block", width: "100%", height: "auto" }, children: [_jsx("defs", { children: _jsxs("pattern", { id: patternId, width: "20", height: "20", patternUnits: "userSpaceOnUse", children: [_jsx("rect", { width: "20", height: "20", fill: "#0d2234" }), _jsx("path", { d: "M 20 0 L 0 0 0 20", fill: "none", stroke: "#1d4b68", strokeWidth: "0.8" })] }) }), _jsx("rect", { width: "280", height: "205", fill: `url(#${patternId})` }), geometry.sidewalks.flatMap((ribbon, ri) => triangles(ribbon, 2.65, 140, 102).map((points, ti) => (_jsx("polygon", { points: points, fill: "#d8c9aa", stroke: "#d8c9aa", strokeWidth: "0.35" }, `s-r-${ri}-${ti}`)))), geometry.sidewalkAprons.flatMap((junction, ji) => triangles(junction, 2.65, 140, 102).map((points, ti) => (_jsx("polygon", { points: points, fill: "#d8c9aa", stroke: "#d8c9aa", strokeWidth: "0.35" }, `s-j-${ji}-${ti}`)))), geometry.ribbons.flatMap((ribbon, ri) => triangles(ribbon, 2.65, 140, 102).map((points, ti) => (_jsx("polygon", { points: points, fill: "#52677d", stroke: "#52677d", strokeWidth: "0.35" }, `r-${ri}-${ti}`)))), triangles(surface, 2.65, 140, 102).map((points, ti) => (_jsx("polygon", { points: points, fill: "#2479a4", stroke: "#2479a4", strokeWidth: "0.35" }, `j-${ti}`))), markings.flatMap((marking, mi) => triangles(marking, 2.65, 140, 102).map((points, ti) => (_jsx("polygon", { points: points, fill: "#f8fafc" }, `m-${mi}-${ti}`))))] })] }, entry.name));
|
|
112
113
|
}), _jsxs("section", { style: { minHeight: 238, background: "linear-gradient(145deg, #18354c, #10263a)", border: "1px solid #356487", borderRadius: 10, padding: 18 }, children: [_jsx("strong", { style: { display: "block", marginBottom: 8, fontSize: 15 }, children: "Shared geometry contract" }), _jsx("p", { style: { margin: "0 0 14px", color: "#8ed8ef", fontSize: 12, lineHeight: 1.5 }, children: "Every panel is built from the same renderer-neutral meshes consumed by the Three.js city." }), [
|
|
113
114
|
["#d8c9aa", "Sidewalk annulus", "No pavement overlap"],
|
|
114
115
|
["#2479a4", "Junction apron", "Bounded by road widths"],
|
|
115
116
|
["#f8fafc", "Lane guidance", "Mouth clearance + stop lines"],
|
|
116
117
|
["#4ade80", "Terrain drape", "Every vertex sampled"],
|
|
117
118
|
["#fbbf24", "Offset continuity", "Positive and negative"],
|
|
118
|
-
].map(([color, label, detail]) => (_jsxs("div", { style: { display: "grid", gridTemplateColumns: "12px 1fr auto", alignItems: "center", gap: 9, marginTop: 9, fontSize:
|
|
119
|
+
].map(([color, label, detail]) => (_jsxs("div", { style: { display: "grid", gridTemplateColumns: "12px 1fr auto", alignItems: "center", gap: 9, marginTop: 9, fontSize: hudType("xs") }, children: [_jsx("span", { style: { width: 10, height: 10, borderRadius: 2, background: color, boxShadow: `0 0 8px ${color}` } }), _jsx("span", { style: { color: "#e2edf5" }, children: label }), _jsx("span", { style: { color: "#82a9c2" }, children: detail })] }, label)))] })] })] }) }));
|
|
119
120
|
}
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/* @jgengine/react engine stylesheet */
|
|
2
|
+
:root { --jg-motion-scale: 1; }
|
|
3
|
+
@media (prefers-reduced-motion: reduce) { :root { --jg-motion-scale: 0; } }
|
|
4
|
+
[data-reduced-motion="true"] { --jg-motion-scale: 0; }
|
|
5
|
+
@media (prefers-reduced-motion: reduce) {
|
|
6
|
+
[data-jg-hud] *, [data-jg-hud] *::before, [data-jg-hud] *::after { animation-duration: 1ms !important; animation-delay: 0ms !important; animation-iteration-count: 1 !important; transition-duration: 1ms !important; transition-delay: 0ms !important; }
|
|
7
|
+
}
|
|
8
|
+
[data-reduced-motion="true"] *, [data-reduced-motion="true"] *::before, [data-reduced-motion="true"] *::after { animation-duration: 1ms !important; animation-delay: 0ms !important; animation-iteration-count: 1 !important; transition-duration: 1ms !important; transition-delay: 0ms !important; }
|
|
9
|
+
|
|
10
|
+
@keyframes jg-fade-in { from { opacity: 0; } to { opacity: 1; } }
|
|
11
|
+
@keyframes jg-fade-out { from { opacity: 1; } to { opacity: 0; } }
|
|
12
|
+
@keyframes jg-panel-in { from { opacity: 0; transform: translateY(6px) scale(0.98); } to { opacity: 1; transform: none; } }
|
|
13
|
+
@keyframes jg-panel-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(4px) scale(0.98); } }
|
|
14
|
+
@keyframes jg-slide-up { from { transform: translateY(14px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
|
15
|
+
@keyframes jg-slide-down { from { transform: translateY(-18px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
|
16
|
+
@keyframes jg-pop { 0% { transform: scale(0.4); opacity: 0; } 60% { transform: scale(1.18); opacity: 1; } 100% { transform: scale(1); opacity: 1; } }
|
|
17
|
+
@keyframes jg-flash { 0% { opacity: 0.9; } 100% { opacity: 0; } }
|
|
18
|
+
@keyframes jg-sheen { from { background-position: -160px 0; } to { background-position: 260px 0; } }
|
|
19
|
+
@keyframes jg-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }
|
|
20
|
+
@keyframes jg-ready-glow { 0%, 100% { filter: brightness(1); } 50% { filter: brightness(1.45); } }
|
|
21
|
+
@keyframes jg-shake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-3px); } 40% { transform: translateX(3px); } 60% { transform: translateX(-2px); } 80% { transform: translateX(2px); } }
|
|
22
|
+
@keyframes jg-float-up { from { transform: translateY(0); opacity: 1; } to { transform: translateY(-46px); opacity: 0; } }
|
|
23
|
+
@keyframes jg-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
24
|
+
@keyframes jg-delta-up { 0% { color: var(--jg-success, #3fa34d); text-shadow: 0 0 10px var(--jg-success, #3fa34d); scale: 1.12; } 100% { scale: 1; } }
|
|
25
|
+
@keyframes jg-delta-down { 0% { color: var(--jg-danger, #e5484d); text-shadow: 0 0 10px var(--jg-danger, #e5484d); scale: 0.94; } 100% { scale: 1; } }
|
|
26
|
+
|
|
27
|
+
[data-jg-hud] { font-family: var(--jg-font-body, "Inter Variable", Inter, "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; }
|
|
28
|
+
:where([data-jg-hud]) :where(button, [role="button"], a[href], summary, [data-jg-interactive]):focus-visible { outline: 2px solid var(--jg-accent, #38bdf8); outline-offset: 2px; }
|
|
29
|
+
:where([data-jg-hud]) :where(button, [role="button"]) { transition: scale calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease, cubic-bezier(0.2, 0, 0, 1)), filter calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease, cubic-bezier(0.2, 0, 0, 1)); }
|
|
30
|
+
@media (hover: hover) {
|
|
31
|
+
:where([data-jg-hud]) :where(button, [role="button"]):where(:not(:disabled, [aria-disabled="true"])):hover { filter: brightness(1.08); }
|
|
32
|
+
}
|
|
33
|
+
:where([data-jg-hud]) :where(button, [role="button"]):where(:not(:disabled, [aria-disabled="true"])):active { scale: var(--jg-press-scale, 0.97); }
|
|
34
|
+
|
|
35
|
+
[data-jg-button] { appearance: none; box-sizing: border-box; position: relative; isolation: isolate; display: inline-flex; align-items: center; justify-content: center; gap: var(--jg-space-sm, 8px); min-height: 36px; padding: 0 var(--jg-space-lg, 16px); margin: 0; font-family: var(--jg-font-body, "Inter Variable", Inter, "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif); font-size: var(--jg-type-md, 14px); font-weight: 600; line-height: 1; letter-spacing: 0.01em; white-space: nowrap; color: var(--jg-text, #f5f7fa); background: linear-gradient(180deg, rgba(255,255,255,0.10), rgba(255,255,255,0.03)), var(--jg-slot-bg, rgba(12,15,21,0.78)); border: var(--jg-slot-border, 1px solid rgba(255,255,255,0.13)); border-radius: var(--jg-slot-radius, 8px); box-shadow: var(--jg-highlight, inset 0 1px 0 rgba(255,255,255,0.08)), var(--jg-elevation-raised, 0 1px 2px rgba(0,0,0,0.45), 0 4px 12px -2px rgba(0,0,0,0.4)); cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; transition: translate calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease, cubic-bezier(0.2, 0, 0, 1)), scale calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease-spring, cubic-bezier(0.34, 1.56, 0.64, 1)), box-shadow calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease, cubic-bezier(0.2, 0, 0, 1)), border-color calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease, cubic-bezier(0.2, 0, 0, 1)), background-color calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease, cubic-bezier(0.2, 0, 0, 1)); }
|
|
36
|
+
[data-jg-button]::after { content: ""; position: absolute; inset: 0; z-index: -1; border-radius: inherit; background: var(--jg-hover-overlay, rgba(255,255,255,0.07)); opacity: 0; pointer-events: none; transition: opacity calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease, cubic-bezier(0.2, 0, 0, 1)); }
|
|
37
|
+
@media (hover: hover) {
|
|
38
|
+
[data-jg-button]:not(:disabled, [aria-disabled="true"]):hover { translate: 0 var(--jg-hover-lift, -1px); box-shadow: var(--jg-highlight, inset 0 1px 0 rgba(255,255,255,0.08)), var(--jg-elevation-overlay, 0 2px 4px rgba(0,0,0,0.4), 0 10px 24px -4px rgba(0,0,0,0.5)); }
|
|
39
|
+
[data-jg-button]:not(:disabled, [aria-disabled="true"]):hover::after { opacity: 1; }
|
|
40
|
+
}
|
|
41
|
+
[data-jg-button]:not(:disabled, [aria-disabled="true"]):active { translate: 0 0; scale: var(--jg-press-scale, 0.97); box-shadow: var(--jg-highlight, inset 0 1px 0 rgba(255,255,255,0.08)), var(--jg-elevation-raised, 0 1px 2px rgba(0,0,0,0.45), 0 4px 12px -2px rgba(0,0,0,0.4)); transition-duration: calc(60ms * var(--jg-motion-scale, 1)); }
|
|
42
|
+
[data-jg-button]:not(:disabled, [aria-disabled="true"]):active::after { opacity: 1; background: var(--jg-press-overlay, rgba(0,0,0,0.14)); }
|
|
43
|
+
[data-jg-button]:focus-visible { outline: none; box-shadow: var(--jg-focus-ring, 0 0 0 2px #0a0c10, 0 0 0 4px var(--jg-accent, #38bdf8)), var(--jg-elevation-raised, 0 1px 2px rgba(0,0,0,0.45), 0 4px 12px -2px rgba(0,0,0,0.4)); }
|
|
44
|
+
[data-jg-button]:is(:disabled, [aria-disabled="true"]) { opacity: var(--jg-disabled-opacity, 0.45); cursor: not-allowed; box-shadow: none; }
|
|
45
|
+
[data-jg-button][data-variant="primary"] { color: var(--jg-surface-deep, #0a0c10); background: linear-gradient(180deg, rgba(255,255,255,0.22), rgba(255,255,255,0) 60%), var(--jg-accent, #38bdf8); border: 1px solid rgba(255,255,255,0.28); }
|
|
46
|
+
[data-jg-button][data-variant="danger"] { color: #fff; background: linear-gradient(180deg, rgba(255,255,255,0.16), rgba(255,255,255,0) 60%), var(--jg-danger, #e5484d); border: 1px solid rgba(255,255,255,0.22); }
|
|
47
|
+
[data-jg-button][data-variant="ghost"] { background: transparent; border-color: transparent; box-shadow: none; }
|
|
48
|
+
[data-jg-button][data-variant="ghost"]:not(:disabled, [aria-disabled="true"]):hover { box-shadow: none; }
|
|
49
|
+
[data-jg-button][data-size="sm"] { min-height: 28px; padding: 0 var(--jg-space-md, 12px); font-size: var(--jg-type-sm, 13px); }
|
|
50
|
+
[data-jg-button][data-size="lg"] { min-height: 44px; padding: 0 var(--jg-space-xl, 24px); font-size: var(--jg-type-lg, 16px); }
|
|
51
|
+
[data-jg-button][data-size="icon"] { min-height: 0; width: 28px; height: 28px; padding: 0; font-size: var(--jg-type-sm, 13px); }
|
|
52
|
+
[data-jg-button][data-block] { display: flex; width: 100%; }
|
|
53
|
+
|
|
54
|
+
[data-jg-panel] { box-sizing: border-box; color: var(--jg-text, #f5f7fa); font-family: var(--jg-font-body, "Inter Variable", Inter, "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif); background: var(--jg-frame-bg, rgba(12,15,21,0.9)); border: var(--jg-frame-border, 1px solid rgba(255,255,255,0.13)); border-radius: var(--jg-frame-radius, 10px); box-shadow: var(--jg-frame-glow, var(--jg-highlight, inset 0 1px 0 rgba(255,255,255,0.08)), var(--jg-elevation-raised, 0 1px 2px rgba(0,0,0,0.45), 0 4px 12px -2px rgba(0,0,0,0.4))); }
|
|
55
|
+
[data-jg-panel][data-elevation="overlay"] { box-shadow: var(--jg-frame-glow, var(--jg-highlight, inset 0 1px 0 rgba(255,255,255,0.08))), var(--jg-elevation-overlay, 0 2px 4px rgba(0,0,0,0.4), 0 10px 24px -4px rgba(0,0,0,0.5)); }
|
|
56
|
+
[data-jg-panel][data-elevation="modal"] { box-shadow: var(--jg-frame-glow, var(--jg-highlight, inset 0 1px 0 rgba(255,255,255,0.08))), var(--jg-elevation-modal, 0 4px 8px rgba(0,0,0,0.35), 0 24px 56px -12px rgba(0,0,0,0.7)); }
|
|
57
|
+
[data-jg-panel][data-state="open"] { animation: jg-panel-in calc(var(--jg-motion-base, 180ms) * var(--jg-motion-scale, 1)) var(--jg-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) both; }
|
|
58
|
+
[data-jg-panel][data-state="closed"] { animation: jg-panel-out calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease, cubic-bezier(0.2, 0, 0, 1)) both; pointer-events: none; }
|
|
59
|
+
[data-jg-window][data-state="open"] { animation: jg-panel-in calc(var(--jg-motion-base, 180ms) * var(--jg-motion-scale, 1)) var(--jg-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) both; }
|
|
60
|
+
[data-jg-window][data-state="closed"] { animation: jg-panel-out calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease, cubic-bezier(0.2, 0, 0, 1)) both; pointer-events: none !important; }
|
|
61
|
+
[data-jg-panel-header] { display: flex; align-items: center; justify-content: space-between; gap: var(--jg-space-sm, 8px); font-size: var(--jg-type-xs, 12px); font-weight: 650; letter-spacing: var(--jg-tracking-label, 0.08em); text-transform: uppercase; color: var(--jg-text-dim, #94a3b8); }
|
|
62
|
+
|
|
63
|
+
[data-modal-backdrop] { animation: jg-fade-in calc(var(--jg-motion-base, 180ms) * var(--jg-motion-scale, 1)) var(--jg-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) both; -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); }
|
|
64
|
+
[data-modal-host] > [role="dialog"] { animation: jg-panel-in calc(var(--jg-motion-base, 180ms) * var(--jg-motion-scale, 1)) var(--jg-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) both; }
|
|
65
|
+
[data-toast] { animation: jg-slide-up calc(var(--jg-motion-base, 180ms) * var(--jg-motion-scale, 1)) var(--jg-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) both; }
|
|
66
|
+
[data-levelup] { animation: jg-pop calc(var(--jg-motion-slow, 320ms) * var(--jg-motion-scale, 1)) var(--jg-ease-spring, cubic-bezier(0.34, 1.56, 0.64, 1)) both; }
|
|
67
|
+
[data-jg-delta="up"] { display: inline-block; animation: jg-delta-up calc(var(--jg-motion-slow, 320ms) * var(--jg-motion-scale, 1)) var(--jg-ease-out, cubic-bezier(0.16, 1, 0.3, 1)); }
|
|
68
|
+
[data-jg-delta="down"] { display: inline-block; animation: jg-delta-down calc(var(--jg-motion-slow, 320ms) * var(--jg-motion-scale, 1)) var(--jg-ease-out, cubic-bezier(0.16, 1, 0.3, 1)); }
|
|
69
|
+
[data-action-id] { transition: translate calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease, cubic-bezier(0.2, 0, 0, 1)), scale calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease-spring, cubic-bezier(0.34, 1.56, 0.64, 1)), border-color calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease, cubic-bezier(0.2, 0, 0, 1)), box-shadow calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease, cubic-bezier(0.2, 0, 0, 1)); }
|
|
70
|
+
@media (hover: hover) { [data-action-id][data-enabled]:hover { translate: 0 var(--jg-hover-lift, -1px); } }
|
|
71
|
+
[data-action-id][data-enabled]:active { translate: 0 0; scale: var(--jg-press-scale, 0.97); }
|
|
72
|
+
[data-action-id]:focus-visible { outline: none; box-shadow: var(--jg-focus-ring, 0 0 0 2px #0a0c10, 0 0 0 4px var(--jg-accent, #38bdf8)); }
|
package/dist/talentTree.js
CHANGED
|
@@ -3,6 +3,7 @@ import {} from "react";
|
|
|
3
3
|
import { talentTreeView, } from "@jgengine/core/game/talentTreeView";
|
|
4
4
|
import { GameIcon, iconForAction, iconForItemId, isGameIconName } from "./gameIcons.js";
|
|
5
5
|
import { HudFrame } from "./hudFrame.js";
|
|
6
|
+
import { hudType } from "./hudTokens.js";
|
|
6
7
|
const STATE_STYLE = {
|
|
7
8
|
locked: {
|
|
8
9
|
border: "1px solid rgba(148,163,184,0.28)",
|
|
@@ -127,7 +128,7 @@ function TalentNode({ node, cx, cy, size, glyph, label, onLearn, }) {
|
|
|
127
128
|
background: node.state === "maxed" ? "var(--jg-accent, #38bdf8)" : "rgba(6,10,16,0.92)",
|
|
128
129
|
color: node.state === "maxed" ? "#06121b" : "var(--jg-bar-text, #f4f6fb)",
|
|
129
130
|
border: "1px solid var(--jg-accent, #38bdf8)",
|
|
130
|
-
fontSize:
|
|
131
|
+
fontSize: hudType("xs"),
|
|
131
132
|
fontWeight: 800,
|
|
132
133
|
fontVariantNumeric: "tabular-nums",
|
|
133
134
|
display: "flex",
|
|
@@ -183,7 +184,7 @@ export function TalentTree({ view: viewProp, nodes, tree, onLearn, icon, label,
|
|
|
183
184
|
left: header.cx,
|
|
184
185
|
top: header.top,
|
|
185
186
|
transform: "translateX(-50%)",
|
|
186
|
-
fontSize:
|
|
187
|
+
fontSize: hudType("xs"),
|
|
187
188
|
fontWeight: 700,
|
|
188
189
|
letterSpacing: 1.4,
|
|
189
190
|
textTransform: "uppercase",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
/** Ease-out cubic, the default counter curve. */
|
|
3
|
+
export declare function easeOutCubic(t: number): number;
|
|
4
|
+
/** Value at progress `t` (0–1) between `from` and `to` under `ease`. */
|
|
5
|
+
export declare function tweenAt(from: number, to: number, t: number, ease?: (t: number) => number): number;
|
|
6
|
+
/** Options for {@link useTweenedValue}. */
|
|
7
|
+
export interface TweenOptions {
|
|
8
|
+
/** Tween length in ms. Default 450. */
|
|
9
|
+
durationMs?: number;
|
|
10
|
+
ease?: (t: number) => number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Animates toward `target` whenever it changes, starting from the currently displayed value, so a
|
|
14
|
+
* counter rolls instead of jumping. Snaps immediately under reduced motion or a non-finite target.
|
|
15
|
+
*
|
|
16
|
+
* @capability tweened-value animate a displayed number toward its target, honouring reduced motion
|
|
17
|
+
*/
|
|
18
|
+
export declare function useTweenedValue(target: number, options?: TweenOptions): number;
|
|
19
|
+
/** Direction of the last change, for the delta flash. */
|
|
20
|
+
export type TweenDelta = "up" | "down";
|
|
21
|
+
/** Props for {@link TweenNumber}. */
|
|
22
|
+
export interface TweenNumberProps extends TweenOptions {
|
|
23
|
+
value: number;
|
|
24
|
+
/** Formats the displayed (in-between) value. Default rounds to an integer with locale grouping. */
|
|
25
|
+
format?: (value: number) => ReactNode;
|
|
26
|
+
/** Flash the success/danger color when the value rises/falls. Default `true`. */
|
|
27
|
+
flash?: boolean;
|
|
28
|
+
className?: string;
|
|
29
|
+
style?: CSSProperties;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A counter that rolls to each new value and flashes the `--jg-success` / `--jg-danger` color with a
|
|
33
|
+
* small scale pop on the change (`data-jg-delta="up" | "down"`). Tabular digits keep its width stable.
|
|
34
|
+
*
|
|
35
|
+
* @capability tween-number rolling number readout with an up/down delta flash
|
|
36
|
+
*/
|
|
37
|
+
export declare function TweenNumber({ value, format, flash, durationMs, ease, className, style }: TweenNumberProps): ReactNode;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useReducedMotion } from "./accessibility.js";
|
|
4
|
+
import { useEngineStyles } from "./engineStyles.js";
|
|
5
|
+
/** Ease-out cubic, the default counter curve. */
|
|
6
|
+
export function easeOutCubic(t) {
|
|
7
|
+
const clamped = Math.min(1, Math.max(0, t));
|
|
8
|
+
return 1 - (1 - clamped) ** 3;
|
|
9
|
+
}
|
|
10
|
+
/** Value at progress `t` (0–1) between `from` and `to` under `ease`. */
|
|
11
|
+
export function tweenAt(from, to, t, ease = easeOutCubic) {
|
|
12
|
+
return from + (to - from) * ease(t);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Animates toward `target` whenever it changes, starting from the currently displayed value, so a
|
|
16
|
+
* counter rolls instead of jumping. Snaps immediately under reduced motion or a non-finite target.
|
|
17
|
+
*
|
|
18
|
+
* @capability tweened-value animate a displayed number toward its target, honouring reduced motion
|
|
19
|
+
*/
|
|
20
|
+
export function useTweenedValue(target, options = {}) {
|
|
21
|
+
const { durationMs = 450, ease = easeOutCubic } = options;
|
|
22
|
+
const reduced = useReducedMotion();
|
|
23
|
+
const [shown, setShown] = useState(target);
|
|
24
|
+
const shownRef = useRef(target);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const from = shownRef.current;
|
|
27
|
+
const instant = reduced || durationMs <= 0 || !Number.isFinite(target) || !Number.isFinite(from) || typeof requestAnimationFrame !== "function";
|
|
28
|
+
if (instant || from === target) {
|
|
29
|
+
shownRef.current = target;
|
|
30
|
+
setShown(target);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const start = performance.now();
|
|
34
|
+
let frame = requestAnimationFrame(function step(now) {
|
|
35
|
+
const t = Math.min(1, (now - start) / durationMs);
|
|
36
|
+
const value = t >= 1 ? target : tweenAt(from, target, t, ease);
|
|
37
|
+
shownRef.current = value;
|
|
38
|
+
setShown(value);
|
|
39
|
+
if (t < 1)
|
|
40
|
+
frame = requestAnimationFrame(step);
|
|
41
|
+
});
|
|
42
|
+
return () => cancelAnimationFrame(frame);
|
|
43
|
+
}, [target, durationMs, ease, reduced]);
|
|
44
|
+
return shown;
|
|
45
|
+
}
|
|
46
|
+
const defaultFormat = (value) => Math.round(value).toLocaleString();
|
|
47
|
+
/**
|
|
48
|
+
* A counter that rolls to each new value and flashes the `--jg-success` / `--jg-danger` color with a
|
|
49
|
+
* small scale pop on the change (`data-jg-delta="up" | "down"`). Tabular digits keep its width stable.
|
|
50
|
+
*
|
|
51
|
+
* @capability tween-number rolling number readout with an up/down delta flash
|
|
52
|
+
*/
|
|
53
|
+
export function TweenNumber({ value, format = defaultFormat, flash = true, durationMs, ease, className, style }) {
|
|
54
|
+
useEngineStyles();
|
|
55
|
+
const shown = useTweenedValue(value, { durationMs, ease });
|
|
56
|
+
const previous = useRef(value);
|
|
57
|
+
const [delta, setDelta] = useState(null);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const before = previous.current;
|
|
60
|
+
previous.current = value;
|
|
61
|
+
if (!flash || value === before)
|
|
62
|
+
return;
|
|
63
|
+
setDelta((current) => ({ dir: value > before ? "up" : "down", seq: (current?.seq ?? 0) + 1 }));
|
|
64
|
+
}, [value, flash]);
|
|
65
|
+
return (_jsx("span", { className: className, "data-jg-tween": "", style: { fontVariantNumeric: "tabular-nums", ...style }, children: _jsx("span", { "data-jg-delta": delta?.dir, children: format(shown) }, delta?.seq ?? 0) }));
|
|
66
|
+
}
|
package/dist/uiCues.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type UiCue } from "@jgengine/core/audio/uiCues";
|
|
2
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
3
|
+
import type { HudInteractEvent } from "./hudInteract.js";
|
|
4
|
+
export type { UiCue } from "@jgengine/core/audio/uiCues";
|
|
5
|
+
/** Play UI cue `cue` through the game's audio seam (`ui.<cue>` in the shell catalog). No-op outside a game. */
|
|
6
|
+
export declare function playUiCue(ctx: Pick<GameContext, "game"> | null, cue: UiCue): void;
|
|
7
|
+
/**
|
|
8
|
+
* Returns a stable `cue(name)` that plays a UI sound: click, hover, open, close, confirm, deny, notify.
|
|
9
|
+
* The shell ships soft procedural defaults on the "ui" bus (volume in Settings, muted with master);
|
|
10
|
+
* a game replaces one by declaring `audio.sounds["ui.click"]` or `audio.uiCues`. Safe outside a GameProvider.
|
|
11
|
+
*
|
|
12
|
+
* @capability ui-sound-cues play click/hover/open/close/confirm/deny/notify UI sounds from any component
|
|
13
|
+
*/
|
|
14
|
+
export declare function useUiCue(): (cue: UiCue) => void;
|
|
15
|
+
/**
|
|
16
|
+
* The UI cue an engine HUD primitive interaction plays, or `null` for none: activating plays the
|
|
17
|
+
* primitive's `cue` when it names a {@link UiCue} (e.g. `cue="confirm"`), else `click`; hovering a
|
|
18
|
+
* button or action slot plays `hover`; panels and windows play `open`/`close`. Press and focus are silent.
|
|
19
|
+
*/
|
|
20
|
+
export declare function uiCueForInteraction(event: HudInteractEvent): UiCue | null;
|
package/dist/uiCues.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { UI_CUES, uiCueSoundId } from "@jgengine/core/audio/uiCues";
|
|
3
|
+
import { useOptionalGameContext } from "./provider.js";
|
|
4
|
+
/** Play UI cue `cue` through the game's audio seam (`ui.<cue>` in the shell catalog). No-op outside a game. */
|
|
5
|
+
export function playUiCue(ctx, cue) {
|
|
6
|
+
ctx?.game.audio.play(uiCueSoundId(cue));
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Returns a stable `cue(name)` that plays a UI sound: click, hover, open, close, confirm, deny, notify.
|
|
10
|
+
* The shell ships soft procedural defaults on the "ui" bus (volume in Settings, muted with master);
|
|
11
|
+
* a game replaces one by declaring `audio.sounds["ui.click"]` or `audio.uiCues`. Safe outside a GameProvider.
|
|
12
|
+
*
|
|
13
|
+
* @capability ui-sound-cues play click/hover/open/close/confirm/deny/notify UI sounds from any component
|
|
14
|
+
*/
|
|
15
|
+
export function useUiCue() {
|
|
16
|
+
const ctx = useOptionalGameContext();
|
|
17
|
+
return useCallback((cue) => playUiCue(ctx, cue), [ctx]);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The UI cue an engine HUD primitive interaction plays, or `null` for none: activating plays the
|
|
21
|
+
* primitive's `cue` when it names a {@link UiCue} (e.g. `cue="confirm"`), else `click`; hovering a
|
|
22
|
+
* button or action slot plays `hover`; panels and windows play `open`/`close`. Press and focus are silent.
|
|
23
|
+
*/
|
|
24
|
+
export function uiCueForInteraction(event) {
|
|
25
|
+
switch (event.kind) {
|
|
26
|
+
case "activate":
|
|
27
|
+
return event.cue !== undefined && UI_CUES.includes(event.cue) ? event.cue : "click";
|
|
28
|
+
case "hover":
|
|
29
|
+
return event.source === "button" || event.source === "action" ? "hover" : null;
|
|
30
|
+
case "open":
|
|
31
|
+
return "open";
|
|
32
|
+
case "close":
|
|
33
|
+
return "close";
|
|
34
|
+
default:
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/dist/waveHud.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useReducer } from "react";
|
|
3
3
|
import { ExperienceBar } from "./bars.js";
|
|
4
4
|
import { HudFrame } from "./hudFrame.js";
|
|
5
|
+
import { hudType } from "./hudTokens.js";
|
|
5
6
|
/**
|
|
6
7
|
* Subscribe to a {@link WaveRunner} and re-render whenever it notifies (wave,
|
|
7
8
|
* budget, spawns, alert change). Returns the runner's pooled {@link WaveView}. The
|
|
@@ -24,7 +25,7 @@ function resolveTheme(theme) {
|
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
function Stat({ caption, value, muted }) {
|
|
27
|
-
return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 1 }, children: [_jsx("span", { style: { fontSize:
|
|
28
|
+
return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 1 }, children: [_jsx("span", { style: { fontSize: hudType("xs"), letterSpacing: 1, textTransform: "uppercase", color: muted }, children: caption }), _jsx("span", { style: { fontSize: 15, fontWeight: 800, fontVariantNumeric: "tabular-nums" }, children: value })] }));
|
|
28
29
|
}
|
|
29
30
|
/**
|
|
30
31
|
* A drop-in wave/spawn HUD panel over a {@link WaveRunner}: a big "WAVE N" label,
|
package/dist/waypointMarkers.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useReducer, useEffect } from "react";
|
|
3
3
|
import { layoutScreenMarker, } from "@jgengine/core/ui/screenMarkers";
|
|
4
4
|
import { formatDistance } from "@jgengine/core/format/distance";
|
|
5
|
+
import { hudType } from "./hudTokens.js";
|
|
5
6
|
/**
|
|
6
7
|
* Subscribe to a {@link WaypointTracker} and re-render on every change, returning
|
|
7
8
|
* its current waypoints. Pass a plain array instead and it is returned as-is
|
|
@@ -69,7 +70,7 @@ function chipStyle(theme, color) {
|
|
|
69
70
|
border: `1px solid ${color}`,
|
|
70
71
|
color: theme.text,
|
|
71
72
|
fontFamily: theme.fontFamily,
|
|
72
|
-
fontSize:
|
|
73
|
+
fontSize: hudType("xs"),
|
|
73
74
|
fontWeight: 700,
|
|
74
75
|
whiteSpace: "nowrap",
|
|
75
76
|
textShadow: "0 1px 2px rgba(0,0,0,0.9)",
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "React UI layer for JGengine: GameProvider, hooks, and headless primitives over @jgengine/core.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"sideEffects":
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"*.css"
|
|
9
|
+
],
|
|
8
10
|
"repository": {
|
|
9
11
|
"type": "git",
|
|
10
12
|
"url": "git+https://github.com/Noisemaker111/jgengine.git",
|
|
@@ -20,18 +22,20 @@
|
|
|
20
22
|
"types": "./dist/index.d.ts",
|
|
21
23
|
"default": "./dist/index.js"
|
|
22
24
|
},
|
|
25
|
+
"./styles.css": "./dist/styles.css",
|
|
23
26
|
"./*": {
|
|
24
27
|
"types": "./dist/*.d.ts",
|
|
25
28
|
"default": "./dist/*.js"
|
|
26
29
|
}
|
|
27
30
|
},
|
|
28
31
|
"scripts": {
|
|
29
|
-
"build": "tsgo -p tsconfig.build.json && bun ../../scripts/fix-extensions.ts dist",
|
|
30
|
-
"check-types": "
|
|
31
|
-
"test": "bun test src"
|
|
32
|
+
"build": "tsgo -p tsconfig.build.json && bun ../../scripts/fix-extensions.ts dist && bun scripts/styles.ts",
|
|
33
|
+
"check-types": "tsgo --noEmit -p tsconfig.json",
|
|
34
|
+
"test": "bun test src",
|
|
35
|
+
"styles": "bun scripts/styles.ts"
|
|
32
36
|
},
|
|
33
37
|
"dependencies": {
|
|
34
|
-
"@jgengine/core": "^0.
|
|
38
|
+
"@jgengine/core": "^0.23.0"
|
|
35
39
|
},
|
|
36
40
|
"peerDependencies": {
|
|
37
41
|
"react": "^19.0.0",
|
package/skills/jgengine/SKILL.md
CHANGED
|
@@ -53,6 +53,7 @@ Load only rows the target needs. Do not preload every domain.
|
|
|
53
53
|
| Need | Skill | Owner |
|
|
54
54
|
| --- | --- | --- |
|
|
55
55
|
| player promise, pillars, verbs, loops, economy, progression, balance | `game-design` | experience and rule intent |
|
|
56
|
+
| substantial existing-game improvement across depth, world and presentation | `game-improvement` | ordered improvement plan and per-game completion |
|
|
56
57
|
| metrics, topology, encounters, navigation, spatial pacing | `level-design` | playable space intent |
|
|
57
58
|
| scene placement, terrain painting, paths, asset authoring | `jgengine-editor` | editor document and authoring tools |
|
|
58
59
|
| runtime world, movement, input, interaction, AI, navigation, audio | `jgengine-world` | spatial simulation and world runtime |
|