@jgengine/react 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/actionHud.d.ts +5 -5
- package/dist/actionHud.js +14 -12
- package/dist/bars.d.ts +103 -0
- package/dist/bars.js +180 -0
- package/dist/barsPreview.d.ts +4 -0
- package/dist/barsPreview.js +47 -0
- package/dist/components.d.ts +8 -1
- package/dist/components.js +8 -1
- package/dist/hooks.d.ts +20 -0
- package/dist/hooks.js +38 -0
- package/dist/hud.d.ts +21 -4
- package/dist/hud.js +38 -27
- package/dist/hudFrame.d.ts +47 -0
- package/dist/hudFrame.js +80 -0
- package/dist/hudLayout.d.ts +13 -1
- package/dist/hudLayout.js +18 -12
- package/dist/hudLayoutPersist.d.ts +23 -0
- package/dist/hudLayoutPersist.js +19 -0
- package/dist/hudTheme.d.ts +76 -0
- package/dist/hudTheme.js +156 -0
- package/dist/hudThemePreview.d.ts +4 -0
- package/dist/hudThemePreview.js +37 -0
- package/dist/iconTreatment.d.ts +50 -0
- package/dist/iconTreatment.js +84 -0
- package/dist/iconsPreview.d.ts +4 -0
- package/dist/iconsPreview.js +29 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/map.d.ts +72 -10
- package/dist/map.js +84 -15
- package/dist/selectionHud.d.ts +11 -5
- package/dist/selectionHud.js +11 -5
- package/dist/slots.d.ts +44 -0
- package/dist/slots.js +37 -0
- package/package.json +2 -2
- package/skills/jgengine/SKILL.md +7 -4
- package/skills/jgengine/api.md +172 -79
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +15 -10
- package/skills/jgengine-ui/api.md +202 -47
- package/skills/jgengine-ui/capabilities.md +71 -2
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +20 -7
- package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
- package/skills/jgengine/reference-cartridge.md +0 -72
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Reach for these before hand-rolling. Each row is *the thing you need* → *the primitive that already does it*.
|
|
6
6
|
|
|
7
|
-
## action-bar —
|
|
7
|
+
## action-bar — optional skinned action-bar composition over the headless model — games own chrome
|
|
8
8
|
|
|
9
9
|
- `ActionBar` (function) · `import { ActionBar } from "@jgengine/react"`
|
|
10
10
|
|
|
@@ -61,6 +61,67 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
61
61
|
|
|
62
62
|
- `useEventMeter` (function) · `import { useEventMeter } from "@jgengine/react"`
|
|
63
63
|
|
|
64
|
+
## heightfield-mesh-update — in-place partial update of a displaced ground plane mesh
|
|
65
|
+
|
|
66
|
+
- `displaceHeightfieldGeometry` (function) · `import { displaceHeightfieldGeometry } from "@jgengine/shell/terrain"`
|
|
67
|
+
|
|
68
|
+
## hud-canvas — the HUD root surface — design-resolution scaling plus live panel-placement editing (F2+C)
|
|
69
|
+
|
|
70
|
+
- `HudCanvas` (function) · `import { HudCanvas } from "@jgengine/react"`
|
|
71
|
+
|
|
72
|
+
## hud-clock — in-game time-of-day readout bound to the sim clock
|
|
73
|
+
|
|
74
|
+
- `Clock` (function) · `import { Clock } from "@jgengine/react"`
|
|
75
|
+
|
|
76
|
+
## hud-coins — currency counter widget bound to a currency id
|
|
77
|
+
|
|
78
|
+
- `Coins` (function) · `import { Coins } from "@jgengine/react"`
|
|
79
|
+
|
|
80
|
+
## hud-frame — optional framed panel skins (glass/plate/retro/themed) — games own product chrome
|
|
81
|
+
|
|
82
|
+
- `HudFrame` (function) · `import { HudFrame } from "@jgengine/react"`
|
|
83
|
+
|
|
84
|
+
## hud-hotbar — optional inventory hotbar building block — games own slot chrome
|
|
85
|
+
|
|
86
|
+
- `Hotbar` (function) · `import { Hotbar } from "@jgengine/react"`
|
|
87
|
+
|
|
88
|
+
## hud-icons — painted-icon treatment for hotbar/action slots — glyph + gradient + count badge
|
|
89
|
+
|
|
90
|
+
- `IconSchool` (type) · `import { IconSchool } from "@jgengine/react"`
|
|
91
|
+
|
|
92
|
+
## hud-layout — persistent, live-editable HUD panel layout state for `HudCanvas`
|
|
93
|
+
|
|
94
|
+
- `useHudLayout` (function) · `import { useHudLayout } from "@jgengine/react"`
|
|
95
|
+
|
|
96
|
+
## hud-layout-persist — inject a host callback so canvas HUD edits commit into the scene document
|
|
97
|
+
|
|
98
|
+
- `HudLayoutPersistProvider` (function) · `import { HudLayoutPersistProvider } from "@jgengine/react"`
|
|
99
|
+
|
|
100
|
+
## hud-layout-persist-hook — read the host panel-commit port from a HUD layout widget
|
|
101
|
+
|
|
102
|
+
- `useHudLayoutPersist` (function) · `import { useHudLayoutPersist } from "@jgengine/react"`
|
|
103
|
+
|
|
104
|
+
## hud-panel — anchored HUD slot — drop any widget in a named, player-movable panel
|
|
105
|
+
|
|
106
|
+
- `HudPanel` (function) · `import { HudPanel } from "@jgengine/react"`
|
|
107
|
+
|
|
108
|
+
## hud-slots — atomic themed equipment/weapon/armor/potion slot grids with painted icons
|
|
109
|
+
|
|
110
|
+
- `SlotItem` (interface) · `import { SlotItem } from "@jgengine/react"`
|
|
111
|
+
|
|
112
|
+
## hud-stat-bar — optional stat-bound bar building block (health, mana, stamina) — prefer game-owned chrome
|
|
113
|
+
|
|
114
|
+
- `StatBar` (function) · `import { StatBar } from "@jgengine/react"`
|
|
115
|
+
|
|
116
|
+
## hud-theme — game-authored token object for bars, frames, slots, and minimap ring
|
|
117
|
+
|
|
118
|
+
- `HudThemePalette` (interface) · `import { HudThemePalette } from "@jgengine/react"`
|
|
119
|
+
|
|
120
|
+
## hud-vitals — atomic purpose-named vitals bars (token-themed parts, not a finished HUD)
|
|
121
|
+
|
|
122
|
+
- `BarTokens` (interface) · `import { BarTokens } from "@jgengine/react"`
|
|
123
|
+
- `barTokens` (function) · `import { barTokens } from "@jgengine/react"`
|
|
124
|
+
|
|
64
125
|
## key-hint — keyboard/mouse control hint that hides itself on touch
|
|
65
126
|
|
|
66
127
|
- `KeyHint` (function) · `import { KeyHint } from "@jgengine/react"`
|
|
@@ -69,6 +130,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
69
130
|
|
|
70
131
|
- `Keycap` (function) · `import { Keycap } from "@jgengine/react"`
|
|
71
132
|
|
|
133
|
+
## live-markers — a self-ticking MarkerSet kept in sync from the live scene each HUD tick — pass a rebuild(markers, ctx) that clears+repopulates, no hand-rolled setInterval
|
|
134
|
+
|
|
135
|
+
- `useLiveMarkers` (function) · `import { useLiveMarkers } from "@jgengine/react"`
|
|
136
|
+
|
|
72
137
|
## minimap — framed circular minimap with terrain bake, fog, markers, and facing arrow
|
|
73
138
|
|
|
74
139
|
- `Minimap` (function) · `import { Minimap } from "@jgengine/react"`
|
|
@@ -81,6 +146,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
81
146
|
|
|
82
147
|
- `MinimapPanel` (function) · `import { MinimapPanel } from "@jgengine/react"`
|
|
83
148
|
|
|
149
|
+
## minimap-track-hud — horizontal linear track minimap (corridor/route progress) — colored zone spans and gate/player pips positioned by 0..1 fraction, sharing chrome with the radial Minimap
|
|
150
|
+
|
|
151
|
+
- `MinimapTrack` (function) · `import { MinimapTrack } from "@jgengine/react"`
|
|
152
|
+
|
|
84
153
|
## nameplates — billboarded name + HP bar over nearby entities
|
|
85
154
|
|
|
86
155
|
- `WorldNameplates` (function) · `import { WorldNameplates } from "@jgengine/shell/world/WorldHud"`
|
|
@@ -113,7 +182,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
113
182
|
|
|
114
183
|
- `useSelectionView` (function) · `import { useSelectionView } from "@jgengine/react"`
|
|
115
184
|
|
|
116
|
-
## selection-panel —
|
|
185
|
+
## selection-panel — optional selection-panel composition: summary + portrait strip / group chips
|
|
117
186
|
|
|
118
187
|
- `SelectionPanel` (function) · `import { SelectionPanel } from "@jgengine/react"`
|
|
119
188
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Add the minimap to an existing React game
|
|
2
|
+
|
|
3
|
+
Use this recipe when a React game already owns its entities in an array, ECS
|
|
4
|
+
query, Zustand/Redux-style store, or another observable collection. JGengine
|
|
5
|
+
only needs a display view of those records; do not copy them into a
|
|
6
|
+
`MarkerSet`.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
Keep the React and React DOM versions already used by the project, then add the
|
|
11
|
+
two focused JGengine packages:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
bun add @jgengine/core @jgengine/react
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The minimap imports neither Three.js nor React Three Fiber and does not require
|
|
18
|
+
`GameProvider`.
|
|
19
|
+
|
|
20
|
+
## Static entity arrays
|
|
21
|
+
|
|
22
|
+
Project an existing array directly. Keep the array stable with the same memo or
|
|
23
|
+
selector the project already uses.
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { useMemo } from "react";
|
|
27
|
+
import { Minimap } from "@jgengine/react/map";
|
|
28
|
+
|
|
29
|
+
interface Unit {
|
|
30
|
+
id: string;
|
|
31
|
+
x: number;
|
|
32
|
+
z: number;
|
|
33
|
+
team: "ally" | "enemy";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function MapHud({
|
|
37
|
+
units,
|
|
38
|
+
cameraTarget,
|
|
39
|
+
}: {
|
|
40
|
+
units: readonly Unit[];
|
|
41
|
+
cameraTarget: { x: number; z: number };
|
|
42
|
+
}) {
|
|
43
|
+
const markers = useMemo(
|
|
44
|
+
() =>
|
|
45
|
+
units.map((unit) => ({
|
|
46
|
+
id: unit.id,
|
|
47
|
+
position: [unit.x, 0, unit.z] as const,
|
|
48
|
+
kind: unit.team,
|
|
49
|
+
})),
|
|
50
|
+
[units],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Minimap
|
|
55
|
+
markers={markers}
|
|
56
|
+
center={[cameraTarget.x, cameraTarget.z]}
|
|
57
|
+
worldRadius={120}
|
|
58
|
+
className="game-minimap"
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Only `id` and `position` are required. `kind`, `label`, `heading`, and `meta`
|
|
65
|
+
are optional display data; lifecycle timestamps are not part of this view.
|
|
66
|
+
|
|
67
|
+
## Observable external store
|
|
68
|
+
|
|
69
|
+
Create the adapter once per store. The source caches its projected marker array
|
|
70
|
+
until the external store emits, so React snapshot reads do not remap the world
|
|
71
|
+
on every frame.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { useMemo } from "react";
|
|
75
|
+
import { createMarkerSource } from "@jgengine/core/world/markers";
|
|
76
|
+
import { Minimap } from "@jgengine/react/map";
|
|
77
|
+
|
|
78
|
+
interface Unit {
|
|
79
|
+
id: string;
|
|
80
|
+
x: number;
|
|
81
|
+
z: number;
|
|
82
|
+
team: string;
|
|
83
|
+
name: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface ExistingWorldStore {
|
|
87
|
+
subscribe(listener: () => void): () => void;
|
|
88
|
+
getState(): { units: readonly Unit[] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function ExistingGameMinimap({
|
|
92
|
+
world,
|
|
93
|
+
cameraTarget,
|
|
94
|
+
}: {
|
|
95
|
+
world: ExistingWorldStore;
|
|
96
|
+
cameraTarget: { x: number; z: number };
|
|
97
|
+
}) {
|
|
98
|
+
const markers = useMemo(
|
|
99
|
+
() =>
|
|
100
|
+
createMarkerSource({
|
|
101
|
+
subscribe: (listener) => world.subscribe(listener),
|
|
102
|
+
getSnapshot: () => world.getState().units,
|
|
103
|
+
project: (unit) => ({
|
|
104
|
+
id: unit.id,
|
|
105
|
+
position: [unit.x, 0, unit.z] as const,
|
|
106
|
+
kind: unit.team,
|
|
107
|
+
label: unit.name,
|
|
108
|
+
}),
|
|
109
|
+
}),
|
|
110
|
+
[world],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<Minimap
|
|
115
|
+
markers={markers}
|
|
116
|
+
center={[cameraTarget.x, cameraTarget.z]}
|
|
117
|
+
worldRadius={120}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
For SSR, make the store's first client snapshot match the server snapshot. When
|
|
124
|
+
they differ structurally, pass `getServerSnapshot` to `createMarkerSource` and
|
|
125
|
+
read the same serialized initial units used to hydrate the external store.
|
|
126
|
+
|
|
127
|
+
## Ownership
|
|
128
|
+
|
|
129
|
+
The existing project still owns entities, updates, persistence, simulation,
|
|
130
|
+
camera, renderer, input, scheduling, and the subscription. JGengine owns only
|
|
131
|
+
the cached entity-to-marker projection, core world-to-map projection math, and
|
|
132
|
+
the React SVG presentation. Save and restore the project's unit state as usual;
|
|
133
|
+
markers are derived data and need no parallel save format.
|
|
134
|
+
|
|
135
|
+
Use `kindStyles`, `className`, and `style` to skin the production `Minimap`.
|
|
136
|
+
When the project owns every chrome element, compose `MinimapChrome` inside its
|
|
137
|
+
own SVG instead of adopting the framed default.
|
|
138
|
+
|
|
139
|
+
## Common traps
|
|
140
|
+
|
|
141
|
+
- Keep `getSnapshot()` array identity stable until `subscribe()` emits. This is
|
|
142
|
+
the React external-store contract.
|
|
143
|
+
- Create `createMarkerSource` outside render or in `useMemo`; recreating it each
|
|
144
|
+
render discards its projection cache.
|
|
145
|
+
- Pass world coordinates, not Three.js screen coordinates. Marker positions are
|
|
146
|
+
`[x, y, z]`; the minimap projects X/Z.
|
|
147
|
+
- Do not poll the source from a render loop. Notify subscribers when the
|
|
148
|
+
caller-owned collection changes.
|
|
149
|
+
- Do not add `createdAt` or `expiresAt` unless the project intentionally adopts
|
|
150
|
+
`MarkerSet` lifecycle behavior.
|
|
@@ -4,9 +4,11 @@ This reference defines the required visual and interaction quality for JGengine
|
|
|
4
4
|
|
|
5
5
|
## The rule
|
|
6
6
|
|
|
7
|
-
A game must visually own its viewport. It must not resemble a dashboard, landing page, documentation page,
|
|
7
|
+
A game must visually own its viewport **and its entire interface**. Presentation is game content: each title designs a custom HUD, menus, feedback, and art direction. It must not resemble a dashboard, landing page, documentation page, ordinary responsive web app, **or another JGengine game's default widget stack**.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
The engine does not ship a finished game UI. `@jgengine/react` offers headless data/layout/accessibility seams and optional building blocks. Dropping in stock `StatBar` / `Hotbar` / `Coins` / glass `HudFrame` / theme presets without game-specific composition is incomplete work — not "using the engine correctly."
|
|
10
|
+
|
|
11
|
+
HTML and React are valid implementation tools. Website visual grammar is not the default. Engine default chrome is not the default either.
|
|
10
12
|
|
|
11
13
|
## 1. Start with a concise UI art direction
|
|
12
14
|
|
|
@@ -95,13 +97,13 @@ The active game should behave like an application mode:
|
|
|
95
97
|
Recommended layer contract:
|
|
96
98
|
|
|
97
99
|
```tsx
|
|
98
|
-
<
|
|
100
|
+
<GameHost>
|
|
99
101
|
<WorldLayer />
|
|
100
102
|
<HudLayer />
|
|
101
103
|
<ControlLayer />
|
|
102
104
|
<ScreenLayer />
|
|
103
105
|
<SystemLayer />
|
|
104
|
-
</
|
|
106
|
+
</GameHost>
|
|
105
107
|
```
|
|
106
108
|
|
|
107
109
|
- `WorldLayer`: game renderer
|
|
@@ -142,9 +144,11 @@ Design-resolution fit is on by default for every game: each `HudCanvas` auto-sca
|
|
|
142
144
|
|
|
143
145
|
**Overflow is an error, not a style note.** `HudCanvas` measures every `HudPanel` against the viewport at runtime; offenders land in a `data-hud-overflow` attribute (and a console warning), and `bun run shoot <game> --device mobile` (or `both`) exits non-zero naming the escaping panels. A game is not mobile-done while shoot reports HUD OVERFLOW.
|
|
144
146
|
|
|
147
|
+
**Authoring placement lives in the editor.** HUD panel placement is authored from the scene editor's **HUD** mode (F2+E → **HUD** toolbar button), not a separate devtools chord — the button mounts the game's real `HudCanvas` over the frozen world with drag/resize editing on. Dragging a panel writes an undoable `setUiPanel` patch into the scene document's `ui.panels` section (the same document the `canvas_move_panel` / `canvas_resize_panel` RPC verbs still target), so authored layout ships in `editor.scene.json`. The author's canvas **Done** button (or Esc / F2+C / F2+E) leaves HUD mode. Per-panel resize axes come from the panel `type`'s `registerHudPanelType` spec.
|
|
148
|
+
|
|
145
149
|
### Phase-gated HUD visibility (`showDuring`)
|
|
146
150
|
|
|
147
|
-
`HudCanvas` and `HudPanel` take an opt-in `showDuring?: GamePhase[]` — the element renders only while `gamePhase` is one of the listed phases (`"menu" | "playing" | "paused" | "ended"`), so a
|
|
151
|
+
`HudCanvas` and `HudPanel` take an opt-in `showDuring?: GamePhase[]` — the element renders only while `gamePhase` is one of the listed phases (`"menu" | "playing" | "paused" | "ended"`), so a game hides its HUD under menu/end overlays without hand-rolling a phase check. Omit it for the default always-visible behavior. Gate the whole HUD with `<HudCanvas showDuring={["playing"]}>`, or keep a single results panel up with a per-`HudPanel` value. The read degrades to `"playing"` when the component renders outside a `GameProvider` (component showcases, previews), so it never throws there.
|
|
148
152
|
|
|
149
153
|
### Shared viewport allocation (`GameViewportProvider`, region collision)
|
|
150
154
|
|
|
@@ -220,7 +224,16 @@ Prefer small headless or lightly styled primitives over a giant universal design
|
|
|
220
224
|
|
|
221
225
|
A primitive should expose game-oriented choices such as shape, material, urgency, placement, hierarchy, entry motion, icon treatment, compactness, and diegetic-versus-overlay presentation.
|
|
222
226
|
|
|
223
|
-
**
|
|
227
|
+
**Portable minimap adoption.** Existing React games pass `MarkerView[]`, a
|
|
228
|
+
`MarkerSource`, or their existing `MarkerSet` to `Minimap`/`Compass`/`WorldMap`.
|
|
229
|
+
Use `createMarkerSource` (`@jgengine/core/world/markers`) to cache a projection
|
|
230
|
+
from an external ECS/store snapshot; it needs no `GameProvider`, Three.js, or
|
|
231
|
+
React Three Fiber. Follow [the portable minimap recipe](recipes/portable-minimap.md)
|
|
232
|
+
instead of mirroring external entities into `createMarkerSet`.
|
|
233
|
+
|
|
234
|
+
**Minimap** is already shipped, not hand-rolled: `Minimap` / `WorldMap` / `Compass` from `@jgengine/react/map` render a framed SVG minimap over a `MarkerSet` (`createMarkerSet`, `@jgengine/core/world/markers`) and optional `FogField`, projecting via `@jgengine/core/world/minimap` (`projectToMinimap`, `headingToBearing`, edge-clamp). A game feeds flat props (`markers`, `center: [x,z]`, `worldRadius`, `size`, `facingYaw`, `rotate`) and repopulates the marker set each HUD tick from live entities — no custom canvas painter. `MinimapPanel` (same module) composes zone-label + clock header slots and a `Compass` strip around a `Minimap` — pass `zoneLabel`/`clock` nodes (e.g. from `useGameClock().calendar`) and `showCompass`/`compassProps`; omit either header slot and it collapses, `showCompass={false}` drops the strip. **Baked terrain background**: to draw the authored terrain under the markers, pass `background={document.minimap?.background}` and `mapBounds={document.minimap?.bounds}` to `Minimap`/`WorldMap` (or `MinimapPanel`, which forwards both through) — the editor's `bake_minimap` action stores a top-down terrain PNG + world bounds on `EditorDocument.minimap` (see `jgengine-editor`), and runtime reads that stored PNG with no re-rasterization. Both are undefined until baked, so an unbaked game renders the plain map unchanged. Rolling a **custom** minimap around your own course/route content (not the full `Minimap` panel)? `MinimapChrome` is the headless chrome layer other games were re-drawing by hand: nest it inside your own `<svg>` for the ring `frame`, and pass `markers: MinimapChromeMarker[]` (`{ id, position, heading?, color?, radius?, glyph? }`) for edge-clamped dots that draw as a rotated arrow whenever a marker carries a `heading` (the compass-arrow / player-blip layer) — never hand-roll `clampToMinimapEdge` + a `<polygon>` again. **Nameplates**: `WorldNameplates` (`@jgengine/shell/world/WorldHud`) billboards a name + optional 78×6px HP bar over every nearby non-local entity — turn it on declaratively via `defineGame({ nameplates: { roles, maxDistance } })` (mirrors `worldHealthBars`) rather than mounting it by hand; skin every part via `className`/`data-*` slots (`nameplateClassName`, `nameClassName`, `barClassName`, `fillClassName`) or swap the whole markup with `renderNameplate`. **Selection rings**: `WorldObjectHighlights` (`@jgengine/shell/world/WorldHud`) draws a ground ring over every `ctx.scene.object.selection`-ed placed object — the object-layer counterpart to `WorldEntityBars`/`WorldNameplates`; mount it once in the game's scene (`color`/`radius`/`y` props, `radius` otherwise derived from the object's catalog `halfExtents`) instead of hand-rolling a selection highlight through `WorldOverlay` against external state. **Playing cards** are already shipped, not hand-rolled: `CardFace` / `StackedPile` / `CardStack` (with `Suit`, `Rank`, `PlayingCard`) from `@jgengine/react/cards` render a French-deck face (rank + suit pips, patterned back when `faceDown`) and an overlapping fan. `StackedPile` composes the existing drag system (`useDragLayer`/`DraggableCard`/`DropZone`) — pass a `layer` for drag and `dropId` for a drop target; `CardStack` binds a fan to a headless `CardPileState` zone (`@jgengine/core/cards/cardPile`). Never draw cards on a canvas or reinvent pointer handling. **Swing timer**: `swingTimerState(player, target, prevPeriod, prevTimer)` (`@jgengine/core/ui/swingTimer`) is the pure, parameter-in/next-state-out core for a melee swing bar (recovers period on the reset edge; hidden unless auto-attacking a live non-object target) — thread the returned `nextPeriod`/`nextTimer` back via refs, render a thin `Meter`.
|
|
235
|
+
|
|
236
|
+
**HUD widgets split into three reusable layers** — data, renderer, chrome — so a second presentation drops in without a forked widget. **Chrome**: `HudFrame` (`@jgengine/react/hudFrame`, `hud-frame` capability) is the single framed-panel primitive — `<HudFrame variation="glass"|"plate"|"retro" shape="rounded"|"circle"|"square" title aside padding width style>` renders a `data-hud-frame` div with an optional small-caps `title`/`aside` header; `glass` is byte-identical to the built-in widgets' dark-glass panel, `plate` a heavier opaque plate, `retro` a hard black-outlined frame. Need only the CSS on your own element? spread `hudFrameStyle(variation, shape)`. Reach for it instead of hand-rolling a bordered panel `<div>` per widget. **Renderer**: `MinimapTrack` (`@jgengine/react/map`, `minimap-track-hud` capability) is a structurally-different second minimap — a horizontal linear progress rail (vs the radial `Minimap`) with colored zone `spans` and gate/exit/player `pips` placed by 0..1 fraction, driven by core `trackFraction([x,z], { from, to })` (`@jgengine/core/world/minimapTrack`, `minimap-track` capability — the 1D analogue of `projectToMinimap`). It reads no store; the caller supplies fractions. Use it for corridor/route progress; use the radial `Minimap` for open worlds — both share `HudFrame` chrome. **Data**: `useLiveMarkers(rebuild, { intervalMs?, deps? })` (`@jgengine/react/hooks`, `live-markers` capability) returns a self-ticking `MarkerSet` that each HUD tick clears and re-runs your `rebuild(markers, ctx)` against the live scene — replaces the hand-rolled `useState` ticker + `useEffect` + `createMarkerSet` + `markers.clear()` + rescan boilerplate; feed the returned set straight into `Minimap`/`MinimapPanel`.
|
|
224
237
|
|
|
225
238
|
**Heat/streak gauges** are already shipped, not hand-rolled `clamp(value/max)`: build the gauge with `createEventMeter` (`@jgengine/core/stats/eventMeter`, `event-meter` capability — ult/adrenaline `"hold"` mode or streak/combo `"reset"` mode over tagged event gains) and bind it to a component with `useEventMeter(meter)` (`@jgengine/react`, `event-meter-hud` capability) — it self-ticks a heartbeat and returns `{ value, fraction, tier, ready }` for a `Meter`/`BossBar`-style fill, no manual `setInterval` polling of `meter.value()`.
|
|
226
239
|
|
|
@@ -477,7 +490,7 @@ Require revision when any of these are true:
|
|
|
477
490
|
|
|
478
491
|
## 15. Compact implementation API appendix
|
|
479
492
|
|
|
480
|
-
Use the main `jgengine` skill for the authoritative engine API routing. The React package exposes `GameProvider`, hooks, and headless primitives from `@jgengine/react` and its documented subpaths. Common hooks include player/game state, entities, stats, inventory, quests, prompts, clocks, markers, fog, and engine stores. The shell provides `
|
|
493
|
+
Use the main `jgengine` skill for the authoritative engine API routing. The React package exposes `GameProvider`, hooks, and headless primitives from `@jgengine/react` and its documented subpaths. Common hooks include player/game state, entities, stats, inventory, quests, prompts, clocks, markers, fog, and engine stores. The shell provides `GameHost` (the game mount), input integration, devtools, and `GameUiPreview`; `@jgengine/shell/GamePlayerShell` (the internal player `GameHost` composes) also re-exports the movement primitives it wires up internally (`applyMotionImpulses`, `nearbyObstacles`, `resolvePhysicsTuning`, `hasEnvironmentTerrain`) for a host that needs to replicate its physics loop, and `@jgengine/shell/terrain` re-exports the `jgengine-world` field builders (`arenaField`, `flatField`, `fractalNoise`, `resolveGroundStep`, `valueNoise`, `withNormal`) alongside its own `CarvedTerrain`/`GrassField` components so a shell consumer never needs a second import from core.
|
|
481
494
|
|
|
482
495
|
Use these APIs to bind state; do not let API wiring dictate visual composition. Keybind labels should derive from the game’s binding table, and UI actions should dispatch through game commands rather than existing only as click handlers.
|
|
483
496
|
|
|
@@ -27,7 +27,7 @@ Score all ten from one `--mode play` shot plus deterministic world/document evid
|
|
|
27
27
|
6. **Materials/textures** — terrain and props carry texture and color variation (`environment({ sculpt, materialRegions })`, catalog GLBs via `jgengine-assets`), not flat untextured solids.
|
|
28
28
|
7. **Lighting/render** — a tuned daylight/sun+hemisphere rig and post stack (per `look: "cinematic"`, #773), not the bare untouched three.js default light.
|
|
29
29
|
8. **VFX/motion** — particle/motion feedback tied to actual gameplay events (hits, pickups, weather), not absent, and not one looping default effect standing in everywhere.
|
|
30
|
-
9. **UI/HUD** — a game-specific `HudCanvas`/`HudPanel` composition; fails automatically if it could pass for a generic admin dashboard.
|
|
30
|
+
9. **UI/HUD** — a game-specific `HudCanvas`/`HudPanel` composition with unique art direction; fails automatically if it could pass for a generic admin dashboard **or** for another game shipping stock engine widgets/theme presets unskinned.
|
|
31
31
|
10. **Performance evidence** — the look holds at the scale the game actually runs (`InstancedScatter` density, many entities), not only a hand-posed hero shot with three objects in frame.
|
|
32
32
|
|
|
33
33
|
## Measured-evidence row
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
# jgengine-api — Cartridge
|
|
2
|
-
|
|
3
|
-
Reference module for the [`jgengine`](./SKILL.md) skill. Load this when building a game as a **cartridge** — one declarative config instead of hand-wired loop/state/UI files. A cartridge game is ~75% smaller than the classic shape and nearly all of it is data; reach for `defineGame` + `defineSystem` ([reference-systems.md](./reference-systems.md)) when the simulation outgrows cartridge archetypes — same single authoring model, no rewrite cliff.
|
|
4
|
-
|
|
5
|
-
## Why
|
|
6
|
-
|
|
7
|
-
Across the classic games, 78–91% of each game's LOC was generic wiring restated per game: a run-state store with subscribe/notify, chase-and-contact enemy loops, auto-fire weapon routines, magnet pickups, draft plumbing, phase-gated HUD trees, and tests for all of it. The cartridge layer owns every one of those; the game ships only its **data** (catalogs, tuning, layout) and any genuinely bespoke rules through typed escape hatches.
|
|
8
|
-
|
|
9
|
-
## Shape
|
|
10
|
-
|
|
11
|
-
`game.config.ts` is the whole game — `cartridge({...})` from `@jgengine/shell/cartridge` compiles the config into a `PlayableGame` (validating it first — a bad reference throws at import with a list of problems). `loop.ts` and `world.ts` disappear; the spec carries the world feature and the loop is engine-owned. `main.tsx` is 5 lines via `mountGame` (`@/components/ui/mount`, registry). Sprites/SVG art stay in `src/game/assets.ts`; the game keeps one test file under `src/game/`.
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
import { cartridge, type CartridgeConfig } from "@jgengine/shell/cartridge";
|
|
15
|
-
import { standardCartridgePanels } from "@/components/ui/cartridge-panels";
|
|
16
|
-
|
|
17
|
-
export const config: CartridgeConfig = {
|
|
18
|
-
name, seed, panels: standardCartridgePanels, assets, entitySprites,
|
|
19
|
-
flow: { start: "gate", countdownSeconds: 3, restart: true }, // press-to-start → 3-2-1 → playing; "restart" command
|
|
20
|
-
player: { kind, health, walkSpeed }, // compiled into content + spawned per player
|
|
21
|
-
enemies: { id: { label, health, walkSpeed, xp, contact: { damage, intervalSeconds }, behavior: "chase" | "none" } },
|
|
22
|
-
combat: { contactRadius },
|
|
23
|
-
spawning: { director: SpawnDirectorConfig, placement: { kind: "ring", radius } },
|
|
24
|
-
weapons: { id: { kind: "projectile" | "orbit" | "pulse" | "custom", damage, cooldownMs, maxLevel, ... } },
|
|
25
|
-
progression: { xp: Curve, maxLevel, draft: { choices, upgrades } },
|
|
26
|
-
fields: { magnetRadius, damageMultiplier }, // named run-scalars upgrades mutate
|
|
27
|
-
xpGems: { collectRadius, pullSpeed, rarityThresholds, defaultRarity },
|
|
28
|
-
rules: { win: { kind: "survive", seconds }, lose: { kind: "playerDeath" } | { kind: "custom", check }, killLeaderboardStat },
|
|
29
|
-
world, physics, camera, worldItem, theme, hud, screens, // screens: start/win/lose
|
|
30
|
-
};
|
|
31
|
-
export const game = cartridge(config);
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
The run is a phase machine every game gets for free: `start → countdown → playing → won | lost`. `flow.start: "gate"` renders `screens.start` (TitleScreen binding) until begin; `flow.countdownSeconds` renders the big-number countdown; `flow.restart: true` registers a `restart` command that resets the run in place, clears cartridge entities, and restores player stats — no hand-rolled session store, phase union, or reset function. `run.playingSeconds` is the run clock (drives the survive rule and the timer panel), so pauses and pre-game phases never skew timing.
|
|
35
|
-
|
|
36
|
-
## Cartridge surface (`@jgengine/shell/cartridge/`)
|
|
37
|
-
|
|
38
|
-
The survivor-shaped defaults (XP gems, contact-radius melee, orbit/pulse/projectile weapons) are a genre archetype, not a core primitive — the whole cartridge implementation lives in `@jgengine/shell` (opt-in, alongside its React binding), never in `@jgengine/core`. `core` only supplies the generic pieces it composes: `spawnDirector`, `abilityKit`, `leveling`/`progression`, `runDraft`, `autoTarget`. A puzzle or tactics game imports none of this.
|
|
39
|
-
|
|
40
|
-
- `@jgengine/shell/cartridge/spec` — the `CartridgeSpec` types, `Leveled` values (`number | { base, perLevel, min?, max? } | { table }` resolved by `leveled(value, level)`), `WASD_KEYBINDS` (the default when `input` is omitted).
|
|
41
|
-
- `@jgengine/shell/cartridge/runtime` — `createCartridge(spec)` → `{ content, loop, run(ctx), weaponKit(ctx), chooseUpgrade(ctx, offerId) }`. Owns the run store (outcome/kills/fields/weapon levels, subscribe/notify), spawn director advance + ring placement, chase + contact-damage enemies (terrain-grounded), the three weapon archetypes with auto-targeting, magnet xp-gem pickups feeding `leveling()`, pause-and-draft level-ups, kill → gem drop + leaderboard, and win/lose rules.
|
|
42
|
-
- `@jgengine/shell/cartridge/validate` — `validateCartridge(spec)` → problem list: spawn waves must reference declared enemies, upgrades must reference declared weapons/fields, stacks can't exceed weapon `maxLevel`, tuning must be positive, rarity thresholds descending. The game's test asserts it returns `[]`.
|
|
43
|
-
|
|
44
|
-
Weapon archetypes: `projectile` (auto-target nearest in `range`, travel-time bolt fx), `orbit` (leveled blade count/radius sweeping `hitRadius`), `pulse` (radial AoE with linear falloff and an expanding-ring fx). Upgrade effects: `weaponLevel` (+1 level per stack), `statBonus` (+max and +current on pick), `fieldAdd` / `fieldMultiply` (recomputed from stacks). Weapon damage is multiplied by the `damageMultiplier` field when declared.
|
|
45
|
-
|
|
46
|
-
## Presentation (shell + registry)
|
|
47
|
-
|
|
48
|
-
`hud.panels` is a schema — items `vital | xp | timer | score | abilityBar | component`, anchored via the movable HUD layout; `screens.win/lose` render the results/death screens; the upgrade-draft modal appears whenever a draft is pending. The shell renders the frame and injects concrete components through the `panels` seam — `standardCartridgePanels` (registry `cartridge-panels`) binds the jgengine UI kit; swap any binding for a custom component without touching the engine. Weapon fx render engine-side from the run store's bolt/pulse feeds (`fxColor`/`fxEmissive` per weapon). `theme` is the `--jg-*` CSS var block — hand-pick it or derive from a few seed colors with `deriveJgTheme` (registry `jg-theme`).
|
|
49
|
-
|
|
50
|
-
## Escape hatches — bespoke logic without leaving the cartridge
|
|
51
|
-
|
|
52
|
-
- `weapons.<id> = { kind: "custom", fire(ctx, run, args) }` — cooldown/level/damage handled for you; the fire shape is yours.
|
|
53
|
-
- `spawning.placement = { kind: "custom", position(ctx, run) }`.
|
|
54
|
-
- `rules.win = { kind: "custom", check(ctx, run) }`.
|
|
55
|
-
- `progression.draft.upgrades[].effect = { kind: "custom", apply(ctx, run, stacks) }`.
|
|
56
|
-
- `systems: [(ctx, run, dt) => ...]` — extra per-tick simulation after the built-ins, only while playing.
|
|
57
|
-
- `hud` items `{ kind: "component", Component }` — arbitrary React panels alongside the schema ones.
|
|
58
|
-
|
|
59
|
-
A game that is *mostly* bespoke simulation (custom physics, session machines) should keep `defineGame` and write its systems — the cartridge is for games whose shape the archetypes already cover.
|
|
60
|
-
|
|
61
|
-
## Testing
|
|
62
|
-
|
|
63
|
-
Cartridge behaviors are engine-tested once (`packages/shell/src/cartridge/runtime.test.ts`); the game test is one call plus any game-specific assertions:
|
|
64
|
-
|
|
65
|
-
```ts
|
|
66
|
-
import { bootCartridge, cartridgeSmokeTest, tickCartridge } from "@jgengine/shell/cartridge/testkit";
|
|
67
|
-
cartridgeSmokeTest(config); // validate + world summary + headless run/spawn/kill/gem smoke
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
`bootCartridge`/`tickCartridge` build a headless `GameContext` and drive the loop (auto-choosing drafts) for custom assertions — see `@jgengine/shell/cartridge/testkit`. The testkit imports `bun:test`; import it from test files only.
|
|
71
|
-
|
|
72
|
-
Barrel surface tokens: createKeyValueStore createUnlocks durabilityFraction isComplete isOverdrawn memorySaveBackend partInSlot pruneToasts policyProjectsViewers projectByVisibleIds projectEntitiesForViewer projectPerUserForViewer visibleEntityIds applyDeltaToSnapshot applySurfaceDeltaToSnapshot boundaryNeighbors catenaryCurve distanceToPolygonEdge footprintObstacles hasValidAdjacency pointInPolygon polygonArea polygonBounds populateNavGridFromEnvironment readScatterPalette readScatterRules revertDeltaFromSnapshot revertSurfaceDeltaFromSnapshot slopeStepCost solveLock solveLockPath
|