@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.
- package/README.md +9 -7
- package/dist/index.js +2623 -1684
- 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 +175 -63
- package/templates/skills/genex-ai-hud/references/masked-fill.md +19 -13
- package/templates/skills/genex-ai-hud/references/stage1-prompt-template.md +42 -5
- package/templates/skills/genex-ai-hud/references/stage2-prompt-template.md +7 -3
- package/templates/skills/genex-ai-image/SKILL.md +40 -2
- package/templates/skills/genex-ai-menu/SKILL.md +47 -22
- 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 +183 -58
- package/templates/skills/genex-game-director/references/design-contract.md +40 -10
- package/templates/skills/genex-game-director/references/routing-map.md +44 -29
- package/templates/skills/genex-threejs-camera-direction/SKILL.md +4 -1
- package/templates/skills/genex-threejs-character-controller/SKILL.md +25 -1
- package/templates/skills/genex-threejs-character-controller/references/wiring.md +13 -2
- package/templates/skills/genex-threejs-creatures/SKILL.md +201 -0
- package/templates/skills/genex-threejs-game-ui/SKILL.md +79 -71
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +6 -1
- 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.
|
|
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
|
|
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.
|
|
@@ -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
|
|
216
|
-
serves as the user's style
|
|
217
|
-
(`$genex-threejs-game-ui` owns the
|
|
218
|
-
|
|
219
|
-
|
|
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,
|
|
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.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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.
|
|
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
|
|
250
|
-
# concept/reference image so the HUD inherits its exact style.
|
|
251
|
-
#
|
|
252
|
-
npx genex image "<filled stage-1 prompt>" --size 2560x1440 --quality high
|
|
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,
|
|
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
|
-
|
|
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
|
-
<
|
|
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; } /*
|
|
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
|
|
430
|
-
|
|
431
|
-
|
|
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
|
|
438
|
-
|
|
439
|
-
|
|
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
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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 (
|
|
597
|
-
|
|
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
|
-
|
|
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
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
├─
|
|
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
|
|