@genex-ai/cli-demo 0.91.0-dev.228 → 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.
@@ -3536,7 +3536,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
3536
3536
  } catch {
3537
3537
  }
3538
3538
  }
3539
- const referenced = (basename) => sources.some((s) => s.text.includes(basename));
3539
+ const referenced = (basename2) => sources.some((s) => s.text.includes(basename2));
3540
3540
  const findings = [...viewportFindings];
3541
3541
  const assetFiles = await walkFiles(absAssets);
3542
3542
  const metaByName = /* @__PURE__ */ new Map();
@@ -4476,7 +4476,10 @@ var KIND_WAIT_TIMEOUT_MS = {
4476
4476
  character_concept: 15 * 60 * 1e3,
4477
4477
  character_preview: 30 * 60 * 1e3,
4478
4478
  character: 30 * 60 * 1e3,
4479
- 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
4480
4483
  };
4481
4484
  var waitTimeoutFor = (kind) => KIND_WAIT_TIMEOUT_MS[kind] ?? WAIT_TIMEOUT_MS;
4482
4485
  var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed"]);
@@ -4592,7 +4595,8 @@ function printHint(kind, view, files, log) {
4592
4595
  character_concept: "Show all three concept candidates to the user and wait for an explicit selection.",
4593
4596
  character_preview: "Show all four preview views to the user and wait for explicit approval before finalizing.",
4594
4597
  character: `Install the controller and current manifest with genex controller character --character ${view.id}.`,
4595
- 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.`
4596
4600
  };
4597
4601
  log.dim(` ${hint[kind]}`);
4598
4602
  log.dim(" Reference the URL directly in your code \u2014 don't download it into the repo.");
@@ -16486,6 +16490,226 @@ async function runCharacterAnimate(opts) {
16486
16490
  });
16487
16491
  }
16488
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
+
16489
16713
  // src/commands/animations.ts
16490
16714
  function animationSearchView(entry) {
16491
16715
  return {
@@ -18201,20 +18425,26 @@ ${c.bold("Usage")}
18201
18425
  genex character finalize <preview-id>
18202
18426
  Finalize an approved preview as a 10,000-face
18203
18427
  controller-ready Meshy character.
18204
- 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.
18205
18439
  genex creature "<desc>" One-shot rigged enemy/creature (biped-shaped
18206
18440
  bodies only \u2014 Meshy rig limit): model \u2192 rig \u2192
18207
18441
  bind library clips via --animation. No approval
18208
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).
18209
18447
  genex animations search "<intent>" Search Meshy's animation library by gameplay intent.
18210
- genex motion <sub> [args] Custom character animations from text (ARDY):
18211
- gen "<prompt>" (GPU service, bills per take) |
18212
- verify <takes\u2026> | compile <takes\u2026> --out \u2026 |
18213
- install [--set rifle] | constraints \u2014 the
18214
- local toolkit + vendored runtime the agent
18215
- owns. Catalog actions (genex animations
18216
- search) stay the first stop; motion covers
18217
- the verbs the catalog lacks.
18218
18448
  genex wait <id> Attach to a generation enqueued with --no-wait and
18219
18449
  print its asset URL(s) when it finishes. Safe to
18220
18450
  re-run \u2014 it never creates a new generation.
@@ -18248,7 +18478,10 @@ ${c.bold("Options for the generators (`model` `skybox` `sfx` `music` `voice` `te
18248
18478
  appended environment-only clause AND the refusal on
18249
18479
  prompts that name structures/objects (a sky is sky only;
18250
18480
  landforms and weather always pass).
18251
- --duration <sec> (sfx, video, music) target clip length in seconds
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.
18252
18485
  (music: 10-300, default 90 \u2014 a looping gameplay track).
18253
18486
  --transparent (image) transparent background (alpha) \u2014 for decals/stickers.
18254
18487
  --glass (image) magenta-key glass lane: generate the panel on a
@@ -18370,8 +18603,6 @@ ${c.bold("Options for `controller character` / legacy `controller anims`")}
18370
18603
 
18371
18604
  ${UI_USAGE}
18372
18605
 
18373
- ${MOTION_USAGE}
18374
-
18375
18606
  ${c.bold("Global")}
18376
18607
  --quiet Reduce output.
18377
18608
  -h, --help Show this help.
@@ -18415,15 +18646,17 @@ ${c.bold("Examples")}
18415
18646
  genex character finalize <preview-id> --user-approved --approve-remesh 10000 --animation 466
18416
18647
  genex character "stylized sci-fi courier" --direct-text --no-wait
18417
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>
18418
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
18419
18656
  genex controller character --character <character-id>
18420
18657
  genex controller anims sword pistol
18421
18658
  genex controller touch
18422
18659
  genex controller networked-physics
18423
- genex motion gen "soldier rifle low-ready idle, fully upright, head held high" --takes 4 --no-wait
18424
- genex motion verify takes/ --json
18425
- genex motion compile takes/rifle-idle.npz takes/rifle-run-forward.npz --out src/motion/rifle.json
18426
- genex motion install --set rifle
18427
18660
  `;
18428
18661
  function parseArgs(argv) {
18429
18662
  const parsed = {
@@ -18493,7 +18726,8 @@ function parseArgs(argv) {
18493
18726
  "--gates",
18494
18727
  "--config",
18495
18728
  "--set",
18496
- "--speed"
18729
+ "--speed",
18730
+ "--video"
18497
18731
  ]);
18498
18732
  let i = 0;
18499
18733
  while (i < argv.length) {
@@ -18529,6 +18763,12 @@ function parseArgs(argv) {
18529
18763
  case "--direct-text":
18530
18764
  parsed.options.directText = true;
18531
18765
  break;
18766
+ case "--locomotion":
18767
+ parsed.options.locomotion = true;
18768
+ break;
18769
+ case "--lean":
18770
+ parsed.options.lean = true;
18771
+ break;
18532
18772
  case "--in-place":
18533
18773
  parsed.options.inPlace = true;
18534
18774
  break;
@@ -18609,20 +18849,27 @@ function parseArgs(argv) {
18609
18849
  } else if (parsed.command === "controller") {
18610
18850
  (parsed.options.selectors ??= []).push(arg);
18611
18851
  } else if (parsed.command === "character") {
18612
- if (parsed.options.name === "animate" && !parsed.options.characterId) {
18852
+ if ((parsed.options.name === "animate" || parsed.options.name === "motions") && !parsed.options.characterId) {
18613
18853
  parsed.options.characterId = arg;
18854
+ } else if (parsed.options.name === "animate") {
18855
+ (parsed.options.verbs ??= []).push(arg);
18614
18856
  } else if (parsed.options.name === "preview" && !parsed.options.conceptId) {
18615
18857
  parsed.options.conceptId = arg;
18616
18858
  } else if (parsed.options.name === "finalize" && !parsed.options.previewId) {
18617
18859
  parsed.options.previewId = arg;
18618
- } else if (!["animate", "preview", "finalize"].includes(parsed.options.name ?? "")) {
18860
+ } else if (!["animate", "motions", "preview", "finalize"].includes(parsed.options.name ?? "")) {
18619
18861
  parsed.options.name = `${parsed.options.name} ${arg}`;
18620
18862
  } else {
18621
18863
  parsed.error = `Unexpected argument: ${arg}`;
18622
18864
  return parsed;
18623
18865
  }
18624
18866
  } else if (parsed.command === "creature") {
18625
- 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
+ }
18626
18873
  } else if (parsed.command === "motion") {
18627
18874
  if (parsed.options.name === "gen") {
18628
18875
  parsed.options.query = parsed.options.query ? `${parsed.options.query} ${arg}` : arg;
@@ -18860,6 +19107,9 @@ function applyValueFlag(options, flag, value) {
18860
19107
  case "--frame":
18861
19108
  options.frameUrl = value;
18862
19109
  break;
19110
+ case "--video":
19111
+ options.video = value;
19112
+ break;
18863
19113
  case "--first-frame":
18864
19114
  options.firstFrameUrl = value;
18865
19115
  break;
@@ -18959,7 +19209,13 @@ async function main() {
18959
19209
  break;
18960
19210
  case "character":
18961
19211
  if (parsed.options.name === "animate") {
18962
- 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);
18963
19219
  } else if (parsed.options.name === "preview") {
18964
19220
  await runCharacterPreview(parsed.options);
18965
19221
  } else if (parsed.options.name === "finalize") {
@@ -18969,6 +19225,10 @@ async function main() {
18969
19225
  }
18970
19226
  break;
18971
19227
  case "creature":
19228
+ if (parsed.options.name === "animate") {
19229
+ await runCharacterAnimateMotion({ ...parsed.options, creature: true });
19230
+ break;
19231
+ }
18972
19232
  if (!parsed.options.json && !parsed.options.quiet) {
18973
19233
  log.dim(
18974
19234
  " Biped-shaped creatures only (Meshy rig limit) \u2014 for true quadrupeds/fliers use a static model + the genex-threejs-creatures procedural recipes."
@@ -19001,6 +19261,23 @@ async function main() {
19001
19261
  case "ui":
19002
19262
  await runUi({ ...parsed.options, sub: parsed.options.name });
19003
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.
19004
19281
  case "motion":
19005
19282
  await runMotion({ ...parsed.options, sub: parsed.options.name });
19006
19283
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.91.0-dev.228",
3
+ "version": "0.92.0-dev.229",
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": {
@@ -169,44 +169,64 @@ Catalog entries are metadata-reviewed. Inspect the preview before choosing a
169
169
  specialty action. Only provider-declared `InPlace` loops or measured overrides
170
170
  may fill locomotion slots automatically.
171
171
 
172
- ## Custom animations — `genex motion`
172
+ ## Custom animations — `genex character animate "<verb>"`
173
173
 
174
174
  Catalog search is ALWAYS the first stop — its stock actions cover most verbs
175
- with no GPU spend. When the catalog genuinely lacks the motion (a bespoke
176
- weapon-hold locomotion set, a signature move, a boss telegraph), `genex
177
- motion` generates it from text on the platform GPU service and compiles it
178
- locally into a rig-independent set:
175
+ with no spend. When the catalog genuinely lacks the motion (a signature move, a
176
+ boss telegraph, a full 8-way movement set), say what the character should DO in
177
+ plain words and the platform generates it for this character's own rig:
179
178
 
180
179
  ```bash
181
- npx genex motion gen "soldier rifle low-ready idle, fully upright, head held high" --takes 4 --no-wait
182
- npx genex motion verify takes/ # objective gates — local, free, reproducible
183
- npx genex motion compile takes/rifle-idle.npz --out src/motion/rifle.json
184
- npx genex motion install --set rifle # vendored runtime + the proven rifle starter set
180
+ npx genex character animate <character-id> "overhead slam" "parry and recover" --no-wait
181
+ npx genex character animate <character-id> --locomotion --no-wait
182
+ npx genex character animate <character-id> "victory pose" --video ./take-3.mp4
185
183
  ```
186
184
 
187
- Billing is per take (default 4 per request); everything after `gen` is local.
188
- Unlike catalog clips (exact-rig only), compiled motion sets retarget onto
189
- Meshy/Mixamo GLB rigs and VRM avatars alike the vendored `src/motion/rigs.js`
190
- carries both verified retarget formulas, and it is the game's file to tune.
191
- Read [references/motion.md](references/motion.md) BEFORE your first
192
- `motion gen`: it carries the prompt wording ladder, the take-naming contract
193
- that drives compilation, the rig limits (finger-less hands cannot curl around
194
- a grip), and the full wiring. Wording mistakes cost paid takes.
185
+ One clip per verb. You never choose the method the platform routes each verb
186
+ (reuse a shared-library motion, generate movement, act it out on video first, or
187
+ generate from a description) and prints the plan BEFORE anything is charged.
188
+
189
+ **Show the plan to the user and let them change it before you continue.** That
190
+ is the one stop in this lane; everything after it runs in the background while
191
+ you keep building.
192
+
193
+ Read [references/motion-generation.md](references/motion-generation.md) before
194
+ the first run: it carries what makes a good verb, which verbs this cannot do,
195
+ and the footage requirements for `--video`.
196
+
197
+ `--locomotion` generates the full 8-way walk + run set (16 clips) — the
198
+ directional slots the shipped controller resolves but the stock pack leaves
199
+ empty, so a strafe stops being a faked forward walk. For an enemy that just
200
+ follows a path, `--lean` keeps it to forward walk + run.
201
+
202
+ Animate an enemy the same way — `genex creature animate <id> "<verb>"`. A
203
+ creature is already a character underneath; the alias exists so you don't have
204
+ to know that.
195
205
 
196
206
  ## Add actions and refresh the game
197
207
 
198
208
  ```bash
199
209
  npx genex character animate <character-id> --action <action-id>
200
210
  npx genex character animate <character-id> --action <first-id> --action <second-id> --no-wait
211
+ npx genex character motions <character-id> # what is installed right now
201
212
  npx genex controller character --character <character-id>
202
213
  ```
203
214
 
215
+ `--action` picks an existing catalog clip; a free-text verb generates one. Both
216
+ land in the same manifest and play through the same state machine.
217
+
204
218
  Ambiguous text queries print ranked candidates instead of silently spending
205
219
  credits. Already-installed actions return without another debit. After an
206
220
  animation job completes, rerun `genex controller character --character <id>`
207
221
  to refresh `public/assets/meshy-character.json`; existing controller source is
208
222
  preserved unless `--force` is explicitly used.
209
223
 
224
+ Six slots belong to the reviewed controller pack and cannot be replaced —
225
+ `idle.default`, `walk.forward`, `run.forward`, `crouch.forward`, `crouch.idle`,
226
+ `jump.full`. A generated locomotion set fills the other fourteen directions; the
227
+ plan says so before you spend anything. A character built without the pack
228
+ (every `genex creature`) takes all sixteen.
229
+
210
230
  Meshy manifests bypass browser cache, so newly installed actions must work
211
231
  after preview without asking the player to disable cache.
212
232
 
@@ -0,0 +1,125 @@
1
+ # Generating character motion
2
+
3
+ `npx genex character animate <character-id> "<verb>"` — one clip per verb, on
4
+ this character's own rig. Read this before the first run; it is short, and the
5
+ first two sections are where the credits go.
6
+
7
+ ```bash
8
+ npx genex character animate <id> "overhead slam" --no-wait
9
+ npx genex character animate <id> --locomotion --no-wait
10
+ npx genex character animate <id> "leap the gap and roll on landing" --duration 6
11
+ npx genex character animate <id> "victory pose" --video ./take-3.mp4
12
+ npx genex character motions <id>
13
+ npx genex creature animate <id> "wind-up ground pound" --no-wait
14
+ ```
15
+
16
+ You do not choose how a verb is made. The platform prints a plan first — which
17
+ verbs it will reuse, generate, or act out on video, and one total. **Show the
18
+ plan to the user.** That is the single stop in this lane; after it, everything
19
+ runs in the background while you keep building. Collect it later with
20
+ `genex wait --all`.
21
+
22
+ ## Write the verb like a stage direction
23
+
24
+ The generator turns your words into a body performance, so name the BODY, not
25
+ the intent. "Overhead slam" is a motion; "attack" is a category.
26
+
27
+ | instead of | write |
28
+ | --- | --- |
29
+ | `attack` | `heavy two-handed overhead slam, ending in a low crouch` |
30
+ | `hurt` | `stagger back two steps from a chest impact, arms flailing` |
31
+ | `win` | `plant both feet, raise one fist overhead, chest out` |
32
+ | `die` | `drop to the knees, then fall forward onto the chest` |
33
+
34
+ Three things make a verb generate well:
35
+
36
+ - **Name the beats.** A wind-up, a committed main beat, a recovery. A motion with
37
+ one beat reads as a twitch.
38
+ - **Say where the weight goes.** "Sinks into the knees as it lands" is the single
39
+ most useful phrase you can add — without it you tend to get a bow from the
40
+ waist where you wanted a crouch.
41
+ - **Keep it one person, standing, on the floor.** No props that define the pose,
42
+ no partner, no furniture.
43
+
44
+ ## What this cannot do
45
+
46
+ Do not spend on these — pick a different approach instead:
47
+
48
+ - **Fingers and fine manipulation** — picking a lock, typing, a trigger squeeze,
49
+ threading a needle. Generated rigs have no finger bones, so the hand simply
50
+ will not do it. Fake it with a prop animation or a cut.
51
+ - **Two characters interacting** — a handshake, a grapple, a carry. Only one body
52
+ is generated. Animate each side separately and time them in code.
53
+ - **Anything a prop defines** — swinging on a rope, climbing a specific ladder,
54
+ sitting in a specific chair. The clip does not know your geometry; drive those
55
+ with `$genex-threejs-procedural-animation` instead.
56
+ - **Non-bipeds** — quadrupeds, fliers, blobs. See `$genex-threejs-creatures`;
57
+ those stay static models plus procedural motion.
58
+
59
+ ## Room to land the beats: `--duration`
60
+
61
+ Some verbs are made by generating a short reference video of a performer and
62
+ converting THAT into motion — the plan says which ones ("acted out on video").
63
+ The footage is a hard ceiling on the animation, and the default 3 seconds is a
64
+ tight fit for anything with more than one beat: a performer given too little
65
+ time rushes, and a rushed wind-up-slam-recover comes back as a bow.
66
+
67
+ `--duration 3|4|5|6` gives those verbs more room. Use it when the verb you wrote
68
+ genuinely has three beats — a leap with a landing roll, a stagger that recovers,
69
+ a combo. Leave it alone for a single action; longer is not better, it is just
70
+ longer, and it costs more.
71
+
72
+ It only reaches verbs the platform routes to video, and the plan tells you
73
+ whether it applied. It cannot change footage you supplied with `--video` —
74
+ that clip already has a length.
75
+
76
+ ## Movement: `--locomotion`
77
+
78
+ Generates the full 8-way walk + run set — 16 clips. This fills the directional
79
+ slots the shipped controller already resolves (`walk.forward-left`,
80
+ `run.back-right`, and so on) but that the stock pack leaves empty, so strafing
81
+ stops being a forward walk played sideways.
82
+
83
+ Six slots belong to the reviewed controller pack and cannot be replaced:
84
+ `idle.default`, `walk.forward`, `run.forward`, `crouch.forward`, `crouch.idle`,
85
+ `jump.full`. A generated set fills the other fourteen; the plan says which. A
86
+ character built without the pack — every `genex creature` — takes all sixteen.
87
+
88
+ For an enemy that follows a path, `--lean` generates forward walk + run only.
89
+ Enemies have no controller state machine driving a direction picker, so the
90
+ other six directions would never play. Ask for the full set when a boss
91
+ genuinely circles the player.
92
+
93
+ ## Your own footage: `--video`
94
+
95
+ `--video ./clip.mp4` converts a real performance instead of a generated one.
96
+ The footage is a hard ceiling on the result — nothing can add weight the
97
+ performer never committed — so it has to meet the extractor's requirements:
98
+
99
+ - **Exactly one person**, alone in frame.
100
+ - **Whole body visible**, head to feet, for the entire clip. A limb that leaves
101
+ the frame is a gap that cannot be recovered.
102
+ - **Locked-off camera.** No pan, no zoom, no handheld drift, no cuts.
103
+ - **Plain contrasting background**, and clothing in distinct solid colors — no
104
+ long coat, cape, or skirt over the legs.
105
+ - **Even lighting.** Heavy shadows or a silhouette degrade the extraction.
106
+ - **2–60 seconds.** `.mp4` or `.mov`.
107
+
108
+ Phone recordings work. So do screen captures and gameplay clips.
109
+
110
+ The video is checked against these rules before anything is converted, and a
111
+ failure tells you which requirement broke and roughly when — re-shoot against
112
+ that note rather than re-running the same file.
113
+
114
+ ## After the clips land
115
+
116
+ ```bash
117
+ npx genex controller character --character <character-id>
118
+ ```
119
+
120
+ Then play the game and watch the motion on the real character. Numbers and a
121
+ completed job prove the clip installed; only your eyes prove it reads right.
122
+
123
+ A clip that plays as a T-pose or does not play at all means it did not bind —
124
+ check `genex character motions <id>` to see what is actually installed. Do not
125
+ "fix" it with runtime bone corrections; regenerate it.
@@ -22,6 +22,7 @@ Three.js release or branch, and do not blindly copy demo architecture.
22
22
  | shot composition, chase/side/orbit rigs, camera handoffs, projection ownership, pointer look, mouse-aimed action (shooter, FPS/first-person, sniper, turret, crosshair/reticle), mouse-look, hand-rolled steering/pan/look input signs (screen-direction contract), floating origins | `$genex-threejs-camera-direction` |
23
23
  | on-foot player movement: walk/run/jump/crouch, third-person character, slopes, stairs, moving platforms, the player's body loader, directional locomotion, transitions, action motion | `$genex-threejs-character-controller` |
24
24
  | the game's own generated character — the player's BODY wherever a human body appears on screen (first-person included; the exemption is "no human body ever appears", not "the camera is in the head"), enqueued with the first art actions; one user stop riding the concept review, owner-ratified auto-pick on silence (director §7) — or Meshy animation coverage beyond UAL: reference-informed A-pose concepts, exact action IDs, same-rig adapter | `$genex-ai-character` + `$genex-threejs-character-controller` |
25
+ | a character or enemy needs a motion the catalog lacks — a signature move, a boss telegraph, a death, a full 8-way movement set, or a performance from the user's own footage: free-text verbs, platform-routed, plan shown before any spend (`genex character animate <id> "<verb>"`, `genex creature animate`, `--locomotion`, `--video`) | `$genex-ai-character` (motion section + `references/motion-generation.md`) |
25
26
  | remote player bodies in multiplayer — NEVER hand-built primitives: the game's generated character when it has one (everyone wears it), the player's `p.avatarUrl` VRM only when it doesn't | `$genex-threejs-multiplayer` + `$genex-threejs-character-controller` |
26
27
  | the game has enemies, NPCs, or creatures — **load whenever an enemy roster exists**: rigged bipeds via `npx genex creature`, static + procedural motion for other body shapes, plus the mechanical floor every enemy owes (collider, verified facing, hit reaction, death moment) | `$genex-threejs-creatures` |
27
28
  | the player drives or flies something: cars, drones, vehicle physics, gearbox, enter/exit between character and vehicle | `$genex-threejs-vehicle-controllers` |
@@ -56,10 +56,11 @@ way, so the character arriving mid-build is a file drop
56
56
  (`npx genex controller character --character <id>`) and not an edit to your
57
57
  most load-bearing code. Wire the block under "Minimal wiring" once, at hour 0.
58
58
 
59
- For a full custom LOCOMOTION SET beyond the UAL packs (a weapon-hold 8-way
60
- shooter kit, a signature gait), see `$genex-ai-character`'s motion section:
61
- `npx genex motion install --set rifle` drops the proven rifle set + its own
62
- runtime, and `genex motion gen` builds new sets from text.
59
+ For a full custom LOCOMOTION SET beyond the UAL packs, see `$genex-ai-character`'s
60
+ motion section: `npx genex character animate <id> --locomotion` generates the
61
+ 8-way walk + run set onto the character's own rig, filling the directional slots
62
+ this controller already resolves but the stock pack leaves empty. Signature moves
63
+ are the same command with a verb.
63
64
 
64
65
  Before generating a Meshy character, discuss two or three visual directions.
65
66
  When the user names a visual reference, inspect references before writing the
@@ -51,10 +51,20 @@ post-mixer limb corrections. Prompt the body UPRIGHT and unpropped (held
51
51
  props fuse into bodies); prompt "facing the viewer" but never trust it —
52
52
  facing is verified below.
53
53
 
54
- When the catalog has no clip for a bespoke attack, telegraph, or death move
55
- on a rigged biped, `genex motion` can generate one from text — see the motion
56
- section of `$genex-ai-character` (its compiled sets retarget onto any
57
- humanoid rig, unlike catalog clips).
54
+ When the catalog has no clip for a bespoke attack, telegraph, or death move on
55
+ a rigged biped, generate one for this creature:
56
+
57
+ ```bash
58
+ npx genex creature animate <creature-id> "wind-up ground pound" --no-wait
59
+ npx genex creature animate <creature-id> --locomotion --lean --no-wait
60
+ ```
61
+
62
+ Say what the body does, in beats, with the weight named — "rears back, both
63
+ arms overhead, slams down and sinks into a low crouch" beats "attack". The
64
+ clips land in the creature's own manifest and play through the raw mixer wired
65
+ below; `--lean` keeps movement to forward walk + run, which is all an enemy
66
+ following a path can ever play. See the motion section of `$genex-ai-character`
67
+ for what this can and cannot generate.
58
68
 
59
69
  ### State → clip wiring (one mixer per creature)
60
70
 
@@ -11,8 +11,8 @@ coordinate frames, velocities, and ownership before writing per-frame updates.
11
11
  Boundary: procedural animation owns held-object and analytic motion (arcs,
12
12
  springs, docking, debris). WHOLE-BODY character verbs (a gait, a weapon
13
13
  hold, a signature move) belong to the character lane — the catalog first,
14
- then `genex motion` for verbs it lacks; see the motion section of
15
- `$genex-ai-character`.
14
+ then `genex character animate <id> "<verb>"` for verbs it lacks; see the
15
+ motion section of `$genex-ai-character`.
16
16
 
17
17
  ## Build order
18
18
 
@@ -1,145 +0,0 @@
1
- # Custom animations with `genex motion` — the full procedure
2
-
3
- Text → raw motion takes on the platform GPU service → objective verify →
4
- YOUR pick → local compile → a rig-independent set the vendored runtime plays
5
- on any humanoid. The skeleton is a 27-joint humanoid core at 20 fps; takes
6
- arrive as `.npz` files plus a machine verify report. Everything after `gen`
7
- runs locally and free — regenerate compiled sets as often as you like.
8
-
9
- **Rig limits (say them to the user before promising a move):** biped,
10
- human-proportioned characters only. Rigs without finger bones (many Meshy
11
- characters) cannot curl fingers around a grip — prefer weapon-carry looks
12
- that read well with flat hands there, or use a rig with fingers for hero
13
- characters. Takes are 2–10 seconds.
14
-
15
- ## The loop
16
-
17
- ```bash
18
- # 1. Generate (bills per take, default 4; ALWAYS --no-wait — jobs run minutes)
19
- npx genex motion gen "soldier rifle low-ready idle, fully upright, head held high" --takes 4 --no-wait
20
- npx genex wait <id> # prints one URL per take + the report
21
-
22
- # 2. Download the takes it printed and RENAME them — filenames drive everything
23
- mkdir -p takes && curl -o takes/rifle-idle.npz "<take-url>"
24
-
25
- # 3. Verify locally (same math the service ran; free, reproducible)
26
- npx genex motion verify takes/
27
-
28
- # 4. Pick the takes whose MOTION reads right (the report is data, not taste),
29
- # then compile the set
30
- npx genex motion compile takes/ --out src/motion/rifle.json
31
-
32
- # 5. Vendor the runtime once per game (+ the proven rifle starter set)
33
- npx genex motion install --set rifle
34
- ```
35
-
36
- ## Take naming — the routing contract
37
-
38
- `verify` and `compile` classify each take by its FILENAME stem. Rename
39
- downloaded takes before running them:
40
-
41
- | Stem pattern | Compiles into |
42
- |---|---|
43
- | `<set>-run-forward` / `-back` | `forward` / `back` gait |
44
- | `<set>-strafe-left` / `-strafe-right` | `strafeLeft` / `strafeRight` |
45
- | `<set>-run-fl` / `-fr` / `-bl` / `-br` | the four diagonal gaits |
46
- | `<set>-idle` or `…stance…` | the `idle` loop |
47
- | `…jump…` (`jump-stand`, `jump-run`) | `jumpStand` / `jumpRun` one-shots |
48
- | anything else | a named freeform loop |
49
-
50
- A stem containing `aim`, `rifle`, or `pistol` marks the take as a WEAPON
51
- HOLD: verify adds the hold-shape gates, and compile grip-line-normalizes the
52
- idle (the whole body blades as one) and plucks `aim0`/`aim0Up`/`aim0Dn`
53
- upper-body mask poses from the hold takes.
54
-
55
- ## The prompt wording ladder (hard-won — wrong words cost paid takes)
56
-
57
- - **Never ask for "sights" or a "cheek weld"** — you get a hunched
58
- face-height hold that reads absurd on game rigs. Ask for the third-person
59
- hold: *"weapon at chest height, arms extended"*, *"low-ready"* as the
60
- fallback.
61
- - **Always add "fully upright, head held high"** — the model's prior slouches.
62
- - **Describe hand geometry literally** (*"left hand forward on the handguard,
63
- right hand at the grip"*), not by weapon jargon.
64
- - The model NEVER squares a two-handed hold to the pelvis — real marksmen
65
- blade, and compile normalizes the heading by the grip line. Do not fight
66
- it with prompt words; it is not a defect.
67
- - **Gaits need constraints, not prose.** Direction words alone drift; pin
68
- the trajectory:
69
-
70
- ```bash
71
- npx genex motion constraints --dir strafe-left --speed 2.5 --duration 5 --out strafe-left.json
72
- npx genex motion gen "soldier strafing with a rifle at low-ready" --constraints strafe-left.json --no-wait
73
- ```
74
-
75
- The constraint pins the root path AND the heading (facing stays +Z) — that
76
- pin is what makes a strafe a strafe. One direction per request; a full
77
- 8-direction set is 8 requests + one idle (run them in parallel with
78
- `--no-wait`, like any batch).
79
-
80
- ## Verify semantics
81
-
82
- `PASS` (≥70) / `WARN` (≥50) / `FAIL`, with a hard-zero cap: a zero on any
83
- load-bearing gate fails the take no matter the average. The reasons column
84
- names the defect in plain words (feet skate, grip line wanders, cheek weld…).
85
- The report is DATA — freeform verbs have no semantic gate, so watch the take
86
- on the rig before shipping it. Tune bands via `--gates gates.json` (partial
87
- JSON, deep-merged: `{"aim": {"hunch": {"bad": 25}}}`).
88
-
89
- ## Compile semantics + tunables
90
-
91
- Compile cuts loop cycles at left-foot strikes (gaits) or the best seam window
92
- (idles), removes the linear trajectory (residual sway + full Y bob stay),
93
- normalizes heading to +Z, phase-aligns gaits (frame 0 = left strike), and for
94
- weapon holds levels the arm assembly, raises the gaze, clamps tall stances,
95
- and yaws mask poses in the SPINE. All rigid whole-body/assembly operations —
96
- no joint ever moves relative to another (the no-bake law; it is why compiled
97
- sets survive retargeting). Every threshold lives in
98
- `src/motion/motion.config.json` after install — pass `--config` to use it.
99
-
100
- ## Wiring the runtime
101
-
102
- `genex motion install` puts the runtime IN YOUR GAME — `src/motion/rigs.js`
103
- (both retarget formulas + the load-time normalizations), `anim-runtime.js`
104
- (ClipSet/Animator/dir8Weights/aimMaskStep), `ik.js`, and the config. They are
105
- yours: read them, tune them, the comments explain every formula. Load order
106
- matters:
107
-
108
- ```js
109
- import { loadRig, captureRestAnkle, reanchorFeet, groundCalibrate, curlFingers } from "./motion/rigs.js";
110
- import { ClipSet, Animator, dir8Weights, aimMaskStep } from "./motion/anim-runtime.js";
111
-
112
- const set = await (await fetch("./motion-sets/rifle.json")).json(); // copy sets where your bundler serves them
113
- const rig = await loadRig("./assets/character.glb", set, modelFrame); // GLB or VRM — auto-detected
114
- rig.computeCorrection();
115
- const restAnkle = captureRestAnkle(rig, scene); // BIND pose, before any clip
116
- reanchorFeet(rig, set, scene); // steep-boot fix — BEFORE ClipSet
117
- const clipSet = new ClipSet(rig, set, set.gaits);
118
- groundCalibrate(rig, clipSet, scene, restAnkle); // short-leg fix — AFTER ClipSet
119
- curlFingers(rig, scene); // grips (no-op on finger-less rigs)
120
- const anim = new Animator(clipSet);
121
-
122
- // per frame: your input → blend weights; the mask follows the COMMAND magnitude
123
- anim.setLoops({ idle: 1 - mag, ...dir8Weights(x, z, mag) }, dt);
124
- maskW = aimMaskStep(maskW, { mag, grounded, pitch, dt });
125
- anim.setMask("aim0", maskW, pitch / 0.6); // ±40° at pitch extremes
126
- anim.update(dt);
127
- ```
128
-
129
- - Physics owns the world transform: hips play LOCAL values only (residual
130
- x/z + absolute y); your character root carries position + yaw.
131
- - Jumps: `anim.playOneShot("jumpRun", { yCap: 0.1 })` — cap hips Y when the
132
- physics capsule owns the jump, and gate the impulse on the clip's own
133
- takeoff frame. The `--set rifle` install includes the shared jumps set;
134
- merge it: `Object.assign(clipSet.tracks, new ClipSet(rig, jumps, jumps.gaits).tracks)`.
135
- - The aim mask WINS over one-shots on masked bones — a jump keeps aiming.
136
- - Standing aim = the UNMASKED bladed idle; the spine-yawed mask pose is only
137
- for moving/airborne/pitched states. `aimMaskStep` already encodes this.
138
-
139
- ## Cost + discipline
140
-
141
- Per-take billing (a 4-take request ≈ one model generation). One regenerate
142
- per verb is a fair budget when takes fail the gates — reword with the ladder
143
- above before spending again, and compare candidates from ONE batch first
144
- (that is what `--takes 4` is for). Never re-run `motion gen` to "pick up" a
145
- result — `genex wait <id>` re-attaches free.