incanto 0.48.0 → 0.50.0

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.
Files changed (54) hide show
  1. package/bin/incanto-model.mjs +13 -1
  2. package/bin/incanto-play.mjs +42 -17
  3. package/bin/incanto-playtest.mjs +7 -0
  4. package/bin/incanto-verify.mjs +10 -2
  5. package/dist/2d.d.ts +21 -12
  6. package/dist/2d.js +57 -3
  7. package/dist/3d.d.ts +38 -1
  8. package/dist/3d.js +114 -4
  9. package/dist/{create-game-CniOiWzN.js → create-game-BLDjy_PW.js} +5 -5
  10. package/dist/{create-game-akjPkFv5.js → create-game-DqqxEax1.js} +5 -5
  11. package/dist/{duplicate-CRtihGmC.js → duplicate-CI9WF_bg.js} +1 -1
  12. package/dist/{environment-presets-D1b0ydTS.js → environment-presets-XFuqu5jv.js} +15 -20
  13. package/dist/{gameplay-BQOeAid6.js → gameplay-DbaI313d.js} +96 -11
  14. package/dist/gameplay.d.ts +17 -0
  15. package/dist/gameplay.js +1 -1
  16. package/dist/index.d.ts +40 -2
  17. package/dist/index.js +7 -7
  18. package/dist/{loader-r49nDwB4.js → loader-DwazzlQb.js} +36 -6
  19. package/dist/net.js +3 -3
  20. package/dist/{physics-2d-BmgXBNDB.js → physics-2d-_9VBOHn6.js} +4 -4
  21. package/dist/{physics-3d-CSoGjM8P.js → physics-3d-DrpF5hcG.js} +9 -9
  22. package/dist/react.js +1 -1
  23. package/dist/{register-R2JTnIMw.js → register-BNPZYJmd.js} +23 -22
  24. package/dist/{register-D651it1J.js → register-CB11yp21.js} +2 -2
  25. package/dist/{register-BSXV8T9F.js → register-uvaZj1KX.js} +62 -2
  26. package/dist/{replay-CAphXMyM.d.ts → replay-DYdy1wb0.d.ts} +5 -1
  27. package/dist/{replay-DilbZgQI.js → replay-j-m6lJ4W.js} +23 -3
  28. package/dist/sprite-animation-CMr6f1K2.d.ts +44 -0
  29. package/dist/{particle-sim-Bw7hB93B.js → sprite-animation-D_p28jwU.js} +63 -1
  30. package/dist/{src-Bl1Kire1.js → src-B3HrKuAi.js} +1 -1
  31. package/dist/{test-E4-otKqK.js → test-D16igj4C.js} +245 -38
  32. package/dist/test.d.ts +39 -7
  33. package/dist/test.js +2 -2
  34. package/dist/vite.js +2 -2
  35. package/editor/assets/{agent8-_007gPF8.js → agent8-t3kl5q9K.js} +1 -1
  36. package/editor/assets/{debug-0DI_MJaq.js → debug-Bu3eeAlO.js} +1 -1
  37. package/editor/assets/{index-B-6eYZEi.js → index-Df5g8ofT.js} +92 -92
  38. package/editor/index.html +1 -1
  39. package/package.json +1 -1
  40. package/schemas/scene.schema.json +10 -3
  41. package/skills/incanto-3d-character.md +13 -2
  42. package/skills/incanto-3d-models.md +40 -0
  43. package/skills/incanto-assets.md +15 -0
  44. package/skills/incanto-building-2d-games.md +57 -5
  45. package/skills/incanto-building-3d-games.md +7 -0
  46. package/skills/incanto-gameplay-behaviors.md +29 -1
  47. package/skills/incanto-hud.md +39 -0
  48. package/skills/incanto-node-reference.md +5 -3
  49. package/skills/incanto-physics-and-input.md +1 -1
  50. package/skills/incanto-playtesting.md +22 -4
  51. package/skills/incanto-verifying-your-game.md +14 -0
  52. package/templates-app/beacon-isle-3d/package.json +1 -1
  53. package/templates-app/tps-3d/package.json +1 -1
  54. package/templates-app/village-quest-3d/package.json +1 -1
@@ -353,7 +353,19 @@ if (args.json) {
353
353
  // The verdict is the part you act on, so it goes last — closest to the
354
354
  // prompt, where a reader stops.
355
355
  out.push('', verdictText(verdict));
356
- if (verdict.sceneJson) {
356
+ // A rig that takes the library clips gets the WHOLE character: the model on
357
+ // its own is a statue, and the steps from there (a dynamic body under the
358
+ // controller, the skin fitted to the capsule and dropped to its feet, the
359
+ // clips declared, the input actions that make anything read the keyboard)
360
+ // are documented in four different places and all needed at once.
361
+ if (verdict.characterJson) {
362
+ const { assets, input, node } = verdict.characterJson;
363
+ out.push(
364
+ '',
365
+ 'paste a playable character into your scene:',
366
+ JSON.stringify({ assets, input, node }, null, 2),
367
+ );
368
+ } else if (verdict.sceneJson) {
357
369
  const { assets, node } = verdict.sceneJson;
358
370
  out.push(
359
371
  '',
@@ -62,8 +62,11 @@ wheel/capture/describe/framing/logs/quit), JSON-line responses on stdout.`);
62
62
  }
63
63
 
64
64
  const out = (obj) => process.stdout.write(`${JSON.stringify(obj)}\n`);
65
- const fail = (cmd, e) =>
65
+ let failures = 0;
66
+ const fail = (cmd, e) => {
67
+ failures += 1;
66
68
  out({ ok: false, cmd, error: { code: e?.code ?? 'ERROR', message: e?.message ?? String(e) } });
69
+ };
67
70
 
68
71
  const { createPlaySession } = await import(pathToFileURL(join(PKG, 'dist', 'test.js')).href);
69
72
  const incanto = await import(pathToFileURL(join(PKG, 'dist', 'index.js')).href);
@@ -204,21 +207,43 @@ function handle(line) {
204
207
  }
205
208
 
206
209
  if (args.commands) {
207
- for (const line of readFileSync(resolve(args.commands), 'utf-8').split('\n')) {
208
- if (!handle(line)) process.exit(0);
210
+ const text = readFileSync(resolve(args.commands), 'utf-8');
211
+ // A REPLAY, not a command script. `incanto-playtest` writes its failing runs
212
+ // as ReplayJson and the docs point this flag at them; reading one as text
213
+ // rejected every line as UNKNOWN_COMMAND and still exited 0, so the tool's
214
+ // whole payoff — "the failure comes back as a file you can watch" — silently
215
+ // did nothing.
216
+ const recording = text.trimStart().startsWith('{') ? JSON.parse(text) : null;
217
+ if (recording?.replay) {
218
+ incanto.replay(session.engine, recording);
219
+ out({
220
+ ok: true,
221
+ cmd: 'replay',
222
+ frames: recording.ticks.length,
223
+ events: recording.events.length,
224
+ });
225
+ out({ ok: true, cmd: 'describe', text: session.describe() });
226
+ session.dispose();
227
+ process.exitCode = 0;
228
+ } else {
229
+ for (const line of text.split('\n')) {
230
+ if (!handle(line)) break;
231
+ }
232
+ session.dispose();
233
+ // A script whose every line was rejected must not report success: that is
234
+ // exactly how a replay pointed at the wrong reader stayed invisible.
235
+ process.exitCode = failures > 0 ? 1 : 0;
209
236
  }
210
- session.dispose();
211
- process.exit(0);
212
- }
213
-
214
- const rl = createInterface({ input: process.stdin });
215
- rl.on('line', (line) => {
216
- if (!handle(line)) {
217
- rl.close();
237
+ } else {
238
+ const rl = createInterface({ input: process.stdin });
239
+ rl.on('line', (line) => {
240
+ if (!handle(line)) {
241
+ rl.close();
242
+ process.exit(0);
243
+ }
244
+ });
245
+ rl.on('close', () => {
246
+ session.dispose();
218
247
  process.exit(0);
219
- }
220
- });
221
- rl.on('close', () => {
222
- session.dispose();
223
- process.exit(0);
224
- });
248
+ });
249
+ }
@@ -34,6 +34,8 @@ function parseArgs(argv) {
34
34
  else if (a === '--runs') args.runs = Number(argv[++i]);
35
35
  else if (a === '--seconds') args.seconds = Number(argv[++i]);
36
36
  else if (a === '--seed') args.seed = Number(argv[++i]);
37
+ else if (a === '--fall-below') args.fallBelow = Number(argv[++i]);
38
+ else if (a === '--reach-radius') args.reachRadius = Number(argv[++i]);
37
39
  else if (a === '--out') args.out = argv[++i];
38
40
  else if (a === '--no-replays') args.noReplays = true;
39
41
  else if (a === '--json') args.json = true;
@@ -53,6 +55,9 @@ if (args.help || !args.scene) {
53
55
  --runs N independent seeded runs (default 20)
54
56
  --seconds N simulated seconds per run before calling it stuck (default 60)
55
57
  --seed N first seed; run i uses seed+i (default 1)
58
+ --fall-below Y left the world past this y (default: 50 under the spawn in
59
+ 3D, 1000 px under it in 2D — 2D counts DOWN the screen)
60
+ --reach-radius R how close counts as "reached" (default 2 in 3D, 32 px in 2D)
56
61
  --behaviors FILE your Behavior subclasses (.ts works on node >= 23.6 / bun)
57
62
  --out DIR where failing replays go (default .incanto/playtest)
58
63
  --no-replays report only, write nothing
@@ -101,6 +106,8 @@ try {
101
106
  seed: args.seed,
102
107
  behaviors,
103
108
  stubMissingBehaviors: !args.behaviors,
109
+ ...(Number.isFinite(args.fallBelow) ? { fallBelow: args.fallBelow } : {}),
110
+ ...(Number.isFinite(args.reachRadius) ? { reachRadius: args.reachRadius } : {}),
104
111
  // sub-scenes resolve relative to the scene file, as the loader does
105
112
  resolveScene: (p) => JSON.parse(readFileSync(resolve(sceneDir, p), 'utf-8')),
106
113
  });
@@ -50,10 +50,18 @@ NOT MEASURED rather than failed — a missing measurement is not a broken game.`
50
50
  process.exit(0);
51
51
  }
52
52
 
53
- /** The scene to verify: the one named, or the only one that can be found. */
53
+ /**
54
+ * The scene to verify: the one named, or the only one that can be found.
55
+ *
56
+ * ALWAYS the same shape. It used to return a bare string for a named scene and
57
+ * an object for a search, and the caller destructures — so `{ scene } = "a.json"`
58
+ * was `undefined` and naming your scene was the one way to make the whole
59
+ * ladder do nothing. It only ever worked when you named nothing, which is how
60
+ * it was run while being written.
61
+ */
54
62
  function findScene() {
55
63
  const named = argv.find((a) => !a.startsWith('-') && a.endsWith('.json'));
56
- if (named) return named;
64
+ if (named) return { scene: named, candidates: [named] };
57
65
  const found = [];
58
66
  const walk = (dir, depth) => {
59
67
  if (depth > 4 || found.length > 8) return;
package/dist/2d.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Et as Node, P as Scene$1, S as Scheduler, T as RendererStats, b as Engi
2
2
  import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DAvWQeld.js";
3
3
  import { i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
4
4
  import { t as LoadSceneOptions } from "./loader-CeyU_bm1.js";
5
+ import { n as AnimationEntry } from "./sprite-animation-CMr6f1K2.js";
5
6
  import { t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
6
7
  import { Group, Mesh, Object3D, Scene, Texture } from "three";
7
8
  import * as RapierNs from "@dimforge/rapier2d-compat";
@@ -135,10 +136,16 @@ declare class RigidBody2D extends PhysicsBody2D {
135
136
  declare class CharacterBody2D extends PhysicsBody2D {
136
137
  static override readonly typeName: string;
137
138
  static override readonly props: PropSchema;
139
+ /**
140
+ * Renamed to match the 3D body, where `snapToGround` had to give the name
141
+ * back to `Node3D`'s PLACEMENT prop. 2D has no placement prop, so nothing is
142
+ * ambiguous here and old scenes keep loading unchanged.
143
+ */
144
+ static readonly propAliases: Record<string, string>;
138
145
  /** px/s, y-down (up = -y). */
139
146
  velocity: number[];
140
147
  /** Keep contact on slopes/steps while not moving upward. */
141
- snapToGround: boolean;
148
+ stickToGround: boolean;
142
149
  /** Max climbable slope angle. */
143
150
  slopeLimitDeg: number;
144
151
  /** @internal Updated by Physics2D.moveAndSlide. */
@@ -373,6 +380,11 @@ interface SpriteFromLibraryResult {
373
380
  * Turn a library sprite-animation JSON into a scene-ready spritesheet asset
374
381
  * declaration + AnimatedSprite2D props. `autoplay` defaults to the first
375
382
  * LOOPING animation (idle-like), falling back to the first one.
383
+ *
384
+ * The animation map also gets ALIASES for the movement states a character
385
+ * controller emits but the sheet does not name (`run` → `move`, `fall` →
386
+ * `idle`), so the converted props and the documented
387
+ * `movementStateChanged → play` wiring work together out of the box.
376
388
  */
377
389
  declare function spriteFromLibraryMeta(meta: unknown, opts: {
378
390
  url: string;
@@ -411,15 +423,6 @@ declare class Sprite2D extends Node2D {
411
423
  }
412
424
  //#endregion
413
425
  //#region src/2d/nodes/animated-sprite-2d.d.ts
414
- interface AnimationDef {
415
- /**
416
- * Two numbers = inclusive [start, end] frame RANGE.
417
- * Any other length = explicit frame list.
418
- */
419
- frames: number[];
420
- fps: number;
421
- loop?: boolean;
422
- }
423
426
  /**
424
427
  * Spritesheet animation: pure-JSON animation map, frame selection via a UV
425
428
  * window on the node's own texture clone. Frames advance in `update(dt)` —
@@ -431,7 +434,7 @@ declare class AnimatedSprite2D extends Sprite2D {
431
434
  static override readonly props: PropSchema;
432
435
  /** `'$assetKey'` of a spritesheet asset. */
433
436
  sheet: string;
434
- animations: Record<string, AnimationDef>;
437
+ animations: Record<string, AnimationEntry>;
435
438
  autoplay: string;
436
439
  playing: boolean;
437
440
  /** Absolute frame index within the sheet. */
@@ -440,8 +443,14 @@ declare class AnimatedSprite2D extends Sprite2D {
440
443
  private frameList;
441
444
  private frameIndex;
442
445
  private frameTime;
446
+ private currentDef;
443
447
  private ownTexture;
444
448
  private loadedSheetRef;
449
+ /**
450
+ * `currentAnimation` reports the clip that is RUNNING, not the name asked
451
+ * for: an alias is a rename, and a state machine that reads back `fall` while
452
+ * the idle frames play is describing something that is not happening.
453
+ */
445
454
  play(name: string): void;
446
455
  stop(): void;
447
456
  override onReady(): void;
@@ -920,4 +929,4 @@ declare function parseCells(cells: readonly (string | readonly number[])[], lege
920
929
  */
921
930
  declare function mergeSolidRects(grid: readonly (readonly number[])[], solid: Set<number>): TileRect[];
922
931
  //#endregion
923
- export { AnimatedSprite2D, type AnimationDef, Area2D, type AssetStatus, AssetStore2D, Camera2D, CharacterBody2D, CharacterController2D, ColorRect2D, type CreateGame2DOptions, type Game2D, Joint2D, type JointType2D, Label, Node2D, Particles2D, Physics2D, type Physics2DOptions, PhysicsBody2D, Renderer2D, type Renderer2DOptions, type ResolvedSpriteTexture, RigidBody2D, type SheetInfo, Sprite2D, type SpriteFromLibraryResult, StaticBody2D, type Sync2DResult, type TextureLoadCallbacks, TileMap2D, type TileRect, type UIAnchor, UILayer, createGame2D, enablePhysics2D, mergeSolidRects, parseCells, registerNodes2D, spriteFromLibraryMeta, syncTree2D };
932
+ export { AnimatedSprite2D, Area2D, type AssetStatus, AssetStore2D, Camera2D, CharacterBody2D, CharacterController2D, ColorRect2D, type CreateGame2DOptions, type Game2D, Joint2D, type JointType2D, Label, Node2D, Particles2D, Physics2D, type Physics2DOptions, PhysicsBody2D, Renderer2D, type Renderer2DOptions, type ResolvedSpriteTexture, RigidBody2D, type SheetInfo, Sprite2D, type SpriteFromLibraryResult, StaticBody2D, type Sync2DResult, type TextureLoadCallbacks, TileMap2D, type TileRect, type UIAnchor, UILayer, createGame2D, enablePhysics2D, mergeSolidRects, parseCells, registerNodes2D, spriteFromLibraryMeta, syncTree2D };
package/dist/2d.js CHANGED
@@ -1,12 +1,65 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
- import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-CniOiWzN.js";
3
- import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-R2JTnIMw.js";
4
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-BmgXBNDB.js";
2
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-BLDjy_PW.js";
3
+ import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-BNPZYJmd.js";
4
+ import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-_9VBOHn6.js";
5
5
  //#region src/2d/library-sprite.ts
6
6
  /**
7
+ * What a `CharacterController2D`/`3D` will ask a skin to play, and the clip in
8
+ * a library sheet that answers for it.
9
+ *
10
+ * The two vocabularies never met: the controller emits `run`, every library
11
+ * sheet calls it `move`, and no sheet anywhere ships a `fall` — so the wiring
12
+ * the 2D skill documents in one line (`movementStateChanged → play`) asked for
13
+ * animations that do not exist, on artwork from the engine's own catalog.
14
+ *
15
+ * These are ALIASES, so they read as what they are, and the author can point
16
+ * any of them somewhere better.
17
+ */
18
+ const CONTROLLER_STATES = [
19
+ ["run", [
20
+ "run",
21
+ "move",
22
+ "walk"
23
+ ]],
24
+ ["jump", ["jump", "idle"]],
25
+ ["fall", [
26
+ "fall",
27
+ "jump",
28
+ "idle"
29
+ ]],
30
+ ["wallSlide", ["wallSlide", "idle"]],
31
+ ["dash", [
32
+ "dash",
33
+ "run",
34
+ "move",
35
+ "idle"
36
+ ]]
37
+ ];
38
+ /**
39
+ * Fill in the states the sheet does not name, and touch nothing it does.
40
+ *
41
+ * Silent when there is no `idle` to fall back on: a spinning-coin sheet is not
42
+ * a character, and inventing movement states for it would put clips in the map
43
+ * that mean nothing.
44
+ */
45
+ function addControllerStates(animations) {
46
+ const real = new Set(Object.keys(animations));
47
+ if (!real.has("idle")) return;
48
+ for (const [state, candidates] of CONTROLLER_STATES) {
49
+ if (real.has(state)) continue;
50
+ const target = candidates.find((c) => c !== state && real.has(c));
51
+ if (target) animations[state] = target;
52
+ }
53
+ }
54
+ /**
7
55
  * Turn a library sprite-animation JSON into a scene-ready spritesheet asset
8
56
  * declaration + AnimatedSprite2D props. `autoplay` defaults to the first
9
57
  * LOOPING animation (idle-like), falling back to the first one.
58
+ *
59
+ * The animation map also gets ALIASES for the movement states a character
60
+ * controller emits but the sheet does not name (`run` → `move`, `fall` →
61
+ * `idle`), so the converted props and the documented
62
+ * `movementStateChanged → play` wiring work together out of the box.
10
63
  */
11
64
  function spriteFromLibraryMeta(meta, opts) {
12
65
  const m = meta;
@@ -25,6 +78,7 @@ function spriteFromLibraryMeta(meta, opts) {
25
78
  firstName ??= name;
26
79
  if (loop && firstLooping === null) firstLooping = name;
27
80
  }
81
+ addControllerStates(animations);
28
82
  const autoplay = opts.autoplay ?? firstLooping ?? firstName;
29
83
  return {
30
84
  asset: {
package/dist/3d.d.ts CHANGED
@@ -370,9 +370,27 @@ declare class CharacterBody3D extends PhysicsBody3D {
370
370
  static override readonly props: PropSchema;
371
371
  /** m/s, y-up (up = +y). */
372
372
  velocity: number[];
373
- snapToGround: boolean;
373
+ /**
374
+ * Hold the capsule against the surface while it moves, so walking down a
375
+ * slope does not become a series of small falls.
376
+ *
377
+ * Named apart from `Node3D.snapToGround` (which PLACES a node on the ground
378
+ * at load) because sharing the name silently broke both: the boolean shadowed
379
+ * the placement prop, its `true` default made the placement pass teleport
380
+ * every character body in a terrain scene, and a lift value was rejected
381
+ * outright — so the only escape from the teleport also turned this off.
382
+ */
383
+ stickToGround: boolean;
374
384
  /** Max climbable slope angle. */
375
385
  slopeLimitDeg: number;
386
+ /**
387
+ * `snapToGround` on a character body is ambiguous, so it is refused.
388
+ *
389
+ * A boolean here used to mean the ground-stick, and as placement it means
390
+ * "leave y alone" — which is already the default. Rather than pick one and
391
+ * silently do the other thing to an existing scene, say which prop was meant.
392
+ */
393
+ override onReady(): void;
376
394
  /** @internal Updated by Physics3D.moveAndSlide. */
377
395
  _grounded: boolean;
378
396
  moveAndSlide(): void;
@@ -1006,6 +1024,25 @@ interface ModelVerdict {
1006
1024
  /** Clip names baked into the file itself (playable by name, no asset entry). */
1007
1025
  embeddedClips: string[];
1008
1026
  sceneJson: SceneJson | null;
1027
+ /**
1028
+ * The whole playable character — body, controller, skin, clips and input —
1029
+ * for a rig that can take the library's locomotion clips. Null for a prop.
1030
+ *
1031
+ * The model on its own is a statue, and every step from there is documented
1032
+ * somewhere different: the controller must sit under a dynamic body, the skin
1033
+ * must be sized to the capsule and dropped to its feet, the clips must be
1034
+ * declared, and the input actions must exist or nothing reads the keyboard.
1035
+ */
1036
+ characterJson: CharacterJson | null;
1037
+ }
1038
+ interface CharacterJson {
1039
+ assets: Record<string, {
1040
+ type: string;
1041
+ url: string;
1042
+ }>;
1043
+ /** The actions `CharacterController3D` reads — without them it never moves. */
1044
+ input: Record<string, unknown>;
1045
+ node: Record<string, unknown>;
1009
1046
  }
1010
1047
  interface VerdictTarget {
1011
1048
  url: string;
package/dist/3d.js CHANGED
@@ -1,9 +1,9 @@
1
- import { A as Water3D, F as PhysicsBody3D, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D, V as WATER_MAX_RIPPLES, j as WATER_CUTOUT_MAX } from "./gameplay-BQOeAid6.js";
2
- import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-D1b0ydTS.js";
3
- import { a as Environment3D, c as parseEnvironment3D, d as AssetStore3D, i as syncTree, l as sunDirectionFromElevationAzimuth, o as setEnvironment3D, r as Renderer3D, s as horizonColorFromSky, t as createGame3D, u as sunDirectionFromSky } from "./create-game-akjPkFv5.js";
1
+ import { A as Water3D, F as PhysicsBody3D, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D, V as WATER_MAX_RIPPLES, j as WATER_CUTOUT_MAX } from "./gameplay-DbaI313d.js";
2
+ import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-XFuqu5jv.js";
3
+ import { a as Environment3D, c as parseEnvironment3D, d as AssetStore3D, i as syncTree, l as sunDirectionFromElevationAzimuth, o as setEnvironment3D, r as Renderer3D, s as horizonColorFromSky, t as createGame3D, u as sunDirectionFromSky } from "./create-game-DqqxEax1.js";
4
4
  import { a as frameSignature, n as diffSignatures, o as frameStats, r as diffText, s as frameText, t as SIGNATURE_GRID } from "./frame-report-njybhZon.js";
5
5
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
6
- import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-CSoGjM8P.js";
6
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-DrpF5hcG.js";
7
7
  //#region src/3d/model-verdict.ts
8
8
  /** Mixamo exports every bone as `mixamorigX`; the retargeter binds by that name. */
9
9
  const MIXAMO = /^mixamorig[:_]?/i;
@@ -25,6 +25,115 @@ function rigOf(facts) {
25
25
  if (facts.nodes.some((n) => MIXAMO.test(n.name))) return "mixamorig";
26
26
  return facts.skins > 0 ? "other" : "none";
27
27
  }
28
+ /**
29
+ * The locomotion clips every mixamorig character can play as they are.
30
+ *
31
+ * They are not art you go looking for — they are the rig's vocabulary, they
32
+ * retarget onto anything mixamo-shaped (and through the humanoid map onto a
33
+ * VRM), and this repo's own examples reference them 29 times. Nothing shipped
34
+ * ever told an agent where they live, so a character found through an asset MCP
35
+ * arrived with no way to walk.
36
+ *
37
+ * Keyed by the states `CharacterController3D` emits, so the map below IS the
38
+ * controller's `animations` prop.
39
+ */
40
+ const CLIP_BASE = "https://agent8-games.verse8.io/assets/3d/animations/mixamorig";
41
+ const LOCOMOTION_CLIPS = {
42
+ idle: `${CLIP_BASE}/idle-00.glb`,
43
+ walk: `${CLIP_BASE}/walk.glb`,
44
+ run: `${CLIP_BASE}/run-medium.glb`,
45
+ fastRun: `${CLIP_BASE}/run-fast.glb`,
46
+ airborne: `${CLIP_BASE}/jump.glb`
47
+ };
48
+ /** The capsule the shipped templates use, and the skin fitted to it. */
49
+ const BODY = {
50
+ radius: .34,
51
+ height: 1
52
+ };
53
+ /**
54
+ * A found character, as a thing that walks.
55
+ *
56
+ * The model alone draws a statue: `CharacterController3D` must sit under a
57
+ * dynamic body, the skin has to be sized to the capsule and dropped to its
58
+ * feet, the clips have to be declared, and the input actions have to exist or
59
+ * nothing reads the keyboard. Every one of those is a documented step and all
60
+ * of them are needed at once, which is exactly the shape of thing worth
61
+ * printing rather than describing.
62
+ */
63
+ function characterJson(target) {
64
+ const assets = { [target.key]: {
65
+ type: "model",
66
+ url: target.url
67
+ } };
68
+ const animations = {};
69
+ for (const [state, url] of Object.entries(LOCOMOTION_CLIPS)) {
70
+ const key = `anims/${state}`;
71
+ assets[key] = {
72
+ type: "animation",
73
+ url
74
+ };
75
+ animations[state] = `$${key}`;
76
+ }
77
+ return {
78
+ assets,
79
+ input: {
80
+ move: {
81
+ type: "vector2",
82
+ keys: {
83
+ up: ["KeyW"],
84
+ down: ["KeyS"],
85
+ left: ["KeyA"],
86
+ right: ["KeyD"]
87
+ }
88
+ },
89
+ jump: {
90
+ type: "button",
91
+ keys: ["Space"]
92
+ },
93
+ sprint: {
94
+ type: "button",
95
+ keys: ["ShiftLeft"]
96
+ }
97
+ },
98
+ node: {
99
+ name: "Player",
100
+ type: "RigidBody3D",
101
+ groups: ["player"],
102
+ props: {
103
+ fixedRotation: true,
104
+ friction: 0,
105
+ collider: {
106
+ shape: "capsule",
107
+ radius: BODY.radius,
108
+ height: BODY.height
109
+ }
110
+ },
111
+ children: [{
112
+ name: "Controller",
113
+ type: "CharacterController3D",
114
+ props: {
115
+ view: "free",
116
+ coyoteSeconds: .12,
117
+ jumpBufferSeconds: .15,
118
+ animations
119
+ }
120
+ }, {
121
+ name: "Skin",
122
+ type: "ModelInstance3D",
123
+ props: {
124
+ model: `$${target.key}`,
125
+ targetHeight: BODY.height + 2 * BODY.radius,
126
+ position: [
127
+ 0,
128
+ -(BODY.height / 2 + BODY.radius),
129
+ 0
130
+ ],
131
+ castShadow: true
132
+ }
133
+ }]
134
+ }
135
+ };
136
+ }
28
137
  function modelVerdict(facts, target) {
29
138
  const rig = rigOf(facts);
30
139
  const kind = facts.meshes === 0 && facts.animations.length > 0 ? "animation" : "model";
@@ -51,6 +160,7 @@ function modelVerdict(facts, target) {
51
160
  originOffset: offset,
52
161
  groundedAtOrigin: grounded,
53
162
  embeddedClips: facts.animations.map((a) => a.name),
163
+ characterJson: target && kind === "model" && (rig === "mixamorig" || rig === "vrm-humanoid") ? characterJson(target) : null,
54
164
  sceneJson: target ? {
55
165
  assets: { [target.key]: {
56
166
  type: kind,
@@ -1,13 +1,13 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { n as loadScene, o as computeViewport, s as resolveViewport, v as diagnose, w as registerBehavior } from "./loader-r49nDwB4.js";
3
- import { h as Engine, m as AudioPlayer } from "./register-BSXV8T9F.js";
2
+ import { n as loadScene, o as computeViewport, s as resolveViewport, v as diagnose, w as registerBehavior } from "./loader-DwazzlQb.js";
3
+ import { h as Engine, m as AudioPlayer } from "./register-uvaZj1KX.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-BoNg_MnF.js";
6
6
  import { a as openBundledEditor, i as devServerLibrary, n as pauseWhenHidden, r as crossFade, t as teardown } from "./teardown-BKTCzLek.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-BQOeAid6.js";
8
- import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-R2JTnIMw.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-DbaI313d.js";
8
+ import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-BNPZYJmd.js";
9
9
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
10
- import { n as enablePhysics2D } from "./physics-2d-BmgXBNDB.js";
10
+ import { n as enablePhysics2D } from "./physics-2d-_9VBOHn6.js";
11
11
  import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
12
12
  //#region src/2d/assets.ts
13
13
  /**
@@ -1,15 +1,15 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { n as loadScene, v as diagnose, w as registerBehavior } from "./loader-r49nDwB4.js";
3
- import { _ as qualityCaps, h as Engine, m as AudioPlayer, y as qualityRendering } from "./register-BSXV8T9F.js";
2
+ import { n as loadScene, v as diagnose, w as registerBehavior } from "./loader-DwazzlQb.js";
3
+ import { _ as qualityCaps, h as Engine, m as AudioPlayer, y as qualityRendering } from "./register-uvaZj1KX.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { r as parseDrive, t as logReport } from "./log-report-lxrQY9cH.js";
6
6
  import { i as resolveRendering, n as attachTouchControls } from "./touch-BoNg_MnF.js";
7
7
  import { a as openBundledEditor, i as devServerLibrary, n as pauseWhenHidden, o as poseFromRenderer, r as crossFade, t as teardown } from "./teardown-BKTCzLek.js";
8
- import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-BQOeAid6.js";
8
+ import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-DbaI313d.js";
9
9
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
10
- import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-D1b0ydTS.js";
10
+ import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-XFuqu5jv.js";
11
11
  import { a as frameSignature, i as frameImage, o as frameStats } from "./frame-report-njybhZon.js";
12
- import { n as enablePhysics3D } from "./physics-3d-CSoGjM8P.js";
12
+ import { n as enablePhysics3D } from "./physics-3d-DrpF5hcG.js";
13
13
  import { ACESFilmicToneMapping, AmbientLight, Box3, BufferAttribute, BufferGeometry, Color, DepthTexture, EquirectangularReflectionMapping, FloatType, Fog, HalfFloatType, LineBasicMaterial, LineSegments, Matrix4, Mesh, PCFShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, Quaternion, Raycaster, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
14
14
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
15
15
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
@@ -1,4 +1,4 @@
1
- import { i as serializeNode, t as buildNodeJson } from "./loader-r49nDwB4.js";
1
+ import { i as serializeNode, t as buildNodeJson } from "./loader-DwazzlQb.js";
2
2
  //#region src/core/scene/duplicate.ts
3
3
  function stripUids(json) {
4
4
  const { uid: _uid, ...rest } = json;
@@ -1,10 +1,10 @@
1
- import { t as registerCoreNodes } from "./register-BSXV8T9F.js";
1
+ import { t as registerCoreNodes } from "./register-uvaZj1KX.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { t as Rng } from "./rng-DP-SR7eg.js";
4
4
  import { i as getNodeSchema, l as registerNode } from "./registry-IyWCGe4q.js";
5
5
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
6
- import { i as applyParticlePreset, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-Bw7hB93B.js";
7
- import { A as Water3D, F as PhysicsBody3D, H as colliderFootDrop, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D } from "./gameplay-BQOeAid6.js";
6
+ import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimationAliases, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-D_p28jwU.js";
7
+ import { A as Water3D, F as PhysicsBody3D, H as colliderFootDrop, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D } from "./gameplay-DbaI313d.js";
8
8
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
9
9
  import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, ClampToEdgeWrapping, Color, ConeGeometry, CylinderGeometry, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, Group, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NoColorSpace, NormalBlending, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector3, Vector4, VectorKeyframeTrack } from "three";
10
10
  import { clone } from "three/addons/utils/SkeletonUtils.js";
@@ -2112,13 +2112,19 @@ var AnimatedSprite3D = class extends Sprite3D {
2112
2112
  frameList = [];
2113
2113
  frameIndex = 0;
2114
2114
  frameTime = 0;
2115
+ currentDef = null;
2115
2116
  sheetTexture = null;
2116
2117
  loadedSheetRef = "";
2118
+ /**
2119
+ * `currentAnimation` reports the clip that is RUNNING, not the name asked
2120
+ * for: an alias is a rename, and a state machine that reads back `fall` while
2121
+ * the idle frames play is describing something that is not happening.
2122
+ */
2117
2123
  play(name) {
2118
- const def = this.animations[name];
2119
- if (!def) throw new IncantoError("UNKNOWN_ANIMATION", `No animation '${name}' on '${this.name}'. Available: [${Object.keys(this.animations).join(", ")}].`);
2120
- this.currentAnimation = name;
2121
- this.frameList = resolveFrames(def.frames, name);
2124
+ const { name: resolved, def } = resolveAnimation(this.animations, name, this.name);
2125
+ this.currentAnimation = resolved;
2126
+ this.currentDef = def;
2127
+ this.frameList = resolveFrames(def.frames, resolved);
2122
2128
  this.frameIndex = 0;
2123
2129
  this.currentFrame = this.frameList[0] ?? 0;
2124
2130
  this.frameTime = 0;
@@ -2128,11 +2134,12 @@ var AnimatedSprite3D = class extends Sprite3D {
2128
2134
  this.playing = false;
2129
2135
  }
2130
2136
  onReady() {
2137
+ validateAnimationAliases(this.animations, this.name);
2131
2138
  if (this.autoplay) this.play(this.autoplay);
2132
2139
  }
2133
2140
  update(dt) {
2134
2141
  if (!this.playing || this.currentAnimation === "") return;
2135
- const def = this.animations[this.currentAnimation];
2142
+ const def = this.currentDef;
2136
2143
  if (!def || !Number.isFinite(def.fps) || def.fps <= 0) return;
2137
2144
  const perFrame = 1 / def.fps;
2138
2145
  this.frameTime += dt;
@@ -2173,18 +2180,6 @@ var AnimatedSprite3D = class extends Sprite3D {
2173
2180
  super.free();
2174
2181
  }
2175
2182
  };
2176
- function resolveFrames(frames, name) {
2177
- if (!Array.isArray(frames) || frames.length === 0) throw new IncantoError("BAD_FORMAT", `Animation '${name}': "frames" must be a non-empty array.`);
2178
- if (frames.length === 2) {
2179
- const [start, end] = frames;
2180
- if (end >= start) {
2181
- const out = [];
2182
- for (let f = start; f <= end; f++) out.push(f);
2183
- return out;
2184
- }
2185
- }
2186
- return [...frames];
2187
- }
2188
2183
  //#endregion
2189
2184
  //#region src/3d/nodes/billboard-3d.ts
2190
2185
  const _parentQ = new Quaternion();