@jgengine/react 0.10.0 → 0.11.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +13 -2
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/actionHud.d.ts +174 -0
  5. package/dist/actionHud.js +292 -0
  6. package/dist/cards.d.ts +59 -0
  7. package/dist/cards.js +116 -0
  8. package/dist/chat.d.ts +3 -1
  9. package/dist/chat.js +2 -2
  10. package/dist/components.d.ts +20 -0
  11. package/dist/components.js +28 -0
  12. package/dist/display.d.ts +1 -0
  13. package/dist/display.js +1 -0
  14. package/dist/engineStore.d.ts +3 -0
  15. package/dist/engineStore.js +3 -0
  16. package/dist/fogOverlay.d.ts +3 -0
  17. package/dist/fogOverlay.js +3 -0
  18. package/dist/gameViewport.js +16 -0
  19. package/dist/hooks.d.ts +20 -0
  20. package/dist/hooks.js +31 -1
  21. package/dist/hud.d.ts +87 -0
  22. package/dist/hud.js +144 -0
  23. package/dist/hudLayout.d.ts +19 -2
  24. package/dist/hudLayout.js +121 -10
  25. package/dist/index.d.ts +8 -0
  26. package/dist/index.js +8 -0
  27. package/dist/keyHint.d.ts +30 -0
  28. package/dist/keyHint.js +31 -0
  29. package/dist/map.d.ts +69 -1
  30. package/dist/map.js +54 -1
  31. package/dist/save.d.ts +25 -0
  32. package/dist/save.js +37 -0
  33. package/dist/selectSnapshot.d.ts +2 -0
  34. package/dist/selectSnapshot.js +2 -0
  35. package/dist/selectionHud.d.ts +133 -0
  36. package/dist/selectionHud.js +182 -0
  37. package/dist/skillCheckPaint.d.ts +2 -0
  38. package/dist/skillCheckPaint.js +2 -0
  39. package/dist/startScreen.d.ts +62 -0
  40. package/dist/startScreen.js +62 -0
  41. package/dist/store.d.ts +24 -0
  42. package/dist/store.js +9 -0
  43. package/package.json +4 -4
  44. package/skills/jgengine/SKILL.md +51 -0
  45. package/skills/jgengine/api.md +434 -0
  46. package/skills/jgengine/capabilities.md +29 -0
  47. package/skills/jgengine/reference-cartridge.md +72 -0
  48. package/skills/jgengine-ui/SKILL.md +36 -0
  49. package/skills/jgengine-ui/api.md +1852 -0
  50. package/skills/jgengine-ui/capabilities.md +142 -0
  51. package/skills/jgengine-ui/reference.md +500 -0
  52. package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
  53. package/llms.txt +0 -1724
@@ -0,0 +1,500 @@
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. It must not resemble a dashboard, landing page, documentation page, or ordinary responsive web app.
8
+
9
+ HTML and React are valid implementation tools. Website visual grammar is not the default.
10
+
11
+ ## 1. Start with a concise UI art direction
12
+
13
+ Before implementing screens, write this short block:
14
+
15
+ ```md
16
+ UI ART DIRECTION
17
+
18
+ Player fantasy:
19
+ Emotional tone:
20
+ Shape language:
21
+ Material language:
22
+ Typography roles: display / body / numerical / labels
23
+ Motion language:
24
+ Icon language:
25
+ Sound language:
26
+ Information hierarchy:
27
+ Forbidden patterns:
28
+ ```
29
+
30
+ Keep it practical. It should directly influence layout, silhouettes, controls, timing, and materials.
31
+
32
+ Example forbidden patterns:
33
+
34
+ - generic rounded dashboard cards
35
+ - pill buttons
36
+ - long centered paragraphs during play
37
+ - ordinary two-column form layouts
38
+ - persistent keyboard-instruction grids
39
+ - multiple equally weighted bordered panels
40
+ - generic translucent mobile circles
41
+ - large website-style modals
42
+ - document-flow wrapping used as HUD layout
43
+
44
+ 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.
45
+
46
+ ## 2. Screen inventory and hierarchy
47
+
48
+ Identify the screens the game actually needs:
49
+
50
+ - boot/loading
51
+ - title or attract screen
52
+ - mode selection
53
+ - onboarding/tutorial
54
+ - gameplay HUD
55
+ - pause
56
+ - settings
57
+ - map/inventory/dialogue where relevant
58
+ - victory/results
59
+ - failure/retry
60
+
61
+ For each screen, define:
62
+
63
+ - the player’s primary question
64
+ - the primary action
65
+ - the most important information
66
+ - what can be hidden
67
+ - what belongs in-world instead of in the HUD
68
+
69
+ ### HUD tiers
70
+
71
+ **Tier 1 — immediate action and survival**
72
+ Health, timer, current target, ammo, danger, capture state.
73
+
74
+ **Tier 2 — short-term decisions**
75
+ Objective progress, route progress, cooldowns, combo, pursuit distance.
76
+
77
+ **Tier 3 — reference information**
78
+ Full map, inventory, controls, schedule, mission details, lore.
79
+
80
+ Tier 1 is immediately readable. Tier 2 is quieter. Tier 3 is usually hidden until requested.
81
+
82
+ Do not style every datum as an equally important bordered box.
83
+
84
+ ## 3. Full-viewport game composition
85
+
86
+ The active game should behave like an application mode:
87
+
88
+ - own the full viewport
89
+ - avoid document scrolling
90
+ - avoid site navigation and marketing chrome during play
91
+ - respect safe-area insets
92
+ - keep exit/settings/fullscreen controls minimal
93
+ - separate world, HUD, controls, screens, and system overlays
94
+
95
+ Recommended layer contract:
96
+
97
+ ```tsx
98
+ <GamePlayer>
99
+ <WorldLayer />
100
+ <HudLayer />
101
+ <ControlLayer />
102
+ <ScreenLayer />
103
+ <SystemLayer />
104
+ </GamePlayer>
105
+ ```
106
+
107
+ - `WorldLayer`: game renderer
108
+ - `HudLayer`: non-blocking gameplay information
109
+ - `ControlLayer`: touch/input surfaces
110
+ - `ScreenLayer`: title, pause, settings, tutorial, victory, failure, transitions
111
+ - `SystemLayer`: exit, fullscreen, engine settings, devtools
112
+
113
+ Do not place unrelated interface pieces into one ordinary DOM flow.
114
+
115
+ ## 4. Explicit game layout modes
116
+
117
+ Do not rely on generic responsive wrapping. Compose explicit modes such as:
118
+
119
+ - desktop-wide
120
+ - desktop-compact
121
+ - mobile-landscape
122
+ - mobile-portrait
123
+
124
+ A mobile layout is not a shrunken desktop HUD.
125
+
126
+ On mobile:
127
+
128
+ - reserve thumb-control zones
129
+ - keep critical HUD out of those zones
130
+ - hide keyboard legends
131
+ - reduce persistent information
132
+ - move Tier 3 information behind contextual panels
133
+ - respect browser and device safe areas
134
+ - support portrait only when intentionally designed
135
+ - otherwise show a polished rotate-device state
136
+
137
+ All viewport anchoring should live in the game’s top-level UI composition file. Child components own their internal layout, not their screen position.
138
+
139
+ ### Design-resolution fit (`platforms` + `hudFit`)
140
+
141
+ 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.
142
+
143
+ **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
+
145
+ ### Phase-gated HUD visibility (`showDuring`)
146
+
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 non-cartridge 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
+
149
+ ### Shared viewport allocation (`GameViewportProvider`, region collision)
150
+
151
+ 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.
152
+
153
+ - **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.
154
+ - **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.
155
+ - **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.
156
+ - **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"`.
157
+
158
+ ### HUD priority + mobile behavior (`HudPanel`)
159
+
160
+ Declare intent, not just CSS. `HudPanel` accepts:
161
+
162
+ - `priority?: "critical" | "secondary" | "tertiary"` — the Tier from §2, surfaced to tooling. Critical stays visible; tertiary folds away first.
163
+ - `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.
164
+
165
+ The game still owns the genre-appropriate composition; the engine owns the geometry and the failure gate.
166
+
167
+ ### Mandatory orientation (`orientation`)
168
+
169
+ A game declares its phone-orientation contract on `defineGame({ orientation })`:
170
+
171
+ - Legacy `"landscape"` / `"portrait"` stays **advisory** — a dismissible rotate hint, never a gate.
172
+ - 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).
173
+
174
+ 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.
175
+
176
+ ## 5. Change the visual grammar
177
+
178
+ Avoid making every element the same rounded translucent rectangle.
179
+
180
+ Use genre-appropriate structures:
181
+
182
+ - clipped corners
183
+ - irregular silhouettes
184
+ - image-backed frames
185
+ - mechanical plates
186
+ - radial interfaces
187
+ - ribbons and tabs
188
+ - gauges and meters
189
+ - emblems and decorative corners
190
+ - notches and edge anchors
191
+ - diegetic objects
192
+ - world-space prompts
193
+ - asymmetrical compositions
194
+ - masks, textures, layered borders, and strong focal elements
195
+
196
+ Practical rule: no more than roughly 20% of a normal gameplay screen should resemble an ordinary web card or modal.
197
+
198
+ Every persistent panel must justify why it exists, remains visible, has that shape, and occupies that position.
199
+
200
+ ## 6. Game UI primitives
201
+
202
+ Prefer small headless or lightly styled primitives over a giant universal design system. Useful concepts include:
203
+
204
+ - `HudAnchor`
205
+ - `StatReadout`
206
+ - `Meter`
207
+ - `ObjectiveTracker`
208
+ - `ActionPrompt`
209
+ - `Reticle`
210
+ - `MinimapFrame`
211
+ - `DialoguePlate`
212
+ - `Countdown`
213
+ - `BossBar`
214
+ - `ItemPickup`
215
+ - `DamageIndicator`
216
+ - `PauseScreen`
217
+ - `ResultsScreen`
218
+ - `VirtualControlZone`
219
+ - `ScreenTransition`
220
+
221
+ 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
+
223
+ **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. 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`.
224
+
225
+ **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
+
227
+ **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.
228
+
229
+ **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.
230
+
231
+ Do not create a primitive whose only value is wrapping a `div` with border radius.
232
+
233
+ ## 7. Complete interaction states
234
+
235
+ Every interactive element needs intentional states:
236
+
237
+ - rest
238
+ - hover where applicable
239
+ - keyboard/controller focus
240
+ - pressed
241
+ - selected
242
+ - disabled
243
+ - success
244
+ - failure
245
+ - warning
246
+
247
+ Do not communicate all states with background-color changes alone. Use appropriate combinations of:
248
+
249
+ - scale compression
250
+ - position shift
251
+ - edge or glow response
252
+ - mask movement
253
+ - icon movement
254
+ - text response
255
+ - brief particles
256
+ - sound
257
+ - haptics when supported
258
+ - controlled shake only when appropriate
259
+
260
+ Focus must look authored while remaining accessible. Menus should support keyboard/controller-style focus navigation when practical.
261
+
262
+ ## Rendering — post-processing, lighting, shadows
263
+
264
+ **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.
265
+
266
+ 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:
267
+
268
+ - `toneMapping: "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none"` (default `aces`) + `exposure`.
269
+ - `bloom: { strength, radius, threshold }` — HDR glow around bright pixels (defaults 0.32 / 0.55 / 0.85).
270
+ - `grade: { lift, gain, gamma, saturation, vignette, grain }` — display-space colour grade: cool-shadow/warm-highlight split, vignette, animated film grain.
271
+ - `ao: { radius, intensity, distanceFalloff, blend }` — ground-truth ambient occlusion; heavier than the rest, omit or `false` on low-end targets.
272
+
273
+ **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.
274
+
275
+ 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.
276
+
277
+ ## First-person viewmodel, WorldOverlay props, render cues (#542)
278
+
279
+ Three composable-chrome seams for FPS/TPS polish — a placement hook plus a good default, never a forced look:
280
+
281
+ - **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.
282
+ - **`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.
283
+ - **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.
284
+ `@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.
285
+
286
+ ## Settings menu
287
+
288
+ **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`):
289
+
290
+ - `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.
291
+ - `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`.
292
+ - `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.
293
+ - `surface: "quick"` — additionally mount compact on-screen volume/graphics buttons. Omit for none. `settings: false` — off entirely.
294
+ - `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) }`.
295
+ - `categories: SettingCategoryDef[]` — declare custom category tabs, or relabel/reorder built-ins (`{ id, label, order? }`).
296
+ - `hide: SettingCategory[]` — drop built-in categories.
297
+
298
+ **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.
299
+
300
+ **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.
301
+
302
+ ## 8. Motion and game feel
303
+
304
+ Add purposeful motion for:
305
+
306
+ - screen entry and exit
307
+ - confirm and cancel
308
+ - warnings
309
+ - score increases
310
+ - objective updates
311
+ - damage
312
+ - victory and failure
313
+ - countdowns
314
+ - pause
315
+ - item pickup
316
+
317
+ Motion should be brief, readable, interruptible when necessary, coordinated, consistent with the game’s art direction, and respectful of reduced-motion settings.
318
+
319
+ Do not animate everything constantly. Motion communicates hierarchy, cause and effect, urgency, and state changes.
320
+
321
+ **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.
322
+
323
+ ## 9. Mobile controls are genre-authored
324
+
325
+ Shared input mechanics may remain shared. Their visual treatment and arrangement must match the game.
326
+
327
+ Examples:
328
+
329
+ **Driving**
330
+ Steering region or wheel, accelerator, brake, handbrake, optional camera/map control.
331
+
332
+ **Stealth**
333
+ Movement zone, sneak/crouch hold, contextual interaction, temporary map/schedule control.
334
+
335
+ **Shooter**
336
+ Movement zone, aim region, fire/action cluster, weapon or ability controls.
337
+
338
+ **Puzzle/arcade**
339
+ Direct drag, tap, swipe, paddle region, or discrete directions. Do not add a joystick without a gameplay reason.
340
+
341
+ Requirements:
342
+
343
+ - never cover critical HUD information
344
+ - use the game’s shape and material language
345
+ - fade training labels after learning
346
+ - consider thumb reach
347
+ - preserve accessible target sizes
348
+ - visually respond to activation
349
+ - support optional scaling where appropriate
350
+
351
+ Do not use one generic translucent controller across all games.
352
+
353
+ **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:
354
+
355
+ - **`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.
356
+ - **`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.
357
+ - **`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.
358
+ - **`movement: { axis: "horizontal" }`** restricts the joystick to a steering-only zone, freeing throttle/brake to render as pedal buttons — the standard driving layout.
359
+ - **`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.
360
+ - **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`.
361
+
362
+ Canyon Chase is the worked example: `movement: { axis: "horizontal" }` steering, `throttle`/`brake` pedals on the `right` rail, `handbrake` lever bottom-right, `style: "mechanical"`.
363
+
364
+ **`presentation: "hud"` games get 3D parity.** A pure-HUD game (no camera rig) now reaches the same input/audio seams as a 3D game:
365
+ - **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.
366
+ - **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*`.
367
+ - **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.
368
+
369
+ ## 10. Progressive instruction
370
+
371
+ Do not leave large control grids visible during gameplay.
372
+
373
+ Prefer:
374
+
375
+ - contextual prompts
376
+ - brief onboarding
377
+ - first-use hints
378
+ - a controls screen
379
+ - pause-menu reference
380
+ - icons attached to actions
381
+ - progressive disclosure
382
+
383
+ 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.
384
+
385
+ 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.
386
+
387
+ 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.
388
+
389
+ ## 11. Reference directions for flagship games
390
+
391
+ ### Clockwork Heist
392
+
393
+ 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.
394
+
395
+ ### Canyon Chase
396
+
397
+ 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.
398
+
399
+ ### Brick Breaker
400
+
401
+ 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.
402
+
403
+ These games must remain structurally distinct, not recolors of one component set.
404
+
405
+ ## 12. Accessibility and performance
406
+
407
+ Maintain:
408
+
409
+ - sufficient contrast
410
+ - readable text size
411
+ - keyboard navigation
412
+ - controller navigation where available
413
+ - reduced-motion support
414
+ - visible authored focus
415
+ - touch target sizing
416
+ - semantic labels where practical
417
+ - responsive scaling
418
+ - reasonable DOM and animation performance
419
+
420
+ Use blur, masks, textures, filters, and full-screen effects carefully, especially on mobile.
421
+
422
+ ## 13. Screenshot verification is mandatory
423
+
424
+ Capture and inspect meaningful states:
425
+
426
+ - desktop title screen
427
+ - desktop gameplay
428
+ - mobile landscape
429
+ - mobile portrait where supported
430
+ - pause
431
+ - victory or failure
432
+ - an interaction prompt
433
+ - touch controls in active use
434
+
435
+ Inspect for:
436
+
437
+ - overlap and clipping
438
+ - weak contrast
439
+ - unreadable scale
440
+ - excessive cards
441
+ - website-like composition
442
+ - broken safe areas
443
+ - conflicting hierarchy
444
+ - browser chrome interference
445
+ - poor thumb reach
446
+ - keyboard instructions on touch devices
447
+ - inconsistent art direction
448
+
449
+ Revise after inspection. Typechecking is not visual proof.
450
+
451
+ ## 14. Rejection criteria
452
+
453
+ Require revision when any of these are true:
454
+
455
+ - normal site navigation remains visible during active gameplay
456
+ - the player must scroll the page to use the game
457
+ - the game is presented inside a normal content card
458
+ - essential HUD is covered by touch controls
459
+ - controls overlap each other
460
+ - keyboard instructions appear on touch devices
461
+ - large instruction panels remain visible during gameplay
462
+ - more than three ordinary card-like panels are persistently visible
463
+ - the main action looks like a standard website button
464
+ - pause resembles a generic website modal
465
+ - victory/failure is only text plus restart
466
+ - restart is permanently visible without a gameplay reason
467
+ - menu elements lack pressed, focused, selected, or disabled states
468
+ - UI changes have no transition or feedback
469
+ - generic virtual controls are used without genre adaptation
470
+ - the theme only changes colors and fonts
471
+ - unrelated UI elements have equal visual weight
472
+ - mobile portrait technically fits but is not intentionally composed
473
+ - important UI sits beneath safe areas
474
+ - ordinary document flow determines the HUD layout
475
+ - generic default styling is used because no art direction was written
476
+ - the hosting website page renders its own title, tagline, or "enter game" button instead of loading straight into the game's own menu
477
+
478
+ ## 15. Compact implementation API appendix
479
+
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 `GamePlayerShell`, input integration, devtools, and `GameUiPreview`; `@jgengine/shell/GamePlayerShell` 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
+
482
+ 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
+
484
+ Headless components are not finished design. They are behavior and accessibility seams that the game must art-direct.
485
+
486
+ ## Definition of done
487
+
488
+ UI work is complete only when:
489
+
490
+ - the game owns the viewport
491
+ - site chrome is absent during active play
492
+ - no document scrolling is required
493
+ - HUD and control layers have clear responsibilities
494
+ - mobile controls do not cover critical content
495
+ - touch controls match the genre and game identity
496
+ - title, pause, and results screens feel authored
497
+ - interaction states and transitions are present
498
+ - screenshots have been reviewed and revised
499
+ - the implementation remains accessible and performant
500
+ - 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; fails automatically if it could pass for a generic admin dashboard.
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.