@genex-ai/cli-demo 0.90.0-dev.227 → 0.92.0-dev.229

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
@@ -252,7 +252,7 @@ var CONTRACT_END = "<!-- genex:contract:end -->";
252
252
  var GENEX_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
253
253
  # Genex build contract (always in effect for this game)
254
254
 
255
- Your capabilities (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skybox\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\`. Full options: \`npx genex --help\`. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 re-load it whenever you're unsure which lane owns a task.
255
+ Your capabilities (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skybox\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\`. Full options: \`npx genex --help\`. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 re-load it whenever you're unsure which lane owns a task.
256
256
 
257
257
  1. Load the \`genex-game-director\` skill and follow it end to end before any game code. After ANY context compaction or session resume, re-read this file and \`DESIGN.md\`, re-load the skill for the stage you are executing, and continue from the Build plan's \`Now:\` line \u2014 never from memory alone.
258
258
  2. The design interview always happens: 2\u20134 build-forking questions, ONE round, before DESIGN.md is written \u2014 even when the request already names the game (then it confirms the pitch). Ask with your built-in question tool with clickable answer options when you have one; a short numbered list in plain chat otherwise. Only after asking may you proceed on stated assumptions if the player stays silent; record each as "assumed \u2014 player didn't answer" in DESIGN.md \u2192 Decisions.
@@ -265,13 +265,16 @@ Your capabilities (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skyb
265
265
  9. Never draw a rectangular backing plate behind bars, digits, or icons \u2014 in sprites or CSS. Ornament lives on the widget's own silhouette; a truly needed shaped plate comes from \`npx genex ui plate\`.
266
266
  10. Fonts: the brief's display + body pair comes from the menu skill's genre table (or carries a one-line stated reason) and is LOADED for real in \`index.html\`.
267
267
  11. Never park ready work behind a question, and never stall on an unanswered one \u2014 decide, state the decision in chat, record it, keep building.
268
- 12. Wire-in floors before any publish and before ending a session: sprite HUD wired (or the recorded CSS lane finished), menu video playing (or its recorded still fallback), logotype placed, fonts loaded, Escape pause, branded loader. Run \`npx genex wait\` on every generation you enqueued and wire what landed \u2014 never park landed assets.
269
- 13. Characters: Meshy/Mixamo/VRM rigs rest facing +Z. Set yaw explicitly when placing a rig; never mirror a SkinnedMesh with negative scale. In any two-character scene, verify in a capture that they face each other, not the camera.
270
- 14. Verify by looking: one smoke check per milestone, after that milestone's preview push, in local test mode (\`?genex_local_test=1\`) with a real gameplay screenshot. A claim without a capture is not verification.
268
+ 12. Wire-in floors before any publish and before ending a session: sprite HUD wired (or the recorded CSS lane finished), menu video playing (or its recorded still fallback), logotype placed, fonts loaded, Escape pause, branded loader, the player wearing the game's own generated character (or DESIGN.md recording why it isn't). Run \`npx genex wait\` on every generation you enqueued and wire what landed \u2014 never park landed assets.
269
+ 13. The player's body is the game's own generated character (\`npx genex character "<look>"\` \u2192 \`npx genex controller character --character <id>\`), enqueued with your first art actions, not after them. It applies wherever a human body appears on screen \u2014 first-person included, the moment remotes, a look-down body, a shadow, or a menu portrait shows one. The profile VRM avatar is the FALLBACK: a temporary body while the character renders (say in one plain line that it's temporary), or the stand-in when generation genuinely could not happen \u2014 out of credits, failed, unverified; record which in DESIGN.md as \`Player character: VRM \u2014 <reason>\`. Games whose player is not a person (car, ship, RTS cursor, board) generate that object with \`npx genex model\` instead. Characters: Meshy/Mixamo/VRM rigs rest facing +Z. Set yaw explicitly when placing a rig; never mirror a SkinnedMesh with negative scale. In any two-character scene, verify in a capture that they face each other, not the camera.
270
+ 14. Verify by looking: one smoke check per milestone, after that milestone's preview push, in local test mode (\`?genex_local_test=1\`) with a real gameplay screenshot. A claim without a capture is not verification. Local-test evidence proves visuals and controls ONLY \u2014 label it that way when you show the player, and never work around the draft sign-in gate any other way.
271
271
  15. Treat every \`genex\` warning line \u2014 preflight, \`ui audit\`, \`wait\` nudges \u2014 as work, not noise.
272
272
  16. Every finished Build-plan milestone ends with \`npx genex preview\` and the player's page link (\`<dashboard>/draft/<slug>\` with \`<dashboard>\` from \`.genex/project.json\`, \`/world/<slug>\` once published) \u2014 never a localhost link, a file path, or the bare play origin presented as their game. After every round of player feedback, end with a preview push.
273
273
  17. Parallel work: this line is your standing authorization and request to use sub-agents / parallel agent work whenever your platform provides them. While drafting the Build plan, decide per module what runs in parallel and what stays serial for THIS game \u2014 dependencies decide, there is no fixed list \u2014 and record each call in the Modules table with a one-line reason. Independent modules default to parallel; building everything serially needs a stated reason. You keep integration, previews, and the player conversation; each sub-agent owns only its module's files.
274
274
  18. Talk to the player in plain game language \u2014 what changed in the game and what to try; never code, file names, build output, or tool internals unless they ask. Short status lines while you work; long silent stretches are a failure.
275
+ 19. Never add debug-only code to the game to check your own work \u2014 no hidden test modes, no special URL parameters, no forced-visible flags, no auth mocks, no pixel-sampling hooks. \`?genex_local_test=1\` is the platform's own supported mode and is fine; your own bypass is not. (The multiplayer skill's small build identifier, token-free status line, and connected-quorum watchdog are production supportability, not a bypass \u2014 keep those.)
276
+ 20. Input directions match their labels: A/\u2190 moves or turns the player screen-LEFT, D/\u2192 screen-RIGHT, mouse-up looks up, and drag-pan axes share ONE convention. The cursor is either the gameplay tool (RTS, card, builder) or locked away during play \u2014 keyboard-only games included. Check it in every milestone's smoke pass.
277
+ 21. v0 is a milestone, not the destination. When the ask was bigger than one loop, every milestone after v0 grows back toward the FULL ask with DESIGN.md's content lines as the checklist \u2014 a slice that previewed well never quietly becomes the game. Cosmetics never jump the queue past promised content.
275
278
  ${CONTRACT_END}
276
279
  `;
277
280
  var CLAUDE_IMPORT_LINE = "@AGENTS.md";
@@ -2113,6 +2116,7 @@ var UI_USAGE = `${c.bold("Options for `ui` (all tools read --in <png path or htt
2113
2116
  white halo over dark scenes). [--expect N: hard-error
2114
2117
  unless exactly N components] [--min-pixels 2000]
2115
2118
  [--padding 8] [--chroma 240] [--dilate 0] [--no-defringe]
2119
+ [--allow-speckle]
2116
2120
  ui masks --out-dir <dir> --pairs "name:cX,cY,w,h:aX,aY,w,h[:n];\u2026"
2117
2121
  OR --auto (detect clean/annotated cell pairs itself) \u2014
2118
2122
  clean frame + green-annotated duplicate \u2192 frame + fill mask
@@ -2128,6 +2132,9 @@ var UI_USAGE = `${c.bold("Options for `ui` (all tools read --in <png path or htt
2128
2132
  [--min-frac 0.004] [--out <json>] [--crop <png>]
2129
2133
  ui trim [--out <png>] \u2014 crop to the alpha bbox (default: overwrite
2130
2134
  --in), print real dims + refresh the .bbox.json sidecar.
2135
+ Hard-refuses a DAMAGED cutout (isolated colour speckle on
2136
+ the soft rim \u2014 what a background-removal pass does to an
2137
+ image that was already transparent). [--allow-speckle]
2131
2138
  ui seams measure a tiling texture's wrap seam (opposite edges vs
2132
2139
  interior detail) \u2192 ratio + verdict as JSON; nonzero exit on
2133
2140
  a visible seam. [--seam-tolerance 3]
@@ -2139,7 +2146,9 @@ var UI_USAGE = `${c.bold("Options for `ui` (all tools read --in <png path or htt
2139
2146
  ui audit mechanical HUD-wiring scan (no --in): mask\u2194frame pairing
2140
2147
  mismatches (recorded crops disagree \u2014 PROVABLE, exit 1),
2141
2148
  white-painted meter troughs (baked sheet-white inside a
2142
- fill channel \u2014 PROVABLE, exit 1), loosened-check stamps,
2149
+ fill channel \u2014 PROVABLE, exit 1), damaged cutouts (colour
2150
+ speckle on a wired sprite's rim \u2014 PROVABLE, exit 1),
2151
+ loosened-check stamps,
2143
2152
  extracted-but-never-wired sprites, generated-but-
2144
2153
  unreferenced masks, naive JS %-width fills, phones-
2145
2154
  preload-video heuristic. Everything but the two provable
@@ -2422,6 +2431,15 @@ async function uiExtract(opts, log) {
2422
2431
  if (inputMode === "chroma" && opts.defringe !== false) {
2423
2432
  defringed = defringeAgainstMatte(out) > 0;
2424
2433
  }
2434
+ const speckle = rimSpeckleRatio(out);
2435
+ if (speckle && speckle.ratio > SPECKLE_MAX_RATIO && !opts.allowSpeckle) {
2436
+ fail(speckleRefusal(name, speckle.ratio), {
2437
+ input,
2438
+ sprite: name,
2439
+ speckleRatio: Number(speckle.ratio.toFixed(4)),
2440
+ rimPixels: speckle.sampled
2441
+ });
2442
+ }
2425
2443
  const outPath = path11.join(outDir, `${name}.png`);
2426
2444
  await writePng(outPath, out);
2427
2445
  const sidecar = {
@@ -3180,13 +3198,30 @@ async function uiTrim(opts, log) {
3180
3198
  h: bbox.maxY - bbox.minY + 1
3181
3199
  };
3182
3200
  const trimmed = cropPng(png, cropBox);
3201
+ const speckle = rimSpeckleRatio(trimmed);
3202
+ if (speckle && speckle.ratio > SPECKLE_MAX_RATIO && !opts.allowSpeckle) {
3203
+ fail(speckleRefusal("this image", speckle.ratio), {
3204
+ input,
3205
+ speckleRatio: Number(speckle.ratio.toFixed(4)),
3206
+ rimPixels: speckle.sampled
3207
+ });
3208
+ }
3183
3209
  await writePng(outPath, trimmed);
3184
3210
  const sidecar = computeBBoxes(trimmed);
3211
+ const speckleAllowed = !!(speckle && speckle.ratio > SPECKLE_MAX_RATIO);
3185
3212
  const sidecarPath = outPath.replace(/\.png$/i, "") + ".bbox.json";
3186
- await fs11.writeFile(sidecarPath, JSON.stringify(sidecar, null, 2));
3213
+ await fs11.writeFile(
3214
+ sidecarPath,
3215
+ JSON.stringify(speckleAllowed ? { ...sidecar, speckleAllowed: true } : sidecar, null, 2)
3216
+ );
3187
3217
  log.success(
3188
3218
  `Trimmed ${png.width}x${png.height} \u2192 ${trimmed.width}x${trimmed.height} (${outPath}).`
3189
3219
  );
3220
+ if (speckleAllowed) {
3221
+ log.warn(
3222
+ "Shipped with --allow-speckle: this cutout is damaged and the override is stamped in its sidecar. Eyeball it over a bright background before it reaches the player."
3223
+ );
3224
+ }
3190
3225
  process.stdout.write(
3191
3226
  `${JSON.stringify({
3192
3227
  out: outPath,
@@ -3195,7 +3230,9 @@ async function uiTrim(opts, log) {
3195
3230
  aspectRatio: sidecar.aspectRatio,
3196
3231
  outerBBox: sidecar.outerBBox,
3197
3232
  innerBBox: sidecar.innerBBox,
3198
- sidecar: sidecarPath
3233
+ sidecar: sidecarPath,
3234
+ ...speckle ? { speckleRatio: Number(speckle.ratio.toFixed(4)) } : {},
3235
+ ...speckleAllowed ? { speckleAllowed: true } : {}
3199
3236
  })}
3200
3237
  `
3201
3238
  );
@@ -3292,6 +3329,51 @@ async function uiPlate(opts, log) {
3292
3329
  `
3293
3330
  );
3294
3331
  }
3332
+ var SPECKLE_RIM_ALPHA_MIN = 16;
3333
+ var SPECKLE_RIM_ALPHA_MAX = 220;
3334
+ var SPECKLE_CHROMA_EXCESS = 60;
3335
+ var SPECKLE_MIN_SAMPLE = 200;
3336
+ var SPECKLE_MAX_RATIO = 0.02;
3337
+ function rimSpeckleRatio(png) {
3338
+ const { width: W, height: H, data } = png;
3339
+ if (W < 3 || H < 3) return null;
3340
+ const chroma = new Uint8Array(W * H);
3341
+ for (let i = 0; i < W * H; i++) {
3342
+ const o = i * 4;
3343
+ const r = data[o];
3344
+ const g = data[o + 1];
3345
+ const b = data[o + 2];
3346
+ chroma[i] = Math.max(r, g, b) - Math.min(r, g, b);
3347
+ }
3348
+ const neighbourhood = [];
3349
+ let sampled = 0;
3350
+ let speckled = 0;
3351
+ for (let y = 1; y < H - 1; y++) {
3352
+ for (let x = 1; x < W - 1; x++) {
3353
+ const i = y * W + x;
3354
+ const a = data[i * 4 + 3];
3355
+ if (a <= SPECKLE_RIM_ALPHA_MIN || a >= SPECKLE_RIM_ALPHA_MAX) continue;
3356
+ sampled++;
3357
+ neighbourhood.length = 0;
3358
+ for (let dy = -1; dy <= 1; dy++) {
3359
+ for (let dx = -1; dx <= 1; dx++) neighbourhood.push(chroma[(y + dy) * W + (x + dx)]);
3360
+ }
3361
+ neighbourhood.sort((p, q) => p - q);
3362
+ if (chroma[i] - neighbourhood[4] > SPECKLE_CHROMA_EXCESS) speckled++;
3363
+ }
3364
+ }
3365
+ if (sampled < SPECKLE_MIN_SAMPLE) return null;
3366
+ return { ratio: speckled / sampled, sampled };
3367
+ }
3368
+ function speckleRefusal(label, ratio) {
3369
+ return [
3370
+ `Damaged cutout \u2014 ${(ratio * 100).toFixed(1)}% of ${label}'s soft edge is isolated colour speckle (want <= ${(SPECKLE_MAX_RATIO * 100).toFixed(0)}%).`,
3371
+ "A background-removal pass over an image that was ALREADY transparent re-keys every edge and sprays colour noise along it. Trimming or defringing cannot fix this.",
3372
+ " -> From `genex image --clean`? Ship the `--transparent` original instead.",
3373
+ " -> Source really had a background? Re-clean with `--bg-mode matte` (soft edges).",
3374
+ " Override (stamped in the output): --allow-speckle"
3375
+ ].join("\n");
3376
+ }
3295
3377
  var DEFRINGE_BAND_PX = 2;
3296
3378
  function defringeAgainstMatte(png, matte = { r: 255, g: 255, b: 255 }) {
3297
3379
  const { width: W, height: H, data } = png;
@@ -3374,7 +3456,8 @@ function defringeAgainstMatte(png, matte = { r: 255, g: 255, b: 255 }) {
3374
3456
  }
3375
3457
  var HARD_AUDIT_KINDS = /* @__PURE__ */ new Set([
3376
3458
  "mask-frame-mismatch",
3377
- "white-trough"
3459
+ "white-trough",
3460
+ "cutout-speckle"
3378
3461
  ]);
3379
3462
  var TROUGH_OPAQUE_MIN_ALPHA = 200;
3380
3463
  var TROUGH_WHITE_MIN = 235;
@@ -3453,7 +3536,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
3453
3536
  } catch {
3454
3537
  }
3455
3538
  }
3456
- const referenced = (basename) => sources.some((s) => s.text.includes(basename));
3539
+ const referenced = (basename2) => sources.some((s) => s.text.includes(basename2));
3457
3540
  const findings = [...viewportFindings];
3458
3541
  const assetFiles = await walkFiles(absAssets);
3459
3542
  const metaByName = /* @__PURE__ */ new Map();
@@ -3535,6 +3618,22 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
3535
3618
  }
3536
3619
  }
3537
3620
  }
3621
+ for (const [base, p] of pngByBase) {
3622
+ if (/-mask\.png$/.test(base)) continue;
3623
+ if (!referenced(base)) continue;
3624
+ let png;
3625
+ try {
3626
+ png = PNG2.sync.read(await fs11.readFile(p));
3627
+ } catch {
3628
+ continue;
3629
+ }
3630
+ const speckle = rimSpeckleRatio(png);
3631
+ if (!speckle || speckle.ratio <= SPECKLE_MAX_RATIO) continue;
3632
+ findings.push({
3633
+ kind: "cutout-speckle",
3634
+ message: `${base}: ${(speckle.ratio * 100).toFixed(1)}% of the soft edge is isolated colour speckle (want <= ${(SPECKLE_MAX_RATIO * 100).toFixed(0)}%) \u2014 a background-removal pass re-keyed edges that were already transparent and sprayed colour noise along them. Nothing downstream repairs it: ship the \`--transparent\` original, or re-clean the true source with \`--bg-mode matte\`. Do not ship this sprite.`
3635
+ });
3636
+ }
3538
3637
  const loosenedNames = [...metaByName.entries()].filter(([, meta]) => meta.loosened).map(([name]) => name);
3539
3638
  if (loosenedNames.length > 0) {
3540
3639
  findings.push({
@@ -3866,13 +3965,109 @@ async function inlineLocalImage(filePath, flag) {
3866
3965
  const mime = IMAGE_MIME_BY_EXT[path13.extname(filePath).toLowerCase()] ?? "image/png";
3867
3966
  return { ok: true, dataUri: `data:${mime};base64,${bytes.toString("base64")}` };
3868
3967
  }
3869
- var SKYBOX_ENVIRONMENT_SUFFIX = ". Pure environment: sky, clouds, atmosphere, distant horizon haze. No buildings, no structures, no trees, no objects, no ground detail.";
3968
+ var SKYBOX_ENVIRONMENT_SUFFIX = ". The image contains ONLY sky: cloud, atmosphere, light, weather and distant haze at the horizon. Every structure, object, plant and ground surface is outside the frame.";
3870
3969
  var SKYBOX_PROMPT_MAX = 2e3;
3970
+ var SKYBOX_STRUCTURE_NOUNS = [
3971
+ "building",
3972
+ "buildings",
3973
+ "house",
3974
+ "houses",
3975
+ "hut",
3976
+ "huts",
3977
+ "cabin",
3978
+ "cabins",
3979
+ "tower",
3980
+ "towers",
3981
+ "castle",
3982
+ "castles",
3983
+ "cathedral",
3984
+ "cathedrals",
3985
+ "church",
3986
+ "churches",
3987
+ "temple",
3988
+ "temples",
3989
+ "shrine",
3990
+ "shrines",
3991
+ "monastery",
3992
+ "palace",
3993
+ "fortress",
3994
+ "citadel",
3995
+ "ruin",
3996
+ "ruins",
3997
+ "city",
3998
+ "cityscape",
3999
+ "skyline",
4000
+ "town",
4001
+ "village",
4002
+ "settlement",
4003
+ "street",
4004
+ "streets",
4005
+ "road",
4006
+ "roads",
4007
+ "courtyard",
4008
+ "plaza",
4009
+ "bridge",
4010
+ "bridges",
4011
+ "wall",
4012
+ "walls",
4013
+ "gate",
4014
+ "gates",
4015
+ "fence",
4016
+ "fences",
4017
+ "pillar",
4018
+ "pillars",
4019
+ "column",
4020
+ "columns",
4021
+ "statue",
4022
+ "statues",
4023
+ "monument",
4024
+ "obelisk",
4025
+ "spire",
4026
+ "spires",
4027
+ "rooftop",
4028
+ "rooftops",
4029
+ "tree",
4030
+ "trees",
4031
+ "forest",
4032
+ "jungle",
4033
+ "ship",
4034
+ "ships",
4035
+ "boat",
4036
+ "boats",
4037
+ "car",
4038
+ "cars",
4039
+ "vehicle",
4040
+ "vehicles",
4041
+ "person",
4042
+ "people",
4043
+ "figure",
4044
+ "figures",
4045
+ "character",
4046
+ "characters",
4047
+ "creature",
4048
+ "creatures",
4049
+ "dragon",
4050
+ "banner",
4051
+ "banners",
4052
+ "flag",
4053
+ "flags",
4054
+ "torch",
4055
+ "torches",
4056
+ "lamp",
4057
+ "lamps"
4058
+ ];
4059
+ var SKYBOX_NOUN_RE = new RegExp(`\\b(${SKYBOX_STRUCTURE_NOUNS.join("|")})\\b`, "gi");
4060
+ function skyboxStructureNouns(prompt) {
4061
+ const found = /* @__PURE__ */ new Set();
4062
+ for (const m of prompt.matchAll(SKYBOX_NOUN_RE)) found.add(m[1].toLowerCase());
4063
+ return [...found];
4064
+ }
3871
4065
  function applySkyboxGuard(prompt, raw) {
3872
- if (raw) return { prompt, guarded: false };
4066
+ if (raw) return { prompt, guarded: false, nouns: [] };
4067
+ const nouns = skyboxStructureNouns(prompt);
3873
4068
  const guarded = `${prompt}${SKYBOX_ENVIRONMENT_SUFFIX}`;
3874
- if (guarded.length > SKYBOX_PROMPT_MAX) return { prompt, guarded: false };
3875
- return { prompt: guarded, guarded: true };
4069
+ if (guarded.length > SKYBOX_PROMPT_MAX) return { prompt, guarded: false, nouns };
4070
+ return { prompt: guarded, guarded: true, nouns };
3876
4071
  }
3877
4072
  function buildGenOptions(kind, opts) {
3878
4073
  const options = { ...opts.generationOptions };
@@ -3927,6 +4122,23 @@ async function runGenerate(kind, opts) {
3927
4122
  return;
3928
4123
  }
3929
4124
  const skyboxGuard = kind === "skybox" ? applySkyboxGuard(typedPrompt, opts.raw) : void 0;
4125
+ if (skyboxGuard && skyboxGuard.nouns.length > 0) {
4126
+ log.error(
4127
+ `A skybox is sky only \u2014 this prompt names ${skyboxGuard.nouns.join(", ")}.`
4128
+ );
4129
+ log.plain(
4130
+ [
4131
+ " Structures painted into a sky render at infinite distance: they never get closer",
4132
+ " as the player moves, and they read as broken the moment the camera strafes.",
4133
+ ` ${c.cyan("\u2192")} Build them as scene geometry (${c.cyan("npx genex model")}); describe only light, colour,`,
4134
+ " cloud, weather and time of day here.",
4135
+ ` ${c.cyan("\u2192")} Genuinely want it painted into the sky (a space-station panorama for a scene`,
4136
+ ` with no world geometry)? Re-run with ${c.cyan("--raw")}.`
4137
+ ].join("\n")
4138
+ );
4139
+ process.exitCode = 1;
4140
+ return;
4141
+ }
3930
4142
  let prompt = skyboxGuard?.prompt ?? typedPrompt;
3931
4143
  if (kind === "image" && opts.glass) {
3932
4144
  const conflict = opts.transparent ? "--transparent" : opts.removeBackground ? "--remove-bg" : opts.editUrl ? "--edit" : opts.cleanUrl ? "--clean" : opts.upscaleUrl ? "--upscale" : opts.noWait ? "--no-wait" : void 0;
@@ -4264,7 +4476,10 @@ var KIND_WAIT_TIMEOUT_MS = {
4264
4476
  character_concept: 15 * 60 * 1e3,
4265
4477
  character_preview: 30 * 60 * 1e3,
4266
4478
  character: 30 * 60 * 1e3,
4267
- character_animation: 20 * 60 * 1e3
4479
+ character_animation: 20 * 60 * 1e3,
4480
+ // Per clip this lane is fast (locomotion ~2s, video ~51s, described ~130s) —
4481
+ // the BATCH is what takes time. A 20-clip standard set is the sizing case.
4482
+ character_motion: 30 * 60 * 1e3
4268
4483
  };
4269
4484
  var waitTimeoutFor = (kind) => KIND_WAIT_TIMEOUT_MS[kind] ?? WAIT_TIMEOUT_MS;
4270
4485
  var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed"]);
@@ -4380,7 +4595,8 @@ function printHint(kind, view, files, log) {
4380
4595
  character_concept: "Show all three concept candidates to the user and wait for an explicit selection.",
4381
4596
  character_preview: "Show all four preview views to the user and wait for explicit approval before finalizing.",
4382
4597
  character: `Install the controller and current manifest with genex controller character --character ${view.id}.`,
4383
- character_animation: "The action is now part of the character's current manifest; refresh the local controller manifest before testing."
4598
+ character_animation: "The action is now part of the character's current manifest; refresh the local controller manifest before testing.",
4599
+ character_motion: `The clips are part of the character's current manifest \u2014 install them with genex controller character --character ${view.id}, then play the game and watch the motion on the real character before calling it done.`
4384
4600
  };
4385
4601
  log.dim(` ${hint[kind]}`);
4386
4602
  log.dim(" Reference the URL directly in your code \u2014 don't download it into the repo.");
@@ -4660,6 +4876,14 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
4660
4876
  );
4661
4877
  }
4662
4878
  }
4879
+ if (!/player character:/i.test(design)) {
4880
+ const hasCharacter = await fs14.access(path14.join(cwd, "public", "assets", "meshy-character.json")).then(() => true, () => false);
4881
+ if (!hasCharacter && await loadsPlayerBody(cwd)) {
4882
+ warnings.push(
4883
+ `Player character is the stock avatar \u2014 no generated character is wired. The game's own generated character is the player's body wherever a human body appears on screen, first-person included (genex-ai-character). Generate it, or record "Player character: VRM \u2014 <reason>" in DESIGN.md (no human body in this game / out of credits / player declined).`
4884
+ );
4885
+ }
4886
+ }
4663
4887
  for (const w of warnings) log.warn(w);
4664
4888
  } catch {
4665
4889
  }
@@ -4687,6 +4911,21 @@ async function hasHudLikeDom(cwd) {
4687
4911
  }
4688
4912
  return false;
4689
4913
  }
4914
+ async function loadsPlayerBody(cwd) {
4915
+ const BODY_LOADERS = /\bloadPlayerCharacter\s*\(|\bloadVrm(?:Clone)?\s*\(/;
4916
+ try {
4917
+ const entries = await fs14.readdir(path14.join(cwd, "src"), { recursive: true });
4918
+ for (const rel of entries) {
4919
+ if (rel.includes("node_modules")) continue;
4920
+ if (rel.split(path14.sep)[0] === "controllers" || rel.startsWith("controllers/")) continue;
4921
+ if (!/\.(ts|tsx|js|jsx)$/.test(rel)) continue;
4922
+ const text = await fs14.readFile(path14.join(cwd, "src", rel), "utf8").catch(() => "");
4923
+ if (BODY_LOADERS.test(text)) return true;
4924
+ }
4925
+ } catch {
4926
+ }
4927
+ return false;
4928
+ }
4690
4929
  async function hasExtractionSidecars(cwd) {
4691
4930
  try {
4692
4931
  const entries = await fs14.readdir(path14.join(cwd, "public"), { recursive: true });
@@ -15372,9 +15611,14 @@ var CONTROLLER_FILE_SETS = {
15372
15611
  "character/animation-packs.ts",
15373
15612
  "character/motion-actions.ts",
15374
15613
  "character/meshy/meshy-loader.ts",
15614
+ // The player's body, resolved once: this game's own generated character
15615
+ // when it has one, the visiting player's avatar when it doesn't. ONE
15616
+ // shape for both lanes, so a character that lands at hour 2 is a file
15617
+ // drop and not a rewrite of the boot path written at hour 0.
15618
+ "character/player-character.ts",
15375
15619
  "character/presets.ts",
15376
15620
  // VRM avatar support (three-vrm): load + retarget the UAL clips + auto-fit
15377
- // the capsule + optional foot IK. Owner's avatar replaces the old mannequin.
15621
+ // the capsule + optional foot IK the FALLBACK body's lane.
15378
15622
  "character/vrm/vrm-loader.ts",
15379
15623
  "character/vrm/vrm-retarget.ts",
15380
15624
  "character/vrm/capsule-fit.ts",
@@ -15383,22 +15627,27 @@ var CONTROLLER_FILE_SETS = {
15383
15627
  NOTICE
15384
15628
  ],
15385
15629
  // The FALLBACK VRM is written to public/assets/avatar.vrm at install time by
15386
- // installOwnerAvatar (owner's avatar, or the bundled default) — so it is NOT
15387
- // a static manifest asset; at runtime the game loads the playing user's own
15388
- // avatar (user.avatarUrl from the embed identity, AG-804) and this file
15389
- // covers local dev + load failures. animation-library.glb (the 12-clip core)
15390
- // still is a manifest asset; extra clips arrive via `genex controller anims`
15391
- // into public/assets/anims/.
15630
+ // installFallbackAvatar (the bundled CC0 default) — so it is NOT a static
15631
+ // manifest asset; at runtime the game loads the playing user's own avatar
15632
+ // (user.avatarUrl from the embed identity, AG-804) and this file covers
15633
+ // local dev + load failures. It is only ever the FALLBACK body: the player
15634
+ // is this game's own generated character whenever it has one.
15635
+ // animation-library.glb (the 12-clip core) still is a manifest asset; extra
15636
+ // clips arrive via `genex controller anims` into public/assets/anims/.
15392
15637
  assets: ["assets/animation-library.glb"],
15393
15638
  skill: "genex-threejs-character-controller",
15639
+ // ONE sketch, with or without --character. The lane is a runtime decision
15640
+ // inside loadPlayerCharacter, never a code edit the agent has to remember
15641
+ // to make once the generated character lands.
15394
15642
  sketch: [
15395
15643
  `const physics = await PhysicsWorld.create();`,
15396
- `const { user } = await waitForPlayer(); // @genex-ai/embed-sdk \u2014 the player's own avatar`,
15397
- `const { scene, vrm } = await loadVrm(user.avatarUrl ?? "./assets/avatar.vrm").catch(() => loadVrm("./assets/avatar.vrm"));`,
15398
- `const clips = await loadCharacterClips(vrm); // core library + every genex-controller-anims pack`,
15399
- `const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...capsuleFromModel(scene), position: { x: 0, y: 2, z: 0 } });`,
15400
- `character.root.add(scene); const anims = new CharacterAnimations(scene, clips);`,
15401
- `addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // per frame: anims.update(character, dt); vrm.update(dt);`
15644
+ `const { user } = await waitForPlayer(); // @genex-ai/embed-sdk`,
15645
+ `const player = await loadPlayerCharacter({ avatarUrl: user.avatarUrl }); // this game's character, else the avatar fallback`,
15646
+ `const fit = capsuleFromModel(player.scene);`,
15647
+ `const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...fit, position: { x: 0, y: 2, z: 0 } });`,
15648
+ `character.root.add(player.scene); player.scene.position.y = fit.modelOffsetY;`,
15649
+ `const anims = new CharacterAnimations(player.scene, player.clips, { locomotionProfile: player.locomotionProfile });`,
15650
+ `addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // per frame: anims.update(character, dt); player.update(dt);`
15402
15651
  ]
15403
15652
  },
15404
15653
  car: {
@@ -15557,8 +15806,7 @@ async function runController(opts) {
15557
15806
  return;
15558
15807
  }
15559
15808
  } else {
15560
- const token = opts.token !== void 0 ? opts.token : await readUserToken();
15561
- await installOwnerAvatar({ root, srcDir, apiUrl: getApiUrl(opts.apiUrl), token, log });
15809
+ await installFallbackAvatar({ root, srcDir, log });
15562
15810
  }
15563
15811
  log.plain("");
15564
15812
  }
@@ -15577,16 +15825,18 @@ async function runController(opts) {
15577
15825
  log.plain(
15578
15826
  kind === "touch" ? ` ${stepOffset + 2}. Wiring sketch (create behind a touch check; read per frame):` : ` ${stepOffset + 2}. Wiring sketch (controllers update BEFORE the physics step):`
15579
15827
  );
15580
- const sketch = kind === "character" && opts.character ? [
15581
- `const physics = await PhysicsWorld.create();`,
15582
- `const native = await loadMeshyCharacter("./assets/meshy-character.json");`,
15583
- `const fit = capsuleFromModel(native.scene); const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...fit, position: { x: 0, y: 2, z: 0 } });`,
15584
- `character.root.add(native.scene); const anims = new CharacterAnimations(native.scene, native.clips, { locomotionProfile: native.locomotionProfile });`,
15585
- `physics.onBeforeStep(() => character.update(dt, input)); // Rapier owns movement; then anims.update(character, dt)`
15586
- ] : set.sketch;
15587
- for (const line of sketch) {
15828
+ for (const line of set.sketch) {
15588
15829
  log.dim(` ${line}`);
15589
15830
  }
15831
+ if (kind === "character" && !await exists3(path17.join(root, ASSETS_DEST, "meshy-character.json"))) {
15832
+ log.plain("");
15833
+ log.plain(
15834
+ ` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
15835
+ 'npx genex character "<look>"'
15836
+ )} makes the player's real body;`
15837
+ );
15838
+ log.plain(" the avatar above is only the fallback until it lands. No code change when it does.");
15839
+ }
15590
15840
  }
15591
15841
  async function installMeshyCharacterManifest(args) {
15592
15842
  const response = await apiFetch(
@@ -15618,6 +15868,7 @@ async function installMeshyCharacterManifest(args) {
15618
15868
  `
15619
15869
  );
15620
15870
  args.log.dim(` public/assets/meshy-character.json (${args.characterId}, current revision)`);
15871
+ args.log.success("This game's generated character is now the player's body \u2014 picked up on the next reload, no code change needed.");
15621
15872
  const pack2 = manifest.controllerPack;
15622
15873
  if (typeof pack2?.key === "string" && typeof pack2.version === "number") {
15623
15874
  args.log.success(`Meshy controller pack ${pack2.key} v${pack2.version}`);
@@ -15747,36 +15998,12 @@ function assertCompleteMeshyControllerPack(manifest) {
15747
15998
  incompletePack(`${pack2.key} v${pack2.version} fingerprint does not match its action bindings.`);
15748
15999
  }
15749
16000
  }
15750
- async function installOwnerAvatar(args) {
15751
- const { root, srcDir, apiUrl, token, log } = args;
16001
+ async function installFallbackAvatar(args) {
16002
+ const { root, srcDir, log } = args;
15752
16003
  const dest = path17.join(root, ASSETS_DEST, "avatar.vrm");
15753
16004
  await fs17.mkdir(path17.dirname(dest), { recursive: true });
15754
- if (token) {
15755
- try {
15756
- const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
15757
- headers: { Authorization: `Bearer ${token}` }
15758
- });
15759
- if (res.ok) {
15760
- const me = await res.json();
15761
- if (me.vrmUrl) {
15762
- const vrmRes = await fetch(me.vrmUrl);
15763
- if (vrmRes.ok) {
15764
- const buf = Buffer.from(await vrmRes.arrayBuffer());
15765
- await fs17.writeFile(dest, buf);
15766
- log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
15767
- return;
15768
- }
15769
- }
15770
- }
15771
- log.dim(" couldn't fetch your avatar; using the bundled default.");
15772
- } catch {
15773
- log.dim(" avatar fetch failed (offline?); using the bundled default.");
15774
- }
15775
- }
15776
16005
  await fs17.copyFile(path17.join(srcDir, "assets", "default-avatar.vrm"), dest);
15777
- log.dim(
15778
- token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
15779
- );
16006
+ log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
15780
16007
  }
15781
16008
  async function exists3(p) {
15782
16009
  try {
@@ -16263,6 +16490,226 @@ async function runCharacterAnimate(opts) {
16263
16490
  });
16264
16491
  }
16265
16492
 
16493
+ // src/commands/character-motion.ts
16494
+ import { readFile } from "fs/promises";
16495
+ import { basename } from "path";
16496
+ var VIDEO_SECONDS = [3, 4, 5, 6];
16497
+ var MAX_VIDEO_BYTES = 200 * 1024 * 1024;
16498
+ async function context2(opts) {
16499
+ const token = opts.token ?? await readUserToken(opts.envPath);
16500
+ if (!token) return null;
16501
+ const project = await readProject();
16502
+ return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
16503
+ }
16504
+ async function readVideo(path19, log) {
16505
+ let bytes;
16506
+ try {
16507
+ bytes = await readFile(path19);
16508
+ } catch {
16509
+ log.error(`Can't read ${path19}.`);
16510
+ return null;
16511
+ }
16512
+ if (bytes.byteLength > MAX_VIDEO_BYTES) {
16513
+ log.error(`${basename(path19)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
16514
+ return null;
16515
+ }
16516
+ return bytes;
16517
+ }
16518
+ async function uploadVideo(apiUrl, token, characterId, path19, bytes, log) {
16519
+ const contentType = /\.mov$/i.test(path19) ? "video/quicktime" : "video/mp4";
16520
+ const minted = await apiFetch(
16521
+ `${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
16522
+ {
16523
+ method: "POST",
16524
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
16525
+ body: JSON.stringify({ bytes: bytes.byteLength, contentType })
16526
+ }
16527
+ );
16528
+ if (printedStructuredError(minted)) return null;
16529
+ if (!minted.ok) {
16530
+ log.error(`Couldn't start the video upload (HTTP ${minted.status}).`);
16531
+ return null;
16532
+ }
16533
+ const { uploadUrl, videoUrl } = await minted.json();
16534
+ log.dim(` uploading ${basename(path19)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
16535
+ const put = await fetch(uploadUrl, {
16536
+ method: "PUT",
16537
+ headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
16538
+ body: new Uint8Array(bytes)
16539
+ });
16540
+ if (!put.ok) {
16541
+ log.error(`Video upload failed (HTTP ${put.status}).`);
16542
+ return null;
16543
+ }
16544
+ return videoUrl;
16545
+ }
16546
+ function printPlan(plan, log, creature) {
16547
+ log.plain(c.bold(creature ? "Creature animation plan" : "Animation plan"));
16548
+ for (const item of plan.items) {
16549
+ const count = item.clips > 1 ? ` (${item.clips} clips)` : "";
16550
+ log.plain(` \xB7 ${c.bold(item.request)}${count} \u2014 ${item.explanation}`);
16551
+ if (item.supersededSlots.length > 0) {
16552
+ log.plain(
16553
+ ` ${c.dim(`${item.supersededSlots.join(", ")} stay on the reviewed pack clips \u2014 those slots are pinned.`)}`
16554
+ );
16555
+ }
16556
+ }
16557
+ if (plan.secondsApplied === false) {
16558
+ log.plain(
16559
+ ` ${c.dim("--duration sets the reference-video length; none of these verbs took the video route, so it did nothing.")}`
16560
+ );
16561
+ }
16562
+ log.plain("");
16563
+ if (plan.credits > 0) log.dim(` ${plan.totalClips} clips \xB7 ${plan.credits} credits`);
16564
+ else log.dim(` ${plan.totalClips} clips`);
16565
+ log.plain("");
16566
+ }
16567
+ async function runCharacterAnimateMotion(opts) {
16568
+ const log = createLogger({ quiet: opts.quiet || opts.json });
16569
+ const characterId = opts.characterId?.trim();
16570
+ if (!characterId) {
16571
+ log.error("Missing character id.");
16572
+ process.exitCode = 1;
16573
+ return;
16574
+ }
16575
+ const verbs = (opts.verbs ?? []).map((v) => v.trim()).filter(Boolean);
16576
+ if (verbs.length === 0 && !opts.locomotion) {
16577
+ log.error(
16578
+ `Say what the character should do. ${c.cyan(`genex character animate ${characterId} "overhead slam"`)}`
16579
+ );
16580
+ log.dim(` Or generate the movement set: ${c.cyan(`genex character animate ${characterId} --locomotion`)}`);
16581
+ process.exitCode = 1;
16582
+ return;
16583
+ }
16584
+ if (opts.video && verbs.length !== 1) {
16585
+ log.error("--video applies to exactly one verb \u2014 name the action the footage shows.");
16586
+ process.exitCode = 1;
16587
+ return;
16588
+ }
16589
+ if (opts.duration !== void 0 && !VIDEO_SECONDS.includes(opts.duration)) {
16590
+ log.error(`--duration must be one of ${VIDEO_SECONDS.join(", ")} seconds (got ${opts.duration}).`);
16591
+ process.exitCode = 1;
16592
+ return;
16593
+ }
16594
+ if (opts.duration !== void 0 && opts.video) {
16595
+ log.error("--duration sets the length of a GENERATED reference video \u2014 it cannot change footage you supplied.");
16596
+ process.exitCode = 1;
16597
+ return;
16598
+ }
16599
+ let videoBytes;
16600
+ if (opts.video) {
16601
+ const read = await readVideo(opts.video, log);
16602
+ if (!read) {
16603
+ process.exitCode = 1;
16604
+ return;
16605
+ }
16606
+ videoBytes = read;
16607
+ }
16608
+ const ctx = await context2(opts);
16609
+ if (!ctx) {
16610
+ log.error("Not authorized. Run `genex init` first to sign in.");
16611
+ process.exitCode = 1;
16612
+ return;
16613
+ }
16614
+ let videoUrl;
16615
+ if (opts.video && videoBytes) {
16616
+ const uploaded = await uploadVideo(ctx.apiUrl, ctx.token, characterId, opts.video, videoBytes, log);
16617
+ if (!uploaded) {
16618
+ process.exitCode = 1;
16619
+ return;
16620
+ }
16621
+ videoUrl = uploaded;
16622
+ }
16623
+ const response = await apiFetch(
16624
+ `${ctx.apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/plan`,
16625
+ {
16626
+ method: "POST",
16627
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${ctx.token}` },
16628
+ body: JSON.stringify({
16629
+ verbs,
16630
+ locomotion: Boolean(opts.locomotion),
16631
+ lean: Boolean(opts.lean),
16632
+ ...videoUrl ? { videoUrl } : {},
16633
+ ...opts.duration === void 0 ? {} : { seconds: opts.duration }
16634
+ })
16635
+ }
16636
+ );
16637
+ if (printedStructuredError(response)) {
16638
+ process.exitCode = 1;
16639
+ return;
16640
+ }
16641
+ if (!response.ok) {
16642
+ const body = await response.json().catch(() => ({}));
16643
+ log.error(body.message ?? `Couldn't plan those animations (HTTP ${response.status}).`);
16644
+ process.exitCode = 1;
16645
+ return;
16646
+ }
16647
+ const planned = await response.json();
16648
+ if (opts.json) {
16649
+ writeJson({ kind: "character_motion", stage: "plan", ...planned.plan });
16650
+ } else {
16651
+ printPlan(planned.plan, log, Boolean(opts.creature));
16652
+ }
16653
+ await runGenerate("character_motion", {
16654
+ ...opts,
16655
+ prompt: verbs.length > 0 ? `Animate character ${characterId}: ${verbs.join(", ")}` : `Generate the locomotion set for character ${characterId}`,
16656
+ token: ctx.token,
16657
+ apiUrl: ctx.apiUrl,
16658
+ generationOptions: planned.request.options
16659
+ });
16660
+ }
16661
+ async function runCharacterMotions(opts) {
16662
+ const log = createLogger({ quiet: opts.quiet || opts.json });
16663
+ const characterId = opts.characterId?.trim();
16664
+ if (!characterId) {
16665
+ log.error("Missing character id.");
16666
+ process.exitCode = 1;
16667
+ return;
16668
+ }
16669
+ const ctx = await context2(opts);
16670
+ if (!ctx) {
16671
+ log.error("Not authorized. Run `genex init` first to sign in.");
16672
+ process.exitCode = 1;
16673
+ return;
16674
+ }
16675
+ const response = await apiFetch(
16676
+ `${ctx.apiUrl}/api/characters/${encodeURIComponent(characterId)}/manifest`,
16677
+ { headers: { Authorization: `Bearer ${ctx.token}` } }
16678
+ );
16679
+ if (printedStructuredError(response)) {
16680
+ process.exitCode = 1;
16681
+ return;
16682
+ }
16683
+ if (!response.ok) {
16684
+ log.error(`Couldn't read that character (HTTP ${response.status}).`);
16685
+ process.exitCode = 1;
16686
+ return;
16687
+ }
16688
+ const { manifest } = await response.json();
16689
+ if (opts.json) {
16690
+ writeJson({
16691
+ kind: "character_motion",
16692
+ stage: "list",
16693
+ characterId,
16694
+ clips: manifest.clips.map((clip) => ({
16695
+ key: clip.key,
16696
+ name: clip.name,
16697
+ provider: clip.provider ?? "meshy",
16698
+ duration: clip.duration,
16699
+ slots: Object.entries(manifest.locomotion.slots).filter(([, value]) => value === clip.key).map(([slot]) => slot)
16700
+ }))
16701
+ });
16702
+ return;
16703
+ }
16704
+ log.plain(c.bold(`${manifest.clips.length} clips on ${characterId}`));
16705
+ for (const clip of manifest.clips) {
16706
+ const slots = Object.entries(manifest.locomotion.slots).filter(([, value]) => value === clip.key).map(([slot]) => slot);
16707
+ const where = slots.length > 0 ? ` \u2192 ${slots.join(", ")}` : "";
16708
+ const source = clip.provider === "uthana" ? c.dim(" generated") : "";
16709
+ log.plain(` ${clip.key}${source}${where}`);
16710
+ }
16711
+ }
16712
+
16266
16713
  // src/commands/animations.ts
16267
16714
  function animationSearchView(entry) {
16268
16715
  return {
@@ -17978,20 +18425,26 @@ ${c.bold("Usage")}
17978
18425
  genex character finalize <preview-id>
17979
18426
  Finalize an approved preview as a 10,000-face
17980
18427
  controller-ready Meshy character.
17981
- genex character animate <id> Add Meshy library actions to that character.
18428
+ genex character animate <id> "<verb>" \u2026
18429
+ Animate the character: say what it should DO,
18430
+ in plain words ("overhead slam"), one clip per
18431
+ verb. The platform picks how each is made and
18432
+ prints the plan before anything is charged.
18433
+ --locomotion generates the 8-way walk + run set;
18434
+ --video ./clip.mp4 uses your own footage;
18435
+ --duration 3-6 lengthens the reference video for
18436
+ multi-beat moves; --action <id-or-query> still
18437
+ picks a catalog clip.
18438
+ genex character motions <id> List the clips installed on that character.
17982
18439
  genex creature "<desc>" One-shot rigged enemy/creature (biped-shaped
17983
18440
  bodies only \u2014 Meshy rig limit): model \u2192 rig \u2192
17984
18441
  bind library clips via --animation. No approval
17985
18442
  ceremony, no player controller pack.
18443
+ genex creature animate <id> "<verb>" \u2026
18444
+ Same as character animate, for an enemy \u2014 a boss's
18445
+ signature move. --lean keeps movement to forward
18446
+ walk + run (enemies follow a path).
17986
18447
  genex animations search "<intent>" Search Meshy's animation library by gameplay intent.
17987
- genex motion <sub> [args] Custom character animations from text (ARDY):
17988
- gen "<prompt>" (GPU service, bills per take) |
17989
- verify <takes\u2026> | compile <takes\u2026> --out \u2026 |
17990
- install [--set rifle] | constraints \u2014 the
17991
- local toolkit + vendored runtime the agent
17992
- owns. Catalog actions (genex animations
17993
- search) stay the first stop; motion covers
17994
- the verbs the catalog lacks.
17995
18448
  genex wait <id> Attach to a generation enqueued with --no-wait and
17996
18449
  print its asset URL(s) when it finishes. Safe to
17997
18450
  re-run \u2014 it never creates a new generation.
@@ -18006,9 +18459,11 @@ ${c.bold("Usage")}
18006
18459
  without a physics controller. "quality" = the
18007
18460
  adaptive-quality kit (device tier, runtime governor,
18008
18461
  per-tier asset rungs) every game wires at boot.
18009
- Character defaults to the player's VRM + UAL pack.
18462
+ Character's player body is the game's own generated
18463
+ character, with a VRM avatar as the fallback.
18010
18464
  genex controller character --character <id>
18011
- Install the Meshy-native controller variant.
18465
+ Point the game at its generated character (writes the
18466
+ manifest; no code change \u2014 the loader picks it up).
18012
18467
  genex controller anims <sel \u2026> Install legacy VRM + UAL animation clips by tag/name
18013
18468
  into public/assets/anims/. --list shows the catalog;
18014
18469
  --reset forgets the previous selection first.
@@ -18020,8 +18475,13 @@ ${c.bold("Usage")}
18020
18475
  ${c.bold("Options for the generators (`model` `skybox` `sfx` `music` `voice` `texture` `image` `video`)")}
18021
18476
  --terrain (texture) seamless tiling surface for terrain/ground.
18022
18477
  --raw (skybox) send the prompt exactly as written \u2014 skip the
18023
- appended environment-only guard.
18024
- --duration <sec> (sfx, video, music) target clip length in seconds
18478
+ appended environment-only clause AND the refusal on
18479
+ prompts that name structures/objects (a sky is sky only;
18480
+ landforms and weather always pass).
18481
+ --duration <sec> (sfx, video, music) target clip length in seconds;
18482
+ (character/creature animate) 3-6 s of reference video for
18483
+ verbs that route to AI video (default 3) \u2014 longer gives a
18484
+ multi-beat move room to land instead of rushing it.
18025
18485
  (music: 10-300, default 90 \u2014 a looping gameplay track).
18026
18486
  --transparent (image) transparent background (alpha) \u2014 for decals/stickers.
18027
18487
  --glass (image) magenta-key glass lane: generate the panel on a
@@ -18136,15 +18596,13 @@ ${c.bold("Options for `character` / `animations search`")}
18136
18596
  controller slots, requirements, review status, and estimated cost.
18137
18597
 
18138
18598
  ${c.bold("Options for `controller character` / legacy `controller anims`")}
18139
- --character <id> (character) install the owned Meshy character manifest and
18140
- same-rig native loader instead of the player's VRM.
18599
+ --character <id> (character) install the owned Meshy character manifest \u2014 the
18600
+ game's own generated character becomes the player's body.
18141
18601
  --list [selectors] (anims) show the bundled VRM + UAL tag/name catalog.
18142
18602
  --reset (anims) forget the previous game-local UAL selection.
18143
18603
 
18144
18604
  ${UI_USAGE}
18145
18605
 
18146
- ${MOTION_USAGE}
18147
-
18148
18606
  ${c.bold("Global")}
18149
18607
  --quiet Reduce output.
18150
18608
  -h, --help Show this help.
@@ -18188,15 +18646,17 @@ ${c.bold("Examples")}
18188
18646
  genex character finalize <preview-id> --user-approved --approve-remesh 10000 --animation 466
18189
18647
  genex character "stylized sci-fi courier" --direct-text --no-wait
18190
18648
  genex character animate <character-id> --action <action-id>
18649
+ genex character animate <character-id> "overhead slam" "parry and recover" --no-wait
18650
+ genex character animate <character-id> --locomotion --no-wait
18651
+ genex character animate <character-id> "leap the gap and roll on landing" --duration 6
18652
+ genex character animate <character-id> "victory pose" --video ./take-3.mp4
18653
+ genex character motions <character-id>
18191
18654
  genex creature "hulking bone seraph, upright stance" --animation <walk-id> --animation <attack-id> --no-wait
18655
+ genex creature animate <creature-id> "wind-up ground pound" --no-wait
18192
18656
  genex controller character --character <character-id>
18193
18657
  genex controller anims sword pistol
18194
18658
  genex controller touch
18195
18659
  genex controller networked-physics
18196
- genex motion gen "soldier rifle low-ready idle, fully upright, head held high" --takes 4 --no-wait
18197
- genex motion verify takes/ --json
18198
- genex motion compile takes/rifle-idle.npz takes/rifle-run-forward.npz --out src/motion/rifle.json
18199
- genex motion install --set rifle
18200
18660
  `;
18201
18661
  function parseArgs(argv) {
18202
18662
  const parsed = {
@@ -18266,7 +18726,8 @@ function parseArgs(argv) {
18266
18726
  "--gates",
18267
18727
  "--config",
18268
18728
  "--set",
18269
- "--speed"
18729
+ "--speed",
18730
+ "--video"
18270
18731
  ]);
18271
18732
  let i = 0;
18272
18733
  while (i < argv.length) {
@@ -18302,6 +18763,12 @@ function parseArgs(argv) {
18302
18763
  case "--direct-text":
18303
18764
  parsed.options.directText = true;
18304
18765
  break;
18766
+ case "--locomotion":
18767
+ parsed.options.locomotion = true;
18768
+ break;
18769
+ case "--lean":
18770
+ parsed.options.lean = true;
18771
+ break;
18305
18772
  case "--in-place":
18306
18773
  parsed.options.inPlace = true;
18307
18774
  break;
@@ -18323,6 +18790,9 @@ function parseArgs(argv) {
18323
18790
  case "--no-defringe":
18324
18791
  parsed.options.defringe = false;
18325
18792
  break;
18793
+ case "--allow-speckle":
18794
+ parsed.options.allowSpeckle = true;
18795
+ break;
18326
18796
  case "--loop":
18327
18797
  parsed.options.loop = true;
18328
18798
  break;
@@ -18379,20 +18849,27 @@ function parseArgs(argv) {
18379
18849
  } else if (parsed.command === "controller") {
18380
18850
  (parsed.options.selectors ??= []).push(arg);
18381
18851
  } else if (parsed.command === "character") {
18382
- if (parsed.options.name === "animate" && !parsed.options.characterId) {
18852
+ if ((parsed.options.name === "animate" || parsed.options.name === "motions") && !parsed.options.characterId) {
18383
18853
  parsed.options.characterId = arg;
18854
+ } else if (parsed.options.name === "animate") {
18855
+ (parsed.options.verbs ??= []).push(arg);
18384
18856
  } else if (parsed.options.name === "preview" && !parsed.options.conceptId) {
18385
18857
  parsed.options.conceptId = arg;
18386
18858
  } else if (parsed.options.name === "finalize" && !parsed.options.previewId) {
18387
18859
  parsed.options.previewId = arg;
18388
- } else if (!["animate", "preview", "finalize"].includes(parsed.options.name ?? "")) {
18860
+ } else if (!["animate", "motions", "preview", "finalize"].includes(parsed.options.name ?? "")) {
18389
18861
  parsed.options.name = `${parsed.options.name} ${arg}`;
18390
18862
  } else {
18391
18863
  parsed.error = `Unexpected argument: ${arg}`;
18392
18864
  return parsed;
18393
18865
  }
18394
18866
  } else if (parsed.command === "creature") {
18395
- parsed.options.name = `${parsed.options.name} ${arg}`;
18867
+ if (parsed.options.name === "animate") {
18868
+ if (!parsed.options.characterId) parsed.options.characterId = arg;
18869
+ else (parsed.options.verbs ??= []).push(arg);
18870
+ } else {
18871
+ parsed.options.name = `${parsed.options.name} ${arg}`;
18872
+ }
18396
18873
  } else if (parsed.command === "motion") {
18397
18874
  if (parsed.options.name === "gen") {
18398
18875
  parsed.options.query = parsed.options.query ? `${parsed.options.query} ${arg}` : arg;
@@ -18630,6 +19107,9 @@ function applyValueFlag(options, flag, value) {
18630
19107
  case "--frame":
18631
19108
  options.frameUrl = value;
18632
19109
  break;
19110
+ case "--video":
19111
+ options.video = value;
19112
+ break;
18633
19113
  case "--first-frame":
18634
19114
  options.firstFrameUrl = value;
18635
19115
  break;
@@ -18729,7 +19209,13 @@ async function main() {
18729
19209
  break;
18730
19210
  case "character":
18731
19211
  if (parsed.options.name === "animate") {
18732
- await runCharacterAnimate(parsed.options);
19212
+ if ((parsed.options.actions?.length ?? 0) > 0) {
19213
+ await runCharacterAnimate(parsed.options);
19214
+ } else {
19215
+ await runCharacterAnimateMotion(parsed.options);
19216
+ }
19217
+ } else if (parsed.options.name === "motions") {
19218
+ await runCharacterMotions(parsed.options);
18733
19219
  } else if (parsed.options.name === "preview") {
18734
19220
  await runCharacterPreview(parsed.options);
18735
19221
  } else if (parsed.options.name === "finalize") {
@@ -18739,6 +19225,10 @@ async function main() {
18739
19225
  }
18740
19226
  break;
18741
19227
  case "creature":
19228
+ if (parsed.options.name === "animate") {
19229
+ await runCharacterAnimateMotion({ ...parsed.options, creature: true });
19230
+ break;
19231
+ }
18742
19232
  if (!parsed.options.json && !parsed.options.quiet) {
18743
19233
  log.dim(
18744
19234
  " Biped-shaped creatures only (Meshy rig limit) \u2014 for true quadrupeds/fliers use a static model + the genex-threejs-creatures procedural recipes."
@@ -18771,6 +19261,23 @@ async function main() {
18771
19261
  case "ui":
18772
19262
  await runUi({ ...parsed.options, sub: parsed.options.name });
18773
19263
  break;
19264
+ // PARKED (AG-824 D12) — still dispatches, deliberately unlisted in --help
19265
+ // and gone from the skills, the same way `genex explore` is parked.
19266
+ //
19267
+ // Why: `genex character animate` replaced it. That lane generates motion
19268
+ // that binds to the character's OWN rig with no retargeting (measured: the
19269
+ // provider returns our exact skeleton), which is what the ~1,700 lines of
19270
+ // local retargeting behind this command existed to solve.
19271
+ //
19272
+ // Everything under it is intact and dormant: src/commands/motion.ts,
19273
+ // src/lib/motion/**, templates/motion/**, the API's runpod-motion provider
19274
+ // and its `motion` kind, and the RunPod worker (idle cost measured at $0).
19275
+ //
19276
+ // To re-enable: restore MOTION_USAGE + the examples in this file and the
19277
+ // skill cross-links. The trap that already cost time —
19278
+ // RUNPOD_MOTION_ENDPOINT_ID must be a GitHub Environment `vars.` entry,
19279
+ // NOT a secret of the same name: a same-named secret upserts EMPTY, and
19280
+ // the provider then silently falls back to the mock with no error anywhere.
18774
19281
  case "motion":
18775
19282
  await runMotion({ ...parsed.options, sub: parsed.options.name });
18776
19283
  break;