@genex-ai/cli-demo 0.85.0-dev.216 → 0.87.0-dev.218

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
@@ -254,10 +254,10 @@ var GENEX_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
254
254
 
255
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.
256
256
 
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, \`DESIGN.md\`, and the skill for the stage you are executing.
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.
259
- 3. \`DESIGN.md\` at the project root is the design contract. Keep it current; record every decision, assumption, and generation id there.
260
- 4. ALL generated art, audio, video, characters, and UI come from \`genex\` commands \u2014 never from any other generation tool your platform bundles, unless the player explicitly asks for that tool by name. A local reference image is not a reason to switch tools: pass its file path to genex (\`--edit\` and \`--inpaint\` accept local paths).
259
+ 3. \`DESIGN.md\` at the project root is the design contract AND the build plan. It must carry a \`## Build plan & status\` section: numbered milestones with status marks and a \`Now:\` line naming the current one \u2014 a milestone is done only when its work reached a preview. Keep it current; record every decision, assumption, and generation id there. When the plan first lands, tell the player in one plain line that the plan is locked in and lives in \`DESIGN.md\`.
260
+ 4. ALL generated art, audio, video, characters, and UI come from \`genex\` commands \u2014 never from any other generation tool your platform bundles, unless the player explicitly asks for that tool by name. A local reference image is not a reason to switch tools: pass its file path to genex (\`--edit\` and \`--inpaint\` accept local paths). The same exclusivity covers shipping: building, previewing, and publishing go only through \`genex preview\` / \`genex publish\` \u2014 never load your platform's own site-building, hosting, or deploy skills for this game.
261
261
  5. The Stage-1 HUD mockup IS the game concept \u2014 it is your FIRST art action at the UI plan gate. ONE concept image, no candidate variants (generate variants only when the player asks for them), anchored to any reference image the player gave.
262
262
  6. The moment the mockup lands: you are the art director \u2014 immediately enqueue the Stage-2 HUD sheet, the menu still, and the logotype (\`--no-wait\`). THEN show the player the frame and ask keep/change as information, never as a gate. Silence = the concept stands; a later "change" loops it with the player's notes.
263
263
  7. The menu video fires at the FIRST of: the player's yes \xB7 the next \`genex preview\` after the menu still landed \xB7 style art being the only work left. It never fires while a player objection is open. Two failed videos \u2192 ship the still (menu skill rule).
@@ -267,8 +267,11 @@ Your capabilities (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skyb
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
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
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 in local test mode (\`?genex_local_test=1\`) with a real gameplay screenshot. A claim without a capture is not verification.
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.
271
271
  15. Treat every \`genex\` warning line \u2014 preflight, \`ui audit\`, \`wait\` nudges \u2014 as work, not noise.
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
+ 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
+ 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.
272
275
  ${CONTRACT_END}
273
276
  `;
274
277
  var CLAUDE_IMPORT_LINE = "@AGENTS.md";
@@ -4641,6 +4644,11 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
4641
4644
  'Menu video not enqueued \u2014 the animated menu backdrop is the default for every game. Enqueue it (genex-ai-menu), or record "Menu video: declined (reason)" in DESIGN.md if the player declined it.'
4642
4645
  );
4643
4646
  }
4647
+ if (design.length > 0 && !/##\s*build plan & status/i.test(design)) {
4648
+ warnings.push(
4649
+ "DESIGN.md has no `## Build plan & status` section \u2014 numbered milestones with a `Now:` line are how a compacted or resumed session knows where the build is (genex-game-director's design contract has the shape)."
4650
+ );
4651
+ }
4644
4652
  if (!/hud lane:/i.test(design)) {
4645
4653
  const hudDom = await hasHudLikeDom(cwd);
4646
4654
  if (hudDom && !await hasExtractionSidecars(cwd)) {
@@ -5763,6 +5771,11 @@ async function runWait(opts) {
5763
5771
  await reportTerminal(kind, view, log, opts.open, opts.json);
5764
5772
  return;
5765
5773
  }
5774
+ if (kind === "character_concept" || kind === "character_preview" || kind === "character") {
5775
+ log.dim(
5776
+ ` \u23F3 Character stages take several minutes server-side \u2014 you don't have to sit here. Keep building and check back with ${c.cyan("genex wait --all")}.`
5777
+ );
5778
+ }
5766
5779
  await awaitAndReport(apiUrl, token, id, kind, log, opts.open, opts.json);
5767
5780
  }
5768
5781
  async function runWaitAll(opts) {
@@ -5851,6 +5864,10 @@ async function runWaitAll(opts) {
5851
5864
  log.plain("");
5852
5865
  log.plain(` ${c.bold(STAGE1_LANDING_NUDGE)}`);
5853
5866
  }
5867
+ log.plain("");
5868
+ log.dim(
5869
+ " \u{1F4DC} Resumed or compacted? Re-read AGENTS.md + DESIGN.md (continue from its Now: line) before building on."
5870
+ );
5854
5871
  }
5855
5872
  async function toRow(e, v, cwd) {
5856
5873
  const base = { id: e.id, kind: e.kind, prompt: e.prompt, queuedAt: e.queuedAt };
@@ -15787,6 +15804,22 @@ async function context(opts) {
15787
15804
  const project = await readProject();
15788
15805
  return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
15789
15806
  }
15807
+ var CHARACTER_BRIEF_MAX_CHARS = 600;
15808
+ function apiErrorMessage(data, fallback) {
15809
+ if (typeof data !== "object" || data === null) return fallback;
15810
+ const body = data;
15811
+ if (typeof body.message === "string" && body.message) return body.message;
15812
+ if (typeof body.error === "string" && body.error) return body.error;
15813
+ const err = body.error;
15814
+ if (err && Array.isArray(err.issues) && err.issues.length > 0) {
15815
+ const lines = err.issues.map((i) => {
15816
+ const where = Array.isArray(i.path) && i.path.length > 0 ? `${i.path.join(".")}: ` : "";
15817
+ return typeof i.message === "string" ? `${where}${i.message}` : "";
15818
+ }).filter(Boolean);
15819
+ if (lines.length > 0) return `${fallback} ${lines.join("; ")}`;
15820
+ }
15821
+ return fallback;
15822
+ }
15790
15823
  async function quote(url, token, body) {
15791
15824
  const response = await apiFetch(url, {
15792
15825
  method: "POST",
@@ -15796,7 +15829,7 @@ async function quote(url, token, body) {
15796
15829
  if (printedStructuredError(response)) return null;
15797
15830
  if (!response.ok) {
15798
15831
  const data = await response.json().catch(() => ({}));
15799
- throw new Error(data.message ?? data.error ?? `Quote failed (HTTP ${response.status}).`);
15832
+ throw new Error(apiErrorMessage(data, `Quote failed (HTTP ${response.status}).`));
15800
15833
  }
15801
15834
  return (await response.json()).quote;
15802
15835
  }
@@ -15848,7 +15881,7 @@ async function postWorkflow(url, token, body) {
15848
15881
  if (printedStructuredError(response)) return null;
15849
15882
  if (!response.ok) {
15850
15883
  const data2 = await response.json().catch(() => ({}));
15851
- throw new Error(data2.message ?? data2.error ?? `Character workflow request failed (HTTP ${response.status}).`);
15884
+ throw new Error(apiErrorMessage(data2, `Character workflow request failed (HTTP ${response.status}).`));
15852
15885
  }
15853
15886
  const data = await response.json();
15854
15887
  if (typeof data.id !== "string" || data.id.length === 0) {
@@ -15909,6 +15942,14 @@ async function runCharacterConcept(opts) {
15909
15942
  fail2(opts, 'Missing brief. Usage: genex character "<brief>".', "character_concept");
15910
15943
  return;
15911
15944
  }
15945
+ if (prompt.length > CHARACTER_BRIEF_MAX_CHARS) {
15946
+ fail2(
15947
+ opts,
15948
+ `The brief is ${prompt.length} characters \u2014 the character concept endpoint caps it at ${CHARACTER_BRIEF_MAX_CHARS}. Tighten it to the look that matters (silhouette, materials, palette, mood); gameplay details don't belong in the brief.`,
15949
+ "character_concept"
15950
+ );
15951
+ return;
15952
+ }
15912
15953
  if (opts.polycount !== void 0) {
15913
15954
  fail2(opts, "--polycount is only available with --direct-text. The approval flow remeshes only after finalize approval.", "character_concept");
15914
15955
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.85.0-dev.216",
3
+ "version": "0.87.0-dev.218",
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": {
@@ -324,6 +324,17 @@ export interface PlayOneShotOptions {
324
324
  clamp?: boolean;
325
325
  /** Fired when the clip finishes (skipped if a newer one-shot interrupts it). */
326
326
  onDone?: () => void;
327
+ /**
328
+ * Movement cancels this one-shot. Every one-shot is FULL-BODY (there is no
329
+ * upper-body layering), so it freezes the legs — a moving character SLIDES
330
+ * across the ground in a static pose for the clip's whole length (a 2.9s hit
331
+ * reaction, a 2.7s cast). With `interruptible: true`, grounded movement
332
+ * intent releases the clip via {@link CharacterAnimations.clearOneShot} and
333
+ * locomotion takes over immediately. Use it for reactions and gestures a
334
+ * player can walk out of; leave it off for poses that must hold to their end
335
+ * (a death/knockdown). Default false.
336
+ */
337
+ interruptible?: boolean;
327
338
  }
328
339
 
329
340
  type MutableAnimationStateContext = {
@@ -358,6 +369,9 @@ export class CharacterAnimations {
358
369
  // When true, the active one-shot holds its final pose on finish (clamp:true —
359
370
  // e.g. Death01) instead of crossfading back to locomotion.
360
371
  #oneShotHoldPose = false;
372
+ // When true, grounded movement intent cancels the active one-shot (see
373
+ // PlayOneShotOptions.interruptible).
374
+ #oneShotInterruptible = false;
361
375
 
362
376
  #state: CharacterAnimationState = "IDLE";
363
377
  #prevActionName: string | null;
@@ -398,7 +412,27 @@ export class CharacterAnimations {
398
412
  options: CharacterAnimationsOptions = {}
399
413
  ) {
400
414
  this.#model = model;
401
- this.#clipMap = buildClipMap(clips, options.clipMap);
415
+ // Seed the fuzzy name-matched map from the locomotion profile's own slots
416
+ // (explicit options.clipMap still wins). Simple boolean snapshots (remote
417
+ // players, bots) never reach the profile's directional resolver and fall
418
+ // through to this map — without the seed they bind DIFFERENT clips than the
419
+ // local player ("Running" vs the profile's run.forward), so the same
420
+ // movement renders as two different gaits on two screens. Jump states are
421
+ // deliberately NOT seeded: jump.full would one-shot-clamp as a loop.
422
+ const slots = options.locomotionProfile?.slots ?? {};
423
+ const profileSeed: Partial<Record<CharacterAnimationState, string>> = {};
424
+ const seedPairs: ReadonlyArray<[CharacterAnimationState, string]> = [
425
+ ["IDLE", "idle.default"],
426
+ ["WALK", "walk.forward"],
427
+ ["RUN", "run.forward"],
428
+ ["CROUCH_IDLE", "crouch.idle"],
429
+ ["CROUCH_MOVE", "crouch.forward"],
430
+ ];
431
+ for (const [state, slot] of seedPairs) {
432
+ const clip = slots[slot];
433
+ if (clip) profileSeed[state] = clip;
434
+ }
435
+ this.#clipMap = buildClipMap(clips, { ...profileSeed, ...options.clipMap });
402
436
  this.#resolver = options.resolver ?? resolveAnimationState;
403
437
  this.#onChange = options.onChange;
404
438
  this.#locomotionProfile = options.locomotionProfile;
@@ -542,6 +576,13 @@ export class CharacterAnimations {
542
576
  // Optional in the snapshot (older five-boolean remote-player objects).
543
577
  ctx.crouchActive = snapshot.crouchActive ?? false;
544
578
 
579
+ // Grounded movement cancels an interruptible one-shot — otherwise the
580
+ // frozen full-body pose slides across the ground for the clip's whole
581
+ // length. clearOneShot crossfades locomotion back in with no T-pose frame.
582
+ if (this.#oneShotAction && this.#oneShotInterruptible && ctx.isMoving && ctx.isOnGround) {
583
+ this.clearOneShot();
584
+ }
585
+
545
586
  const next = this.#resolver(ctx);
546
587
  const stateChanged = next !== this.#state;
547
588
  if (stateChanged) this.#state = next;
@@ -550,6 +591,19 @@ export class CharacterAnimations {
550
591
  // (Jump_Start/Jump_Land) finishes and unlocks, the pending loop clip must still fade in
551
592
  // even though the state did not change again. Internal guards make this a no-op otherwise.
552
593
  this.#desiredMotionName = this.#resolveLocomotionClip(snapshot, next) ?? this.#clipMap[next];
594
+ // Rigs whose airborne slots fall back to the SAME clip as jump.start (every
595
+ // Meshy rig: jump.rise/jump.fall → jump.full) never re-enter #applyTransition
596
+ // while airborne — the name never changes — so once the one-shot finishes the
597
+ // character hangs FROZEN in the jump's final frame for the rest of the fall
598
+ // (long drops, knockback launches). Once the held action has finished
599
+ // (paused+clamped), retarget sustained airtime at the idle loop so the limbs
600
+ // keep animating while descending.
601
+ if ((next === "JUMP_IDLE" || next === "JUMP_FALL") && this.#desiredMotionName !== null) {
602
+ const held = this.#actions.get(this.#desiredMotionName);
603
+ if (held && held.paused) {
604
+ this.#desiredMotionName = this.#resolveProfileSlot("idle.default") ?? this.#clipMap.IDLE;
605
+ }
606
+ }
553
607
  // A full-jump profile clip may outlast a very short physics hop. Release
554
608
  // its one-shot lock as soon as the controller lands so idle/walk can
555
609
  // crossfade immediately instead of waiting for the authored clip to end.
@@ -643,6 +697,7 @@ export class CharacterAnimations {
643
697
  // finish and the rig snaps to bind pose for a frame — the T-pose flash.
644
698
  action.clampWhenFinished = true;
645
699
  this.#oneShotHoldPose = options.clamp ?? false;
700
+ this.#oneShotInterruptible = options.interruptible ?? false;
646
701
  action.timeScale = options.timeScale ?? 1;
647
702
  action.reset();
648
703
  if (current && current !== action) {
@@ -660,6 +715,42 @@ export class CharacterAnimations {
660
715
  return true;
661
716
  }
662
717
 
718
+ /**
719
+ * Cancel a one-shot and hand control back to locomotion NOW. The case that
720
+ * REQUIRES it: a `clamp:true` one-shot (death/knockdown, a held wind-up)
721
+ * holds its final frame and keeps the locomotion lock closed FOREVER by
722
+ * design — so a respawn or an aborted action must explicitly release it, or
723
+ * the rig keeps the clamped pose while the body moves around. Safe to call
724
+ * at any time; a no-op when nothing is held.
725
+ *
726
+ * Call it on TRANSITIONS (respawn, revive, action aborted) — never per
727
+ * frame, or it cancels every in-flight reaction clip.
728
+ *
729
+ * @example
730
+ * // on respawn, before teleporting the body:
731
+ * anims.clearOneShot();
732
+ */
733
+ clearOneShot(): void {
734
+ if (this.#disposed) return;
735
+ const finished = this.#oneShotAction;
736
+ this.#oneShotAction = null;
737
+ this.#oneShotOnDone = undefined;
738
+ this.#oneShotHoldPose = false;
739
+ this.#oneShotInterruptible = false;
740
+ this.#canPlayNext = true;
741
+ // Crossfade the current state's loop in FROM the held pose — same path the
742
+ // "finished" handler uses, so there is no unposed/T-pose frame.
743
+ if (finished) this.#recoverFromOneShot(finished);
744
+ }
745
+
746
+ /** Clip name of the currently-playing (or clamped-held) one-shot, or null.
747
+ * Lets a caller cancel a SPECIFIC one-shot (e.g. a held charge wind-up)
748
+ * without clobbering whatever replaced it:
749
+ * `if (anims.currentOneShotName === WINDUP_CLIP) anims.clearOneShot();` */
750
+ get currentOneShotName(): string | null {
751
+ return this.#oneShotAction?.getClip().name ?? null;
752
+ }
753
+
663
754
  /** Stop all actions, uncache clips, remove the mixer's 'finished' listener. */
664
755
  dispose(): void {
665
756
  if (this.#disposed) return;
@@ -766,9 +857,23 @@ export class CharacterAnimations {
766
857
  #releaseStuckLocks(): void {
767
858
  const prevActionName = this.#prevActionName;
768
859
  if (prevActionName === null) return;
860
+ // Match BOTH bindings a jump one-shot can have: the fuzzy clipMap names and
861
+ // the profile slots. On profile rigs the clipMap entries are usually null
862
+ // (no clip is literally named "jump_start"), which made this escape hatch
863
+ // dead code — a stuck jump lock could never self-release. The jump.land
864
+ // slot is ignored when it falls back to idle.default (same guard
865
+ // #applyTransition uses), or an idle prevAction would spuriously match.
866
+ const startName = this.#clipMap.JUMP_START ?? this.#resolveProfileSlot("jump.start");
867
+ const profileLand = this.#resolveProfileSlot("jump.land");
868
+ const landName =
869
+ this.#clipMap.JUMP_LAND ??
870
+ (profileLand !== this.#resolveProfileSlot("idle.default") ? profileLand : null);
871
+ // A manual playOneShot() lock is NOT ours to release — even when its clip
872
+ // happens to share a name with the jump binding (clearOneShot owns that).
873
+ if (this.#oneShotAction) return;
769
874
  if (
770
875
  !this.#canPlayNext &&
771
- prevActionName === this.#clipMap.JUMP_START &&
876
+ prevActionName === startName &&
772
877
  this.#state !== "JUMP_IDLE" &&
773
878
  this.#state !== "JUMP_START"
774
879
  ) {
@@ -776,7 +881,7 @@ export class CharacterAnimations {
776
881
  }
777
882
  if (
778
883
  !this.#canPlayNext &&
779
- prevActionName === this.#clipMap.JUMP_LAND &&
884
+ prevActionName === landName &&
780
885
  this.#state !== "IDLE" &&
781
886
  this.#state !== "JUMP_LAND"
782
887
  ) {
@@ -878,13 +983,19 @@ export class CharacterAnimations {
878
983
  snapshot: CharacterStateSnapshot,
879
984
  state: CharacterAnimationState,
880
985
  ): void {
881
- if (!this.#locomotionProfile || !isAdvancedSnapshot(snapshot) || this.oneShotActive) return;
986
+ // Gate on a numeric moveSpeed rather than the full advanced snapshot:
987
+ // remote players and bots drive with simple boolean snapshots plus an
988
+ // estimated speed (position deltas / their own velocity), and without this
989
+ // they play locomotion at a flat 1.0 while the local player's clips
990
+ // speed-match — visibly different gaits for the same movement.
991
+ const speed = (snapshot as Partial<AdvancedCharacterStateSnapshot>).moveSpeed;
992
+ if (!this.#locomotionProfile || typeof speed !== "number" || this.oneShotActive) return;
882
993
  const band = state === "RUN" ? "run" : state === "CROUCH_MOVE" ? "crouch" : state === "WALK" ? "walk" : null;
883
994
  if (!band || !this.#prevActionName) return;
884
995
  const nominal = this.#locomotionProfile.nominalSpeed?.[band];
885
996
  if (!nominal || nominal <= 0) return;
886
997
  const limits = this.#locomotionProfile.playbackRate ?? { min: 0.75, max: 1.35 };
887
- const rate = THREE.MathUtils.clamp(snapshot.moveSpeed / nominal, limits.min, limits.max);
998
+ const rate = THREE.MathUtils.clamp(speed / nominal, limits.min, limits.max);
888
999
  this.#actions.get(this.#prevActionName)?.setEffectiveTimeScale(rate);
889
1000
  }
890
1001
  }
@@ -48,6 +48,16 @@ separately.
48
48
  npx genex character "stylized desert courier, practical layered clothing"
49
49
  ```
50
50
 
51
+ **Keep the brief under 600 characters** — the concept endpoint rejects longer
52
+ ones. A brief is the LOOK (silhouette, materials, palette, mood), not the
53
+ game design; gameplay details never belong in it.
54
+
55
+ **Every character stage takes minutes server-side — never sit in a foreground
56
+ `genex wait` on one.** Enqueue with `--no-wait`, keep building the game, and
57
+ pick the result up at the next natural pause with `genex wait --all`. Watching
58
+ a character render in 30-second poll chunks stalls the whole build and is the
59
+ single fastest way to make the player ask why nothing else is happening.
60
+
51
61
  Generate concept images first and show the actual images to the user. Do not
52
62
  start Image-to-3D until the user explicitly selects a candidate.
53
63
 
@@ -34,11 +34,15 @@ Never claim a capability you didn't find, and never stall because one is
34
34
  missing.
35
35
 
36
36
  One more thing to note while you're looking: your platform may bundle its own
37
- image / video / site generation workflows. **They are not part of any Genex
38
- lane.** All generated art, audio, video, characters, and UI come from `genex`
39
- commands, unless the player explicitly asks for another tool by name — and a
40
- local reference image is never a reason to switch tools: `genex image --edit`
41
- and `--inpaint` take a local file path directly.
37
+ image / video generation workflows AND its own site-building / hosting /
38
+ deploy skills. **None of them are part of any Genex lane — don't load them
39
+ for this project.** All generated art, audio, video, characters, and UI come
40
+ from `genex` commands, unless the player explicitly asks for another tool by
41
+ name and a local reference image is never a reason to switch tools:
42
+ `genex image --edit` and `--inpaint` take a local file path directly. The
43
+ same exclusivity covers shipping: the game builds, previews, and publishes
44
+ only through `genex preview` / `genex publish` — a platform hosting skill in
45
+ context is pure confusion fuel.
42
46
 
43
47
  ## 2. Scope check — what is this?
44
48
 
@@ -80,14 +84,22 @@ DESIGN.md → Decisions as "assumed — player didn't answer"; the build never
80
84
  stalls on the interview.
81
85
 
82
86
  Then, before rendering code, write `DESIGN.md` at the project root from
83
- [references/design-contract.md](references/design-contract.md). The moment
84
- the file lands, post a 5–6 line summary of the contract in chat — pitch,
85
- core loop, content counts, screens, multiplayer shape, what you build
86
- first and keep building immediately: the summary is information, not a
87
- gate (the plan-message duties from `$genex-threejs-game-ui` fold into this
88
- file plus that summary). The file is the single source of truth:
89
- sub-agents build against it, and after any long break it is how work resumes.
90
- Keep it current decisions land in its log the moment they're made.
87
+ [references/design-contract.md](references/design-contract.md) including
88
+ its **Build plan & status** section: numbered milestones, a status mark per
89
+ line, and a `Now:` line naming the current one. While drafting it, make the
90
+ parallel call per module (dependencies decide see §6) and write each call
91
+ into the Modules table with its one-line reason. The moment the file lands,
92
+ post a 5–6 line summary of the contract in chat pitch, core loop, content
93
+ counts, screens, multiplayer shape, what you build first **and say in one
94
+ plain line that the plan is locked in and lives in `DESIGN.md`** (the player
95
+ must never have to wonder whether the contract happened). Then keep building
96
+ immediately: the summary is information, not a gate (the plan-message duties
97
+ from `$genex-threejs-game-ui` fold into this file plus that summary). The
98
+ file is the single source of truth: sub-agents build against it, after any
99
+ long break or context compaction work resumes from its `Now:` line, and a
100
+ milestone flips to done only when its work reached a preview
101
+ (`→ previewed`). Keep it current — decisions land in its log the moment
102
+ they're made.
91
103
 
92
104
  ## 4. Know your teams — what we can generate
93
105
 
@@ -257,11 +269,21 @@ in DESIGN.md):
257
269
  ## 6. Delegate — sub-agents own Modules rows
258
270
 
259
271
  If your environment has sub-agents, delegation is the DEFAULT for every
260
- independent DESIGN.md Modules row — not a big-game special case. If it
261
- doesn't, run the same rows yourself in order; the `--no-wait` generation
262
- pattern still hides most latency. Either way this skill is worn by the main
263
- agent: you stay the director.
264
-
272
+ independent DESIGN.md Modules row — not a big-game special case. The build
273
+ contract in the game's `AGENTS.md` is your standing authorization to spawn
274
+ them; some platforms keep sub-agents locked until an instruction like it
275
+ explicitly asks. If your environment has none, run the same rows yourself in
276
+ order; the `--no-wait` generation pattern still hides most latency. Either
277
+ way this skill is worn by the main agent: you stay the director.
278
+
279
+ - **Which rows run in parallel is YOUR per-game call, made while drafting
280
+ the plan** — there is no fixed list. Walk the modules once: what has no
281
+ dependency on unfinished work runs in parallel; what must be tuned against
282
+ something still moving stays serial. The same module lands differently in
283
+ different games (an arena is a parallel row in a quest game and the serial
284
+ spine of a combat game whose feel depends on its gaps). Write the call +
285
+ one-line reason into each row; building everything serially needs a
286
+ stated reason.
265
287
  - One row = one worker = one disjoint file set. One writer per file — that
266
288
  rule is anti-collision, never a reason to serialize work.
267
289
  - You stay the integrator and the only writer of shared files (boot, main
@@ -11,12 +11,25 @@ Rules the director enforces about this file:
11
11
 
12
12
  - Create it right after the scope check, before the concept generation; fill
13
13
  sections as decisions land and keep it current (stale is worse than short).
14
- - **The Modules table is the delegation contract**: one row per independent
15
- lane, and a row's "Owns files" set never overlaps another row's. One writer
16
- per file. "Sub-agent when available" stays flexiblebut if a
17
- planned-sub-agent module gets built inline, the agent says why in one line.
18
- Verification is never a module row: the milestone smoke pass belongs to the
19
- director (the scaffold prompt owns its ceilings).
14
+ - **The Build plan & status section is the compass.** Numbered milestones,
15
+ each with a status mark, and a `Now:` line naming the current one. Update
16
+ it the moment a milestone starts or finishes a milestone is done only
17
+ when its work reached a `genex preview` (mark it `→ previewed`). After any
18
+ context compaction or session resume, this section not memory says
19
+ where the build is; continue from `Now:`. When the plan first lands, tell
20
+ the player in one plain line that it's locked in and lives in `DESIGN.md`.
21
+ - **The Modules table is the delegation contract, and "Built by" is a
22
+ per-game DECISION, not a default you copy.** While drafting the plan, walk
23
+ the modules once and decide what can run in parallel and what must stay
24
+ serial for THIS game — dependencies decide (an arena can be a parallel row
25
+ in one game and the serial spine of another). Write the call into each row
26
+ with a one-line reason. When your platform has sub-agents, independent
27
+ modules default to parallel; building everything serially needs a stated
28
+ reason. One row per independent lane, and a row's "Owns files" set never
29
+ overlaps another row's — one writer per file. If a planned-parallel module
30
+ gets built inline, say why in one line. Verification is never a module
31
+ row: the milestone smoke pass belongs to the director (the scaffold prompt
32
+ owns its ceilings).
20
33
  - **The Assets table is the budget**: the asset list falls out of Content
21
34
  (enemies → models, an arena → a ground texture, outdoors → a skybox).
22
35
  Status flow per row: `proposed → planned → generating (id) → landed (URL) →
@@ -50,6 +63,14 @@ Concept image: <asset URL> (standing — shown <date>; player said
50
63
  - **You earn:** <reward / progression>
51
64
  - **You lose when:** <fail state> → **and retry by:** <restart shape>
52
65
 
66
+ ## Build plan & status
67
+ Now: ▶ <number + name of the milestone in progress>
68
+ 1. <milestone> — ✅ → previewed
69
+ 2. <milestone> — ▶ in progress (<who: main agent | sub-agent>)
70
+ 3. <milestone> — ⬜ (<parallel: sub-agent | serial: main agent — one-line why>)
71
+ (One line per milestone; update on every start/finish; done requires its
72
+ preview. This section is the post-compaction compass — resume from `Now:`.)
73
+
53
74
  ## Content (only when the request names plural content or a content genre)
54
75
  Every plural noun from the request becomes a countable line:
55
76
  - Quests: <N>, chained by <how>
@@ -99,13 +120,15 @@ teams, backfill…)> — and why. Play-button rule: nothing connects before the
99
120
  click (when a Play screen exists).
100
121
 
101
122
  ## Modules — the build split
123
+ ("Built by" is decided per game while drafting the plan — dependencies
124
+ decide, with a one-line reason per row; the rows below are only a SHAPE.)
102
125
  | Module | Owns files | Built by | Done when |
103
126
  |---|---|---|---|
104
- | Boot & identity | src/main.ts, src/genex-boot.ts | main agent (only writer) | boots, tier wired, initEmbed runs |
105
- | World/terrain | src/world/** | sub-agent when available | walkable, matches World & scale |
106
- | Quests & dialogue data | src/content/** | sub-agent when available | counts match Content lines |
107
- | Enemies/AI | src/enemies/** | sub-agent when available | roster matches Content |
108
- | HUD chain (style-dependent) | src/ui/** | sub-agent on concept landing | all screens from Screens & UI |
127
+ | Boot & identity | src/main.ts, src/genex-boot.ts | main agent (only writer of shared boot) | boots, tier wired, initEmbed runs |
128
+ | World/terrain | src/world/** | parallel: sub-agent (no dependency on combat) | walkable, matches World & scale |
129
+ | Quests & dialogue data | src/content/** | parallel: sub-agent (pure data) | counts match Content lines |
130
+ | Enemies/AI | src/enemies/** | serial: main agent (tuned against movement feel) | roster matches Content |
131
+ | HUD chain (style-dependent) | src/ui/** | parallel: sub-agent on concept landing | all screens from Screens & UI |
109
132
 
110
133
  ## Decisions & changes
111
134
  - <date> — <one-liner per decision, including anything the player said no or
@@ -133,6 +156,15 @@ Concept image: https://assets.genex.technology/g/rustyard/concept.png
133
156
  - **You lose when:** your bot breaks → **and retry by:** one key, instant
134
157
  respawn, same round
135
158
 
159
+ ## Build plan & status
160
+ Now: ▶ 3. Rival AI feels dangerous
161
+ 1. Arena skeleton + movement — ✅ → previewed
162
+ 2. Shooting + scrap scoring — ✅ → previewed
163
+ 3. Rival AI feels dangerous — ▶ main agent
164
+ 4. HUD sprite swap — ⬜ parallel: sub-agent (concept landed, chain enqueued)
165
+ 5. Magnet cycle + pickups — ⬜ parallel: sub-agent (own files, no AI dependency)
166
+ 6. Menus, music, publish floors — ⬜
167
+
136
168
  ## Screens & UI
137
169
  Screens: loader, title menu, HUD (health, scrap, cycle timer), pause,
138
170
  win/lose. Style brief: oily metal, warning-stripe accents, stencil type.
@@ -167,10 +199,10 @@ Single-player v1 (rivals are host-free local AI). Revisit after publish.
167
199
  ## Modules
168
200
  | Module | Owns files | Built by | Done when |
169
201
  |---|---|---|---|
170
- | Boot & identity | src/main.ts | main agent | boots, tier wired, initEmbed runs |
171
- | Arena & pickups | src/world/** | sub-agent | walkable, magnet cycle fires |
172
- | Rival AI | src/enemies/** | sub-agent | 3 rivals hunt + shoot |
173
- | HUD chain | src/ui/** | sub-agent after yes | all 5 screens live |
202
+ | Boot & identity | src/main.ts | main agent (shared boot) | boots, tier wired, initEmbed runs |
203
+ | Arena & pickups | src/world/** | parallel: sub-agent (independent of AI) | walkable, magnet cycle fires |
204
+ | Rival AI | src/enemies/** | serial: main agent (tuned vs player feel) | 3 rivals hunt + shoot |
205
+ | HUD chain | src/ui/** | parallel: sub-agent on concept landing | all 5 screens live |
174
206
 
175
207
  ## Decisions & changes
176
208
  - 2026-07-20 — player picked concept candidate 2; asked for "more sparks".
@@ -257,7 +257,34 @@ addEventListener("pointerdown", () => {
257
257
  ```
258
258
 
259
259
  `options`: `fadeIn` (default 0.1 s), `timeScale`, `clamp` (hold the final pose —
260
- for deaths), `onDone`.
260
+ for deaths), `interruptible` (grounded movement cancels the clip), `onDone`.
261
+
262
+ **Every one-shot is FULL-BODY** (there is no upper-body layering), so it freezes
263
+ the legs — a moving character SLIDES across the ground in a frozen pose for the
264
+ clip's whole length. Two rules keep that from ever being visible:
265
+
266
+ - **Reactions and gestures a player can walk out of get `interruptible: true`**
267
+ (hit flinches, casts, taunts, celebrations). Movement intent releases the clip
268
+ and locomotion takes over the same frame. A 2.9 s hit reaction without it
269
+ locks the player's animation for 2.9 s *per hit taken*.
270
+ - **`clamp: true` holds the lock FOREVER by design** — nothing releases it
271
+ automatically, not even the clip ending. Every clamped one-shot needs a
272
+ guaranteed paired release: call `anims.clearOneShot()` on the transition out
273
+ (respawn, revive, action aborted). Audit every early-return on the path
274
+ between "clamped one-shot started" and "the replacing clip plays" — an abort
275
+ branch that skips the replacement strands the pose and the character slides
276
+ around locked until something else clears it.
277
+
278
+ `anims.clearOneShot()` cancels the active one-shot and crossfades locomotion
279
+ back in with no T-pose frame; it is safe to call when nothing is held. Call it
280
+ on TRANSITIONS only — per-frame calls cancel every in-flight reaction clip. To
281
+ cancel one SPECIFIC one-shot without clobbering whatever replaced it, check
282
+ `anims.currentOneShotName` first:
283
+
284
+ ```ts
285
+ // abort a held charge wind-up, but never a death pose that replaced it:
286
+ if (anims.currentOneShotName === WINDUP_CLIP) anims.clearOneShot();
287
+ ```
261
288
 
262
289
  The 12 core clips are always available; everything else comes from
263
290
  `npx genex controller anims <selectors…>` — selectors are **tags** (install a
@@ -658,6 +658,25 @@ Corollary: **cap top speeds against the network, not just the physics** — an o
658
658
  crossing time should stay above ~2× the smoothing delay (~0.25 s), or receivers are reacting to
659
659
  history no matter how correct the code is.
660
660
 
661
+ **Packed animation-flag bitfields are the sneakiest violation.** Smoothing lerps EVERY numeric
662
+ field — there is no integer exemption — and a lerped bitfield decodes to garbage: with
663
+ `moving=1, running=2, grounded=8`, walking is `f=9` and running is `f=11`, and the interpolation
664
+ passes through `10`, where `10 & moving === 0` — the sprinting opponent renders as *standing
665
+ still*. Worse, the exponential ease approaches an increasing target from below, so truncation
666
+ reads `target - 1` for the whole approach (~1.5 s median before RUN appears; at a vsync-locked
667
+ frame rate it can stick one ULP below the target *forever*). A field-verified failure: a
668
+ playtester swore he was running while his opponent's screen showed him walking — both were right.
669
+ Always drive remote animation from `stateRaw`:
670
+
671
+ ```js
672
+ avatar.updateFromFlags(p.stateRaw.f ?? 0, dt); // flags are DISCRETE — never p.state.f
673
+ ```
674
+
675
+ **Reaction one-shots on remotes fire on EDGES, never per frame.** If you replay a remote's
676
+ death/hit pose from their published `out`/`hp` state, track the previous value and act only on
677
+ the transition — a per-frame `reset()`/`playDown()` restarts or cancels every in-flight reaction
678
+ clip (and re-writing ghost/material flags 60×/s is pure churn).
679
+
661
680
  ## Shared objects (the ball, the NPC) — use `objects`, never `shared`
662
681
 
663
682
  A ball belongs to no player. Put it on `objects`: exactly one client owns it at a time (the SDK +