incanto 0.59.0 → 0.61.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 (64) hide show
  1. package/bin/incanto-check.mjs +58 -4
  2. package/dist/2d.d.ts +22 -3
  3. package/dist/2d.js +3 -3
  4. package/dist/3d.d.ts +5 -5
  5. package/dist/3d.js +5 -4
  6. package/dist/{pathfinding-BqWBb0kh.d.ts → audio-player-D5GJgb_x.d.ts} +103 -38
  7. package/dist/{behavior-DWKTUzKI.d.ts → behavior-DsgayMsH.d.ts} +79 -1
  8. package/dist/{create-game-CHDLDQsQ.js → create-game-DH7JI5xx.js} +76 -11
  9. package/dist/{create-game-BiW8Men_.js → create-game-IX5lEH0P.js} +21 -294
  10. package/dist/debug.d.ts +1 -1
  11. package/dist/debug.js +1 -1
  12. package/dist/{duplicate-DJQd44CD.js → duplicate-CGqAmK2h.js} +1 -1
  13. package/dist/editor.js +2 -1
  14. package/dist/{environment-presets-DRAz5EV9.js → environment-presets-CNxCuhZF.js} +48 -13
  15. package/dist/{gameplay-BBEjPFsR.js → gameplay-Dtzd2itW.js} +53 -11
  16. package/dist/gameplay.d.ts +1 -1
  17. package/dist/gameplay.js +1 -1
  18. package/dist/index.d.ts +23 -6
  19. package/dist/index.js +7 -7
  20. package/dist/{loader-D8n7TU8W.js → loader-D7jTvDQv.js} +797 -2
  21. package/dist/{loader-TvkRFbyL.d.ts → loader-DolLJWJn.d.ts} +13 -1
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +1 -1
  24. package/dist/pathfinding-_fGrCFmH.d.ts +28 -0
  25. package/dist/{physics-2d-BaRSRrrZ.js → physics-2d-CBnor8Zf.js} +2 -2
  26. package/dist/{physics-3d-CYxjh-HW.js → physics-3d-BTUfUSWO.js} +3 -3
  27. package/dist/react.d.ts +1 -1
  28. package/dist/react.js +1 -1
  29. package/dist/{register-BpFcgdcL.js → register-DL3izw8j.js} +46 -9
  30. package/dist/{register-CDrAQqPp.js → register-xuSRyD6b.js} +119 -21
  31. package/dist/{registry-WWcQcfMr.js → registry-CF70EArN.js} +55 -3
  32. package/dist/{replay-O-yAGM76.d.ts → replay-C5x2vPF5.d.ts} +16 -3
  33. package/dist/{replay-CEPyQtF_.js → replay-D7-yle3s.js} +56 -205
  34. package/dist/{split-screen-DDMZutQ6.js → split-screen-5Ban4q4n.js} +3 -3
  35. package/dist/{split-screen-BQ3tAsf-.d.ts → split-screen-D7OopelJ.d.ts} +2 -2
  36. package/dist/{src-CY21B462.js → src-DozXvyZS.js} +1 -1
  37. package/dist/{teardown-RApWnM1G.js → teardown-B6rwJOyS.js} +1 -1
  38. package/dist/{test-DHYuFyAu.js → test-Dch_7VQD.js} +19 -14
  39. package/dist/test.d.ts +24 -4
  40. package/dist/test.js +2 -2
  41. package/dist/vite.js +2 -2
  42. package/editor/assets/{agent8-BoRGtVxK.js → agent8-CF1JL2tR.js} +1 -1
  43. package/editor/assets/{debug-CzdyCg75.js → debug-3QzYhOPA.js} +1 -1
  44. package/editor/assets/{index-VesuVEhe.js → index-CAD2c5ug.js} +92 -92
  45. package/editor/index.html +1 -1
  46. package/package.json +1 -1
  47. package/schemas/scene.schema.json +4 -0
  48. package/skills/incanto-3d-models.md +1 -1
  49. package/skills/incanto-assets.md +10 -1
  50. package/skills/incanto-audio.md +21 -13
  51. package/skills/incanto-behaviors-and-scripts.md +7 -3
  52. package/skills/incanto-building-2d-games.md +12 -3
  53. package/skills/incanto-editor.md +1 -1
  54. package/skills/incanto-gameplay-behaviors.md +24 -1
  55. package/skills/incanto-hud.md +5 -3
  56. package/skills/incanto-localization.md +31 -0
  57. package/skills/incanto-node-reference.md +1 -0
  58. package/skills/incanto-scene-json-authoring.md +6 -1
  59. package/templates-app/beacon-isle-3d/package.json +1 -1
  60. package/templates-app/platformer-2d/package.json +1 -1
  61. package/templates-app/star-survivor/package.json +1 -1
  62. package/templates-app/tps-3d/package.json +1 -1
  63. package/templates-app/village-quest-3d/package.json +1 -1
  64. package/dist/particle-sim-C5OfBbmU.d.ts +0 -77
@@ -8,6 +8,7 @@
8
8
  * npx incanto-check src/game.scene.json → specific files (or directories)
9
9
  * npx incanto-check --json → machine-readable report
10
10
  * npx incanto-check --strict-behaviors → unregistered script names fail too
11
+ * npx incanto-check --strict-warnings → a warning is a failure (CI, agents)
11
12
  *
12
13
  * Unregistered behaviors are STUBBED by default: structure-only validation
13
14
  * that needs no TypeScript. Sub-scene `instance` paths resolve relative to
@@ -25,6 +26,7 @@ function parseArgs(argv) {
25
26
  for (const a of argv) {
26
27
  if (a === '--json') args.json = true;
27
28
  else if (a === '--strict-behaviors') args.strict = true;
29
+ else if (a === '--strict-warnings') args.strictWarnings = true;
28
30
  else if (a === '--help' || a === '-h') args.help = true;
29
31
  else if (a.startsWith('--')) {
30
32
  console.error(`unknown flag: ${a}`);
@@ -36,7 +38,7 @@ function parseArgs(argv) {
36
38
 
37
39
  const args = parseArgs(process.argv.slice(2));
38
40
  if (args.help || args.invalid) {
39
- console.log(`Usage: npx incanto-check [files|dirs...] [--json] [--strict-behaviors]
41
+ console.log(`Usage: npx incanto check [files|dirs...] [--json] [--strict-behaviors] [--strict-warnings]
40
42
 
41
43
  Validates *.scene.json files headlessly with the installed engine.
42
44
  With no arguments, scans the current directory recursively
@@ -155,6 +157,40 @@ if (files.length === 0) {
155
157
 
156
158
  const { auditScene, validateScene } = await import(pathToFileURL(join(DIST, 'test.js')).href);
157
159
 
160
+ /**
161
+ * Every `strings` table in the corpus, unioned.
162
+ *
163
+ * Localization tables MERGE across scenes at runtime — a game's shared UI
164
+ * strings live in the scene it boots from — so auditing one file in isolation
165
+ * reported a key declared in the boot scene as "nothing declares" it, and told
166
+ * its author to add a string they already have. Checking a project means
167
+ * knowing what the project declares.
168
+ */
169
+ const declaredElsewhere = {};
170
+ /**
171
+ * Every asset the project declares.
172
+ *
173
+ * A scene a spawner instantiates resolves its `$ref`s against the HOST's
174
+ * `assets{}` — `remote-player.scene.json` names `$characters/base` and declares
175
+ * nothing itself, which is correct. Checked alone it cannot resolve; checked as
176
+ * part of its project it can.
177
+ */
178
+ const declaredAssets = {};
179
+ for (const file of files) {
180
+ try {
181
+ const doc = JSON.parse(readFileSync(file, 'utf-8'));
182
+ for (const [locale, entries] of Object.entries(doc?.strings ?? {})) {
183
+ if (typeof entries !== 'object' || entries === null) continue;
184
+ declaredElsewhere[locale] = { ...(declaredElsewhere[locale] ?? {}), ...entries };
185
+ }
186
+ for (const [key, entry] of Object.entries(doc?.assets ?? {})) {
187
+ if (typeof entry === 'object' && entry !== null) declaredAssets[key] ??= entry;
188
+ }
189
+ } catch {
190
+ /* a file that will not parse is reported below, on its own line */
191
+ }
192
+ }
193
+
158
194
  const results = files.map((file) => {
159
195
  let json;
160
196
  try {
@@ -164,9 +200,16 @@ const results = files.map((file) => {
164
200
  }
165
201
  const res = validateScene(json, {
166
202
  strictBehaviors: args.strict,
203
+ declaredAssets,
167
204
  resolveScene: (p) => JSON.parse(readFileSync(resolve(dirname(file), p), 'utf-8')),
168
205
  });
169
- if (res.ok) return { file, ok: true, warnings: [...auditScene(json), ...missingArt(file, json)] };
206
+ if (res.ok) {
207
+ return {
208
+ file,
209
+ ok: true,
210
+ warnings: [...auditScene(json, { declaredElsewhere }), ...missingArt(file, json)],
211
+ };
212
+ }
170
213
  return {
171
214
  file,
172
215
  ok: false,
@@ -177,6 +220,7 @@ const results = files.map((file) => {
177
220
  });
178
221
 
179
222
  const failed = results.filter((r) => !r.ok);
223
+ const warned = results.reduce((n, r) => n + (r.warnings?.length ?? 0), 0);
180
224
  if (args.json) {
181
225
  console.log(JSON.stringify({ ok: failed.length === 0, results }, null, 2));
182
226
  } else {
@@ -186,10 +230,20 @@ if (args.json) {
186
230
  for (const w of r.warnings ?? []) console.log(` warn: ${w}`);
187
231
  } else console.log(`FAIL ${r.file}\n [${r.code}] ${r.message}`);
188
232
  }
189
- console.log(`\n${results.length - failed.length}/${results.length} scene(s) valid`);
233
+ // Say the warning count on the summary line. A warning printed under an `ok`
234
+ // and a summary that never mentions it is a finding an agent scrolls past.
235
+ console.log(
236
+ `\n${results.length - failed.length}/${results.length} scene(s) valid` +
237
+ (warned > 0
238
+ ? `, ${warned} warning(s)${args.strictWarnings ? '' : ' — `--strict-warnings` to fail on them'}`
239
+ : ''),
240
+ );
190
241
  }
191
242
  // `exitCode`, never `process.exit()`: stdout to a PIPE is written
192
243
  // asynchronously, and exiting discards whatever has not flushed. A --json
193
244
  // report read by another program came back truncated — silently, and only
194
245
  // when piped, which is the only way a program reads it.
195
- process.exitCode = failed.length === 0 ? 0 : 1;
246
+ // A warning is a finding, and a checker read by a CI step or an agent is read
247
+ // through its EXIT CODE. Opt-in, because turning it on by default would fail
248
+ // every project that has been living with one.
249
+ process.exitCode = failed.length === 0 && !(args.strictWarnings && warned > 0) ? 0 : 1;
package/dist/2d.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { At as Node, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-DWKTUzKI.js";
1
+ import { P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, w as GameStats } from "./behavior-DsgayMsH.js";
2
2
  import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DAvWQeld.js";
3
3
  import { i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
4
4
  import { r as FrameStats } from "./frame-report-DCnHFmto.js";
5
- import { t as LoadSceneOptions } from "./loader-TvkRFbyL.js";
5
+ import { t as LoadSceneOptions } from "./loader-DolLJWJn.js";
6
6
  import { n as AnimationEntry } from "./sprite-animation-CMr6f1K2.js";
7
- import { r as ParticleView, t as ParticleSim } from "./particle-sim-C5OfBbmU.js";
7
+ import { i as ParticleSim, o as ParticleView, r as SpatialPose } from "./audio-player-D5GJgb_x.js";
8
8
  import { Group, Mesh, Object3D, Scene, Texture } from "three";
9
9
  import * as RapierNs from "@dimforge/rapier2d-compat";
10
10
 
@@ -701,6 +701,16 @@ declare class Label extends Node2D {
701
701
  font: string;
702
702
  /** 'left' | 'center' | 'right' — anchor of the text block on the node origin. */
703
703
  align: string;
704
+ /**
705
+ * 0..1, like `Sprite2D` and `ColorRect2D`.
706
+ *
707
+ * Label was the one 2D drawable without it, and the gap showed up through
708
+ * `FloatAway` — whose own doc names "a 2D `Label`" as a node it works on.
709
+ * `FloatAway` writes `opacity` when the node has one, so on a Label the
710
+ * number rose to full height at FULL opacity and then vanished in a single
711
+ * frame: the pop that behavior exists to avoid.
712
+ */
713
+ opacity: number;
704
714
  private quadMesh;
705
715
  private canvas;
706
716
  private lastKey;
@@ -1081,6 +1091,15 @@ interface UiSize {
1081
1091
  interface Sync2DScratch {
1082
1092
  visited: Set<Object3D>;
1083
1093
  cameras: Camera2D[];
1094
+ emitters: Array<{
1095
+ node: Node & SpatialConsumer;
1096
+ parent: Object3D;
1097
+ }>;
1098
+ }
1099
+ /** A node that accepts a per-frame spatial pose (an AudioPlayer with spatial on). */
1100
+ interface SpatialConsumer {
1101
+ spatial: boolean;
1102
+ _setSpatialPose(pose: SpatialPose): void;
1084
1103
  }
1085
1104
  declare function syncTree2D(root: Node, world: Scene, ui: Scene, assets: AssetStore2D | null, uiSize?: UiSize, scratch?: Sync2DScratch, opts?: {
1086
1105
  ignoreStatic?: boolean;
package/dist/2d.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
- import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-CHDLDQsQ.js";
3
- import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-BpFcgdcL.js";
4
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-BaRSRrrZ.js";
2
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-DH7JI5xx.js";
3
+ import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-DL3izw8j.js";
4
+ import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-CBnor8Zf.js";
5
5
  //#region src/2d/library-sprite.ts
6
6
  /**
7
7
  * What a `CharacterController2D`/`3D` will ask a skin to play, and the clip in
package/dist/3d.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { At as Node, D as QualityCaps, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-DWKTUzKI.js";
1
+ import { D as QualityCaps, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, w as GameStats } from "./behavior-DsgayMsH.js";
2
2
  import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DAvWQeld.js";
3
3
  import { i as SceneJson$1, s as JsonObject } from "./schema-CFeioQRE.js";
4
4
  import { a as GridCell, c as diffText, d as frameText, i as FrameStatsOptions, l as frameSignature, n as FrameSignature, o as SIGNATURE_GRID, r as FrameStats, s as diffSignatures, t as FrameDiff, u as frameStats } from "./frame-report-DCnHFmto.js";
5
- import { t as LoadSceneOptions } from "./loader-TvkRFbyL.js";
6
- import { r as ParticleView, t as ParticleSim } from "./particle-sim-C5OfBbmU.js";
7
- import { n as PathGrid, s as SpatialPose } from "./pathfinding-BqWBb0kh.js";
5
+ import { t as LoadSceneOptions } from "./loader-DolLJWJn.js";
6
+ import { i as ParticleSim, o as ParticleView, r as SpatialPose } from "./audio-player-D5GJgb_x.js";
7
+ import { n as PathGrid } from "./pathfinding-_fGrCFmH.js";
8
8
  import { AnimationClip, AnimationMixer, BufferGeometry, Color, DirectionalLight, Group, InstancedMesh, Mesh, MeshPhysicalMaterial, Object3D, PerspectiveCamera, Scene, ShaderMaterial, Texture, Vector3, WebGLRenderer } from "three";
9
9
  import { VRM } from "@pixiv/three-vrm";
10
10
  import { Sky } from "three/examples/jsm/objects/Sky.js";
@@ -730,7 +730,7 @@ interface Game3D {
730
730
  */
731
731
  declare function createGame3D(opts: CreateGame3DOptions): Promise<Game3D>;
732
732
  //#endregion
733
- //#region src/3d/environment.d.ts
733
+ //#region src/core/scene/environment.d.ts
734
734
  /**
735
735
  * The 3D `environment` header, parsed and validated — the renderer's
736
736
  * "rendering stage". Pure data in, plain data out (NO three imports), so the
package/dist/3d.js CHANGED
@@ -1,9 +1,10 @@
1
+ import { c as parseEnvironment3D, l as sunDirectionFromElevationAzimuth, s as horizonColorFromSky, u as sunDirectionFromSky } from "./loader-D7jTvDQv.js";
1
2
  import { a as frameSignature, n as diffSignatures, o as frameStats, r as diffText, s as frameText, t as SIGNATURE_GRID } from "./frame-report-BSMny7oe.js";
2
- import { B as StaticBody3D, F as WaterCutout3D, I as Area3D, L as CharacterBody3D, N as Water3D, P as WATER_CUTOUT_MAX, R as PhysicsBody3D, V as Node3D, W as WATER_MAX_RIPPLES, z as RigidBody3D } from "./gameplay-BBEjPFsR.js";
3
- import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-DRAz5EV9.js";
4
- import { a as Environment3D, c as parseEnvironment3D, d as AssetStore3D, i as syncTree, l as sunDirectionFromElevationAzimuth, o as setEnvironment3D, r as Renderer3D, s as horizonColorFromSky, t as createGame3D, u as sunDirectionFromSky } from "./create-game-BiW8Men_.js";
3
+ import { B as StaticBody3D, F as WaterCutout3D, I as Area3D, L as CharacterBody3D, N as Water3D, P as WATER_CUTOUT_MAX, R as PhysicsBody3D, V as Node3D, W as WATER_MAX_RIPPLES, z as RigidBody3D } from "./gameplay-Dtzd2itW.js";
4
+ import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-CNxCuhZF.js";
5
+ import { a as Environment3D, i as syncTree, o as setEnvironment3D, r as Renderer3D, s as AssetStore3D, t as createGame3D } from "./create-game-IX5lEH0P.js";
5
6
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
6
- import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-CYxjh-HW.js";
7
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-BTUfUSWO.js";
7
8
  //#region src/3d/model-verdict.ts
8
9
  /** Mixamo exports every bone as `mixamorigX`; the retargeter binds by that name. */
9
10
  const MIXAMO = /^mixamorig[:_]?/i;
@@ -1,5 +1,80 @@
1
- import { At as Node, d as PropSchema, dt as Listener } from "./behavior-DWKTUzKI.js";
1
+ import { d as PropSchema, dt as Listener, jt as Node } from "./behavior-DsgayMsH.js";
2
+ import { a as Rng } from "./schema-CFeioQRE.js";
2
3
 
4
+ //#region src/core/particle-sim.d.ts
5
+ /**
6
+ * Deterministic particle pool — pure math, renderer-agnostic, headless-
7
+ * testable. 2D uses the xy plane (y-down, directionDeg 0 = +x, -90 = up);
8
+ * 3D feeds the same sim with a z spread.
9
+ */
10
+ interface ParticleSimConfig {
11
+ /** Particles per second (0 = burst-only). */
12
+ rate: number;
13
+ /** Seconds, [min, max]. */
14
+ lifetime: [number, number];
15
+ /** Initial speed, [min, max] (units/sec — px in 2D, meters in 3D). */
16
+ speed: [number, number];
17
+ /** Emission direction center, degrees (0 = +x, -90 = up in y-down 2D). */
18
+ directionDeg: number;
19
+ /** Cone width, degrees (360 = all directions). */
20
+ spreadDeg: number;
21
+ /** Constant acceleration (y-down in 2D). */
22
+ gravity: [number, number, number];
23
+ /** Exponential velocity damping per second (0 = none). */
24
+ drag: number;
25
+ maxParticles: number;
26
+ /** Spread emission into the z axis too (3D). */
27
+ spreadZ?: boolean;
28
+ }
29
+ interface ParticleView {
30
+ x: number;
31
+ y: number;
32
+ z: number;
33
+ vx: number;
34
+ vy: number;
35
+ vz: number;
36
+ /** Seconds alive. */
37
+ age: number;
38
+ /** Total lifetime in seconds. */
39
+ life: number;
40
+ /** age/life in [0, 1] — drives size/color/alpha ramps. */
41
+ t: number;
42
+ /** Per-particle random in [0, 1) — stable for the particle's lifetime. */
43
+ seed: number;
44
+ }
45
+ declare class ParticleSim {
46
+ private readonly config;
47
+ private readonly rng;
48
+ private readonly data;
49
+ private alive;
50
+ private spawnAccumulator;
51
+ private everSpawned;
52
+ constructor(config: ParticleSimConfig, rng: Rng);
53
+ /**
54
+ * Shift every live particle — how world-space emission is done.
55
+ *
56
+ * The pool is in the emitter's LOCAL space, so a moving emitter drags its
57
+ * whole plume with it: dust glued to a running player instead of left behind,
58
+ * and the documented "move the emitter and replay" recipe for a one-shot
59
+ * teleporting the previous explosion across the level. Counter-translating by
60
+ * the emitter's own motion each frame leaves the particles where they were
61
+ * born, exactly, and costs one pass over the live ones.
62
+ */
63
+ translateAll(dx: number, dy: number, dz: number): void;
64
+ /** Change the emission rate LIVE (particles/sec; 0 pauses emission) — lets an
65
+ * emitter toggle on/off at runtime (drift smoke, throttle flames). */
66
+ setRate(rate: number): void;
67
+ get count(): number;
68
+ /** True when nothing is alive and at least one particle has ever spawned. */
69
+ get done(): boolean;
70
+ /** Spawn n particles immediately (fireworks, explosions, flashes). */
71
+ burst(n: number): void;
72
+ update(dt: number): void;
73
+ forEach(fn: (p: ParticleView) => void): void;
74
+ private spawn;
75
+ private kill;
76
+ }
77
+ //#endregion
3
78
  //#region src/core/nodes/audio-player.d.ts
4
79
  /**
5
80
  * The per-frame spatial feed the 3D adapter pushes onto a spatial AudioPlayer:
@@ -23,7 +98,7 @@ interface AudioElementLike {
23
98
  }
24
99
  /**
25
100
  * One sound. `play()` (or `autoplay: true`) starts it; `finished` fires when
26
- * it ends. Browsers block audio before the first user gesture — a blocked
101
+ * it ends — for a procedural preset as well as a `src` clip. Browsers block audio before the first user gesture — a blocked
27
102
  * play marks `pendingGesture`; `createGame` retries pending players on the
28
103
  * first pointer/key gesture automatically.
29
104
  *
@@ -58,10 +133,12 @@ declare class AudioPlayer extends Node {
58
133
  /** Start on the first frame in the tree (subject to the gesture policy). */
59
134
  autoplay: boolean;
60
135
  /**
61
- * 3D positional audio: in a 3D scene the sound pans + attenuates by the
62
- * emitter's world position relative to the active Camera3D (the listener).
63
- * Default false → identical non-spatial behavior (back-compat). In a 2D scene
64
- * spatial is currently ignored (the adapter feeds no pose) — see incanto-audio.
136
+ * Positional audio: the sound pans + attenuates by the emitter's world
137
+ * position relative to the active camera (the listener). Default false →
138
+ * identical non-spatial behavior.
139
+ *
140
+ * Both dimensions. A 2D scene measures in PIXELS, so the distances below —
141
+ * metre-shaped defaults — need setting there; `auditScene` says so.
65
142
  */
66
143
  spatial: boolean;
67
144
  /** Distance at which spatial gain is full; closer never gets louder. */
@@ -81,6 +158,21 @@ declare class AudioPlayer extends Node {
81
158
  loadError: string | null;
82
159
  private element;
83
160
  private _playing;
161
+ /**
162
+ * `engine.unscaledTime` at which the preset currently sounding ends.
163
+ *
164
+ * A preset used to be fire-and-forget: `playing` stayed false through a sound
165
+ * that was audibly playing, and `finished` — a signal this node DECLARES, so
166
+ * a connection to it loads clean — never fired at all. Wiring "when the coin
167
+ * chime ends, free the pickup" to a preset player produced a wire that is
168
+ * dead for the life of the game and says nothing.
169
+ *
170
+ * The end is known exactly: `attack + sustain + decay` is the length
171
+ * `synthSfx` sizes its buffer to. Measured on the UNSCALED clock because a
172
+ * sound does not slow down when the game does, and a paused game still hears
173
+ * the tail of the hit that paused it.
174
+ */
175
+ private presetEndsAt;
84
176
  private autoplayed;
85
177
  /** Last spatial pose pushed by the 3D adapter (null until/unless spatial). */
86
178
  private _spatialPose;
@@ -108,10 +200,10 @@ declare class AudioPlayer extends Node {
108
200
  play(): void;
109
201
  /**
110
202
  * Procedural-SFX path: synthesize the preset and fire it through WebAudio
111
- * (low-latency, overlap-friendly). Fire-and-forget no `finished`/`playing`
112
- * tracking (these are short one-shots, not the element-driven `src` clip).
113
- * Headless (no AudioContext) silent no-op. WebAudio also needs a gesture;
114
- * a suspended context is resumed by the same unlock listener as `src` players.
203
+ * (low-latency, overlap-friendly). Headless (no AudioContext) plays nothing —
204
+ * but still reports `playing` and still emits `finished` on schedule, because
205
+ * the length is arithmetic, not a device. WebAudio needs a gesture; a
206
+ * suspended context is resumed by the same unlock listener as `src` players.
115
207
  */
116
208
  private playPreset;
117
209
  /**
@@ -130,31 +222,4 @@ declare class AudioPlayer extends Node {
130
222
  override update(_dt: number): void;
131
223
  }
132
224
  //#endregion
133
- //#region src/core/pathfinding.d.ts
134
- /**
135
- * Grid A* — the pathfinding every top-down/RPG/tower-defense asks for and
136
- * agents keep re-inventing badly. Pure and dimension-free: cells in, cells
137
- * out; the caller maps cells to world units (px, meters, tiles).
138
- */
139
- interface PathGrid {
140
- width: number;
141
- height: number;
142
- /** True = impassable. Out-of-bounds is always solid. */
143
- solid(x: number, y: number): boolean;
144
- }
145
- interface FindPathOptions {
146
- /** Allow diagonal steps (blocked from cutting corners). Default true. */
147
- diagonal?: boolean;
148
- /** Abort guard for huge/impossible searches. Default 20000 nodes. */
149
- maxExpansions?: number;
150
- }
151
- /**
152
- * A* from `from` to `to` (inclusive cell coords). Returns the cell path
153
- * INCLUDING both endpoints, or null when unreachable. Straight steps cost
154
- * 1, diagonals √2; the heuristic is octile (admissible for both modes).
155
- */
156
- declare function findPath(grid: PathGrid, from: [number, number], to: [number, number], opts?: FindPathOptions): [number, number][] | null;
157
- /** Build a PathGrid from row-strings ('#' = solid) — tests and tile games. */
158
- declare function gridFromRows(rows: readonly string[]): PathGrid;
159
- //#endregion
160
- export { AudioElementLike as a, gridFromRows as i, PathGrid as n, AudioPlayer as o, findPath as r, SpatialPose as s, FindPathOptions as t };
225
+ export { ParticleSimConfig as a, ParticleSim as i, AudioPlayer as n, ParticleView as o, SpatialPose as r, AudioElementLike as t };
@@ -551,6 +551,14 @@ interface SynthOptions {
551
551
  seed?: number;
552
552
  }
553
553
  /**
554
+ * How long a preset lasts, in seconds — the same `attack + sustain + decay`
555
+ * `synthSfx` uses to size its buffer.
556
+ *
557
+ * `pitch` deliberately does not enter into it: it is a frequency multiplier,
558
+ * not a playback rate, so a preset played an octave up is the same length.
559
+ */
560
+ declare function sfxDuration(params: SfxParams): number;
561
+ /**
554
562
  * Render a preset to mono PCM in [-1, 1]. PURE: identical inputs → identical
555
563
  * output (the only randomness is the seeded noise generator), so determinism is
556
564
  * unit-testable headlessly. Length = ceil((attack+sustain+decay) * sampleRate).
@@ -1780,6 +1788,44 @@ declare class Engine {
1780
1788
  tick(nowMs: number): void;
1781
1789
  }
1782
1790
  //#endregion
1791
+ //#region src/core/prop-range.d.ts
1792
+ /**
1793
+ * The numeric range a prop documents, checked at LOAD.
1794
+ *
1795
+ * The docs give ranges constantly — `metalness`/`roughness` "0..1 … `-1` = keep
1796
+ * authored", `targetHeight` "> 0 uniformly scales the model", `opacity` 0..1 —
1797
+ * and the schema had no way to say so, so every one was a type check and
1798
+ * nothing more. Measured on `ModelInstance3D`:
1799
+ *
1800
+ * ```
1801
+ * targetHeight: -1.8 NO ERROR (kept -1.8)
1802
+ * metalness: 5 NO ERROR (kept 5)
1803
+ * roughness: -3 NO ERROR (kept -3)
1804
+ * ```
1805
+ *
1806
+ * The two failure modes differ, which is why neither is noticed. A negative
1807
+ * `targetHeight` folds into the same branch as "not set", so the model keeps
1808
+ * its authored size and looks like the prop did nothing; `metalness: 5` reaches
1809
+ * three and the surface goes wrong. An author who mistypes a decimal point gets
1810
+ * a different wrong answer depending on which prop they mistyped it in, and no
1811
+ * message in either case.
1812
+ */
1813
+ interface PropRange {
1814
+ /** Lowest accepted value. */
1815
+ min?: number;
1816
+ /** Highest accepted value. */
1817
+ max?: number;
1818
+ /** `min` is a bound the value must EXCEED, not reach (`targetHeight > 0`). */
1819
+ exclusiveMin?: boolean;
1820
+ /**
1821
+ * Values outside the range that mean something anyway.
1822
+ *
1823
+ * `metalness: -1` is "keep whatever the GLB authored" — a real value with a
1824
+ * real meaning, and the reason a range cannot just be a clamp.
1825
+ */
1826
+ allow?: readonly number[];
1827
+ }
1828
+ //#endregion
1783
1829
  //#region src/core/registry.d.ts
1784
1830
  /** A node prop definition. The default both documents and types the prop. */
1785
1831
  interface PropDef {
@@ -1808,6 +1854,38 @@ interface PropDef {
1808
1854
  */
1809
1855
  required?: boolean;
1810
1856
  /**
1857
+ * This prop names an entry in the scene's `assets{}` block.
1858
+ *
1859
+ * Declaring it is what makes a typo a LOAD error. The check already existed —
1860
+ * `AssetStore2D.resolve` throws `UNKNOWN_ASSET` — and it runs inside the
1861
+ * renderer, in a browser, when the texture is first needed. Headless the
1862
+ * scene loaded clean, so `incanto-check` certified a sprite that will draw
1863
+ * nothing, and the docs' promise that "the engine hard-fails on a raw URL"
1864
+ * was not true anywhere an author could see it.
1865
+ *
1866
+ * The same story as `nodePath` and `required`: the rule existed, nothing
1867
+ * routed to it at author time.
1868
+ */
1869
+ /**
1870
+ * The numeric range this prop documents.
1871
+ *
1872
+ * Without it a documented range is prose only: `metalness: 5` and
1873
+ * `targetHeight: -1.8` both passed the kind check and then failed in two
1874
+ * different silent ways. See prop-range.
1875
+ */
1876
+ range?: PropRange;
1877
+ asset?: {
1878
+ /** The `assets{}` entry `type` a `$ref` here must point at. */kind: "texture" | "spritesheet" | "model" | "animation";
1879
+ /**
1880
+ * A value that is not a `$ref` is an error.
1881
+ *
1882
+ * False for props that legitimately take something else — a raw URL
1883
+ * (`UiImage.src`, `Sprite3D.texture`) or the name of a clip baked into a
1884
+ * GLB (`ModelInstance3D.animation`).
1885
+ */
1886
+ refOnly?: boolean;
1887
+ };
1888
+ /**
1811
1889
  * For an OBJECT-valued prop shaped as a tagged union — `collider`, whose
1812
1890
  * `shape` decides which other keys are required. One declaration gives an
1813
1891
  * inspector both the dropdown AND something valid to write when the tag
@@ -1943,4 +2021,4 @@ declare function registeredBehaviors(): string[];
1943
2021
  /** Test isolation helper. */
1944
2022
  declare function clearBehaviors(): void;
1945
2023
  //#endregion
1946
- export { T_PREFIX as $, SettingsValues as A, Node as At, captureBehaviors as B, EngineStats as C, synthSfx as Ct, QualityCaps as D, PlayMusicOptions as Dt, DeviceHints as E, MusicTrack as Et, BehaviorState as F, LogManager as Ft, ORDER_GROUP_BASE as G, savesWithoutUid as H, RestoreReport as I, Signal as It, effectiveOrder as J, OrderGroup as K, SaveSlot as L, SignalListener as Lt, readDeviceHints as M, SceneTree as Mt, suggestQuality as N, LogEntry as Nt, QualityTier as O, AudioBuses as Ot, Scene as P, LogLevel as Pt, Localization as Q, SaveSlots as R, Scheduler as S, SynthOptions as St, RendererStats as T, MusicManager as Tt, SaveStore as U, restoreBehaviors as V, createSaveStore as W, BASE_LOCALE as X, resolveOrderGroups as Y, LocaleTables as Z, mergeStaticSignals as _, spatialPan as _t, clearBehaviors as a, EffectLog as at, Engine as b, SfxParams as bt, registeredBehaviors as c, isAudioContextAvailable as ct, PropSchema as d, Listener as dt, suggestLocale as et, clearRegistry as f, ROLLOFF_MODELS as ft, getNodeType as g, spatialGain as gt, getNodeSignals as h, Vec3 as ht, behaviorSignals as i, EffectKind as it, qualityEnvironment as j, NodeLifecycle as jt, Settings as k, BusName as kt, NodeCtor as l, Voice as lt, getNodeSchema as m, SpatialParams as mt, BehaviorCtor as n, InputMap as nt, getBehavior as o, SfxEngine as ot, createNode as p, RolloffModel as pt, OrderGroupTable as q, behaviorSchema as r, EffectEvent as rt, registerBehavior as s, SfxPlayOptions as st, Behavior as t, translationKey as tt, PropDef as u, VoicePreset as ut, registerNode as v, SFX_PRESETS as vt, GameStats as w, MusicBackend as wt, EngineOptions as x, SfxWave as xt, registeredTypes as y, SFX_PRESET_NAMES as yt, behaviorsWithoutSave as z };
2024
+ export { T_PREFIX as $, SettingsValues as A, BusName as At, captureBehaviors as B, EngineStats as C, sfxDuration as Ct, QualityCaps as D, MusicTrack as Dt, DeviceHints as E, MusicManager as Et, BehaviorState as F, LogLevel as Ft, ORDER_GROUP_BASE as G, savesWithoutUid as H, RestoreReport as I, LogManager as It, effectiveOrder as J, OrderGroup as K, SaveSlot as L, Signal as Lt, readDeviceHints as M, NodeLifecycle as Mt, suggestQuality as N, SceneTree as Nt, QualityTier as O, PlayMusicOptions as Ot, Scene as P, LogEntry as Pt, Localization as Q, SaveSlots as R, SignalListener as Rt, Scheduler as S, SynthOptions as St, RendererStats as T, MusicBackend as Tt, SaveStore as U, restoreBehaviors as V, createSaveStore as W, BASE_LOCALE as X, resolveOrderGroups as Y, LocaleTables as Z, mergeStaticSignals as _, spatialPan as _t, clearBehaviors as a, EffectLog as at, Engine as b, SfxParams as bt, registeredBehaviors as c, isAudioContextAvailable as ct, PropSchema as d, Listener as dt, suggestLocale as et, clearRegistry as f, ROLLOFF_MODELS as ft, getNodeType as g, spatialGain as gt, getNodeSignals as h, Vec3 as ht, behaviorSignals as i, EffectKind as it, qualityEnvironment as j, Node as jt, Settings as k, AudioBuses as kt, NodeCtor as l, Voice as lt, getNodeSchema as m, SpatialParams as mt, BehaviorCtor as n, InputMap as nt, getBehavior as o, SfxEngine as ot, createNode as p, RolloffModel as pt, OrderGroupTable as q, behaviorSchema as r, EffectEvent as rt, registerBehavior as s, SfxPlayOptions as st, Behavior as t, translationKey as tt, PropDef as u, VoicePreset as ut, registerNode as v, SFX_PRESETS as vt, GameStats as w, synthSfx as wt, EngineOptions as x, SfxWave as xt, registeredTypes as y, SFX_PRESET_NAMES as yt, behaviorsWithoutSave as z };
@@ -1,14 +1,14 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { c as resolveViewport, j as registerBehavior, n as loadScene, s as computeViewport, w as diagnose } from "./loader-D8n7TU8W.js";
3
- import { _ as Engine, g as AudioPlayer } from "./register-CDrAQqPp.js";
2
+ import { d as computeViewport, f as resolveViewport, k as diagnose, n as loadScene, z as registerBehavior } from "./loader-D7jTvDQv.js";
3
+ import { _ as Engine, g as AudioPlayer } from "./register-xuSRyD6b.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-DESwnpOc.js";
6
- import { a as openBundledEditor, c as claimCanvasGestures, i as devServerLibrary, l as audioErrors, n as pauseWhenHidden, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-RApWnM1G.js";
6
+ import { a as openBundledEditor, c as claimCanvasGestures, i as devServerLibrary, l as audioErrors, n as pauseWhenHidden, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-B6rwJOyS.js";
7
7
  import { o as frameStats } from "./frame-report-BSMny7oe.js";
8
- import { n as registerGameplayBehaviors } from "./gameplay-BBEjPFsR.js";
9
- import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-BpFcgdcL.js";
8
+ import { n as registerGameplayBehaviors } from "./gameplay-Dtzd2itW.js";
9
+ import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-DL3izw8j.js";
10
10
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
11
- import { n as enablePhysics2D } from "./physics-2d-BaRSRrrZ.js";
11
+ import { n as enablePhysics2D } from "./physics-2d-CBnor8Zf.js";
12
12
  import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
13
13
  //#region src/2d/assets.ts
14
14
  /**
@@ -199,14 +199,19 @@ function selectionOutline2D(node) {
199
199
  function createSync2DScratch() {
200
200
  return {
201
201
  visited: /* @__PURE__ */ new Set(),
202
- cameras: []
202
+ cameras: [],
203
+ emitters: []
203
204
  };
204
205
  }
206
+ function isSpatialConsumer(node) {
207
+ return node.spatial === true && typeof node._setSpatialPose === "function";
208
+ }
205
209
  function syncTree2D(root, world, ui, assets, uiSize, scratch, opts) {
206
210
  const s = scratch ?? createSync2DScratch();
207
211
  s.visited.clear();
208
212
  s.cameras.length = 0;
209
- walk(root, world, ui, false, s.visited, s.cameras, assets, uiSize, opts?.ignoreStatic === true);
213
+ s.emitters.length = 0;
214
+ walk(root, world, ui, false, s.visited, s.cameras, s.emitters, assets, uiSize, opts?.ignoreStatic === true);
210
215
  prune(world, s.visited);
211
216
  prune(ui, s.visited);
212
217
  let current = null;
@@ -218,9 +223,66 @@ function syncTree2D(root, world, ui, assets, uiSize, scratch, opts) {
218
223
  }
219
224
  }
220
225
  if (!current) current = s.cameras[0] ?? null;
226
+ feedSpatialEmitters2D(s.emitters, current);
221
227
  return { activeCamera: current };
222
228
  }
223
- function walk(node, parentObj, ui, inUi, visited, cameras, assets, uiSize, ignoreStatic) {
229
+ const emitterScratch = new Vector3();
230
+ const listenerScratch = new Vector3();
231
+ const upScratch = new Vector3();
232
+ /**
233
+ * Give a 2D scene's spatial AudioPlayers the same per-frame pose the 3D sync
234
+ * gives its own.
235
+ *
236
+ * `spatial`, `refDistance`, `maxDistance` and `rolloff` have been props of
237
+ * `AudioPlayer` since positional audio landed, and in a 2D scene all four did
238
+ * NOTHING: the node only attenuates when an adapter pushes it a pose, and only
239
+ * the 3D one ever did. The scene loaded, `incanto check` said valid, and the
240
+ * sound played at full volume from the far side of the level.
241
+ *
242
+ * The listener is the active `Camera2D` — its world position is the view
243
+ * centre, its own rotation turns the stereo image with it, and the view looks
244
+ * down −Z like every other camera. Distances are therefore in the same units
245
+ * the rest of a 2D scene is in: PIXELS. `refDistance: 1` / `maxDistance: 50`
246
+ * are metre-shaped defaults and read as "silent one sprite away" here, so a 2D
247
+ * scene has to set them; `auditScene` says so when it sees the defaults.
248
+ */
249
+ function feedSpatialEmitters2D(emitters, camera) {
250
+ if (emitters.length === 0 || !camera) return;
251
+ const view = camera._ensureObject2D();
252
+ view.updateWorldMatrix(true, false);
253
+ view.getWorldPosition(listenerScratch);
254
+ upScratch.set(0, 1, 0).applyQuaternion(view.quaternion);
255
+ const listener = {
256
+ position: [
257
+ listenerScratch.x,
258
+ listenerScratch.y,
259
+ 0
260
+ ],
261
+ forward: [
262
+ 0,
263
+ 0,
264
+ -1
265
+ ],
266
+ up: [
267
+ upScratch.x,
268
+ upScratch.y,
269
+ upScratch.z
270
+ ]
271
+ };
272
+ for (const { node, parent } of emitters) {
273
+ parent.updateWorldMatrix(true, false);
274
+ parent.getWorldPosition(emitterScratch);
275
+ node._setSpatialPose({
276
+ position: [
277
+ emitterScratch.x,
278
+ emitterScratch.y,
279
+ 0
280
+ ],
281
+ listener
282
+ });
283
+ }
284
+ }
285
+ function walk(node, parentObj, ui, inUi, visited, cameras, emitters, assets, uiSize, ignoreStatic) {
224
286
  let nextParent = parentObj;
225
287
  let nextInUi = inUi;
226
288
  if (node instanceof Node2D && !(node instanceof UILayer)) {
@@ -249,8 +311,11 @@ function walk(node, parentObj, ui, inUi, visited, cameras, assets, uiSize, ignor
249
311
  visited.add(obj);
250
312
  if (node instanceof Camera2D && !inUi) cameras.push(node);
251
313
  nextParent = obj;
252
- }
253
- for (const child of node.children) walk(child, nextParent, ui, nextInUi, visited, cameras, assets, uiSize, ignoreStatic);
314
+ } else if (isSpatialConsumer(node)) emitters.push({
315
+ node,
316
+ parent: nextParent
317
+ });
318
+ for (const child of node.children) walk(child, nextParent, ui, nextInUi, visited, cameras, emitters, assets, uiSize, ignoreStatic);
254
319
  if (node instanceof Node2D && !(node instanceof UILayer)) {
255
320
  const obj = node._ensureObject2D();
256
321
  if (node.static && !ignoreStatic) {