incanto 0.60.0 → 0.62.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 (68) hide show
  1. package/bin/incanto-check.mjs +13 -0
  2. package/bin/incanto-verify.mjs +134 -55
  3. package/dist/2d.d.ts +64 -3
  4. package/dist/2d.js +3 -3
  5. package/dist/3d.d.ts +5 -5
  6. package/dist/3d.js +5 -4
  7. package/dist/{pathfinding-BqWBb0kh.d.ts → audio-player-D5GJgb_x.d.ts} +103 -38
  8. package/dist/{behavior-DWKTUzKI.d.ts → behavior-DsgayMsH.d.ts} +79 -1
  9. package/dist/{create-game-ClnIb_M5.js → create-game-BpunnGPX.js} +78 -13
  10. package/dist/{create-game-BCm38FJV.js → create-game-Caut3bqN.js} +21 -294
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/debug.js +1 -1
  13. package/dist/{duplicate-DJQd44CD.js → duplicate-E4FUs5Bn.js} +1 -1
  14. package/dist/editor.js +41 -19
  15. package/dist/{environment-presets-8cjF3t6w.js → environment-presets-BAWeOeqf.js} +50 -13
  16. package/dist/{gameplay-BBEjPFsR.js → gameplay-CaHqDiQD.js} +177 -24
  17. package/dist/gameplay.d.ts +1 -1
  18. package/dist/gameplay.js +1 -1
  19. package/dist/index.d.ts +97 -6
  20. package/dist/index.js +7 -7
  21. package/dist/{loader-D8n7TU8W.js → loader-DEe272nY.js} +900 -2
  22. package/dist/{loader-TvkRFbyL.d.ts → loader-DolLJWJn.d.ts} +13 -1
  23. package/dist/net.d.ts +2 -2
  24. package/dist/net.js +1 -1
  25. package/dist/pathfinding-_fGrCFmH.d.ts +28 -0
  26. package/dist/{physics-2d-DqdVp1bt.js → physics-2d-BXmu2i7W.js} +11 -3
  27. package/dist/{physics-3d-BP0DZb_1.js → physics-3d-ClxP6Uv7.js} +121 -17
  28. package/dist/react.d.ts +1 -1
  29. package/dist/react.js +1 -1
  30. package/dist/{register-BSu2dWGC.js → register-CNh4FlbD.js} +104 -16
  31. package/dist/{register-Da3hXh2H.js → register-D3yx8D4r.js} +228 -16
  32. package/dist/{registry-WWcQcfMr.js → registry-CF70EArN.js} +55 -3
  33. package/dist/{replay-BCMK_VRP.d.ts → replay-C5x2vPF5.d.ts} +2 -2
  34. package/dist/{replay-BlNuIDdg.js → replay-DlgHItNv.js} +57 -197
  35. package/dist/{split-screen-CL5Yvxse.js → split-screen-CBM9wcMX.js} +3 -3
  36. package/dist/{split-screen-BQ3tAsf-.d.ts → split-screen-D7OopelJ.d.ts} +2 -2
  37. package/dist/{src-DFpXBMJN.js → src-CH00_JsR.js} +1 -1
  38. package/dist/{teardown-CTTwhWSe.js → teardown-C7qP-dcC.js} +1 -1
  39. package/dist/{test-BeZ95pqw.js → test-BMkg8zMV.js} +29 -16
  40. package/dist/test.d.ts +45 -4
  41. package/dist/test.js +2 -2
  42. package/dist/vite.js +2 -2
  43. package/editor/assets/{agent8-DSJries_.js → agent8-m5mtAO_A.js} +1 -1
  44. package/editor/assets/{debug-D15Wi5TO.js → debug-CPhzCT8f.js} +1 -1
  45. package/editor/assets/{index-BjC88k97.js → index-D422P4kW.js} +92 -92
  46. package/editor/index.html +1 -1
  47. package/package.json +1 -1
  48. package/schemas/scene.schema.json +91 -0
  49. package/skills/incanto-3d-models.md +1 -1
  50. package/skills/incanto-assets.md +10 -1
  51. package/skills/incanto-audio.md +21 -13
  52. package/skills/incanto-behaviors-and-scripts.md +44 -3
  53. package/skills/incanto-building-2d-games.md +32 -4
  54. package/skills/incanto-building-3d-games.md +1 -1
  55. package/skills/incanto-editor.md +1 -1
  56. package/skills/incanto-gameplay-behaviors.md +39 -2
  57. package/skills/incanto-hud.md +5 -3
  58. package/skills/incanto-localization.md +7 -0
  59. package/skills/incanto-node-reference.md +12 -0
  60. package/skills/incanto-physics-and-input.md +23 -2
  61. package/skills/incanto-scene-json-authoring.md +6 -1
  62. package/skills/incanto-verifying-your-game.md +31 -1
  63. package/templates-app/beacon-isle-3d/package.json +1 -1
  64. package/templates-app/platformer-2d/package.json +1 -1
  65. package/templates-app/star-survivor/package.json +1 -1
  66. package/templates-app/tps-3d/package.json +1 -1
  67. package/templates-app/village-quest-3d/package.json +1 -1
  68. package/dist/particle-sim-C5OfBbmU.d.ts +0 -77
@@ -167,6 +167,15 @@ const { auditScene, validateScene } = await import(pathToFileURL(join(DIST, 'tes
167
167
  * knowing what the project declares.
168
168
  */
169
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 = {};
170
179
  for (const file of files) {
171
180
  try {
172
181
  const doc = JSON.parse(readFileSync(file, 'utf-8'));
@@ -174,6 +183,9 @@ for (const file of files) {
174
183
  if (typeof entries !== 'object' || entries === null) continue;
175
184
  declaredElsewhere[locale] = { ...(declaredElsewhere[locale] ?? {}), ...entries };
176
185
  }
186
+ for (const [key, entry] of Object.entries(doc?.assets ?? {})) {
187
+ if (typeof entry === 'object' && entry !== null) declaredAssets[key] ??= entry;
188
+ }
177
189
  } catch {
178
190
  /* a file that will not parse is reported below, on its own line */
179
191
  }
@@ -188,6 +200,7 @@ const results = files.map((file) => {
188
200
  }
189
201
  const res = validateScene(json, {
190
202
  strictBehaviors: args.strict,
203
+ declaredAssets,
191
204
  resolveScene: (p) => JSON.parse(readFileSync(resolve(dirname(file), p), 'utf-8')),
192
205
  });
193
206
  if (res.ok) {
@@ -147,9 +147,24 @@ const rungs = [];
147
147
  // the legal values sitting one key away in the same object.
148
148
  const broke = out?.results?.find((entry) => entry && entry.ok === false);
149
149
  const detail = broke && (broke.message || broke.code) ? `[${broke.code}] ${broke.message}` : null;
150
+ // What `incanto-check` SAID, beyond whether it passed. The rung read `ok` and
151
+ // dropped `warnings` on the floor, so a scene the child describes as "nothing
152
+ // lights this 3D scene … It will render black" was reported by the headline
153
+ // command as "✓ loads — the scene is legal and its assets resolve". The
154
+ // ladder's own header table names this tool as the one for "the scene will
155
+ // not load, OR RENDERS BLACK".
156
+ const notes = (out?.results ?? []).flatMap((entry) => entry?.warnings ?? []);
157
+ // "and its assets resolve" is a claim, and `missing art` is the check saying
158
+ // it does not. Do not make the claim when the child just contradicted it.
159
+ const missingArt = notes.some((n) => /is not in the project|missing|not found/i.test(String(n)));
150
160
  rungs.push(
151
161
  r.status === 0
152
- ? { name: 'loads', status: 'pass', summary: 'the scene is legal and its assets resolve' }
162
+ ? {
163
+ name: 'loads',
164
+ status: 'pass',
165
+ summary: missingArt ? 'the scene is legal' : 'the scene is legal and its assets resolve',
166
+ ...(notes.length > 0 ? { notes, fix: `incanto-check ${scene}` } : {}),
167
+ }
153
168
  : {
154
169
  name: 'loads',
155
170
  status: 'fail',
@@ -175,62 +190,126 @@ if (rungs[0].status === 'pass') {
175
190
  // walkabout template has no end, and calling that a failure sends its author
176
191
  // hunting a bug that was never there.
177
192
  const noGoal = out && out.declaresWin === false;
178
- // A quest — talk to the NPC, clear the enemies, light the wards cannot be
179
- // finished by a random walker, ever. Reporting that as a FAILED rung means
180
- // the headline command permanently says NOT verified about a correct game,
181
- // which teaches its author to stop reading it. If every run played to the
182
- // end of its budget without erroring, falling or wedging, the rung has not
183
- // failed: it has not measured, and the author's own scripted harness is what
184
- // can judge this game.
185
- // Three of the five outcomes are GAMEPLAY, not defects. `won`, `lost` and
193
+ // Three of the six outcomes are GAMEPLAY, not defects. `won`, `lost` and
186
194
  // `unfinished` all mean the game ran; a random player dying half the time in
187
- // a platformer is the hazards working. The defects are `error` (a behaviour
188
- // threw), `fell` (left the world) and `stuck` (went nowhere), and those are
189
- // what this rung is for.
190
- const PLAYED = new Set(['won', 'lost', 'unfinished']);
191
- const playedOut =
192
- out && (out.runs?.length ?? 0) > 0 && out.runs.every((x) => PLAYED.has(x.outcome));
195
+ // a platformer is the hazards working, and a quest cannot be finished by a
196
+ // random walker ever reporting that as a FAILED rung means the headline
197
+ // command permanently says NOT verified about a correct game, which teaches
198
+ // its author to stop reading it.
199
+ //
200
+ // The other three are DEFECTS: `error` (a behaviour threw), `fell` (left the
201
+ // world) and `stuck` (went nowhere). They are counted BEFORE a branch is
202
+ // chosen, and they used to be counted after — only on the path where a win
203
+ // existed to miss. A scene that declares no win took the `noGoal` branch for
204
+ // ANY mix of outcomes, and that branch's summary was the hard-coded phrase
205
+ // "N runs played without error": an assumption wearing the clothes of a
206
+ // measurement. Measured on the shipped `basic-3d-sideview`, whose player
207
+ // walks off the edge on every seeded run:
208
+ //
209
+ // They used to be counted after, and only on the path where a win existed to
210
+ // miss. A scene that declares no win took the `noGoal` branch for ANY mix of
211
+ // outcomes, and that branch's summary was the hard-coded phrase "N runs
212
+ // played without error" — an assumption wearing the clothes of a
213
+ // measurement. Measured on the shipped `basic-3d-sideview`, whose player
214
+ // walks off the edge on every seeded run:
215
+ //
216
+ // incanto-playtest → ✗ fell in 4/4 (last at y=-48.4)
217
+ // incanto-verify → ? plays — nothing declares a win — 8 runs played
218
+ // without error, and there was no end to reach
219
+ // passes what was measured … VERIFY EXIT=0
220
+ //
221
+ // The child had just printed the defect; the parent reported the opposite of
222
+ // it and exited 0. The three outcomes this rung exists to catch were the
223
+ // three it could not report.
224
+ //
225
+ // `stuck` is the one that needs a fair chance before it counts. It means the
226
+ // player went nowhere, and the player goes nowhere for three different
227
+ // reasons: it is wedged (a defect), there IS no player (`2d-phaser-basic` is
228
+ // a ground, a sprite and a camera — the driver had nobody to move), or the
229
+ // game's own behaviours were never loaded, which is most of what makes a
230
+ // character move. Only the first is this rung's business.
193
231
  const tally = (name) => out?.runs?.filter((x) => x.outcome === name).length ?? 0;
194
- rungs.push(
195
- r.status === 0
196
- ? { name: 'plays', status: 'pass', summary: `${won} of ${total} seeded runs finished it` }
197
- : playedOut
198
- ? {
199
- name: 'plays',
200
- status: 'unmeasured',
201
- summary:
202
- `${total} runs played without reaching a win` +
203
- ` (${[
204
- tally('lost') && `${tally('lost')} lost`,
205
- tally('unfinished') && `${tally('unfinished')} ran out the clock`,
206
- ]
207
- .filter(Boolean)
208
- .join(', ')})`,
209
- fix: 'nothing here is broken — a win that takes skill or a sequence is out of reach of random play. Judge it with a scripted run: `bun run verify`, or `runScript` from `incanto/test`',
210
- }
211
- : noGoal
212
- ? {
213
- name: 'plays',
214
- status: 'unmeasured',
215
- summary: `nothing declares a win — ${total} runs played without error, and there was no end to reach`,
216
- fix: 'if it is meant to be finishable, emit `won` (GameFlow, ScoreKeeper, or your own behaviour)',
217
- }
218
- : {
219
- name: 'plays',
220
- status: 'fail',
221
- summary:
222
- total > 0
223
- ? `no run finished it (${total} tried)`
224
- : // "could not run" with no reason is the tool doing to its
225
- // reader exactly what this whole ladder exists to prevent:
226
- // reporting a failure it already knows the cause of. The
227
- // child printed one; pass it on.
228
- `the playtest could not run — ${firstLine(r.stderr) || `exit ${r.status}`}`,
229
- fix: behaviors
230
- ? `see which runs stalled and where: \`incanto-playtest ${scene} --behaviors ${behaviors}\``
231
- : `run it with your behaviourswithout them the structure plays and your game logic does not: \`incanto-playtest ${scene} --behaviors src/behaviors.ts\``,
232
- },
233
- );
232
+ const stuckCounts = out?.hasPlayer !== false && Boolean(behaviors);
233
+ const DEFECT = new Set(stuckCounts ? ['error', 'fell', 'stuck'] : ['error', 'fell']);
234
+ const defects = out?.runs?.filter((x) => DEFECT.has(x.outcome)) ?? [];
235
+ const undrivable = total > 0 && defects.length === 0 && tally('stuck') === total;
236
+ const count = (n, word) => n && `${n} ${word}`;
237
+
238
+ if (total === 0) {
239
+ rungs.push({
240
+ name: 'plays',
241
+ status: 'fail',
242
+ // "could not run" with no reason is the tool doing to its reader exactly
243
+ // what this whole ladder exists to prevent: reporting a failure it
244
+ // already knows the cause of. The child printed one; pass it on.
245
+ summary: `the playtest could not run — ${firstLine(r.stderr) || `exit ${r.status}`}`,
246
+ fix: behaviors
247
+ ? `see it directly: \`incanto-playtest ${scene} --behaviors ${behaviors}\``
248
+ : `run it with your behaviours: \`incanto-playtest ${scene} --behaviors src/behaviors.ts\``,
249
+ });
250
+ } else if (defects.length > 0) {
251
+ rungs.push({
252
+ name: 'plays',
253
+ status: 'fail',
254
+ summary:
255
+ `${defects.length} of ${total} runs ended in a defect (` +
256
+ [
257
+ count(tally('fell'), 'fell out of the world'),
258
+ count(tally('error'), 'errored'),
259
+ count(tally('stuck'), 'went nowhere'),
260
+ ]
261
+ .filter(Boolean)
262
+ .join(', ') +
263
+ ')' +
264
+ (won > 0 ? ` ${won} did finish it` : ''),
265
+ fix:
266
+ tally('error') > 0
267
+ ? `read the throw: \`incanto-playtest ${scene}${behaviors ? ` --behaviors ${behaviors}` : ''}\`, and \`engine.stats().errors\` in your own harness`
268
+ : tally('fell') > 0
269
+ ? `the player left the world with nothing catching them give the level a floor, walls, or a respawn (\`incanto-playtest ${scene}${behaviors ? ` --behaviors ${behaviors}` : ''}\` prints where)`
270
+ : `nothing moved — check the input map and the controller: \`incanto-playtest ${scene}${behaviors ? ` --behaviors ${behaviors}` : ''}\``,
271
+ });
272
+ } else if (r.status === 0) {
273
+ rungs.push({
274
+ name: 'plays',
275
+ status: 'pass',
276
+ summary: `${won} of ${total} seeded runs finished it`,
277
+ });
278
+ } else if (undrivable) {
279
+ rungs.push({
280
+ name: 'plays',
281
+ status: 'unmeasured',
282
+ summary:
283
+ out?.hasPlayer === false
284
+ ? `nothing here is drivable — ${total} runs had nobody to move`
285
+ : `${total} runs went nowhere, and your behaviours were not loaded — that is most of what moves a character`,
286
+ fix:
287
+ out?.hasPlayer === false
288
+ ? 'give the player a character controller, the `player` group, or the name Player — otherwise no seeded run can play this'
289
+ : `name them: \`incanto-verify ${scene} --behaviors src/behaviors.ts\``,
290
+ });
291
+ } else if (noGoal) {
292
+ rungs.push({
293
+ name: 'plays',
294
+ status: 'unmeasured',
295
+ // Now this phrase IS measured: `defects.length === 0` got us here.
296
+ summary: `nothing declares a win — ${total} runs played without a defect, and there was no end to reach`,
297
+ fix: 'if it is meant to be finishable, emit `won` (GameFlow, ScoreKeeper, or your own behaviour)',
298
+ });
299
+ } else {
300
+ // A quest — talk to the NPC, clear the enemies, light the wards — cannot be
301
+ // finished by a random walker, ever. Reporting that as a FAILED rung means
302
+ // the headline command permanently says NOT verified about a correct game,
303
+ // which teaches its author to stop reading it.
304
+ rungs.push({
305
+ name: 'plays',
306
+ status: 'unmeasured',
307
+ summary:
308
+ `${total} runs played without reaching a win` +
309
+ ` (${[count(tally('lost'), 'lost'), count(tally('unfinished'), 'ran out the clock')].filter(Boolean).join(', ')})`,
310
+ fix: 'nothing here is broken — a win that takes skill or a sequence is out of reach of random play. Judge it with a scripted run: `bun run verify`, or `runScript` from `incanto/test`',
311
+ });
312
+ }
234
313
  } else {
235
314
  rungs.push({ name: 'plays', status: 'skipped', summary: 'not run — the scene does not load' });
236
315
  }
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
 
@@ -90,6 +90,28 @@ declare class Node2D extends Node {
90
90
  /** @internal Override point. */
91
91
  protected _createObject2D(): Object3D;
92
92
  /** @internal Push JSON props onto the backing object. Called every frame. */
93
+ /**
94
+ * Has this node produced the thing `static: true` is about to freeze?
95
+ *
96
+ * `static` latches after the first sync, and a textured node's first sync
97
+ * happens BEFORE its texture has decoded — `TextureLoader.load()` returns a
98
+ * `Texture` with `image === undefined` and fills it asynchronously, while
99
+ * `Renderer2D.render()` calls `assets.load()` and `syncTree2D()` in the same
100
+ * frame. So the state frozen was `visible = false`, forever. Measured on two
101
+ * identical sprites differing only in `static`:
102
+ *
103
+ * ```
104
+ * frame 1 Backdrop(static) visible: false Control visible: false
105
+ * 600 frames later Backdrop(static) visible: false width 1
106
+ * Control visible: true width 64
107
+ * ```
108
+ *
109
+ * Nothing throws: the texture loaded fine, so `assetErrors()` is empty,
110
+ * `stats().errors` is 0, and `framing` reads props rather than pixels and
111
+ * still calls the node on-screen. Overridden by the nodes that wait on an
112
+ * image; a node with nothing to wait for is ready by definition.
113
+ */
114
+ _staticReady(_assets: AssetStore2D | null): boolean;
93
115
  _syncObject2D(_assets: AssetStore2D | null): void;
94
116
  override free(): void;
95
117
  }
@@ -498,6 +520,7 @@ declare class Sprite2D extends Node2D {
498
520
  protected override _createObject2D(): Object3D;
499
521
  /** Override point: AnimatedSprite2D substitutes its frame window here. */
500
522
  protected resolveTexture(assets: AssetStore2D | null): ResolvedSpriteTexture | null;
523
+ override _staticReady(assets: AssetStore2D | null): boolean;
501
524
  override _syncObject2D(assets: AssetStore2D | null): void;
502
525
  }
503
526
  //#endregion
@@ -568,6 +591,24 @@ declare class Camera2D extends Node2D {
568
591
  get effectiveZoom(): number;
569
592
  override update(dt: number): void;
570
593
  /** View center after clamping the (vw×vh)/zoom view rect inside `limits`. */
594
+ /**
595
+ * The view centre the renderer draws around, in WORLD space.
596
+ *
597
+ * It used to read `this.position` raw — the LOCAL prop — while every other
598
+ * 2D node gets its ancestors composed for free by the three scene graph. So a
599
+ * camera parented to the player, the Godot/Phaser idiom the authoring skill
600
+ * explicitly permits, framed the world origin:
601
+ *
602
+ * ```
603
+ * player world position : [1400, 900]
604
+ * renderer view centre : {"x":0,"y":0}
605
+ * framing : camera /Level/Player/Cam centred [1400, 900]
606
+ * 1 in view, 0 outside it
607
+ * ```
608
+ *
609
+ * `framing` composes full world matrices, so the instrument the skills tell
610
+ * you to trust certified a view the renderer never drew.
611
+ */
571
612
  clampedCenter(vw: number, vh: number): {
572
613
  x: number;
573
614
  y: number;
@@ -701,6 +742,16 @@ declare class Label extends Node2D {
701
742
  font: string;
702
743
  /** 'left' | 'center' | 'right' — anchor of the text block on the node origin. */
703
744
  align: string;
745
+ /**
746
+ * 0..1, like `Sprite2D` and `ColorRect2D`.
747
+ *
748
+ * Label was the one 2D drawable without it, and the gap showed up through
749
+ * `FloatAway` — whose own doc names "a 2D `Label`" as a node it works on.
750
+ * `FloatAway` writes `opacity` when the node has one, so on a Label the
751
+ * number rose to full height at FULL opacity and then vanished in a single
752
+ * frame: the pop that behavior exists to avoid.
753
+ */
754
+ opacity: number;
704
755
  private quadMesh;
705
756
  private canvas;
706
757
  private lastKey;
@@ -872,6 +923,7 @@ declare class TileMap2D extends Node2D {
872
923
  private rebuildColliders;
873
924
  /** @internal The drawable mesh (lazily created under the backing object). */
874
925
  _mesh(): Mesh;
926
+ override _staticReady(assets: AssetStore2D | null): boolean;
875
927
  override _syncObject2D(assets: AssetStore2D | null): void;
876
928
  /** One quad per visible tile, atlas UVs — a single draw call for the level. */
877
929
  private buildGeometry;
@@ -1081,6 +1133,15 @@ interface UiSize {
1081
1133
  interface Sync2DScratch {
1082
1134
  visited: Set<Object3D>;
1083
1135
  cameras: Camera2D[];
1136
+ emitters: Array<{
1137
+ node: Node & SpatialConsumer;
1138
+ parent: Object3D;
1139
+ }>;
1140
+ }
1141
+ /** A node that accepts a per-frame spatial pose (an AudioPlayer with spatial on). */
1142
+ interface SpatialConsumer {
1143
+ spatial: boolean;
1144
+ _setSpatialPose(pose: SpatialPose): void;
1084
1145
  }
1085
1146
  declare function syncTree2D(root: Node, world: Scene, ui: Scene, assets: AssetStore2D | null, uiSize?: UiSize, scratch?: Sync2DScratch, opts?: {
1086
1147
  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-ClnIb_M5.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-BSu2dWGC.js";
4
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-DqdVp1bt.js";
2
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-BpunnGPX.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-CNh4FlbD.js";
4
+ import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-BXmu2i7W.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-DEe272nY.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-8cjF3t6w.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-BCm38FJV.js";
3
+ import { B as CharacterBody3D, F as Water3D, H as RigidBody3D, I as WATER_CUTOUT_MAX, L as WaterCutout3D, U as StaticBody3D, V as PhysicsBody3D, W as Node3D, q as WATER_MAX_RIPPLES, z as Area3D } from "./gameplay-CaHqDiQD.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-BAWeOeqf.js";
5
+ import { a as Environment3D, i as syncTree, o as setEnvironment3D, r as Renderer3D, s as AssetStore3D, t as createGame3D } from "./create-game-Caut3bqN.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-BP0DZb_1.js";
7
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-ClxP6Uv7.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 };