@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 +47 -6
- package/package.json +1 -1
- package/templates/controllers/character/character-animations.ts +116 -5
- package/templates/skills/genex-ai-character/SKILL.md +10 -0
- package/templates/skills/genex-game-director/SKILL.md +40 -18
- package/templates/skills/genex-game-director/references/design-contract.md +47 -15
- package/templates/skills/genex-threejs-character-controller/references/animations.md +28 -1
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +19 -0
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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 ===
|
|
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 ===
|
|
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
|
-
|
|
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(
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
106
|
-
| Quests & dialogue data | src/content/** | sub-agent
|
|
107
|
-
| Enemies/AI | src/enemies/** |
|
|
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/** |
|
|
173
|
-
| HUD chain | src/ui/** | sub-agent
|
|
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 +
|