incanto 0.47.0 → 0.49.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 (55) hide show
  1. package/bin/incanto-frame.mjs +58 -1
  2. package/bin/incanto-logs.mjs +127 -0
  3. package/bin/incanto-model.mjs +13 -1
  4. package/bin/incanto-playtest.mjs +7 -0
  5. package/bin/incanto-verify.mjs +41 -2
  6. package/dist/2d.d.ts +21 -12
  7. package/dist/2d.js +57 -3
  8. package/dist/3d.d.ts +38 -1
  9. package/dist/3d.js +114 -4
  10. package/dist/{create-game-CniOiWzN.js → create-game-BgV6UbVA.js} +5 -5
  11. package/dist/{create-game-D16MVIPO.js → create-game-DFBjMetZ.js} +83 -6
  12. package/dist/{duplicate-CRtihGmC.js → duplicate-CI9WF_bg.js} +1 -1
  13. package/dist/{environment-presets-D1b0ydTS.js → environment-presets-CZOH5TY5.js} +15 -20
  14. package/dist/{gameplay-BQOeAid6.js → gameplay-02Btmmjn.js} +80 -9
  15. package/dist/gameplay.d.ts +17 -0
  16. package/dist/gameplay.js +1 -1
  17. package/dist/index.d.ts +139 -1
  18. package/dist/index.js +8 -7
  19. package/dist/{loader-r49nDwB4.js → loader-DwazzlQb.js} +36 -6
  20. package/dist/log-report-lxrQY9cH.js +0 -0
  21. package/dist/net.js +3 -3
  22. package/dist/{physics-2d-BmgXBNDB.js → physics-2d-vyCBfACH.js} +3 -3
  23. package/dist/{physics-3d-CSoGjM8P.js → physics-3d-DpRqw8Mz.js} +4 -4
  24. package/dist/react.js +1 -1
  25. package/dist/{register-R2JTnIMw.js → register-BNPZYJmd.js} +23 -22
  26. package/dist/{register-D651it1J.js → register-CB11yp21.js} +2 -2
  27. package/dist/{register-BSXV8T9F.js → register-uvaZj1KX.js} +62 -2
  28. package/dist/{replay-DilbZgQI.js → replay-C0XJIsO7.js} +1 -1
  29. package/dist/sprite-animation-CMr6f1K2.d.ts +44 -0
  30. package/dist/{particle-sim-Bw7hB93B.js → sprite-animation-D_p28jwU.js} +63 -1
  31. package/dist/{src-Ca3oV1fe.js → src-DF4gCsqO.js} +1 -1
  32. package/dist/{test-E4-otKqK.js → test-DRna_BQU.js} +210 -30
  33. package/dist/test.d.ts +38 -6
  34. package/dist/test.js +2 -2
  35. package/dist/vite.js +41 -4
  36. package/editor/assets/{agent8-_007gPF8.js → agent8-DCW4TgDt.js} +1 -1
  37. package/editor/assets/{debug-0DI_MJaq.js → debug-RC6qts6S.js} +1 -1
  38. package/editor/assets/{index-B-6eYZEi.js → index-5dEIhvsf.js} +92 -92
  39. package/editor/index.html +1 -1
  40. package/package.json +3 -2
  41. package/schemas/scene.schema.json +10 -3
  42. package/skills/incanto-3d-character.md +13 -2
  43. package/skills/incanto-3d-models.md +40 -0
  44. package/skills/incanto-assets.md +15 -0
  45. package/skills/incanto-building-2d-games.md +57 -5
  46. package/skills/incanto-building-3d-games.md +7 -0
  47. package/skills/incanto-gameplay-behaviors.md +18 -1
  48. package/skills/incanto-hud.md +39 -0
  49. package/skills/incanto-node-reference.md +5 -3
  50. package/skills/incanto-physics-and-input.md +1 -1
  51. package/skills/incanto-playtesting.md +9 -2
  52. package/skills/incanto-verifying-your-game.md +67 -0
  53. package/templates-app/beacon-isle-3d/package.json +1 -1
  54. package/templates-app/tps-3d/package.json +1 -1
  55. package/templates-app/village-quest-3d/package.json +1 -1
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-D16MVIPO.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-02Btmmjn.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-CZOH5TY5.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-DFBjMetZ.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-DpRqw8Mz.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-02Btmmjn.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-vyCBfACH.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,14 +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
+ import { r as parseDrive, t as logReport } from "./log-report-lxrQY9cH.js";
5
6
  import { i as resolveRendering, n as attachTouchControls } from "./touch-BoNg_MnF.js";
6
7
  import { a as openBundledEditor, i as devServerLibrary, n as pauseWhenHidden, o as poseFromRenderer, r as crossFade, t as teardown } from "./teardown-BKTCzLek.js";
7
- 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-02Btmmjn.js";
8
9
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
9
- 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-CZOH5TY5.js";
10
11
  import { a as frameSignature, i as frameImage, o as frameStats } from "./frame-report-njybhZon.js";
11
- import { n as enablePhysics3D } from "./physics-3d-CSoGjM8P.js";
12
+ import { n as enablePhysics3D } from "./physics-3d-DpRqw8Mz.js";
12
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";
13
14
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
14
15
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
@@ -2455,17 +2456,69 @@ async function createGame3D(opts) {
2455
2456
  if (!capture) throw new IncantoError("TREE_VIOLATION", "this renderer cannot capture frames");
2456
2457
  return capture.call(renderer);
2457
2458
  };
2459
+ /**
2460
+ * Run a drive script against the LIVE game, in real time.
2461
+ *
2462
+ * `step` waits on the wall clock rather than advancing a fixed timestep: this
2463
+ * is the running game, with its own loop, its own physics and its own
2464
+ * animation. Stepping it by hand would fight all three.
2465
+ */
2466
+ const applyDrive = async (steps) => {
2467
+ let frames = 0;
2468
+ let watching = true;
2469
+ const tick = () => {
2470
+ if (!watching) return;
2471
+ frames += 1;
2472
+ requestAnimationFrame(tick);
2473
+ };
2474
+ requestAnimationFrame(tick);
2475
+ for (const s of steps) switch (s.kind) {
2476
+ case "press":
2477
+ engine.input.pressAction(s.action);
2478
+ break;
2479
+ case "release":
2480
+ engine.input.releaseAction(s.action);
2481
+ break;
2482
+ case "vector":
2483
+ engine.input.setActionVector(s.action, s.x, s.y);
2484
+ break;
2485
+ case "key":
2486
+ engine.input.handleKey(s.code, s.down);
2487
+ break;
2488
+ case "pointer":
2489
+ engine.input.handlePointerMove(s.dx, s.dy);
2490
+ break;
2491
+ case "step":
2492
+ await new Promise((r) => setTimeout(r, s.ms));
2493
+ break;
2494
+ }
2495
+ watching = false;
2496
+ return frames;
2497
+ };
2458
2498
  const hot = import.meta.hot;
2459
2499
  if (hot) {
2460
2500
  const onFrameRequest = (data) => {
2461
2501
  const request = data;
2462
2502
  const id = request?.id;
2463
2503
  hot.send("incanto:frame-ack", { id });
2464
- captureFramePixels().then((shot) => hot.send("incanto:frame-report", {
2504
+ const drive = request?.drive ? parseDrive(request.drive) : null;
2505
+ if (drive?.error) {
2506
+ hot.send("incanto:frame-report", {
2507
+ id,
2508
+ report: { error: drive.error }
2509
+ });
2510
+ return;
2511
+ }
2512
+ let droveFrames = null;
2513
+ (drive ? applyDrive(drive.steps) : Promise.resolve(null)).then((frames) => {
2514
+ droveFrames = frames;
2515
+ return captureFramePixels();
2516
+ }).then((shot) => hot.send("incanto:frame-report", {
2465
2517
  id,
2466
2518
  report: {
2467
2519
  ...frameStats(shot.pixels, shot.width, shot.height, { grid: request?.grid }),
2468
2520
  signature: frameSignature(shot.pixels, shot.width, shot.height),
2521
+ ...droveFrames === null ? {} : { droveFrames },
2469
2522
  ...request?.image ? { image: frameImage(shot.pixels, shot.width, shot.height, (w, h) => Object.assign(document.createElement("canvas"), {
2470
2523
  width: w,
2471
2524
  height: h
@@ -2478,6 +2531,30 @@ async function createGame3D(opts) {
2478
2531
  };
2479
2532
  hot.on("incanto:frame-request", onFrameRequest);
2480
2533
  cleanups.push(() => hot.off?.("incanto:frame-request", onFrameRequest));
2534
+ const onLogsRequest = (data) => {
2535
+ const id = data?.id;
2536
+ try {
2537
+ hot.send("incanto:logs-report", {
2538
+ id,
2539
+ report: logReport({
2540
+ entries: engine.log.entries(),
2541
+ stats: {
2542
+ ...engine.stats(),
2543
+ ...renderer.stats?.() ?? {}
2544
+ },
2545
+ assetErrors: renderer.assets?.errors() ?? [],
2546
+ hidden: typeof document !== "undefined" && document.hidden
2547
+ })
2548
+ });
2549
+ } catch (error) {
2550
+ hot.send("incanto:logs-report", {
2551
+ id,
2552
+ report: { error: error instanceof Error ? error.message : String(error) }
2553
+ });
2554
+ }
2555
+ };
2556
+ hot.on("incanto:logs-request", onLogsRequest);
2557
+ cleanups.push(() => hot.off?.("incanto:logs-request", onLogsRequest));
2481
2558
  }
2482
2559
  cleanups.push(pauseWhenHidden(engine));
2483
2560
  await report(1, "ready");
@@ -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-02Btmmjn.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();
@@ -1,7 +1,7 @@
1
- import { f as Node, h as effectiveOrder, n as loadScene, w as registerBehavior, y as Behavior } from "./loader-r49nDwB4.js";
1
+ import { f as Node, h as effectiveOrder, n as loadScene, w as registerBehavior, y as Behavior } from "./loader-DwazzlQb.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { t as jsonClone } from "./json-BLk7H2Qa.js";
4
- import { t as duplicateNode } from "./duplicate-CRtihGmC.js";
4
+ import { t as duplicateNode } from "./duplicate-CI9WF_bg.js";
5
5
  import { Color, CubeCamera, DepthTexture, DoubleSide, Euler, FloatType, Frustum, HalfFloatType, LinearMipmapLinearFilter, MathUtils, Matrix4, Mesh, MeshDepthMaterial, Object3D, PerspectiveCamera, Plane, PlaneGeometry, Quaternion, ShaderMaterial, Sphere, Vector2, Vector3, Vector4, WebGLCubeRenderTarget, WebGLRenderTarget } from "three";
6
6
  //#region src/3d/frustum.ts
7
7
  const scratchFrustum = new Frustum();
@@ -2553,7 +2553,7 @@ var CharacterBody3D = class extends PhysicsBody3D {
2553
2553
  0,
2554
2554
  0
2555
2555
  ] },
2556
- snapToGround: { default: true },
2556
+ stickToGround: { default: true },
2557
2557
  slopeLimitDeg: { default: 45 }
2558
2558
  };
2559
2559
  /** m/s, y-up (up = +y). */
@@ -2562,9 +2562,30 @@ var CharacterBody3D = class extends PhysicsBody3D {
2562
2562
  0,
2563
2563
  0
2564
2564
  ];
2565
- snapToGround = true;
2565
+ /**
2566
+ * Hold the capsule against the surface while it moves, so walking down a
2567
+ * slope does not become a series of small falls.
2568
+ *
2569
+ * Named apart from `Node3D.snapToGround` (which PLACES a node on the ground
2570
+ * at load) because sharing the name silently broke both: the boolean shadowed
2571
+ * the placement prop, its `true` default made the placement pass teleport
2572
+ * every character body in a terrain scene, and a lift value was rejected
2573
+ * outright — so the only escape from the teleport also turned this off.
2574
+ */
2575
+ stickToGround = true;
2566
2576
  /** Max climbable slope angle. */
2567
2577
  slopeLimitDeg = 45;
2578
+ /**
2579
+ * `snapToGround` on a character body is ambiguous, so it is refused.
2580
+ *
2581
+ * A boolean here used to mean the ground-stick, and as placement it means
2582
+ * "leave y alone" — which is already the default. Rather than pick one and
2583
+ * silently do the other thing to an existing scene, say which prop was meant.
2584
+ */
2585
+ onReady() {
2586
+ if (typeof this.snapToGround === "boolean") throw new IncantoError("PROP_TYPE_MISMATCH", `'${this.getPath()}' sets snapToGround to a boolean. On a character body that is ambiguous: 'stickToGround' holds the capsule against the surface while it moves (default true), while 'snapToGround' PLACES the node on the ground at load and takes a lift in metres (e.g. 0.8) — omit it to leave y exactly as authored.`, { prop: "snapToGround" });
2587
+ super.onReady();
2588
+ }
2568
2589
  /** @internal Updated by Physics3D.moveAndSlide. */
2569
2590
  _grounded = false;
2570
2591
  moveAndSlide() {
@@ -4553,7 +4574,8 @@ var Health = class extends Behavior {
4553
4574
  static signals = [
4554
4575
  "damaged",
4555
4576
  "healed",
4556
- "died"
4577
+ "died",
4578
+ "healthChanged"
4557
4579
  ];
4558
4580
  max = 100;
4559
4581
  regenPerSec = 0;
@@ -4602,13 +4624,29 @@ var Health = class extends Behavior {
4602
4624
  this.setCurrent(0);
4603
4625
  this.die();
4604
4626
  }
4627
+ /**
4628
+ * The one funnel every change goes through — and therefore the one place a
4629
+ * HUD can be told about.
4630
+ *
4631
+ * `damaged`/`healed` both lead with the DELTA, which is the number a bar must
4632
+ * not show; wire either to `UiBar.setValue` and the bar paints the damage as
4633
+ * the health and looks like it works. Regeneration is worse: it moves
4634
+ * `current` through here and emits NEITHER, so a regen bar sat still while
4635
+ * the character healed. `healthChanged(current, max)` carries the max too, so
4636
+ * a raised ceiling needs no second wire.
4637
+ *
4638
+ * Silent when nothing moved: a bar that repaints on nothing is noise.
4639
+ */
4605
4640
  setCurrent(value) {
4606
- this.current = Math.max(0, Math.min(this.max, value));
4641
+ const next = Math.max(0, Math.min(this.max, value));
4642
+ if (next === this.current) return;
4643
+ this.current = next;
4644
+ this.emit("healthChanged", this.current, this.max);
4607
4645
  }
4608
4646
  die() {
4609
4647
  if (this.dead) return;
4610
4648
  this.dead = true;
4611
- this.current = 0;
4649
+ this.setCurrent(0);
4612
4650
  this.emit("died");
4613
4651
  if (this.freeOnDeath) this.node.queueFree();
4614
4652
  }
@@ -5675,6 +5713,10 @@ var Patrol = class extends Behavior {
5675
5713
  * On overlap with a node in `collectorGroup`, emits `collected(value, other)`
5676
5714
  * then `queueFree()`s itself. Wire `collected → ScoreKeeper.addScore` (the
5677
5715
  * value is the first arg) or `collected → Collector.collect`.
5716
+ *
5717
+ * `other` is the ENTITY, not whichever of its parts touched — see
5718
+ * {@link findCollector}. A player is a body with children (a reach sensor, a
5719
+ * hitbox), and the part that enters first is rarely the part carrying the group.
5678
5720
  */
5679
5721
  var Pickup = class extends Behavior {
5680
5722
  static props = {
@@ -5695,12 +5737,41 @@ var Pickup = class extends Behavior {
5695
5737
  }
5696
5738
  tryCollect(other) {
5697
5739
  if (this.collectedAlready) return;
5698
- if (!other.isInGroup(this.collectorGroup)) return;
5740
+ const collector = findCollector(other, this.collectorGroup);
5741
+ if (!collector) return;
5699
5742
  this.collectedAlready = true;
5700
- this.emit("collected", this.value, other);
5743
+ this.emit("collected", this.value, collector);
5701
5744
  this.node.queueFree();
5702
5745
  }
5703
5746
  };
5747
+ /**
5748
+ * The entity in `group` that this contact belongs to, or null.
5749
+ *
5750
+ * The same search `DamageOnContact` has always used for `Health` — the
5751
+ * contacted node, its descendants, then the ancestor CHAIN — and for the same
5752
+ * reason. An entity is a body with children (a reach sensor, a hitbox, a skin),
5753
+ * the group sits on one node of it, and whichever part happens to touch first
5754
+ * is rarely that node.
5755
+ *
5756
+ * Matching the literal entering node was a silent failure in exactly the shape
5757
+ * the gameplay skill recommends: a player with a `Grab` area collected almost
5758
+ * nothing, because the sensor is wider than the body and entered first. Five
5759
+ * pickups fired `triggerEnter`; one fired `collected`.
5760
+ *
5761
+ * The ancestor climb deliberately does NOT descend into siblings: past a shared
5762
+ * parent every other entity hangs off it too, and a coin would be collected by
5763
+ * a crate standing next to the player.
5764
+ */
5765
+ function findCollector(node, group) {
5766
+ const queue = [node];
5767
+ while (queue.length > 0) {
5768
+ const n = queue.shift();
5769
+ if (n.isInGroup(group)) return n;
5770
+ queue.push(...n.children);
5771
+ }
5772
+ for (let n = node.parent; n; n = n.parent) if (n.isInGroup(group)) return n;
5773
+ return null;
5774
+ }
5704
5775
  //#endregion
5705
5776
  //#region src/gameplay/projectile.ts
5706
5777
  /**
@@ -97,6 +97,19 @@ declare class Health extends Behavior {
97
97
  heal(n: number): void;
98
98
  /** Drop to 0 and die immediately (ignores i-frames). */
99
99
  kill(): void;
100
+ /**
101
+ * The one funnel every change goes through — and therefore the one place a
102
+ * HUD can be told about.
103
+ *
104
+ * `damaged`/`healed` both lead with the DELTA, which is the number a bar must
105
+ * not show; wire either to `UiBar.setValue` and the bar paints the damage as
106
+ * the health and looks like it works. Regeneration is worse: it moves
107
+ * `current` through here and emits NEITHER, so a regen bar sat still while
108
+ * the character healed. `healthChanged(current, max)` carries the max too, so
109
+ * a raised ceiling needs no second wire.
110
+ *
111
+ * Silent when nothing moved: a bar that repaints on nothing is noise.
112
+ */
100
113
  private setCurrent;
101
114
  private die;
102
115
  /** Current HP and whether death already fired — `max` comes back from JSON. */
@@ -318,6 +331,10 @@ declare class Patrol extends Behavior {
318
331
  * On overlap with a node in `collectorGroup`, emits `collected(value, other)`
319
332
  * then `queueFree()`s itself. Wire `collected → ScoreKeeper.addScore` (the
320
333
  * value is the first arg) or `collected → Collector.collect`.
334
+ *
335
+ * `other` is the ENTITY, not whichever of its parts touched — see
336
+ * {@link findCollector}. A player is a body with children (a reach sensor, a
337
+ * hitbox), and the part that enters first is rarely the part carrying the group.
321
338
  */
322
339
  declare class Pickup extends Behavior {
323
340
  static readonly props: PropSchema;
package/dist/gameplay.js CHANGED
@@ -1,2 +1,2 @@
1
- import { C as FollowCamera, D as Health, E as DamageOnContact, O as Collector, S as restartScene, T as phaseOf, _ as hitStop, a as Wander, b as GameFlow, c as Projectile, d as PathFollow, f as Oscillate, g as Cooldown, h as CameraShake, i as WaveSpawner, k as Chase, l as Pickup, m as Lifetime, n as registerGameplayBehaviors, o as Spawner, p as MoveTo, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Patrol, v as screenFlash, w as DayNight, x as goToScene, y as Interactable } from "./gameplay-BQOeAid6.js";
1
+ import { C as FollowCamera, D as Health, E as DamageOnContact, O as Collector, S as restartScene, T as phaseOf, _ as hitStop, a as Wander, b as GameFlow, c as Projectile, d as PathFollow, f as Oscillate, g as Cooldown, h as CameraShake, i as WaveSpawner, k as Chase, l as Pickup, m as Lifetime, n as registerGameplayBehaviors, o as Spawner, p as MoveTo, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Patrol, v as screenFlash, w as DayNight, x as goToScene, y as Interactable } from "./gameplay-02Btmmjn.js";
2
2
  export { CameraShake, Chase, Collector, Cooldown, DamageOnContact, DayNight, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, Projectile, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, phaseOf, registerGameplayBehaviors, restartScene, screenFlash };