@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.
- package/dist/index.js +801 -83
- package/package.json +1 -1
- package/templates/controllers/character/first-person.ts +54 -0
- package/templates/controllers/character/follow-camera.ts +35 -1
- package/templates/skills/genex-ai-character/SKILL.md +25 -5
- package/templates/skills/genex-ai-hud/SKILL.md +131 -36
- package/templates/skills/genex-ai-hud/references/masked-fill.md +19 -13
- package/templates/skills/genex-ai-hud/references/stage2-prompt-template.md +2 -2
- package/templates/skills/genex-ai-image/SKILL.md +40 -2
- package/templates/skills/genex-ai-menu/SKILL.md +35 -17
- package/templates/skills/genex-ai-model/SKILL.md +8 -0
- package/templates/skills/genex-ai-music/SKILL.md +142 -0
- package/templates/skills/genex-ai-skybox/SKILL.md +21 -5
- package/templates/skills/genex-ai-video/SKILL.md +13 -7
- package/templates/skills/genex-ai-voice/SKILL.md +151 -0
- package/templates/skills/genex-game-director/SKILL.md +128 -44
- package/templates/skills/genex-game-director/references/design-contract.md +7 -2
- package/templates/skills/genex-game-director/references/routing-map.md +4 -1
- package/templates/skills/genex-threejs-camera-direction/SKILL.md +4 -1
- package/templates/skills/genex-threejs-character-controller/SKILL.md +6 -1
- package/templates/skills/genex-threejs-character-controller/references/wiring.md +13 -2
- package/templates/skills/genex-threejs-creatures/SKILL.md +193 -0
- package/templates/skills/genex-threejs-game-ui/SKILL.md +5 -1
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +6 -1
- 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.
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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,
|
|
320
|
-
NEVER generate `*-fill.png` sprites, and NEVER use a
|
|
321
|
-
percentage of the widget box (it drifts ~12% off the
|
|
322
|
-
empty at 25%, full by 75%).
|
|
323
|
-
|
|
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
|
-
<
|
|
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; } /*
|
|
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
|
|
430
|
-
|
|
431
|
-
|
|
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
|
|
438
|
-
|
|
439
|
-
|
|
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
|
|
619
|
-
|
|
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
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
├─
|
|
22
|
-
│
|
|
23
|
-
└─
|
|
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
|
-
**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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`
|
|
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).
|