incanto 0.72.0 → 0.74.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 (84) hide show
  1. package/bin/incanto-check.mjs +114 -42
  2. package/bin/incanto-verify.mjs +9 -2
  3. package/dist/2d.d.ts +29 -4
  4. package/dist/2d.js +3 -3
  5. package/dist/3d.d.ts +137 -15
  6. package/dist/3d.js +6 -6
  7. package/dist/{agent8-BqZ0_O1u.js → agent8-CHTT4unP.js} +1 -1
  8. package/dist/{audio-player-ioZ7VKC6.d.ts → audio-player-BOrk4eQx.d.ts} +1 -1
  9. package/dist/{behavior-Bf9P9oB2.d.ts → behavior-J1cmVah0.d.ts} +8 -0
  10. package/dist/{create-game-CmFAtLZp.js → create-game-BAiA-FjP.js} +7 -7
  11. package/dist/{create-game-C9rWqqAC.js → create-game-ssvGvP7W.js} +7 -7
  12. package/dist/debug.d.ts +1 -1
  13. package/dist/debug.js +1 -1
  14. package/dist/editor.js +965 -893
  15. package/dist/{environment-presets-BeHoaGhy.js → environment-presets-BkqlWewf.js} +277 -60
  16. package/dist/{gameplay-BlQe-M07.js → gameplay-Bxe1sMVT.js} +611 -39
  17. package/dist/gameplay.d.ts +141 -7
  18. package/dist/gameplay.js +2 -2
  19. package/dist/index.d.ts +6 -4
  20. package/dist/index.js +6 -6
  21. package/dist/{loader-CBfQzB6S.d.ts → loader-BAeWMYfF.d.ts} +1 -1
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +2 -2
  24. package/dist/{physics-2d-DrIgTlLk.js → physics-2d-C-SfvjzH.js} +94 -11
  25. package/dist/{physics-3d-BzOCw1VS.js → physics-3d-rOzeg890.js} +202 -15
  26. package/dist/{picking-C9McVJrO.js → picking-BMV34Pjl.js} +2 -2
  27. package/dist/react.d.ts +1 -1
  28. package/dist/react.js +1 -1
  29. package/dist/{register-5IYfooAN.js → register-CjociOtt.js} +19 -4
  30. package/dist/{register-C8HuRkOf.js → register-DRQyZKGU.js} +45 -9
  31. package/dist/{replay-datiAE-b.js → replay-B6BbgGM_.js} +42 -6
  32. package/dist/{replay-CBEChqXq.d.ts → replay-wZK5Ok5c.d.ts} +1 -1
  33. package/dist/{save-slots-CEuJPUle.js → save-slots-CEUGeLuB.js} +11 -2
  34. package/dist/{split-screen-Bm5DTElG.js → split-screen-BxZ5rBe7.js} +4 -4
  35. package/dist/{split-screen-DkqLng6q.d.ts → split-screen-D6sx8z3Z.d.ts} +2 -2
  36. package/dist/{sprite-animation-CqR2o3SA.js → sprite-animation-Bz3DHD_4.js} +1 -1
  37. package/dist/{src-DRSsRleD.js → src-CqtvYtSN.js} +1 -1
  38. package/dist/{test-Djuq3Vhq.js → test-CEl0y2Sw.js} +162 -30
  39. package/dist/test.d.ts +56 -5
  40. package/dist/test.js +2 -2
  41. package/dist/{touch-BnCyPA0G.js → touch-CioTZB-y.js} +1 -1
  42. package/dist/vite.js +3 -3
  43. package/editor/assets/{agent8-CMKz2cax.js → agent8-BlzturGi.js} +1 -1
  44. package/editor/assets/{debug-DLrQm-az.js → debug-BMUi8usj.js} +1 -1
  45. package/editor/assets/{index-BEKuxYcE.js → index-CrUCQoaB.js} +53 -53
  46. package/editor/index.html +1 -1
  47. package/package.json +1 -1
  48. package/schemas/scene.schema.json +634 -10
  49. package/skills/incanto-3d-character.md +113 -5
  50. package/skills/incanto-assets.md +9 -3
  51. package/skills/incanto-behaviors-and-scripts.md +26 -2
  52. package/skills/incanto-building-2d-games.md +9 -0
  53. package/skills/incanto-building-3d-games.md +45 -0
  54. package/skills/incanto-editor.md +6 -1
  55. package/skills/incanto-environment.md +7 -2
  56. package/skills/incanto-game-feel.md +16 -11
  57. package/skills/incanto-gameplay-behaviors.md +179 -16
  58. package/skills/incanto-node-reference.md +124 -45
  59. package/skills/incanto-physics-and-input.md +67 -5
  60. package/skills/incanto-scene-json-authoring.md +9 -3
  61. package/skills/incanto-verifying-your-game.md +19 -2
  62. package/templates-app/beacon-isle-3d/PROJECT/Status.md +8 -0
  63. package/templates-app/beacon-isle-3d/PROJECT/Structure.md +3 -0
  64. package/templates-app/beacon-isle-3d/package.json +1 -1
  65. package/templates-app/beacon-isle-3d/src/game.scene.json +136 -0
  66. package/templates-app/beacon-isle-3d/verify.ts +144 -0
  67. package/templates-app/molehill-2d/package.json +1 -1
  68. package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
  69. package/templates-app/platformer-2d/PROJECT/Structure.md +3 -0
  70. package/templates-app/platformer-2d/package.json +1 -1
  71. package/templates-app/platformer-2d/src/game.scene.json +126 -0
  72. package/templates-app/platformer-2d/verify.ts +54 -0
  73. package/templates-app/star-survivor/package.json +1 -1
  74. package/templates-app/tps-3d/PROJECT/Context.md +1 -1
  75. package/templates-app/tps-3d/PROJECT/Status.md +40 -1
  76. package/templates-app/tps-3d/PROJECT/Structure.md +19 -0
  77. package/templates-app/tps-3d/package.json +1 -1
  78. package/templates-app/tps-3d/src/behaviors.ts +9 -0
  79. package/templates-app/tps-3d/src/cover.scene.json +26 -0
  80. package/templates-app/tps-3d/src/game.scene.json +331 -75
  81. package/templates-app/tps-3d/src/main.ts +14 -0
  82. package/templates-app/tps-3d/src/pillar.scene.json +25 -0
  83. package/templates-app/tps-3d/verify.ts +582 -3
  84. package/templates-app/village-quest-3d/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, t as Behavior } from "./behavior-Bf9P9oB2.js";
1
+ import { b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, t as Behavior } from "./behavior-J1cmVah0.js";
2
2
  import { c as JsonValue, s as JsonObject } from "./rng-Bb-IutXB.js";
3
3
 
4
4
  //#region src/gameplay/chase.d.ts
@@ -23,15 +23,39 @@ declare class Chase extends Behavior {
23
23
  static readonly props: PropSchema;
24
24
  static readonly signals: readonly string[];
25
25
  target: string;
26
+ targetGroup: string;
27
+ retargetEvery: number;
26
28
  speed: number;
29
+ /** The group member currently chased (`targetGroup`), or null. */
30
+ private picked;
31
+ private sincePick;
27
32
  stopRange: number;
28
33
  loseRange: number;
29
34
  moveParent: boolean;
35
+ ground: boolean | null;
30
36
  facePath: string;
37
+ /** Signed fall speed accumulated while a ground chaser is off the floor. */
38
+ private fall;
31
39
  turnSpeed: number;
32
40
  private inRange;
33
41
  private lost;
34
42
  override onReady(): void;
43
+ /**
44
+ * A walker falls off what it walks off. `CharacterBody3D` applies no gravity
45
+ * of its own ("integrate velocity yourself"), and a chaser that ignores the
46
+ * target's height would otherwise walk off a ledge and keep its altitude —
47
+ * measured on tps-3d's 0.6 m Ramp, where the old chase glided down over five
48
+ * seconds only because the player happened to stand lower. Scene gravity
49
+ * (default 9.81), accumulated while `isOnFloor()` says no, reset when it says
50
+ * yes; the KCC's snap-to-ground handles the last 10 cm.
51
+ */
52
+ private fallStep;
53
+ /** Off the floor with nowhere to chase: still fall. */
54
+ private dropIfAirborne;
55
+ /** The nearest live member of `targetGroup` — re-picked every `retargetEvery` seconds, never the mover itself. */
56
+ private nearestOfGroup;
57
+ /** See `ground`: an explicit answer, else "is the mover a 3D kinematic capsule". */
58
+ private walksTheGround;
35
59
  override update(dt: number): void;
36
60
  }
37
61
  //#endregion
@@ -142,6 +166,14 @@ declare class Health extends Behavior {
142
166
  override update(dt: number): void;
143
167
  /** Apply `n` damage. No-op while invulnerable or already dead. Emits `damaged`. */
144
168
  damage(n: number): void;
169
+ /**
170
+ * Be immune for `seconds` from NOW — a dodge roll, a spawn, a cutscene.
171
+ *
172
+ * `invulnerableFor` grants i-frames only AFTER a hit; this is the window
173
+ * before one. It extends a window already open and never shortens it, so a
174
+ * second dodge mid-window cannot expose you. Dead or not, it is harmless.
175
+ */
176
+ protect(seconds: number): void;
145
177
  /** Restore `n` hit points (clamped to `max`). No-op when dead. Emits `healed`. */
146
178
  heal(n: number): void;
147
179
  /**
@@ -231,8 +263,10 @@ declare class Health extends Behavior {
231
263
  * whose Health-owner node is in that group takes damage. This is how you stop
232
264
  * enemies killing each other — give the player's weapon `targetGroup:'enemy'`
233
265
  * and each enemy's contact hitbox `targetGroup:'player'`.
234
- * - `oncePerTarget` (default) prevents re-hitting the same node (a stationary
235
- * hazard would otherwise drain a resting body every frame).
266
+ * - `oncePerTarget` (default) prevents re-hitting the same node WHILE it stays
267
+ * in contact (a stationary hazard would otherwise drain a resting body every
268
+ * frame). It is per overlap, not per lifetime: leave and come back, or disarm
269
+ * the hitbox and arm it again over the same foe, and the hit lands again.
236
270
  * - `repeatEvery` (seconds, 0 = off) re-damages targets that STAY overlapped —
237
271
  * lava pools, poison clouds, an enemy standing on you. Contact events fire
238
272
  * only on entry/exit; this is the "and it keeps hurting" knob.
@@ -344,9 +378,20 @@ declare class Interactable extends Behavior {
344
378
  action: string;
345
379
  range: number;
346
380
  actorGroup: string;
381
+ prompt: string;
382
+ focus: "any" | "nearest" | "facing";
383
+ coneDeg: number;
384
+ /**
385
+ * The actor this Interactable would answer RIGHT NOW, or null — updated every
386
+ * frame, with `focused(actor)` / `unfocused(actor)` on the edges. A HUD reads
387
+ * the focused one's `prompt`.
388
+ */
389
+ focusedBy: Node | null;
347
390
  /** Actions already reported missing, so a per-frame read says it once. */
348
391
  private readonly reportedActions;
349
392
  override update(): void;
393
+ /** Is THIS the Interactable `actor` means — the nearest (that it faces) of every one in reach? */
394
+ private claims;
350
395
  }
351
396
  //#endregion
352
397
  //#region src/gameplay/lifetime.d.ts
@@ -725,6 +770,13 @@ declare class ScoreKeeper extends Behavior {
725
770
  scoreToWin: number;
726
771
  private hasWon;
727
772
  private hasLost;
773
+ /**
774
+ * Whether the run is over — readable, not only heard. A stealth yard's HUD
775
+ * and a melee arena's both read `score.won` for their banner and got
776
+ * `undefined`, silently, because only the signals were public.
777
+ */
778
+ get won(): boolean;
779
+ get lost(): boolean;
728
780
  override onReady(): void;
729
781
  /** Add `n` to the score (negative subtracts), emit `scoreChanged`, check win. */
730
782
  addScore(n: number): void;
@@ -769,6 +821,7 @@ declare class Spawner extends Behavior {
769
821
  at: number[];
770
822
  autoStart: boolean;
771
823
  total: number;
824
+ into: string;
772
825
  private readonly source;
773
826
  private template;
774
827
  private readonly live;
@@ -839,6 +892,10 @@ declare class WaveSpawner extends Behavior {
839
892
  private parsed;
840
893
  private readonly live;
841
894
  private waveIndex;
895
+ /** The wave now running or being cleared, 1-based — what a HUD prints as `wave 2 / 5`. `0` before the first. */
896
+ get wave(): number;
897
+ /** How many waves were declared. */
898
+ get waveCount(): number;
842
899
  private phase;
843
900
  private timer;
844
901
  private spawnedThisWave;
@@ -926,6 +983,33 @@ declare class ZombieAI extends Behavior {
926
983
  private pickGoal;
927
984
  }
928
985
  //#endregion
986
+ //#region src/gameplay/checkpoint.d.ts
987
+ /**
988
+ * A checkpoint: touch it and a `Respawn` puts you back HERE from now on.
989
+ *
990
+ * Sits on an `Area2D`/`Area3D` (listens to `triggerEnter`). `respawn` names the
991
+ * `Respawn` node whose `to` becomes this checkpoint's world position, less
992
+ * `dropBelow` so the character lands ON the flag rather than inside it.
993
+ * `group` gates who can light it; `once` (default) lights it a single time.
994
+ * Emits `activated(other)`.
995
+ *
996
+ * Built after a platformer built from the tarball wrote exactly this in nine
997
+ * lines — the ninth being the one that found `Respawn.to` was read once at
998
+ * ready. A checkpoint is the second most common thing a platformer has, and it
999
+ * should be one node with two props, not a behaviour every game rewrites.
1000
+ */
1001
+ declare class Checkpoint extends Behavior {
1002
+ static readonly props: PropSchema;
1003
+ static readonly signals: readonly string[];
1004
+ respawn: string;
1005
+ group: string;
1006
+ once: boolean;
1007
+ dropBelow: number;
1008
+ /** True after the first activation. */
1009
+ lit: boolean;
1010
+ override onReady(): void;
1011
+ }
1012
+ //#endregion
929
1013
  //#region src/gameplay/day-night.d.ts
930
1014
  /**
931
1015
  * A live day/night cycle — drives the scene's `environment` (sun elevation +
@@ -1250,12 +1334,13 @@ declare function hitStop(engine: Engine, seconds?: number): void;
1250
1334
  */
1251
1335
  declare class PathFollow extends Behavior {
1252
1336
  static readonly signals: readonly string[];
1253
- static readonly props: Record<string, {
1254
- default: number | boolean;
1255
- }>;
1337
+ static readonly props: PropSchema;
1256
1338
  /** Units per second (px in 2D, meters in 3D). */
1257
1339
  speed: number;
1258
1340
  loop: boolean;
1341
+ moveParent: boolean;
1342
+ facePath: string;
1343
+ turnSpeed: number;
1259
1344
  private waypoints;
1260
1345
  private index;
1261
1346
  /** True while there is somewhere left to go. */
@@ -1264,6 +1349,10 @@ declare class PathFollow extends Behavior {
1264
1349
  /** Stop in place (keeps the node where it is). */
1265
1350
  stop(): void;
1266
1351
  override update(dt: number): void;
1352
+ /** `facePath` turns toward where this frame went — nothing when it did not move. */
1353
+ private face;
1354
+ /** The node this walks: the parent under `moveParent`, else this one — both must be spatial. */
1355
+ private mover;
1267
1356
  }
1268
1357
  //#endregion
1269
1358
  //#region src/gameplay/prefab-shelf.d.ts
@@ -1363,6 +1452,51 @@ declare function worldPosition(node: Node): number[];
1363
1452
  */
1364
1453
  declare function localFromWorld(node: Node, world: readonly number[]): number[];
1365
1454
  //#endregion
1455
+ //#region src/gameplay/turret.d.ts
1456
+ /**
1457
+ * The oldest verb in games: pick the nearest live member of `targetGroup`
1458
+ * within `range` and hit it every `cooldown` — a defense tower, a ship's gun,
1459
+ * a sentry, a trooper who fights what he meets.
1460
+ *
1461
+ * Two games in a row wrote these forty lines before it existed (an RTS's
1462
+ * trooper, a tower defense's tower): the engine had `Chase` for going TO a
1463
+ * target and `Projectile` for the bullet, and nothing that decides to shoot.
1464
+ *
1465
+ * - **Hitscan** by default: the target's `Health` (on it or a child, as
1466
+ * `DamageOnContact` finds it) takes `damage(n)`.
1467
+ * - **Or a clone**: name a `shelf` (a `PrefabShelf`) and a `prefab` on it; each
1468
+ * shot takes one, puts it at the turret plus `muzzle`, points its
1469
+ * `Projectile` at the target and adds it under `into` (the turret's parent
1470
+ * when empty). The clone does the hurting then — give it `DamageOnContact`.
1471
+ * - `facePath` turns a barrel toward the target at `turnSpeed`.
1472
+ *
1473
+ * Signals: `acquired(target)` when something comes in range, `fired(target)`
1474
+ * per shot, `lost()` when nothing live is in range any more.
1475
+ */
1476
+ declare class Turret extends Behavior {
1477
+ static readonly props: PropSchema;
1478
+ static readonly signals: readonly string[];
1479
+ targetGroup: string;
1480
+ range: number;
1481
+ damage: number;
1482
+ cooldown: number;
1483
+ shelf: string;
1484
+ prefab: string;
1485
+ muzzle: number[];
1486
+ into: string;
1487
+ facePath: string;
1488
+ turnSpeed: number;
1489
+ /** What it is shooting at right now, or null. */
1490
+ target: Node | null;
1491
+ /** Shots fired so far. */
1492
+ shots: number;
1493
+ private wait;
1494
+ override onReady(): void;
1495
+ override update(dt: number): void;
1496
+ /** One clone from the shelf, at the muzzle, aimed at the target, into the container. */
1497
+ private launch;
1498
+ }
1499
+ //#endregion
1366
1500
  //#region src/gameplay/index.d.ts
1367
1501
  /** Every built-in gameplay behavior, keyed by its registration name. */
1368
1502
  declare const GAMEPLAY_BEHAVIORS: Readonly<Record<string, BehaviorCtor>>;
@@ -1375,4 +1509,4 @@ declare function registerGameplayBehaviors(opts?: {
1375
1509
  replace?: boolean;
1376
1510
  }): void;
1377
1511
  //#endregion
1378
- export { CameraShake, Chase, Collector, Cooldown, Currency, DamageOnContact, DayNight, type DayPhase, FaceTarget, FloatAway, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, type GameFlowState, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, PrefabShelf, Projectile, SavePoint, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, localFromWorld, phaseOf, registerGameplayBehaviors, restartScene, screenFlash, worldPosition };
1512
+ export { CameraShake, Chase, Checkpoint, Collector, Cooldown, Currency, DamageOnContact, DayNight, type DayPhase, FaceTarget, FloatAway, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, type GameFlowState, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, PrefabShelf, Projectile, SavePoint, ScoreKeeper, Spawner, Turret, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, localFromWorld, phaseOf, registerGameplayBehaviors, restartScene, screenFlash, worldPosition };
package/dist/gameplay.js CHANGED
@@ -1,2 +1,2 @@
1
- import { A as DayNight, C as goToScene, E as FollowCamera, F as Collector, I as Chase, M as DamageOnContact, N as Health, O as FloatAway, P as Currency, Q as worldPosition, S as GameFlow, Z as localFromWorld, _ as CameraShake, a as Wander, b as screenFlash, c as SavePoint, d as Pickup, f as Patrol, g as Lifetime, h as MoveTo, i as WaveSpawner, j as phaseOf, k as FaceTarget, l as Projectile, m as Oscillate, n as registerGameplayBehaviors, o as Spawner, p as PathFollow, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as PrefabShelf, v as Cooldown, w as restartScene, x as Interactable, y as hitStop } from "./gameplay-BlQe-M07.js";
2
- export { CameraShake, Chase, Collector, Cooldown, Currency, DamageOnContact, DayNight, FaceTarget, FloatAway, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, PrefabShelf, Projectile, SavePoint, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, localFromWorld, phaseOf, registerGameplayBehaviors, restartScene, screenFlash, worldPosition };
1
+ import { $ as localFromWorld, A as FaceTarget, C as GameFlow, D as FollowCamera, F as Currency, I as Collector, L as Checkpoint, M as phaseOf, N as DamageOnContact, P as Health, R as Chase, S as Interactable, T as restartScene, _ as Lifetime, a as Wander, b as hitStop, c as ScoreKeeper, d as PrefabShelf, et as worldPosition, f as Pickup, g as MoveTo, h as Oscillate, i as WaveSpawner, j as DayNight, k as FloatAway, l as SavePoint, m as PathFollow, n as registerGameplayBehaviors, o as Turret, p as Patrol, r as ZombieAI, s as Spawner, t as GAMEPLAY_BEHAVIORS, u as Projectile, v as CameraShake, w as goToScene, x as screenFlash, y as Cooldown } from "./gameplay-Bxe1sMVT.js";
2
+ export { CameraShake, Chase, Checkpoint, Collector, Cooldown, Currency, DamageOnContact, DayNight, FaceTarget, FloatAway, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, PrefabShelf, Projectile, SavePoint, ScoreKeeper, Spawner, Turret, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, localFromWorld, phaseOf, registerGameplayBehaviors, restartScene, screenFlash, worldPosition };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { a as ParticleSimConfig, c as showBootFailure, i as ParticleSim, n as AudioPlayer, o as ParticleView, s as isWebGLAvailable, t as AudioElementLike } from "./audio-player-ioZ7VKC6.js";
2
- import { $ as T_PREFIX, A as SettingsValues, At as BusName, B as captureBehaviors, C as EngineStats, Ct as sfxDuration, Dt as MusicTrack, E as DeviceHints, Et as MusicManager, F as BehaviorState, Ft as LogLevel, G as ORDER_GROUP_BASE, H as savesWithoutUid, I as RestoreReport, It as LogManager, J as effectiveOrder, K as OrderGroup, L as SaveSlot, Lt as Signal, M as readDeviceHints, Mt as NodeLifecycle, N as suggestQuality, Nt as SceneTree, O as QualityTier, Ot as PlayMusicOptions, P as Scene, Pt as LogEntry, Q as Localization, R as SaveSlots, Rt as SignalListener, S as Scheduler, St as SynthOptions, T as RendererStats, Tt as MusicBackend, U as SaveStore, V as restoreBehaviors, W as createSaveStore, X as BASE_LOCALE, Y as resolveOrderGroups, Z as LocaleTables, _ as mergeStaticSignals, _t as spatialPan, a as clearBehaviors, at as EffectLog, b as Engine, bt as SfxParams, c as registeredBehaviors, ct as isAudioContextAvailable, d as PropSchema, dt as Listener, et as suggestLocale, f as clearRegistry, ft as ROLLOFF_MODELS, g as getNodeType, gt as spatialGain, h as getNodeSignals, ht as Vec3, i as behaviorSignals, it as EffectKind, j as qualityEnvironment, jt as Node, k as Settings, kt as AudioBuses, l as NodeCtor, lt as Voice, m as getNodeSchema, mt as SpatialParams, n as BehaviorCtor, nt as InputMap, o as getBehavior, ot as SfxEngine, p as createNode, pt as RolloffModel, q as OrderGroupTable, r as behaviorSchema, rt as EffectEvent, s as registerBehavior, st as SfxPlayOptions, t as Behavior, tt as translationKey, u as PropDef, ut as VoicePreset, v as registerNode, vt as SFX_PRESETS, w as GameStats, wt as synthSfx, x as EngineOptions, xt as SfxWave, y as registeredTypes, yt as SFX_PRESET_NAMES, z as behaviorsWithoutSave } from "./behavior-Bf9P9oB2.js";
1
+ import { a as ParticleSimConfig, c as showBootFailure, i as ParticleSim, n as AudioPlayer, o as ParticleView, s as isWebGLAvailable, t as AudioElementLike } from "./audio-player-BOrk4eQx.js";
2
+ import { $ as T_PREFIX, A as SettingsValues, At as BusName, B as captureBehaviors, C as EngineStats, Ct as sfxDuration, Dt as MusicTrack, E as DeviceHints, Et as MusicManager, F as BehaviorState, Ft as LogLevel, G as ORDER_GROUP_BASE, H as savesWithoutUid, I as RestoreReport, It as LogManager, J as effectiveOrder, K as OrderGroup, L as SaveSlot, Lt as Signal, M as readDeviceHints, Mt as NodeLifecycle, N as suggestQuality, Nt as SceneTree, O as QualityTier, Ot as PlayMusicOptions, P as Scene, Pt as LogEntry, Q as Localization, R as SaveSlots, Rt as SignalListener, S as Scheduler, St as SynthOptions, T as RendererStats, Tt as MusicBackend, U as SaveStore, V as restoreBehaviors, W as createSaveStore, X as BASE_LOCALE, Y as resolveOrderGroups, Z as LocaleTables, _ as mergeStaticSignals, _t as spatialPan, a as clearBehaviors, at as EffectLog, b as Engine, bt as SfxParams, c as registeredBehaviors, ct as isAudioContextAvailable, d as PropSchema, dt as Listener, et as suggestLocale, f as clearRegistry, ft as ROLLOFF_MODELS, g as getNodeType, gt as spatialGain, h as getNodeSignals, ht as Vec3, i as behaviorSignals, it as EffectKind, j as qualityEnvironment, jt as Node, k as Settings, kt as AudioBuses, l as NodeCtor, lt as Voice, m as getNodeSchema, mt as SpatialParams, n as BehaviorCtor, nt as InputMap, o as getBehavior, ot as SfxEngine, p as createNode, pt as RolloffModel, q as OrderGroupTable, r as behaviorSchema, rt as EffectEvent, s as registerBehavior, st as SfxPlayOptions, t as Behavior, tt as translationKey, u as PropDef, ut as VoicePreset, v as registerNode, vt as SFX_PRESETS, w as GameStats, wt as synthSfx, x as EngineOptions, xt as SfxWave, y as registeredTypes, yt as SFX_PRESET_NAMES, z as behaviorsWithoutSave } from "./behavior-J1cmVah0.js";
3
3
  import { a as SceneJson, c as JsonValue, d as jsonKind, i as SCENE_FORMAT, l as jsonClone, n as ConnectionJson, o as JsonKind, r as NodeJson, s as JsonObject, t as Rng, u as jsonEquals } from "./rng-Bb-IutXB.js";
4
- import { n as loadScene, t as LoadSceneOptions } from "./loader-CBfQzB6S.js";
4
+ import { n as loadScene, t as LoadSceneOptions } from "./loader-BAeWMYfF.js";
5
5
  import { i as resolveFrames, n as AnimationEntry, r as resolveAnimation, t as AnimationDef } from "./sprite-animation-CMr6f1K2.js";
6
6
  import { i as gridFromRows, n as PathGrid, r as findPath, t as FindPathOptions } from "./pathfinding-_fGrCFmH.js";
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-CBEChqXq.js";
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-wZK5Ok5c.js";
8
8
 
9
9
  //#region src/core/audio/crossfade.d.ts
10
10
  /**
@@ -879,6 +879,8 @@ declare class Timer extends Node {
879
879
  autostart: boolean;
880
880
  running: boolean;
881
881
  private remaining;
882
+ /** Seconds until the next `timeout` — a countdown a HUD can draw. `0` when stopped. */
883
+ get timeLeft(): number;
882
884
  start(time?: number): void;
883
885
  stop(): void;
884
886
  /**
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { n as jsonEquals, r as jsonKind, t as jsonClone } from "./json-CfTjpvW8.js";
2
- import { $ as parseNodePath, A as SceneTree, B as HudLayer, D as computeViewport, E as resolveConstants, F as resolveOrderGroups, G as UiText, I as InputMap, J as Node, N as ORDER_GROUP_BASE, O as resolveViewport, P as effectiveOrder, Q as resolveRefInJson, T as isConstRef, U as UiBanner, W as UiBar, X as describeRefProblem, Z as nodeRefWarnings, _ as Scene, a as restoreBehaviors, at as getBehavior, ct as Signal, et as Behavior, h as loadScene, i as captureBehaviors, it as clearBehaviors, j as createSaveStore, k as newUid, n as SaveSlots, nt as behaviorSchema, o as savesWithoutUid, ot as registerBehavior, r as behaviorsWithoutSave, rt as behaviorSignals, s as duplicateNode, st as registeredBehaviors, v as serializeNode, w as CONST_REF_KEY, y as SCENE_FORMAT } from "./save-slots-CEuJPUle.js";
3
- import { A as T_PREFIX, B as SFX_PRESET_NAMES, D as EffectLog, E as LogManager, F as isAudioContextAvailable, G as crossfadeGains, H as synthSfx, I as ROLLOFF_MODELS, K as fadeGain, L as spatialGain, N as translationKey, O as BASE_LOCALE, P as SfxEngine, R as spatialPan, S as qualityEnvironment, T as suggestQuality, U as MusicManager, V as sfxDuration, W as WebAudioMusicBackend, _ as AudioPlayer, a as UiMuteToggle, b as Settings, c as UiRenderScaleSelect, d as UiToggle, f as UiVolumeSlider, g as UiDialogue, h as UiButton, i as UiLanguageSelect, j as suggestLocale, k as Localization, l as UiSelect, m as Respawn, n as UiFrameCapSelect, o as UiPanel, p as Timer, q as AudioBuses, r as UiImage, s as UiQualitySelect, t as registerCoreNodes, u as UiSlider, v as Engine, w as readDeviceHints, z as SFX_PRESETS } from "./register-5IYfooAN.js";
2
+ import { $ as parseNodePath, A as SceneTree, B as HudLayer, D as computeViewport, E as resolveConstants, F as resolveOrderGroups, G as UiText, I as InputMap, J as Node, N as ORDER_GROUP_BASE, O as resolveViewport, P as effectiveOrder, Q as resolveRefInJson, T as isConstRef, U as UiBanner, W as UiBar, X as describeRefProblem, Z as nodeRefWarnings, _ as Scene, a as restoreBehaviors, at as getBehavior, ct as Signal, et as Behavior, h as loadScene, i as captureBehaviors, it as clearBehaviors, j as createSaveStore, k as newUid, n as SaveSlots, nt as behaviorSchema, o as savesWithoutUid, ot as registerBehavior, r as behaviorsWithoutSave, rt as behaviorSignals, s as duplicateNode, st as registeredBehaviors, v as serializeNode, w as CONST_REF_KEY, y as SCENE_FORMAT } from "./save-slots-CEUGeLuB.js";
3
+ import { A as T_PREFIX, B as SFX_PRESET_NAMES, D as EffectLog, E as LogManager, F as isAudioContextAvailable, G as crossfadeGains, H as synthSfx, I as ROLLOFF_MODELS, K as fadeGain, L as spatialGain, N as translationKey, O as BASE_LOCALE, P as SfxEngine, R as spatialPan, S as qualityEnvironment, T as suggestQuality, U as MusicManager, V as sfxDuration, W as WebAudioMusicBackend, _ as AudioPlayer, a as UiMuteToggle, b as Settings, c as UiRenderScaleSelect, d as UiToggle, f as UiVolumeSlider, g as UiDialogue, h as UiButton, i as UiLanguageSelect, j as suggestLocale, k as Localization, l as UiSelect, m as Respawn, n as UiFrameCapSelect, o as UiPanel, p as Timer, q as AudioBuses, r as UiImage, s as UiQualitySelect, t as registerCoreNodes, u as UiSlider, v as Engine, w as readDeviceHints, z as SFX_PRESETS } from "./register-CjociOtt.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { t as Rng } from "./rng-CDOMybym.js";
6
- import { c as getNodeSchema, f as mergeStaticSignals, i as joystickVector, l as getNodeSignals, m as registeredTypes, n as attachTouchControls, o as clearRegistry, p as registerNode, s as createNode, t as TouchControls, u as getNodeType } from "./touch-BnCyPA0G.js";
7
- import { n as startRecording, r as auditScene, t as replay } from "./replay-datiAE-b.js";
6
+ import { n as startRecording, r as auditScene, t as replay } from "./replay-B6BbgGM_.js";
7
+ import { c as getNodeSchema, f as mergeStaticSignals, i as joystickVector, l as getNodeSignals, m as registeredTypes, n as attachTouchControls, o as clearRegistry, p as registerNode, s as createNode, t as TouchControls, u as getNodeType } from "./touch-CioTZB-y.js";
8
8
  import { t as showBootFailure } from "./boot-failure-CKYrEcGF.js";
9
9
  import { a as logText, i as logReport, o as parseDrive, r as resolveRendering, t as isWebGLAvailable } from "./webgl-unavailable-C8aDbGmR.js";
10
10
  import { t as createNoise2D } from "./noise-D3nPpmFg.js";
11
- 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-CqR2o3SA.js";
12
- import { a as findPath, i as preloadUrls, n as assetUrls, o as gridFromRows, r as preloadSceneAssets, t as VERSION } from "./src-DRSsRleD.js";
11
+ 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-Bz3DHD_4.js";
12
+ import { a as findPath, i as preloadUrls, n as assetUrls, o as gridFromRows, r as preloadSceneAssets, t as VERSION } from "./src-CqtvYtSN.js";
13
13
  export { AudioBuses, AudioPlayer, BASE_LOCALE, Behavior, CONST_REF_KEY, EffectLog, Engine, HudLayer, IncantoError, InputMap, Localization, LogManager, MusicManager, Node, ORDER_GROUP_BASE, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, ParticleSim, ROLLOFF_MODELS, Respawn, Rng, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, SaveSlots, Scene, SceneTree, Settings, SfxEngine, Signal, T_PREFIX, Timer, TouchControls, UiBanner, UiBar, UiButton, UiDialogue, UiFrameCapSelect, UiImage, UiLanguageSelect, UiMuteToggle, UiPanel, UiQualitySelect, UiRenderScaleSelect, UiSelect, UiSlider, UiText, UiToggle, UiVolumeSlider, VERSION, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, behaviorSchema, behaviorSignals, behaviorsWithoutSave, captureBehaviors, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, describeRefProblem, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, isWebGLAvailable, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, logReport, logText, mergeStaticSignals, newUid, nodeRefWarnings, parseDrive, parseNodePath, preloadSceneAssets, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveAnimation, resolveConstants, resolveFrames, resolveOrderGroups, resolveRefInJson, resolveRendering, resolveViewport, restoreBehaviors, savesWithoutUid, serializeNode, sfxDuration, showBootFailure, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
@@ -1,4 +1,4 @@
1
- import { P as Scene, b as Engine } from "./behavior-Bf9P9oB2.js";
1
+ import { P as Scene, b as Engine } from "./behavior-J1cmVah0.js";
2
2
  import { a as SceneJson } from "./rng-Bb-IutXB.js";
3
3
 
4
4
  //#region src/core/scene/loader.d.ts
package/dist/net.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { d as PropSchema, jt as Node } from "./behavior-Bf9P9oB2.js";
1
+ import { d as PropSchema, jt as Node } from "./behavior-J1cmVah0.js";
2
2
  import { t as DiagnosticSink } from "./diagnostics-Cu85N3tL.js";
3
- import { a as NetworkManagerOptions, c as LocalGameServer, d as ServerClass, f as createLocalGameServer, g as Unsubscribe, h as NetworkTransport, i as NetworkManager, l as LocalGameServerOptions, m as LoopbackTransport, n as SplitScreenPlayer, o as applySyncPatch, p as LoopbackHub, r as createSplitScreen, s as CollectionOptions, t as SplitScreenOptions, u as LocalGameServerTransport } from "./split-screen-DkqLng6q.js";
3
+ import { a as NetworkManagerOptions, c as LocalGameServer, d as ServerClass, f as createLocalGameServer, g as Unsubscribe, h as NetworkTransport, i as NetworkManager, l as LocalGameServerOptions, m as LoopbackTransport, n as SplitScreenPlayer, o as applySyncPatch, p as LoopbackHub, r as createSplitScreen, s as CollectionOptions, t as SplitScreenOptions, u as LocalGameServerTransport } from "./split-screen-D6sx8z3Z.js";
4
4
 
5
5
  //#region src/net/agent8.d.ts
6
6
  /**
package/dist/net.js CHANGED
@@ -1,3 +1,3 @@
1
- import { n as createAgent8Server } from "./agent8-BqZ0_O1u.js";
2
- import { a as NetworkManager, d as createLocalGameServer, f as LoopbackHub, l as LocalGameServer, n as registerNodesNet, o as applySyncPatch, p as LoopbackTransport, r as NetworkSpawner, t as createSplitScreen, u as LocalGameServerTransport } from "./split-screen-Bm5DTElG.js";
1
+ import { n as createAgent8Server } from "./agent8-CHTT4unP.js";
2
+ import { a as NetworkManager, d as createLocalGameServer, f as LoopbackHub, l as LocalGameServer, n as registerNodesNet, o as applySyncPatch, p as LoopbackTransport, r as NetworkSpawner, t as createSplitScreen, u as LocalGameServerTransport } from "./split-screen-BxZ5rBe7.js";
3
3
  export { LocalGameServer, LocalGameServerTransport, LoopbackHub, LoopbackTransport, NetworkManager, NetworkSpawner, applySyncPatch, createAgent8Server, createLocalGameServer, createSplitScreen, registerNodesNet };
@@ -1,7 +1,7 @@
1
1
  import { o as __exportAll } from "./json-CfTjpvW8.js";
2
- import { Y as diagnose } from "./save-slots-CEuJPUle.js";
2
+ import { Y as diagnose } from "./save-slots-CEUGeLuB.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-C8HuRkOf.js";
4
+ import { _ as RigidBody2D, b as validateCollider2D, g as PhysicsBody2D, h as CharacterBody2D, m as Area2D, p as Joint2D, v as StaticBody2D, y as Node2D } from "./register-DRQyZKGU.js";
5
5
  import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
6
6
  import { n as jointContacts, t as withoutRapierInitNoise } from "./quiet-rapier-C6fW4zcW.js";
7
7
  //#region src/2d/physics/physics-2d.ts
@@ -122,6 +122,7 @@ var Physics2D = class {
122
122
  const b = this.byColliderHandle.get(h2);
123
123
  if (!a || !b) return;
124
124
  const sig = started ? "triggerEnter" : "triggerExit";
125
+ if (started === (this.overlaps.get(a)?.has(b) ?? false)) return;
125
126
  this.trackOverlap(a, b, started);
126
127
  this.trackOverlap(b, a, started);
127
128
  try {
@@ -185,7 +186,8 @@ var Physics2D = class {
185
186
  }
186
187
  return true;
187
188
  };
188
- this.kcc.computeColliderMovement(e.collider, desired, this.R.QueryFilterFlags.EXCLUDE_SENSORS, void 0, predicate);
189
+ const filter = node.collideWithCharacters ? predicate : (c) => !(this.byColliderHandle.get(c.handle) instanceof CharacterBody2D) && predicate(c);
190
+ this.kcc.computeColliderMovement(e.collider, desired, this.R.QueryFilterFlags.EXCLUDE_SENSORS, void 0, filter);
189
191
  const corrected = this.kcc.computedMovement();
190
192
  const t = e.body.translation();
191
193
  e.body.setNextKinematicTranslation({
@@ -350,6 +352,45 @@ var Physics2D = class {
350
352
  if (others.size === 0) this.overlaps.delete(self);
351
353
  }
352
354
  }
355
+ /** Every overlap `node` is in ends now — the exits Rapier does not send for a disabled collider. */
356
+ releaseOverlaps(node) {
357
+ const set = this.overlaps.get(node);
358
+ if (!set || set.size === 0) return;
359
+ for (const other of [...set]) {
360
+ this.trackOverlap(node, other, false);
361
+ this.trackOverlap(other, node, false);
362
+ try {
363
+ node.emit("triggerExit", other);
364
+ other.emit("triggerExit", node);
365
+ } catch (error) {
366
+ diagnose(this.engine, "error", `incanto: error in a 'triggerExit' handler (${node.name} ↔ ${other.name}):`, error);
367
+ }
368
+ }
369
+ }
370
+ /** Every body the armed sensor is inside of RIGHT NOW enters it — the starts Rapier does not send for a re-enabled collider. */
371
+ armOverlaps(node, collider) {
372
+ const found = [];
373
+ const t = collider.translation();
374
+ this.world.intersectionsWithShape({
375
+ x: t.x,
376
+ y: t.y
377
+ }, collider.rotation(), collider.shape, (hit) => {
378
+ const other = this.byColliderHandle.get(hit.handle);
379
+ if (other && other !== node && hit.isEnabled()) found.push(other);
380
+ return true;
381
+ }, void 0, void 0, collider);
382
+ for (const other of found) {
383
+ if (this.overlaps.get(node)?.has(other)) continue;
384
+ this.trackOverlap(node, other, true);
385
+ this.trackOverlap(other, node, true);
386
+ try {
387
+ node.emit("triggerEnter", other);
388
+ other.emit("triggerEnter", node);
389
+ } catch (error) {
390
+ diagnose(this.engine, "error", `incanto: error in a 'triggerEnter' handler (${node.name} ↔ ${other.name}):`, error);
391
+ }
392
+ }
393
+ }
353
394
  pruneOverlaps() {
354
395
  for (const [self, others] of this.overlaps) {
355
396
  if (self.tree === null) {
@@ -570,6 +611,7 @@ var Physics2D = class {
570
611
  desc.setTranslation(x, y).setRotation(node.rotation * DEG);
571
612
  if (node instanceof RigidBody2D) {
572
613
  desc.setGravityScale(node.gravityScale);
614
+ desc.setLinearDamping(node.linearDamping).setAngularDamping(node.angularDamping);
573
615
  desc.setLinvel((node.linearVelocity[0] ?? 0) / SCALE, (node.linearVelocity[1] ?? 0) / SCALE);
574
616
  desc.setAngvel(node.fixedRotation ? 0 : node.angularVelocity);
575
617
  if (node.fixedRotation) desc.lockRotations();
@@ -579,7 +621,8 @@ var Physics2D = class {
579
621
  colDesc.setActiveEvents(R.ActiveEvents.COLLISION_EVENTS);
580
622
  colDesc.setActiveCollisionTypes(R.ActiveCollisionTypes.ALL);
581
623
  if (node instanceof Area2D) colDesc.setSensor(true);
582
- if (node instanceof RigidBody2D) colDesc.setMass(node.mass).setFriction(node.friction).setRestitution(node.restitution);
624
+ if (node instanceof RigidBody2D) colDesc.setMass(node.mass);
625
+ if (node instanceof RigidBody2D || node instanceof StaticBody2D) applySurface(R, colDesc, node);
583
626
  const collider = this.world.createCollider(colDesc, body);
584
627
  e = {
585
628
  body,
@@ -591,9 +634,11 @@ var Physics2D = class {
591
634
  lastVY: 0,
592
635
  lastAV: dyn ? node.angularVelocity : 0,
593
636
  lastMass: dyn ? node.mass : 1,
594
- lastFriction: dyn ? node.friction : .5,
595
- lastRestitution: dyn ? node.restitution : 0,
596
- lastGravityScale: dyn ? node.gravityScale : 1
637
+ lastFriction: node.friction ?? .5,
638
+ lastRestitution: node.restitution ?? 0,
639
+ lastGravityScale: dyn ? node.gravityScale : 1,
640
+ lastLinDamp: dyn ? node.linearDamping : 0,
641
+ lastAngDamp: dyn ? node.angularDamping : 0
597
642
  };
598
643
  this.entries.set(node, e);
599
644
  this.byColliderHandle.set(collider.handle, node);
@@ -607,9 +652,11 @@ var Physics2D = class {
607
652
  return e;
608
653
  }
609
654
  if (node._enabledChanged) {
655
+ if (!node.enabled) this.releaseOverlaps(node);
610
656
  e.collider.setEnabled(node.enabled);
611
657
  e.body.setEnabled(node.enabled);
612
658
  node._enabledChanged = false;
659
+ if (node.enabled) this.armOverlaps(node, e.collider);
613
660
  }
614
661
  const [px, py] = worldPosition2D(node);
615
662
  if (px !== e.lastX || py !== e.lastY) {
@@ -647,12 +694,23 @@ var Physics2D = class {
647
694
  e.collider.setMass(node.mass);
648
695
  e.lastMass = node.mass;
649
696
  }
650
- if (node.friction !== e.lastFriction) {
651
- e.collider.setFriction(node.friction);
697
+ if (node.friction !== e.lastFriction || node.restitution !== e.lastRestitution) {
698
+ applySurface(this.R, e.collider, node);
652
699
  e.lastFriction = node.friction;
700
+ e.lastRestitution = node.restitution;
701
+ }
702
+ if (node.linearDamping !== e.lastLinDamp) {
703
+ e.body.setLinearDamping(node.linearDamping);
704
+ e.lastLinDamp = node.linearDamping;
705
+ }
706
+ if (node.angularDamping !== e.lastAngDamp) {
707
+ e.body.setAngularDamping(node.angularDamping);
708
+ e.lastAngDamp = node.angularDamping;
653
709
  }
654
- if (node.restitution !== e.lastRestitution) {
655
- e.collider.setRestitution(node.restitution);
710
+ } else if (node instanceof StaticBody2D) {
711
+ if (node.friction !== e.lastFriction || node.restitution !== e.lastRestitution) {
712
+ applySurface(this.R, e.collider, node);
713
+ e.lastFriction = node.friction;
656
714
  e.lastRestitution = node.restitution;
657
715
  }
658
716
  }
@@ -727,5 +785,30 @@ function buildColliderDesc(R, collider, nodeName) {
727
785
  }
728
786
  throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': "shape" must be 'rect', 'circle', or 'capsule', got ${JSON.stringify(shape)}.`);
729
787
  }
788
+ /**
789
+ * A static body's authored surface, and the rule that makes it WIN the pair.
790
+ *
791
+ * Rapier combines two colliders' coefficients by the higher-priority rule of
792
+ * the two (Max > Multiply > Min > Average) and defaults to Average, which is
793
+ * why a 0.75 ball on a fixed-0 floor kept 37% of its speed. The surface an
794
+ * author writes on a floor is an intent about whatever touches it: a
795
+ * trampoline launches a dead ball (Max), an ice patch slides a grippy crate
796
+ * (Min), glue stops a slick one (Max). At the defaults nothing is set, so a
797
+ * scene that says nothing pairs exactly as it always did.
798
+ *
799
+ * The same rule on the MOVING body, because the character skill had said for
800
+ * a year "set `friction: 0` on the body" and nothing honoured it: a
801
+ * RigidBody's 0 met the floor's unwritten 0.5 under Average and became 0.25,
802
+ * and every 3D character walked at 3.7 where the skill promised 4.0. An
803
+ * authored value wins the pair whichever side wrote it; when both sides
804
+ * speak, Rapier's priority order (Max > Multiply > Min > Average) decides.
805
+ */
806
+ function applySurface(R, target, node) {
807
+ const Rule = R.CoefficientCombineRule;
808
+ target.setFriction(node.friction);
809
+ target.setFrictionCombineRule(node.friction === .5 ? Rule.Average : node.friction < .5 ? Rule.Min : Rule.Max);
810
+ target.setRestitution(node.restitution);
811
+ target.setRestitutionCombineRule(node.restitution > 0 ? Rule.Max : Rule.Average);
812
+ }
730
813
  //#endregion
731
814
  export { enablePhysics2D as n, physics_2d_exports as r, Physics2D as t };