incanto 0.36.2 → 0.38.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 (57) hide show
  1. package/bin/incanto-feel.mjs +93 -0
  2. package/bin/incanto-playtest.mjs +136 -0
  3. package/dist/2d.d.ts +2 -2
  4. package/dist/2d.js +3 -3
  5. package/dist/3d.d.ts +44 -3
  6. package/dist/3d.js +4 -32
  7. package/dist/{behavior-DibCwrW7.d.ts → behavior-B5PJ32eu.d.ts} +106 -6
  8. package/dist/{create-game-D-StzrTj.js → create-game-C4t5gVhc.js} +5 -5
  9. package/dist/{create-game-DnOu2aAa.js → create-game-DqY2lMBc.js} +783 -689
  10. package/dist/debug.d.ts +1 -1
  11. package/dist/{duplicate-Cvb1BSca.js → duplicate-CRtihGmC.js} +1 -1
  12. package/dist/editor.js +21 -0
  13. package/dist/{environment-presets-fK9oyrrR.js → environment-presets-C0Axpfth.js} +75 -9
  14. package/dist/{gameplay-C0MUMSak.js → gameplay-CsJDUQh_.js} +2 -2
  15. package/dist/gameplay.d.ts +1 -1
  16. package/dist/gameplay.js +1 -1
  17. package/dist/index.d.ts +4 -50
  18. package/dist/index.js +5 -75
  19. package/dist/{loader-BqigZsfF.d.ts → loader-DSMdqasu.d.ts} +1 -1
  20. package/dist/{loader-DAZlgqeC.js → loader-r49nDwB4.js} +15 -0
  21. package/dist/net.d.ts +1 -1
  22. package/dist/net.js +3 -3
  23. package/dist/{pathfinding-B3QtdhvZ.d.ts → pathfinding-BrE5Uec6.d.ts} +1 -1
  24. package/dist/{physics-2d-B4v39hca.js → physics-2d-OfKb0PV_.js} +2 -2
  25. package/dist/{physics-3d-DFJVMYET.js → physics-3d-C8242nXr.js} +3 -3
  26. package/dist/react.d.ts +1 -1
  27. package/dist/react.js +1 -1
  28. package/dist/{register-DXwtIVqP.js → register-B4RRQbyq.js} +2 -2
  29. package/dist/{register-BHAwM4bK.js → register-Dm7nrd-E.js} +322 -34
  30. package/dist/{register-COGCNitr.js → register-DnB9m7Rl.js} +2 -2
  31. package/dist/replay-Cxrr-ApH.d.ts +86 -0
  32. package/dist/{audit-D7F3n3Nt.js → replay-DH0UOuZ-.js} +72 -2
  33. package/dist/test-CoDPWx-m.js +1404 -0
  34. package/dist/test.d.ts +113 -4
  35. package/dist/test.js +3 -3
  36. package/dist/vite.js +1 -1
  37. package/editor/assets/{agent8-DMvBVhe4.js → agent8-BtIlblPm.js} +1 -1
  38. package/editor/assets/{debug-BgAq55oC.js → debug-BvTmHXI7.js} +1 -1
  39. package/editor/assets/{index-Cy_QsKbh.js → index-BS01WWgM.js} +91 -91
  40. package/editor/index.html +1 -1
  41. package/package.json +3 -1
  42. package/schemas/scene.schema.json +318 -0
  43. package/skills/incanto-3d-character.md +35 -0
  44. package/skills/incanto-building-2d-games.md +6 -0
  45. package/skills/incanto-building-3d-games.md +6 -0
  46. package/skills/incanto-game-feel.md +76 -0
  47. package/skills/incanto-node-reference.md +51 -0
  48. package/skills/incanto-performance.md +212 -0
  49. package/skills/incanto-playtesting.md +143 -0
  50. package/skills/incanto-verifying-your-game.md +41 -0
  51. package/templates-app/beacon-isle-3d/package.json +1 -1
  52. package/templates-app/tps-3d/package.json +1 -1
  53. package/templates-app/village-quest-3d/package.json +1 -1
  54. package/templates-app/village-quest-3d/src/village.scene.json +3 -1
  55. package/templates-app/village-quest-3d/verify.ts +55 -5
  56. package/dist/errors-DGRtWlSx.d.ts +0 -39
  57. package/dist/test-X_xE6Yay.js +0 -642
package/dist/debug.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { T as RendererStats, b as Engine, kt as LogLevel } from "./behavior-DibCwrW7.js";
1
+ import { T as RendererStats, b as Engine, kt as LogLevel } from "./behavior-B5PJ32eu.js";
2
2
 
3
3
  //#region src/debug/panel.d.ts
4
4
  /** Minimal document surface the overlay needs (injectable for tests). */
@@ -1,4 +1,4 @@
1
- import { i as serializeNode, t as buildNodeJson } from "./loader-DAZlgqeC.js";
1
+ import { i as serializeNode, t as buildNodeJson } from "./loader-r49nDwB4.js";
2
2
  //#region src/core/scene/duplicate.ts
3
3
  function stripUids(json) {
4
4
  const { uid: _uid, ...rest } = json;
package/dist/editor.js CHANGED
@@ -1121,6 +1121,27 @@ var ht = {
1121
1121
  en: "Options are the locales the scene declares in \"strings\", each labeled with its own endonym. Picking one switches the game live and persists it. English is the base: any key a locale omits falls back to it, on purpose.",
1122
1122
  ko: "옵션은 씬의 \"strings\"가 선언한 로케일이며, 각 언어의 자기 이름으로 표시됩니다. 선택하면 즉시 바뀌고 저장됩니다. 영어가 기준이라 로케일이 빠뜨린 키는 의도적으로 영어로 표시됩니다."
1123
1123
  }),
1124
+ R("UiQualitySelect", {
1125
+ en: "Graphics quality — Low, Medium, High. Already wired.",
1126
+ ko: "그래픽 품질 — Low, Medium, High. 이미 연결되어 있습니다."
1127
+ }, {
1128
+ en: "Low turns off shadows, bloom, post and clouds; Medium keeps shadows but makes them static and drops post; High is everything. Applies live and persists. Antialiasing follows the tier on the NEXT launch — it is a WebGL context attribute and cannot change while a context exists. Picking records a human choice, so device detection never overrides it later.",
1129
+ ko: "Low는 그림자·블룸·포스트·구름을 끄고, Medium은 그림자를 정적으로 유지하며 포스트를 끄고, High는 전부 켭니다. 즉시 적용되고 저장됩니다. 안티에일리어싱만 다음 실행부터 반영됩니다(WebGL 컨텍스트 속성이라 생성 후에는 못 바꿉니다). 사람이 고른 값으로 기록되어 이후 자동 감지가 덮어쓰지 않습니다."
1130
+ }),
1131
+ R("UiFrameCapSelect", {
1132
+ en: "Frame rate cap — 30, 60, 120 or Unlimited.",
1133
+ ko: "프레임 제한 — 30, 60, 120 또는 무제한."
1134
+ }, {
1135
+ en: "Gates the whole frame, not just the render, so it saves CPU as well as GPU — a cooler phone and a longer battery. Unlimited (0) is the default. Restrict the list with options, e.g. \"60,120,0\".",
1136
+ ko: "렌더뿐 아니라 프레임 전체를 제한하므로 GPU와 CPU를 함께 아낍니다 — 발열과 배터리에 유리합니다. 기본값은 무제한(0). options로 목록을 줄일 수 있습니다(예: \"60,120,0\")."
1137
+ }),
1138
+ R("UiRenderScaleSelect", {
1139
+ en: "Resolution scale — 50%, 75%, 100%.",
1140
+ ko: "해상도 배율 — 50%, 75%, 100%."
1141
+ }, {
1142
+ en: "The cheapest frames on a weak GPU: at 75% the fragment shaders run over 56% of the pixels for a slightly softer image and nothing else, where dropping a quality tier costs the shadows outright. Multiplies with the engine's automatic resolution governor rather than fighting it.",
1143
+ ko: "저사양 GPU에서 가장 값싼 프레임: 75%면 픽셀의 약 56%만 셰이딩하므로 이미지가 약간 부드러워질 뿐이지만, 품질 단계를 낮추면 그림자가 통째로 사라집니다. 엔진의 자동 해상도 조절과 충돌하지 않고 곱해집니다."
1144
+ }),
1124
1145
  R("UiSelect", {
1125
1146
  en: "One of several — quality, resolution, language.",
1126
1147
  ko: "여러 개 중 하나 — 품질, 해상도, 언어."
@@ -1,10 +1,10 @@
1
- import { t as registerCoreNodes } from "./register-BHAwM4bK.js";
1
+ import { t as registerCoreNodes } from "./register-Dm7nrd-E.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
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-C0MUMSak.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-CsJDUQh_.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";
@@ -2488,11 +2488,9 @@ var Camera3D = class extends Node3D {
2488
2488
  //#endregion
2489
2489
  //#region src/3d/nodes/character-controller-3d.ts
2490
2490
  const ACC_DELTA_TIME = 8;
2491
- const AIR_DRAG = .2;
2492
2491
  const DRAG_DAMPING = .15;
2493
2492
  const SPRING_K = 1.2;
2494
2493
  const SPRING_DAMPING = .08;
2495
- const FALLING_GRAVITY_SCALE = 2.5;
2496
2494
  const FALLING_MAX_VEL = -20;
2497
2495
  const LOOK_SPEED = .002;
2498
2496
  const RAD2DEG = 180 / Math.PI;
@@ -2539,6 +2537,18 @@ var CharacterController3D = class extends Node3D {
2539
2537
  sprintMultiplier: { default: 2 },
2540
2538
  jumpVelocity: { default: 4 },
2541
2539
  sprintJumpMultiplier: { default: 1.2 },
2540
+ /** Jump this long AFTER walking off a ledge. The single biggest one. */
2541
+ coyoteSeconds: { default: 0 },
2542
+ /** Press jump this long BEFORE landing and still get it. */
2543
+ jumpBufferSeconds: { default: 0 },
2544
+ /** Releasing jump early cuts the rise to this fraction (1 = fixed height). */
2545
+ jumpCutMultiplier: { default: 1 },
2546
+ /** 2 = double jump. Extra jumps work in mid-air. */
2547
+ maxJumps: { default: 1 },
2548
+ /** How much of your ground control you keep in the air (0 = none, 1 = full). */
2549
+ airControl: { default: .2 },
2550
+ /** Gravity multiplier while falling — >1 is the "snappy" arc. */
2551
+ fallGravity: { default: 2.5 },
2542
2552
  floatHeight: { default: .01 },
2543
2553
  mouseLook: { default: true },
2544
2554
  zoomMin: { default: 0 },
@@ -2565,6 +2575,12 @@ var CharacterController3D = class extends Node3D {
2565
2575
  sprintMultiplier = 2;
2566
2576
  jumpVelocity = 4;
2567
2577
  sprintJumpMultiplier = 1.2;
2578
+ coyoteSeconds = 0;
2579
+ jumpBufferSeconds = 0;
2580
+ jumpCutMultiplier = 1;
2581
+ maxJumps = 1;
2582
+ airControl = .2;
2583
+ fallGravity = 2.5;
2568
2584
  floatHeight = .01;
2569
2585
  mouseLook = true;
2570
2586
  zoomMin = 0;
@@ -2600,6 +2616,11 @@ var CharacterController3D = class extends Node3D {
2600
2616
  /** idle | walk | run | fastRun | airborne — drive animations from this. */
2601
2617
  state = "idle";
2602
2618
  grounded = false;
2619
+ /** @internal Feel timers — see the jump block for what each one buys. */
2620
+ coyoteLeft = 0;
2621
+ bufferLeft = 0;
2622
+ jumpsUsed = 0;
2623
+ rising = false;
2603
2624
  get body() {
2604
2625
  const parent = this.parent;
2605
2626
  if (!(parent instanceof RigidBody3D)) throw new IncantoError("TREE_VIOLATION", `CharacterController3D '${this.name}' must be a child of a RigidBody3D (parent is '${parent?.name ?? "none"}').`);
@@ -2653,6 +2674,7 @@ var CharacterController3D = class extends Node3D {
2653
2674
  }
2654
2675
  }
2655
2676
  this.grounded = toi !== null && toi < floatingDis * 2;
2677
+ const footed = toi !== null && toi < floatingDis * 1.3 && (vel[1] ?? 0) <= .5;
2656
2678
  let mx = 0;
2657
2679
  let mz = 0;
2658
2680
  let moving = false;
@@ -2673,7 +2695,7 @@ var CharacterController3D = class extends Node3D {
2673
2695
  const intensity = keyboardIntensity(moving, sprinting);
2674
2696
  const speedMultiplier = 1 + (this.sprintMultiplier - 1) * intensity;
2675
2697
  const targetSpeed = moving ? this.maxSpeed * speedMultiplier : 0;
2676
- const control = this.grounded ? 1 : AIR_DRAG;
2698
+ const control = this.grounded ? 1 : this.airControl;
2677
2699
  const ax = (mx * targetSpeed - vel[0]) / ACC_DELTA_TIME * control;
2678
2700
  const az = (mz * targetSpeed - vel[2]) / ACC_DELTA_TIME * control;
2679
2701
  body.applyImpulse([
@@ -2694,7 +2716,18 @@ var CharacterController3D = class extends Node3D {
2694
2716
  0
2695
2717
  ]);
2696
2718
  }
2697
- if (this.grounded && safePressed(input, this.jumpAction)) {
2719
+ if (footed) {
2720
+ this.coyoteLeft = this.coyoteSeconds;
2721
+ this.jumpsUsed = 0;
2722
+ this.rising = false;
2723
+ } else {
2724
+ this.coyoteLeft = Math.max(0, this.coyoteLeft - dt);
2725
+ if (this.coyoteLeft <= 0 && this.jumpsUsed === 0) this.jumpsUsed = 1;
2726
+ }
2727
+ const pressedJump = safeJustPressed(input, this.jumpAction);
2728
+ if (pressedJump) this.bufferLeft = Math.max(this.jumpBufferSeconds, 0);
2729
+ else this.bufferLeft = Math.max(0, this.bufferLeft - dt);
2730
+ if ((this.bufferLeft > 0 || pressedJump) && (footed || this.coyoteLeft > 0 || this.jumpsUsed < this.maxJumps)) {
2698
2731
  const jumpVel = this.jumpVelocity * (1 + (this.sprintJumpMultiplier - 1) * intensity);
2699
2732
  body.linearVelocity = [
2700
2733
  vel[0],
@@ -2702,16 +2735,34 @@ var CharacterController3D = class extends Node3D {
2702
2735
  vel[2]
2703
2736
  ];
2704
2737
  this.grounded = false;
2738
+ this.jumpsUsed += 1;
2739
+ this.bufferLeft = 0;
2740
+ this.coyoteLeft = 0;
2741
+ this.rising = true;
2742
+ vel[1] = jumpVel;
2743
+ }
2744
+ if (this.rising) {
2745
+ if ((vel[1] ?? 0) <= 0) this.rising = false;
2746
+ else if (this.jumpCutMultiplier < 1 && !safePressed(input, this.jumpAction)) {
2747
+ const cut = (vel[1] ?? 0) * this.jumpCutMultiplier;
2748
+ body.linearVelocity = [
2749
+ vel[0],
2750
+ cut,
2751
+ vel[2]
2752
+ ];
2753
+ vel[1] = cut;
2754
+ this.rising = false;
2755
+ }
2705
2756
  }
2706
- if (vel[1] < FALLING_MAX_VEL) body.gravityScale = 0;
2707
- else if (vel[1] < 0 && !this.grounded) body.gravityScale = FALLING_GRAVITY_SCALE;
2757
+ if ((vel[1] ?? 0) < FALLING_MAX_VEL) body.gravityScale = 0;
2758
+ else if ((vel[1] ?? 0) < 0 && !footed) body.gravityScale = this.fallGravity;
2708
2759
  else body.gravityScale = 1;
2709
2760
  if (this.view === "side" && Math.abs(pos[2]) > .01) body.position = [
2710
2761
  body.position[0] ?? 0,
2711
2762
  body.position[1] ?? 0,
2712
2763
  0
2713
2764
  ];
2714
- const next = movementState(this.grounded, moving ? intensity : 0);
2765
+ const next = movementState(footed, moving ? intensity : 0);
2715
2766
  if (next !== this.state) {
2716
2767
  this.state = next;
2717
2768
  const clip = this.animations[next];
@@ -2855,6 +2906,21 @@ function safePressed(input, action) {
2855
2906
  return false;
2856
2907
  }
2857
2908
  }
2909
+ /**
2910
+ * The EDGE, not the level.
2911
+ *
2912
+ * Jump used to read `isPressed`, so holding the button re-jumped on every frame
2913
+ * the raycast found ground — an auto-hop nobody asked for, and one that makes
2914
+ * coyote time incoherent (a held button would re-fire through the whole
2915
+ * window). Same undeclared-action tolerance as `safePressed`.
2916
+ */
2917
+ function safeJustPressed(input, action) {
2918
+ try {
2919
+ return input.justPressed(action);
2920
+ } catch {
2921
+ return false;
2922
+ }
2923
+ }
2858
2924
  //#endregion
2859
2925
  //#region src/3d/vegetation/grass-placement.ts
2860
2926
  const NOISE_SCALE = .15;
@@ -1,7 +1,7 @@
1
- import { f as Node, h as effectiveOrder, n as loadScene, w as registerBehavior, y as Behavior } from "./loader-DAZlgqeC.js";
1
+ import { f as Node, h as effectiveOrder, n as loadScene, w as registerBehavior, y as Behavior } from "./loader-r49nDwB4.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-Cvb1BSca.js";
4
+ import { t as duplicateNode } from "./duplicate-CRtihGmC.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();
@@ -1,4 +1,4 @@
1
- import { Tt as Node, b as Engine, d as PropSchema, n as BehaviorCtor, t as Behavior } from "./behavior-DibCwrW7.js";
1
+ import { Tt as Node, b as Engine, d as PropSchema, n as BehaviorCtor, t as Behavior } from "./behavior-B5PJ32eu.js";
2
2
  import { c as JsonValue, s as JsonObject } from "./schema-CFeioQRE.js";
3
3
 
4
4
  //#region src/gameplay/chase.d.ts
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-C0MUMSak.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-CsJDUQh_.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 };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { $ as translationKey, A as qualityEnvironment, At as LogManager, B as restoreBehaviors, C as EngineStats, Ct as AudioBuses, D as QualityTier, Dt as SceneTree, E as DeviceHints, Et as NodeLifecycle, F as RestoreReport, G as OrderGroupTable, H as createSaveStore, I as SaveSlot, J as BASE_LOCALE, K as effectiveOrder, L as SaveSlots, M as suggestQuality, Mt as SignalListener, N as Scene, O as Settings, Ot as LogEntry, P as BehaviorState, Q as suggestLocale, R as behaviorsWithoutSave, S as Scheduler, St as PlayMusicOptions, T as RendererStats, Tt as Node, U as ORDER_GROUP_BASE, V as SaveStore, W as OrderGroup, X as Localization, Y as LocaleTables, Z as T_PREFIX, _ as mergeStaticSignals, _t as SynthOptions, a as clearBehaviors, at as VoicePreset, b as Engine, bt as MusicManager, c as registeredBehaviors, ct as RolloffModel, d as PropSchema, dt as spatialGain, et as InputMap, f as clearRegistry, ft as spatialPan, g as getNodeType, gt as SfxWave, h as getNodeSignals, ht as SfxParams, i as behaviorSignals, it as Voice, j as readDeviceHints, jt as Signal, k as SettingsValues, kt as LogLevel, l as NodeCtor, lt as SpatialParams, m as getNodeSchema, mt as SFX_PRESET_NAMES, n as BehaviorCtor, nt as SfxPlayOptions, o as getBehavior, ot as Listener, p as createNode, pt as SFX_PRESETS, q as resolveOrderGroups, r as behaviorSchema, rt as isAudioContextAvailable, s as registerBehavior, st as ROLLOFF_MODELS, t as Behavior, tt as SfxEngine, u as PropDef, ut as Vec3, v as registerNode, vt as synthSfx, w as GameStats, wt as BusName, x as EngineOptions, xt as MusicTrack, y as registeredTypes, yt as MusicBackend, z as captureBehaviors } from "./behavior-DibCwrW7.js";
1
+ import { $ as translationKey, A as qualityEnvironment, At as LogManager, B as restoreBehaviors, C as EngineStats, Ct as AudioBuses, D as QualityTier, Dt as SceneTree, E as DeviceHints, Et as NodeLifecycle, F as RestoreReport, G as OrderGroupTable, H as createSaveStore, I as SaveSlot, J as BASE_LOCALE, K as effectiveOrder, L as SaveSlots, M as suggestQuality, Mt as SignalListener, N as Scene, O as Settings, Ot as LogEntry, P as BehaviorState, Q as suggestLocale, R as behaviorsWithoutSave, S as Scheduler, St as PlayMusicOptions, T as RendererStats, Tt as Node, U as ORDER_GROUP_BASE, V as SaveStore, W as OrderGroup, X as Localization, Y as LocaleTables, Z as T_PREFIX, _ as mergeStaticSignals, _t as SynthOptions, a as clearBehaviors, at as VoicePreset, b as Engine, bt as MusicManager, c as registeredBehaviors, ct as RolloffModel, d as PropSchema, dt as spatialGain, et as InputMap, f as clearRegistry, ft as spatialPan, g as getNodeType, gt as SfxWave, h as getNodeSignals, ht as SfxParams, i as behaviorSignals, it as Voice, j as readDeviceHints, jt as Signal, k as SettingsValues, kt as LogLevel, l as NodeCtor, lt as SpatialParams, m as getNodeSchema, mt as SFX_PRESET_NAMES, n as BehaviorCtor, nt as SfxPlayOptions, o as getBehavior, ot as Listener, p as createNode, pt as SFX_PRESETS, q as resolveOrderGroups, r as behaviorSchema, rt as isAudioContextAvailable, s as registerBehavior, st as ROLLOFF_MODELS, t as Behavior, tt as SfxEngine, u as PropDef, ut as Vec3, v as registerNode, vt as synthSfx, w as GameStats, wt as BusName, x as EngineOptions, xt as MusicTrack, y as registeredTypes, yt as MusicBackend, z as captureBehaviors } from "./behavior-B5PJ32eu.js";
2
2
  import { a as Rng, c as JsonValue, d as jsonKind, i as SceneJson, l as jsonClone, n as NodeJson, o as JsonKind, r as SCENE_FORMAT, s as JsonObject, t as ConnectionJson, u as jsonEquals } from "./schema-CFeioQRE.js";
3
- import { n as loadScene, t as LoadSceneOptions } from "./loader-BqigZsfF.js";
3
+ import { n as loadScene, t as LoadSceneOptions } from "./loader-DSMdqasu.js";
4
4
  import { n as ParticleSimConfig, r as ParticleView, t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
5
- import { a as AudioElementLike, i as gridFromRows, n as PathGrid, o as AudioPlayer, r as findPath, t as FindPathOptions } from "./pathfinding-B3QtdhvZ.js";
6
- import { i as auditScene, n as IncantoErrorCode, r as IncantoErrorDetails, t as IncantoError } from "./errors-DGRtWlSx.js";
5
+ import { a as AudioElementLike, i as gridFromRows, n as PathGrid, o as AudioPlayer, r as findPath, t as FindPathOptions } from "./pathfinding-BrE5Uec6.js";
6
+ import { a as startRecording, c as IncantoErrorDetails, i as replay, l as auditScene, n as ReplayEvent, o as IncantoError, r as ReplayJson, s as IncantoErrorCode, t as Recorder } from "./replay-Cxrr-ApH.js";
7
7
 
8
8
  //#region src/core/audio/crossfade.d.ts
9
9
  /**
@@ -586,52 +586,6 @@ declare function resolveRendering(environment: JsonObject | undefined, fallback:
586
586
  preserveDrawingBuffer?: boolean;
587
587
  }): ResolvedRendering;
588
588
  //#endregion
589
- //#region src/core/replay.d.ts
590
- /**
591
- * Deterministic input replay — the engine already runs on a seeded RNG and
592
- * an injectable clock, so a recording of (tick timestamps + input events)
593
- * replays BIT-IDENTICALLY into a fresh engine with the same seed and scene.
594
- * That turns "does my game still work?" into an assertion an agent can run
595
- * headlessly: record once while playing, replay forever as a regression.
596
- *
597
- * const rec = startRecording(engine); // play the game...
598
- * const replayJson = rec.stop(); // ...save this JSON
599
- *
600
- * const fresh = new Engine({ seed: SAME_SEED }); // same seed + scene JSON
601
- * fresh.setScene(loadScene(sceneJson));
602
- * replay(fresh, replayJson); // now assert game state
603
- *
604
- * Caveats (documented, by design): game code must use `engine.rng` (not
605
- * Math.random) and dt/engine.time (not Date.now) — the engine's own rules.
606
- * Gamepad polling is not recorded (bind pads to actions; actions replay).
607
- */
608
- /** [tickIndex, methodName, ...primitive args] */
609
- type ReplayEvent = [number, string, ...(string | number | boolean)[]];
610
- interface ReplayJson {
611
- /** File-format marker for forward compatibility. */
612
- replay: 1;
613
- /** Tick timestamps (ms) exactly as the loop delivered them. */
614
- ticks: number[];
615
- events: ReplayEvent[];
616
- }
617
- interface Recorder {
618
- /** Detach the taps and return the recording. */
619
- stop(): ReplayJson;
620
- }
621
- /**
622
- * Tap the engine's input entry points and tick clock. Recording starts at
623
- * the CURRENT moment — start it before the first tick for full-run replays.
624
- */
625
- declare function startRecording(engine: Engine): Recorder;
626
- /**
627
- * Drive a fresh engine through a recording: for each recorded tick, feed
628
- * that tick's input events first, then tick with the recorded timestamp.
629
- * `onTick(index)` lets tests assert mid-run state.
630
- */
631
- declare function replay(engine: Engine, recording: ReplayJson, opts?: {
632
- onTick?: (index: number) => void;
633
- }): void;
634
- //#endregion
635
589
  //#region src/core/scene/constants.d.ts
636
590
  /** The single reserved key marking a prop value as a constant reference. */
637
591
  declare const CONST_REF_KEY = "@const";
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
- import { C as getBehavior, E as Signal, S as clearBehaviors, T as registeredBehaviors, _ as InputMap, a as SCENE_FORMAT, b as behaviorSchema, c as SceneTree, d as resolveConstants, f as Node, g as resolveOrderGroups, h as effectiveOrder, i as serializeNode, l as CONST_REF_KEY, m as ORDER_GROUP_BASE, n as loadScene, o as computeViewport, p as parseNodePath, r as Scene, s as resolveViewport, u as isConstRef, w as registerBehavior, x as behaviorSignals, y as Behavior } from "./loader-DAZlgqeC.js";
2
- import { A as translationKey, B as WebAudioMusicBackend, C as restoreBehaviors, D as Localization, E as BASE_LOCALE, F as spatialPan, H as fadeGain, I as SFX_PRESETS, L as SFX_PRESET_NAMES, M as isAudioContextAvailable, N as ROLLOFF_MODELS, O as T_PREFIX, P as spatialGain, R as synthSfx, S as captureBehaviors, T as LogManager, U as AudioBuses, V as crossfadeGains, _ as qualityEnvironment, a as UiSlider, b as SaveSlots, c as UiButton, d as UiBanner, f as UiBar, g as Settings, h as Engine, i as UiSelect, j as SfxEngine, k as suggestLocale, l as UiDialogue, m as AudioPlayer, n as UiImage, o as UiToggle, p as UiText, r as UiPanel, s as Timer, t as registerCoreNodes, u as HudLayer, v as readDeviceHints, w as createSaveStore, x as behaviorsWithoutSave, y as suggestQuality, z as MusicManager } from "./register-BHAwM4bK.js";
1
+ import { C as getBehavior, E as Signal, S as clearBehaviors, T as registeredBehaviors, _ as InputMap, a as SCENE_FORMAT, b as behaviorSchema, c as SceneTree, d as resolveConstants, f as Node, g as resolveOrderGroups, h as effectiveOrder, i as serializeNode, l as CONST_REF_KEY, m as ORDER_GROUP_BASE, n as loadScene, o as computeViewport, p as parseNodePath, r as Scene, s as resolveViewport, u as isConstRef, w as registerBehavior, x as behaviorSignals, y as Behavior } from "./loader-r49nDwB4.js";
2
+ import { A as suggestLocale, B as MusicManager, C as captureBehaviors, D as BASE_LOCALE, E as LogManager, F as spatialGain, H as crossfadeGains, I as spatialPan, L as SFX_PRESETS, M as SfxEngine, N as isAudioContextAvailable, O as Localization, P as ROLLOFF_MODELS, R as SFX_PRESET_NAMES, S as behaviorsWithoutSave, T as createSaveStore, U as fadeGain, V as WebAudioMusicBackend, W as AudioBuses, _ as qualityEnvironment, a as UiSlider, b as suggestQuality, c as UiButton, d as UiBanner, f as UiBar, g as Settings, h as Engine, i as UiSelect, j as translationKey, k as T_PREFIX, l as UiDialogue, m as AudioPlayer, n as UiImage, o as UiToggle, p as UiText, r as UiPanel, s as Timer, t as registerCoreNodes, u as HudLayer, w as restoreBehaviors, x as SaveSlots, y as readDeviceHints, z as synthSfx } from "./register-Dm7nrd-E.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
- import { t as auditScene } from "./audit-D7F3n3Nt.js";
5
+ import { n as startRecording, r as auditScene, t as replay } from "./replay-DH0UOuZ-.js";
6
6
  import { n as jsonEquals, r as jsonKind, t as jsonClone } from "./json-BLk7H2Qa.js";
7
7
  import { a as getNodeSignals, c as mergeStaticSignals, i as getNodeSchema, l as registerNode, n as clearRegistry, o as getNodeType, r as createNode, u as registeredTypes } from "./registry-IyWCGe4q.js";
8
8
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
9
9
  import { i as applyParticlePreset, n as PARTICLE_PRESETS, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-Bw7hB93B.js";
10
10
  import { i as resolveRendering, n as attachTouchControls, r as joystickVector, t as TouchControls } from "./touch-BoNg_MnF.js";
11
- import { t as duplicateNode } from "./duplicate-Cvb1BSca.js";
11
+ import { t as duplicateNode } from "./duplicate-CRtihGmC.js";
12
12
  //#region src/core/pathfinding.ts
13
13
  /**
14
14
  * A* from `from` to `to` (inclusive cell coords). Returns the cell path
@@ -155,76 +155,6 @@ function assetUrls(assets) {
155
155
  return Object.values(assets ?? {}).map((a) => a.url).filter((u) => typeof u === "string" && !u.startsWith("data:"));
156
156
  }
157
157
  //#endregion
158
- //#region src/core/replay.ts
159
- const RECORDED_METHODS = [
160
- "handleKey",
161
- "handleMouseButton",
162
- "handlePointerMove",
163
- "handleWheel",
164
- "pressAction",
165
- "releaseAction"
166
- ];
167
- /**
168
- * Tap the engine's input entry points and tick clock. Recording starts at
169
- * the CURRENT moment — start it before the first tick for full-run replays.
170
- */
171
- function startRecording(engine) {
172
- const ticks = [];
173
- const events = [];
174
- const input = engine.input;
175
- const originals = /* @__PURE__ */ new Map();
176
- for (const name of RECORDED_METHODS) {
177
- const original = input[name];
178
- originals.set(name, original);
179
- input[name] = (...args) => {
180
- events.push([
181
- ticks.length,
182
- name,
183
- ...args
184
- ]);
185
- return original.apply(engine.input, args);
186
- };
187
- }
188
- const originalTick = engine.tick.bind(engine);
189
- const engineAny = engine;
190
- engineAny.tick = (nowMs) => {
191
- ticks.push(nowMs);
192
- originalTick(nowMs);
193
- };
194
- let stopped = false;
195
- return { stop() {
196
- if (!stopped) {
197
- stopped = true;
198
- for (const [name, fn] of originals) input[name] = fn;
199
- engineAny.tick = originalTick;
200
- }
201
- return {
202
- replay: 1,
203
- ticks,
204
- events
205
- };
206
- } };
207
- }
208
- /**
209
- * Drive a fresh engine through a recording: for each recorded tick, feed
210
- * that tick's input events first, then tick with the recorded timestamp.
211
- * `onTick(index)` lets tests assert mid-run state.
212
- */
213
- function replay(engine, recording, opts) {
214
- const input = engine.input;
215
- let cursor = 0;
216
- for (let i = 0; i < recording.ticks.length; i++) {
217
- while (cursor < recording.events.length && recording.events[cursor][0] === i) {
218
- const [, method, ...args] = recording.events[cursor];
219
- const fn = input[method];
220
- if (typeof fn === "function") fn.apply(engine.input, args);
221
- cursor += 1;
222
- }
223
- engine.tick(recording.ticks[i]);
224
- opts?.onTick?.(i);
225
- }
226
- }
227
- //#endregion
228
158
  //#region src/core/uid-gen.ts
229
159
  /**
230
160
  * The ONE way to mint a node uid: crypto-strength, `n_` + 16 base36 chars
@@ -242,6 +172,6 @@ function newUid() {
242
172
  //#endregion
243
173
  //#region src/index.ts
244
174
  /** Engine version. Kept in sync with package.json by the release pipeline. */
245
- const VERSION = "0.36.2";
175
+ const VERSION = "0.38.0";
246
176
  //#endregion
247
177
  export { AudioBuses, AudioPlayer, BASE_LOCALE, Behavior, CONST_REF_KEY, Engine, HudLayer, IncantoError, InputMap, Localization, LogManager, MusicManager, Node, ORDER_GROUP_BASE, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, ParticleSim, ROLLOFF_MODELS, Rng, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, SaveSlots, Scene, SceneTree, Settings, SfxEngine, Signal, T_PREFIX, Timer, TouchControls, UiBanner, UiBar, UiButton, UiDialogue, UiImage, UiPanel, UiSelect, UiSlider, UiText, UiToggle, VERSION, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, behaviorSchema, behaviorSignals, behaviorsWithoutSave, captureBehaviors, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, mergeStaticSignals, newUid, parseNodePath, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveConstants, resolveOrderGroups, resolveRendering, resolveViewport, restoreBehaviors, serializeNode, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
@@ -1,4 +1,4 @@
1
- import { N as Scene, b as Engine } from "./behavior-DibCwrW7.js";
1
+ import { N as Scene, b as Engine } from "./behavior-B5PJ32eu.js";
2
2
  import { i as SceneJson } from "./schema-CFeioQRE.js";
3
3
 
4
4
  //#region src/core/scene/loader.d.ts
@@ -221,6 +221,21 @@ var InputMap = class {
221
221
  * scripted gameplay tests and touch buttons drive the game by intent
222
222
  * (`press('jump')`) instead of reverse-engineering keybinds.
223
223
  */
224
+ /**
225
+ * Every action this scene declared, with its kind.
226
+ *
227
+ * A scene states its own control vocabulary in `input{}`, which means a tool
228
+ * can drive a game it has never seen — the automated playtester's entire
229
+ * premise. Nothing else could ask: `actions` was private.
230
+ */
231
+ declaredActions() {
232
+ const out = [];
233
+ for (const [name, def] of this.actions) out.push({
234
+ name,
235
+ type: def.type
236
+ });
237
+ return out;
238
+ }
224
239
  pressAction(action) {
225
240
  this.button(action);
226
241
  if (!this.injectedDown.has(action)) {
package/dist/net.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Tt as Node, b as Engine, d as PropSchema, jt as Signal } from "./behavior-DibCwrW7.js";
1
+ import { Tt as Node, b as Engine, d as PropSchema, jt as Signal } from "./behavior-B5PJ32eu.js";
2
2
  import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
3
3
 
4
4
  //#region src/net/types.d.ts
package/dist/net.js CHANGED
@@ -1,9 +1,9 @@
1
- import { n as loadScene } from "./loader-DAZlgqeC.js";
2
- import { h as Engine } from "./register-BHAwM4bK.js";
1
+ import { n as loadScene } from "./loader-r49nDwB4.js";
2
+ import { h as Engine } from "./register-Dm7nrd-E.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as jsonClone } from "./json-BLk7H2Qa.js";
5
5
  import { n as createAgent8Server } from "./agent8-CvsfVskX.js";
6
- import { i as applySyncPatch, n as NetworkSpawner, r as NetworkManager, t as registerNodesNet } from "./register-DXwtIVqP.js";
6
+ import { i as applySyncPatch, n as NetworkSpawner, r as NetworkManager, t as registerNodesNet } from "./register-B4RRQbyq.js";
7
7
  //#region src/net/loopback.ts
8
8
  /**
9
9
  * In-memory multiplayer hub implementing the SAME kernel contract as
@@ -1,4 +1,4 @@
1
- import { Tt as Node, d as PropSchema, ot as Listener } from "./behavior-DibCwrW7.js";
1
+ import { Tt as Node, d as PropSchema, ot as Listener } from "./behavior-B5PJ32eu.js";
2
2
 
3
3
  //#region src/core/nodes/audio-player.d.ts
4
4
  /**
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { v as diagnose } from "./loader-DAZlgqeC.js";
2
+ import { v as diagnose } from "./loader-r49nDwB4.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
- import { _ as RigidBody2D, b as validateCollider2D, g as PhysicsBody2D, h as CharacterBody2D, m as Area2D, p as Joint2D, y as Node2D } from "./register-COGCNitr.js";
4
+ import { _ as RigidBody2D, b as validateCollider2D, g as PhysicsBody2D, h as CharacterBody2D, m as Area2D, p as Joint2D, y as Node2D } from "./register-DnB9m7Rl.js";
5
5
  import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
6
6
  //#region src/2d/physics/physics-2d.ts
7
7
  var physics_2d_exports = /* @__PURE__ */ __exportAll({
@@ -1,9 +1,9 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { v as diagnose } from "./loader-DAZlgqeC.js";
2
+ import { v as diagnose } from "./loader-r49nDwB4.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
- import { F as PhysicsBody3D, I as RigidBody3D, N as Area3D, P as CharacterBody3D, R as Node3D, z as validateCollider3D } from "./gameplay-C0MUMSak.js";
4
+ import { F as PhysicsBody3D, I as RigidBody3D, N as Area3D, P as CharacterBody3D, R as Node3D, z as validateCollider3D } from "./gameplay-CsJDUQh_.js";
5
5
  import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
6
- import { A as Terrain3D, F as InstancedMesh3D, L as buildMeshGeometry, P as Joint3D } from "./environment-presets-fK9oyrrR.js";
6
+ import { A as Terrain3D, F as InstancedMesh3D, L as buildMeshGeometry, P as Joint3D } from "./environment-presets-C0Axpfth.js";
7
7
  import { Euler, Matrix4, Quaternion, Vector3 } from "three";
8
8
  //#region src/3d/physics/collider-lines.ts
9
9
  /**
package/dist/react.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { N as Scene, b as Engine, n as BehaviorCtor } from "./behavior-DibCwrW7.js";
1
+ import { N as Scene, b as Engine, n as BehaviorCtor } from "./behavior-B5PJ32eu.js";
2
2
  import { c as JsonValue } from "./schema-CFeioQRE.js";
3
3
  import { CSSProperties, ReactNode } from "react";
4
4
 
package/dist/react.js CHANGED
@@ -156,7 +156,7 @@ function IncantoCanvas(props) {
156
156
  pointer: latest.pointer,
157
157
  ...keyboard !== void 0 ? { keyboard } : {}
158
158
  };
159
- const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-DnOu2aAa.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-D-StzrTj.js").then((n) => n.n)).createGame2D(o)))(opts);
159
+ const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-DqY2lMBc.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-C4t5gVhc.js").then((n) => n.n)).createGame2D(o)))(opts);
160
160
  if (disposed) {
161
161
  next.dispose();
162
162
  return;
@@ -1,5 +1,5 @@
1
- import { E as Signal, f as Node, t as buildNodeJson } from "./loader-DAZlgqeC.js";
2
- import { t as registerCoreNodes } from "./register-BHAwM4bK.js";
1
+ import { E as Signal, f as Node, t as buildNodeJson } from "./loader-r49nDwB4.js";
2
+ import { t as registerCoreNodes } from "./register-Dm7nrd-E.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
5
5
  import { l as registerNode } from "./registry-IyWCGe4q.js";