@genex-ai/cli-demo 0.85.0-dev.216 → 0.86.0-dev.217
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.86.0-dev.217",
|
|
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
|
}
|
|
@@ -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 +
|