@genex-ai/cli-demo 0.81.0-dev.214 → 0.85.0-dev.216

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 (27) hide show
  1. package/README.md +9 -7
  2. package/dist/index.js +2623 -1684
  3. package/package.json +1 -1
  4. package/templates/controllers/character/first-person.ts +54 -0
  5. package/templates/controllers/character/follow-camera.ts +35 -1
  6. package/templates/skills/genex-ai-character/SKILL.md +25 -5
  7. package/templates/skills/genex-ai-hud/SKILL.md +175 -63
  8. package/templates/skills/genex-ai-hud/references/masked-fill.md +19 -13
  9. package/templates/skills/genex-ai-hud/references/stage1-prompt-template.md +42 -5
  10. package/templates/skills/genex-ai-hud/references/stage2-prompt-template.md +7 -3
  11. package/templates/skills/genex-ai-image/SKILL.md +40 -2
  12. package/templates/skills/genex-ai-menu/SKILL.md +47 -22
  13. package/templates/skills/genex-ai-model/SKILL.md +8 -0
  14. package/templates/skills/genex-ai-music/SKILL.md +142 -0
  15. package/templates/skills/genex-ai-skybox/SKILL.md +21 -5
  16. package/templates/skills/genex-ai-video/SKILL.md +13 -7
  17. package/templates/skills/genex-ai-voice/SKILL.md +151 -0
  18. package/templates/skills/genex-game-director/SKILL.md +183 -58
  19. package/templates/skills/genex-game-director/references/design-contract.md +40 -10
  20. package/templates/skills/genex-game-director/references/routing-map.md +44 -29
  21. package/templates/skills/genex-threejs-camera-direction/SKILL.md +4 -1
  22. package/templates/skills/genex-threejs-character-controller/SKILL.md +25 -1
  23. package/templates/skills/genex-threejs-character-controller/references/wiring.md +13 -2
  24. package/templates/skills/genex-threejs-creatures/SKILL.md +201 -0
  25. package/templates/skills/genex-threejs-game-ui/SKILL.md +79 -71
  26. package/templates/skills/genex-threejs-multiplayer/SKILL.md +6 -1
  27. package/templates/skills/genex-threejs-visual-validation/SKILL.md +9 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.81.0-dev.214",
3
+ "version": "0.85.0-dev.216",
4
4
  "description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,54 @@
1
+ // First-person body treatment (Revenant pilot D2): hide the LOCAL player's
2
+ // body from its own camera while keeping its shadow on the ground.
3
+ //
4
+ // Why not `visible = false`? That removes the mesh from the shadow pass too —
5
+ // the player loses their own shadow, one of the strongest grounding cues a
6
+ // first-person game has. Instead the meshes keep rendering but write no color
7
+ // and no depth: the shadow-map pass uses its own depth-material override, so
8
+ // the silhouette still lands in the shadow map.
9
+ //
10
+ // Why CLONED materials? VRM/Meshy loaders share materials across instances
11
+ // (the multiplayer clone pool shares one GPU set for every remote) — flipping
12
+ // `colorWrite` on a shared material would erase every REMOTE player's body
13
+ // too. Cloning localizes the change to this one model; restore disposes the
14
+ // clones and puts the originals back.
15
+ import * as THREE from "three";
16
+
17
+ const savedMaterials = new WeakMap<THREE.Mesh, THREE.Material | THREE.Material[]>();
18
+
19
+ /**
20
+ * Hide (or restore) a character model for first-person play. Call with the
21
+ * LOCAL player's model root after it loads — never with a remote player's
22
+ * clone. Idempotent in both directions.
23
+ *
24
+ * ```ts
25
+ * followCam.firstPerson = true; // camera at the eye anchor
26
+ * setFirstPersonBody(playerModel, true); // body invisible, shadow kept
27
+ * ```
28
+ */
29
+ export function setFirstPersonBody(root: THREE.Object3D, hidden: boolean): void {
30
+ root.traverse((obj) => {
31
+ const mesh = obj as THREE.Mesh;
32
+ if (!mesh.isMesh) return;
33
+ if (hidden) {
34
+ if (savedMaterials.has(mesh)) return; // already hidden
35
+ savedMaterials.set(mesh, mesh.material);
36
+ const clones = (Array.isArray(mesh.material) ? mesh.material : [mesh.material]).map(
37
+ (material) => {
38
+ const clone = material.clone();
39
+ clone.colorWrite = false; // draws nothing…
40
+ clone.depthWrite = false; // …and never occludes the scene
41
+ return clone;
42
+ },
43
+ );
44
+ mesh.material = Array.isArray(mesh.material) ? clones : clones[0]!;
45
+ } else {
46
+ const original = savedMaterials.get(mesh);
47
+ if (original === undefined) return; // was never hidden
48
+ const clones = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
49
+ for (const clone of clones) clone.dispose();
50
+ mesh.material = original;
51
+ savedMaterials.delete(mesh);
52
+ }
53
+ });
54
+ }
@@ -81,6 +81,19 @@ export type FollowCameraOptions = {
81
81
  * reference).
82
82
  */
83
83
  pointerLockAim?: boolean;
84
+ /**
85
+ * REAL first-person mode (Revenant pilot D2) — not the follow-distance hack.
86
+ * The camera sits AT the follow target (feed it the character's EYE height,
87
+ * ~1.6–1.7 m, not the head-top orbit anchor) and looks OUT along the exact
88
+ * direction the orbit camera would have looked from behind, so the
89
+ * pointer-lock aim math — and its verified input signs — are shared, not
90
+ * duplicated. While on: wheel zoom is ignored and collision pullback is
91
+ * skipped (there is nothing behind the eyes). Toggle at runtime via the
92
+ * public `firstPerson` field (e.g. enter/exit a vehicle). Hide the local
93
+ * body with `setFirstPersonBody()` (first-person.ts) — it keeps the shadow.
94
+ * Default false.
95
+ */
96
+ firstPerson?: boolean;
84
97
  /** Radians of view rotation per pixel of locked mouse movement. Default 0.0023. Mutable via `aimSensitivity` setter. */
85
98
  aimSensitivity?: number;
86
99
  /**
@@ -208,6 +221,8 @@ export class FollowCamera {
208
221
  smoothTime: number;
209
222
  /** Static environment meshes for collision pullback; mutate freely (e.g. after level load). */
210
223
  colliderMeshes: THREE.Mesh[];
224
+ /** First-person mode (see {@link FollowCameraOptions.firstPerson}); toggle freely at runtime. */
225
+ firstPerson: boolean;
211
226
 
212
227
  private _camera: THREE.PerspectiveCamera;
213
228
  private _domElement: HTMLElement;
@@ -255,6 +270,7 @@ export class FollowCamera {
255
270
  // Preallocated scratch (no allocations in the per-frame path).
256
271
  private _sdRef: ScalarRef;
257
272
  private _sphericalDir: THREE.Vector3;
273
+ private _fpLook: THREE.Vector3;
258
274
  private _camDir: THREE.Vector3;
259
275
  private _finalDir: THREE.Vector3;
260
276
  private _crossAxis: THREE.Vector3;
@@ -295,6 +311,7 @@ export class FollowCamera {
295
311
  this.followEnabled = true;
296
312
  this.smoothTime = options.smoothTime ?? 0.1;
297
313
  this.colliderMeshes = options.colliderMeshes ?? [];
314
+ this.firstPerson = options.firstPerson ?? false;
298
315
 
299
316
  this._draggingSmoothTime = options.draggingSmoothTime ?? 0.125;
300
317
  this._upLerpFactor = options.upLerpFactor ?? 0.1;
@@ -339,6 +356,7 @@ export class FollowCamera {
339
356
 
340
357
  this._sdRef = { value: 0 };
341
358
  this._sphericalDir = new THREE.Vector3();
359
+ this._fpLook = new THREE.Vector3();
342
360
  this._camDir = new THREE.Vector3();
343
361
  this._finalDir = new THREE.Vector3();
344
362
  this._crossAxis = new THREE.Vector3();
@@ -433,6 +451,10 @@ export class FollowCamera {
433
451
  this._onWheel = (e: WheelEvent) => {
434
452
  if (!this.enabled) return;
435
453
  e.preventDefault();
454
+ // First-person has no zoom — and letting the wheel mutate the hidden
455
+ // orbit distance would make the NEXT third-person switch snap to a
456
+ // surprise distance.
457
+ if (this.firstPerson) return;
436
458
  // Upstream wheel normalization (unclamped, proportional): pixel mode divides deltaY by
437
459
  // deltaYFactor*10 (-30, or -10 on Mac); line mode (Firefox) or ctrlKey (trackpad pinch
438
460
  // gesture) divides by deltaYFactor only. Then multiplicative zoom, dollyScale =
@@ -735,7 +757,7 @@ export class FollowCamera {
735
757
  // the DAMPED distance itself is clamped to the hit; `_distanceEnd` (and the velocity) stay
736
758
  // untouched, so when the obstruction clears, SmoothDamp eases the camera back out to the
737
759
  // user's zoom instead of snapping.
738
- if (this.colliderMeshes.length > 0) {
760
+ if (!this.firstPerson && this.colliderMeshes.length > 0) {
739
761
  this._distance = Math.min(this._distance, this._collisionTest());
740
762
  }
741
763
 
@@ -747,6 +769,18 @@ export class FollowCamera {
747
769
  Math.sin(this._polar) * Math.cos(this._azimuth)
748
770
  );
749
771
 
772
+ if (this.firstPerson) {
773
+ // First-person: the camera sits AT the (eye-height) follow target and
774
+ // looks OUT along the exact direction the orbit camera would have looked
775
+ // from behind (-dir). Sharing the angle state means the pointer-lock aim
776
+ // math — and its verified mouse-direction signs — apply unchanged, and a
777
+ // runtime toggle back to third-person keeps view continuity.
778
+ this._camera.position.copy(this._target);
779
+ this._fpLook.copy(this._target).addScaledVector(dir, -1);
780
+ this._camera.lookAt(this._fpLook);
781
+ return;
782
+ }
783
+
750
784
  this._camera.position.copy(this._target).addScaledVector(dir, this._distance);
751
785
  this._camera.lookAt(this._target);
752
786
  }
@@ -1,14 +1,34 @@
1
1
  ---
2
2
  name: genex-ai-character
3
- description: Generate a controller-ready Meshy humanoid with Genex, search the committed Meshy animation catalog by gameplay intent, add exact same-rig actions, and install the shared physics controller's Meshy-native adapter. Use when a game needs a custom playable character or motion not covered by the default VRM + UAL lane.
3
+ description: Generate a controller-ready Meshy humanoid with Genex, search the committed Meshy animation catalog by gameplay intent, add exact same-rig actions, and install the shared physics controller's Meshy-native adapter. Use for the game's themed character the default whenever the protagonist is visible — or for motion not covered by the VRM + UAL lane.
4
4
  ---
5
5
 
6
6
  # Genex AI Character
7
7
 
8
- Use the existing VRM + UAL character controller by default. Use this lane when
9
- the game needs a custom generated humanoid or an action unavailable in UAL.
10
- Both lanes use the same ECCTRL-derived Rapier controller, camera, inputs,
11
- crossfades, transition rules, and multiplayer authority contract.
8
+ A themed Meshy character is the DEFAULT for any game whose protagonist is
9
+ visible (third-person, or first-person with co-op/remote players); the
10
+ VRM + UAL controller is the instant placeholder while it builds, and the
11
+ final character only for games with no themed protagonist. Both lanes use
12
+ the same ECCTRL-derived Rapier controller, camera, inputs, crossfades,
13
+ transition rules, and multiplayer authority contract.
14
+
15
+ **Two lanes, two approval shapes:**
16
+
17
+ - **The game-default lane (one user stop).** The three concept candidates
18
+ ride the SAME review beat as the game-concept keep-or-change question —
19
+ one review, two picks. The player's pick authorizes the whole lane
20
+ (preview and the 10,000-face rigging remesh both proceed on it — state
21
+ each step plainly as you run it). If the player hasn't picked by the
22
+ time the character blocks progress (or ~10 minutes), pick the strongest
23
+ candidate yourself, say which and why in chat, and proceed. That
24
+ auto-proceed — including its generation spend — is owner-ratified
25
+ platform policy (2026-07-23), not an agent liberty: record the pick in
26
+ DESIGN.md → Decisions.
27
+ - **The user-initiated custom-character lane (two stops — the ceremony
28
+ below, in full).** When the user themselves asked for a custom
29
+ character, the approvals ARE the product: an explicit candidate
30
+ selection, then an explicit approval of the separate 10,000-face
31
+ remesh. Never compress these two stops for a user-initiated ask.
12
32
 
13
33
  ## Generate a controller-ready character
14
34
 
@@ -39,25 +39,38 @@ ring) that no hand-written CSS can fake.
39
39
  Behind an OPAQUE frame sprite there is no plate — the frame IS the backing
40
40
  surface, and a dark div stacked behind it only protrudes as a box over the
41
41
  scene (the recurring black-box defect). Where a plate IS needed — bare DOM
42
- readouts, thin-outline widgets — it stays WITHIN the widget's silhouette:
43
- inset inside the frame, or shaped to it, never a full rectangle spilling
44
- past the art onto the scene.
42
+ readouts, thin-outline widgets — it stays WITHIN the widget's silhouette,
43
+ and the mechanism is the **silhouette plate**: run
44
+ `npx genex ui plate --in <frame.png>` (free, local) to trace the frame's
45
+ REAL interior into a mask PNG, then give the plate div
46
+ `mask-image: url(<name>-plate.png)` (+ `-webkit-` twin,
47
+ `mask-size: 100% 100%`) alongside its usual `rgba`/`backdrop-filter`. The
48
+ plate then fits spiky gothic panels and shaped medallions exactly — a bare
49
+ rounded rectangle spills past thin frames and can't follow shaped art (the
50
+ recurring plate-spill defect).
45
51
  **Glass is a technique, not a default** — do not reach for translucency
46
52
  because this skill mentions it; reach for it when THIS game's brief does.
47
- What is fixed is WHERE plates are built the DOM because
48
- semi-transparency physically cannot survive sprite extraction: a composited
49
- panel's pixels are a blend of panel and background, and no cutout can
50
- un-blend them. Plates are NEVER baked into sprites. Give the plate its
51
- corners with `border-radius` **never a raw `clip-path`/`mask` chamfer**
53
+ Plates stay DOM by default because a sprite cut from an UNKNOWN background
54
+ cannot carry translucency (its pixels are a blend of panel and scene) — but
55
+ a genuinely glassy identity panel has a second lane now:
56
+ `npx genex image "<glass panel prompt>" --glass` generates it on a
57
+ controlled flat-magenta key screen and solves REAL per-pixel translucency
58
+ into the shipped PNG (`$genex-ai-image` documents the lane; a frosted panel
59
+ reads as true glass over any scene). Use DOM plates for tint-and-blur
60
+ backing; use `--glass` when the glass itself is crafted identity art. Give
61
+ a DOM plate its corners with `border-radius` or shape it with a `ui plate`
62
+ silhouette mask — **never a raw `clip-path`/`mask` chamfer**
52
63
  (it shears off borders, shadows, and content near the cut and re-breaks on
53
64
  every padding/value change: the recurring "cut corners" defect). A genuinely
54
- ornamented or angular frame belongs in chrome, generated. The one
55
- load-bearing `mask`/`clip-path` in this skill is the masked-fill reveal
56
- below — never corner shaping.
65
+ ornamented or angular frame belongs in chrome, generated. The load-bearing
66
+ `mask`/`clip-path` uses in this skill are the masked-fill reveal below and
67
+ the `ui plate` silhouette — never corner shaping.
57
68
  - **Chrome (sprites — what THIS pipeline generates).** Opaque frames, corner
58
69
  brackets, ornaments, emblems, icons, medallions — hard-alpha art laid over
59
70
  the glass. The Stage-2 sheet contains ONLY chrome; never a panel with its
60
- fill or glass surface baked in.
71
+ fill or glass surface baked in. One deliberate exception to "no fills":
72
+ every meter frame ships WITH its EMPTY dark in-style track baked in (the
73
+ Stage-2 track rule) — the empty state is art; only the moving fill is DOM.
61
74
  - **Data (DOM).** Numbers, labels, and bar fills are DOM text and masked
62
75
  fills (the fillBox pattern below) — always over the glass, never
63
76
  rasterized into art.
@@ -109,6 +122,15 @@ The HUD overlays a **live 3D scene**. The pixels between widgets show the game.
109
122
  their opaque chrome (frame, brackets, ornament); the plate itself is
110
123
  rebuilt at runtime in CSS matching the mockup's tint (see the three layers
111
124
  above).
125
+ - **No rectangular backing plates behind bars, digits, or icons — in the
126
+ mockup, in sprites, or in CSS.** The Stage-1 template's widget-construction
127
+ paragraph forbids them at the source (widgets are shaped silhouettes drawn
128
+ over the scene — never delete that paragraph), and the runtime never adds
129
+ one back: ornament lives on the widget's own silhouette. A screen that
130
+ truly needs a plate (a menu sheet, an inventory panel) shapes it from the
131
+ art's real interior with `npx genex ui plate` — never a bare rounded
132
+ rectangle. Measured across 7 genres: without this rule every genre grows
133
+ heavy generic plates behind its bars and digits.
112
134
  - **4–7 widgets.** Fewer doesn't read as a HUD; more clutters the screen and
113
135
  burns generations.
114
136
  - **Every widget needs internal contrast** — a panel fill, outline stroke, or
@@ -212,22 +234,29 @@ and [references/stage2-prompt-template.md](references/stage2-prompt-template.md)
212
234
 
213
235
  **Order of work: kick Stage 1 off as your FIRST action at the UI plan
214
236
  gate — the Stage-1 mockup IS the game concept.** There is no separate
215
- UI-free concept image before it: this one generation carries scene + HUD,
216
- serves as the user's style checkpoint, and anchors all later art
217
- (`$genex-threejs-game-ui` owns the checkpoint choreography). **Stage 2 onward
218
- is frame-anchored, so it waits for the user's keep/change YES on that
219
- checkpoint kick it off the instant they approve, not before.** The scene half
237
+ UI-free concept image before it: this ONE generation (no candidate variants
238
+ unless the player asks) carries scene + HUD, serves as the user's style
239
+ checkpoint, and anchors all later art (`$genex-threejs-game-ui` owns the
240
+ checkpoint choreography). **The moment the mockup lands, enqueue Stage 2 +
241
+ the menu still + the logotype `--no-wait` IMMEDIATELY THEN show the
242
+ player the frame and ask keep/change as information, never as a gate.**
243
+ Silence = the concept stands; a "change" answer loops the concept with the
244
+ player's notes and the chain re-runs from the new frame — image-priced,
245
+ cheap by design, so say so in one line and do it. Only the menu VIDEO waits
246
+ (`$genex-ai-menu` owns its event triple). The scene half
220
247
  of the prompt is written in TEXT from the game plan — `[GAME_SCENE]` in the
221
248
  Stage-1 template: setting, the moment, what the player is doing, lighting.
222
249
  **If a concept/reference image already exists — the user's own concept art, or
223
250
  a look frame you generated and they approved — anchor Stage 1 to it with
224
- `--edit <that-url>` so the HUD inherits its exact palette, materials, and
225
- lighting; that anchoring is what makes the final HUD actually match the
251
+ `--edit <that-url-or-file>` so the HUD inherits its exact palette, materials,
252
+ and lighting; that anchoring is what makes the final HUD actually match the
226
253
  concept, and skipping it is why a text-only mockup drifts.** A text-only
227
- Stage 1 is the fallback for when no reference exists. (To `--edit` a
228
- user-supplied image it must be reachable as an R2 URL a bare chat attachment
229
- is not; reproduce it as a generation first, or say plainly you can't anchor to
230
- it rather than silently guessing.) Then write the
254
+ Stage 1 is the fallback for when no reference exists. A user-supplied
255
+ reference needs no upload step: `--edit` takes a local file path directly
256
+ (`--edit ./reference.png`, ≤4 MB inlined) a chat attachment saved to disk
257
+ is a perfectly good anchor, and "I can't feed the local screenshot into the
258
+ art chain" is never true and never a reason to reach for another tool.
259
+ Then write the
231
260
  widget layout and wiring code (placement, masked-fill scaffolding,
232
261
  plain-CSS placeholder bars) while it renders — the CSS HUD keeps
233
262
  the game playable until the sprites land. When a sprite lands it REPLACES
@@ -237,19 +266,14 @@ is the black-box defect. The mockup is a STYLE anchor
237
266
  only: the widget set and
238
267
  layout come from the element inventory and the game contract — a mechanic
239
268
  the mockup invented (a lap counter, a stamina orb) does not enter the HUD,
240
- and a mechanic it failed to show still does. Generate 2 mockup candidates in one
241
- call (`--candidates 2`) and pick the better one: a re-roll costs the whole
242
- serial chain, a second candidate costs nothing extra in wall-clock. If the
243
- user's concept-loop feedback rejects the style after Stage 2+ ran,
244
- re-run from Stage 1 with their notes — cheaper than it sounds (one image
245
- is the whole concept now), so say so in one line and do it.
269
+ and a mechanic it failed to show still does.
246
270
 
247
271
  ```bash
248
272
  # Stage 1 — the game CONCEPT: full HUD composited over the game's own scene.
249
- # TEXT-described; add `--edit <concept-url>` to anchor it to an existing
250
- # concept/reference image so the HUD inherits its exact style.
251
- # Two candidates in one call; pick the better, save its URL:
252
- npx genex image "<filled stage-1 prompt>" --size 2560x1440 --quality high --candidates 2
273
+ # TEXT-described; add `--edit <concept-url-or-local-file>` to anchor it to an
274
+ # existing concept/reference image so the HUD inherits its exact style.
275
+ # ONE concept image; save its URL:
276
+ npx genex image "<filled stage-1 prompt>" --size 2560x1440 --quality high
253
277
 
254
278
  # Stage 2 — deconstruct the mockup into an asset sheet on white:
255
279
  npx genex image "<filled stage-2 prompt>" --edit <mockup-url> --quality high
@@ -306,6 +330,12 @@ To fix ONE sprite, don't re-run the pipeline:
306
330
  (the canvas sliced it — regenerate the sheet with more margin around every
307
331
  element) or looks like a fragment (extreme aspect / near-empty box — try
308
332
  `--dilate` to glue split pieces).
333
+ - **Stacked meters cluster.** Meters that sit adjacent/stacked in the mockup
334
+ (HP directly over stamina) tend to come back as one clustered crop on the
335
+ sheet — when your mockup stacks meters, ask the Stage-2 sheet for extra
336
+ whitespace between the STACKED meter cells specifically (measured on a
337
+ live run: the tools hard-refuse the resulting neighbor-in-crop defect, so
338
+ spacing up front saves a sheet re-roll).
309
339
  - **gpt-image-2 silently squashes past 3:1** — never request a canvas with
310
340
  aspect beyond 3:1. For thin strips (a wide bar frame), generate inside a
311
341
  ≤3:1 canvas with margins, then `genex ui trim` down to the art.
@@ -314,13 +344,43 @@ To fix ONE sprite, don't re-run the pipeline:
314
344
  or region-`--edit` the sheet and re-extract. Re-running Stage 2 re-rolls
315
345
  every OTHER sprite too — the most expensive way to fix one.
316
346
 
347
+ ## The sheet repair lane — model edits, never pixel hacks
348
+
349
+ When a sheet defect surfaces AFTER generation — a white-painted trough
350
+ (`ui audit`'s hard `white-trough` finding, or `ui masks`' refusal), a wrong
351
+ ornament, a mis-drawn cell — the fix is a MODEL edit of the sheet, then
352
+ re-clean + re-extract. Never patch shipped pixels by hand (threshold-punching
353
+ a trough chews the frame's bezel — a tried-and-rejected workaround; the one
354
+ allowed pixel surgery is extraction's own known-matte rim defringe):
355
+
356
+ ```bash
357
+ # Whole-sheet repair (all bar cells at once — proven to preserve bezels,
358
+ # ornaments AND the green annotated twins):
359
+ npx genex image "Repaint every meter channel interior as the meter's EMPTY state: a flat dark in-style track. Change nothing else." --edit <sheet-url>
360
+
361
+ # Region repair (one cell, others must not re-roll): add a mask whose
362
+ # TRANSPARENT hole covers the zone to change — assemble it from the channel
363
+ # mask + the cell's crop rect, or a hand-drawn rectangle:
364
+ npx genex image "<same repaint ask>" --edit <sheet-url> --inpaint <mask.png>
365
+ ```
366
+
367
+ **`--inpaint` honesty — a targeting scope, not a scalpel.** The mask focuses
368
+ WHERE the change happens (proven: the masked cell's trough repainted while the
369
+ other bar's design survived untouched) — but the WHOLE sheet still re-renders
370
+ and any transparency is destroyed either way. So after ANY repair edit,
371
+ mandatory: re-run `--clean` on the result, re-extract, and re-derive masks —
372
+ the old crops/masks describe the pre-edit sheet.
373
+
317
374
  ## Composition rules (each one is a documented failure class)
318
375
 
319
- - **Continuous fills = masked reveal.** HP, mana, fuel, XP, stamina, charge —
320
- NEVER generate `*-fill.png` sprites, and NEVER use a naive `width`/`height`
321
- percentage of the widget box (it drifts ~12% off the art's real channel:
322
- empty at 25%, full by 75%). The frame stays a sprite; the fill is the
323
- masked-fill DOM pattern below.
376
+ - **Continuous fills = fill-on-top masked reveal.** HP, mana, fuel, XP,
377
+ stamina, charge — NEVER generate `*-fill.png` sprites, and NEVER use a
378
+ naive `width`/`height` percentage of the widget box (it drifts ~12% off the
379
+ art's real channel: empty at 25%, full by 75%). THE bar recipe: the frame
380
+ sprite carries its EMPTY dark track baked at source (Stage-2 track rule),
381
+ and a near-opaque gradient fill paints ON TOP of it, clipped to `FB` inside
382
+ the channel mask — the masked-fill DOM pattern below. Never a plate/track
383
+ div stacked behind the frame to fake an empty state.
324
384
  - **Minimap/radar interiors are an empty CSS disc** (`border-radius: 50%`,
325
385
  dark background). Only the decorative ring frame is a sprite, prompted with
326
386
  an explicitly **transparent center — no map, no terrain inside**. Gameplay
@@ -389,12 +449,12 @@ form:
389
449
 
390
450
  ```html
391
451
  <div class="widget" id="hud-hp"> <!-- root: overflow VISIBLE (glows may bleed) -->
392
- <div class="hp-mask"> <!-- outer: the silhouette mask -->
452
+ <img class="hp-frame" src="/assets/hud/hp-frame.png" alt=""> <!-- frame + baked empty track -->
453
+ <div class="hp-mask"> <!-- outer: the silhouette mask, ON TOP of the frame -->
393
454
  <div class="hp-fill" data-fill data-fill-mask="hp-mask.png"
394
455
  data-fill-box="0.0784,0.3469,0.9216,0.3605"
395
456
  data-fill-from="left" data-fill-ratio="0.72"></div>
396
457
  </div>
397
- <img class="hp-frame" src="/assets/hud/hp-frame.png" alt="">
398
458
  </div>
399
459
  ```
400
460
 
@@ -408,7 +468,7 @@ form:
408
468
  .hp-fill { position: absolute; inset: 0; /* FULL-BOX gradient; the clip places the edge */
409
469
  background: linear-gradient(180deg, #ef5b5b, #7a1d18); }
410
470
  .hp-frame { position: absolute; inset: 0; width: 100%; height: 100%;
411
- object-fit: fill; } /* frame paints on top */
471
+ object-fit: fill; } /* under the fill: its baked track IS empty */
412
472
  ```
413
473
 
414
474
  ```ts
@@ -426,17 +486,19 @@ function setHp(hp: number, maxHp: number): void {
426
486
  }
427
487
  ```
428
488
 
429
- The structure, in one breath: the OUTER div carries the alpha mask
430
- (`mask-image` + `mask-size: 100% 100%`, `overflow: hidden`) so the fill can
431
- only exist inside the art's silhouette; the INNER div is a FULL-BOX gradient
489
+ The structure, in one breath: the frame `<img>` (with its baked EMPTY track)
490
+ paints first with `object-fit: fill`; the OUTER div after it carries the alpha
491
+ mask (`mask-image` + `mask-size: 100% 100%`, `overflow: hidden`) so the fill
492
+ can only exist inside the art's channel; the INNER div is a FULL-BOX gradient
432
493
  whose `clip-path` places the leading edge at the **channel-relative** level
433
494
  using `FB` — for a left fill the right edge sits at `(FB.x + ratio * FB.w) * 100%`,
434
495
  for a bottom fill the top edge at `(FB.y + (1 - ratio) * FB.h) * 100%`. Stamp
435
496
  `data-fill`, `data-fill-mask`, `data-fill-box`, `data-fill-from`, and
436
497
  `data-fill-ratio` on the inner div — they make every fill auditable against
437
- the mask JSON. The frame `<img>` sits on top with `object-fit: fill`, and the
438
- widget root keeps `overflow: visible` so glow effects bleed past the art
439
- instead of clipping to a hard square.
498
+ the mask JSON. The widget root keeps `overflow: visible` so glow effects bleed
499
+ past the art instead of clipping to a hard square. (Fill UNDER the frame is
500
+ the exception, for genuinely transparent channel cavities with a reason
501
+ [references/masked-fill.md](references/masked-fill.md) has the rule.)
440
502
 
441
503
  ## Reactivity — the juice floor
442
504
 
@@ -512,7 +574,10 @@ out of budget to check, say the HUD is unverified — never call it done.
512
574
  with corrected name order rather than regenerating anything.
513
575
  - **Fills grow monotonically** — drive each fill through 0 → 50 → 100 and
514
576
  confirm the reveal grows and stays inside the art's channel (never bulging
515
- past the frame's track).
577
+ past the frame's track). A screenshot at full HP proves nothing about a
578
+ meter — the classic shipped defect (a painted-full trough behind the mask)
579
+ is invisible at 100%. This is why the milestone smoke pass's gameplay
580
+ capture is taken AFTER taking damage once.
516
581
  - **No text clips** — for every text node, `scrollWidth <= clientWidth`.
517
582
  Display fonts run 25–50% wider than a naive estimate; widen the box or drop
518
583
  the weight, don't shrink the font.
@@ -571,10 +636,10 @@ separate concept spend), a couple of minutes each at high quality — budget an
571
636
  hour end to end, not five minutes. That fits comfortably inside the image rate
572
637
  limit; local `genex ui` steps are free and instant. Two rules keep the clock
573
638
  honest: the sprite pipeline runs WHILE you build (kick Stage 1 first, code
574
- against CSS placeholders, swap sprites in as stages land never make the
575
- player-facing v0 wait on it), and candidates beat re-rolls (`--candidates 2`
576
- on the mockup: choosing between two costs zero extra wall-clock; regenerating
577
- after a bad single costs the whole chain).
639
+ against CSS placeholders, swap sprites in as stages land), and the chain
640
+ never parks behind the keep/change answer — Stage 2, the menu still, and the
641
+ logotype enqueue the moment the mockup lands (the order-of-work rule above);
642
+ a later "change" loops the concept at image prices.
578
643
 
579
644
  ## Publish checklist
580
645
 
@@ -593,13 +658,19 @@ after a bad single costs the whole chain).
593
658
  - `npx genex image` — `--size <WxH>` exact pixels (multiples of 16, each side
594
659
  ≤ 3840, aspect at most 3:1); `--quality <low|medium|high>` (high for the
595
660
  mockup/sheet, medium for single sprites); `--candidates <2|3|4>` several
596
- variants in ONE call (use 2 for the mockup pick the better, skip the
597
- re-roll); `--edit <url>` image-to-image edit of an R2 URL; `--clean <url>`
661
+ variants in ONE call (only when the player asks for variants the
662
+ doctrine is ONE concept); `--edit <url|file>` image-to-image edit of an
663
+ R2 URL or a local image file (≤4 MB, inlined); `--clean <url>`
598
664
  background removal only; `--remove-bg` chains removal after a
599
665
  generation/edit; `--bg-mode <sprite|glyph|sheet>` picks the removal model
600
- (`glyph` for digits/closed shapes; `--clean` defaults to `sheet`);
666
+ (`glyph` for digits/closed shapes; `--clean` defaults to `sheet`; `matte`
667
+ = BiRefNet soft alpha for hair/glow/smoke edges the binary cutters butcher);
601
668
  `--no-wait` enqueue and continue — `npx genex wait <id>` picks the result
602
- up later (safe to re-run; never creates a new generation).
669
+ up later (safe to re-run; never creates a new generation);
670
+ `--inpaint <mask.png|url>` region mask for `--edit` (transparent hole =
671
+ the zone to change — see the repair lane's honesty note);
672
+ `--upscale <url>` 2x utility upscale of an existing asset;
673
+ `--glass` the magenta-key glass lane (`$genex-ai-image`).
603
674
  - `npx genex ui extract --in <png|url> --out-dir <dir> --names a,b,c` — also
604
675
  writes a `.bbox.json` sidecar per sprite (with `innerBBox` for text
605
676
  placement); `--expect <n>` hard-errors on a component miscount before
@@ -614,10 +685,17 @@ after a bad single costs the whole chain).
614
685
  eyedropper: background/plate color + dark/light/chromatic ink candidates.
615
686
  Use the returned hex verbatim for the DOM text over that region.
616
687
  - `npx genex ui trim --in <png>` — crop to alpha content + report real dims.
688
+ - `npx genex ui plate --in <frame.png> [--erode 2]` — trace the frame's real
689
+ interior (art + enclosed cavity) into `<name>-plate.png`; wire it as the
690
+ CSS plate's `mask-image` (the silhouette-plate rule above). Free, local.
617
691
  - `npx genex ui audit [--dir public/assets/hud] [--src src]` — the mechanical
618
692
  wiring scan (unreferenced sprites/masks, naive %-fill patterns, and a
619
- missing/incomplete mobile viewport meta in the root `index.html`); warn-only,
620
- `--strict` exits 1 on findings.
693
+ missing/incomplete mobile viewport meta in the root `index.html`); warn-only
694
+ except the provable kinds (`mask-frame-mismatch`, `white-trough`), which
695
+ exit 1; `--strict` exits 1 on any findings.
696
+ - Extraction defringes chroma-keyed sprite rims by default (2px matte
697
+ un-blend against the sheet white — kills the halo over dark scenes; the
698
+ sidecar stamps `defringed`); `--no-defringe` opts out.
621
699
 
622
700
  ## Troubleshooting
623
701
 
@@ -637,15 +715,49 @@ after a bad single costs the whole chain).
637
715
  - **The fill drifts off the art's channel** — a naive `width`/`height` % crept
638
716
  in, or `FB` doesn't match the mask. Use the clipPath formula with the
639
717
  `fillBox` from the masks JSON, verbatim.
640
- - **The fill never shows, at any ratio** — the frame PNG's channel interior is
641
- opaque track art occluding the fill beneath it. Raise the mask div above the
642
- frame (`z-index`); the mask keeps the fill inside the channel, so the rim
643
- and baked labels are unaffected (see the z-order note in
644
- [references/masked-fill.md](references/masked-fill.md)).
718
+ - **The fill never shows, at any ratio** — the fill is painting UNDER the
719
+ frame while the frame's channel is opaque track art. The recipe puts the
720
+ mask div AFTER the frame `<img>` in source order (fill on top); check the
721
+ DOM order first. The mask keeps the fill inside the channel, so the rim and
722
+ baked labels are unaffected (see [references/masked-fill.md](references/masked-fill.md)).
645
723
  - **`genex ui masks` fails registration/coverage checks** — the annotated
646
724
  duplicate drifted from the clean cell (different scale/position) or the
647
725
  green isn't flat `#00ff00`. Region-edit the sheet (`--edit <sheet-url>`)
648
726
  asking for a same-size duplicate with flat green fill zones, then re-run.
727
+ Do NOT loosen the tolerance to force a pair through: a mask registered off
728
+ its frame ships a fill that floats off the art, and loosened runs are
729
+ stamped in the metadata (preview/publish will warn about them).
730
+ - **`ui masks` refuses: "the fill channel is painted filled in the frame
731
+ art"** — the sheet model ignored "REMOVE the fill" and left the bar painted
732
+ full (usually near-white). The defect is invisible at 100% and shows the
733
+ moment the value drops. Region-edit that cell so the trough is EMPTY
734
+ (dark/recessed, no fill), then re-run — never wire that frame as-is.
735
+ - **`ui audit` errors with `white-trough`** — a shipped frame's fill channel
736
+ measures opaque near-sheet-white: the model drew the trough white/filled and
737
+ the cleaner can't reach enclosed regions. Under-fills silently never show
738
+ behind it. Run the sheet repair lane (region-edit the trough to its EMPTY
739
+ dark track, `--inpaint` for one cell), then re-clean + re-extract — never
740
+ ship the frame, never pixel-punch the white out.
741
+ - **After any `--edit`/`--inpaint` repair the sprites look stale/wrong** —
742
+ the whole sheet re-rendered (that's the edit contract): the OLD extractions
743
+ and masks describe the pre-edit sheet. Re-run `--clean`, re-extract, and
744
+ re-derive masks against the new sheet URL.
745
+ - **Wisps, glows, smoke, hair get chopped by `--remove-bg`** — the binary
746
+ cutters can't do soft edges. Recut with `--bg-mode matte` (BiRefNet soft
747
+ alpha).
748
+ - **`ui masks`/`ui extract` refuses: "the crop catches another sheet
749
+ element"** — elements sit too close on the sheet, so the crop would bake a
750
+ sliver of a neighbor into this asset. Regenerate the sheet with more
751
+ spacing between elements (or tighten the crop / lower `--padding`).
752
+ - **"the crop box is slicing the element" (flush edge)** — widen the crop box
753
+ if the sheet has room; otherwise the sheet ran out of margin here —
754
+ region-edit or regenerate it with more margin around the element. There is
755
+ no tolerance to bump: a sliced frame ships as a cut-off corner.
756
+ - **`ui audit` errors with `mask-frame-mismatch`** — the CSS ships a frame
757
+ PNG cut from DIFFERENT sheet geometry than the crop the mask was derived
758
+ against, so the fill floats off the frame. If you swap which frame variant
759
+ ships, re-run `ui masks` against it (or ship the mask's own `-frame.png`) —
760
+ a frame swap is never cosmetic once a mask is involved.
649
761
  - **`masks --auto` reports ambiguity or a missing twin** — it never guesses:
650
762
  fall back to explicit `--pairs` with hand-read crop rects for just that
651
763
  meter (the error lists the candidate cells it saw).
@@ -18,24 +18,30 @@ mask's opaque pixels and prints in its JSON.
18
18
 
19
19
  ```
20
20
  widget root position:absolute; overflow: VISIBLE ← glows may bleed past the art
21
- ├─ outer mask div inset:0; overflow:hidden; mask-image:url(<mask.png>); mask-size:100% 100%
22
- └─ inner fill div inset:0; FULL-BOX gradient; clip-path places the edge; data-fill-* stamps
23
- └─ frame <img> inset:0; width/height:100%; object-fit:fill ← painted on top
21
+ ├─ frame <img> inset:0; width/height:100%; object-fit:fill ← the frame WITH its baked
22
+ empty dark track (Stage-2 rule)
23
+ └─ outer mask div inset:0; overflow:hidden; mask-image:url(<mask.png>); mask-size:100% 100%
24
+ └─ inner fill div inset:0; FULL-BOX gradient; clip-path places the edge; data-fill-* stamps
24
25
  ```
25
26
 
26
27
  Confinement is the outer div's job (`overflow: hidden` + the mask). The
27
28
  widget **root stays `overflow: visible`** — otherwise any glow/halo that
28
29
  blooms past the silhouette gets sliced to a hard square.
29
30
 
30
- **Z-order depends on the frame art's channel.** "Frame on top" assumes the
31
- frame PNG's fill channel is transparent or translucent glass. This pipeline's
32
- deconstructed frames usually keep an **opaque** dark track texture inside the
33
- channel a fill underneath is completely occluded (the bar looks stuck at
34
- empty no matter the ratio). In that case raise the mask div above the frame
35
- (`z-index` on the outer mask div): the mask confines the fill to the channel
36
- pixels, so the rim and any baked labels stay untouched. Check one extracted
37
- frame's channel pixels before wiring; if you can't see the fill move, this
38
- z-order is the first suspect.
31
+ **The fill paints ON TOP of the frame that is the recipe, not a fallback.**
32
+ The Stage-2 sheet rule bakes every meter's EMPTY dark in-style track into the
33
+ frame art, so the empty state is the art itself; the fill is a near-opaque
34
+ gradient revealed over that track, and the mask confines it to the channel
35
+ pixels so the rim, bezels, and any baked labels stay untouched. (Source order
36
+ does the stacking: frame `<img>` first, mask div after no z-index needed.)
37
+ Painting the fill UNDER the frame is the exception, taken only when the frame
38
+ has a genuinely transparent channel cavity AND a reason to use it e.g. a
39
+ real-translucency glass channel (`genex image --glass` art) the fill should
40
+ glow through, or ornament overhangs that must paint over the moving fill.
41
+ Never stack a plate/track div behind an opaque frame to compensate for a
42
+ white-painted trough — a white or stained trough is a sheet defect
43
+ (`ui audit` flags it); repair the SHEET (region-edit the trough to its empty
44
+ dark track, re-clean, re-extract), never patch it at composite time.
39
45
 
40
46
  ## The reveal formulas
41
47
 
@@ -146,12 +152,12 @@ pair; the run's index `annotated-progress.json` nests the same objects in a
146
152
 
147
153
  ```html
148
154
  <div class="widget" id="hud-hp">
155
+ <img class="frame" src="/assets/hud/hp-frame.png" alt=""> <!-- frame + baked empty track -->
149
156
  <div class="fill-mask" style="-webkit-mask-image: url(/assets/hud/hp-mask.png); mask-image: url(/assets/hud/hp-mask.png);">
150
157
  <div class="fill" data-fill data-fill-mask="hp-mask.png"
151
158
  data-fill-box="0.0784,0.3469,0.9216,0.3605"
152
159
  data-fill-from="left" data-fill-ratio="0.72"></div>
153
160
  </div>
154
- <img class="frame" src="/assets/hud/hp-frame.png" alt="">
155
161
  </div>
156
162
  ```
157
163