@genex-ai/cli-demo 0.77.1-dev.199 → 0.78.1-dev.203

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 CHANGED
@@ -3225,7 +3225,7 @@ async function deployGame(ctx, opts, log) {
3225
3225
  return false;
3226
3226
  }
3227
3227
  log.step(`Uploading ${files.length} files\u2026`);
3228
- if (!await uploadAll(grant.uploadUrl, grant.token, files, log)) {
3228
+ if (!await uploadAll(grant, files, log)) {
3229
3229
  log.error("Couldn't upload your game \u2014 please try again.");
3230
3230
  return false;
3231
3231
  }
@@ -3306,21 +3306,24 @@ async function getUploadToken(ctx, commit, log) {
3306
3306
  }
3307
3307
  return await res.json();
3308
3308
  }
3309
- async function uploadAll(uploadUrl, uploadToken, files, log) {
3310
- const base = uploadUrl.replace(/\/+$/, "");
3309
+ async function uploadAll(grant, files, log) {
3310
+ const base = grant.uploadUrl.replace(/\/+$/, "");
3311
+ const threshold = grant.limits.singlePutMaxBytes ?? grant.limits.maxFileBytes;
3312
+ const small = files.filter((f) => f.bytes.length <= threshold);
3313
+ const large = files.filter((f) => f.bytes.length > threshold);
3311
3314
  let failed = false;
3312
3315
  let next = 0;
3313
3316
  const worker = async () => {
3314
3317
  while (!failed) {
3315
3318
  const i = next++;
3316
- if (i >= files.length) return;
3317
- const f = files[i];
3319
+ if (i >= small.length) return;
3320
+ const f = small[i];
3318
3321
  const encoded = f.relPath.split("/").map(encodeURIComponent).join("/");
3319
3322
  let res = null;
3320
3323
  try {
3321
3324
  res = await fetch(`${base}/${encoded}`, {
3322
3325
  method: "PUT",
3323
- headers: { Authorization: `Bearer ${uploadToken}` },
3326
+ headers: { Authorization: `Bearer ${grant.token}` },
3324
3327
  body: f.bytes
3325
3328
  });
3326
3329
  } catch {
@@ -3332,8 +3335,97 @@ async function uploadAll(uploadUrl, uploadToken, files, log) {
3332
3335
  }
3333
3336
  }
3334
3337
  };
3335
- await Promise.all(Array.from({ length: Math.min(8, files.length) }, () => worker()));
3336
- return !failed;
3338
+ await Promise.all(Array.from({ length: Math.min(8, small.length) }, () => worker()));
3339
+ if (failed) return false;
3340
+ for (const f of large) {
3341
+ if (!await uploadMultipart(base, grant.token, f, grant.limits.partSizeBytes, log)) return false;
3342
+ }
3343
+ return true;
3344
+ }
3345
+ async function uploadMultipart(base, uploadToken, f, partSize, log) {
3346
+ const encoded = f.relPath.split("/").map(encodeURIComponent).join("/");
3347
+ const auth = { Authorization: `Bearer ${uploadToken}` };
3348
+ const totalParts = Math.ceil(f.bytes.length / partSize);
3349
+ log.step(`Uploading ${f.relPath} (${(f.bytes.length / 1048576).toFixed(1)} MB in ${totalParts} parts)\u2026`);
3350
+ let uploadId;
3351
+ try {
3352
+ const res = await fetch(`${base}/${encoded}?action=mpu-create`, { method: "POST", headers: auth });
3353
+ if (!res.ok) {
3354
+ log.warn(`Upload failed for ${f.relPath} (HTTP ${res.status} starting multipart).`);
3355
+ return false;
3356
+ }
3357
+ uploadId = (await res.json()).uploadId;
3358
+ } catch {
3359
+ log.warn(`Upload failed for ${f.relPath} (network error starting multipart).`);
3360
+ return false;
3361
+ }
3362
+ const abort = async () => {
3363
+ try {
3364
+ await fetch(`${base}/${encoded}?action=mpu-abort&uploadId=${encodeURIComponent(uploadId)}`, {
3365
+ method: "DELETE",
3366
+ headers: auth
3367
+ });
3368
+ } catch {
3369
+ }
3370
+ };
3371
+ const parts = [];
3372
+ let failed = false;
3373
+ let next = 0;
3374
+ const partWorker = async () => {
3375
+ while (!failed) {
3376
+ const i = next++;
3377
+ if (i >= totalParts) return;
3378
+ const partNumber = i + 1;
3379
+ const bytes = f.bytes.subarray(i * partSize, Math.min((i + 1) * partSize, f.bytes.length));
3380
+ const part = await uploadPartWithRetry(base, encoded, auth, uploadId, partNumber, bytes);
3381
+ if (!part) {
3382
+ failed = true;
3383
+ log.warn(`Upload failed for ${f.relPath} (part ${partNumber}/${totalParts}).`);
3384
+ return;
3385
+ }
3386
+ parts.push(part);
3387
+ }
3388
+ };
3389
+ await Promise.all(Array.from({ length: Math.min(4, totalParts) }, () => partWorker()));
3390
+ if (failed) {
3391
+ await abort();
3392
+ return false;
3393
+ }
3394
+ parts.sort((a, b) => a.partNumber - b.partNumber);
3395
+ try {
3396
+ const res = await fetch(`${base}/${encoded}?action=mpu-complete&uploadId=${encodeURIComponent(uploadId)}`, {
3397
+ method: "POST",
3398
+ headers: { ...auth, "Content-Type": "application/json" },
3399
+ body: JSON.stringify({ parts })
3400
+ });
3401
+ if (!res.ok) {
3402
+ log.warn(`Upload failed for ${f.relPath} (HTTP ${res.status} completing multipart).`);
3403
+ await abort();
3404
+ return false;
3405
+ }
3406
+ } catch {
3407
+ log.warn(`Upload failed for ${f.relPath} (network error completing multipart).`);
3408
+ await abort();
3409
+ return false;
3410
+ }
3411
+ return true;
3412
+ }
3413
+ async function uploadPartWithRetry(base, encoded, auth, uploadId, partNumber, bytes) {
3414
+ for (let attempt = 0; ; attempt++) {
3415
+ let res = null;
3416
+ try {
3417
+ res = await fetch(
3418
+ `${base}/${encoded}?action=mpu-part&uploadId=${encodeURIComponent(uploadId)}&partNumber=${partNumber}`,
3419
+ { method: "PUT", headers: auth, body: bytes }
3420
+ );
3421
+ if (res.ok) return await res.json();
3422
+ } catch {
3423
+ res = null;
3424
+ }
3425
+ if (res && res.status < 500) return null;
3426
+ if (attempt >= 2) return null;
3427
+ await new Promise((resolve) => setTimeout(resolve, attempt === 0 ? 500 : 2e3));
3428
+ }
3337
3429
  }
3338
3430
  async function callPublish(ctx, commit, opts, log) {
3339
3431
  let res;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.77.1-dev.199",
3
+ "version": "0.78.1-dev.203",
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": {
@@ -80,26 +80,31 @@ export async function loadTextureWithFallback<T>(
80
80
  // Generated MODELS (mesh-compression lane). GLB rungs use the same numeric
81
81
  // suffix — `model-glb@1024` = embedded textures ≤1024 + meshopt (+simplify) —
82
82
  // but unlike images there is NO edge rewrite for old games: selection happens
83
- // ONLY here, in games that wired the decoders (createGltfLoader). Desktop
84
- // always loads the original.
83
+ // ONLY here, in games that wired the decoders (createGltfLoader). EVERY tier
84
+ // loads a game-ready rung — provider-raw originals are archival/remix source,
85
+ // not game assets (a Tripo prop is ~500k tris + 3x4096² textures; a scene of
86
+ // them floored an M4 Max). Desktop gets @2048, phones @1024; the original is
87
+ // only ever the fallback of last resort.
85
88
 
86
- const MODEL_TEXTURE_BUDGET = 1024;
89
+ /** Per-tier texture budget: desktop tiers @2048, phone tiers @1024. */
90
+ function modelBudgetFor(tier: QualityTier): number {
91
+ return tier.name === "phone" || tier.name === "phone-low" ? 1024 : 2048;
92
+ }
87
93
  const MODEL_ROLE_RE = /^(model-glb|character-rigged(-a\d+)?-glb(-r\d+)?)$/;
88
94
 
89
95
  /** Resolve the model URL a THIS-tier device should load. `ktx2: true` (from
90
- * createGltfLoader) upgrades phones to the GPU-compressed sibling. */
96
+ * createGltfLoader) upgrades to the GPU-compressed sibling. */
91
97
  export function pickModel(url: string, tier: QualityTier, opts?: { ktx2?: boolean }): string {
92
98
  if (!GENEX_GENERATIONS_RE.test(url)) return url;
93
99
  const role = url.split("/").pop() ?? "";
94
100
  if (!MODEL_ROLE_RE.test(role)) return url;
95
- if (tier.name !== "phone" && tier.name !== "phone-low") return url;
96
- const rung = `${url}@${MODEL_TEXTURE_BUDGET}`;
101
+ const rung = `${url}@${modelBudgetFor(tier)}`;
97
102
  return opts?.ktx2 ? `${rung}.ktx2` : rung;
98
103
  }
99
104
 
100
105
  /**
101
106
  * Load a generated model through the rung ladder with the full fallback chain
102
- * (`.ktx2` → `@1024` → original). Use with the decoder-wired loader:
107
+ * (`.ktx2` → the tier's rung → original). Use with the decoder-wired loader:
103
108
  *
104
109
  * const gltf = createGltfLoader(renderer);
105
110
  * const model = await loadModelWithFallback(MODEL_URL, tier, (u) => gltf.loader.loadAsync(u), { ktx2: gltf.ktx2 });
@@ -35,6 +35,13 @@ ring) that no hand-written CSS can fake.
35
35
  (`background: rgba(<darkest hue>, 0.35–0.6)` + `backdrop-filter: blur(6–12px)`
36
36
  and its `-webkit-` twin) when the style wants translucency, a solid painted
37
37
  plate, a subtle gradient — or no plate at all for an outline-led look.
38
+ **A plate exists only where the widget's own art doesn't already back it.**
39
+ Behind an OPAQUE frame sprite there is no plate — the frame IS the backing
40
+ surface, and a dark div stacked behind it only protrudes as a box over the
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.
38
45
  **Glass is a technique, not a default** — do not reach for translucency
39
46
  because this skill mentions it; reach for it when THIS game's brief does.
40
47
  What is fixed is WHERE plates are built — the DOM — because
@@ -44,7 +51,9 @@ ring) that no hand-written CSS can fake.
44
51
  corners with `border-radius` or a clean `clip-path`/`mask` shape — a CSS
45
52
  chamfer/notch is allowed, as long as the cut never clips the plate's
46
53
  content (text, padding, a glow), leaves no jagged-edge artifacts, and
47
- nothing ends up crooked; a genuinely ornamented angular frame is still
54
+ nothing ends up crooked — and a plain `border`/`box-shadow` won't follow a
55
+ `clip-path` cut, so use `$genex-threejs-game-ui`'s two-layer chamfer-border
56
+ recipe when a chamfer needs a frame; a genuinely ornamented angular frame is still
48
57
  best as chrome, generated. The load-bearing `mask`/`clip-path` in this
49
58
  skill is the masked-fill reveal below.
50
59
  - **Chrome (sprites — what THIS pipeline generates).** Opaque frames, corner
@@ -105,7 +114,10 @@ The HUD overlays a **live 3D scene**. The pixels between widgets show the game.
105
114
  burns generations.
106
115
  - **Every widget needs internal contrast** — a panel fill, outline stroke, or
107
116
  semi-opaque backdrop — because the scene behind it might be a snowfield or a
108
- torchlit dungeon. Pure-outline widgets over arbitrary scenes fail.
117
+ torchlit dungeon. Pure-outline widgets over arbitrary scenes fail. An opaque
118
+ frame sprite already IS that contrast; don't stack a plate behind it. A
119
+ needed backdrop lives inside the widget's silhouette, never as a rectangle
120
+ spilling past the art (see the plate rule under "three layers").
109
121
  - **The mockup must be flat and head-on** — a 2D screen-space overlay, never
110
122
  tilted, isometric, or in perspective. Angled panels have no clean silhouette
111
123
  and deconstruct into skewed slices. If the mockup comes back tilted,
@@ -203,13 +215,18 @@ and [references/stage2-prompt-template.md](references/stage2-prompt-template.md)
203
215
  gate — the Stage-1 mockup IS the game concept.** There is no separate
204
216
  UI-free concept image before it: this one generation carries scene + HUD,
205
217
  serves as the user's style checkpoint, and anchors all later art
206
- (`$genex-threejs-game-ui` owns the checkpoint choreography). The scene half
218
+ (`$genex-threejs-game-ui` owns the checkpoint choreography). **Stage 2 onward
219
+ is frame-anchored, so it waits for the user's keep/change YES on that
220
+ checkpoint — kick it off the instant they approve, not before.** The scene half
207
221
  of the prompt is written in TEXT from the game plan — `[GAME_SCENE]` in the
208
222
  Stage-1 template: setting, the moment, what the player is doing, lighting —
209
223
  never `--edit`-anchored to a prior image (there is none). Then write the
210
224
  widget layout and wiring code (placement, masked-fill scaffolding,
211
225
  plain-CSS placeholder bars) while it renders — the CSS HUD keeps
212
- the game playable until the sprites land. The mockup is a STYLE anchor
226
+ the game playable until the sprites land. When a sprite lands it REPLACES
227
+ its placeholder: delete the placeholder's own background/plate as you wire
228
+ the opaque frame in — a placeholder dark div left behind an opaque sprite
229
+ is the black-box defect. The mockup is a STYLE anchor
213
230
  only: the widget set and
214
231
  layout come from the element inventory and the game contract — a mechanic
215
232
  the mockup invented (a lap counter, a stamina orb) does not enter the HUD,
@@ -311,11 +328,13 @@ To fix ONE sprite, don't re-run the pipeline:
311
328
  painted ON TOP. Never put the gradient on the scaling fill itself — its `%`
312
329
  stops anchor to the fill's own width and the segments visibly compress as
313
330
  the value drops.
314
- - **Every numeric readout gets a backing plate** a
315
- `background: rgba(10, 8, 6, 0.6)` rounded div behind the text by default, an
331
+ - **A numeric readout sitting as bare text over the scene gets a backing
332
+ plate** — a `background: rgba(10, 8, 6, 0.6)` rounded div behind the text, an
316
333
  extracted plate sprite when the art has one, or a deliberate skip documented
317
334
  in a comment (`/* no plate — reads against <reason> */`). Bare digits look
318
- fine over the dev background and vanish over a bright scene.
335
+ fine over the dev background and vanish over a bright scene. A readout
336
+ already sitting on an opaque frame sprite needs NO separate plate — the frame
337
+ backs it; a second dark div only boxes it (the no-double-backing rule above).
319
338
 
320
339
  ## Discrete vs continuous — classify every meter before Stage 2
321
340
 
@@ -173,8 +173,10 @@ Pause both videos when the menu phase hides (they're decode work), and
173
173
  resume the front one when it returns. Watch ONE full cycle in the browser
174
174
  before calling the menu done — that's the seam check.
175
175
 
176
- **Work async — the menu must never block the game.** Generate the frame right
177
- after the UI plan gate, enqueue the video with `--no-wait`, ship the CSS menu
176
+ **Work async — the menu must never block the game.** The still is
177
+ `--edit`-anchored to the concept mockup, so it's style-dependent: generate it
178
+ the moment the user approves the concept (not before), enqueue the video with
179
+ `--no-wait`, ship the CSS menu
178
180
  (buttons + title over the frame IMAGE as a static backdrop), and swap the
179
181
  `<video>` in when `genex wait` prints the URL. The frame image doubles as the
180
182
  **loading screen background** — it exists minutes before the video does (see
@@ -67,11 +67,12 @@ side — reads as broken at a glance.
67
67
 
68
68
  Load through the quality kit's decoder-wired loader and the model rung ladder
69
69
  (`$genex-threejs-adaptive-quality`; `npx genex controller quality` installs
70
- it). The bare URL is the provider-raw original — multiple 1–4K PBR textures +
71
- dense geometry; phones load the `@1024` rung instead (textures budgeted to
72
- 1024, meshopt-compressed, simplified) and KTX2-capable games get the
73
- GPU-compressed sibling. Desktop always loads the original. R2 sends the right
74
- CORS headers, so cross-origin loading just works:
70
+ it). The bare URL is the provider-raw original — ~500k triangles + several 4K
71
+ PBR textures, archival/remix source, NOT a game asset (31 raw props once
72
+ floored an M4 Max). Every model ships a game-ready ladder instead: desktop
73
+ tiers load the `@2048` rung, phones the `@1024` rung (both meshopt-compressed
74
+ and simplified), and KTX2-capable games get the GPU-compressed sibling. R2
75
+ sends the right CORS headers, so cross-origin loading just works:
75
76
 
76
77
  ```ts
77
78
  import { createGltfLoader } from "./controllers/quality/gltf-loader.ts";
@@ -86,6 +87,16 @@ const gltf = await loadModelWithFallback(
86
87
  MODEL_URL, tier, (u) => gltfLoader.loader.loadAsync(u), { ktx2: gltfLoader.ktx2 },
87
88
  );
88
89
  const model = gltf.scene;
90
+ // Clamp the provider's mirror-metal PBR (metalness~1 reflects the whole sky
91
+ // env and swims with every camera move):
92
+ model.traverse((o) => {
93
+ const m = (o as THREE.Mesh).material as THREE.MeshStandardMaterial;
94
+ if (m?.isMeshStandardMaterial) {
95
+ m.metalness = Math.min(m.metalness, 0.6);
96
+ m.roughness = Math.max(m.roughness, 0.35);
97
+ m.envMapIntensity = 0.6;
98
+ }
99
+ });
89
100
  model.scale.setScalar(1); // tune to taste
90
101
  model.position.set(0, 0, 0);
91
102
  scene.add(model);
@@ -100,8 +111,42 @@ Never swallow a failed load silently: if you wrap a generated-asset load in a
100
111
  `catch`, `console.warn` the asset name in it — your own self-check reads the
101
112
  console, and a bare `.catch(() => null)` hides a missing model from you too.
102
113
 
103
- To place many copies, `model.clone()` per instance. For animated GLBs, drive
104
- `gltf.animations` with a `THREE.AnimationMixer`.
114
+ **Copies**: one or two, `model.clone()` (clones share geometry/textures on the
115
+ GPU). **Three or more of the same model → `InstancedMesh`** clones still cost
116
+ one draw call per mesh per copy; instancing renders all copies in one draw
117
+ call per source mesh:
118
+
119
+ ```ts
120
+ // placements: THREE.Matrix4[] — one world transform per copy
121
+ const group = new THREE.Group();
122
+ gltf.scene.updateMatrixWorld(true);
123
+ const tmp = new THREE.Matrix4();
124
+ gltf.scene.traverse((o) => {
125
+ const src = o as THREE.Mesh;
126
+ if (!src.isMesh) return;
127
+ const im = new THREE.InstancedMesh(src.geometry, src.material, placements.length);
128
+ placements.forEach((p, i) => im.setMatrixAt(i, tmp.multiplyMatrices(p, src.matrixWorld)));
129
+ im.instanceMatrix.needsUpdate = true;
130
+ im.castShadow = im.receiveShadow = true;
131
+ group.add(im);
132
+ });
133
+ scene.add(group);
134
+ ```
135
+
136
+ **Distance LOD** (open worlds / long sightlines): the rung ladder already ships
137
+ a lighter mesh — use the phone rung as the far level. One extra fetch; worth it
138
+ past ~25 m draw ranges, skip it in small arenas:
139
+
140
+ ```ts
141
+ const far = await gltfLoader.loader.loadAsync(`${MODEL_URL}@1024`);
142
+ const lod = new THREE.LOD();
143
+ lod.addLevel(gltf.scene, 0); // near: the tier's rung
144
+ lod.addLevel(far.scene, 25); // far: phone rung — three swaps by camera distance
145
+ scene.add(lod);
146
+ ```
147
+
148
+ For animated GLBs, drive `gltf.animations` with a `THREE.AnimationMixer`
149
+ (animated/skinned meshes can't instance — clone those).
105
150
 
106
151
  **If the game has physics, give the model a collider** — a GLB added only to the
107
152
  scene is a ghost: players and objects pass straight through it.
@@ -112,11 +112,13 @@ and `$genex-ai-texture` skills show the wiring in place.
112
112
 
113
113
  ## Generated models: load through the rungs
114
114
 
115
- Generated GLBs are provider-raw multiple 1–4K PBR textures + dense geometry.
116
- Every model ships a `@1024` mobile rung (textures budgeted to 1024, meshopt-
117
- compressed, simplified where safe) plus a `.ktx2` sibling whose textures stay
115
+ Provider-raw GLBs are NOT game assets: one prop is ~500k triangles + several
116
+ 4K PBR textures (a pilot scene of them submitted 66M triangles/frame and
117
+ floored an M4 Max). Every model ships a game-ready ladder `@2048` (the
118
+ desktop rung) and `@1024` (the phone rung), both meshopt-compressed and
119
+ simplified where safe, each with a `.ktx2` sibling whose textures stay
118
120
  compressed ON the GPU (~6× less texture VRAM). A plain `GLTFLoader` can decode
119
- neither — wire the decoders once and load through the ladder:
121
+ none of them — wire the decoders once and load through the ladder:
120
122
 
121
123
  ```ts
122
124
  import { createGltfLoader } from "./controllers/quality/gltf-loader.ts";
@@ -126,11 +128,22 @@ const gltfLoader = createGltfLoader(renderer); // meshopt always; KTX2 when the
126
128
  const gltf = await loadModelWithFallback(
127
129
  MODEL_URL, tier, (u) => gltfLoader.loader.loadAsync(u), { ktx2: gltfLoader.ktx2 },
128
130
  );
131
+ // Provider PBR ships mirror-metal (metalness~1) that reflects the sky env and
132
+ // swims with camera motion — clamp it on every loaded model:
133
+ gltf.scene.traverse((o) => {
134
+ const m = (o as THREE.Mesh).material as THREE.MeshStandardMaterial;
135
+ if (m?.isMeshStandardMaterial) {
136
+ m.metalness = Math.min(m.metalness, 0.6);
137
+ m.roughness = Math.max(m.roughness, 0.35);
138
+ m.envMapIntensity = 0.6;
139
+ }
140
+ });
129
141
  ```
130
142
 
131
- Fallback chain: `.ktx2` rung → universal `@1024` rung → original — each step
132
- warns, the worst case is today's full download, never a broken boot. Desktop
133
- always loads the original. Meshy characters take the same ladder through
143
+ Fallback chain: `.ktx2` rung → the tier's universal rung (`@2048` desktop,
144
+ `@1024` phones) original each step warns; the original is archival/remix
145
+ source, fetched only as the fallback of last resort. Meshy characters take
146
+ the same ladder through
134
147
  `loadMeshyCharacter(manifestUrl, { loader: gltfLoader.loader, modelUrlCandidates: (u) => [pickModel(u, tier, { ktx2: gltfLoader.ktx2 }), pickModel(u, tier), u] })`.
135
148
  KTX2-capable games can also pass `ktx2Load` to `loadTextureWithFallback` so
136
149
  skyboxes/textures use their `.ktx2` variants.
@@ -22,7 +22,7 @@ cost of guessing high is a dead page.
22
22
  | Shadow map | 512 (static-cached) | 1024 | 1024 | 2048 |
23
23
  | Post level | tone map only | + FXAA/vignette | + FXAA/vignette | full named stack |
24
24
  | Skybox rung | @2048 (~11 MB) | @4096 (~45 MB) | original | original |
25
- | Model rung | @1024 (+.ktx2 when wired) | @1024 (+.ktx2 when wired) | original | original |
25
+ | Model rung | @1024 (+.ktx2 when wired) | @1024 (+.ktx2 when wired) | @2048 (+.ktx2) | @2048 (+.ktx2) |
26
26
  | Texture rung (props) | @1024 | @2048 | original | original |
27
27
  | Particles/scatter | 0.25× | 0.5× | 0.75× | 1× |
28
28
  | Draw distance | 0.5× | 0.75× | 1× | 1× |
@@ -69,6 +69,12 @@ Getting this wrong produces silent no-ops or a session stuck ugly:
69
69
  fog, LOD bias, particle counts, mixer update rates, frame cap, remote-avatar
70
70
  animation count. Light `intensity` is runtime-free; light COUNT is not (see
71
71
  above).
72
+ - **Shadow maps of STATIC lights render once, not per frame:** shadow maps
73
+ default to re-rendering every frame, and a PointLight's is a 6-face cube —
74
+ four static shadow lanterns = 24 redundant passes/frame over the whole
75
+ caster set (measured 66M submitted triangles/frame in a pilot). Freeze them:
76
+ `light.shadow.autoUpdate = false; light.shadow.needsUpdate = true;` — only
77
+ the key light that shadows moving actors stays dynamic.
72
78
 
73
79
  ## Governor mechanics
74
80
 
@@ -113,9 +119,15 @@ Getting this wrong produces silent no-ops or a session stuck ugly:
113
119
  - Watch `renderer.info.memory.{textures,geometries}` across swaps in dev; a
114
120
  monotonic climb is a leak marching toward the OS kill. The governor
115
121
  publishes these counts for the platform's field telemetry.
116
- - Prefer meshopt/instanced geometry for repeats; `BatchedMesh` batches
117
- HETEROGENEOUS static meshes into one draw where instancing (identical
118
- meshes only) can't.
122
+ - Prefer meshopt/instanced geometry for repeats: 3+ copies of the same
123
+ generated model `InstancedMesh` (one draw call per source mesh instead of
124
+ per copy — `$genex-ai-model` has the collapse snippet); `BatchedMesh`
125
+ batches HETEROGENEOUS static meshes into one draw where instancing
126
+ (identical meshes only) can't.
127
+ - Distance LOD comes free with the rung ladder: `THREE.LOD` with the phone
128
+ rung (`@1024`) as the far level — `lod.addLevel(near, 0);
129
+ lod.addLevel(far, 25)`. One extra fetch; worth it past ~25 m sightlines,
130
+ skip in small arenas.
119
131
  - Half-resolution transparency: render heavy particle/transparency passes to a
120
132
  half-size target and composite up — fill-rate is the phone bottleneck.
121
133
 
@@ -82,44 +82,63 @@ game doesn't have (a lap counter in a game without laps). Generate it
82
82
  `--size 2560x1440 --quality high --candidates 2 --no-wait`, enqueued FIRST
83
83
  of all art; the picked candidate's URL goes into the style-brief comment.
84
84
 
85
- **Share it the moment it lands queue first, show immediately, never wait.**
86
- When the mockup lands, pick the stronger of the two candidates yourself,
87
- enqueue the downstream UI jobs and the core asset set against it in the same
88
- breath the frame you just generated IS the working style until the user
89
- says otherwise. Then pick it up with
90
- `genex wait <id> --open` (or generate it with `--open`): it opens in the
91
- user's browser AND prints the links. Paste BOTH candidates' URLs as
92
- clickable links in your message and say which one you picked and why — a URL
93
- is invisible in a terminal, and "do you like it?" with
94
- no picture in front of the user is the #1 way this checkpoint fails (they end
95
- up digging logs for the file path). Ask ONE keep-or-change question "this
96
- is roughly how the game and its HUD will look: keep it, or change
97
- something?" — **as
98
- plain chat text, never via the blocking structured-question tool: a suspended
99
- turn is a gate, and this question must not gate anything.** Silence is
100
- acceptance of YOUR pick; only explicit notes reopen the style. An unanswered style
101
- question that parks the art pipeline is how a finished game ships bare walls
102
- and a placeholder HUD strictly worse than re-rolling a few assets later.
103
- **Feedback triggers a LOOP whenever it arrives:** if the user comes back
104
- disliking ANYTHING immediately or an hour later regenerate with their
105
- exact notes (`--candidates 2–3` gives them options to choose from), open +
106
- link the new frame, and ask again the same non-blocking way. Carry every note
107
- forward so each round compounds; if two rounds don't converge, offer 2–3
108
- distinct directions instead of re-rolling blind (plain chat, same rule).
109
- Every shown frame becomes the working style exactly like the first did:
110
- silence after a shown regeneration accepts THAT frame, and when
111
- `--candidates` goes unanswered, pick the strongest yourself and say so. When
112
- the style actually changes, re-run Stage 1 the concept mockup itself
113
- with the user's exact notes (strictly CHEAPER than it used to be: one image
114
- carries scene + HUD, there is no separate concept to redo), then re-anchor
115
- the style-carrying art made against the old mockup: the menu still is
116
- re-edited (`--edit`) against the new mockup and its video re-run once from
117
- the new still (`$genex-ai-menu` a user-driven style change re-opens its
118
- one-video rule), and the HUD deconstruct (Stage 2 onward, `$genex-ai-hud`)
119
- restarts from the new mockup. Style-neutral assets (most textures, sfx,
120
- models) usually survive judge each in one line. The same "open it + paste the
121
- link" rule covers every image the user weighs in on the menu still, the
122
- HUD mockup candidates.
85
+ **Describe the concept, show it, then get a YES before the style-dependent
86
+ work fans out.** First lay the concept out in WORDS — the playable moment
87
+ (the verb), what threatens the player, what they chase, how the space reads,
88
+ and the brief's palette / materials / light / references so the user is
89
+ weighing a stated direction, not guessing at a picture. Generate the mockup
90
+ FIRST of all art (`--candidates 2`) and get it in front of the user AS FAST
91
+ AS POSSIBLE: pick the stronger candidate yourself, pick it up with
92
+ `genex wait <id> --open` (or generate it with `--open`) so it opens in the
93
+ user's browser AND prints the links, and paste BOTH candidates' URLs as
94
+ clickable links saying which one you picked and why a URL is invisible in a
95
+ terminal, and "do you like it?" with no picture in front of the user is the
96
+ #1 way this checkpoint fails (they end up digging logs for the file path).
97
+
98
+ **Then ask for the yes with your environment's structured question tool**
99
+ Claude Code: `AskUserQuestion`; Codex: `request_user_input`; a short numbered
100
+ list only where there is none (e.g. Cursor). ONE question "this is roughly
101
+ how the game and its HUD will look keep this direction, or change
102
+ something?" with concrete keep / change options. This confirmation is
103
+ REQUIRED for every game: the concept sets the STYLE every later asset
104
+ inherits, so the user gets a real say before that style is committed. This
105
+ reverses the old "advisory, never gate" rule the concept now gates, but
106
+ ONLY the style-dependent work (below), never the whole build.
107
+
108
+ **The yes gates the STYLE-DEPENDENT work nothing else.** Until the user
109
+ approves a frame, do NOT fan out the art or subagents that build ON the
110
+ chosen look: the HUD Stage-2 deconstruct (from the picked candidate), the
111
+ menu still + video (`--edit`-anchored to the mockup), and the logotype. (The core asset set — hero model, ground texture, skybox,
112
+ sfx is prompted from the game IDEA, not the picked image, and mostly
113
+ survives a style change, so it does NOT wait: launch it up front with
114
+ placeholders, re-rolling only the few that don't survive if the look shifts.)
115
+ Everything else that does NOT depend on the look
116
+ keeps moving at full speed IN PARALLEL the scaffold and boot wiring, the
117
+ core gameplay / network loop, the controller, and the gameplay-LOGIC subagent
118
+ modules (world, quests, enemies, items their structure comes from the
119
+ contract, not the picture). The wait is therefore never idle: a slow-to-answer
120
+ user still gets a walkable first version, and the instant they say yes the
121
+ style pipeline launches against the frame they approved. (A user who vanishes
122
+ entirely leaves the LOOK pending, not the game — the deliberate trade for
123
+ never committing a style behind their back.)
124
+
125
+ **Change reopens the loop, same shape.** If the user picks "change" — or
126
+ comes back with notes immediately or an hour later — re-run Stage 1 (the
127
+ concept mockup itself) with their exact notes (`--candidates 2–3` gives them
128
+ options to choose from), open + link the new frame, and ask again the same
129
+ structured way. Carry every note forward so each round compounds; if two
130
+ rounds don't converge, offer 2–3 distinct directions instead of re-rolling
131
+ blind. Each frame the user approves becomes the working style, and the
132
+ style-dependent pipeline stays gated on the latest yes (strictly CHEAPER than
133
+ it used to be: one image carries scene + HUD, there is no separate concept to
134
+ redo). When the style actually changes after downstream art already ran,
135
+ re-anchor it: the menu still is re-edited (`--edit`) against the new mockup
136
+ and its video re-run once from the new still (`$genex-ai-menu` — a
137
+ user-driven style change re-opens its one-video rule), and the HUD deconstruct
138
+ (Stage 2 onward, `$genex-ai-hud`) restarts from the new mockup. Style-neutral
139
+ assets (most textures, sfx, models) usually survive — judge each in one line.
140
+ The same "open it + paste the link" rule covers every image the user weighs
141
+ in on — the menu still, the HUD mockup candidates.
123
142
 
124
143
  **The concept anchors STYLE, not truth.** Palette, materials, light, and
125
144
  register come from the frame; CONTENT comes from the game contract. This
@@ -158,9 +177,11 @@ UI plan
158
177
  • Style: <4–5 named hues> · fonts <display> / <body>
159
178
  • References: <2–3 AAA games — one line on what's borrowed>
160
179
  • Menu: <archetype + button treatment, one-line reason from the brief>
161
- • Concept (HUD mockup): <generation id — 2 candidates>
162
- • Queued next: HUD sheet (Stage 2, from the picked mockup) · menu still →
163
- video <or "no menu: reason"> · logotype <or "skipped: reason">
180
+ • Concept (HUD mockup): <generation id — 2 candidates> — awaiting your keep / change
181
+ • Queued ON YOUR YES (style-dependent): HUD sheet (Stage 2, from the picked
182
+ mockup) · menu still → video <or "no menu: reason"> · logotype <or
183
+ "skipped: reason">
184
+ • Building now (style-independent): <core loop · logic modules already moving>
164
185
  • Deferred from mockup: <widgets the image invented but the game lacks — or "none">
165
186
  ```
166
187
 
@@ -178,7 +199,8 @@ A message missing any line means the gate did not run — go back and run it.
178
199
  every game), and the generated **logotype** — one `--transparent` wordmark
179
200
  in the brief's display register (`$genex-ai-menu`'s logotype step), default
180
201
  YES for every game with a menu; skipping it needs a one-line stated reason.
181
- Enqueue the chain at the plan gate with `--no-wait`,
202
+ Enqueue this chain the instant the user approves the concept
203
+ (`--no-wait`) — it is style-dependent, so it waits on the yes — then
182
204
  keep building, pick results up with `npx genex wait <id>`, swap them in as
183
205
  they land. **Tier 2 must never block a playable v0 — but the game is NOT
184
206
  DONE until its Tier-2 assets have landed and been wired in** (the only
@@ -454,6 +476,30 @@ Order the HUD by what the player loses the game for ignoring:
454
476
  frame, a generated frame sprite (`$genex-ai-hud` chrome, or a Tier-3
455
477
  9-slice panel) is still the richer tool. The masked-fill HUD reveal and
456
478
  `genex ui` masks remain the other established uses of `mask`/`clip-path`.
479
+ - **The #1 chamfer defect: a frame that STOPS at the cut.** A plain
480
+ `border` or `box-shadow` does NOT follow a `clip-path` chamfer — the clip
481
+ shears it off along the diagonal, so the straight sides keep their frame
482
+ while the cut edge goes bare and the panel reads as broken. Two clean
483
+ fixes: prefer **`border-radius`** when a soft corner reads fine (it keeps
484
+ its `border` natively — no clip needed); for a HARD angular cut, draw the
485
+ frame by clipping TWO stacked layers to the SAME polygon — outer = the
486
+ frame, inner (inset by the frame width) = the fill — so the outer shows
487
+ through as a uniform edge on the diagonal too:
488
+
489
+ ```css
490
+ /* Chamfered panel whose frame follows the cut. Never `border` + clip-path. */
491
+ .panel {
492
+ --chamfer: 16px; --edge: 2px; /* --edge = frame width */
493
+ --cut: polygon(var(--chamfer) 0, 100% 0, 100% calc(100% - var(--chamfer)),
494
+ calc(100% - var(--chamfer)) 100%, 0 100%, 0 var(--chamfer));
495
+ clip-path: var(--cut); background: var(--frame-hue); /* this IS the frame */
496
+ padding: var(--edge); /* revealed all around, incl. the diagonal */
497
+ }
498
+ .panel > .panel__fill { clip-path: var(--cut); background: var(--plate-hue); }
499
+ ```
500
+
501
+ Same rule for chamfered buttons and any notched plate; verify the frame
502
+ is unbroken at every corner over real gameplay.
457
503
  - **One cohesion layer.** A single full-screen vignette div (a subtle radial
458
504
  gradient darkening the corners, optionally faint grain) over canvas + UI is
459
505
  the cheapest way to make DOM-over-WebGL read as one composed image instead
@@ -510,12 +556,19 @@ architecture and consume the shared style brief.
510
556
  - Hard-cut phase swaps, a menu whose elements just appear, numbers that
511
557
  teleport.
512
558
  - A silent menu; a bare "Loading…" over black.
513
- - The art pipeline parked on an unanswered look question — the question is
514
- advisory: queue Tier-2 + the asset set against the shown frame and keep
515
- moving; silence is acceptance.
559
+ - Style-dependent art fanned out before the user approved the concept — the
560
+ concept confirmation is REQUIRED and gates the look-dependent work (HUD
561
+ Stage 2, menu still/video, logotype, style-matched assets); OR the whole
562
+ build stalled waiting on that yes, when concept-INDEPENDENT work (scaffold,
563
+ core loop, logic subagents) must keep moving in parallel while it is pending.
516
564
  - A CSS-cut corner (`clip-path`/`mask`) that shears its own content — clipped
517
- text or padding, a lost focus ring or glow, a jagged aliased edge the
565
+ text or padding, a lost focus ring or glow, a jagged aliased edge, or a
566
+ border/frame that stops at the cut instead of following it — the
518
567
  technique is fine; the sloppy cut is the defect.
568
+ - A rectangular semi-transparent plate protruding past an opaque/angular
569
+ widget frame — a dark box floating over the scene. The frame's own art is
570
+ the backing; a plate is only for bare-text/outline widgets and stays inside
571
+ the widget silhouette (`$genex-ai-hud`'s plate rule).
519
572
  - UI panels covering the player or the thing about to kill them.
520
573
  - Layout shifting as numbers grow.
521
574
  - A fail state with no visible restart key, or a restart that reloads the page.
@@ -9,8 +9,9 @@ the capsule says a racing HUD leans oblique and bottom-right-heavy; your brief
9
9
  says what it's made of in THIS game.
10
10
 
11
11
  Every capsule below assumes the base rules from the skill: corners/edges for
12
- UI, one display + one body font, tabular numerals, contrast plates over
13
- arbitrary scenes, and panel/button corners executed cleanly `border-radius`,
12
+ UI, one display + one body font, tabular numerals, contrast plates for bare
13
+ text over arbitrary scenes (never a second plate stacked behind an opaque
14
+ frame sprite), and panel/button corners executed cleanly — `border-radius`,
14
15
  a CSS `clip-path`/`mask` shape, or a generated frame all work, as long as the
15
16
  cut never clips content (text, padding, glow), stays free of jagged-edge
16
17
  artifacts, and text never collides or truncates.
@@ -111,6 +111,13 @@ kill-switch diagnostic.
111
111
  legitimate time the population changes.
112
112
  - Couple a practical to its emitter: one color, one envelope, one on/off state.
113
113
  - The key owns shadows; practicals cast none until a shot proves they must.
114
+ - **A shadow-casting practical that never moves gets a FROZEN map**: a
115
+ PointLight shadow is a 6-face cube render, re-drawn every frame by default —
116
+ four static lanterns cost 24 shadow passes/frame over your densest props for
117
+ zero visual change (a pilot game submitted 66M triangles/frame this way).
118
+ Set `light.shadow.autoUpdate = false; light.shadow.needsUpdate = true;` so
119
+ the cube renders once; keep only the key (which shadows the movers) dynamic,
120
+ and budget frozen practicals at ≤1024 map size.
114
121
  - Tune intensities only after the renderer baseline is locked — retuning the
115
122
  whole rig after a tone-mapping change is self-inflicted.
116
123
  - Never repair unbalanced light ratios with exposure — fix the lights.
@@ -98,9 +98,10 @@ ECCTRL-derived character controller owns collision and world translation.
98
98
  **UI is mandatory routing:** every game has an interface — load
99
99
  `$genex-threejs-game-ui` for every NEW game and run its "Plan the UI first" gate
100
100
  (screen inventory, one shared style brief, tier decisions) right after the game
101
- concept is locked, before any UI code. Its Tier-2 generations (cinematic menu,
102
- sprite HUD) are enqueued async (`--no-wait` + `npx genex wait <id>`) so they land
103
- while the game is being built they never block a playable v0, and the game is
101
+ concept is locked, before any UI code. Its frame-anchored Tier-2 generations
102
+ (cinematic menu, sprite HUD) are enqueued async (`--no-wait` + `npx genex wait
103
+ <id>`) the moment the user approves the concept, so they land while the game is
104
+ being built — they never block a playable v0, and the game is
104
105
  not done, published, or handed off until they've been picked up (`npx genex wait`)
105
106
  and wired in.
106
107
 
@@ -195,8 +196,10 @@ Prefer the **procedural** skills above for abstract/parametric/animated systems
195
196
  each other.
196
197
 
197
198
  **Generate a core asset set by default — don't wait to be asked.** For any game that
198
- needs concrete objects or surfaces, decide a small core set from the concept and start
199
- generating it **up front, in parallel** (each `npx genex` is an independent ~1-minute
199
+ needs concrete objects or surfaces, decide a small core set from the game IDEA and start
200
+ generating it **up front, in parallel** this set is concept-INDEPENDENT (prompted from
201
+ the idea, not the concept image, and it mostly survives a style change), so it does NOT
202
+ wait on the concept yes (each `npx genex` is an independent ~1-minute
200
203
  job — launch them concurrently in the background, then scaffold the scene while they run
201
204
  and wire each asset in as it lands, with a procedural placeholder as fallback until then):
202
205
 
@@ -40,21 +40,26 @@ Three.js release or branch, and do not blindly copy demo architecture.
40
40
  saves, leaderboards, and multiplayer auth all come from it.
41
41
  3. Run `$genex-threejs-game-ui`'s "Plan the UI first" gate: screen inventory,
42
42
  one shared style brief, a tier per screen — stated visibly in chat, never
43
- decided silently. Enqueue the Tier-2 UI generations now with `--no-wait`
44
- (pick them up later with `npx genex wait <id>`) — they render server-side
45
- while you build the game. FIRST of all art: the gate's **concept mockup** —
43
+ decided silently. FIRST of all art, generate the gate's **concept mockup** —
46
44
  a playable-moment shot (verb + threat + objective in frame, per the
47
45
  game-ui gate) WITH the full HUD composited over it: the `$genex-ai-hud`
48
46
  Stage-1 image, one generation serving as concept, style checkpoint, and
49
- HUD blueprint (never a separate UI-free concept first). Shown to the user
50
- for a keep-or-change answer the moment
51
- it lands the answer is advisory, never a gate: the art pipeline starts
52
- against the frame as-is, and the game-ui skill owns the re-anchor loop
53
- when notes arrive; later `--edit`-able generations anchor to it for STYLE
54
- while the game contract owns content. Then the `$genex-ai-hud` Stage-1 mockup, enqueued
55
- here for EVERY game; and the `$genex-ai-menu` video whenever the menu
56
- decision is yes (the default for every game "it's only a draft" is not a
57
- reason to decide no). Skipping this enqueue is
47
+ HUD blueprint (never a separate UI-free concept first). Show it to the user
48
+ the moment it lands and get a keep-or-change answer through the
49
+ environment's structured question tool (Claude Code: `AskUserQuestion`;
50
+ Codex: `request_user_input`; numbered-list fallback otherwise). This answer
51
+ GATES but only the style-dependent pipeline: until the user approves a
52
+ frame, do NOT enqueue the Tier-2 art that builds on the look (the
53
+ `$genex-ai-hud` Stage-2 chain, and the `$genex-ai-menu` still + video
54
+ whenever the menu decision is yes the default for every game, "it's only
55
+ a draft" is not a reason to decide no — plus the logotype). The instant
56
+ they say yes, fire those with `--no-wait` (pick them up with
57
+ `npx genex wait <id>`); the game-ui skill owns the re-anchor loop when notes
58
+ arrive, and later `--edit`-able generations anchor to the approved frame for
59
+ STYLE while the game contract owns content. Everything that does NOT depend
60
+ on the look — scaffold, boot wiring, the core loop, and the
61
+ concept-independent content subagents below — keeps building in parallel
62
+ while the answer is pending, so the wait is never idle. Skipping this enqueue is
58
63
  the #1 way a finished game ships an ugly HUD — by step 12 there is nothing
59
64
  to swap in.
60
65
  4. Lock the visual direction — the same plan-first logic as the UI gate, in
@@ -212,19 +217,29 @@ Three.js release or branch, and do not blindly copy demo architecture.
212
217
  13. Validate in a real browser with fixed seeds, captures, interaction checks,
213
218
  and performance evidence.
214
219
 
215
- ## Parallelize with subagents (when the environment has them)
220
+ ## Parallelize with subagents the default whenever the environment has them
216
221
 
217
222
  Generations already render server-side in parallel — the wall-clock savings
218
- live in the ATTENDED chains. When the coding agent supports background
219
- subagents (e.g. Claude Code's Agent tool), split these off; in environments
220
- without them (Cursor), the same order simply runs sequentially and the
221
- `--no-wait` pattern still hides most generation latency.
223
+ live in the ATTENDED chains. Whenever the coding agent supports background
224
+ subagents (e.g. Claude Code's Agent tool), fanning the independent work out is
225
+ the DEFAULT, not a big-scope special case: split every disjoint chain below off
226
+ by default and keep the main thread building. In environments without them
227
+ (Cursor), the same order simply runs sequentially and the `--no-wait` pattern
228
+ still hides most generation latency.
222
229
 
223
- - **HUD chain worker.** The moment the Stage-1 mockup (the game concept)
224
- lands, hand the whole
230
+ **One ordering rule from the concept gate (step 3):** subagents that build ON
231
+ the look are CONCEPT-DEPENDENT and wait for the user's keep/change yes — the
232
+ HUD chain worker and any style-matched art. Subagents that do NOT depend on the
233
+ look — the content-module fan-out, verification of concept-independent parts —
234
+ launch immediately and run while the concept answer is still pending. Never let
235
+ the pending yes stall concept-independent work, and never fan out
236
+ concept-dependent work before it.
237
+
238
+ - **HUD chain worker (concept-dependent — starts on the yes).** The moment the
239
+ user APPROVES the concept, hand the whole
225
240
  `$genex-ai-hud` chain to one subagent: pick the better candidate → Stage-2
226
241
  sheet → clean → extract → masks → write the sprite wiring. Its prompt must
227
- be self-contained — the style brief, the mockup URL, the
242
+ be self-contained — the style brief, the approved mockup URL, the
228
243
  output dir, and exactly which files it owns. **Handoff rule:** whoever
229
244
  wires the HUD after the subagent finishes MUST read the produced
230
245
  `*.annotated-progress.json` and `.bbox.json` files from disk — fillBox
@@ -237,7 +252,8 @@ without them (Cursor), the same order simply runs sequentially and the
237
252
  - **Verification runner.** Browser evidence — screenshots, control presses,
238
253
  the menu loop-seam watch, in-situ checks of placed art — can run in a
239
254
  subagent while the main agent keeps building.
240
- - **Content module fan-out (big scopes).** When the content contract names a
255
+ - **Content module fan-out (big scopes concept-independent, launch immediately).**
256
+ When the content contract names a
241
257
  big world or several content systems, serial hand-typing is what runs out
242
258
  of session: split the gameplay modules across parallel subagents instead —
243
259
  one each for world/terrain, quest + dialogue DATA, enemies/AI, items/