incanto 0.42.0 → 0.44.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 (41) hide show
  1. package/bin/incanto-feel.mjs +20 -1
  2. package/bin/incanto-frame.mjs +55 -11
  3. package/dist/2d.js +1 -1
  4. package/dist/3d.d.ts +2 -40
  5. package/dist/3d.js +6 -5
  6. package/dist/{create-game-DItIRyXH.js → create-game-D90pyPMx.js} +17 -81
  7. package/dist/{create-game-9KPppR0L.js → create-game-DFRbG0Bj.js} +3 -2
  8. package/dist/debug.d.ts +44 -3
  9. package/dist/debug.js +562 -4
  10. package/dist/{environment-presets-Vl5xBsXp.js → environment-presets-Ds5kXLoF.js} +1 -1
  11. package/dist/frame-report-Ct8XgsmV.d.ts +80 -0
  12. package/dist/frame-report-Lr3VO24R.js +197 -0
  13. package/dist/{gameplay-DRi9524r.js → gameplay-BQOeAid6.js} +37 -0
  14. package/dist/gameplay.d.ts +17 -0
  15. package/dist/gameplay.js +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/{physics-3d-C2G604O1.js → physics-3d-CLPFv99o.js} +2 -2
  18. package/dist/react.js +1 -1
  19. package/dist/{src-cU57Uwdw.js → src-B3n06SsL.js} +1 -1
  20. package/dist/{teardown-CCtAMDLB.js → teardown-yePMOE1K.js} +32 -1
  21. package/dist/{test-DuOD1DO8.js → test-BMgiiD5i.js} +117 -5
  22. package/dist/test.d.ts +35 -1
  23. package/dist/test.js +2 -2
  24. package/dist/vite.d.ts +15 -1
  25. package/dist/vite.js +49 -6
  26. package/editor/assets/{agent8-Csw0T7jh.js → agent8-MgX6kYWH.js} +1 -1
  27. package/editor/assets/debug-Czri9cgX.js +3 -0
  28. package/editor/assets/{index-B1rUkWxB.js → index-D9MX3UHF.js} +60 -60
  29. package/editor/index.html +1 -1
  30. package/package.json +1 -1
  31. package/skills/incanto-3d-character.md +19 -0
  32. package/skills/incanto-gameplay-behaviors.md +27 -0
  33. package/skills/incanto-node-reference.md +2 -0
  34. package/skills/incanto-verifying-your-game.md +60 -4
  35. package/templates-app/beacon-isle-3d/package.json +1 -1
  36. package/templates-app/beacon-isle-3d/src/game.scene.json +47 -0
  37. package/templates-app/tps-3d/package.json +1 -1
  38. package/templates-app/tps-3d/src/game.scene.json +53 -0
  39. package/templates-app/village-quest-3d/package.json +1 -1
  40. package/templates-app/village-quest-3d/src/grove.scene.json +43 -0
  41. package/editor/assets/debug-C3qeBD4X.js +0 -3
@@ -0,0 +1,80 @@
1
+ //#region src/3d/frame-report.d.ts
2
+ /**
3
+ * What the frame looks like, as something worth saying.
4
+ *
5
+ * The pixels are not the deliverable. A 1280x720 frame is 2.8 million numbers:
6
+ * unreadable by a person, unaffordable for a model (roughly a thousand tokens
7
+ * as an image, roughly a million as text). The deliverable is the JUDGEMENT —
8
+ * black screen, one flat colour, washed out — and the numbers behind it, small
9
+ * enough to print and compare.
10
+ *
11
+ * Same principle as `framing`'s "lit by: NOTHING": a report is a sentence
12
+ * before it is a number, because a number needs a reader who already knows what
13
+ * to compare it against.
14
+ *
15
+ * Pure arithmetic over an RGBA buffer — no three, no DOM, no GPU. Whoever has
16
+ * the pixels calls this; in practice that is the browser holding the canvas.
17
+ */
18
+ /** Per-cell mean colour, `[r, g, b]` 0..255. */
19
+ type GridCell = [number, number, number];
20
+ interface FrameStats {
21
+ width: number;
22
+ height: number;
23
+ /** Every pixel below the black threshold — the render produced nothing. */
24
+ black: boolean;
25
+ /** One colour everywhere: a clear-colour fill, or a camera inside geometry. */
26
+ uniform: boolean;
27
+ /** 0..1 mean relative luminance. */
28
+ meanLuma: number;
29
+ /** Coarse mean colour per cell, rows top to bottom. */
30
+ grid: GridCell[][];
31
+ }
32
+ interface FrameStatsOptions {
33
+ /** Cells across and down. Default 16x9 — 144 cells, printable. */
34
+ grid?: [number, number];
35
+ }
36
+ declare function frameStats(pixels: Uint8Array | Uint8ClampedArray, width: number, height: number, opts?: FrameStatsOptions): FrameStats;
37
+ /** The report a person or an agent reads. Verdict first, numbers after. */
38
+ declare function frameText(stats: FrameStats): string;
39
+ /**
40
+ * Comparison resolution. Fine enough that a one-pixel line moves a cell by
41
+ * several levels at 720p, small enough (6912 bytes) to hold and to send.
42
+ */
43
+ declare const SIGNATURE_GRID: readonly [number, number];
44
+ interface FrameSignature {
45
+ /** The frame this was made from — a different size is not comparable. */
46
+ width: number;
47
+ height: number;
48
+ cols: number;
49
+ rows: number;
50
+ /** base64 of `cols * rows * 3` bytes: cell means, rows top to bottom. */
51
+ data: string;
52
+ }
53
+ interface FrameDiff {
54
+ /** False when the two cannot be subtracted at all; see `reason`. */
55
+ comparable: boolean;
56
+ reason?: string;
57
+ /** 0..1 fraction of cells that moved past the threshold. */
58
+ changed: number;
59
+ /** Largest single-channel move, 0..255. */
60
+ maxDelta: number;
61
+ /** Mean single-channel move across the frame, 0..255. */
62
+ meanDelta: number;
63
+ /** Where the change is, in pixels of the compared frame. */
64
+ box: {
65
+ x: number;
66
+ y: number;
67
+ w: number;
68
+ h: number;
69
+ } | null;
70
+ /** A thin full-length band is worth naming: that is what a seam looks like. */
71
+ shape: "vertical band" | "horizontal band" | null;
72
+ }
73
+ declare function frameSignature(pixels: Uint8Array | Uint8ClampedArray, width: number, height: number, grid?: readonly [number, number]): FrameSignature;
74
+ declare function diffSignatures(before: FrameSignature, after: FrameSignature, opts?: {
75
+ threshold?: number;
76
+ }): FrameDiff;
77
+ /** The comparison a person or an agent reads. */
78
+ declare function diffText(diff: FrameDiff): string;
79
+ //#endregion
80
+ export { GridCell as a, diffText as c, frameText as d, FrameStatsOptions as i, frameSignature as l, FrameSignature as n, SIGNATURE_GRID as o, FrameStats as r, diffSignatures as s, FrameDiff as t, frameStats as u };
@@ -0,0 +1,197 @@
1
+ //#region src/3d/frame-report.ts
2
+ /** Rec. 709 relative luminance of an 8-bit triple, 0..1. */
3
+ function luma(r, g, b) {
4
+ return (.2126 * r + .7152 * g + .0722 * b) / 255;
5
+ }
6
+ /**
7
+ * A pixel this dark is indistinguishable from "nothing was drawn". Not zero:
8
+ * a real render of a night scene still lands a few levels above black, and
9
+ * calling that a failure would make the check useless where it matters most.
10
+ */
11
+ const BLACK_LEVEL = .012;
12
+ /** Cell-to-cell luminance range below this: the frame is one flat colour. */
13
+ const UNIFORM_EPSILON = .004;
14
+ /**
15
+ * Mean colour of every cell of a `cols` x `rows` box filter, as a flat
16
+ * `cols * rows * 3` byte array, rows top to bottom.
17
+ *
18
+ * The one place a frame is reduced. Both the printable report and the
19
+ * comparison signature are this at different resolutions, so a boundary that
20
+ * moves moves for both.
21
+ */
22
+ function cellMeans(pixels, width, height, cols, rows) {
23
+ const out = new Uint8Array(cols * rows * 3);
24
+ for (let row = 0; row < rows; row++) for (let col = 0; col < cols; col++) {
25
+ const x0 = Math.floor(col * width / cols);
26
+ const x1 = Math.max(x0 + 1, Math.floor((col + 1) * width / cols));
27
+ const y0 = Math.floor(row * height / rows);
28
+ const y1 = Math.max(y0 + 1, Math.floor((row + 1) * height / rows));
29
+ let r = 0;
30
+ let g = 0;
31
+ let b = 0;
32
+ let n = 0;
33
+ for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) {
34
+ const i = (y * width + x) * 4;
35
+ r += pixels[i] ?? 0;
36
+ g += pixels[i + 1] ?? 0;
37
+ b += pixels[i + 2] ?? 0;
38
+ n += 1;
39
+ }
40
+ const o = (row * cols + col) * 3;
41
+ out[o] = Math.round(r / n);
42
+ out[o + 1] = Math.round(g / n);
43
+ out[o + 2] = Math.round(b / n);
44
+ }
45
+ return out;
46
+ }
47
+ function frameStats(pixels, width, height, opts = {}) {
48
+ const [cols, rows] = opts.grid ?? [16, 9];
49
+ const means = cellMeans(pixels, width, height, cols, rows);
50
+ const grid = [];
51
+ let total = 0;
52
+ let maxLuma = 0;
53
+ let minLuma = 1;
54
+ for (let row = 0; row < rows; row++) {
55
+ const line = [];
56
+ for (let col = 0; col < cols; col++) {
57
+ const o = (row * cols + col) * 3;
58
+ const cell = [
59
+ means[o],
60
+ means[o + 1],
61
+ means[o + 2]
62
+ ];
63
+ const cellLuma = luma(cell[0], cell[1], cell[2]);
64
+ total += cellLuma;
65
+ maxLuma = Math.max(maxLuma, cellLuma);
66
+ minLuma = Math.min(minLuma, cellLuma);
67
+ line.push(cell);
68
+ }
69
+ grid.push(line);
70
+ }
71
+ const cells = cols * rows;
72
+ const meanLuma = total / cells;
73
+ return {
74
+ width,
75
+ height,
76
+ black: maxLuma <= BLACK_LEVEL,
77
+ uniform: maxLuma - minLuma < UNIFORM_EPSILON,
78
+ meanLuma: Math.round(meanLuma * 1e3) / 1e3,
79
+ grid
80
+ };
81
+ }
82
+ /** The report a person or an agent reads. Verdict first, numbers after. */
83
+ function frameText(stats) {
84
+ const lines = [];
85
+ if (stats.black) lines.push("BLACK SCREEN — nothing was drawn (no light, no camera, or nothing in view)");
86
+ else if (stats.uniform) lines.push("one flat colour — the camera may be inside geometry, or only the sky is drawn");
87
+ lines.push(`frame ${stats.width}×${stats.height} · luminance ${stats.meanLuma.toFixed(2)}`);
88
+ return lines.join("\n");
89
+ }
90
+ /**
91
+ * Comparison resolution. Fine enough that a one-pixel line moves a cell by
92
+ * several levels at 720p, small enough (6912 bytes) to hold and to send.
93
+ */
94
+ const SIGNATURE_GRID = [64, 36];
95
+ /**
96
+ * Below this, a cell has not changed. Compression, dithering and the last bit
97
+ * of a tonemap all wander by a level or two between two captures of the same
98
+ * still frame; calling that a change would bury the real one.
99
+ */
100
+ const DIFF_THRESHOLD = 3;
101
+ function frameSignature(pixels, width, height, grid = SIGNATURE_GRID) {
102
+ const [cols, rows] = grid;
103
+ return {
104
+ width,
105
+ height,
106
+ cols,
107
+ rows,
108
+ data: toBase64(cellMeans(pixels, width, height, cols, rows))
109
+ };
110
+ }
111
+ function diffSignatures(before, after, opts = {}) {
112
+ const none = {
113
+ changed: 0,
114
+ maxDelta: 0,
115
+ meanDelta: 0,
116
+ box: null,
117
+ shape: null
118
+ };
119
+ if (before.width !== after.width || before.height !== after.height) return {
120
+ comparable: false,
121
+ reason: `frame size changed: ${before.width}×${before.height} → ${after.width}×${after.height}`,
122
+ ...none
123
+ };
124
+ if (before.cols !== after.cols || before.rows !== after.rows) return {
125
+ comparable: false,
126
+ reason: "signature grid changed",
127
+ ...none
128
+ };
129
+ const threshold = opts.threshold ?? DIFF_THRESHOLD;
130
+ const a = fromBase64(before.data);
131
+ const b = fromBase64(after.data);
132
+ const { cols, rows } = after;
133
+ let changedCells = 0;
134
+ let maxDelta = 0;
135
+ let totalDelta = 0;
136
+ let minCol = cols;
137
+ let maxCol = -1;
138
+ let minRow = rows;
139
+ let maxRow = -1;
140
+ for (let row = 0; row < rows; row++) for (let col = 0; col < cols; col++) {
141
+ const o = (row * cols + col) * 3;
142
+ const delta = Math.max(Math.abs((a[o] ?? 0) - (b[o] ?? 0)), Math.abs((a[o + 1] ?? 0) - (b[o + 1] ?? 0)), Math.abs((a[o + 2] ?? 0) - (b[o + 2] ?? 0)));
143
+ totalDelta += delta;
144
+ maxDelta = Math.max(maxDelta, delta);
145
+ if (delta >= threshold) {
146
+ changedCells += 1;
147
+ minCol = Math.min(minCol, col);
148
+ maxCol = Math.max(maxCol, col);
149
+ minRow = Math.min(minRow, row);
150
+ maxRow = Math.max(maxRow, row);
151
+ }
152
+ }
153
+ const cells = cols * rows;
154
+ const box = maxCol < 0 ? null : {
155
+ x: Math.floor(minCol * after.width / cols),
156
+ y: Math.floor(minRow * after.height / rows),
157
+ w: Math.ceil((maxCol + 1 - minCol) * after.width / cols),
158
+ h: Math.ceil((maxRow + 1 - minRow) * after.height / rows)
159
+ };
160
+ const spanCols = maxCol < 0 ? 0 : maxCol + 1 - minCol;
161
+ const spanRows = maxRow < 0 ? 0 : maxRow + 1 - minRow;
162
+ return {
163
+ comparable: true,
164
+ changed: changedCells / cells,
165
+ maxDelta,
166
+ meanDelta: Math.round(totalDelta / cells * 10) / 10,
167
+ box,
168
+ shape: spanCols > 0 && spanCols <= 2 && spanRows >= rows / 2 ? "vertical band" : spanRows > 0 && spanRows <= 2 && spanCols >= cols / 2 ? "horizontal band" : null
169
+ };
170
+ }
171
+ /** The comparison a person or an agent reads. */
172
+ function diffText(diff) {
173
+ if (!diff.comparable) return `cannot compare — ${diff.reason ?? "unknown reason"}`;
174
+ if (diff.changed === 0) return `identical — nothing moved (largest cell shift ${diff.maxDelta}/255)`;
175
+ const lines = [`changed ${(diff.changed * 100).toFixed(1)}% of the frame · largest shift ${diff.maxDelta}/255, mean ${diff.meanDelta}`];
176
+ if (diff.box) {
177
+ const { x, y, w, h } = diff.box;
178
+ lines.push(`region x ${x}..${x + w}, y ${y}..${y + h}${diff.shape ? ` — a ${diff.shape}` : ""}`);
179
+ }
180
+ if (diff.changed > .9) lines.push("the whole frame moved — if the scene is animating, pause it first (debug ☰ → Time → Pause) so the comparison is of your change, not of time passing");
181
+ return lines.join("\n");
182
+ }
183
+ /** Portable in a browser and in node; both have had `btoa`/`atob` for years. */
184
+ function toBase64(bytes) {
185
+ let binary = "";
186
+ const CHUNK = 32768;
187
+ for (let i = 0; i < bytes.length; i += CHUNK) binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
188
+ return btoa(binary);
189
+ }
190
+ function fromBase64(text) {
191
+ const binary = atob(text);
192
+ const out = new Uint8Array(binary.length);
193
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
194
+ return out;
195
+ }
196
+ //#endregion
197
+ export { frameStats as a, frameSignature as i, diffSignatures as n, frameText as o, diffText as r, SIGNATURE_GRID as t };
@@ -4973,10 +4973,15 @@ var GameFlow = class extends Behavior {
4973
4973
  static signals = ["flowChanged"];
4974
4974
  static props = {
4975
4975
  restartAction: { default: "restart" },
4976
+ pauseAction: { default: "pause" },
4976
4977
  freezeOnEnd: { default: true },
4977
4978
  bannerPath: {
4978
4979
  default: "%Banner",
4979
4980
  nodePath: true
4981
+ },
4982
+ pausePanelPath: {
4983
+ default: "%PauseMenu",
4984
+ nodePath: true
4980
4985
  }
4981
4986
  };
4982
4987
  /** Input action that restarts from gameover/won (declare it in `input{}`). */
@@ -4985,8 +4990,31 @@ var GameFlow = class extends Behavior {
4985
4990
  freezeOnEnd = true;
4986
4991
  /** Where the flow looks for a UiBanner ('' = never). */
4987
4992
  bannerPath = "%Banner";
4993
+ /**
4994
+ * Input action that toggles pause (declare it in `input{}`; undeclared is
4995
+ * fine and means pause is API-only).
4996
+ */
4997
+ pauseAction = "pause";
4998
+ /**
4999
+ * A node shown while paused and hidden otherwise — a `UiPanel` of buttons in
5000
+ * practice. '' turns the whole thing off.
5001
+ *
5002
+ * This lives here rather than in each game because it is identical in all of
5003
+ * them, and a pause menu that costs a script is a pause menu no generated
5004
+ * game will have. `UiPanel` appeared in zero template scenes before this.
5005
+ */
5006
+ pausePanelPath = "%PauseMenu";
4988
5007
  state = "playing";
4989
5008
  frozeScale = false;
5009
+ onReady() {
5010
+ this.syncPausePanel();
5011
+ }
5012
+ /** The panel follows the state; nothing else may own its visibility. */
5013
+ syncPausePanel() {
5014
+ if (this.pausePanelPath === "") return;
5015
+ const panel = this.node.getNodeOrNull(this.pausePanelPath);
5016
+ if (panel && "visible" in panel) panel.visible = this.state === "paused";
5017
+ }
4990
5018
  gameOver(text = "GAME OVER", color = "#ef4444") {
4991
5019
  this.transition("gameover", text, color);
4992
5020
  }
@@ -4997,12 +5025,14 @@ var GameFlow = class extends Behavior {
4997
5025
  if (this.state !== "playing") return;
4998
5026
  this.state = "paused";
4999
5027
  this.freeze();
5028
+ this.syncPausePanel();
5000
5029
  this.node.emit("flowChanged", this.state);
5001
5030
  }
5002
5031
  resume() {
5003
5032
  if (this.state !== "paused") return;
5004
5033
  this.state = "playing";
5005
5034
  this.thaw();
5035
+ this.syncPausePanel();
5006
5036
  this.node.emit("flowChanged", this.state);
5007
5037
  }
5008
5038
  restart() {
@@ -5035,6 +5065,13 @@ var GameFlow = class extends Behavior {
5035
5065
  this.engine.timeScale = 1;
5036
5066
  }
5037
5067
  update() {
5068
+ if (this.state === "playing" || this.state === "paused") {
5069
+ try {
5070
+ if (this.engine.input.justPressed(this.pauseAction)) if (this.state === "playing") this.pause();
5071
+ else this.resume();
5072
+ } catch {}
5073
+ return;
5074
+ }
5038
5075
  if (this.state !== "gameover" && this.state !== "won") return;
5039
5076
  try {
5040
5077
  if (this.engine.input.justPressed(this.restartAction)) this.restart();
@@ -622,8 +622,25 @@ declare class GameFlow extends Behavior {
622
622
  freezeOnEnd: boolean;
623
623
  /** Where the flow looks for a UiBanner ('' = never). */
624
624
  bannerPath: string;
625
+ /**
626
+ * Input action that toggles pause (declare it in `input{}`; undeclared is
627
+ * fine and means pause is API-only).
628
+ */
629
+ pauseAction: string;
630
+ /**
631
+ * A node shown while paused and hidden otherwise — a `UiPanel` of buttons in
632
+ * practice. '' turns the whole thing off.
633
+ *
634
+ * This lives here rather than in each game because it is identical in all of
635
+ * them, and a pause menu that costs a script is a pause menu no generated
636
+ * game will have. `UiPanel` appeared in zero template scenes before this.
637
+ */
638
+ pausePanelPath: string;
625
639
  state: GameFlowState;
626
640
  private frozeScale;
641
+ override onReady(): void;
642
+ /** The panel follows the state; nothing else may own its visibility. */
643
+ private syncPausePanel;
627
644
  gameOver(text?: string, color?: string): void;
628
645
  win(text?: string, color?: string): void;
629
646
  pause(): void;
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-DRi9524r.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-BQOeAid6.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.js CHANGED
@@ -7,7 +7,7 @@ import { n as jsonEquals, r as jsonKind, t as jsonClone } from "./json-BLk7H2Qa.
7
7
  import { a as getNodeSignals, c as mergeStaticSignals, i as getNodeSchema, l as registerNode, n as clearRegistry, o as getNodeType, r as createNode, u as registeredTypes } from "./registry-IyWCGe4q.js";
8
8
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
9
9
  import { i as applyParticlePreset, n as PARTICLE_PRESETS, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-Bw7hB93B.js";
10
- import { a as findPath, i as preloadUrls, n as newUid, o as gridFromRows, r as assetUrls, t as VERSION } from "./src-cU57Uwdw.js";
10
+ import { a as findPath, i as preloadUrls, n as newUid, o as gridFromRows, r as assetUrls, t as VERSION } from "./src-B3n06SsL.js";
11
11
  import { i as resolveRendering, n as attachTouchControls, r as joystickVector, t as TouchControls } from "./touch-BoNg_MnF.js";
12
12
  import { t as duplicateNode } from "./duplicate-CRtihGmC.js";
13
13
  export { AudioBuses, AudioPlayer, BASE_LOCALE, Behavior, CONST_REF_KEY, Engine, HudLayer, IncantoError, InputMap, Localization, LogManager, MusicManager, Node, ORDER_GROUP_BASE, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, ParticleSim, ROLLOFF_MODELS, Rng, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, SaveSlots, Scene, SceneTree, Settings, SfxEngine, Signal, T_PREFIX, Timer, TouchControls, UiBanner, UiBar, UiButton, UiDialogue, UiImage, UiPanel, UiSelect, UiSlider, UiText, UiToggle, VERSION, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, behaviorSchema, behaviorSignals, behaviorsWithoutSave, captureBehaviors, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, mergeStaticSignals, newUid, parseNodePath, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveConstants, resolveOrderGroups, resolveRendering, resolveViewport, restoreBehaviors, serializeNode, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
@@ -1,9 +1,9 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { v as diagnose } from "./loader-r49nDwB4.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
- import { F as PhysicsBody3D, I as RigidBody3D, N as Area3D, P as CharacterBody3D, R as Node3D, z as validateCollider3D } from "./gameplay-DRi9524r.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";
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-Vl5xBsXp.js";
6
+ import { A as Terrain3D, F as InstancedMesh3D, L as buildMeshGeometry, P as Joint3D } from "./environment-presets-Ds5kXLoF.js";
7
7
  import { Euler, Matrix4, Quaternion, Vector3 } from "three";
8
8
  //#region src/3d/physics/collider-lines.ts
9
9
  /**
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-DItIRyXH.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-9KPppR0L.js").then((n) => n.n)).createGame2D(o)))(opts);
159
+ const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-D90pyPMx.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-DFRbG0Bj.js").then((n) => n.n)).createGame2D(o)))(opts);
160
160
  if (disposed) {
161
161
  next.dispose();
162
162
  return;
@@ -161,6 +161,6 @@ function newUid() {
161
161
  //#endregion
162
162
  //#region src/index.ts
163
163
  /** Engine version. Kept in sync with package.json by the release pipeline. */
164
- const VERSION = "0.42.0";
164
+ const VERSION = "0.44.0";
165
165
  //#endregion
166
166
  export { findPath as a, preloadUrls as i, newUid as n, gridFromRows as o, assetUrls as r, VERSION as t };
@@ -238,6 +238,37 @@ function poseFromRenderer(renderer, fallbackDistance = 8) {
238
238
  };
239
239
  }
240
240
  //#endregion
241
+ //#region src/core/pause-when-hidden.ts
242
+ function pauseWhenHidden(engine, doc = typeof document === "undefined" ? null : document) {
243
+ if (!doc) return () => {};
244
+ /** The scale to come back to — null when we did not pause anything. */
245
+ let heldScale = null;
246
+ let mutedByUs = false;
247
+ const onChange = () => {
248
+ if (doc.hidden) {
249
+ if (heldScale === null && engine.timeScale > 0) {
250
+ heldScale = engine.timeScale;
251
+ engine.timeScale = 0;
252
+ }
253
+ if (!engine.audio.muted) {
254
+ engine.audio.muted = true;
255
+ mutedByUs = true;
256
+ }
257
+ return;
258
+ }
259
+ if (heldScale !== null) {
260
+ if (engine.timeScale === 0) engine.timeScale = heldScale;
261
+ heldScale = null;
262
+ }
263
+ if (mutedByUs) {
264
+ engine.audio.muted = false;
265
+ mutedByUs = false;
266
+ }
267
+ };
268
+ doc.addEventListener("visibilitychange", onChange);
269
+ return () => doc.removeEventListener("visibilitychange", onChange);
270
+ }
271
+ //#endregion
241
272
  //#region src/core/teardown.ts
242
273
  function teardown(steps, onError = defaultReport) {
243
274
  const failures = [];
@@ -262,4 +293,4 @@ function defaultReport(what, error) {
262
293
  console.error(`[incanto] teardown: ${what} failed (continuing)`, error);
263
294
  }
264
295
  //#endregion
265
- export { poseFromRenderer as a, openBundledEditor as i, crossFade as n, devServerLibrary as r, teardown as t };
296
+ export { openBundledEditor as a, devServerLibrary as i, pauseWhenHidden as n, poseFromRenderer as o, crossFade as r, teardown as t };
@@ -4,9 +4,9 @@ import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { n as startRecording } from "./replay-DilbZgQI.js";
5
5
  import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
6
6
  import { i as getNodeSchema, s as mergeStaticProps } from "./registry-IyWCGe4q.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-DRi9524r.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-BQOeAid6.js";
8
8
  import { t as registerNodes2D } from "./register-R2JTnIMw.js";
9
- import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-Vl5xBsXp.js";
9
+ import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-Ds5kXLoF.js";
10
10
  import { t as registerNodesNet } from "./register-D651it1J.js";
11
11
  import { Box3, Euler, Matrix4, PerspectiveCamera, Quaternion, Vector3 } from "three";
12
12
  //#region src/test/framing.ts
@@ -651,6 +651,118 @@ function failingReplays(report) {
651
651
  }));
652
652
  }
653
653
  //#endregion
654
+ //#region src/test/facing.ts
655
+ const DEG2RAD = Math.PI / 180;
656
+ /** The forward vector of a yaw (degrees) for a given art convention. */
657
+ function forwardOf(yawDeg, axis) {
658
+ const yaw = yawDeg * DEG2RAD;
659
+ const base = [Math.sin(yaw), Math.cos(yaw)];
660
+ switch (axis) {
661
+ case "-z": return [-base[0], -base[1]];
662
+ case "+x": return [base[1], -base[0]];
663
+ case "-x": return [-base[1], base[0]];
664
+ default: return base;
665
+ }
666
+ }
667
+ /**
668
+ * The skin, and the yaw the art is DECLARED to be rotated by.
669
+ *
670
+ * `skinYawOffset` is how a model that does not face +Z says so, and it is the
671
+ * supported fix rather than a bug. Subtracting it is what stops this check
672
+ * crying wolf on every correctly-configured non-+Z model.
673
+ */
674
+ function findSkin(player, explicit) {
675
+ if (explicit) return {
676
+ skin: player.getNodeOrNull(explicit) ?? null,
677
+ declaredOffset: 0
678
+ };
679
+ for (const child of player.children) {
680
+ const ctl = child;
681
+ if (typeof ctl.skinPath === "string" && ctl.skinPath !== "") {
682
+ const skin = child.getNodeOrNull(ctl.skinPath);
683
+ if (skin) return {
684
+ skin,
685
+ declaredOffset: ctl.skinYawOffset ?? 0
686
+ };
687
+ }
688
+ }
689
+ return {
690
+ skin: player.getNodeOrNull("%Skin") ?? null,
691
+ declaredOffset: 0
692
+ };
693
+ }
694
+ /**
695
+ * Drive the player forward and compare where the skin points with where it went.
696
+ *
697
+ * The travel direction is measured, not assumed: whatever the input map means by
698
+ * "up", the character ends up going somewhere, and that somewhere is the truth
699
+ * the art has to agree with.
700
+ */
701
+ async function facingReport(json, opts = {}) {
702
+ const session = await createPlaySession(json, {
703
+ seed: opts.seed ?? 1,
704
+ ...opts.behaviors ? { behaviors: opts.behaviors } : {},
705
+ stubMissingBehaviors: opts.stubMissingBehaviors ?? true,
706
+ ...opts.resolveScene ? { resolveScene: opts.resolveScene } : {}
707
+ });
708
+ const player = findPlayer(session.scene.root);
709
+ if (!player) {
710
+ session.dispose();
711
+ return {
712
+ player: null,
713
+ skin: null,
714
+ dot: null,
715
+ backwards: false,
716
+ travelled: 0
717
+ };
718
+ }
719
+ const declared = session.engine.input.declaredActions();
720
+ const move = opts.moveAction ?? declared.find((a) => a.type === "vector2")?.name ?? null;
721
+ const posOf = () => {
722
+ const p = player.position;
723
+ return [p[0] ?? 0, p[2] ?? 0];
724
+ };
725
+ session.step(500);
726
+ const from = posOf();
727
+ if (move) session.engine.input.setActionVector(move, 1, 0);
728
+ session.step(700);
729
+ const to = posOf();
730
+ if (move) session.engine.input.setActionVector(move, 0, 0);
731
+ const dx = to[0] - from[0];
732
+ const dz = to[1] - from[1];
733
+ const travelled = Math.hypot(dx, dz);
734
+ const { skin, declaredOffset } = findSkin(player, opts.skinPath);
735
+ const skinPath = skin ? skin.getPath() : null;
736
+ if (!skin || travelled < .05) {
737
+ session.dispose();
738
+ return {
739
+ player: player.getPath(),
740
+ skin: skinPath,
741
+ dot: null,
742
+ backwards: false,
743
+ travelled
744
+ };
745
+ }
746
+ const [fx, fz] = forwardOf((skin.rotation?.[1] ?? 0) - declaredOffset, opts.forwardAxis ?? "+z");
747
+ const dot = (fx * dx + fz * dz) / travelled;
748
+ session.dispose();
749
+ return {
750
+ player: player.getPath(),
751
+ skin: skinPath,
752
+ dot: Math.round(dot * 1e3) / 1e3,
753
+ backwards: dot < -.5,
754
+ travelled: Math.round(travelled * 1e3) / 1e3
755
+ };
756
+ }
757
+ /** One line for a report, or the failure spelled out. */
758
+ function facingText(report) {
759
+ if (!report.player) return "facing: no player to drive";
760
+ if (report.skin === null) return `facing: ${report.player} has no skin to check`;
761
+ if (report.dot === null) return `facing: ${report.player} did not move — nothing to compare`;
762
+ if (report.backwards) return `facing: ${report.skin} RUNS BACKWARDS (dot ${report.dot.toFixed(2)}) — the skin points away from the direction of travel. The rule is +Z forward, yaw = atan2(dx, dz), with NO +180; for art that faces another way set skinYawOffset rather than adding to the yaw.`;
763
+ return `facing: ${report.skin} faces its travel (dot ${report.dot.toFixed(2)})`;
764
+ }
765
+ //#endregion
654
766
  //#region src/test/feel.ts
655
767
  const STEP_MS = 1e3 / 60;
656
768
  /** Drop height for the buffer probe — long enough to outlast a coyote window. */
@@ -1258,7 +1370,7 @@ async function runScript(json, opts) {
1258
1370
  const { enablePhysics2D } = await import("./physics-2d-BmgXBNDB.js").then((n) => n.r);
1259
1371
  await enablePhysics2D(engine);
1260
1372
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
1261
- const { enablePhysics3D } = await import("./physics-3d-C2G604O1.js").then((n) => n.r);
1373
+ const { enablePhysics3D } = await import("./physics-3d-CLPFv99o.js").then((n) => n.r);
1262
1374
  await enablePhysics3D(engine);
1263
1375
  }
1264
1376
  const failures = [];
@@ -1374,7 +1486,7 @@ async function createPlaySession(json, opts = {}) {
1374
1486
  const { enablePhysics2D } = await import("./physics-2d-BmgXBNDB.js").then((n) => n.r);
1375
1487
  await enablePhysics2D(engine);
1376
1488
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
1377
- const { enablePhysics3D } = await import("./physics-3d-C2G604O1.js").then((n) => n.r);
1489
+ const { enablePhysics3D } = await import("./physics-3d-CLPFv99o.js").then((n) => n.r);
1378
1490
  await enablePhysics3D(engine);
1379
1491
  }
1380
1492
  const stepMs = 1e3 / (opts.fixedHz ?? 60);
@@ -1412,4 +1524,4 @@ async function createPlaySession(json, opts = {}) {
1412
1524
  };
1413
1525
  }
1414
1526
  //#endregion
1415
- export { registerAllNodes as a, feelReport as c, findPlayer as d, playtest as f, framingText as h, findFloatingProps as i, feelText as l, describeFraming as m, createPlaySession as n, runScript as o, playtestText as p, describeCapture as r, validateScene as s, captureScene as t, failingReplays as u };
1527
+ export { framingText as _, registerAllNodes as a, feelReport as c, facingText as d, failingReplays as f, describeFraming as g, playtestText as h, findFloatingProps as i, feelText as l, playtest as m, createPlaySession as n, runScript as o, findPlayer as p, describeCapture as r, validateScene as s, captureScene as t, facingReport as u };