@genex-ai/cli-demo 0.80.2-dev.213 → 0.84.0-dev.215

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 (25) hide show
  1. package/dist/index.js +801 -83
  2. package/package.json +1 -1
  3. package/templates/controllers/character/first-person.ts +54 -0
  4. package/templates/controllers/character/follow-camera.ts +35 -1
  5. package/templates/skills/genex-ai-character/SKILL.md +25 -5
  6. package/templates/skills/genex-ai-hud/SKILL.md +131 -36
  7. package/templates/skills/genex-ai-hud/references/masked-fill.md +19 -13
  8. package/templates/skills/genex-ai-hud/references/stage2-prompt-template.md +2 -2
  9. package/templates/skills/genex-ai-image/SKILL.md +40 -2
  10. package/templates/skills/genex-ai-menu/SKILL.md +35 -17
  11. package/templates/skills/genex-ai-model/SKILL.md +8 -0
  12. package/templates/skills/genex-ai-music/SKILL.md +142 -0
  13. package/templates/skills/genex-ai-skybox/SKILL.md +21 -5
  14. package/templates/skills/genex-ai-video/SKILL.md +13 -7
  15. package/templates/skills/genex-ai-voice/SKILL.md +151 -0
  16. package/templates/skills/genex-game-director/SKILL.md +128 -44
  17. package/templates/skills/genex-game-director/references/design-contract.md +7 -2
  18. package/templates/skills/genex-game-director/references/routing-map.md +4 -1
  19. package/templates/skills/genex-threejs-camera-direction/SKILL.md +4 -1
  20. package/templates/skills/genex-threejs-character-controller/SKILL.md +6 -1
  21. package/templates/skills/genex-threejs-character-controller/references/wiring.md +13 -2
  22. package/templates/skills/genex-threejs-creatures/SKILL.md +193 -0
  23. package/templates/skills/genex-threejs-game-ui/SKILL.md +5 -1
  24. package/templates/skills/genex-threejs-multiplayer/SKILL.md +6 -1
  25. package/templates/skills/genex-threejs-touch-controls/SKILL.md +7 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.80.2-dev.213",
3
+ "version": "0.84.0-dev.215",
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.
@@ -314,13 +327,43 @@ To fix ONE sprite, don't re-run the pipeline:
314
327
  or region-`--edit` the sheet and re-extract. Re-running Stage 2 re-rolls
315
328
  every OTHER sprite too — the most expensive way to fix one.
316
329
 
330
+ ## The sheet repair lane — model edits, never pixel hacks
331
+
332
+ When a sheet defect surfaces AFTER generation — a white-painted trough
333
+ (`ui audit`'s hard `white-trough` finding, or `ui masks`' refusal), a wrong
334
+ ornament, a mis-drawn cell — the fix is a MODEL edit of the sheet, then
335
+ re-clean + re-extract. Never patch shipped pixels by hand (threshold-punching
336
+ a trough chews the frame's bezel — a tried-and-rejected workaround; the one
337
+ allowed pixel surgery is extraction's own known-matte rim defringe):
338
+
339
+ ```bash
340
+ # Whole-sheet repair (all bar cells at once — proven to preserve bezels,
341
+ # ornaments AND the green annotated twins):
342
+ 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>
343
+
344
+ # Region repair (one cell, others must not re-roll): add a mask whose
345
+ # TRANSPARENT hole covers the zone to change — assemble it from the channel
346
+ # mask + the cell's crop rect, or a hand-drawn rectangle:
347
+ npx genex image "<same repaint ask>" --edit <sheet-url> --inpaint <mask.png>
348
+ ```
349
+
350
+ **`--inpaint` honesty — a targeting scope, not a scalpel.** The mask focuses
351
+ WHERE the change happens (proven: the masked cell's trough repainted while the
352
+ other bar's design survived untouched) — but the WHOLE sheet still re-renders
353
+ and any transparency is destroyed either way. So after ANY repair edit,
354
+ mandatory: re-run `--clean` on the result, re-extract, and re-derive masks —
355
+ the old crops/masks describe the pre-edit sheet.
356
+
317
357
  ## Composition rules (each one is a documented failure class)
318
358
 
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.
359
+ - **Continuous fills = fill-on-top masked reveal.** HP, mana, fuel, XP,
360
+ stamina, charge — NEVER generate `*-fill.png` sprites, and NEVER use a
361
+ naive `width`/`height` percentage of the widget box (it drifts ~12% off the
362
+ art's real channel: empty at 25%, full by 75%). THE bar recipe: the frame
363
+ sprite carries its EMPTY dark track baked at source (Stage-2 track rule),
364
+ and a near-opaque gradient fill paints ON TOP of it, clipped to `FB` inside
365
+ the channel mask — the masked-fill DOM pattern below. Never a plate/track
366
+ div stacked behind the frame to fake an empty state.
324
367
  - **Minimap/radar interiors are an empty CSS disc** (`border-radius: 50%`,
325
368
  dark background). Only the decorative ring frame is a sprite, prompted with
326
369
  an explicitly **transparent center — no map, no terrain inside**. Gameplay
@@ -389,12 +432,12 @@ form:
389
432
 
390
433
  ```html
391
434
  <div class="widget" id="hud-hp"> <!-- root: overflow VISIBLE (glows may bleed) -->
392
- <div class="hp-mask"> <!-- outer: the silhouette mask -->
435
+ <img class="hp-frame" src="/assets/hud/hp-frame.png" alt=""> <!-- frame + baked empty track -->
436
+ <div class="hp-mask"> <!-- outer: the silhouette mask, ON TOP of the frame -->
393
437
  <div class="hp-fill" data-fill data-fill-mask="hp-mask.png"
394
438
  data-fill-box="0.0784,0.3469,0.9216,0.3605"
395
439
  data-fill-from="left" data-fill-ratio="0.72"></div>
396
440
  </div>
397
- <img class="hp-frame" src="/assets/hud/hp-frame.png" alt="">
398
441
  </div>
399
442
  ```
400
443
 
@@ -408,7 +451,7 @@ form:
408
451
  .hp-fill { position: absolute; inset: 0; /* FULL-BOX gradient; the clip places the edge */
409
452
  background: linear-gradient(180deg, #ef5b5b, #7a1d18); }
410
453
  .hp-frame { position: absolute; inset: 0; width: 100%; height: 100%;
411
- object-fit: fill; } /* frame paints on top */
454
+ object-fit: fill; } /* under the fill: its baked track IS empty */
412
455
  ```
413
456
 
414
457
  ```ts
@@ -426,17 +469,19 @@ function setHp(hp: number, maxHp: number): void {
426
469
  }
427
470
  ```
428
471
 
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
472
+ The structure, in one breath: the frame `<img>` (with its baked EMPTY track)
473
+ paints first with `object-fit: fill`; the OUTER div after it carries the alpha
474
+ mask (`mask-image` + `mask-size: 100% 100%`, `overflow: hidden`) so the fill
475
+ can only exist inside the art's channel; the INNER div is a FULL-BOX gradient
432
476
  whose `clip-path` places the leading edge at the **channel-relative** level
433
477
  using `FB` — for a left fill the right edge sits at `(FB.x + ratio * FB.w) * 100%`,
434
478
  for a bottom fill the top edge at `(FB.y + (1 - ratio) * FB.h) * 100%`. Stamp
435
479
  `data-fill`, `data-fill-mask`, `data-fill-box`, `data-fill-from`, and
436
480
  `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.
481
+ the mask JSON. The widget root keeps `overflow: visible` so glow effects bleed
482
+ past the art instead of clipping to a hard square. (Fill UNDER the frame is
483
+ the exception, for genuinely transparent channel cavities with a reason
484
+ [references/masked-fill.md](references/masked-fill.md) has the rule.)
440
485
 
441
486
  ## Reactivity — the juice floor
442
487
 
@@ -512,7 +557,10 @@ out of budget to check, say the HUD is unverified — never call it done.
512
557
  with corrected name order rather than regenerating anything.
513
558
  - **Fills grow monotonically** — drive each fill through 0 → 50 → 100 and
514
559
  confirm the reveal grows and stays inside the art's channel (never bulging
515
- past the frame's track).
560
+ past the frame's track). A screenshot at full HP proves nothing about a
561
+ meter — the classic shipped defect (a painted-full trough behind the mask)
562
+ is invisible at 100%. This is why the milestone smoke pass's gameplay
563
+ capture is taken AFTER taking damage once.
516
564
  - **No text clips** — for every text node, `scrollWidth <= clientWidth`.
517
565
  Display fonts run 25–50% wider than a naive estimate; widen the box or drop
518
566
  the weight, don't shrink the font.
@@ -597,9 +645,14 @@ after a bad single costs the whole chain).
597
645
  re-roll); `--edit <url>` image-to-image edit of an R2 URL; `--clean <url>`
598
646
  background removal only; `--remove-bg` chains removal after a
599
647
  generation/edit; `--bg-mode <sprite|glyph|sheet>` picks the removal model
600
- (`glyph` for digits/closed shapes; `--clean` defaults to `sheet`);
648
+ (`glyph` for digits/closed shapes; `--clean` defaults to `sheet`; `matte`
649
+ = BiRefNet soft alpha for hair/glow/smoke edges the binary cutters butcher);
601
650
  `--no-wait` enqueue and continue — `npx genex wait <id>` picks the result
602
- up later (safe to re-run; never creates a new generation).
651
+ up later (safe to re-run; never creates a new generation);
652
+ `--inpaint <mask.png|url>` region mask for `--edit` (transparent hole =
653
+ the zone to change — see the repair lane's honesty note);
654
+ `--upscale <url>` 2x utility upscale of an existing asset;
655
+ `--glass` the magenta-key glass lane (`$genex-ai-image`).
603
656
  - `npx genex ui extract --in <png|url> --out-dir <dir> --names a,b,c` — also
604
657
  writes a `.bbox.json` sidecar per sprite (with `innerBBox` for text
605
658
  placement); `--expect <n>` hard-errors on a component miscount before
@@ -614,9 +667,17 @@ after a bad single costs the whole chain).
614
667
  eyedropper: background/plate color + dark/light/chromatic ink candidates.
615
668
  Use the returned hex verbatim for the DOM text over that region.
616
669
  - `npx genex ui trim --in <png>` — crop to alpha content + report real dims.
670
+ - `npx genex ui plate --in <frame.png> [--erode 2]` — trace the frame's real
671
+ interior (art + enclosed cavity) into `<name>-plate.png`; wire it as the
672
+ CSS plate's `mask-image` (the silhouette-plate rule above). Free, local.
617
673
  - `npx genex ui audit [--dir public/assets/hud] [--src src]` — the mechanical
618
- wiring scan (unreferenced sprites/masks, naive %-fill patterns); warn-only,
619
- `--strict` exits 1 on findings.
674
+ wiring scan (unreferenced sprites/masks, naive %-fill patterns, and a
675
+ missing/incomplete mobile viewport meta in the root `index.html`); warn-only
676
+ except the provable kinds (`mask-frame-mismatch`, `white-trough`), which
677
+ exit 1; `--strict` exits 1 on any findings.
678
+ - Extraction defringes chroma-keyed sprite rims by default (2px matte
679
+ un-blend against the sheet white — kills the halo over dark scenes; the
680
+ sidecar stamps `defringed`); `--no-defringe` opts out.
620
681
 
621
682
  ## Troubleshooting
622
683
 
@@ -636,15 +697,49 @@ after a bad single costs the whole chain).
636
697
  - **The fill drifts off the art's channel** — a naive `width`/`height` % crept
637
698
  in, or `FB` doesn't match the mask. Use the clipPath formula with the
638
699
  `fillBox` from the masks JSON, verbatim.
639
- - **The fill never shows, at any ratio** — the frame PNG's channel interior is
640
- opaque track art occluding the fill beneath it. Raise the mask div above the
641
- frame (`z-index`); the mask keeps the fill inside the channel, so the rim
642
- and baked labels are unaffected (see the z-order note in
643
- [references/masked-fill.md](references/masked-fill.md)).
700
+ - **The fill never shows, at any ratio** — the fill is painting UNDER the
701
+ frame while the frame's channel is opaque track art. The recipe puts the
702
+ mask div AFTER the frame `<img>` in source order (fill on top); check the
703
+ DOM order first. The mask keeps the fill inside the channel, so the rim and
704
+ baked labels are unaffected (see [references/masked-fill.md](references/masked-fill.md)).
644
705
  - **`genex ui masks` fails registration/coverage checks** — the annotated
645
706
  duplicate drifted from the clean cell (different scale/position) or the
646
707
  green isn't flat `#00ff00`. Region-edit the sheet (`--edit <sheet-url>`)
647
708
  asking for a same-size duplicate with flat green fill zones, then re-run.
709
+ Do NOT loosen the tolerance to force a pair through: a mask registered off
710
+ its frame ships a fill that floats off the art, and loosened runs are
711
+ stamped in the metadata (preview/publish will warn about them).
712
+ - **`ui masks` refuses: "the fill channel is painted filled in the frame
713
+ art"** — the sheet model ignored "REMOVE the fill" and left the bar painted
714
+ full (usually near-white). The defect is invisible at 100% and shows the
715
+ moment the value drops. Region-edit that cell so the trough is EMPTY
716
+ (dark/recessed, no fill), then re-run — never wire that frame as-is.
717
+ - **`ui audit` errors with `white-trough`** — a shipped frame's fill channel
718
+ measures opaque near-sheet-white: the model drew the trough white/filled and
719
+ the cleaner can't reach enclosed regions. Under-fills silently never show
720
+ behind it. Run the sheet repair lane (region-edit the trough to its EMPTY
721
+ dark track, `--inpaint` for one cell), then re-clean + re-extract — never
722
+ ship the frame, never pixel-punch the white out.
723
+ - **After any `--edit`/`--inpaint` repair the sprites look stale/wrong** —
724
+ the whole sheet re-rendered (that's the edit contract): the OLD extractions
725
+ and masks describe the pre-edit sheet. Re-run `--clean`, re-extract, and
726
+ re-derive masks against the new sheet URL.
727
+ - **Wisps, glows, smoke, hair get chopped by `--remove-bg`** — the binary
728
+ cutters can't do soft edges. Recut with `--bg-mode matte` (BiRefNet soft
729
+ alpha).
730
+ - **`ui masks`/`ui extract` refuses: "the crop catches another sheet
731
+ element"** — elements sit too close on the sheet, so the crop would bake a
732
+ sliver of a neighbor into this asset. Regenerate the sheet with more
733
+ spacing between elements (or tighten the crop / lower `--padding`).
734
+ - **"the crop box is slicing the element" (flush edge)** — widen the crop box
735
+ if the sheet has room; otherwise the sheet ran out of margin here —
736
+ region-edit or regenerate it with more margin around the element. There is
737
+ no tolerance to bump: a sliced frame ships as a cut-off corner.
738
+ - **`ui audit` errors with `mask-frame-mismatch`** — the CSS ships a frame
739
+ PNG cut from DIFFERENT sheet geometry than the crop the mask was derived
740
+ against, so the fill floats off the frame. If you swap which frame variant
741
+ ships, re-run `ui masks` against it (or ship the mask's own `-frame.png`) —
742
+ a frame swap is never cosmetic once a mask is involved.
648
743
  - **`masks --auto` reports ambiguity or a missing twin** — it never guesses:
649
744
  fall back to explicit `--pairs` with hand-read crop rects for just that
650
745
  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
 
@@ -37,7 +37,7 @@ Lay these assets on a pure flat white #ffffff background, separated by at least
37
37
  [ASSET_LIST]
38
38
 
39
39
  If an asset is a generated progress/bar/meter frame, output TWO CELLS immediately beside each other:
40
- 1. LEFT CELL — the clean production asset the game will use. Preserve exact style, baked_static labels, frame metal, bevels, dividers, scratches, proportions, and texture. Remove runtime dynamic fill and values. Do not paint technical colors on this clean cell.
40
+ 1. LEFT CELL — the clean production asset the game will use. Preserve exact style, baked_static labels, frame metal, bevels, dividers, scratches, proportions, and texture. Remove runtime dynamic fill and values. Every meter/bar cell shows the meter EMPTY: the channel interior is painted as the meter's dark empty track in the art's own style — recessed, unlit, like a drained lamp-oil groove or a powered-down light strip. NEVER white, NEVER a filled bar, NEVER the sheet background color inside a frame. Do not paint technical colors on this clean cell.
41
41
  2. RIGHT CELL — an annotated duplicate of the same production asset. It must be the same asset, same scale, same canvas bounds, same position, and same outer shape as the left cell. The only difference: paint runtime-fill zones with pure green key color #00ff00. Green marks only the places where runtime bar/progress fill should appear. Paint the green as a FLAT, UNIFORM, fully-saturated #00ff00 fill — no shading, no gradient, no inner shadow, no vignette, NO DARKENING TOWARD THE EDGES, no lighting, no highlights, no texture; constant color edge-to-edge with a HARD, CRISP boundary against the frame. The green is a chroma-key, not a lit surface — any edge shading makes the extracted alpha mask ragged. For segmented meters, paint each segment slot as a separate green shape; do not merge slots into one continuous strip and do not paint half-segments. Never place a label or ornament in the middle of a fill channel — labels belong above/beside the channel or on the frame's end caps.
42
42
 
43
43
  Do NOT create a separate black-background mask cell. Do NOT create a standalone simplified silhouette. The right cell is a registration-safe annotation map; local tooling derives the alpha mask from its green pixels and uses the left cell as the frame.
@@ -55,7 +55,7 @@ KEEP these baked into the asset — they are part of the panel's IDENTITY and ne
55
55
 
56
56
  REMOVE runtime-owned content:
57
57
  - Current numeric values (the "1247" coins, the "47" ammo count)
58
- - Current bar fill levels (the colored fill INSIDE a bar — runtime code reveals it through a mask)
58
+ - Current bar fill levels (the colored fill INSIDE a bar — runtime code paints the fill; the emptied channel shows the meter's own dark empty track, never white, never the sheet background)
59
59
  - Current item icons inside slots (the sword, potion — runtime renders the equipped item)
60
60
  - Current map content / radar blips (the game paints these at runtime)
61
61
  - Any editable or localizable label the design does not fuse into the frame, even if it does not change frame-to-frame
@@ -141,6 +141,32 @@ use the quad fallback or a hit-spark VFX instead. Impact marks are a first-class
141
141
  generated surface for any weapon/collision game — inventory them up front with
142
142
  the rest of your art, don't discover the bare walls at the end.
143
143
 
144
+ ## Real-translucency glass panels — `--glass`
145
+
146
+ A sprite cut from an unknown background can never carry semi-transparency —
147
+ but a panel generated on a CONTROLLED backdrop can. `--glass` appends a
148
+ flat-magenta key-screen suffix to your prompt, then (after generation) solves
149
+ the per-pixel contamination locally into REAL fractional alpha and writes
150
+ ready RGBA files (`glass-1.png`, one per candidate, into `--out-dir`,
151
+ default `.`) plus the key metrics (key color, strength, flatness):
152
+
153
+ ```bash
154
+ npx genex image "frosted translucent dark glass panel, about 40 percent opacity, thin luminous cyan frame" --glass --size 1280x832 --quality high
155
+ ```
156
+
157
+ - **Steer the translucency with words**: "frosted translucent … about 40
158
+ percent opacity" → real see-through glass; "dark smoked glass" → near-
159
+ opaque tint. The suffix handles the backdrop; your prompt owns the glass.
160
+ - **Wire the PNG like any sprite** — its alpha IS the glass. No CSS opacity
161
+ on top, no plate underneath.
162
+ - **Not for magenta/pink-hued art**: magenta in the SUBJECT keys as
163
+ translucency. Panels, frames, HUD plates — yes; a pink neon sign — no.
164
+ - A candidate whose backdrop came back shaded is REJECTED with a retryable
165
+ message (rare — regenerate). `--glass` is generation-only: it conflicts
166
+ with `--edit`/`--clean`/`--upscale`/`--transparent`/`--remove-bg`/`--no-wait`.
167
+ - Glass files are LOCAL outputs (like HUD sprites) — they ship with the
168
+ build; commit them under `public/assets/`.
169
+
144
170
  ## Multiplayer
145
171
 
146
172
  The asset URL is public, permanent, and CORS-open, so it is safe to broadcast the
@@ -181,10 +207,22 @@ See `$genex-threejs-multiplayer` for the `shared` channel rules and the room API
181
207
  - `--quality <low|medium|high>` — quality preset; higher costs more and takes longer.
182
208
  - `--size <WxH>` — exact pixel size (multiples of 16, each side ≤ 3840, aspect ≤ 3:1).
183
209
  - `--edit <url>` — edit THAT generated image with the prompt (image-to-image).
210
+ - `--inpaint <mask.png|url>` — with `--edit`: a region mask (local PNG or
211
+ asset URL; TRANSPARENT hole = the zone to change). It targets WHERE the
212
+ edit lands — the other regions keep their designs — but the WHOLE image
213
+ still re-renders and alpha is destroyed either way: after any masked edit,
214
+ re-run `--clean`/re-extraction downstream. A targeting scope, not a pixel
215
+ freeze.
184
216
  - `--clean <url>` — ML background removal of THAT image only (prompt recorded, unused).
217
+ - `--upscale <url>` — 2x utility upscale of THAT image (prompt recorded,
218
+ unused; ~2-credit class). Use before printing/large billboards, not by
219
+ default.
185
220
  - `--remove-bg` — chain ML background removal after the generation/edit.
186
- - `--bg-mode <sprite|glyph|sheet>` — background-removal model (`glyph` for
187
- digits/closed shapes; `--clean` defaults to `sheet`).
221
+ - `--bg-mode <sprite|glyph|sheet|matte>` — background-removal model (`glyph`
222
+ for digits/closed shapes; `--clean` defaults to `sheet`; `matte` = BiRefNet
223
+ SOFT alpha for hair/glow/smoke edges the binary cutters butcher).
224
+ - `--glass` — the magenta-key real-translucency lane (section above);
225
+ writes local RGBA files into `--out-dir` (default `.`).
188
226
  - `--no-wait` — enqueue and return immediately, without the URL. Fire-and-forget
189
227
  only: re-running the command creates (and bills) a NEW image.
190
228
  - `--api-url <url>` — override the API base (local dev).