incanto 0.68.0 → 0.69.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 (75) hide show
  1. package/assets/catalog.json +9 -5
  2. package/bin/incanto-assets.mjs +14 -1
  3. package/bin/incanto-check.mjs +53 -3
  4. package/bin/incanto-new.mjs +29 -1
  5. package/bin/incanto-serve.mjs +160 -0
  6. package/bin/incanto-verify.mjs +40 -0
  7. package/bin/incanto.mjs +1 -0
  8. package/dist/2d.d.ts +7 -5
  9. package/dist/2d.js +4 -4
  10. package/dist/3d.d.ts +4 -4
  11. package/dist/3d.js +7 -7
  12. package/dist/{audio-player-DOrq7sP-.d.ts → audio-player-C0j2k-yB.d.ts} +32 -13
  13. package/dist/{behavior-DoFPYrgo.d.ts → behavior-rm8vVzu_.d.ts} +67 -0
  14. package/dist/{create-game-DbWtVTxD.js → create-game-CvJ1wSRP.js} +13 -9
  15. package/dist/{create-game-IZIydDwI.js → create-game-CwxEDlCm.js} +13 -9
  16. package/dist/debug.d.ts +1 -1
  17. package/dist/{duplicate-IWIqk0HJ.js → duplicate-Dj4L-5AA.js} +1 -1
  18. package/dist/editor.js +1 -1
  19. package/dist/{environment-presets-CybQXNqS.js → environment-presets-BO-7XoFV.js} +4 -4
  20. package/dist/{frame-report-DCnHFmto.d.ts → frame-report-DNxDAb1w.d.ts} +8 -0
  21. package/dist/{frame-report-BSMny7oe.js → frame-report-Dlq13Gyj.js} +1 -0
  22. package/dist/{gameplay-DM1eu_cV.js → gameplay-D1KixWK8.js} +157 -151
  23. package/dist/gameplay.d.ts +1 -1
  24. package/dist/gameplay.js +1 -1
  25. package/dist/index.d.ts +4 -4
  26. package/dist/index.js +6 -6
  27. package/dist/{loader-BC4PNtJX.js → loader-DZE7B07H.js} +22 -6
  28. package/dist/{loader-CcB533FR.d.ts → loader-DadV2QCT.d.ts} +1 -1
  29. package/dist/net.d.ts +2 -2
  30. package/dist/net.js +1 -1
  31. package/dist/{physics-2d-B7Y6dPZO.js → physics-2d-CF5yyjbj.js} +2 -2
  32. package/dist/{physics-3d-bG3n70Ky.js → physics-3d-DbYaEcZy.js} +3 -3
  33. package/dist/react.d.ts +1 -1
  34. package/dist/react.js +1 -1
  35. package/dist/{register-ibCjm-wH.js → register-DbVArsns.js} +19 -10
  36. package/dist/{register-3ta-2Xig.js → register-mmfugpaQ.js} +107 -15
  37. package/dist/{replay-CYvhVHHN.js → replay-BEHLDcQV.js} +2 -2
  38. package/dist/{replay-Dvn8aeBd.d.ts → replay-BQfkX4x7.d.ts} +1 -1
  39. package/dist/{sheet-grid-BT6N_Bjs.js → sheet-grid-Cea343VO.js} +6 -2
  40. package/dist/{split-screen-CYwDkbLF.js → split-screen-CauMwu6y.js} +2 -2
  41. package/dist/{split-screen-DhrSzZIB.d.ts → split-screen-Cg76MCV5.d.ts} +2 -2
  42. package/dist/{src-D7RIqXYF.js → src-CYEu-_S9.js} +1 -1
  43. package/dist/{teardown-D2NEmxPB.js → teardown-CMqFVxO9.js} +5 -4
  44. package/dist/{test-8hoHeRmo.js → test-D6ISmuFE.js} +60 -17
  45. package/dist/test.d.ts +15 -4
  46. package/dist/test.js +2 -2
  47. package/dist/vite.d.ts +70 -2
  48. package/dist/vite.js +176 -14
  49. package/dist/{webgl-unavailable-N9nQqesw.js → webgl-unavailable-Z5_g8x9H.js} +12 -1
  50. package/editor/assets/{agent8-BrrHOjMJ.js → agent8-BDvaZMQJ.js} +1 -1
  51. package/editor/assets/{debug-BBhKuBNV.js → debug-CMULinBW.js} +1 -1
  52. package/editor/assets/{index-eVd0BToA.js → index-CZIRr5EL.js} +91 -91
  53. package/editor/index.html +1 -1
  54. package/package.json +4 -3
  55. package/skills/incanto-assets.md +16 -2
  56. package/skills/incanto-audio.md +19 -3
  57. package/skills/incanto-building-2d-games.md +29 -0
  58. package/skills/incanto-building-3d-games.md +60 -2
  59. package/skills/incanto-gameplay-behaviors.md +18 -1
  60. package/skills/incanto-save-slots.md +27 -0
  61. package/skills/incanto-verifying-your-game.md +96 -2
  62. package/skills/incanto-your-first-game.md +2 -1
  63. package/templates-app/beacon-isle-3d/package.json +1 -1
  64. package/templates-app/beacon-isle-3d/src/main.ts +13 -3
  65. package/templates-app/platformer-2d/index.html +0 -23
  66. package/templates-app/platformer-2d/package.json +1 -1
  67. package/templates-app/platformer-2d/src/behaviors.ts +26 -16
  68. package/templates-app/platformer-2d/src/game.scene.json +17 -27
  69. package/templates-app/platformer-2d/src/main.ts +24 -12
  70. package/templates-app/star-survivor/package.json +1 -1
  71. package/templates-app/star-survivor/src/main.ts +17 -6
  72. package/templates-app/tps-3d/package.json +1 -1
  73. package/templates-app/tps-3d/src/main.ts +26 -15
  74. package/templates-app/village-quest-3d/package.json +1 -1
  75. package/templates-app/village-quest-3d/src/main.ts +13 -3
@@ -1,11 +1,11 @@
1
- import { O as Node, k as diagnose, w as effectiveOrder } from "./loader-BC4PNtJX.js";
2
- import { L as translateOn, t as registerCoreNodes } from "./register-3ta-2Xig.js";
1
+ import { O as Node, k as diagnose, w as effectiveOrder } from "./loader-DZE7B07H.js";
2
+ import { L as translateOn, t as registerCoreNodes } from "./register-mmfugpaQ.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-CF70EArN.js";
6
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-CY-mrr1L.js";
7
- import { F as localFromWorld, I as worldPosition } from "./gameplay-DM1eu_cV.js";
8
- import { t as checkSheetGrid } from "./sheet-grid-BT6N_Bjs.js";
7
+ import { F as localFromWorld, I as worldPosition } from "./gameplay-D1KixWK8.js";
8
+ import { t as checkSheetGrid } from "./sheet-grid-Cea343VO.js";
9
9
  import { AdditiveBlending, BufferAttribute, BufferGeometry, CanvasTexture, Color, DynamicDrawUsage, Group, InstancedMesh, MathUtils, Matrix4, Mesh, MeshBasicMaterial, NearestFilter, NormalBlending, Object3D, PlaneGeometry, SRGBColorSpace } from "three";
10
10
  //#region src/2d/physics/collider-validate.ts
11
11
  /**
@@ -1443,9 +1443,11 @@ var TileMap2D = class extends Node2D {
1443
1443
  _solid = [];
1444
1444
  /** Rebuild flags — raised when the map-shaping props are REPLACED. */
1445
1445
  geometryDirty = true;
1446
- /** Reported once per atlas, so a bad grid says so on the first build only. */
1446
+ /** `texture|highestTile` already reported, so a bad grid says so once. */
1447
1447
  gridChecked = "";
1448
1448
  collidersDirty = true;
1449
+ /** The `scale` the colliders were last built for — see `rebuildColliders`. */
1450
+ builtScale = [1, 1];
1449
1451
  /** Replace the whole array to change the map (mutations are not watched). */
1450
1452
  get cells() {
1451
1453
  return this._cells;
@@ -1487,19 +1489,23 @@ var TileMap2D = class extends Node2D {
1487
1489
  this.rebuildColliders();
1488
1490
  }
1489
1491
  update(_dt) {
1492
+ const [sx = 1, sy = 1] = this.scale;
1493
+ if (sx !== this.builtScale[0] || sy !== this.builtScale[1]) this.collidersDirty = true;
1490
1494
  if (this.collidersDirty) this.rebuildColliders();
1491
1495
  }
1492
1496
  rebuildColliders() {
1493
1497
  this.collidersDirty = false;
1494
1498
  for (const child of [...this.children]) if (child.name.startsWith("__tileSolid")) child.free();
1499
+ this.builtScale = [this.scale[0] ?? 1, this.scale[1] ?? 1];
1495
1500
  if (this._solid.length === 0) return;
1496
1501
  const ts = this.tileSize;
1502
+ const [sx, sy] = this.builtScale;
1497
1503
  mergeSolidRects(this.grid(), new Set(this._solid)).forEach((r, i) => {
1498
1504
  const body = new StaticBody2D(`__tileSolid${i}`);
1499
- body.position = [(r.x + r.w / 2) * ts, (r.y + r.h / 2) * ts];
1505
+ body.position = [(r.x + r.w / 2) * ts * sx, (r.y + r.h / 2) * ts * sy];
1500
1506
  body.collider = {
1501
1507
  shape: "rect",
1502
- size: [r.w * ts, r.h * ts]
1508
+ size: [r.w * ts * sx, r.h * ts * sy]
1503
1509
  };
1504
1510
  this.addChild(body);
1505
1511
  });
@@ -1543,9 +1549,12 @@ var TileMap2D = class extends Node2D {
1543
1549
  mat.needsUpdate = true;
1544
1550
  this.geometryDirty = true;
1545
1551
  }
1546
- if (this.gridChecked !== this.texture) {
1547
- this.gridChecked = this.texture;
1548
- const problem = checkSheetGrid(this.texture, this.tileSize, this.tileSize, image.width, image.height);
1552
+ let highestAsked = -1;
1553
+ for (const row of this.grid()) for (const tile of row) if (tile > highestAsked) highestAsked = tile;
1554
+ const checkKey = `${this.texture}|${highestAsked}`;
1555
+ if (this.gridChecked !== checkKey) {
1556
+ this.gridChecked = checkKey;
1557
+ const problem = checkSheetGrid(this.texture, this.tileSize, this.tileSize, image.width, image.height, highestAsked >= 0 ? highestAsked : void 0, "tile");
1549
1558
  if (problem) diagnose(this.tree?.engine ?? null, "error", problem);
1550
1559
  }
1551
1560
  const columns = this.columns > 0 ? this.columns : Math.max(1, Math.floor(image.width / this.tileSize));
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { C as ORDER_GROUP_BASE, E as InputMap, O as Node, S as createSaveStore, T as resolveOrderGroups, V as Signal, b as restoreBehaviors, k as diagnose, y as captureBehaviors } from "./loader-BC4PNtJX.js";
2
+ import { C as ORDER_GROUP_BASE, E as InputMap, O as Node, S as createSaveStore, T as resolveOrderGroups, V as Signal, b as restoreBehaviors, k as diagnose, y as captureBehaviors } from "./loader-DZE7B07H.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 { l as registerNode } from "./registry-CF70EArN.js";
@@ -2031,6 +2031,25 @@ var FrameStatsRing = class {
2031
2031
  const span = this.spanMs();
2032
2032
  return span > 0 ? span / (this.count - 1) : 0;
2033
2033
  }
2034
+ /**
2035
+ * How long ago the newest sample arrived, in ms. `null` with no samples.
2036
+ *
2037
+ * `fps()` is a rate over a WINDOW, so it keeps answering after the frames
2038
+ * stop: a hidden tab, a descheduled loop, a page the browser froze. Measured
2039
+ * on an engine that ticked 40 times and then stopped being ticked:
2040
+ *
2041
+ * while ticking : fps 59.9
2042
+ * after 0 more ticks : fps 59.9 <- the last sample is 668 ms old
2043
+ *
2044
+ * Both numbers are true about the window they describe. What was missing was
2045
+ * any way to ask WHEN that window was, so "60 fps" read the same whether it
2046
+ * was measured 8 ms ago or four seconds ago.
2047
+ */
2048
+ ageMs(nowMs) {
2049
+ if (this.count === 0) return null;
2050
+ const newest = this.times[(this.head + this.count - 1) % this.capacity];
2051
+ return Math.max(0, nowMs - newest);
2052
+ }
2034
2053
  /** Forget all samples (engine.stop() — a paused game has no frame rate). */
2035
2054
  clear() {
2036
2055
  this.head = 0;
@@ -2433,6 +2452,49 @@ var Engine = class {
2433
2452
  * learns about Rapier or three. From a behavior, prefer `this.physics`.
2434
2453
  */
2435
2454
  physics = null;
2455
+ /**
2456
+ * How to build a physics world for a scene that needs one.
2457
+ *
2458
+ * `createGame2D`/`createGame3D` install this; core never learns what Rapier
2459
+ * is. It exists because `physics: 'auto'` can only look at the scene the game
2460
+ * BOOTED with, and the most universal act of finishing a game is putting a
2461
+ * title screen in front of it:
2462
+ *
2463
+ * ```
2464
+ * after boot on the title : engine.physics = null
2465
+ * after goToScene into game: engine.physics = null
2466
+ * player y after 2 s : 0 -> 0
2467
+ * ```
2468
+ *
2469
+ * The game rendered perfectly and was inert — nothing fell, nothing collided,
2470
+ * nothing could be collected or hurt. One scene swap turned a working game
2471
+ * into a diorama, and nothing was logged.
2472
+ */
2473
+ physicsProvider = null;
2474
+ ensuring = null;
2475
+ /**
2476
+ * Give the current scene a physics world if it needs one and has none.
2477
+ *
2478
+ * Idempotent and safe to call on every swap: the provider decides whether the
2479
+ * incoming tree actually wants physics, and an explicit `physics: false` at
2480
+ * boot installs no provider at all, so opting out stays opted out.
2481
+ *
2482
+ * Asynchronous because the Rapier WASM has to load. The first frames after a
2483
+ * swap therefore run unsimulated — which every AI behaviour already tolerates
2484
+ * (`moveBody` falls back to a direct write when `_physics` is null) and which
2485
+ * is a hitch, where the alternative was a game that never simulated at all.
2486
+ */
2487
+ ensurePhysics() {
2488
+ if (this.physics || !this.physicsProvider || !this._scene) return Promise.resolve();
2489
+ if (this.ensuring) return this.ensuring;
2490
+ const root = this._scene.root;
2491
+ this.ensuring = this.physicsProvider(root).catch((e) => {
2492
+ this.log.error(`[incanto] could not enable physics for this scene: ${String(e?.message ?? e)}`);
2493
+ }).finally(() => {
2494
+ this.ensuring = null;
2495
+ });
2496
+ return this.ensuring;
2497
+ }
2436
2498
  /** Seeded randomness for game logic (deterministic when `seed` is set). */
2437
2499
  rng;
2438
2500
  /** The engine log channel (debug overlay + headless harness tail this). */
@@ -2756,6 +2818,7 @@ var Engine = class {
2756
2818
  return {
2757
2819
  fps: this.frameStats.fps(),
2758
2820
  frameMs: this.frameStats.frameMs(),
2821
+ lastFrameAgeMs: this.frameStats.ageMs(performance.now()),
2759
2822
  nodes: this._scene?.tree.root ? countNodes(this._scene.tree.root) : 0,
2760
2823
  running: this.disposeScheduler !== null,
2761
2824
  errors: this.errorCount,
@@ -2808,6 +2871,7 @@ var Engine = class {
2808
2871
  this._time += dt;
2809
2872
  this._unscaledTime += this.fixedStep;
2810
2873
  const startedFixed = performance.now();
2874
+ this.input.beginFrame();
2811
2875
  scene.tree.fixedUpdate(dt);
2812
2876
  this.fixedUpdated.emit(dt);
2813
2877
  const startedUpdate = performance.now();
@@ -2835,6 +2899,7 @@ var Engine = class {
2835
2899
  this.lastMs = nowMs;
2836
2900
  const scene = this._scene;
2837
2901
  if (!scene) return;
2902
+ this.input.beginFrame();
2838
2903
  this.accumulator += dt;
2839
2904
  let steps = 0;
2840
2905
  const startedFixed = performance.now();
@@ -3007,7 +3072,8 @@ var AudioPlayer = class extends Node {
3007
3072
  name,
3008
3073
  from: this.getPath(),
3009
3074
  bus: this.bus === "music" ? "music" : "sfx",
3010
- at: engine.time
3075
+ at: engine.time,
3076
+ ...this.loop ? { loop: true } : {}
3011
3077
  });
3012
3078
  }
3013
3079
  /** Final gain = engine buses × this volume (1 when not in a tree). */
@@ -3040,31 +3106,57 @@ var AudioPlayer = class extends Node {
3040
3106
  };
3041
3107
  }
3042
3108
  /**
3043
- * Start the sound. `at` schedules it on the AUDIO clock instead of now.
3109
+ * Start the sound, now.
3110
+ *
3111
+ * **Takes no arguments, on purpose.** This is the method scenes wire signals
3112
+ * to — `incanto-audio.md` teaches
3113
+ * `{"signal": "collected", "from": "Player/Collector", "to": "Coin",
3114
+ * "handler": "play"}` — and a signal hands its handler whatever it carries.
3115
+ * `collected` leads with a NUMBER. When this briefly took an optional
3116
+ * scheduling time, that wiring became `play(10)`, which scheduled the pickup
3117
+ * sound at ABSOLUTE audio-clock second 10: inaudible for the first ten
3118
+ * seconds of the game and fine thereafter. The same optional parameter also
3119
+ * made `Function.length` 1, so the engine's own arity checker cried wolf on
3120
+ * the shipped `platformer-2d` template's own harness.
3121
+ *
3122
+ * Scheduling has its own name: {@link playAt}.
3123
+ */
3124
+ /**
3125
+ * Start the sound at `when` on the AUDIO clock (`engine.sfx.now` + a lead).
3044
3126
  *
3045
3127
  * A frame is a 16.67 ms grid at 60 Hz and 33.33 ms at 30, so a sound fired
3046
3128
  * from `update()` cannot land closer than one frame to where a chart wants
3047
- * it — measured 0 of 64 notes scheduled on the documented path. Queue it a
3048
- * lead ahead of `engine.sfx.now` and the same notes land at |mean| 0.000000
3049
- * ms:
3129
+ * it — measured 0 of 64 notes given a scheduled start. Queue them a lead
3130
+ * ahead and the same notes land at |mean| 0.000000 ms:
3050
3131
  *
3051
3132
  * ```ts
3052
- * const lead = 0.08; // schedule this far ahead
3053
- * for (const note of dueSoon(engine.sfx.now + lead)) {
3054
- * hit.play(engine.sfx.now + (note.atSec - songSeconds));
3133
+ * const LEAD = 0.08;
3134
+ * for (const note of dueSoon(engine.sfx.now + LEAD)) {
3135
+ * hit.playAt(startedAt + note.atSec);
3055
3136
  * }
3056
3137
  * ```
3057
3138
  *
3058
3139
  * PRESETS only — a `src` clip goes through an `<audio>` element, which has no
3059
- * scheduling clock, and passing `at` for one is ignored rather than silently
3060
- * approximated. Everything else is unchanged: the bus gain applies, the sound
3061
- * lands in `engine.audio.recent()`, and `muted` still silences it, none of
3062
- * which a hand-rolled `AudioContext` scheduler keeps.
3140
+ * scheduling clock, so `playAt` on one plays immediately rather than being
3141
+ * silently approximated. A time already past plays now (Web Audio's own
3142
+ * rule), so a late scheduler is late, not silent. The bus gain, the
3143
+ * `engine.audio.recent()` entry and `muted` all still apply.
3144
+ *
3145
+ * Separate from {@link play} because `play` is what scenes wire signals to,
3146
+ * and a signal hands its handler whatever it carries.
3063
3147
  */
3064
- play(at) {
3148
+ playAt(when) {
3149
+ if (this.preset !== "custom") {
3150
+ this.note("preset", this.preset);
3151
+ this.playPreset(Number.isFinite(when) ? when : void 0);
3152
+ return;
3153
+ }
3154
+ this.play();
3155
+ }
3156
+ play() {
3065
3157
  if (this.preset !== "custom") {
3066
3158
  this.note("preset", this.preset);
3067
- this.playPreset(at);
3159
+ this.playPreset();
3068
3160
  return;
3069
3161
  }
3070
3162
  if (!this.src) return;
@@ -1,5 +1,5 @@
1
- import { D as handlerShadowWarnings, j as nodeRefWarnings } from "./loader-BC4PNtJX.js";
2
- import { R as translationKey, k as HudWidgetBase } from "./register-3ta-2Xig.js";
1
+ import { D as handlerShadowWarnings, j as nodeRefWarnings } from "./loader-DZE7B07H.js";
2
+ import { R as translationKey, k as HudWidgetBase } from "./register-mmfugpaQ.js";
3
3
  import { o as getNodeType } from "./registry-CF70EArN.js";
4
4
  //#region src/core/audit.ts
5
5
  const ANIMATED_TYPES = new Set([
@@ -1,4 +1,4 @@
1
- import { b as Engine } from "./behavior-DoFPYrgo.js";
1
+ import { b as Engine } from "./behavior-rm8vVzu_.js";
2
2
  import { s as JsonObject } from "./schema-B6ugCV1Q.js";
3
3
 
4
4
  //#region src/core/audit.d.ts
@@ -35,7 +35,7 @@ function divisors(total) {
35
35
  * known — an animation range that runs off the end is the other half of the
36
36
  * same mistake.
37
37
  */
38
- function checkSheetGrid(key, frameWidth, frameHeight, imageWidth, imageHeight, highestFrame) {
38
+ function checkSheetGrid(key, frameWidth, frameHeight, imageWidth, imageHeight, highestFrame, unit = "frame") {
39
39
  if (!imageWidth || !imageHeight || !frameWidth || !frameHeight) return null;
40
40
  if (imageWidth % frameWidth !== 0 || imageHeight % frameHeight !== 0) {
41
41
  const parts = [];
@@ -51,7 +51,11 @@ function checkSheetGrid(key, frameWidth, frameHeight, imageWidth, imageHeight, h
51
51
  }
52
52
  if (highestFrame !== void 0) {
53
53
  const frames = imageWidth / frameWidth * (imageHeight / frameHeight);
54
- if (highestFrame >= frames) return `[incanto] spritesheet '${key}' has ${frames} frames (0–${frames - 1}), and an animation asks for frame ${highestFrame}. Those frames draw whatever is at the wrong end of the sheet.`;
54
+ if (highestFrame >= frames) {
55
+ const sheet = unit === "tile" ? "tile atlas" : "spritesheet";
56
+ const asks = unit === "tile" ? "a cell asks for tile" : "an animation asks for frame";
57
+ return `[incanto] ${sheet} '${key}' has ${frames} ${unit}s (0–${frames - 1}), and ${asks} ${highestFrame}. Those ${unit}s draw whatever is at the wrong end of the sheet.`;
58
+ }
55
59
  }
56
60
  return null;
57
61
  }
@@ -1,5 +1,5 @@
1
- import { O as Node, V as Signal, k as diagnose, n as loadScene, t as buildNodeJson } from "./loader-BC4PNtJX.js";
2
- import { t as registerCoreNodes, v as Engine } from "./register-3ta-2Xig.js";
1
+ import { O as Node, V as Signal, k as diagnose, n as loadScene, t as buildNodeJson } from "./loader-DZE7B07H.js";
2
+ import { t as registerCoreNodes, v as Engine } from "./register-mmfugpaQ.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { n as jsonEquals, t as jsonClone } from "./json-CwwhxQgb.js";
5
5
  import { l as registerNode } from "./registry-CF70EArN.js";
@@ -1,6 +1,6 @@
1
- import { Lt as Signal, b as Engine, jt as Node } from "./behavior-DoFPYrgo.js";
1
+ import { Lt as Signal, b as Engine, jt as Node } from "./behavior-rm8vVzu_.js";
2
2
  import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-B6ugCV1Q.js";
3
- import { t as LoadSceneOptions } from "./loader-CcB533FR.js";
3
+ import { t as LoadSceneOptions } from "./loader-DadV2QCT.js";
4
4
 
5
5
  //#region src/net/types.d.ts
6
6
  type Unsubscribe = () => void;
@@ -214,6 +214,6 @@ function newUid() {
214
214
  //#endregion
215
215
  //#region src/index.ts
216
216
  /** Engine version. Kept in sync with package.json by the release pipeline. */
217
- const VERSION = "0.68.0";
217
+ const VERSION = "0.69.0";
218
218
  //#endregion
219
219
  export { preloadUrls as a, preloadSceneAssets as i, newUid as n, findPath as o, assetUrls as r, gridFromRows as s, VERSION as t };
@@ -1,6 +1,6 @@
1
- import { _ as AudioPlayer } from "./register-3ta-2Xig.js";
2
- import { i as logReport, o as parseDrive } from "./webgl-unavailable-N9nQqesw.js";
3
- import { a as frameSignature, i as frameImage, o as frameStats } from "./frame-report-BSMny7oe.js";
1
+ import { _ as AudioPlayer } from "./register-mmfugpaQ.js";
2
+ import { i as logReport, o as parseDrive } from "./webgl-unavailable-Z5_g8x9H.js";
3
+ import { a as frameSignature, i as frameImage, o as frameStats } from "./frame-report-Dlq13Gyj.js";
4
4
  //#region src/core/audio-errors.ts
5
5
  /** Every AudioPlayer under `root` whose source could not be played. */
6
6
  function audioErrors(root) {
@@ -35,7 +35,7 @@ function claimCanvasGestures(canvas) {
35
35
  * empty list outside a dev server.
36
36
  */
37
37
  function wireDevChannel(opts) {
38
- const hot = import.meta.hot;
38
+ const hot = import.meta.hot ?? globalThis.__incantoInspect;
39
39
  const cleanups = [];
40
40
  if (!hot) return cleanups;
41
41
  const onFrameRequest = (data) => {
@@ -59,6 +59,7 @@ function wireDevChannel(opts) {
59
59
  report: {
60
60
  ...frameStats(shot.pixels, shot.width, shot.height, { grid: request?.grid }),
61
61
  signature: frameSignature(shot.pixels, shot.width, shot.height),
62
+ hidden: typeof document !== "undefined" && document.hidden,
62
63
  ...droveFrames === null ? {} : { droveFrames },
63
64
  ...request?.image ? { image: frameImage(shot.pixels, shot.width, shot.height, (w, h) => Object.assign(document.createElement("canvas"), {
64
65
  width: w,
@@ -1,13 +1,13 @@
1
- import { f as resolveViewport, n as loadScene, z as registerBehavior } from "./loader-BC4PNtJX.js";
2
- import { v as Engine } from "./register-3ta-2Xig.js";
1
+ import { f as resolveViewport, n as loadScene, z as registerBehavior } from "./loader-DZE7B07H.js";
2
+ import { v as Engine } from "./register-mmfugpaQ.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { n as jsonEquals, t as jsonClone } from "./json-CwwhxQgb.js";
5
5
  import { i as getNodeSchema, s as mergeStaticProps } from "./registry-CF70EArN.js";
6
- import { n as startRecording } from "./replay-CYvhVHHN.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-DM1eu_cV.js";
8
- import { t as registerNodes2D } from "./register-ibCjm-wH.js";
9
- import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-CybQXNqS.js";
10
- import { a as NetworkManager, c as readSyncKey, i as sanitizeName, n as registerNodesNet, r as NetworkSpawner, s as findOwnerNode, t as createSplitScreen } from "./split-screen-CYwDkbLF.js";
6
+ import { n as startRecording } from "./replay-BEHLDcQV.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-D1KixWK8.js";
8
+ import { t as registerNodes2D } from "./register-DbVArsns.js";
9
+ import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-BO-7XoFV.js";
10
+ import { a as NetworkManager, c as readSyncKey, i as sanitizeName, n as registerNodesNet, r as NetworkSpawner, s as findOwnerNode, t as createSplitScreen } from "./split-screen-CauMwu6y.js";
11
11
  import { Box3, Euler, Matrix4, PerspectiveCamera, Quaternion, Vector3 } from "three";
12
12
  //#region src/test/framing.ts
13
13
  /**
@@ -252,7 +252,7 @@ function describeFraming2D(scene, placed, opts) {
252
252
  round(cy),
253
253
  0
254
254
  ],
255
- view: [vw, vh],
255
+ view: [round(vw / zoom), round(vh / zoom)],
256
256
  zoom
257
257
  };
258
258
  }
@@ -630,6 +630,23 @@ var Driver = class {
630
630
  headingLeft = 0;
631
631
  visits = /* @__PURE__ */ new Map();
632
632
  buttons;
633
+ /**
634
+ * Buttons the bot pressed and will never press again, because pressing them
635
+ * STOPPED THE CLOCK.
636
+ *
637
+ * It drove every declared button action, and `incanto-your-first-game.md`
638
+ * tells you to declare `pause`. Measured on one losable scene: 0/10 losses
639
+ * with `pause` declared, 10/10 without it, same seed, same everything else.
640
+ * A frozen game cannot hurt you, so the run also reported the game as having
641
+ * no way to lose.
642
+ *
643
+ * This is OBSERVED, not guessed from the name — a game is free to call it
644
+ * `menu`, `escape`, `halt` or nothing at all, and the bot notices the same
645
+ * way a player would: the world stopped moving.
646
+ */
647
+ frozeTheGame = /* @__PURE__ */ new Set();
648
+ /** What was pressed most recently, so a freeze can be attributed to it. */
649
+ lastPressed = null;
633
650
  moveAction;
634
651
  held = /* @__PURE__ */ new Map();
635
652
  /**
@@ -680,7 +697,20 @@ var Driver = class {
680
697
  cell(p) {
681
698
  return `${Math.round(p[0] / 4)},${Math.round(p[2] / 4)}`;
682
699
  }
700
+ /** Actions this run avoided after they stopped the clock. */
701
+ paused() {
702
+ return [...this.frozeTheGame];
703
+ }
683
704
  step(dt, at) {
705
+ if (this.engine.timeScale === 0) {
706
+ if (this.lastPressed) this.frozeTheGame.add(this.lastPressed);
707
+ for (const button of this.buttons) {
708
+ this.engine.input.releaseAction(button);
709
+ this.held.set(button, -1);
710
+ }
711
+ this.engine.timeScale = 1;
712
+ this.lastPressed = null;
713
+ }
684
714
  this.visits.set(this.cell(at), (this.visits.get(this.cell(at)) ?? 0) + 1);
685
715
  this.headingLeft -= dt;
686
716
  if (this.headingLeft <= 0) {
@@ -689,6 +719,7 @@ var Driver = class {
689
719
  }
690
720
  if (this.moveAction) this.engine.input.setActionVector(this.moveAction, Math.cos(this.heading), Math.sin(this.heading));
691
721
  for (const button of this.buttons) {
722
+ if (this.frozeTheGame.has(button)) continue;
692
723
  const left = (this.held.get(button) ?? 0) - dt;
693
724
  if (left > 0) {
694
725
  this.held.set(button, left);
@@ -702,6 +733,7 @@ var Driver = class {
702
733
  const chance = button === "jump" ? .06 : .02;
703
734
  if (this.rand() < chance) {
704
735
  this.engine.input.pressAction(button);
736
+ this.lastPressed = button;
705
737
  this.held.set(button, .08 + this.rand() * .12);
706
738
  }
707
739
  }
@@ -820,9 +852,10 @@ async function runOnce(json, seed, opts) {
820
852
  });
821
853
  let at = start;
822
854
  let wentAsFarAs = 0;
855
+ const travelled = (a, b) => scene.dimension === "2d" ? Math.hypot((a[0] ?? 0) - (b[0] ?? 0), (a[1] ?? 0) - (b[1] ?? 0)) : Math.hypot((a[0] ?? 0) - (b[0] ?? 0), (a[2] ?? 0) - (b[2] ?? 0));
823
856
  while (session.timeMs < deadline) {
824
857
  at = player ? worldOf(player) : start;
825
- wentAsFarAs = Math.max(wentAsFarAs, Math.hypot(at[0] - start[0], at[2] - start[2]));
858
+ wentAsFarAs = Math.max(wentAsFarAs, travelled(at, start));
826
859
  driver.step(stepMs / 1e3, at);
827
860
  session.step(stepMs);
828
861
  if (player) for (const [path, node] of targetNodes) {
@@ -878,7 +911,8 @@ async function runOnce(json, seed, opts) {
878
911
  signals,
879
912
  declaresEnd: oracle.declaresEnd(),
880
913
  hazards,
881
- hasPlayer: player !== null
914
+ hasPlayer: player !== null,
915
+ froze: driver.paused()
882
916
  };
883
917
  }
884
918
  /**
@@ -908,6 +942,7 @@ async function playtest(json, opts = {}) {
908
942
  let declaresWin = false;
909
943
  let hasPlayer = false;
910
944
  let hazards = 0;
945
+ const froze = /* @__PURE__ */ new Set();
911
946
  for (let i = 0; i < count; i++) {
912
947
  const one = await runOnce(json, base + i, opts);
913
948
  runs.push(one.run);
@@ -916,6 +951,7 @@ async function playtest(json, opts = {}) {
916
951
  declaresWin = declaresWin || one.declaresEnd;
917
952
  hasPlayer = hasPlayer || one.hasPlayer;
918
953
  hazards = one.hazards;
954
+ for (const action of one.froze) froze.add(action);
919
955
  }
920
956
  const probe = await createPlaySession(json, {
921
957
  seed: base,
@@ -946,6 +982,7 @@ async function playtest(json, opts = {}) {
946
982
  declaresWin,
947
983
  hasPlayer,
948
984
  hazards,
985
+ froze: [...froze],
949
986
  seconds: opts.seconds ?? 60,
950
987
  feedback: {
951
988
  declaredAudio: feedbackNodes.audio,
@@ -1016,8 +1053,14 @@ function playtestText(report) {
1016
1053
  if (silent.length > 12) lines.push(` …and ${silent.length - 12} more`);
1017
1054
  }
1018
1055
  const damage = runs.reduce((n, r) => n + r.damageTaken, 0);
1056
+ const lost = runs.filter((r) => r.outcome === "lost").length;
1057
+ if (report.froze.length > 0) {
1058
+ lines.push("");
1059
+ lines.push(` · the bot stopped pressing ${report.froze.map((a) => `\`${a}\``).join(", ")} — pressing it stopped the clock, and a run spent inside a pause menu measures nothing. Your game is fine; this is the bot getting out of its own way.`);
1060
+ }
1019
1061
  lines.push("");
1020
1062
  if (damage > 0) lines.push(` danger: ${damage} hits across ${total} runs (${(damage / total).toFixed(1)} per run)`);
1063
+ else if (hazards === 0 && lost > 0) lines.push(` danger: nothing DEALT DAMAGE — no DamageOnContact, no hazard group — and yet ${lost} of ${total} runs were lost. The loss comes from something else: a fall, a timer, a trigger, or running out of lives.`);
1021
1064
  else if (hazards === 0) lines.push(" danger: nothing in this scene can hurt the player — no DamageOnContact, no hazard group. A game with no way to lose is a walkabout.");
1022
1065
  else lines.push(` danger: ${hazards} hazard(s) are wired and none of them landed a hit in ${total} runs. They may be unreachable — or the bot may simply have outrun them, which a real player would not. Judge it with a scripted run.`);
1023
1066
  return lines.join("\n");
@@ -1812,8 +1855,8 @@ async function playMultiplayer(opts) {
1812
1855
  */
1813
1856
  async function joinLate(server, first, opts) {
1814
1857
  try {
1815
- const { Engine } = await import("./register-3ta-2Xig.js").then((n) => n.y);
1816
- const { loadScene } = await import("./loader-BC4PNtJX.js").then((n) => n.r);
1858
+ const { Engine } = await import("./register-mmfugpaQ.js").then((n) => n.y);
1859
+ const { loadScene } = await import("./loader-DZE7B07H.js").then((n) => n.r);
1817
1860
  const { jsonClone } = await import("./json-CwwhxQgb.js").then((n) => n.i);
1818
1861
  const engine = new Engine(opts.seed !== void 0 ? { seed: opts.seed + 999 } : {});
1819
1862
  engine.setScene(loadScene(jsonClone(opts.scene), opts.load));
@@ -2175,10 +2218,10 @@ async function runScript(json, opts) {
2175
2218
  if (opts.locale) engine.locale.locale = opts.locale;
2176
2219
  const physics = opts.physics ?? "auto";
2177
2220
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
2178
- const { enablePhysics2D } = await import("./physics-2d-B7Y6dPZO.js").then((n) => n.r);
2221
+ const { enablePhysics2D } = await import("./physics-2d-CF5yyjbj.js").then((n) => n.r);
2179
2222
  await enablePhysics2D(engine);
2180
2223
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
2181
- const { enablePhysics3D } = await import("./physics-3d-bG3n70Ky.js").then((n) => n.r);
2224
+ const { enablePhysics3D } = await import("./physics-3d-DbYaEcZy.js").then((n) => n.r);
2182
2225
  await enablePhysics3D(engine);
2183
2226
  }
2184
2227
  const failures = [];
@@ -2294,10 +2337,10 @@ async function createPlaySession(json, opts = {}) {
2294
2337
  engine.setScene(scene);
2295
2338
  const physics = opts.physics ?? "auto";
2296
2339
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
2297
- const { enablePhysics2D } = await import("./physics-2d-B7Y6dPZO.js").then((n) => n.r);
2340
+ const { enablePhysics2D } = await import("./physics-2d-CF5yyjbj.js").then((n) => n.r);
2298
2341
  await enablePhysics2D(engine);
2299
2342
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
2300
- const { enablePhysics3D } = await import("./physics-3d-bG3n70Ky.js").then((n) => n.r);
2343
+ const { enablePhysics3D } = await import("./physics-3d-DbYaEcZy.js").then((n) => n.r);
2301
2344
  await enablePhysics3D(engine);
2302
2345
  }
2303
2346
  const stepMs = 1e3 / (opts.fixedHz ?? 60);
@@ -2317,7 +2360,7 @@ async function createPlaySession(json, opts = {}) {
2317
2360
  timeMs += ticks * stepMs;
2318
2361
  },
2319
2362
  capture() {
2320
- return scene.toJSON();
2363
+ return (engine.scene ?? scene).toJSON();
2321
2364
  },
2322
2365
  describe() {
2323
2366
  return describeCapture(captureScene(engine.scene ?? scene));
package/dist/test.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { P as Scene, Pt as LogEntry, b as Engine, jt as Node, n as BehaviorCtor } from "./behavior-DoFPYrgo.js";
1
+ import { P as Scene, Pt as LogEntry, b as Engine, jt as Node, n as BehaviorCtor } from "./behavior-rm8vVzu_.js";
2
2
  import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-B6ugCV1Q.js";
3
- import { t as LoadSceneOptions } from "./loader-CcB533FR.js";
4
- import { l as auditScene, o as IncantoError, r as ReplayJson, u as instancedScenes } from "./replay-Dvn8aeBd.js";
5
- import { l as LocalGameServerOptions, n as SplitScreenPlayer } from "./split-screen-DhrSzZIB.js";
3
+ import { t as LoadSceneOptions } from "./loader-DadV2QCT.js";
4
+ import { l as auditScene, o as IncantoError, r as ReplayJson, u as instancedScenes } from "./replay-BQfkX4x7.js";
5
+ import { l as LocalGameServerOptions, n as SplitScreenPlayer } from "./split-screen-Cg76MCV5.js";
6
6
 
7
7
  //#region src/test/facing.d.ts
8
8
  interface FacingOptions {
@@ -361,6 +361,17 @@ interface PlaytestReport {
361
361
  /** How many nodes are wired to hurt the player — see the `danger:` line. */
362
362
  hazards: number;
363
363
  /**
364
+ * Actions the bot stopped pressing because they STOPPED THE CLOCK.
365
+ *
366
+ * It drove every declared button, and `incanto-your-first-game.md` tells you
367
+ * to declare `pause`. Measured on one losable scene: 0/10 losses with `pause`
368
+ * declared, 10/10 without, everything else identical — a frozen game cannot
369
+ * hurt you, so the same run also reported the game as having no way to lose.
370
+ * Named here rather than guessed from the word "pause": a game may call it
371
+ * `menu` or `escape`, and the bot notices the way a player would.
372
+ */
373
+ froze: string[];
374
+ /**
364
375
  * Did `findPlayer` find anyone to drive?
365
376
  *
366
377
  * `findPlayer`'s own comment says a scene it cannot drive "says so rather
package/dist/test.js CHANGED
@@ -1,3 +1,3 @@
1
- import { i as instancedScenes, r as auditScene } from "./replay-CYvhVHHN.js";
2
- import { C as describeFraming, S as playtestText, _ as facingReport, a as registerAllNodes, b as findPlayer, c as ladderText, d as multiplayText, f as playMultiplayer, g as agreementProblems, h as takeAgreementSnapshots, i as findFloatingProps, l as ladderVerdict, m as feelText, n as createPlaySession, o as runScript, p as feelReport, r as describeCapture, s as validateScene, t as captureScene, u as multiplayProblems, v as facingText, w as framingText, x as playtest, y as failingReplays } from "./test-8hoHeRmo.js";
1
+ import { i as instancedScenes, r as auditScene } from "./replay-BEHLDcQV.js";
2
+ import { C as describeFraming, S as playtestText, _ as facingReport, a as registerAllNodes, b as findPlayer, c as ladderText, d as multiplayText, f as playMultiplayer, g as agreementProblems, h as takeAgreementSnapshots, i as findFloatingProps, l as ladderVerdict, m as feelText, n as createPlaySession, o as runScript, p as feelReport, r as describeCapture, s as validateScene, t as captureScene, u as multiplayProblems, v as facingText, w as framingText, x as playtest, y as failingReplays } from "./test-D6ISmuFE.js";
3
3
  export { agreementProblems, auditScene, captureScene, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, framingText, instancedScenes, ladderText, ladderVerdict, multiplayProblems, multiplayText, playMultiplayer, playtest, playtestText, registerAllNodes, runScript, takeAgreementSnapshots, validateScene };