incanto 0.48.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 (52) hide show
  1. package/bin/incanto-model.mjs +13 -1
  2. package/bin/incanto-playtest.mjs +7 -0
  3. package/bin/incanto-verify.mjs +10 -2
  4. package/dist/2d.d.ts +21 -12
  5. package/dist/2d.js +57 -3
  6. package/dist/3d.d.ts +38 -1
  7. package/dist/3d.js +114 -4
  8. package/dist/{create-game-CniOiWzN.js → create-game-BgV6UbVA.js} +5 -5
  9. package/dist/{create-game-akjPkFv5.js → create-game-DFBjMetZ.js} +5 -5
  10. package/dist/{duplicate-CRtihGmC.js → duplicate-CI9WF_bg.js} +1 -1
  11. package/dist/{environment-presets-D1b0ydTS.js → environment-presets-CZOH5TY5.js} +15 -20
  12. package/dist/{gameplay-BQOeAid6.js → gameplay-02Btmmjn.js} +80 -9
  13. package/dist/gameplay.d.ts +17 -0
  14. package/dist/gameplay.js +1 -1
  15. package/dist/index.d.ts +39 -1
  16. package/dist/index.js +7 -7
  17. package/dist/{loader-r49nDwB4.js → loader-DwazzlQb.js} +36 -6
  18. package/dist/net.js +3 -3
  19. package/dist/{physics-2d-BmgXBNDB.js → physics-2d-vyCBfACH.js} +3 -3
  20. package/dist/{physics-3d-CSoGjM8P.js → physics-3d-DpRqw8Mz.js} +4 -4
  21. package/dist/react.js +1 -1
  22. package/dist/{register-R2JTnIMw.js → register-BNPZYJmd.js} +23 -22
  23. package/dist/{register-D651it1J.js → register-CB11yp21.js} +2 -2
  24. package/dist/{register-BSXV8T9F.js → register-uvaZj1KX.js} +62 -2
  25. package/dist/{replay-DilbZgQI.js → replay-C0XJIsO7.js} +1 -1
  26. package/dist/sprite-animation-CMr6f1K2.d.ts +44 -0
  27. package/dist/{particle-sim-Bw7hB93B.js → sprite-animation-D_p28jwU.js} +63 -1
  28. package/dist/{src-Bl1Kire1.js → src-DF4gCsqO.js} +1 -1
  29. package/dist/{test-E4-otKqK.js → test-DRna_BQU.js} +210 -30
  30. package/dist/test.d.ts +38 -6
  31. package/dist/test.js +2 -2
  32. package/dist/vite.js +2 -2
  33. package/editor/assets/{agent8-_007gPF8.js → agent8-DCW4TgDt.js} +1 -1
  34. package/editor/assets/{debug-0DI_MJaq.js → debug-RC6qts6S.js} +1 -1
  35. package/editor/assets/{index-B-6eYZEi.js → index-5dEIhvsf.js} +92 -92
  36. package/editor/index.html +1 -1
  37. package/package.json +1 -1
  38. package/schemas/scene.schema.json +10 -3
  39. package/skills/incanto-3d-character.md +13 -2
  40. package/skills/incanto-3d-models.md +40 -0
  41. package/skills/incanto-assets.md +15 -0
  42. package/skills/incanto-building-2d-games.md +57 -5
  43. package/skills/incanto-building-3d-games.md +7 -0
  44. package/skills/incanto-gameplay-behaviors.md +18 -1
  45. package/skills/incanto-hud.md +39 -0
  46. package/skills/incanto-node-reference.md +5 -3
  47. package/skills/incanto-physics-and-input.md +1 -1
  48. package/skills/incanto-playtesting.md +9 -2
  49. package/skills/incanto-verifying-your-game.md +14 -0
  50. package/templates-app/beacon-isle-3d/package.json +1 -1
  51. package/templates-app/tps-3d/package.json +1 -1
  52. package/templates-app/village-quest-3d/package.json +1 -1
@@ -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 };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { $ as suggestLocale, A as SettingsValues, At as LogLevel, B as captureBehaviors, C as EngineStats, Ct as PlayMusicOptions, Dt as NodeLifecycle, E as DeviceHints, Et as Node, F as BehaviorState, G as OrderGroup, H as SaveStore, I as RestoreReport, J as resolveOrderGroups, K as OrderGroupTable, L as SaveSlot, M as readDeviceHints, Mt as Signal, N as suggestQuality, Nt as SignalListener, O as QualityTier, Ot as SceneTree, P as Scene, Q as T_PREFIX, R as SaveSlots, S as Scheduler, St as MusicTrack, T as RendererStats, Tt as BusName, U as createSaveStore, V as restoreBehaviors, W as ORDER_GROUP_BASE, X as LocaleTables, Y as BASE_LOCALE, Z as Localization, _ as mergeStaticSignals, _t as SfxWave, a as clearBehaviors, at as Voice, b as Engine, bt as MusicBackend, c as registeredBehaviors, ct as ROLLOFF_MODELS, d as PropSchema, dt as Vec3, et as translationKey, f as clearRegistry, ft as spatialGain, g as getNodeType, gt as SfxParams, h as getNodeSignals, ht as SFX_PRESET_NAMES, i as behaviorSignals, it as isAudioContextAvailable, j as qualityEnvironment, jt as LogManager, k as Settings, kt as LogEntry, l as NodeCtor, lt as RolloffModel, m as getNodeSchema, mt as SFX_PRESETS, n as BehaviorCtor, nt as SfxEngine, o as getBehavior, ot as VoicePreset, p as createNode, pt as spatialPan, q as effectiveOrder, r as behaviorSchema, rt as SfxPlayOptions, s as registerBehavior, st as Listener, t as Behavior, tt as InputMap, u as PropDef, ut as SpatialParams, v as registerNode, vt as SynthOptions, w as GameStats, wt as AudioBuses, x as EngineOptions, xt as MusicManager, y as registeredTypes, yt as synthSfx, z as behaviorsWithoutSave } from "./behavior-62q0HWBO.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
3
  import { n as loadScene, t as LoadSceneOptions } from "./loader-CeyU_bm1.js";
4
+ import { i as resolveFrames, n as AnimationEntry, r as resolveAnimation, t as AnimationDef } from "./sprite-animation-CMr6f1K2.js";
4
5
  import { n as ParticleSimConfig, r as ParticleView, t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
5
6
  import { a as AudioElementLike, i as gridFromRows, n as PathGrid, o as AudioPlayer, r as findPath, t as FindPathOptions } from "./pathfinding-C49JSNNq.js";
6
7
  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-CAphXMyM.js";
@@ -319,10 +320,37 @@ declare class UiText extends HudWidgetBase {
319
320
  static override readonly typeName: string;
320
321
  static override readonly props: PropSchema;
321
322
  text: string;
323
+ /**
324
+ * A template with a `{}` slot that `setText` fills — `"Gems {} / 8"`.
325
+ *
326
+ * Empty (the default) means `setText` replaces the whole line. Resolved at
327
+ * PAINT like `text`, so `"@t:hud.gems"` works and changing locale re-reads
328
+ * it: the value lives here, the words live in the strings table.
329
+ */
330
+ format: string;
322
331
  size: number;
323
332
  color: string;
324
333
  shadow: boolean;
325
334
  private last;
335
+ /** null until something has actually set a value — see `shown`. */
336
+ private slot;
337
+ /**
338
+ * Put a signal's value on screen — `scoreChanged → setText`.
339
+ *
340
+ * A `connections[]` handler must be a METHOD and `text` is a property, so the
341
+ * one wire every game needs (score to screen) could not be declared: every
342
+ * HUD began with a behavior whose whole job was one assignment.
343
+ */
344
+ setText(value: unknown): void;
345
+ /**
346
+ * What it actually paints, after the template and the locale.
347
+ *
348
+ * Before the first value arrives, a formatted widget shows `text` — its
349
+ * authored opening line. Filling the slot with nothing instead would put
350
+ * `"Gems / 8"` on screen for every game's first frame, and a score line
351
+ * that starts blank looks broken rather than empty.
352
+ */
353
+ get shown(): string;
326
354
  protected _build(): HTMLElement;
327
355
  protected override _sync(): void;
328
356
  }
@@ -345,6 +373,16 @@ declare class UiBar extends HudWidgetBase {
345
373
  private lastRatio;
346
374
  /** Current fill ratio 0..1 (what the bar shows). */
347
375
  get ratio(): number;
376
+ /**
377
+ * Drive the bar from a signal — `healthChanged → setValue`.
378
+ *
379
+ * Takes the max as a second argument because that is the shape the signal
380
+ * already has, so a bar whose ceiling moves (a max-HP upgrade) needs no
381
+ * second wire.
382
+ */
383
+ setValue(value: number, max?: number): void;
384
+ /** Move the ceiling on its own, leaving the fill where it is. */
385
+ setMax(max: number): void;
348
386
  protected _build(): HTMLElement;
349
387
  protected override _sync(): void;
350
388
  }
@@ -810,4 +848,4 @@ declare function computeViewport(canvasW: number, canvasH: number, viewport: {
810
848
  /** Engine version. Kept in sync with package.json by the release pipeline. */
811
849
  declare const VERSION: string;
812
850
  //#endregion
813
- export { type AssetFailure, AudioBuses, type AudioElementLike, AudioPlayer, BASE_LOCALE, Behavior, type BehaviorCtor, type BehaviorState, type BusName, CONST_REF_KEY, type ComputedViewport, type ConnectionJson, type CrossfadeGains, type DebugLineSource, type DeviceHints, type DriveScript, type DriveStep, Engine, type EngineOptions, type EngineStats, type FindPathOptions, type GameStats, type GroupedLog, type HudAnchor, HudLayer, IncantoError, type IncantoErrorCode, type IncantoErrorDetails, InputMap, type JsonKind, type JsonObject, type JsonValue, type Listener, type LiveInput, type LiveReport, type LoadSceneOptions, type LocaleTables, Localization, type LogEntry, type LogLevel, LogManager, type MusicBackend, MusicManager, type MusicTrack, Node, type NodeCtor, type NodeJson, type NodeLifecycle, ORDER_GROUP_BASE, type OrderGroup, type OrderGroupTable, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, type ParsedNodePath, type ParticlePresetValues, ParticleSim, type ParticleSimConfig, type ParticleView, type PathGrid, type PlayMusicOptions, type PreloadResult, type PropDef, type PropSchema, type QualityTier, ROLLOFF_MODELS, type Recorder, type RendererStats, type ReplayEvent, type ReplayJson, type ResolvedRendering, type ResolvedViewport, type RestoreReport, Rng, type RolloffModel, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, type SaveSlot, SaveSlots, type SaveStore, Scene, type SceneJson, SceneTree, type Scheduler, Settings, type SettingsValues, SfxEngine, type SfxParams, type SfxPlayOptions, type SfxWave, Signal, type SignalListener, type SpatialParams, type SynthOptions, T_PREFIX, Timer, TouchControls, type TouchControlsOptions, UiBanner, UiBar, UiButton, UiDialogue, UiImage, UiPanel, UiSelect, UiSlider, UiText, UiToggle, VERSION, type Vec3, type ViewportFit, type Voice, type VoicePreset, 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, logReport, logText, mergeStaticSignals, newUid, parseDrive, parseNodePath, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveConstants, resolveOrderGroups, resolveRendering, resolveViewport, restoreBehaviors, serializeNode, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
851
+ export { type AnimationDef, type AnimationEntry, type AssetFailure, AudioBuses, type AudioElementLike, AudioPlayer, BASE_LOCALE, Behavior, type BehaviorCtor, type BehaviorState, type BusName, CONST_REF_KEY, type ComputedViewport, type ConnectionJson, type CrossfadeGains, type DebugLineSource, type DeviceHints, type DriveScript, type DriveStep, Engine, type EngineOptions, type EngineStats, type FindPathOptions, type GameStats, type GroupedLog, type HudAnchor, HudLayer, IncantoError, type IncantoErrorCode, type IncantoErrorDetails, InputMap, type JsonKind, type JsonObject, type JsonValue, type Listener, type LiveInput, type LiveReport, type LoadSceneOptions, type LocaleTables, Localization, type LogEntry, type LogLevel, LogManager, type MusicBackend, MusicManager, type MusicTrack, Node, type NodeCtor, type NodeJson, type NodeLifecycle, ORDER_GROUP_BASE, type OrderGroup, type OrderGroupTable, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, type ParsedNodePath, type ParticlePresetValues, ParticleSim, type ParticleSimConfig, type ParticleView, type PathGrid, type PlayMusicOptions, type PreloadResult, type PropDef, type PropSchema, type QualityTier, ROLLOFF_MODELS, type Recorder, type RendererStats, type ReplayEvent, type ReplayJson, type ResolvedRendering, type ResolvedViewport, type RestoreReport, Rng, type RolloffModel, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, type SaveSlot, SaveSlots, type SaveStore, Scene, type SceneJson, SceneTree, type Scheduler, Settings, type SettingsValues, SfxEngine, type SfxParams, type SfxPlayOptions, type SfxWave, Signal, type SignalListener, type SpatialParams, type SynthOptions, T_PREFIX, Timer, TouchControls, type TouchControlsOptions, UiBanner, UiBar, UiButton, UiDialogue, UiImage, UiPanel, UiSelect, UiSlider, UiText, UiToggle, VERSION, type Vec3, type ViewportFit, type Voice, type VoicePreset, 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, logReport, logText, mergeStaticSignals, newUid, parseDrive, parseNodePath, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveAnimation, resolveConstants, resolveFrames, resolveOrderGroups, resolveRendering, resolveViewport, restoreBehaviors, serializeNode, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
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-r49nDwB4.js";
2
- import { A as T_PREFIX, B as synthSfx, C as behaviorsWithoutSave, D as LogManager, E as createSaveStore, F as ROLLOFF_MODELS, G as AudioBuses, H as WebAudioMusicBackend, I as spatialGain, L as spatialPan, M as translationKey, N as SfxEngine, O as BASE_LOCALE, P as isAudioContextAvailable, R as SFX_PRESETS, S as SaveSlots, T as restoreBehaviors, U as crossfadeGains, V as MusicManager, W as fadeGain, a as UiSlider, b as readDeviceHints, c as UiButton, d as UiBanner, f as UiBar, g as Settings, h as Engine, i as UiSelect, j as suggestLocale, k as Localization, 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 qualityEnvironment, w as captureBehaviors, x as suggestQuality, z as SFX_PRESET_NAMES } from "./register-BSXV8T9F.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-DwazzlQb.js";
2
+ import { A as T_PREFIX, B as synthSfx, C as behaviorsWithoutSave, D as LogManager, E as createSaveStore, F as ROLLOFF_MODELS, G as AudioBuses, H as WebAudioMusicBackend, I as spatialGain, L as spatialPan, M as translationKey, N as SfxEngine, O as BASE_LOCALE, P as isAudioContextAvailable, R as SFX_PRESETS, S as SaveSlots, T as restoreBehaviors, U as crossfadeGains, V as MusicManager, W as fadeGain, a as UiSlider, b as readDeviceHints, c as UiButton, d as UiBanner, f as UiBar, g as Settings, h as Engine, i as UiSelect, j as suggestLocale, k as Localization, 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 qualityEnvironment, w as captureBehaviors, x as suggestQuality, z as SFX_PRESET_NAMES } from "./register-uvaZj1KX.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
- import { n as startRecording, r as auditScene, t as replay } from "./replay-DilbZgQI.js";
5
+ import { n as startRecording, r as auditScene, t as replay } from "./replay-C0XJIsO7.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 { n as logText, r as parseDrive, t as logReport } from "./log-report-lxrQY9cH.js";
9
9
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
10
- import { i as applyParticlePreset, n as PARTICLE_PRESETS, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-Bw7hB93B.js";
11
- import { a as findPath, i as preloadUrls, n as newUid, o as gridFromRows, r as assetUrls, t as VERSION } from "./src-Bl1Kire1.js";
10
+ import { a as PARTICLE_PRESETS, i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-D_p28jwU.js";
11
+ import { a as findPath, i as preloadUrls, n as newUid, o as gridFromRows, r as assetUrls, t as VERSION } from "./src-DF4gCsqO.js";
12
12
  import { i as resolveRendering, n as attachTouchControls, r as joystickVector, t as TouchControls } from "./touch-BoNg_MnF.js";
13
- import { t as duplicateNode } from "./duplicate-CRtihGmC.js";
14
- 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, logReport, logText, mergeStaticSignals, newUid, parseDrive, parseNodePath, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveConstants, resolveOrderGroups, resolveRendering, resolveViewport, restoreBehaviors, serializeNode, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
13
+ import { t as duplicateNode } from "./duplicate-CI9WF_bg.js";
14
+ 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, logReport, logText, mergeStaticSignals, newUid, parseDrive, parseNodePath, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveAnimation, resolveConstants, resolveFrames, resolveOrderGroups, resolveRendering, resolveViewport, restoreBehaviors, serializeNode, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
@@ -1606,13 +1606,17 @@ function wireConnections(root, connections, declareSignals = false) {
1606
1606
  const listener = (...args) => {
1607
1607
  if (conn.filter && !matchesFilter(args[0], conn.filter)) return;
1608
1608
  if (conn.once) fromNode.off(conn.signal, listener);
1609
- const fn = toNode[conn.handler];
1610
- if (typeof fn === "function") {
1611
- fn.apply(toNode, args);
1612
- return;
1609
+ try {
1610
+ const fn = toNode[conn.handler];
1611
+ if (typeof fn === "function") {
1612
+ fn.apply(toNode, args);
1613
+ return;
1614
+ }
1615
+ const bfn = toNode.behavior ? toNode.behavior[conn.handler] : void 0;
1616
+ if (typeof bfn === "function") bfn.apply(toNode.behavior, args);
1617
+ } catch (error) {
1618
+ quarantineWire(fromNode, toNode, conn, listener, error);
1613
1619
  }
1614
- const bfn = toNode.behavior ? toNode.behavior[conn.handler] : void 0;
1615
- if (typeof bfn === "function") bfn.apply(toNode.behavior, args);
1616
1620
  };
1617
1621
  try {
1618
1622
  fromNode.on(conn.signal, listener);
@@ -1622,6 +1626,32 @@ function wireConnections(root, connections, declareSignals = false) {
1622
1626
  }
1623
1627
  }
1624
1628
  }
1629
+ /**
1630
+ * A handler threw. Turn THAT WIRE off, and leave the game running.
1631
+ *
1632
+ * The engine already draws this line for scripts: "a behavior is your code; the
1633
+ * node is the engine's. A typo in the first must not switch off the second, or
1634
+ * breaking a sword script stops the character from walking." A connection is
1635
+ * the same category — authored wiring, not the emitting node's own logic — and
1636
+ * it behaved the opposite way, because the handler runs inside `Signal.emit`
1637
+ * and its exception surfaces in whoever EMITTED. The node quarantine then blames
1638
+ * and skips that node, so a `movementStateChanged → play` wire naming a clip the
1639
+ * sheet does not have switched off the character controller: no animation, and
1640
+ * no movement either.
1641
+ *
1642
+ * Reported ONCE and disconnected, on the same reasoning as the node quarantine:
1643
+ * a handler that throws throws every frame, and sixty identical traces a second
1644
+ * bury the one line that matters.
1645
+ */
1646
+ function quarantineWire(fromNode, toNode, conn, listener, error) {
1647
+ fromNode.off(conn.signal, listener);
1648
+ const target = toNode.behavior ? `'${conn.handler}' on behavior '${toNode.script?.name}' (${toNode.getPath()})` : `'${conn.handler}' on ${toNode.getPath()}`;
1649
+ const message = `[incanto] connection '${conn.signal}' from ${fromNode.getPath()} → handler ${target} threw — THIS WIRE is now off; the node that emitted the signal, and the rest of the scene, keep running.`;
1650
+ const engine = toNode.tree?.engine ?? fromNode.tree?.engine;
1651
+ engine?.log.error(message, error);
1652
+ engine?._nodeErrored(toNode);
1653
+ console.error(message, error);
1654
+ }
1625
1655
  function resolveEndpoint(root, path, conn, side) {
1626
1656
  try {
1627
1657
  return root.getNode(path);
package/dist/net.js CHANGED
@@ -1,9 +1,9 @@
1
- import { n as loadScene } from "./loader-r49nDwB4.js";
2
- import { h as Engine } from "./register-BSXV8T9F.js";
1
+ import { n as loadScene } from "./loader-DwazzlQb.js";
2
+ import { h as Engine } from "./register-uvaZj1KX.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-D651it1J.js";
6
+ import { i as applySyncPatch, n as NetworkSpawner, r as NetworkManager, t as registerNodesNet } from "./register-CB11yp21.js";
7
7
  //#region src/net/loopback.ts
8
8
  /**
9
9
  * In-memory multiplayer hub implementing the SAME kernel contract as
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { v as diagnose } from "./loader-r49nDwB4.js";
2
+ import { v as diagnose } from "./loader-DwazzlQb.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-R2JTnIMw.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-BNPZYJmd.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({
@@ -122,7 +122,7 @@ var Physics2D = class {
122
122
  moveAndSlide(node) {
123
123
  const e = this.ensureEntry(node);
124
124
  const vy = node.velocity[1] ?? 0;
125
- if (node.snapToGround && vy >= 0) this.kcc.enableSnapToGround(.05);
125
+ if (node.stickToGround && vy >= 0) this.kcc.enableSnapToGround(.05);
126
126
  else this.kcc.disableSnapToGround();
127
127
  this.kcc.setMaxSlopeClimbAngle(node.slopeLimitDeg * DEG);
128
128
  let carryX = 0;
@@ -1,9 +1,9 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { v as diagnose } from "./loader-r49nDwB4.js";
2
+ import { v as diagnose } from "./loader-DwazzlQb.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-BQOeAid6.js";
4
+ import { F as PhysicsBody3D, I as RigidBody3D, N as Area3D, P as CharacterBody3D, R as Node3D, z as validateCollider3D } from "./gameplay-02Btmmjn.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-D1b0ydTS.js";
6
+ import { A as Terrain3D, F as InstancedMesh3D, L as buildMeshGeometry, P as Joint3D } from "./environment-presets-CZOH5TY5.js";
7
7
  import { Euler, Matrix4, Quaternion, Vector3 } from "three";
8
8
  //#region src/3d/physics/collider-lines.ts
9
9
  /**
@@ -373,7 +373,7 @@ var Physics3D = class {
373
373
  const e = this.ensureEntry(node);
374
374
  if (!e) return;
375
375
  const vy = node.velocity[1] ?? 0;
376
- if (node.snapToGround && vy <= 0) this.kcc.enableSnapToGround(.1);
376
+ if (node.stickToGround && vy <= 0) this.kcc.enableSnapToGround(.1);
377
377
  else this.kcc.disableSnapToGround();
378
378
  this.kcc.setMaxSlopeClimbAngle(node.slopeLimitDeg * DEG);
379
379
  let carryX = 0;
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-akjPkFv5.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-CniOiWzN.js").then((n) => n.n)).createGame2D(o)))(opts);
159
+ const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-DFBjMetZ.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-BgV6UbVA.js").then((n) => n.n)).createGame2D(o)))(opts);
160
160
  if (disposed) {
161
161
  next.dispose();
162
162
  return;
@@ -1,9 +1,9 @@
1
- import { f as Node, h as effectiveOrder } from "./loader-r49nDwB4.js";
2
- import { t as registerCoreNodes } from "./register-BSXV8T9F.js";
1
+ import { f as Node, h as effectiveOrder } from "./loader-DwazzlQb.js";
2
+ import { t as registerCoreNodes } from "./register-uvaZj1KX.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
5
  import { i as getNodeSchema, l as registerNode } from "./registry-IyWCGe4q.js";
6
- import { i as applyParticlePreset, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-Bw7hB93B.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
7
  import { AdditiveBlending, BufferAttribute, BufferGeometry, CanvasTexture, Color, DynamicDrawUsage, Group, InstancedMesh, MathUtils, Matrix4, Mesh, MeshBasicMaterial, NearestFilter, NormalBlending, Object3D, PlaneGeometry } from "three";
8
8
  //#region src/2d/physics/collider-validate.ts
9
9
  /**
@@ -198,13 +198,19 @@ var CharacterBody2D = class extends PhysicsBody2D {
198
198
  static typeName = "CharacterBody2D";
199
199
  static props = {
200
200
  velocity: { default: [0, 0] },
201
- snapToGround: { default: true },
201
+ stickToGround: { default: true },
202
202
  slopeLimitDeg: { default: 45 }
203
203
  };
204
+ /**
205
+ * Renamed to match the 3D body, where `snapToGround` had to give the name
206
+ * back to `Node3D`'s PLACEMENT prop. 2D has no placement prop, so nothing is
207
+ * ambiguous here and old scenes keep loading unchanged.
208
+ */
209
+ static propAliases = { snapToGround: "stickToGround" };
204
210
  /** px/s, y-down (up = -y). */
205
211
  velocity = [0, 0];
206
212
  /** Keep contact on slopes/steps while not moving upward. */
207
- snapToGround = true;
213
+ stickToGround = true;
208
214
  /** Max climbable slope angle. */
209
215
  slopeLimitDeg = 45;
210
216
  /** @internal Updated by Physics2D.moveAndSlide. */
@@ -402,13 +408,19 @@ var AnimatedSprite2D = class extends Sprite2D {
402
408
  frameList = [];
403
409
  frameIndex = 0;
404
410
  frameTime = 0;
411
+ currentDef = null;
405
412
  ownTexture = null;
406
413
  loadedSheetRef = "";
414
+ /**
415
+ * `currentAnimation` reports the clip that is RUNNING, not the name asked
416
+ * for: an alias is a rename, and a state machine that reads back `fall` while
417
+ * the idle frames play is describing something that is not happening.
418
+ */
407
419
  play(name) {
408
- const def = this.animations[name];
409
- if (!def) throw new IncantoError("UNKNOWN_ANIMATION", `No animation '${name}' on '${this.name}'. Available: [${Object.keys(this.animations).join(", ")}].`);
410
- this.currentAnimation = name;
411
- this.frameList = resolveFrames(def.frames, name);
420
+ const { name: resolved, def } = resolveAnimation(this.animations, name, this.name);
421
+ this.currentAnimation = resolved;
422
+ this.currentDef = def;
423
+ this.frameList = resolveFrames(def.frames, resolved);
412
424
  this.frameIndex = 0;
413
425
  this.currentFrame = this.frameList[0] ?? 0;
414
426
  this.frameTime = 0;
@@ -418,11 +430,12 @@ var AnimatedSprite2D = class extends Sprite2D {
418
430
  this.playing = false;
419
431
  }
420
432
  onReady() {
433
+ validateAnimationAliases(this.animations, this.name);
421
434
  if (this.autoplay) this.play(this.autoplay);
422
435
  }
423
436
  update(dt) {
424
437
  if (!this.playing || this.currentAnimation === "") return;
425
- const def = this.animations[this.currentAnimation];
438
+ const def = this.currentDef;
426
439
  if (!def || !Number.isFinite(def.fps) || def.fps <= 0) return;
427
440
  const perFrame = 1 / def.fps;
428
441
  this.frameTime += dt;
@@ -462,18 +475,6 @@ var AnimatedSprite2D = class extends Sprite2D {
462
475
  };
463
476
  }
464
477
  };
465
- function resolveFrames(frames, name) {
466
- if (!Array.isArray(frames) || frames.length === 0) throw new IncantoError("BAD_FORMAT", `Animation '${name}': "frames" must be a non-empty array.`);
467
- if (frames.length === 2) {
468
- const [start, end] = frames;
469
- if (end >= start) {
470
- const out = [];
471
- for (let f = start; f <= end; f++) out.push(f);
472
- return out;
473
- }
474
- }
475
- return [...frames];
476
- }
477
478
  //#endregion
478
479
  //#region src/2d/nodes/camera-2d.ts
479
480
  /**
@@ -1,5 +1,5 @@
1
- import { E as Signal, f as Node, t as buildNodeJson } from "./loader-r49nDwB4.js";
2
- import { t as registerCoreNodes } from "./register-BSXV8T9F.js";
1
+ import { E as Signal, f as Node, t as buildNodeJson } from "./loader-DwazzlQb.js";
2
+ import { t as registerCoreNodes } from "./register-uvaZj1KX.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";
@@ -1,4 +1,4 @@
1
- import { E as Signal, _ as InputMap, b as behaviorSchema, f as Node, g as resolveOrderGroups, m as ORDER_GROUP_BASE, v as diagnose } from "./loader-r49nDwB4.js";
1
+ import { E as Signal, _ as InputMap, b as behaviorSchema, f as Node, g as resolveOrderGroups, m as ORDER_GROUP_BASE, v as diagnose } from "./loader-DwazzlQb.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 { l as registerNode } from "./registry-IyWCGe4q.js";
@@ -2558,28 +2558,73 @@ var UiText = class extends HudWidgetBase {
2558
2558
  static props = {
2559
2559
  ...HudWidgetBase.props,
2560
2560
  text: { default: "" },
2561
+ format: { default: "" },
2561
2562
  size: { default: 16 },
2562
2563
  color: { default: "#ffffff" },
2563
2564
  shadow: { default: true }
2564
2565
  };
2565
2566
  text = "";
2567
+ /**
2568
+ * A template with a `{}` slot that `setText` fills — `"Gems {} / 8"`.
2569
+ *
2570
+ * Empty (the default) means `setText` replaces the whole line. Resolved at
2571
+ * PAINT like `text`, so `"@t:hud.gems"` works and changing locale re-reads
2572
+ * it: the value lives here, the words live in the strings table.
2573
+ */
2574
+ format = "";
2566
2575
  size = 16;
2567
2576
  color = "#ffffff";
2568
2577
  shadow = true;
2569
2578
  last = "\0";
2579
+ /** null until something has actually set a value — see `shown`. */
2580
+ slot = null;
2581
+ /**
2582
+ * Put a signal's value on screen — `scoreChanged → setText`.
2583
+ *
2584
+ * A `connections[]` handler must be a METHOD and `text` is a property, so the
2585
+ * one wire every game needs (score to screen) could not be declared: every
2586
+ * HUD began with a behavior whose whole job was one assignment.
2587
+ */
2588
+ setText(value) {
2589
+ this.slot = String(value ?? "");
2590
+ if (!this.format) this.text = this.slot;
2591
+ }
2592
+ /**
2593
+ * What it actually paints, after the template and the locale.
2594
+ *
2595
+ * Before the first value arrives, a formatted widget shows `text` — its
2596
+ * authored opening line. Filling the slot with nothing instead would put
2597
+ * `"Gems / 8"` on screen for every game's first frame, and a score line
2598
+ * that starts blank looks broken rather than empty.
2599
+ */
2600
+ get shown() {
2601
+ if (this.format && this.slot !== null) return this._t(this.format).replace("{}", this.slot);
2602
+ return this._t(this.text);
2603
+ }
2570
2604
  _build() {
2571
2605
  const el = document.createElement("div");
2572
2606
  el.style.cssText = `font-size:${this.size}px;color:${this.color};${this.shadow ? "text-shadow:0 1px 3px rgba(0,0,0,.7);" : ""}white-space:pre;`;
2573
2607
  return el;
2574
2608
  }
2575
2609
  _sync() {
2576
- const shown = this._t(this.text);
2610
+ const shown = this.shown;
2577
2611
  if (shown !== this.last && this._element) {
2578
2612
  this.last = shown;
2579
2613
  this._element.textContent = shown;
2580
2614
  }
2581
2615
  }
2582
2616
  };
2617
+ /**
2618
+ * A bar fed the wrong argument shows a plausible wrong number forever.
2619
+ *
2620
+ * The trap is one signal away: `Health.damaged` is `(amount, current)`, so
2621
+ * wiring it to `setValue` paints the DAMAGE as the health and looks like a
2622
+ * working health bar. Say which signal has the shape a bar wants.
2623
+ */
2624
+ function requireNumber(value, node, method) {
2625
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2626
+ throw new IncantoError("PROP_TYPE_MISMATCH", `UiBar '${node.getPath()}'.${method} needs a number, got ${JSON.stringify(value)}. Check what the wired signal passes FIRST — 'damaged' is (amount, current), 'healthChanged' is (current, max), which is the shape a bar wants.`);
2627
+ }
2583
2628
  /** A labeled progress bar (health, stamina, reload, boss HP). */
2584
2629
  var UiBar = class extends HudWidgetBase {
2585
2630
  static typeName = "UiBar";
@@ -2612,6 +2657,21 @@ var UiBar = class extends HudWidgetBase {
2612
2657
  get ratio() {
2613
2658
  return this.max > 0 ? Math.min(1, Math.max(0, this.value / this.max)) : 0;
2614
2659
  }
2660
+ /**
2661
+ * Drive the bar from a signal — `healthChanged → setValue`.
2662
+ *
2663
+ * Takes the max as a second argument because that is the shape the signal
2664
+ * already has, so a bar whose ceiling moves (a max-HP upgrade) needs no
2665
+ * second wire.
2666
+ */
2667
+ setValue(value, max) {
2668
+ this.value = requireNumber(value, this, "setValue");
2669
+ if (max !== void 0) this.max = requireNumber(max, this, "setValue");
2670
+ }
2671
+ /** Move the ceiling on its own, leaving the fill where it is. */
2672
+ setMax(max) {
2673
+ this.max = requireNumber(max, this, "setMax");
2674
+ }
2615
2675
  _build() {
2616
2676
  const wrap = document.createElement("div");
2617
2677
  wrap.style.cssText = "display:flex;align-items:center;gap:6px;";
@@ -1,4 +1,4 @@
1
- import { M as translationKey } from "./register-BSXV8T9F.js";
1
+ import { M as translationKey } from "./register-uvaZj1KX.js";
2
2
  //#region src/core/audit.ts
3
3
  const ANIMATED_TYPES = new Set([
4
4
  "Particles2D",