incanto 0.70.0 → 0.72.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 (74) hide show
  1. package/bin/incanto-editor.mjs +15 -9
  2. package/bin/incanto-feel.mjs +11 -1
  3. package/bin/incanto-play.mjs +11 -1
  4. package/bin/incanto-playtest.mjs +11 -1
  5. package/bin/incanto-verify.mjs +36 -98
  6. package/dist/2d.d.ts +18 -3
  7. package/dist/2d.js +3 -3
  8. package/dist/3d.d.ts +3 -3
  9. package/dist/3d.js +6 -6
  10. package/dist/{agent8-CmNF01gA.js → agent8-BqZ0_O1u.js} +1 -1
  11. package/dist/{audio-player-DaMxqfNE.d.ts → audio-player-ioZ7VKC6.d.ts} +1 -1
  12. package/dist/{behavior-DZExDn9o.d.ts → behavior-Bf9P9oB2.d.ts} +12 -1
  13. package/dist/{create-game-Bwvh6q8A.js → create-game-C9rWqqAC.js} +37 -13
  14. package/dist/{create-game-C7ffQWW7.js → create-game-CmFAtLZp.js} +8 -7
  15. package/dist/debug.d.ts +1 -1
  16. package/dist/editor.js +1083 -1045
  17. package/dist/{environment-presets-D6Q5BxeE.js → environment-presets-BeHoaGhy.js} +3 -3
  18. package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
  19. package/dist/{gameplay-BfHkuzVb.js → gameplay-BlQe-M07.js} +108 -6
  20. package/dist/gameplay.d.ts +68 -4
  21. package/dist/gameplay.js +2 -2
  22. package/dist/index.d.ts +27 -4
  23. package/dist/index.js +4 -4
  24. package/dist/{loader-Cff09LMm.d.ts → loader-CBfQzB6S.d.ts} +10 -3
  25. package/dist/net.d.ts +2 -2
  26. package/dist/net.js +2 -2
  27. package/dist/{physics-2d-CE0Qvy3V.js → physics-2d-DrIgTlLk.js} +2 -2
  28. package/dist/{physics-3d-CpH-2gn5.js → physics-3d-BzOCw1VS.js} +3 -3
  29. package/dist/{picking-CQJ_PJKh.js → picking-C9McVJrO.js} +3 -3
  30. package/dist/react.d.ts +1 -1
  31. package/dist/react.js +1 -1
  32. package/dist/{register-6DYnKZcy.js → register-5IYfooAN.js} +49 -15
  33. package/dist/{register-Bkk0wSDB.js → register-C8HuRkOf.js} +100 -8
  34. package/dist/{replay-Dmw-PKQu.d.ts → replay-CBEChqXq.d.ts} +2 -2
  35. package/dist/{replay-DjAkAzMq.js → replay-datiAE-b.js} +15 -8
  36. package/dist/{save-slots-BXVg148r.js → save-slots-CEuJPUle.js} +102 -4
  37. package/dist/{split-screen-PL78oVXP.js → split-screen-Bm5DTElG.js} +3 -3
  38. package/dist/{split-screen--k-XpBjr.d.ts → split-screen-DkqLng6q.d.ts} +2 -2
  39. package/dist/{src-Cxfiv1Hg.js → src-DRSsRleD.js} +1 -1
  40. package/dist/{test-iHYVUcDK.js → test-Djuq3Vhq.js} +252 -24
  41. package/dist/test.d.ts +113 -5
  42. package/dist/test.js +3 -3
  43. package/dist/vite.d.ts +8 -1
  44. package/dist/vite.js +23 -6
  45. package/editor/assets/{agent8-D0MS174y.js → agent8-CMKz2cax.js} +1 -1
  46. package/editor/assets/{debug-BnXkKuYu.js → debug-DLrQm-az.js} +1 -1
  47. package/editor/assets/{index-CIu3uc3l.js → index-BEKuxYcE.js} +94 -94
  48. package/editor/index.html +1 -1
  49. package/package.json +1 -1
  50. package/schemas/scene.schema.json +4 -0
  51. package/skills/incanto-behaviors-and-scripts.md +30 -9
  52. package/skills/incanto-building-2d-games.md +26 -2
  53. package/skills/incanto-editor.md +4 -0
  54. package/skills/incanto-gameplay-behaviors.md +63 -1
  55. package/skills/incanto-hud.md +14 -0
  56. package/skills/incanto-node-reference.md +18 -2
  57. package/skills/incanto-playtesting.md +34 -0
  58. package/skills/incanto-save-slots.md +56 -1
  59. package/skills/incanto-scene-json-authoring.md +30 -0
  60. package/skills/incanto-verifying-your-game.md +11 -0
  61. package/templates-app/beacon-isle-3d/package.json +1 -1
  62. package/templates-app/beacon-isle-3d/src/game.scene.json +396 -386
  63. package/templates-app/molehill-2d/package.json +1 -1
  64. package/templates-app/molehill-2d/src/game.scene.json +10 -60
  65. package/templates-app/platformer-2d/package.json +1 -1
  66. package/templates-app/platformer-2d/src/game.scene.json +268 -504
  67. package/templates-app/star-survivor/package.json +1 -1
  68. package/templates-app/star-survivor/src/game.scene.json +23 -33
  69. package/templates-app/tps-3d/package.json +1 -1
  70. package/templates-app/tps-3d/src/game.scene.json +90 -105
  71. package/templates-app/village-quest-3d/package.json +1 -1
  72. package/templates-app/village-quest-3d/src/behaviors.ts +21 -4
  73. package/templates-app/village-quest-3d/src/grove.scene.json +99 -118
  74. package/templates-app/village-quest-3d/src/village.scene.json +449 -245
@@ -30,7 +30,7 @@
30
30
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
31
31
  import { createServer } from 'node:http';
32
32
  import { basename, dirname, extname, join, normalize, relative, resolve, sep } from 'node:path';
33
- import { fileURLToPath } from 'node:url';
33
+ import { fileURLToPath, pathToFileURL } from 'node:url';
34
34
  import { readJson } from './_read-json.mjs';
35
35
 
36
36
  const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
@@ -463,7 +463,7 @@ const server = createServer((req, res) => {
463
463
  if (!originAllowed(req)) {
464
464
  return send(res, 403, JSON.stringify({ error: 'cross-origin write rejected' }));
465
465
  }
466
- readBody(req, res, (body) => {
466
+ readBody(req, res, async (body) => {
467
467
  // a path with no extension means the usual thing, so `levels/arena` works
468
468
  const wanted =
469
469
  typeof body?.path === 'string' && body.path && !body.path.endsWith('.json')
@@ -495,14 +495,20 @@ const server = createServer((req, res) => {
495
495
  */
496
496
  const asked = body?.dimension === '3d' || body?.dimension === '2d' ? body.dimension : null;
497
497
  const dimension = asked ?? projectDimension() ?? '2d';
498
- const scene = {
499
- format: 1,
500
- type: 'scene',
501
- dimension,
502
- name,
503
- root: { name, type: dimension === '3d' ? 'Node3D' : 'Node2D' },
504
- };
505
498
  try {
499
+ // uid at CREATION, like every node the editor makes: without it the
500
+ // first save backfills one and the diff carries a line nobody wrote.
501
+ // Imported here rather than at the top so starting the editor never
502
+ // pays for `dist` it may not otherwise need — inside the try, so a
503
+ // missing build is a 400 and not a dead server.
504
+ const { newUid } = await import(pathToFileURL(join(PKG_ROOT, 'dist', 'index.js')).href);
505
+ const scene = {
506
+ format: 1,
507
+ type: 'scene',
508
+ dimension,
509
+ name,
510
+ root: { name, type: dimension === '3d' ? 'Node3D' : 'Node2D', uid: newUid() },
511
+ };
506
512
  mkdirSync(dirname(abs), { recursive: true });
507
513
  writeFileSync(abs, formatSceneJson(scene));
508
514
  return send(res, 201, JSON.stringify({ rel: relative(ROOT, abs), abs }));
@@ -60,7 +60,17 @@ if (args.help || !args.scene) {
60
60
  pushed +x/-y/+y/-x until the player moves)
61
61
  --jump ACTION the button to jump with (default: "jump")
62
62
  --json the report as JSON instead of prose`);
63
- process.exit(!args.invalid && args.help && args.scene !== undefined ? 0 : 1);
63
+ /*
64
+ * `--help` is a REQUEST, and the answer is the text above.
65
+ *
66
+ * The rule used to be "0 only if a scene was also named", so the exact thing
67
+ * an agent types first — `incanto-playtest --help` — printed the usage and
68
+ * exited 1, while thirteen sibling bins exited 0 for the same input. A
69
+ * non-zero exit says "you did it wrong" to someone who did the documented
70
+ * thing, and under `set -e` it stops the script. A typo still exits 1
71
+ * (`invalid`), and so does a missing scene when help was not asked for.
72
+ */
73
+ process.exit(args.help && !args.invalid ? 0 : 1);
64
74
  }
65
75
 
66
76
  const { facingReport, facingText, feelReport, feelText } = await import(
@@ -69,7 +69,17 @@ if (args.help || !args.scene) {
69
69
 
70
70
  Headless play: stdin commands (step/press/release/vector/key/mouse/pointer/
71
71
  wheel/capture/describe/framing/logs/quit), JSON-line responses on stdout.`);
72
- process.exit(!args.invalid && args.help && args.scene !== undefined ? 0 : 1);
72
+ /*
73
+ * `--help` is a REQUEST, and the answer is the text above.
74
+ *
75
+ * The rule used to be "0 only if a scene was also named", so the exact thing
76
+ * an agent types first — `incanto-playtest --help` — printed the usage and
77
+ * exited 1, while thirteen sibling bins exited 0 for the same input. A
78
+ * non-zero exit says "you did it wrong" to someone who did the documented
79
+ * thing, and under `set -e` it stops the script. A typo still exits 1
80
+ * (`invalid`), and so does a missing scene when help was not asked for.
81
+ */
82
+ process.exit(args.help && !args.invalid ? 0 : 1);
73
83
  }
74
84
 
75
85
  const out = (obj) => process.stdout.write(`${JSON.stringify(obj)}\n`);
@@ -92,7 +92,17 @@ if (args.help || !args.scene) {
92
92
  Exits 1 when a run THREW or left the world, or when a win is declared and no
93
93
  run reached it. A scene that declares no win, or has nobody to drive, exits 0 —
94
94
  the report marks those · rather than ✗, and the exit code says the same.`);
95
- process.exit(!args.invalid && args.help && args.scene !== undefined ? 0 : 1);
95
+ /*
96
+ * `--help` is a REQUEST, and the answer is the text above.
97
+ *
98
+ * The rule used to be "0 only if a scene was also named", so the exact thing
99
+ * an agent types first — `incanto-playtest --help` — printed the usage and
100
+ * exited 1, while thirteen sibling bins exited 0 for the same input. A
101
+ * non-zero exit says "you did it wrong" to someone who did the documented
102
+ * thing, and under `set -e` it stops the script. A typo still exits 1
103
+ * (`invalid`), and so does a missing scene when help was not asked for.
104
+ */
105
+ process.exit(args.help && !args.invalid ? 0 : 1);
96
106
  }
97
107
 
98
108
  const { playtest, playtestText, playtestFailed, failingReplays } = await import(
@@ -25,7 +25,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
25
25
  import { readJson } from './_read-json.mjs';
26
26
 
27
27
  const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
28
- const { firstLine, ladderText, ladderVerdict, saysRung } = await import(
28
+ const { feelsRung, firstLine, ladderText, ladderVerdict, saysRung, unmeasuredPlays } = await import(
29
29
  pathToFileURL(join(PKG, 'dist', 'test.js')).href
30
30
  );
31
31
 
@@ -218,6 +218,9 @@ const rungs = [];
218
218
  /** The playtest's JSON, shared by the `plays` and `feels` rungs. */
219
219
  let playtestReport = null;
220
220
 
221
+ /** Did the bot get the game MOVING? Set by the `plays` rung, read by `feels`. */
222
+ let botPlayed = true;
223
+
221
224
  // ---- plays ---------------------------------------------------------------
222
225
  if (rungs[0].status === 'pass') {
223
226
  const args = [scene, '--json', '--runs', '8'];
@@ -274,6 +277,8 @@ if (rungs[0].status === 'pass') {
274
277
  const DEFECT = new Set(stuckCounts ? ['error', 'fell', 'stuck'] : ['error', 'fell']);
275
278
  const defects = out?.runs?.filter((x) => DEFECT.has(x.outcome)) ?? [];
276
279
  const undrivable = total > 0 && defects.length === 0 && tally('stuck') === total;
280
+ // …and the `feels` rung below asks the same question, so it is answered once.
281
+ botPlayed = !undrivable;
277
282
  const count = (n, word) => n && `${n} ${word}`;
278
283
 
279
284
  if (total === 0) {
@@ -343,23 +348,10 @@ if (rungs[0].status === 'pass') {
343
348
  * match-3, tower defense, cards, point-and-click, RTS, board games — has no
344
349
  * character to add, and the advice was to add one anyway.
345
350
  */
346
- const clicks = (out?.runs ?? []).reduce((n, r) => n + (r.clicks ?? 0), 0);
347
- rungs.push({
348
- name: 'plays',
349
- status: 'unmeasured',
350
- summary:
351
- clicks > 0
352
- ? `played with the MOUSE — ${clicks} click(s) across ${total} runs, and no win to reach`
353
- : out?.hasPlayer === false
354
- ? `nothing here is drivable — ${total} runs had nobody to move and nothing to click`
355
- : `${total} runs went nowhere, and your behaviours were not loaded — that is most of what moves a character`,
356
- fix:
357
- clicks > 0
358
- ? 'a mouse game is played, not walked — emit `won`/`lost` and this rung can pass'
359
- : out?.hasPlayer === false
360
- ? 'give the player a character controller, the `player` group, or the name Player — or `Clickable` on what the cursor drives'
361
- : `name them: \`incanto-verify ${scene} --behaviors src/behaviors.ts\``,
362
- });
351
+ // The wording lives in `verify-ladder.ts` (pure, and tested there)
352
+ // this branch owns only the fact that the bot could not WALK.
353
+ const said = unmeasuredPlays({ ...(out ?? {}), scene });
354
+ rungs.push({ name: 'plays', status: 'unmeasured', summary: said.summary, fix: said.fix });
363
355
  } else if (noGoal) {
364
356
  rungs.push({
365
357
  name: 'plays',
@@ -403,71 +395,10 @@ if (rungs[0].status === 'pass') {
403
395
  // Sound and effects, from the same playtest that just ran. A game whose
404
396
  // feedback is wired but never triggered plays perfectly and feels dead, and
405
397
  // every other rung here calls it healthy.
406
- {
407
- const fb = playtestReport?.feedback;
408
- const declared = (fb?.declaredAudio?.length ?? 0) + (fb?.declaredEffects?.length ?? 0);
409
- const fired = new Set([...(fb?.heard ?? []), ...(fb?.shown ?? [])]);
410
- if (!fb) {
411
- rungs.push({
412
- name: 'feels',
413
- status: 'skipped',
414
- summary: 'not run — the playtest did not report',
415
- });
416
- } else if (declared === 0) {
417
- rungs.push({
418
- name: 'feels',
419
- status: 'skipped',
420
- summary: 'this game declares no sound and no effects',
421
- });
422
- } else if (fired.size === 0) {
423
- rungs.push({
424
- name: 'feels',
425
- status: 'fail',
426
- summary: `${declared} sound/effect node(s) declared, and not one fired in any run`,
427
- fix: 'connect them — a signal to `play` on an AudioPlayer, or `replay()` on a one-shot; `engine.audio.recent()` and `engine.effects.recent()` say what did fire',
428
- });
429
- } else {
430
- const declaredPaths = [...(fb.declaredAudio ?? []), ...(fb.declaredEffects ?? [])];
431
- const silent = declaredPaths.filter((path) => !fired.has(path));
432
- // Of the nodes this rung is ABOUT, how many fired. `fired` also holds paths
433
- // nobody declared — a spawned clone sounds from its own path, a procedural
434
- // preset from wherever a behaviour played it — so using its size printed
435
- // arithmetic that cannot be true: `✓ feels — 12 of 9 fired`. The number has
436
- // to add up with the `silent` list beside it.
437
- const firedDeclared = declaredPaths.length - silent.length;
438
- /*
439
- * A rung that exists because "wired but never triggered plays perfectly and
440
- * feels dead, and every other rung here calls it healthy" — and which then
441
- * called it healthy itself. `fired.size === 0` failed and ANYTHING else
442
- * passed, so the flagship starter reported
443
- *
444
- * ✓ feels — 13 of 26 fired — silent: /Game/Ward1/ChimeSfx …
445
- *
446
- * A tick next to thirteen emitters nobody heard is the same claim the rung
447
- * was written to stop.
448
- *
449
- * It is not a FAILURE either: a random bot does not light three wards, so a
450
- * win chime it never reached is not a broken wire. That is exactly what
451
- * `unmeasured` means in this ladder — the tool did not get to look — and it
452
- * is the one status that cannot be read as approval.
453
- */
454
- rungs.push({
455
- name: 'feels',
456
- ...(silent.length === 0
457
- ? { status: 'pass', summary: `every one of the ${declared} sound/effect nodes fired` }
458
- : {
459
- status: 'unmeasured',
460
- summary:
461
- `${firedDeclared} of ${declared} fired — silent: ${silent.slice(0, 3).join(', ')}` +
462
- `${silent.length > 3 ? ` +${silent.length - 3}` : ''}`,
463
- fix:
464
- 'a silent emitter is either a broken wire or a moment random play never ' +
465
- 'reached — the two look identical from here. Reach it in a scripted run ' +
466
- '(`runScript` from `incanto/test`), and this rung can tell you which.',
467
- }),
468
- });
469
- }
470
- }
398
+ // The wording lives in `verify-ladder.ts` (pure, and tested there).
399
+ // `undrivable` is the plays rung's own "the bot never got it moving", so the
400
+ // two never disagree about whether anything was played.
401
+ rungs.push(feelsRung(playtestReport?.feedback, { played: botPlayed }));
471
402
 
472
403
  // ---- agrees --------------------------------------------------------------
473
404
  // A desync is the purest silent failure this engine has: both clients run,
@@ -544,28 +475,27 @@ if (rungs[0].status === 'pass') {
544
475
  ? 'bring the preview window to the front — a hidden tab does not render, and a frame is captured inside a render'
545
476
  : 'start your dev server, open the page, and run this again — the pixels are in the browser',
546
477
  });
547
- } else if (report.hidden) {
478
+ } else if (report.hidden && report.droveFrames === 0) {
548
479
  /*
549
- * A hidden page is UNMEASURED, not passed.
550
- *
551
- * A browser stops animating a page it considers hidden — a window merely
552
- * covered by another counts — and the back buffer keeps whatever it last
553
- * drew. Measured at one instant, same page:
480
+ * A hidden page cannot be DRIVEN, and that one is still unmeasured.
554
481
  *
555
- * incanto-frame --do the page is hidden a frozen game (exit 1)
556
- * incanto-logs frame rate not measurable hidden
557
- * incanto-frame subject fills 17.5% of frame
558
- * incanto-verify ✓ draws — subject fills 17.5%
482
+ * The rung used to refuse any hidden page, on the belief that its pixels
483
+ * were "whatever it last drew". They are not: `captureFrame` in both
484
+ * renderers waits ~120 ms for a loop that a hidden tab never runs, and then
485
+ * draws a frame itself. Measured hide every node from the console and
486
+ * capture again: 2D `luminance 0.09 → 0.00 BLACK SCREEN`, 3D `0.64 → 0.96,
487
+ * subject 44.3% → 22.0%`.
559
488
  *
560
- * Two of six rungs reported off a stale buffer while the engine already
561
- * knew, and said elsewhere, that the page was drawing nothing. The skills
562
- * state the rule: an unmeasured rung is not a failure, and this is exactly
563
- * the unmeasured case.
489
+ * So a plain capture is current and measurable, and the ONLY thing a hidden
490
+ * page cannot do is advance which is exactly what a drive needs, and what
491
+ * `droveFrames: 0` reports. Refusing both cost this rung every measurement
492
+ * it could have made in the setup the whole toolchain is built for: a
493
+ * terminal in front and a browser behind it.
564
494
  */
565
495
  rungs.push({
566
496
  name: 'draws',
567
497
  status: 'unmeasured',
568
- summary: 'the page is HIDDEN — those pixels are whatever it last drew',
498
+ summary: 'the drive ran on a HIDDEN page 0 frames drawn, so nothing it did shows here',
569
499
  fix: 'bring the window to the front (one covered by another counts as hidden) and run this again',
570
500
  });
571
501
  } else if (report.black) {
@@ -598,6 +528,14 @@ if (rungs[0].status === 'pass') {
598
528
  if (report.uniform) {
599
529
  notes.push('one flat colour — the camera may be inside geometry, or only the sky is drawn');
600
530
  }
531
+ // Measured, not refused — but the reader still has to know the world was
532
+ // not moving while it was drawn.
533
+ if (report.hidden) {
534
+ notes.push(
535
+ 'the page is hidden, so the game is PAUSED — this frame was rendered for the ' +
536
+ 'capture, and shows the moment it froze',
537
+ );
538
+ }
601
539
  rungs.push({
602
540
  name: 'draws',
603
541
  status: 'pass',
package/dist/2d.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { c as showBootFailure, i as ParticleSim, o as ParticleView, r as SpatialPose, s as isWebGLAvailable } from "./audio-player-DaMxqfNE.js";
2
- 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, x as EngineOptions } from "./behavior-DZExDn9o.js";
1
+ import { c as showBootFailure, i as ParticleSim, o as ParticleView, r as SpatialPose, s as isWebGLAvailable } from "./audio-player-ioZ7VKC6.js";
2
+ 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, x as EngineOptions } from "./behavior-Bf9P9oB2.js";
3
3
  import { t as DiagnosticSink } from "./diagnostics-Cu85N3tL.js";
4
4
  import { a as SceneJson, s as JsonObject } from "./rng-Bb-IutXB.js";
5
5
  import { t as EditorSwitchOptions } from "./editor-switch-CFU9mCec.js";
6
6
  import { r as FrameStats } from "./frame-report-DNxDAb1w.js";
7
- import { t as LoadSceneOptions } from "./loader-Cff09LMm.js";
7
+ import { t as LoadSceneOptions } from "./loader-CBfQzB6S.js";
8
8
  import { n as AnimationEntry } from "./sprite-animation-CMr6f1K2.js";
9
9
  import { Group, Mesh, Object3D, Scene, Texture } from "three";
10
10
  import * as RapierNs from "@dimforge/rapier2d-compat";
@@ -1151,6 +1151,21 @@ declare class TileMap2D extends Node2D {
1151
1151
  set cells(value: (string | number[])[]);
1152
1152
  get legend(): Record<string, number>;
1153
1153
  set legend(value: Record<string, number>);
1154
+ private _colors;
1155
+ get colors(): Record<string, string>;
1156
+ set colors(value: Record<string, string>);
1157
+ /**
1158
+ * The colours, in declaration order — a cell's tile index is `COLOR_BASE + i`.
1159
+ *
1160
+ * A synthetic index rather than a second grid: everything downstream (the
1161
+ * collider merge, `tileAt`, `setTile`, the editor's paint tool) already
1162
+ * speaks in indices, and one number per cell keeps it that way.
1163
+ */
1164
+ private palette;
1165
+ /** `legend` plus the colour chars, which is what every cell lookup wants. */
1166
+ private allChars;
1167
+ /** `solid` as INDICES: an entry may name the char that painted the cell. */
1168
+ private solidIndices;
1154
1169
  get solid(): number[];
1155
1170
  set solid(value: number[]);
1156
1171
  opacity: number;
package/dist/2d.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
2
  import { t as showBootFailure } from "./boot-failure-CKYrEcGF.js";
3
3
  import { t as isWebGLAvailable } from "./webgl-unavailable-C8aDbGmR.js";
4
- import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-Bwvh6q8A.js";
5
- 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-Bkk0wSDB.js";
6
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-CE0Qvy3V.js";
4
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-C9rWqqAC.js";
5
+ 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-C8HuRkOf.js";
6
+ import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-DrIgTlLk.js";
7
7
  //#region src/2d/library-sprite.ts
8
8
  /**
9
9
  * 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 { c as showBootFailure, i as ParticleSim, o as ParticleView, r as SpatialPose, s as isWebGLAvailable } from "./audio-player-DaMxqfNE.js";
2
- 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, x as EngineOptions } from "./behavior-DZExDn9o.js";
1
+ import { c as showBootFailure, i as ParticleSim, o as ParticleView, r as SpatialPose, s as isWebGLAvailable } from "./audio-player-ioZ7VKC6.js";
2
+ 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, x as EngineOptions } from "./behavior-Bf9P9oB2.js";
3
3
  import { t as DiagnosticSink } from "./diagnostics-Cu85N3tL.js";
4
4
  import { a as SceneJson$1, s as JsonObject } from "./rng-Bb-IutXB.js";
5
5
  import { t as EditorSwitchOptions } from "./editor-switch-CFU9mCec.js";
6
6
  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-DNxDAb1w.js";
7
- import { t as LoadSceneOptions } from "./loader-Cff09LMm.js";
7
+ import { t as LoadSceneOptions } from "./loader-CBfQzB6S.js";
8
8
  import { n as PathGrid } from "./pathfinding-_fGrCFmH.js";
9
9
  import { AnimationClip, AnimationMixer, BufferGeometry, Color, DirectionalLight, Group, InstancedMesh, Mesh, MeshPhysicalMaterial, Object3D, PerspectiveCamera, Scene, ShaderMaterial, Texture, Vector3, WebGLRenderer } from "three";
10
10
  import { VRM } from "@pixiv/three-vrm";
package/dist/3d.js CHANGED
@@ -1,12 +1,12 @@
1
- import { b as sunDirectionFromElevationAzimuth, v as horizonColorFromSky, x as sunDirectionFromSky, y as parseEnvironment3D } from "./save-slots-BXVg148r.js";
1
+ import { C as sunDirectionFromSky, S as sunDirectionFromElevationAzimuth, b as horizonColorFromSky, x as parseEnvironment3D } from "./save-slots-CEuJPUle.js";
2
2
  import { t as showBootFailure } from "./boot-failure-CKYrEcGF.js";
3
3
  import { t as isWebGLAvailable } from "./webgl-unavailable-C8aDbGmR.js";
4
- import { a as frameSignature, n as diffSignatures, o as frameStats, r as diffText, s as frameText, t as SIGNATURE_GRID } from "./frame-report-Dlq13Gyj.js";
5
- import { B as Area3D, G as Node3D, H as PhysicsBody3D, I as Water3D, J as WATER_MAX_RIPPLES, L as WATER_CUTOUT_MAX, R as WaterCutout3D, U as RigidBody3D, V as CharacterBody3D, W as StaticBody3D } from "./gameplay-BfHkuzVb.js";
6
- 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-D6Q5BxeE.js";
7
- import { a as Environment3D, c as AssetStore3D, i as syncTree, o as applyQualityTier3D, r as Renderer3D, s as setEnvironment3D, t as createGame3D } from "./create-game-C7ffQWW7.js";
4
+ import { a as frameSignature, n as diffSignatures, o as frameStats, r as diffText, s as frameText, t as SIGNATURE_GRID } from "./frame-report-D-_7YF2G.js";
5
+ import { G as StaticBody3D, H as CharacterBody3D, K as Node3D, L as Water3D, R as WATER_CUTOUT_MAX, U as PhysicsBody3D, V as Area3D, W as RigidBody3D, Y as WATER_MAX_RIPPLES, z as WaterCutout3D } from "./gameplay-BlQe-M07.js";
6
+ 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-BeHoaGhy.js";
7
+ import { a as Environment3D, c as AssetStore3D, i as syncTree, o as applyQualityTier3D, r as Renderer3D, s as setEnvironment3D, t as createGame3D } from "./create-game-CmFAtLZp.js";
8
8
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-BYgD5Edk.js";
9
- import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-CpH-2gn5.js";
9
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-BzOCw1VS.js";
10
10
  //#region src/3d/model-verdict.ts
11
11
  /** Mixamo exports every bone as `mixamorigX`; the retargeter binds by that name. */
12
12
  const MIXAMO = /^mixamorig[:_]?/i;
@@ -1,5 +1,5 @@
1
1
  import { o as __exportAll } from "./json-CfTjpvW8.js";
2
- import { q as diagnose } from "./save-slots-BXVg148r.js";
2
+ import { Y as diagnose } from "./save-slots-CEuJPUle.js";
3
3
  //#region src/net/agent8.ts
4
4
  var agent8_exports = /* @__PURE__ */ __exportAll({
5
5
  Agent8TransportForTest: () => Agent8Transport,
@@ -1,4 +1,4 @@
1
- import { d as PropSchema, dt as Listener, jt as Node } from "./behavior-DZExDn9o.js";
1
+ import { d as PropSchema, dt as Listener, jt as Node } from "./behavior-Bf9P9oB2.js";
2
2
  import { t as Rng } from "./rng-Bb-IutXB.js";
3
3
 
4
4
  //#region src/core/boot-failure.d.ts
@@ -414,6 +414,17 @@ declare class Node implements NodeLifecycle {
414
414
  * SceneTree). Frees immediately when detached from any tree.
415
415
  */
416
416
  queueFree(): void;
417
+ /**
418
+ * Has this node been torn down?
419
+ *
420
+ * A scene swap frees the tree it replaces, so the answer is `true` for every
421
+ * node of the scene you just left — which is exactly the state a behavior is
422
+ * in when it runs one more line after `goToScene`. Kept as a flag rather than
423
+ * inferred from `parent`/`tree`, because a DETACHED node (a prefab on a
424
+ * shelf, a clone waiting to be added) has neither and is perfectly alive.
425
+ */
426
+ get freed(): boolean;
427
+ private _freed;
417
428
  /** Immediately detach and tear down this node and its children. */
418
429
  free(): void;
419
430
  /**
@@ -2286,7 +2297,7 @@ declare class Engine {
2286
2297
  * pass it back. Without that a game using the documented sub-scenes booted
2287
2298
  * fine and threw `UNRESOLVED_INSTANCE` on its first restart.
2288
2299
  */
2289
- resolveScene?: (path: string) => SceneJson;
2300
+ resolveScene?: (path: string) => SceneJson | null | undefined;
2290
2301
  /**
2291
2302
  * How the current scene was LOADED, for the rebuilds that happen later.
2292
2303
  *
@@ -1,15 +1,15 @@
1
1
  import { o as __exportAll } from "./json-CfTjpvW8.js";
2
- import { E as resolveViewport, T as computeViewport, it as registerBehavior, p as loadScene, q as diagnose } from "./save-slots-BXVg148r.js";
3
- import { C as qualityRendering, _ as AudioPlayer, v as Engine } from "./register-6DYnKZcy.js";
2
+ import { D as computeViewport, O as resolveViewport, R as sceneDimension, Y as diagnose, h as loadScene, ot as registerBehavior } from "./save-slots-CEuJPUle.js";
3
+ import { C as qualityRendering, _ as AudioPlayer, v as Engine } from "./register-5IYfooAN.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { n as attachTouchControls } from "./touch-BnCyPA0G.js";
6
6
  import { n as withWebGLContext, r as resolveRendering } from "./webgl-unavailable-C8aDbGmR.js";
7
- import { a as devServerLibrary, c as applyDriveStep, d as audioErrors, i as crossFade, l as wireDevChannel, n as teardown, o as openBundledEditor, r as pauseWhenHidden, t as nodeFromHits, u as claimCanvasGestures } from "./picking-CQJ_PJKh.js";
8
- import { o as frameStats } from "./frame-report-Dlq13Gyj.js";
9
- import { n as registerGameplayBehaviors } from "./gameplay-BfHkuzVb.js";
10
- import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-Bkk0wSDB.js";
7
+ import { a as devServerLibrary, c as applyDriveStep, d as audioErrors, i as crossFade, l as wireDevChannel, n as teardown, o as openBundledEditor, r as pauseWhenHidden, t as nodeFromHits, u as claimCanvasGestures } from "./picking-C9McVJrO.js";
8
+ import { o as frameStats } from "./frame-report-D-_7YF2G.js";
9
+ import { n as registerGameplayBehaviors } from "./gameplay-BlQe-M07.js";
10
+ import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-C8HuRkOf.js";
11
11
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
12
- import { n as enablePhysics2D } from "./physics-2d-CE0Qvy3V.js";
12
+ import { n as enablePhysics2D } from "./physics-2d-DrIgTlLk.js";
13
13
  import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
14
14
  //#region src/2d/assets.ts
15
15
  /**
@@ -132,6 +132,34 @@ var AssetStore2D = class {
132
132
  }
133
133
  };
134
134
  //#endregion
135
+ //#region src/2d/debug-lines.ts
136
+ /**
137
+ * Physics debug segments, moved into the space the 2D renderer draws in.
138
+ *
139
+ * Two coordinate systems meet here and they disagree about Y. Engine 2D is
140
+ * y-DOWN (screen pixels, gravity `+980`, and Rapier's world is built the same
141
+ * way — `kcc.setUp({x: 0, y: -1})`). The three.js scene the renderer paints
142
+ * into is y-UP, which is why every node's own sync writes
143
+ * `o.position.set(x, -y, 0)`.
144
+ *
145
+ * `syncDebugLines` copied Rapier's vertices straight across without that
146
+ * negation, so every collider outline was MIRRORED about the world's y=0 line:
147
+ * a crate at y=120 drew its box at −120, the further from the axis the further
148
+ * off, and a tilemap's merged floor at y=384 drew itself off the top of the
149
+ * canvas entirely. The overlay whose whole promise is "the colliders the
150
+ * ENGINE builds… wherever physics actually puts them" was the one thing in the
151
+ * 2D view that was not where physics put it.
152
+ */
153
+ function debugLinesToScene(px) {
154
+ const xyz = new Float32Array(px.length / 2 * 3);
155
+ for (let i = 0, j = 0; i < px.length; i += 2, j += 3) {
156
+ xyz[j] = px[i];
157
+ xyz[j + 1] = -px[i + 1];
158
+ xyz[j + 2] = 0;
159
+ }
160
+ return xyz;
161
+ }
162
+ //#endregion
135
163
  //#region src/2d/picking.ts
136
164
  function screenFromWorld(view, wx, wy) {
137
165
  return {
@@ -547,12 +575,7 @@ var Renderer2D = class {
547
575
  }
548
576
  this.debugLines.visible = vertices !== null;
549
577
  if (!vertices) return;
550
- const xyz = new Float32Array(vertices.length / 2 * 3);
551
- for (let i = 0, j = 0; i < vertices.length; i += 2, j += 3) {
552
- xyz[j] = vertices[i];
553
- xyz[j + 1] = vertices[i + 1];
554
- xyz[j + 2] = 0;
555
- }
578
+ const xyz = debugLinesToScene(vertices);
556
579
  this.debugLines.geometry.setAttribute("position", new BufferAttribute(xyz, 3));
557
580
  }
558
581
  render() {
@@ -809,6 +832,7 @@ async function createGame2D(opts) {
809
832
  ...opts._scheduler ? { scheduler: opts._scheduler } : {}
810
833
  });
811
834
  const sourceJson = cloneSceneJson(opts.scene);
835
+ if (sceneDimension(sourceJson) === "3d") throw new IncantoError("WRONG_DIMENSION", `createGame2D was given a 3D scene ('${String(sourceJson.name ?? "scene")}'). Its nodes are never drawn by the 2D renderer and never simulated by 2D physics — you get a blank canvas. Boot it with createGame3D instead.`);
812
836
  const scene = loadScene(cloneSceneJson(sourceJson), {
813
837
  resolveScene: opts.resolveScene,
814
838
  engine
@@ -1,15 +1,15 @@
1
1
  import { o as __exportAll } from "./json-CfTjpvW8.js";
2
- import { it as registerBehavior, p as loadScene, q as diagnose, v as horizonColorFromSky, x as sunDirectionFromSky, y as parseEnvironment3D } from "./save-slots-BXVg148r.js";
3
- import { C as qualityRendering, S as qualityEnvironment, _ as AudioPlayer, v as Engine, x as qualityCaps } from "./register-6DYnKZcy.js";
2
+ import { C as sunDirectionFromSky, R as sceneDimension, Y as diagnose, b as horizonColorFromSky, h as loadScene, ot as registerBehavior, x as parseEnvironment3D } from "./save-slots-CEuJPUle.js";
3
+ import { C as qualityRendering, S as qualityEnvironment, _ as AudioPlayer, v as Engine, x as qualityCaps } from "./register-5IYfooAN.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { n as attachTouchControls } from "./touch-BnCyPA0G.js";
6
6
  import { n as withWebGLContext, r as resolveRendering } from "./webgl-unavailable-C8aDbGmR.js";
7
- import { a as devServerLibrary, c as applyDriveStep, d as audioErrors, i as crossFade, l as wireDevChannel, n as teardown, o as openBundledEditor, r as pauseWhenHidden, s as poseFromRenderer, t as nodeFromHits, u as claimCanvasGestures } from "./picking-CQJ_PJKh.js";
8
- import { o as frameStats } from "./frame-report-Dlq13Gyj.js";
9
- import { G as Node3D, H as PhysicsBody3D, n as registerGameplayBehaviors, q as createCausticsQuad } from "./gameplay-BfHkuzVb.js";
7
+ import { a as devServerLibrary, c as applyDriveStep, d as audioErrors, i as crossFade, l as wireDevChannel, n as teardown, o as openBundledEditor, r as pauseWhenHidden, s as poseFromRenderer, t as nodeFromHits, u as claimCanvasGestures } from "./picking-C9McVJrO.js";
8
+ import { o as frameStats } from "./frame-report-D-_7YF2G.js";
9
+ import { J as createCausticsQuad, K as Node3D, U as PhysicsBody3D, n as registerGameplayBehaviors } from "./gameplay-BlQe-M07.js";
10
10
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
11
- import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-D6Q5BxeE.js";
12
- import { n as enablePhysics3D } from "./physics-3d-CpH-2gn5.js";
11
+ import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-BeHoaGhy.js";
12
+ import { n as enablePhysics3D } from "./physics-3d-BzOCw1VS.js";
13
13
  import { ACESFilmicToneMapping, AmbientLight, Box3, BufferAttribute, BufferGeometry, Color, DepthTexture, EquirectangularReflectionMapping, FloatType, Fog, HalfFloatType, LineBasicMaterial, LineSegments, LoadingManager, Matrix4, Mesh, PCFShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, Quaternion, Raycaster, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
14
14
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
15
15
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
@@ -2253,6 +2253,7 @@ async function createGame3D(opts) {
2253
2253
  ...opts._scheduler ? { scheduler: opts._scheduler } : {}
2254
2254
  });
2255
2255
  const sourceJson = cloneSceneJson(opts.scene);
2256
+ if (sceneDimension(sourceJson) === "2d") throw new IncantoError("WRONG_DIMENSION", `createGame3D was given a 2D scene ('${String(sourceJson.name ?? "scene")}'). Its nodes are never drawn by the 3D renderer and never simulated by 3D physics — you get a blank canvas. Boot it with createGame2D instead.`);
2256
2257
  const scene = loadScene(cloneSceneJson(sourceJson), {
2257
2258
  resolveScene: opts.resolveScene,
2258
2259
  engine
package/dist/debug.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Ft as LogLevel, T as RendererStats, b as Engine } from "./behavior-DZExDn9o.js";
1
+ import { Ft as LogLevel, T as RendererStats, b as Engine } from "./behavior-Bf9P9oB2.js";
2
2
 
3
3
  //#region src/debug/panel.d.ts
4
4
  /** Minimal document surface the overlay needs (injectable for tests). */