@jgengine/react 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -2
- package/LICENSE +203 -661
- package/README.md +1 -1
- package/dist/actionHud.d.ts +174 -0
- package/dist/actionHud.js +294 -0
- package/dist/bars.d.ts +103 -0
- package/dist/bars.js +180 -0
- package/dist/barsPreview.d.ts +4 -0
- package/dist/barsPreview.js +47 -0
- package/dist/cards.d.ts +59 -0
- package/dist/cards.js +116 -0
- package/dist/chat.d.ts +3 -1
- package/dist/chat.js +2 -2
- package/dist/components.d.ts +28 -1
- package/dist/components.js +36 -1
- package/dist/display.d.ts +1 -0
- package/dist/display.js +1 -0
- package/dist/engineStore.d.ts +3 -0
- package/dist/engineStore.js +3 -0
- package/dist/fogOverlay.d.ts +3 -0
- package/dist/fogOverlay.js +3 -0
- package/dist/gameViewport.js +16 -0
- package/dist/hooks.d.ts +40 -0
- package/dist/hooks.js +69 -1
- package/dist/hud.d.ts +104 -0
- package/dist/hud.js +155 -0
- package/dist/hudFrame.d.ts +47 -0
- package/dist/hudFrame.js +80 -0
- package/dist/hudLayout.d.ts +31 -2
- package/dist/hudLayout.js +127 -10
- package/dist/hudLayoutPersist.d.ts +23 -0
- package/dist/hudLayoutPersist.js +19 -0
- package/dist/hudTheme.d.ts +76 -0
- package/dist/hudTheme.js +156 -0
- package/dist/hudThemePreview.d.ts +4 -0
- package/dist/hudThemePreview.js +37 -0
- package/dist/iconTreatment.d.ts +50 -0
- package/dist/iconTreatment.js +84 -0
- package/dist/iconsPreview.d.ts +4 -0
- package/dist/iconsPreview.js +29 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/keyHint.d.ts +30 -0
- package/dist/keyHint.js +31 -0
- package/dist/map.d.ts +141 -11
- package/dist/map.js +138 -16
- package/dist/save.d.ts +25 -0
- package/dist/save.js +37 -0
- package/dist/selectSnapshot.d.ts +2 -0
- package/dist/selectSnapshot.js +2 -0
- package/dist/selectionHud.d.ts +139 -0
- package/dist/selectionHud.js +188 -0
- package/dist/skillCheckPaint.d.ts +2 -0
- package/dist/skillCheckPaint.js +2 -0
- package/dist/slots.d.ts +44 -0
- package/dist/slots.js +37 -0
- package/dist/startScreen.d.ts +62 -0
- package/dist/startScreen.js +62 -0
- package/dist/store.d.ts +24 -0
- package/dist/store.js +9 -0
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +54 -0
- package/skills/jgengine/api.md +527 -0
- package/skills/jgengine/capabilities.md +131 -0
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +41 -0
- package/skills/jgengine-ui/api.md +2007 -0
- package/skills/jgengine-ui/capabilities.md +211 -0
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +513 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/llms.txt +0 -1724
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
# JGengine UI — game presentation reference
|
|
2
|
+
|
|
3
|
+
This reference defines the required visual and interaction quality for JGengine games. The main `jgengine` skill owns engine architecture, hooks, input commands, and routing. This document owns what the interface looks like, how it is composed, how it responds, and how it is verified.
|
|
4
|
+
|
|
5
|
+
## The rule
|
|
6
|
+
|
|
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
|
+
|
|
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.
|
|
12
|
+
|
|
13
|
+
## 1. Start with a concise UI art direction
|
|
14
|
+
|
|
15
|
+
Before implementing screens, write this short block:
|
|
16
|
+
|
|
17
|
+
```md
|
|
18
|
+
UI ART DIRECTION
|
|
19
|
+
|
|
20
|
+
Player fantasy:
|
|
21
|
+
Emotional tone:
|
|
22
|
+
Shape language:
|
|
23
|
+
Material language:
|
|
24
|
+
Typography roles: display / body / numerical / labels
|
|
25
|
+
Motion language:
|
|
26
|
+
Icon language:
|
|
27
|
+
Sound language:
|
|
28
|
+
Information hierarchy:
|
|
29
|
+
Forbidden patterns:
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Keep it practical. It should directly influence layout, silhouettes, controls, timing, and materials.
|
|
33
|
+
|
|
34
|
+
Example forbidden patterns:
|
|
35
|
+
|
|
36
|
+
- generic rounded dashboard cards
|
|
37
|
+
- pill buttons
|
|
38
|
+
- long centered paragraphs during play
|
|
39
|
+
- ordinary two-column form layouts
|
|
40
|
+
- persistent keyboard-instruction grids
|
|
41
|
+
- multiple equally weighted bordered panels
|
|
42
|
+
- generic translucent mobile circles
|
|
43
|
+
- large website-style modals
|
|
44
|
+
- document-flow wrapping used as HUD layout
|
|
45
|
+
|
|
46
|
+
A theme is not complete when only colors and fonts change. It must also affect composition, geometry, spacing rhythm, borders, icons, animation, sound, information density, terminology, button construction, and touch controls.
|
|
47
|
+
|
|
48
|
+
## 2. Screen inventory and hierarchy
|
|
49
|
+
|
|
50
|
+
Identify the screens the game actually needs:
|
|
51
|
+
|
|
52
|
+
- boot/loading
|
|
53
|
+
- title or attract screen
|
|
54
|
+
- mode selection
|
|
55
|
+
- onboarding/tutorial
|
|
56
|
+
- gameplay HUD
|
|
57
|
+
- pause
|
|
58
|
+
- settings
|
|
59
|
+
- map/inventory/dialogue where relevant
|
|
60
|
+
- victory/results
|
|
61
|
+
- failure/retry
|
|
62
|
+
|
|
63
|
+
For each screen, define:
|
|
64
|
+
|
|
65
|
+
- the player’s primary question
|
|
66
|
+
- the primary action
|
|
67
|
+
- the most important information
|
|
68
|
+
- what can be hidden
|
|
69
|
+
- what belongs in-world instead of in the HUD
|
|
70
|
+
|
|
71
|
+
### HUD tiers
|
|
72
|
+
|
|
73
|
+
**Tier 1 — immediate action and survival**
|
|
74
|
+
Health, timer, current target, ammo, danger, capture state.
|
|
75
|
+
|
|
76
|
+
**Tier 2 — short-term decisions**
|
|
77
|
+
Objective progress, route progress, cooldowns, combo, pursuit distance.
|
|
78
|
+
|
|
79
|
+
**Tier 3 — reference information**
|
|
80
|
+
Full map, inventory, controls, schedule, mission details, lore.
|
|
81
|
+
|
|
82
|
+
Tier 1 is immediately readable. Tier 2 is quieter. Tier 3 is usually hidden until requested.
|
|
83
|
+
|
|
84
|
+
Do not style every datum as an equally important bordered box.
|
|
85
|
+
|
|
86
|
+
## 3. Full-viewport game composition
|
|
87
|
+
|
|
88
|
+
The active game should behave like an application mode:
|
|
89
|
+
|
|
90
|
+
- own the full viewport
|
|
91
|
+
- avoid document scrolling
|
|
92
|
+
- avoid site navigation and marketing chrome during play
|
|
93
|
+
- respect safe-area insets
|
|
94
|
+
- keep exit/settings/fullscreen controls minimal
|
|
95
|
+
- separate world, HUD, controls, screens, and system overlays
|
|
96
|
+
|
|
97
|
+
Recommended layer contract:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
<GameHost>
|
|
101
|
+
<WorldLayer />
|
|
102
|
+
<HudLayer />
|
|
103
|
+
<ControlLayer />
|
|
104
|
+
<ScreenLayer />
|
|
105
|
+
<SystemLayer />
|
|
106
|
+
</GameHost>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- `WorldLayer`: game renderer
|
|
110
|
+
- `HudLayer`: non-blocking gameplay information
|
|
111
|
+
- `ControlLayer`: touch/input surfaces
|
|
112
|
+
- `ScreenLayer`: title, pause, settings, tutorial, victory, failure, transitions
|
|
113
|
+
- `SystemLayer`: exit, fullscreen, engine settings, devtools
|
|
114
|
+
|
|
115
|
+
Do not place unrelated interface pieces into one ordinary DOM flow.
|
|
116
|
+
|
|
117
|
+
## 4. Explicit game layout modes
|
|
118
|
+
|
|
119
|
+
Do not rely on generic responsive wrapping. Compose explicit modes such as:
|
|
120
|
+
|
|
121
|
+
- desktop-wide
|
|
122
|
+
- desktop-compact
|
|
123
|
+
- mobile-landscape
|
|
124
|
+
- mobile-portrait
|
|
125
|
+
|
|
126
|
+
A mobile layout is not a shrunken desktop HUD.
|
|
127
|
+
|
|
128
|
+
On mobile:
|
|
129
|
+
|
|
130
|
+
- reserve thumb-control zones
|
|
131
|
+
- keep critical HUD out of those zones
|
|
132
|
+
- hide keyboard legends
|
|
133
|
+
- reduce persistent information
|
|
134
|
+
- move Tier 3 information behind contextual panels
|
|
135
|
+
- respect browser and device safe areas
|
|
136
|
+
- support portrait only when intentionally designed
|
|
137
|
+
- otherwise show a polished rotate-device state
|
|
138
|
+
|
|
139
|
+
All viewport anchoring should live in the game’s top-level UI composition file. Child components own their internal layout, not their screen position.
|
|
140
|
+
|
|
141
|
+
### Design-resolution fit (`platforms` + `hudFit`)
|
|
142
|
+
|
|
143
|
+
Design-resolution fit is on by default for every game: each `HudCanvas` auto-scales from `hudFit.designSize` (default 1600×900) down to the live viewport, clamped by `hudFit.minScale`/`maxScale` (default 0.4–1), so the authored layout shrinks instead of overflowing a phone. No declaration needed. `hudFit.mobile` overrides the fit on compact displays only — tune the phone presentation there instead of hand-rolling media queries. The player's Graphics → UI scale setting multiplies the computed scale on every platform. Declaring `platforms: ["web"]` (without `"mobile"`) opts a desktop-only game out; its compact displays keep the legacy fixed 0.85 zoom.
|
|
144
|
+
|
|
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.
|
|
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
|
+
|
|
149
|
+
### Phase-gated HUD visibility (`showDuring`)
|
|
150
|
+
|
|
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.
|
|
152
|
+
|
|
153
|
+
### Shared viewport allocation (`GameViewportProvider`, region collision)
|
|
154
|
+
|
|
155
|
+
The shell allocates the live viewport once — visual viewport, safe-area insets, orientation, and layout mode — and hosts a **layout registry** every UI subsystem publishes its occupied rectangle to. This replaces "every subsystem independently claims an edge" (the mobile-overlap smell). It is wired automatically for every game; author against it, don't rebuild it.
|
|
156
|
+
|
|
157
|
+
- **The mode** is one of `desktop-wide | desktop-compact | mobile-landscape | mobile-portrait`, resolved from the live `visualViewport` + coarse-pointer + `platforms`. Read it with `useGameLayoutMode()` (or `useGameViewportLayout()` for the full geometry: `mode`, `orientation`, `safeArea`, `controlZones`, `gameplayRect`). Both degrade to a sane desktop default outside the provider, so previews never throw.
|
|
158
|
+
- **Live CSS variables** are published on the provider root: `--jg-viewport-*`, `--jg-visual-viewport-*`, `--jg-safe-{top,right,bottom,left}`. Use them (with `100dvh` fallbacks) instead of assuming `100vh` equals the visible area on mobile Safari.
|
|
159
|
+
- **Touch controls reserve real rectangles.** The engine measures the joystick / action-cluster / utility zones at runtime (ResizeObserver, not guessed percentages) and registers them as `control` regions. `HudCanvas` already lifts bottom-anchored panels above the dock via `--jg-hud-dock-clearance`; the registry additionally makes any residual overlap a hard error.
|
|
160
|
+
- **Collision is detected, not hoped for.** A `HudPanel` inside a `HudCanvas` registers as a `hud` region automatically. `bun run shoot <game> --device mobile` / `mobile-landscape` reads a `data-jg-layout-collision` attribute and exits non-zero naming both colliding regions (e.g. `throttle ∩ radio (4270px²)`) — the same failure discipline as HUD overflow. In dev the colliding elements also carry `data-jg-collision` for outlining. Opt an intentional overlap out with `allowOverlapWith` / `collisionGroup`, or a soft `mobileBehavior="transient"`.
|
|
161
|
+
|
|
162
|
+
### HUD priority + mobile behavior (`HudPanel`)
|
|
163
|
+
|
|
164
|
+
Declare intent, not just CSS. `HudPanel` accepts:
|
|
165
|
+
|
|
166
|
+
- `priority?: "critical" | "secondary" | "tertiary"` — the Tier from §2, surfaced to tooling. Critical stays visible; tertiary folds away first.
|
|
167
|
+
- `mobileBehavior?: "persistent" | "compact" | "icon" | "transient" | "hidden" | "sheet" | "modal"` — `"hidden"` unmounts the panel on phones (move that Tier-3 readout behind a contextual action instead); `"transient"` softens its collision policy for a fleeting line; the rest tag the element (`data-hud-mobile-behavior`) for the game's own responsive CSS. Keep these game-authored and lightly styled — this is a placement contract, not a website design system.
|
|
168
|
+
|
|
169
|
+
The game still owns the genre-appropriate composition; the engine owns the geometry and the failure gate.
|
|
170
|
+
|
|
171
|
+
### Mandatory orientation (`orientation`)
|
|
172
|
+
|
|
173
|
+
A game declares its phone-orientation contract on `defineGame({ orientation })`:
|
|
174
|
+
|
|
175
|
+
- Legacy `"landscape"` / `"portrait"` stays **advisory** — a dismissible rotate hint, never a gate.
|
|
176
|
+
- The object form `{ mobile: <rule> }` is the strict contract, where `<rule>` is `any` (both) · `portrait` / `landscape` (advisory preference) · `portrait-required` / `landscape-required` (hard gate) · `unsupported` (no phone support).
|
|
177
|
+
|
|
178
|
+
For a driving/landscape game: `orientation: { mobile: "landscape-required" }`. When the device is held the wrong way the shell shows an **engine-owned `RotateDeviceScreen`** (polished, safe-area-aware, `visualViewport`-sized, reduced-motion-respecting, themeable via `--jg-*`) above every layer, **suppresses game input, freezes the simulation, and unmounts the HUD and touch controls** — gameplay never runs behind the gate. The gate is derived live from orientation, so rotating back to a valid orientation resumes automatically with no stuck-paused state. Never re-implement a "rotate for best experience" toast; declare the requirement and the engine owns the rest.
|
|
179
|
+
|
|
180
|
+
## 5. Change the visual grammar
|
|
181
|
+
|
|
182
|
+
Avoid making every element the same rounded translucent rectangle.
|
|
183
|
+
|
|
184
|
+
Use genre-appropriate structures:
|
|
185
|
+
|
|
186
|
+
- clipped corners
|
|
187
|
+
- irregular silhouettes
|
|
188
|
+
- image-backed frames
|
|
189
|
+
- mechanical plates
|
|
190
|
+
- radial interfaces
|
|
191
|
+
- ribbons and tabs
|
|
192
|
+
- gauges and meters
|
|
193
|
+
- emblems and decorative corners
|
|
194
|
+
- notches and edge anchors
|
|
195
|
+
- diegetic objects
|
|
196
|
+
- world-space prompts
|
|
197
|
+
- asymmetrical compositions
|
|
198
|
+
- masks, textures, layered borders, and strong focal elements
|
|
199
|
+
|
|
200
|
+
Practical rule: no more than roughly 20% of a normal gameplay screen should resemble an ordinary web card or modal.
|
|
201
|
+
|
|
202
|
+
Every persistent panel must justify why it exists, remains visible, has that shape, and occupies that position.
|
|
203
|
+
|
|
204
|
+
## 6. Game UI primitives
|
|
205
|
+
|
|
206
|
+
Prefer small headless or lightly styled primitives over a giant universal design system. Useful concepts include:
|
|
207
|
+
|
|
208
|
+
- `HudAnchor`
|
|
209
|
+
- `StatReadout`
|
|
210
|
+
- `Meter`
|
|
211
|
+
- `ObjectiveTracker`
|
|
212
|
+
- `ActionPrompt`
|
|
213
|
+
- `Reticle`
|
|
214
|
+
- `MinimapFrame`
|
|
215
|
+
- `DialoguePlate`
|
|
216
|
+
- `Countdown`
|
|
217
|
+
- `BossBar`
|
|
218
|
+
- `ItemPickup`
|
|
219
|
+
- `DamageIndicator`
|
|
220
|
+
- `PauseScreen`
|
|
221
|
+
- `ResultsScreen`
|
|
222
|
+
- `VirtualControlZone`
|
|
223
|
+
- `ScreenTransition`
|
|
224
|
+
|
|
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.
|
|
226
|
+
|
|
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`.
|
|
237
|
+
|
|
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()`.
|
|
239
|
+
|
|
240
|
+
**Action cards / command bars / ability bars** are a headless data → renderer → chrome split, not a hand-rolled cooldown grid. The model is caller-owned data: build `ActionDef[]` (`@jgengine/core/ui/actionModel` — `id`, optional `label`/`icon`/`hotkey`/`group`/`active`/`disabled`, plus `cooldown` via `actionCooldown`/`actionCooldownFromFraction` and `costs` via `actionCost`); `resolveAction`/`resolveActionCollection` compute `enabled` + ordered blocking `reasons` (cooldown, then unmet cost, then caller reasons). React side (`@jgengine/react`): `useActionBar(defs, { columns, wrap, hotkeys, onActivate })` is the DATA/HOOK layer (`action-bar-model` — focus, hover, `moveGridFocus` keyboard/controller grid nav, `actionByHotkey` routing); `ActionBarChrome` is the composable RENDERER (`action-bar-chrome` — `role="toolbar"`, roving tabindex, `layout: "grid" | "list"`, swap per-item chrome with `renderItem`); `ActionButton` is the default item chrome (`action-button` — radial cooldown wipe, cost line, `Keycap` hotkey, full rest/hover/focus/pressed/active/disabled states, `aria-disabled`/`aria-pressed`/`aria-keyshortcuts`); `ActionBar` is the batteries-included skinned default. A radial UI ignores the chrome and lays out `ActionButton`s from the model. Bind a combat `AbilityKit` with `actionFromAbilitySlot(slot, { label, icon, hotkey, resourceId })` over `useAbilitySlots`. Tooltips use the shared seam (no more DOM `title=` hand-rolling): `actionTooltip(resolved)` → `TooltipContent`, rendered by `ActionTooltip` inside a `Popover` (`popover` — pure `placePopover` viewport flip/clamp math from `@jgengine/core/ui/tooltipModel`, SSR-safe). **Selection panels / party frames / squad summaries** are the same split for entities: feed caller-owned `EntitySummaryDef[]` (`@jgengine/core/ui/selectionModel` — `id`, `name`, `kind`, `icon`, `vitals`, `tags`) to `summarizeSelection` (primary focus, `kind` buckets, `grouped` past a threshold, `selectionWindow` virtualization) or the `useSelectionView` hook (`selection-model` — focus stepping + keyboard nav). Renderers (`@jgengine/react`): `EntityPortrait` (`entity-portrait` selectable tile + health strip), `EntitySummary` (`entity-summary` primary detail with `VitalBar` rows), `SelectionGroupChip` (`selection-group-chip` icon+count), `SelectionCollectionChrome` (`selection-collection` portrait strip that flips to group chips when large, roving tabindex), and `SelectionPanel` (`selection-panel` batteries-included primary-summary-beside-strip default). Keep layout, dimensions, hotkeys, catalog-id mapping, renderer, and chrome caller-owned — the defaults are examples, not fixed RTS rules; never fork the model to reskin.
|
|
241
|
+
|
|
242
|
+
**Toast/announcer stacks**: two supported sources, pick the one that matches where the message comes from. An engine event already bound onto `ctx.game.feed` (kill feed, loot log, quest update) renders with `@jgengine/react`'s `ToastStack({ action })` directly. A game-raised one-off message with its own lifetime (boss-intro banner, objective callout) belongs in a `createToastQueue` (`@jgengine/core/game/toasts`, `toast-feed` capability — see `jgengine-gameplay`) rendered from `queue.list()`; don't reimplement the append-cap-plus-TTL-prune loop as a local `Toast`/`pushToast` pair.
|
|
243
|
+
|
|
244
|
+
Do not create a primitive whose only value is wrapping a `div` with border radius.
|
|
245
|
+
|
|
246
|
+
## 7. Complete interaction states
|
|
247
|
+
|
|
248
|
+
Every interactive element needs intentional states:
|
|
249
|
+
|
|
250
|
+
- rest
|
|
251
|
+
- hover where applicable
|
|
252
|
+
- keyboard/controller focus
|
|
253
|
+
- pressed
|
|
254
|
+
- selected
|
|
255
|
+
- disabled
|
|
256
|
+
- success
|
|
257
|
+
- failure
|
|
258
|
+
- warning
|
|
259
|
+
|
|
260
|
+
Do not communicate all states with background-color changes alone. Use appropriate combinations of:
|
|
261
|
+
|
|
262
|
+
- scale compression
|
|
263
|
+
- position shift
|
|
264
|
+
- edge or glow response
|
|
265
|
+
- mask movement
|
|
266
|
+
- icon movement
|
|
267
|
+
- text response
|
|
268
|
+
- brief particles
|
|
269
|
+
- sound
|
|
270
|
+
- haptics when supported
|
|
271
|
+
- controlled shake only when appropriate
|
|
272
|
+
|
|
273
|
+
Focus must look authored while remaining accessible. Menus should support keyboard/controller-style focus navigation when practical.
|
|
274
|
+
|
|
275
|
+
## Rendering — post-processing, lighting, shadows
|
|
276
|
+
|
|
277
|
+
**Default look is cinematic (#773).** A 3D game reads lit-like-a-shipped-game out of the box: unset `look` (or `look: "cinematic"`) composes the existing sky/lighting/post knobs — a real day sky with a view-following shadow-casting sun + hemisphere fill, and a tuned tone-map/bloom/gentle-SSAO/vignette post stack. One field opts out: `defineGame({ look: "flat" })` restores the bare ambient+directional rig (the old default) with no post. The preset only fills knobs you didn't set — any explicit `lighting`/`backdrop`/`postProcessing` always wins, and it never adds a sky when the world already declares one. `resolveGameLook` (`@jgengine/core/render/lookPreset`) is the pure resolver behind it — expand a `look` into concrete `{ lighting, backdrop, postProcessing }` and assert on the result in a `bun test` (the post/sky layers are not visible to `summarizeEnvironment`). HUD-presentation and no-camera games are unaffected.
|
|
278
|
+
|
|
279
|
+
The post chain itself is never hand-wired render passes. Set `defineGame({ postProcessing })` (`PostProcessingConfig` from `@jgengine/core/render/postProcessing`) and the shell mounts an `EffectComposer` and owns the render: RenderPass → AO → Bloom → tone-map output → Grade. Each stage is a config object, `false` to skip, or omitted for its tuned default:
|
|
280
|
+
|
|
281
|
+
- `toneMapping: "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none"` (default `aces`) + `exposure`.
|
|
282
|
+
- `bloom: { strength, radius, threshold }` — HDR glow around bright pixels (defaults 0.32 / 0.55 / 0.85).
|
|
283
|
+
- `grade: { lift, gain, gamma, saturation, vignette, grain }` — display-space colour grade: cool-shadow/warm-highlight split, vignette, animated film grain.
|
|
284
|
+
- `ao: { radius, intensity, distanceFalloff, blend }` — ground-truth ambient occlusion; heavier than the rest, omit or `false` on low-end targets.
|
|
285
|
+
|
|
286
|
+
**Orbit camera occlusion:** the third-person orbit rig takes `camera: { collision: { enabled, padding?, minTargetDistance? } }` — a spring-arm that raycasts target→camera each frame and pulls the boom in past walls/terrain so the camera never clips inside geometry. Off by default (unchanged chase feel); enable it for any world with interiors or dense structures.
|
|
287
|
+
|
|
288
|
+
Lighting is `defineGame({ lighting })` (`LightingConfig`): ambient / hemisphere / directional, replacing the shell's default lights when set. A `DirectionalLightingConfig` with `castShadow` takes `shadowMapSize`, `shadowCameraSize`, `shadowBias`, `shadowNormalBias` for crisp contact shadows. Sky-lit worlds (a `sky()` world feature) get a high-res sun whose shadow camera follows the view each frame, so grounded shadows stay sharp under the player anywhere in a large world.
|
|
289
|
+
|
|
290
|
+
## First-person viewmodel, WorldOverlay props, render cues (#542)
|
|
291
|
+
|
|
292
|
+
Three composable-chrome seams for FPS/TPS polish — a placement hook plus a good default, never a forced look:
|
|
293
|
+
|
|
294
|
+
- **Custom viewmodel.** `defineGame({ viewmodel })` supplies a component rendered inside the shell's camera-locked, muzzle-tracked anchor in place of the built-in three-mesh gun — read when the active rig is first-person. It receives `ViewmodelProps` (`@jgengine/shell/camera`): `{ cuesRef }`, a live `MutableRefObject<EntityRenderCues>` for the followed entity — read `cuesRef.current` inside your own `useFrame` to drive bob/recoil/reload poses; never store it as render state (it updates every frame). `camera.firstPerson.viewmodel: false` renders no viewmodel at all regardless of `defineGame`'s `viewmodel` field — the explicit opt-out always wins. `readFirstPersonMuzzle(target: THREE.Vector3)` (also `@jgengine/shell/camera`) writes the live muzzle world position into `target` and returns `false` when no viewmodel is mounted — the seam for muzzle-flash lights, tracer origins, or shell casings.
|
|
295
|
+
- **`WorldOverlay` receives `{ ctx }`.** `defineGame({ WorldOverlay })` components take `WorldOverlayProps` (`@jgengine/core/game/playableGame`) — `ctx: GameContext` handed directly, no extra hook or module-global workaround needed to reach engine state from canvas-layer VFX.
|
|
296
|
+
- **Render cues for any custom rig.** `useEntityRenderCues(instanceId, tuning?)` (`@jgengine/shell/render/useEntityRenderCues`) returns the same `cuesRef` the viewmodel gets, for a custom `renderEntity` component: velocity-driven `bobPhase`, `firing`/`recoil` from `ctx.game.playEntityAnimation(instanceId, "fire")`, `reloading` from `"reload"`/`"reloadEnd"`, `hit` from `combat.hitReaction`, `dead` from `entity.died` — no diffing the parent group's position, no game-side module map for attack timing. `RenderCueTuning` (`@jgengine/core/combat/renderCues`) overrides the bob rate / recoil decay / pulse durations; see `jgengine-combat`'s `combat/renderCues` for the underlying pure math.
|
|
297
|
+
`@jgengine/shell/terrain`'s `ProceduralGround`/`CarvedTerrain`/`GrassField` are the game-facing terrain surface; the height-field math underneath (`arenaField`, `flatField`, `fractalNoise`, `resolveGroundStep`, `valueNoise`, `withNormal`, re-exported from `@jgengine/core/world/terrain`) and `GamePlayerShell`'s physics helpers (`applyMotionImpulses`, `nearbyObstacles`, `resolvePhysicsTuning`, `hasEnvironmentTerrain`, sourced from `core/runtime/motionIntents` / `core/movement`) are internal building blocks the shell composes for you — reach for `defineGame({ movement, world })` and the ground primitives above instead of calling these directly.
|
|
298
|
+
|
|
299
|
+
## Settings menu
|
|
300
|
+
|
|
301
|
+
**Settings menu (themed, four layouts, no forced chrome).** The engine builds the whole menu for free — Sound (master + per-bus volume), Graphics (quality/dpr + shadows), Gameplay (FOV slider, default 40–120), Controls (per-action key rebinding, inline click-to-rebind, persisted) — from the game's `audio.buses` and `input` map. What it does **not** do is bolt a fixed gear onto every game: **there is no auto trigger.** You place the entry yourself so it lives *inline with your game's own UI*, never a stray corner overlay. Drop `<SettingsTrigger className=…>` (from `@jgengine/react`) anywhere in your HUD or menu — headless button, `className` for skin/placement, optional `children` to replace the default gear glyph, renders nothing when there's nothing to show. Or call `useSettings().open()` from your own control. Tune the menu via `defineGame({ settings })` (`GameSettingsConfig` from `@jgengine/core/settings/settingsModel`):
|
|
302
|
+
|
|
303
|
+
- `variant: "panel" | "sheet" | "sidebar" | "fullscreen"` — the layout + skin (default `panel`; `sheet` is the mobile bottom-sheet). All four are fixed-size (no shrink-to-content jitter) and read the game's `--jg-*` theme tokens, falling back to a neutral dark skin.
|
|
304
|
+
- `actions: SettingsActionDef[]` — game-state actions (Restart, Quit to menu, …). They become the **first "Game" tab, shown before anything else** — the home for buttons that used to float over the HUD. Each: `{ id, label, kind?: "default"|"danger", description?, run(ctx) }`; the menu closes right after `run`.
|
|
305
|
+
- `hideBindings: string[]` — input actions to drop from the rebindable Controls list. A game-state key like `restart` belongs in `actions`, not the rebind grid — hide it here so it stops showing up as a "rebindable" control.
|
|
306
|
+
- `surface: "quick"` — additionally mount compact on-screen volume/graphics buttons. Omit for none. `settings: false` — off entirely.
|
|
307
|
+
- `extra: GameSettingDef[]` — append rows to any category, built-in *or a brand-new one* named by `category` (any string). Each row: `{ id, label, category, kind: "slider"|"toggle"|"select", default, min?, max?, step?, options?, onChange?(value, ctx) }`.
|
|
308
|
+
- `categories: SettingCategoryDef[]` — declare custom category tabs, or relabel/reorder built-ins (`{ id, label, order? }`).
|
|
309
|
+
- `hide: SettingCategory[]` — drop built-in categories.
|
|
310
|
+
|
|
311
|
+
**Game-state controls go in `actions`, never a floating button.** Restart/quit/new-game buttons stapled to the bottom of the HUD are the anti-pattern — declare them as `actions` (first Game tab) and place a `<SettingsTrigger>` inline. A contextual button on a win/lose *results* card is fine; a persistent game-state button pinned over live play is not.
|
|
312
|
+
|
|
313
|
+
**Present it any way you want.** `useSettings()` (`@jgengine/react`) returns the live controller — `{ categories, actions, variant, surface, isOpen, open, close, setOpen }` — so a game can drive its own pause-menu button, or render `categories`/`actions` (rows carry `value`/`set`/bounds, keybinds carry `rebind`/`reset`) entirely inside its own HUD. `useHasSettings()` gates a custom entry; `useSetting(id, fallback)` reads/writes one value. Set a slider's `min`/`max` explicitly — an omitted range collapses the thumb to 0/1.
|
|
314
|
+
|
|
315
|
+
## 8. Motion and game feel
|
|
316
|
+
|
|
317
|
+
Add purposeful motion for:
|
|
318
|
+
|
|
319
|
+
- screen entry and exit
|
|
320
|
+
- confirm and cancel
|
|
321
|
+
- warnings
|
|
322
|
+
- score increases
|
|
323
|
+
- objective updates
|
|
324
|
+
- damage
|
|
325
|
+
- victory and failure
|
|
326
|
+
- countdowns
|
|
327
|
+
- pause
|
|
328
|
+
- item pickup
|
|
329
|
+
|
|
330
|
+
Motion should be brief, readable, interruptible when necessary, coordinated, consistent with the game’s art direction, and respectful of reduced-motion settings.
|
|
331
|
+
|
|
332
|
+
Do not animate everything constantly. Motion communicates hierarchy, cause and effect, urgency, and state changes.
|
|
333
|
+
|
|
334
|
+
**Camera shake.** `@jgengine/shell/camera`'s trauma channel (`useCameraShake`/`cameraShake`) is amplitude/decay by default (`.shake(amplitude, decayPerSecond?)`, sampled with `shakeOffset`'s `maxOffset`/`maxRoll`/`exponent`/`frequency`). `traumaShake(trauma, time?)` is the calibrated trauma² curve (offset ∝ trauma²) behind `@jgengine/core/combat/hitReaction`'s `impactPresets` — decay 1.4/s, max offset 0.55, max roll 0.1 rad, noise freq 32×t (`CALIBRATED_TRAUMA_SHAKE_DECAY_PER_SECOND`/`_MAX_OFFSET`/`_MAX_ROLL`/`_FREQUENCY`). `CombatCameraShake` (mounted once per world) already wires `combat.hitReaction`'s `trauma` field into the channel at that calibrated decay automatically — a preset-driven hit reaction shakes the camera with zero extra setup.
|
|
335
|
+
|
|
336
|
+
## 9. Mobile controls are genre-authored
|
|
337
|
+
|
|
338
|
+
Shared input mechanics may remain shared. Their visual treatment and arrangement must match the game.
|
|
339
|
+
|
|
340
|
+
Examples:
|
|
341
|
+
|
|
342
|
+
**Driving**
|
|
343
|
+
Steering region or wheel, accelerator, brake, handbrake, optional camera/map control.
|
|
344
|
+
|
|
345
|
+
**Stealth**
|
|
346
|
+
Movement zone, sneak/crouch hold, contextual interaction, temporary map/schedule control.
|
|
347
|
+
|
|
348
|
+
**Shooter**
|
|
349
|
+
Movement zone, aim region, fire/action cluster, weapon or ability controls.
|
|
350
|
+
|
|
351
|
+
**Puzzle/arcade**
|
|
352
|
+
Direct drag, tap, swipe, paddle region, or discrete directions. Do not add a joystick without a gameplay reason.
|
|
353
|
+
|
|
354
|
+
Requirements:
|
|
355
|
+
|
|
356
|
+
- never cover critical HUD information
|
|
357
|
+
- use the game’s shape and material language
|
|
358
|
+
- fade training labels after learning
|
|
359
|
+
- consider thumb reach
|
|
360
|
+
- preserve accessible target sizes
|
|
361
|
+
- visually respond to activation
|
|
362
|
+
- support optional scaling where appropriate
|
|
363
|
+
|
|
364
|
+
Do not use one generic translucent controller across all games.
|
|
365
|
+
|
|
366
|
+
**Shape, skin, and placement are data on `defineGame({ touch })`** — the engine derives a working dock from the input map, and you refine it without touching render code:
|
|
367
|
+
|
|
368
|
+
- **`shape`** per button (`TouchButtonSpec.shape`): `pedal | lever | trigger | wheel | square | circle | tab`. The capture layer draws each as its physical silhouette, so `brake` is a foot pedal and `handbrake` a pull lever instead of a labelled circle. Unset derives one from the action name (`brake`/`throttle`→pedal, `handbrake`/`boost`→lever, `fire`/`attack`→trigger, `steer*`→wheel, `spell*`/`slot*`/`item*`/`inventory`→square). Inventory and spell slots default to square tiles.
|
|
369
|
+
- **`image`** per button (`TouchButtonSpec.image`): any image URL or `data:image/svg+xml` URI drawn as the button face instead of the built-in silhouette — drop your own slot frame or spell plate behind a control; the icon/label still sits on top and press still scales/brightens it.
|
|
370
|
+
- **`anchor`** per button, or **`layout: { movement, actions, utility }`** per cluster: dock controls to any of `bottom-left | bottom-center | bottom-right | left | right | top-left | top-center | top-right`. `left`/`right` are vertical rails (MMO-style hotbars) — use them to spread controls across the whole viewport instead of one bottom bar. `bottom-left`/`bottom-right` keep the thumb-arc; other anchors stack.
|
|
371
|
+
- **`movement: { axis: "horizontal" }`** restricts the joystick to a steering-only zone, freeing throttle/brake to render as pedal buttons — the standard driving layout.
|
|
372
|
+
- **`style`** suggests a skin: `glass` (default translucent) · `arcade` (neon bezel) · `mechanical` (metal plate) · `minimal` (thin outline). A skin changes material and geometry, not only colour. The **player overrides it in Settings → Controls** ("Touch controls", persisted); leave it on `Auto` to honour the game's `style`. This is the four-variant chooser — never hand-roll a control-skin toggle.
|
|
373
|
+
- **Lifecycle actions never auto-dock.** Derivation skips `start`/`restart`/`reset`/`retry`/`pause`/`resume`/`menu`/`quit`/`exit`/`settings`/`options` — binding a `restart` key does *not* put a RESTART chip over live play (the same anti-pattern §274–283 forbids for HUD buttons, on the touch layer). These belong in `settings.actions` + a `<SettingsTrigger>` / pause menu. To dock one anyway, list it explicitly in `touch.buttons`.
|
|
374
|
+
|
|
375
|
+
Canyon Chase is the worked example: `movement: { axis: "horizontal" }` steering, `throttle`/`brake` pedals on the `right` rail, `handbrake` lever bottom-right, `style: "mechanical"`.
|
|
376
|
+
|
|
377
|
+
**`presentation: "hud"` games get 3D parity.** A pure-HUD game (no camera rig) now reaches the same input/audio seams as a 3D game:
|
|
378
|
+
- **Touch gestures** — the shell mounts a headless `TouchPlaySurface` in the hud branch too, so `touch.gestures` (swipe/tap) reach actions without the game hand-wiring pointer events on its own canvas. The visible dock stays game-authored per the rule above.
|
|
379
|
+
- **No phantom reservations** — camera action names (`turnLeft`/`turnRight`/`interact`/…) are reserved *only* when a camera rig is active, so a hud game may bind them directly instead of renaming to `steer*`.
|
|
380
|
+
- **Audio actually plays** — audio resumes on the first pointer gesture in hud games (not just 3D), and `playOneShot` self-resumes the suspended context. Trigger sound from anywhere holding `ctx` via `ctx.game.audio.play(soundId, at?)` / `ctx.game.audio.resume()` — the reachable seam over the shell's audio engine.
|
|
381
|
+
|
|
382
|
+
## 10. Progressive instruction
|
|
383
|
+
|
|
384
|
+
Do not leave large control grids visible during gameplay.
|
|
385
|
+
|
|
386
|
+
Prefer:
|
|
387
|
+
|
|
388
|
+
- contextual prompts
|
|
389
|
+
- brief onboarding
|
|
390
|
+
- first-use hints
|
|
391
|
+
- a controls screen
|
|
392
|
+
- pause-menu reference
|
|
393
|
+
- icons attached to actions
|
|
394
|
+
- progressive disclosure
|
|
395
|
+
|
|
396
|
+
Desktop keyboard legends must not appear on touch devices. Prompts should appear near the relevant action, object, or HUD region and clear when no longer useful.
|
|
397
|
+
|
|
398
|
+
The shared `KeybindBadge` (and everything built on it — `MenuButton`, `MenuList`, `TitleScreen`, `ResultsScreen`) already renders nothing on a coarse pointer, so route key hints through it instead of hand-rolling `<kbd>`. When a screen hand-rolls its own control grid or a `START — {key}` / `Press Enter` call-to-action, gate that text on `const { coarsePointer } = useDisplayProfile()` (`@jgengine/react`) — hide the keyboard legend and let the tappable button carry a plain label (`Start`, `Restart`). Never surface a bare "press E / press enter" line as the only way to advance; the button itself must be tappable.
|
|
399
|
+
|
|
400
|
+
The dev runner locks its own document (`overscroll-behavior: none`, `touch-action: manipulation`, no page zoom or rubber-band, no long-press text selection). Games never add scroll/pinch guards or `position: fixed` body hacks of their own; active play owns the viewport by default.
|
|
401
|
+
|
|
402
|
+
## 11. Reference directions for flagship games
|
|
403
|
+
|
|
404
|
+
### Clockwork Heist
|
|
405
|
+
|
|
406
|
+
Use gentleman-thief mechanical field-kit language: watch geometry, midnight enamel, aged brass, ivory paper, engraved labels, mechanical shutters, and an authored schedule timeline. The timer should feel like a clock. The schedule should be an on-demand pocket-watch or dossier panel. Touch controls should use brass/enamel construction. Restart belongs in pause, not permanently over the world.
|
|
407
|
+
|
|
408
|
+
### Canyon Chase
|
|
409
|
+
|
|
410
|
+
Use desert pursuit language: battered dashboard, analog instruments, radio display, route strip, warning lamps, and road-sign typography. Target gap should read as a pursuit gauge. Border progress should resemble an odometer, route strip, or mile marker. Driving controls should feel like steering, throttle, brake, and handbrake—not generic circles.
|
|
411
|
+
|
|
412
|
+
### Brick Breaker
|
|
413
|
+
|
|
414
|
+
Use arcade-cabinet language: bezel framing, CRT/vector treatment, arcade numerical readouts, attract mode, launch feedback, and brief level overlays that clear before play. Pause and results should feel like cabinet states, not web dialogs.
|
|
415
|
+
|
|
416
|
+
These games must remain structurally distinct, not recolors of one component set.
|
|
417
|
+
|
|
418
|
+
## 12. Accessibility and performance
|
|
419
|
+
|
|
420
|
+
Maintain:
|
|
421
|
+
|
|
422
|
+
- sufficient contrast
|
|
423
|
+
- readable text size
|
|
424
|
+
- keyboard navigation
|
|
425
|
+
- controller navigation where available
|
|
426
|
+
- reduced-motion support
|
|
427
|
+
- visible authored focus
|
|
428
|
+
- touch target sizing
|
|
429
|
+
- semantic labels where practical
|
|
430
|
+
- responsive scaling
|
|
431
|
+
- reasonable DOM and animation performance
|
|
432
|
+
|
|
433
|
+
Use blur, masks, textures, filters, and full-screen effects carefully, especially on mobile.
|
|
434
|
+
|
|
435
|
+
## 13. Screenshot verification is mandatory
|
|
436
|
+
|
|
437
|
+
Capture and inspect meaningful states:
|
|
438
|
+
|
|
439
|
+
- desktop title screen
|
|
440
|
+
- desktop gameplay
|
|
441
|
+
- mobile landscape
|
|
442
|
+
- mobile portrait where supported
|
|
443
|
+
- pause
|
|
444
|
+
- victory or failure
|
|
445
|
+
- an interaction prompt
|
|
446
|
+
- touch controls in active use
|
|
447
|
+
|
|
448
|
+
Inspect for:
|
|
449
|
+
|
|
450
|
+
- overlap and clipping
|
|
451
|
+
- weak contrast
|
|
452
|
+
- unreadable scale
|
|
453
|
+
- excessive cards
|
|
454
|
+
- website-like composition
|
|
455
|
+
- broken safe areas
|
|
456
|
+
- conflicting hierarchy
|
|
457
|
+
- browser chrome interference
|
|
458
|
+
- poor thumb reach
|
|
459
|
+
- keyboard instructions on touch devices
|
|
460
|
+
- inconsistent art direction
|
|
461
|
+
|
|
462
|
+
Revise after inspection. Typechecking is not visual proof.
|
|
463
|
+
|
|
464
|
+
## 14. Rejection criteria
|
|
465
|
+
|
|
466
|
+
Require revision when any of these are true:
|
|
467
|
+
|
|
468
|
+
- normal site navigation remains visible during active gameplay
|
|
469
|
+
- the player must scroll the page to use the game
|
|
470
|
+
- the game is presented inside a normal content card
|
|
471
|
+
- essential HUD is covered by touch controls
|
|
472
|
+
- controls overlap each other
|
|
473
|
+
- keyboard instructions appear on touch devices
|
|
474
|
+
- large instruction panels remain visible during gameplay
|
|
475
|
+
- more than three ordinary card-like panels are persistently visible
|
|
476
|
+
- the main action looks like a standard website button
|
|
477
|
+
- pause resembles a generic website modal
|
|
478
|
+
- victory/failure is only text plus restart
|
|
479
|
+
- restart is permanently visible without a gameplay reason
|
|
480
|
+
- menu elements lack pressed, focused, selected, or disabled states
|
|
481
|
+
- UI changes have no transition or feedback
|
|
482
|
+
- generic virtual controls are used without genre adaptation
|
|
483
|
+
- the theme only changes colors and fonts
|
|
484
|
+
- unrelated UI elements have equal visual weight
|
|
485
|
+
- mobile portrait technically fits but is not intentionally composed
|
|
486
|
+
- important UI sits beneath safe areas
|
|
487
|
+
- ordinary document flow determines the HUD layout
|
|
488
|
+
- generic default styling is used because no art direction was written
|
|
489
|
+
- the hosting website page renders its own title, tagline, or "enter game" button instead of loading straight into the game's own menu
|
|
490
|
+
|
|
491
|
+
## 15. Compact implementation API appendix
|
|
492
|
+
|
|
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.
|
|
494
|
+
|
|
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.
|
|
496
|
+
|
|
497
|
+
Headless components are not finished design. They are behavior and accessibility seams that the game must art-direct.
|
|
498
|
+
|
|
499
|
+
## Definition of done
|
|
500
|
+
|
|
501
|
+
UI work is complete only when:
|
|
502
|
+
|
|
503
|
+
- the game owns the viewport
|
|
504
|
+
- site chrome is absent during active play
|
|
505
|
+
- no document scrolling is required
|
|
506
|
+
- HUD and control layers have clear responsibilities
|
|
507
|
+
- mobile controls do not cover critical content
|
|
508
|
+
- touch controls match the genre and game identity
|
|
509
|
+
- title, pause, and results screens feel authored
|
|
510
|
+
- interaction states and transitions are present
|
|
511
|
+
- screenshots have been reviewed and revised
|
|
512
|
+
- the implementation remains accessible and performant
|
|
513
|
+
- future generated UI is explicitly prevented from falling back to generic website-card styling
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Visual scorecard
|
|
2
|
+
|
|
3
|
+
This repeatable 0–3 rubric makes "looks shipped" consistent across sessions and usable by a fresh-eyes reviewer. Apply it to milestone shots, not every tweak, and place the scored table beside PR screenshots.
|
|
4
|
+
|
|
5
|
+
Structure and thresholds adapted (concept only, reworded for this stack — not copied verbatim) from `majidmanzarpour/threejs-game-skills`' `visual-scorecard.md` (MIT); see `CREDITS.md`.
|
|
6
|
+
|
|
7
|
+
## The 0–3 scale
|
|
8
|
+
|
|
9
|
+
Score each category independently against what's actually in the shot, not what the code intends.
|
|
10
|
+
|
|
11
|
+
| Score | Meaning |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| **0** | Engine defaults or debug output. Untextured primitives, default grey material, flat ambient light, no sky/fog treatment, no authored placement. |
|
|
14
|
+
| **1** | Basic styled prototype. Readable and on-genre, but generic — solid-color materials, one light, sparse or untuned `AuthoredScene` content, no distinct silhouettes. |
|
|
15
|
+
| **2** | Premium stylized. Authored silhouettes on hero/obstacles, a coherent material system (not one grey PBR everywhere), tuned `environment()` lighting/sky/post, foliage and props placed through the editor read as a real place, not a demo. |
|
|
16
|
+
| **3** | Showcase art direction. Everything in 2, plus: no repeated silhouette anywhere in frame, deliberate foreground/midground/background composition, purposeful VFX and motion, a HUD that could not be mistaken for another game. Reads like a store page, not a build. |
|
|
17
|
+
|
|
18
|
+
## The 10 categories
|
|
19
|
+
|
|
20
|
+
Score all ten from one `--mode play` shot plus deterministic world/document evidence from `jgengine-verify`. Data presence is a prerequisite, not a substitute for visual judgment.
|
|
21
|
+
|
|
22
|
+
1. **Art direction** — one coherent palette and mood across ground, props, sky, and UI; not three unrelated hue choices stapled together.
|
|
23
|
+
2. **Hero/player** — an authored silhouette and material set the player is meant to be, not a default primitive or a capsule with an emissive glow standing in for a character.
|
|
24
|
+
3. **Obstacles/enemies** — distinct types read apart at a glance; not one mesh reused at different scales/colors.
|
|
25
|
+
4. **Rewards/interactables** — visually distinct from hazards and terrain at a glance, not the same proxy geometry as everything else.
|
|
26
|
+
5. **World/environment** — foreground, midground, and background read as separate layers (props/landmarks near camera, terrain shaping mid-distance, sky/fog treatment at the horizon); not one flat plane running to a bare horizon.
|
|
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
|
+
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
|
+
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 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
|
+
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
|
+
|
|
33
|
+
## Measured-evidence row
|
|
34
|
+
|
|
35
|
+
Pixel metrics from `bun run shoot <id> --mode play --inspect` (or `bun run inspect-shot.ts <shot.png>` on an existing PNG — [#788](https://github.com/Noisemaker111/jgengine/issues/788), shipped) are the objective inputs to categories 5–7 — they don't replace the eye, they catch what a rushed glance misses. Output lands at `shots/<name>.metrics.json`:
|
|
36
|
+
|
|
37
|
+
| Metric | Threshold | Flags |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `colorEntropyBits` | `< 3.0` | sparse palette |
|
|
40
|
+
| `dominantColorShare` | `> 0.6` | sparse / one material dominates the frame |
|
|
41
|
+
| `edgeDensity` | `< 0.04` | primitive-dominant, too few silhouettes |
|
|
42
|
+
| `luminance.contrast` | `< 60` | fog/post compressing out real geometry |
|
|
43
|
+
|
|
44
|
+
Any metric over threshold caps the affected category at **1** regardless of how the shot reads by eye — the number is a floor, not a veto the eye can talk it out of. `--strict` hard-fails on a crossed threshold for CI; the scorecard itself always records this row from the JSON, never "not measured."
|
|
45
|
+
|
|
46
|
+
## Pass thresholds
|
|
47
|
+
|
|
48
|
+
- **Premium**: every category ≥ 2, average ≥ 2.3.
|
|
49
|
+
- **Showcase**: at least 6 categories at 3, no category < 2, average ≥ 2.7.
|
|
50
|
+
|
|
51
|
+
Below "premium" on a claimed milestone is not shippable; keep working the art stack or narrow the claim.
|
|
52
|
+
|
|
53
|
+
## Automatic failures
|
|
54
|
+
|
|
55
|
+
Any one of these fails the shot regardless of the averaged score:
|
|
56
|
+
|
|
57
|
+
- **Primitive-dominant shots** — untextured boxes/spheres/cylinders make up the visible mass of the frame.
|
|
58
|
+
- **Default primitive + glow standing in for a hero** — an emissive-glow capsule/sphere is not a character, even at full brightness.
|
|
59
|
+
- **One repeated silhouette everywhere** — `InstancedScatter`'s stylized proxy species (or any single catalog model) used with no palette variety across the whole visible world.
|
|
60
|
+
- **Fog/bloom hiding missing geometry** — atmosphere used to obscure an empty or unfinished scene instead of to add depth to a populated one.
|
|
61
|
+
- **No active-play shot** — only editor/menu/poster shots submitted. `--mode play` (or `drive` past the menu) evidence is mandatory; a scene that only looks good in the editor camera hasn't proven anything about how the game actually reads.
|
|
62
|
+
|
|
63
|
+
## Fresh-eyes review and the lower-score rule
|
|
64
|
+
|
|
65
|
+
Hand a reviewer **only** the screenshots, this rubric, and the measured-evidence JSON — no PR description, no "here's what I was going for." A reviewer who knows the intent scores the intent; a fresh reviewer scores the pixels, which is the only thing a player ever sees.
|
|
66
|
+
|
|
67
|
+
- The author scores first (self-score).
|
|
68
|
+
- A fresh-eyes pass (a different session, or the same session after clearing framing context) scores independently.
|
|
69
|
+
- Where the two disagree, **the PR carries the lower score**, never the average and never the higher one. Optimistic self-scoring is the failure mode this guards against — take the lower number and keep working the art stack instead of arguing the rubric.
|
|
70
|
+
|
|
71
|
+
## Using this in a PR
|
|
72
|
+
|
|
73
|
+
Any PR that changes what a player sees pastes the ten-category table (score + one-line reason per category below 2), the measured-evidence row, and the reconciled score next to embedded screenshots. Report that verdict through `jgengine-verify`'s evidence format.
|