@genex-ai/cli-demo 0.78.1-dev.203 → 0.80.2-dev.213
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 +12 -5
- package/package.json +1 -1
- package/templates/README.md +1 -1
- package/templates/controllers/character/character-controller.ts +97 -0
- package/templates/skills/genex-ai-character/SKILL.md +2 -4
- package/templates/skills/genex-ai-hud/SKILL.md +46 -19
- package/templates/skills/genex-ai-menu/SKILL.md +13 -10
- package/templates/skills/genex-ai-model/SKILL.md +12 -6
- package/templates/skills/genex-game-director/SKILL.md +265 -0
- package/templates/skills/genex-game-director/references/design-contract.md +148 -0
- package/templates/skills/{genex-threejs-skill-router → genex-game-director}/references/routing-map.md +70 -67
- package/templates/skills/genex-getting-started/SKILL.md +3 -2
- package/templates/skills/genex-threejs-adaptive-quality/SKILL.md +9 -7
- package/templates/skills/genex-threejs-character-controller/SKILL.md +12 -9
- package/templates/skills/genex-threejs-embed-auth/SKILL.md +6 -48
- package/templates/skills/genex-threejs-game-content/SKILL.md +3 -4
- package/templates/skills/genex-threejs-game-ui/SKILL.md +23 -57
- package/templates/skills/genex-threejs-game-ui/references/style-capsules.md +7 -4
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +7 -2
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +2 -4
- package/templates/skills/genex-threejs-skill-router/SKILL.md +0 -266
package/dist/index.js
CHANGED
|
@@ -287,7 +287,7 @@ async function hasGenexSkills(skillsDir) {
|
|
|
287
287
|
return false;
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
|
-
var REMOVED_SKILLS = ["genex-explore"];
|
|
290
|
+
var REMOVED_SKILLS = ["genex-explore", "genex-threejs-skill-router"];
|
|
291
291
|
async function pruneRemovedSkills(skillsDir, log) {
|
|
292
292
|
const removed = [];
|
|
293
293
|
for (const name of REMOVED_SKILLS) {
|
|
@@ -1109,7 +1109,7 @@ async function writeProject(meta, cwd = process.cwd()) {
|
|
|
1109
1109
|
import fs7 from "fs/promises";
|
|
1110
1110
|
import path7 from "path";
|
|
1111
1111
|
var DEFAULT_DASHBOARD_ORIGIN = new URL(DEFAULT_AUTH_URL).origin;
|
|
1112
|
-
function renderGenexConfig() {
|
|
1112
|
+
function renderGenexConfig(slug) {
|
|
1113
1113
|
return `// src/genex.config.ts \u2014 written by \`genex init\`. DO NOT hardcode URLs here.
|
|
1114
1114
|
//
|
|
1115
1115
|
// Build once, run anywhere. The games host injects the serving environment into
|
|
@@ -1117,14 +1117,21 @@ function renderGenexConfig() {
|
|
|
1117
1117
|
// bundle), so ONE build is correct on every stand \u2014 promoting a game is a copy,
|
|
1118
1118
|
// never a rebuild, and an immutable bundle can never pin the wrong environment.
|
|
1119
1119
|
// Vite env still overrides, but is loaded ONLY by \`npm run dev\`:
|
|
1120
|
-
// .env -> VITE_GENEX_SLUG (
|
|
1120
|
+
// .env -> VITE_GENEX_SLUG (local override for \`npm run dev\`)
|
|
1121
1121
|
// .env.development.local -> local-stack URL overrides (dev mode ONLY; gitignored)
|
|
1122
|
+
//
|
|
1123
|
+
// The slug is ALSO baked as a literal below, on purpose: \`genex preview/publish\`
|
|
1124
|
+
// excludes .env from the source push (it's treated as a secret file), so a clone of
|
|
1125
|
+
// the published repo has no .env. Reading the slug from env ALONE would then build
|
|
1126
|
+
// \`slug: undefined\` -> the guest-session call posts {} -> 400 -> guests can't play a
|
|
1127
|
+
// remixed game. The literal keeps a bare clone rebuilding into a working game; the
|
|
1128
|
+
// env var still wins locally.
|
|
1122
1129
|
const injected: { apiUrl?: string; dashboardOrigins?: string[] } =
|
|
1123
1130
|
(typeof window !== "undefined" && (window as { __GENEX__?: unknown }).__GENEX__) as
|
|
1124
1131
|
| { apiUrl?: string; dashboardOrigins?: string[] }
|
|
1125
1132
|
| undefined ?? {};
|
|
1126
1133
|
export const GENEX = {
|
|
1127
|
-
slug: import.meta.env.VITE_GENEX_SLUG as string,
|
|
1134
|
+
slug: (import.meta.env.VITE_GENEX_SLUG as string | undefined) ?? ${JSON.stringify(slug)},
|
|
1128
1135
|
apiUrl:
|
|
1129
1136
|
(import.meta.env.VITE_GENEX_API_URL as string | undefined) ??
|
|
1130
1137
|
injected.apiUrl ??
|
|
@@ -1170,7 +1177,7 @@ async function writeIfAbsent(file, content, log) {
|
|
|
1170
1177
|
}
|
|
1171
1178
|
async function writeGameConfigFiles(meta, log, cwd = process.cwd()) {
|
|
1172
1179
|
await fs7.mkdir(path7.join(cwd, "src"), { recursive: true });
|
|
1173
|
-
await writeIfAbsent(path7.join(cwd, "src", "genex.config.ts"), renderGenexConfig(), log);
|
|
1180
|
+
await writeIfAbsent(path7.join(cwd, "src", "genex.config.ts"), renderGenexConfig(meta.slug), log);
|
|
1174
1181
|
await writeIfAbsent(path7.join(cwd, ".env"), renderSlugEnv(meta.slug), log);
|
|
1175
1182
|
const overrides = renderDevOverrides(meta);
|
|
1176
1183
|
if (overrides) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.80.2-dev.213",
|
|
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": {
|
package/templates/README.md
CHANGED
|
@@ -17,5 +17,5 @@ command keeps them in sync with the installed CLI version automatically
|
|
|
17
17
|
- `agents/` - example subagent definitions.
|
|
18
18
|
- `commands/` - example slash commands.
|
|
19
19
|
|
|
20
|
-
Start with `skills/genex-
|
|
20
|
+
Start with `skills/genex-game-director/SKILL.md` when asking your agent
|
|
21
21
|
to build or improve a 3D browser game.
|
|
@@ -88,6 +88,36 @@ type ResolvedMovementInput = {
|
|
|
88
88
|
/** Crouch input interpretation — see {@link CharacterControllerOptions.crouchMode}. */
|
|
89
89
|
export type CrouchMode = "toggle" | "hold";
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* The over-the-network snapshot of a character. Publish it every fixed tick with
|
|
93
|
+
* `room.me.set(character.netState())`; apply a remote's smoothed copy with
|
|
94
|
+
* `applyNetState(remoteObject, players.get(id).state)`.
|
|
95
|
+
*
|
|
96
|
+
* `y` is the SETTLED body height while grounded: the float-spring's idle up/down
|
|
97
|
+
* oscillation is removed, so a standing remote player does NOT bob in place.
|
|
98
|
+
* Publish this — never `currPos`, which is the raw physics capsule and bobs on
|
|
99
|
+
* its suspension spring. Numbers are rounded to ~cm; rotation is a 4-number
|
|
100
|
+
* quaternion (never a scalar yaw — that lerps the long way across ±π). The six
|
|
101
|
+
* booleans drive a remote's CharacterAnimations.
|
|
102
|
+
*/
|
|
103
|
+
export interface NetState {
|
|
104
|
+
x: number;
|
|
105
|
+
y: number;
|
|
106
|
+
z: number;
|
|
107
|
+
q: [number, number, number, number];
|
|
108
|
+
onGround: boolean;
|
|
109
|
+
falling: boolean;
|
|
110
|
+
moving: boolean;
|
|
111
|
+
running: boolean;
|
|
112
|
+
jumping: boolean;
|
|
113
|
+
crouching: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Round a networked number to ~cm precision (raw floats serialize as 17-digit JSON). */
|
|
117
|
+
function roundNet(v: number): number {
|
|
118
|
+
return Math.round(v * 100) / 100;
|
|
119
|
+
}
|
|
120
|
+
|
|
91
121
|
export interface CharacterRecoveryEvent {
|
|
92
122
|
reason: "non-finite" | "out-of-bounds";
|
|
93
123
|
position: { x: number; y: number; z: number };
|
|
@@ -484,6 +514,8 @@ export class CharacterController {
|
|
|
484
514
|
private slideFrictionCoef = 0;
|
|
485
515
|
private standingPointFriction = 0;
|
|
486
516
|
private readonly standingPoint = new THREE.Vector3();
|
|
517
|
+
/** Scratch for {@link netPos} — the float-oscillation-free networked position. */
|
|
518
|
+
private readonly _netPos = new THREE.Vector3();
|
|
487
519
|
private readonly characterMassImpulse = new THREE.Vector3();
|
|
488
520
|
private readonly movingObjectPosition = new THREE.Vector3();
|
|
489
521
|
private readonly movingObjectVelocity = new THREE.Vector3();
|
|
@@ -648,6 +680,29 @@ export class CharacterController {
|
|
|
648
680
|
get currPos(): THREE.Vector3 {
|
|
649
681
|
return this.currentPos;
|
|
650
682
|
}
|
|
683
|
+
/**
|
|
684
|
+
* Body position to PUBLISH over the network. Identical to {@link currPos} while
|
|
685
|
+
* airborne (jump/fall arcs must survive), but while grounded the up-axis
|
|
686
|
+
* component is snapped to the SETTLED float height above the ground — removing
|
|
687
|
+
* the suspension-spring idle oscillation that otherwise makes a standing remote
|
|
688
|
+
* player bob up and down. Publish this (via {@link netState}), never `currPos`.
|
|
689
|
+
* Live vector — copy it if you keep it. Follows custom gravity (up = bodyYAxis).
|
|
690
|
+
*/
|
|
691
|
+
get netPos(): THREE.Vector3 {
|
|
692
|
+
this._netPos.copy(this.currentPos);
|
|
693
|
+
if (this._isOnGround) {
|
|
694
|
+
// currPos bobs because the float spring holds the capsule a VARYING gap
|
|
695
|
+
// above the ground each step. standingPoint is the STATIC ground contact;
|
|
696
|
+
// the settled center→ground gap (along up) is groundFloatingDistance −
|
|
697
|
+
// rayOriginOffset. Shift the up component by (settled − current) so the
|
|
698
|
+
// published height is the settled one, not the oscillating one.
|
|
699
|
+
const gapNow =
|
|
700
|
+
this._netPos.dot(this.characterYAxis) - this.standingPoint.dot(this.characterYAxis);
|
|
701
|
+
const gapSettled = this.groundFloatingDistance - this.rayOriginOffset;
|
|
702
|
+
this._netPos.addScaledVector(this.characterYAxis, gapSettled - gapNow);
|
|
703
|
+
}
|
|
704
|
+
return this._netPos;
|
|
705
|
+
}
|
|
651
706
|
/** Body rotation (this step). Live quaternion. */
|
|
652
707
|
get currQuat(): THREE.Quaternion {
|
|
653
708
|
return this.currentQuat;
|
|
@@ -799,6 +854,32 @@ export class CharacterController {
|
|
|
799
854
|
// Public methods
|
|
800
855
|
// ────────────────────────────────────────────────────────────────────────
|
|
801
856
|
|
|
857
|
+
/**
|
|
858
|
+
* The snapshot to PUBLISH for multiplayer, in one call:
|
|
859
|
+
* `room.me.set(character.netState())` — on the fixed 10–20 Hz tick, never per
|
|
860
|
+
* frame. Position is {@link netPos} (float-oscillation-free, so remotes never
|
|
861
|
+
* bob), rotation is a 4-number quaternion, plus the six animation booleans a
|
|
862
|
+
* remote feeds to its own CharacterAnimations. Numbers are rounded to ~cm to
|
|
863
|
+
* keep the wire small. This is the ONLY position you should network — reaching
|
|
864
|
+
* for `currPos` here is the classic "standing remote player bobs" bug.
|
|
865
|
+
*/
|
|
866
|
+
netState(): NetState {
|
|
867
|
+
const p = this.netPos;
|
|
868
|
+
const q = this.currentQuat;
|
|
869
|
+
return {
|
|
870
|
+
x: roundNet(p.x),
|
|
871
|
+
y: roundNet(p.y),
|
|
872
|
+
z: roundNet(p.z),
|
|
873
|
+
q: [roundNet(q.x), roundNet(q.y), roundNet(q.z), roundNet(q.w)],
|
|
874
|
+
onGround: this.isOnGround,
|
|
875
|
+
falling: this.isFalling,
|
|
876
|
+
moving: this.isMoving,
|
|
877
|
+
running: this.runActive,
|
|
878
|
+
jumping: this.jumpActive,
|
|
879
|
+
crouching: this.crouchActive,
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
802
883
|
/**
|
|
803
884
|
* Merge movement intents into the input state. Only fields you pass are
|
|
804
885
|
* changed, so different input sources (keyboard, joystick, buttons) can each
|
|
@@ -1927,3 +2008,19 @@ export class CharacterController {
|
|
|
1927
2008
|
d.velocityArrow.setLength(this._relativeVel.length() / this.targetMoveSpeed(this._runActive));
|
|
1928
2009
|
}
|
|
1929
2010
|
}
|
|
2011
|
+
|
|
2012
|
+
/**
|
|
2013
|
+
* Apply a remote player's networked snapshot to their VISUAL object (a plain
|
|
2014
|
+
* Object3D — never a CharacterController; remotes are interpolated visuals, not
|
|
2015
|
+
* simulated). Read the SMOOTHED copy the SDK exposes so motion glides:
|
|
2016
|
+
* `applyNetState(remoteObject, players.get(id).state)`. The six booleans
|
|
2017
|
+
* (`s.onGround`, `s.falling`, …) feed that remote's CharacterAnimations
|
|
2018
|
+
* separately — see the character-controller skill's multiplayer rule.
|
|
2019
|
+
*/
|
|
2020
|
+
export function applyNetState(target: THREE.Object3D, s: NetState): void {
|
|
2021
|
+
target.position.set(s.x, s.y, s.z);
|
|
2022
|
+
// normalize: netState() rounds each quaternion component to ~cm precision, so the 4-tuple is
|
|
2023
|
+
// almost never exactly unit — Three.js bakes a non-unit quaternion straight into the matrix as a
|
|
2024
|
+
// small (~0.4%) scale that shifts as the remote turns. One call keeps remotes at true scale.
|
|
2025
|
+
target.quaternion.set(s.q[0], s.q[1], s.q[2], s.q[3]).normalize();
|
|
2026
|
+
}
|
|
@@ -43,10 +43,8 @@ Meshy Image-to-3D first produces an unremeshed high-detail model. Show its
|
|
|
43
43
|
front, back, left, and right views and report its measured face count. Preserve
|
|
44
44
|
that model in R2. Before rigging, ask the user to approve a separate
|
|
45
45
|
10,000-face triangle remesh. The 10k remesh—not the high-detail source—is
|
|
46
|
-
rigged and animated. (For these approvals, use your
|
|
47
|
-
|
|
48
|
-
`request_user_input`; if it has none, e.g. Cursor, a short numbered list in
|
|
49
|
-
chat.)
|
|
46
|
+
rigged and animated. (For these approvals, use your question tool when you
|
|
47
|
+
have one; if you have none, a short numbered list in chat.)
|
|
50
48
|
|
|
51
49
|
The high-detail pre-rig generation stays in the selected neutral A-pose. There
|
|
52
50
|
is no dynamic-pose concept and no silent T-pose fallback. After the user has
|
|
@@ -48,14 +48,12 @@ ring) that no hand-written CSS can fake.
|
|
|
48
48
|
semi-transparency physically cannot survive sprite extraction: a composited
|
|
49
49
|
panel's pixels are a blend of panel and background, and no cutout can
|
|
50
50
|
un-blend them. Plates are NEVER baked into sprites. Give the plate its
|
|
51
|
-
corners with `border-radius`
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
`clip-path`
|
|
56
|
-
|
|
57
|
-
best as chrome, generated. The load-bearing `mask`/`clip-path` in this
|
|
58
|
-
skill is the masked-fill reveal below.
|
|
51
|
+
corners with `border-radius` — **never a raw `clip-path`/`mask` chamfer**
|
|
52
|
+
(it shears off borders, shadows, and content near the cut and re-breaks on
|
|
53
|
+
every padding/value change: the recurring "cut corners" defect). A genuinely
|
|
54
|
+
ornamented or angular frame belongs in chrome, generated. The one
|
|
55
|
+
load-bearing `mask`/`clip-path` in this skill is the masked-fill reveal
|
|
56
|
+
below — never corner shaping.
|
|
59
57
|
- **Chrome (sprites — what THIS pipeline generates).** Opaque frames, corner
|
|
60
58
|
brackets, ornaments, emblems, icons, medallions — hard-alpha art laid over
|
|
61
59
|
the glass. The Stage-2 sheet contains ONLY chrome; never a panel with its
|
|
@@ -82,7 +80,7 @@ ring) that no hand-written CSS can fake.
|
|
|
82
80
|
the container is the sprite and the content is DOM — the default split.
|
|
83
81
|
- **Rule B — image vs code (the 5-minute test).** If CSS can build the
|
|
84
82
|
element in under ~5 minutes without losing fidelity — flat plates, simple
|
|
85
|
-
dots/pips, plain strokes, a
|
|
83
|
+
dots/pips, plain strokes, a rounded corner — it is CSS. If it needs
|
|
86
84
|
craft (material, ornament, painterly texture), it is a sprite. One
|
|
87
85
|
exception cuts the other way: a static label engraved/embossed into a
|
|
88
86
|
frame's craft stays BAKED in the sprite (`baked_static` below) even
|
|
@@ -90,9 +88,10 @@ ring) that no hand-written CSS can fake.
|
|
|
90
88
|
- **CSS-chrome recipes — the middle ground.** Between bare CSS and the full
|
|
91
89
|
sprite pipeline sits brief-styled CSS chrome, right for utilitarian panels
|
|
92
90
|
and secondary screens: a two-hue gradient frame (border + inset
|
|
93
|
-
box-shadow in brief hues),
|
|
94
|
-
`$genex-threejs-game-ui`'s corner rules), a
|
|
95
|
-
divs (outer div = border hue, inset div =
|
|
91
|
+
box-shadow in brief hues), rounded with `border-radius` (never a raw
|
|
92
|
+
`clip-path` chamfer — see `$genex-threejs-game-ui`'s corner rules), a
|
|
93
|
+
9-slice-ish panel from nested divs (outer div = border hue, inset div =
|
|
94
|
+
plate hue, 2–3px reveal). What
|
|
96
95
|
makes these legitimate: **CSS elements are styled FROM THE BRIEF — hue,
|
|
97
96
|
weight, texture. Default-gray CSS anywhere on screen is a defect** (the
|
|
98
97
|
classic failure: score pips shipped as unstyled "○ ●" system glyphs).
|
|
@@ -219,8 +218,16 @@ serves as the user's style checkpoint, and anchors all later art
|
|
|
219
218
|
is frame-anchored, so it waits for the user's keep/change YES on that
|
|
220
219
|
checkpoint — kick it off the instant they approve, not before.** The scene half
|
|
221
220
|
of the prompt is written in TEXT from the game plan — `[GAME_SCENE]` in the
|
|
222
|
-
Stage-1 template: setting, the moment, what the player is doing, lighting
|
|
223
|
-
|
|
221
|
+
Stage-1 template: setting, the moment, what the player is doing, lighting.
|
|
222
|
+
**If a concept/reference image already exists — the user's own concept art, or
|
|
223
|
+
a look frame you generated and they approved — anchor Stage 1 to it with
|
|
224
|
+
`--edit <that-url>` so the HUD inherits its exact palette, materials, and
|
|
225
|
+
lighting; that anchoring is what makes the final HUD actually match the
|
|
226
|
+
concept, and skipping it is why a text-only mockup drifts.** A text-only
|
|
227
|
+
Stage 1 is the fallback for when no reference exists. (To `--edit` a
|
|
228
|
+
user-supplied image it must be reachable as an R2 URL — a bare chat attachment
|
|
229
|
+
is not; reproduce it as a generation first, or say plainly you can't anchor to
|
|
230
|
+
it rather than silently guessing.) Then write the
|
|
224
231
|
widget layout and wiring code (placement, masked-fill scaffolding,
|
|
225
232
|
plain-CSS placeholder bars) while it renders — the CSS HUD keeps
|
|
226
233
|
the game playable until the sprites land. When a sprite lands it REPLACES
|
|
@@ -238,8 +245,9 @@ re-run from Stage 1 with their notes — cheaper than it sounds (one image
|
|
|
238
245
|
is the whole concept now), so say so in one line and do it.
|
|
239
246
|
|
|
240
247
|
```bash
|
|
241
|
-
# Stage 1 — the game CONCEPT: full HUD composited over the game's own scene
|
|
242
|
-
# described
|
|
248
|
+
# Stage 1 — the game CONCEPT: full HUD composited over the game's own scene.
|
|
249
|
+
# TEXT-described; add `--edit <concept-url>` to anchor it to an existing
|
|
250
|
+
# concept/reference image so the HUD inherits its exact style.
|
|
243
251
|
# Two candidates in one call; pick the better, save its URL:
|
|
244
252
|
npx genex image "<filled stage-1 prompt>" --size 2560x1440 --quality high --candidates 2
|
|
245
253
|
|
|
@@ -492,6 +500,13 @@ Two hard rules:
|
|
|
492
500
|
|
|
493
501
|
## Verify before calling it done
|
|
494
502
|
|
|
503
|
+
**This is a gate, not a suggestion — the HUD is not "done", does not ship, and
|
|
504
|
+
is not called a milestone until you have looked at it running.** The field
|
|
505
|
+
failure is an agent that wires the sprites, takes one glance, notices a possible
|
|
506
|
+
clip or cutoff, and ships anyway with "I'll refine if the user reports issues."
|
|
507
|
+
That punt IS the defect. You are the one who verifies, not the user. If you are
|
|
508
|
+
out of budget to check, say the HUD is unverified — never call it done.
|
|
509
|
+
|
|
495
510
|
- **Open every extracted PNG and eyeball it** — right subject, right name.
|
|
496
511
|
The reading-order sort can mismap when rows are uneven; re-run `extract`
|
|
497
512
|
with corrected name order rather than regenerating anything.
|
|
@@ -502,9 +517,11 @@ Two hard rules:
|
|
|
502
517
|
Display fonts run 25–50% wider than a naive estimate; widen the box or drop
|
|
503
518
|
the weight, don't shrink the font.
|
|
504
519
|
- **No widget overlaps** at the reference viewport, and none off-screen.
|
|
505
|
-
- **Screenshot
|
|
506
|
-
`$genex-threejs-visual-validation`'s capture discipline.
|
|
507
|
-
|
|
520
|
+
- **Screenshot the running HUD and inspect it** — bright AND dark scenes —
|
|
521
|
+
with `$genex-threejs-visual-validation`'s capture discipline. **Zoom every
|
|
522
|
+
corner:** no sheared frames, no clipped/cut corners, no text truncated or
|
|
523
|
+
colliding with an edge. Judge the pixels, not your intent — skipping this is
|
|
524
|
+
exactly how the "cut corners" defect ships.
|
|
508
525
|
|
|
509
526
|
## The closing wiring audit — the pipeline's final stage, not optional
|
|
510
527
|
|
|
@@ -536,6 +553,16 @@ does not count. fillBox numbers, `segments`, and real sprite dims do not
|
|
|
536
553
|
survive paraphrase; the canonical version of this failure is a subagent
|
|
537
554
|
reporting "masks validated" while the parent ships width% fills.
|
|
538
555
|
|
|
556
|
+
**Do not fire-and-forget the whole HUD/menu/logo into one background subagent.**
|
|
557
|
+
The field failure: the entire production art pipeline was handed to a single
|
|
558
|
+
background agent that then stalled overnight, so every preview the user played
|
|
559
|
+
showed the CSS placeholder and the real HUD only landed hours later, unseen. If
|
|
560
|
+
art runs in a subagent it is a BOUNDED task you wait on, then wire and verify in
|
|
561
|
+
THIS session — not an overnight handoff. While the art is pending the HUD is
|
|
562
|
+
unverified: do not call the game done or push it as a finished milestone. If a
|
|
563
|
+
subagent stalls or misses its window, wire what landed and say plainly what is
|
|
564
|
+
still placeholder — never present a placeholder HUD as the finished look.
|
|
565
|
+
|
|
539
566
|
## Cost & latency honesty
|
|
540
567
|
|
|
541
568
|
A full HUD is **~9 image generations** (mockup + deconstruct + clean + a few
|
|
@@ -370,13 +370,14 @@ not):
|
|
|
370
370
|
re-anchored the look) re-opens this rule once — re-edit the still
|
|
371
371
|
(`--edit` against the new concept mockup) and re-run the video from the
|
|
372
372
|
new still. Agent-initiated polish never does.
|
|
373
|
-
- **
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
373
|
+
- **The menu (`--frame`) path renders 720p — don't force 1080p.** The
|
|
374
|
+
frame-conditioned first-last-frame route was rejected by the provider at
|
|
375
|
+
1080p (repeated 422 failures that shipped a dead static menu), so the menu
|
|
376
|
+
video path defaults to the proven-good, seamless-looping 720p — no flag
|
|
377
|
+
needed. The DOM crossfade + full-screen scale read fine at 720p; do NOT pass
|
|
378
|
+
`--resolution 1080p` on a `--frame` menu clip just to sharpen it, or you risk
|
|
379
|
+
re-triggering the 422 and losing the video entirely. (Plain text-to-video,
|
|
380
|
+
without `--frame`, still defaults to 1080p.)
|
|
380
381
|
- **Pause/victory/defeat variants reuse the same video — as GRADES.** Same
|
|
381
382
|
`<video>` element or URL, different emotion via CSS `filter` on the
|
|
382
383
|
background: pause = a plain dark overlay (`rgba(0,0,0,0.55)`); defeat =
|
|
@@ -406,9 +407,11 @@ not):
|
|
|
406
407
|
genuine state change; expect a loop seam.
|
|
407
408
|
- `--duration <sec>` (video) — 4, 6, or 8 for frame-conditioned clips;
|
|
408
409
|
default 8.
|
|
409
|
-
- `--resolution <720p|1080p>` (video) —
|
|
410
|
-
|
|
411
|
-
|
|
410
|
+
- `--resolution <720p|1080p>` (video) — plain text-to-video defaults to 1080p;
|
|
411
|
+
the frame-conditioned (`--frame`) menu path defaults to **720p** for
|
|
412
|
+
reliability (1080p was rejected there). `720p` is also the cost fallback
|
|
413
|
+
(~half the credits). Loop clips (`--loop`) ignore it (that model has no
|
|
414
|
+
resolution parameter).
|
|
412
415
|
- `--aspect 16:9 --quality high` (image) — the right settings for a menu frame.
|
|
413
416
|
- `--no-wait` — enqueue and return immediately with the generation id; pick
|
|
414
417
|
the result up later with `npx genex wait <id>` (safe to re-run — it attaches
|
|
@@ -87,16 +87,22 @@ const gltf = await loadModelWithFallback(
|
|
|
87
87
|
MODEL_URL, tier, (u) => gltfLoader.loader.loadAsync(u), { ktx2: gltfLoader.ktx2 },
|
|
88
88
|
);
|
|
89
89
|
const model = gltf.scene;
|
|
90
|
-
//
|
|
91
|
-
// env and swims with every camera move)
|
|
90
|
+
// Tame ONLY the provider's mirror-metal artifact (metalness≈1 + near-zero
|
|
91
|
+
// roughness mirrors the whole sky env and swims with every camera move). Do
|
|
92
|
+
// NOT flatten every material to 0.6 — that was dulling legitimately metallic
|
|
93
|
+
// props (gunmetal, chrome, gold, polished stone). Clamp the extreme case only;
|
|
94
|
+
// leave everything else as authored.
|
|
92
95
|
model.traverse((o) => {
|
|
93
96
|
const m = (o as THREE.Mesh).material as THREE.MeshStandardMaterial;
|
|
94
|
-
if (m?.isMeshStandardMaterial)
|
|
95
|
-
|
|
96
|
-
m.
|
|
97
|
-
m.
|
|
97
|
+
if (!m?.isMeshStandardMaterial) return;
|
|
98
|
+
if (m.metalness > 0.85 && m.roughness < 0.2) {
|
|
99
|
+
m.metalness = 0.7; // still reads metallic, no longer a full mirror
|
|
100
|
+
m.roughness = Math.max(m.roughness, 0.3);
|
|
98
101
|
}
|
|
102
|
+
m.envMapIntensity = Math.min(m.envMapIntensity, 0.8);
|
|
99
103
|
});
|
|
104
|
+
// If a model that SHOULD read metallic still looks dull, this clamp is not the
|
|
105
|
+
// cause — check the model against the concept and lift per-material as needed.
|
|
100
106
|
model.scale.setScalar(1); // tune to taste
|
|
101
107
|
model.position.set(0, 0, 0);
|
|
102
108
|
scene.add(model);
|