incanto 0.70.0 → 0.71.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 (67) hide show
  1. package/bin/incanto-verify.mjs +36 -98
  2. package/dist/2d.d.ts +18 -3
  3. package/dist/2d.js +3 -3
  4. package/dist/3d.d.ts +3 -3
  5. package/dist/3d.js +6 -6
  6. package/dist/{agent8-CmNF01gA.js → agent8-Cfmd3ar_.js} +1 -1
  7. package/dist/{audio-player-DaMxqfNE.d.ts → audio-player-L8yccdMP.d.ts} +1 -1
  8. package/dist/{behavior-DZExDn9o.d.ts → behavior-Dcz0fr1S.d.ts} +11 -0
  9. package/dist/{create-game-Bwvh6q8A.js → create-game-CH4kt78v.js} +7 -7
  10. package/dist/{create-game-C7ffQWW7.js → create-game-LCHBLfA4.js} +7 -7
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/editor.js +29 -25
  13. package/dist/{environment-presets-D6Q5BxeE.js → environment-presets-99BzP_L-.js} +3 -3
  14. package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
  15. package/dist/{gameplay-BfHkuzVb.js → gameplay-CNULJvwh.js} +108 -6
  16. package/dist/gameplay.d.ts +68 -4
  17. package/dist/gameplay.js +2 -2
  18. package/dist/index.d.ts +27 -4
  19. package/dist/index.js +4 -4
  20. package/dist/{loader-Cff09LMm.d.ts → loader-8-IHvbKD.d.ts} +1 -1
  21. package/dist/net.d.ts +2 -2
  22. package/dist/net.js +2 -2
  23. package/dist/{physics-2d-CE0Qvy3V.js → physics-2d-FhrXUc6B.js} +2 -2
  24. package/dist/{physics-3d-CpH-2gn5.js → physics-3d-CFEGxBXZ.js} +3 -3
  25. package/dist/{picking-CQJ_PJKh.js → picking-DVo7fI13.js} +3 -3
  26. package/dist/react.d.ts +1 -1
  27. package/dist/react.js +1 -1
  28. package/dist/{register-Bkk0wSDB.js → register-BYQCBySi.js} +100 -8
  29. package/dist/{register-6DYnKZcy.js → register-DuVdzxbq.js} +49 -15
  30. package/dist/{replay-DjAkAzMq.js → replay-DEvp3kyV.js} +2 -2
  31. package/dist/{replay-Dmw-PKQu.d.ts → replay-F7IZHdFR.d.ts} +1 -1
  32. package/dist/{save-slots-BXVg148r.js → save-slots-Bvuh2p_r.js} +56 -3
  33. package/dist/{split-screen--k-XpBjr.d.ts → split-screen-CIYf1zSf.d.ts} +2 -2
  34. package/dist/{split-screen-PL78oVXP.js → split-screen-eULetcg-.js} +3 -3
  35. package/dist/{src-Cxfiv1Hg.js → src-_jk0qLsS.js} +1 -1
  36. package/dist/{test-iHYVUcDK.js → test-CmN54kWv.js} +252 -24
  37. package/dist/test.d.ts +113 -5
  38. package/dist/test.js +3 -3
  39. package/dist/vite.js +3 -3
  40. package/editor/assets/{agent8-D0MS174y.js → agent8-yLIEYHbd.js} +1 -1
  41. package/editor/assets/{debug-BnXkKuYu.js → debug-u31w_yhq.js} +1 -1
  42. package/editor/assets/{index-CIu3uc3l.js → index-DK9xMGpW.js} +94 -94
  43. package/editor/index.html +1 -1
  44. package/package.json +1 -1
  45. package/schemas/scene.schema.json +4 -0
  46. package/skills/incanto-behaviors-and-scripts.md +30 -9
  47. package/skills/incanto-building-2d-games.md +20 -2
  48. package/skills/incanto-gameplay-behaviors.md +63 -1
  49. package/skills/incanto-hud.md +14 -0
  50. package/skills/incanto-node-reference.md +18 -2
  51. package/skills/incanto-playtesting.md +34 -0
  52. package/skills/incanto-save-slots.md +56 -1
  53. package/skills/incanto-verifying-your-game.md +11 -0
  54. package/templates-app/beacon-isle-3d/package.json +1 -1
  55. package/templates-app/beacon-isle-3d/src/game.scene.json +44 -226
  56. package/templates-app/molehill-2d/package.json +1 -1
  57. package/templates-app/molehill-2d/src/game.scene.json +10 -60
  58. package/templates-app/platformer-2d/package.json +1 -1
  59. package/templates-app/platformer-2d/src/game.scene.json +80 -416
  60. package/templates-app/star-survivor/package.json +1 -1
  61. package/templates-app/star-survivor/src/game.scene.json +5 -24
  62. package/templates-app/tps-3d/package.json +1 -1
  63. package/templates-app/tps-3d/src/game.scene.json +13 -71
  64. package/templates-app/village-quest-3d/package.json +1 -1
  65. package/templates-app/village-quest-3d/src/behaviors.ts +21 -4
  66. package/templates-app/village-quest-3d/src/grove.scene.json +16 -87
  67. package/templates-app/village-quest-3d/src/village.scene.json +5 -27
@@ -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-L8yccdMP.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-Dcz0fr1S.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-8-IHvbKD.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-CH4kt78v.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-BYQCBySi.js";
6
+ import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-FhrXUc6B.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-L8yccdMP.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-Dcz0fr1S.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-8-IHvbKD.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-Bvuh2p_r.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-CNULJvwh.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-99BzP_L-.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-LCHBLfA4.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-CFEGxBXZ.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-Bvuh2p_r.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-Dcz0fr1S.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
  /**
@@ -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, Y as diagnose, h as loadScene, ot as registerBehavior } from "./save-slots-Bvuh2p_r.js";
3
+ import { C as qualityRendering, _ as AudioPlayer, v as Engine } from "./register-DuVdzxbq.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-DVo7fI13.js";
8
+ import { o as frameStats } from "./frame-report-D-_7YF2G.js";
9
+ import { n as registerGameplayBehaviors } from "./gameplay-CNULJvwh.js";
10
+ import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-BYQCBySi.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-FhrXUc6B.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
  /**
@@ -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, Y as diagnose, b as horizonColorFromSky, h as loadScene, ot as registerBehavior, x as parseEnvironment3D } from "./save-slots-Bvuh2p_r.js";
3
+ import { C as qualityRendering, S as qualityEnvironment, _ as AudioPlayer, v as Engine, x as qualityCaps } from "./register-DuVdzxbq.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-DVo7fI13.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-CNULJvwh.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-99BzP_L-.js";
12
+ import { n as enablePhysics3D } from "./physics-3d-CFEGxBXZ.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";
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-Dcz0fr1S.js";
2
2
 
3
3
  //#region src/debug/panel.d.ts
4
4
  /** Minimal document surface the overlay needs (injectable for tests). */
package/dist/editor.js CHANGED
@@ -5,18 +5,21 @@ import { registerGameplayBehaviors as A } from "incanto/gameplay";
5
5
  import { registerNodesNet as ee } from "incanto/net";
6
6
  import { GENERATORS as te, runGenerator as j } from "incanto/env";
7
7
  //#region src/format-json.ts
8
- var ne = 80, re = " ";
8
+ var ne = 100, re = " ";
9
9
  function M(e) {
10
- return `${ie(e, 0)}\n`;
10
+ return `${ie(e, 0, 0)}\n`;
11
11
  }
12
- function ie(e, t) {
12
+ function ie(e, t, n) {
13
13
  if (typeof e != "object" || !e) return JSON.stringify(e);
14
- let n = N(e);
15
- if (t * 2 + n.length <= ne) return n;
16
- let r = re.repeat(t + 1), i = re.repeat(t);
17
- if (Array.isArray(e)) return e.length === 0 ? "[]" : `[\n${e.map((e) => r + ie(e, t + 1)).join(",\n")}\n${i}]`;
18
- let a = Object.entries(e);
19
- return a.length === 0 ? "{}" : `{\n${a.map(([e, n]) => `${r}${JSON.stringify(e)}: ${ie(n, t + 1)}`).join(",\n")}\n${i}}`;
14
+ let r = N(e);
15
+ if (n + r.length + 1 <= ne) return r;
16
+ let i = re.repeat(t + 1), a = re.repeat(t), o = (t + 1) * 2;
17
+ if (Array.isArray(e)) return e.length === 0 ? "[]" : `[\n${e.map((e) => i + ie(e, t + 1, o)).join(",\n")}\n${a}]`;
18
+ let s = Object.entries(e);
19
+ return s.length === 0 ? "{}" : `{\n${s.map(([e, n]) => {
20
+ let r = `${JSON.stringify(e)}: `;
21
+ return `${i}${r}${ie(n, t + 1, o + r.length)}`;
22
+ }).join(",\n")}\n${a}}`;
20
23
  }
21
24
  function N(e) {
22
25
  if (typeof e != "object" || !e) return JSON.stringify(e);
@@ -1289,8 +1292,8 @@ var kt = {
1289
1292
  en: "A whole tile level as ONE node — grid render + merged static colliders.",
1290
1293
  ko: "타일 레벨 전체를 노드 하나로 — 그리드 렌더 + 병합된 정적 콜라이더."
1291
1294
  }, {
1292
- en: "Author cells as rows of characters: \".\" / space = empty, digits 0-9 = atlas tile index, other chars map through legend ({\"G\": 12}). texture is a \"$atlas\" asset read left-to-right top-to-bottom in tileSize squares (columns: 0 derives from the texture width). Tile indices listed in solid become static colliders, greedy-merged into a few rectangles. Cell (0,0) hangs its top-left on the node origin.",
1293
- ko: "cells를 문자 행으로 작성합니다: \".\"/공백 = 빈 칸, 숫자 0-9 = 아틀라스 타일 번호, 그 외 문자는 legend({\"G\": 12})로 매핑. texture는 tileSize 정사각형을 좌→우, 상→하로 읽는 \"$아틀라스\" 에셋입니다(columns: 0이면 텍스처 폭에서 유도). solid 나열된 타일 번호는 정적 콜라이더가 되며 소수의 직사각형으로 탐욕 병합됩니다. (0,0) 칸의 좌상단이 노드 원점에 걸립니다."
1295
+ en: "Author cells as rows of characters: \".\" / space = empty, digits 0-9 = atlas tile index, other chars map through legend ({\"G\": 12}) or colors ({\"#\": \"#3a4356\"}), which paints a flat colour and needs no atlas at all — use it to block out a level before the art arrives (exclusive with texture). texture is a \"$atlas\" asset read left-to-right top-to-bottom in tileSize squares (columns: 0 derives from the texture width). solid becomes static colliders, greedy-merged into a few rectangles, and takes the CHAR you painted as well as a tile index. Cell (0,0) hangs its top-left on the node origin.",
1296
+ ko: "cells를 문자 행으로 작성합니다: \".\"/공백 = 빈 칸, 숫자 0-9 = 아틀라스 타일 번호, 그 외 문자는 legend({\"G\": 12}) 또는 colors({\"#\": \"#3a4356\"})매핑합니다. colors는 아틀라스 없이 단색 칸을 그려서 아트가 오기 전에 레벨을 잡을 때 씁니다(texture와 동시 사용 불가). texture는 tileSize 정사각형을 좌→우, 상→하로 읽는 \"$아틀라스\" 에셋입니다(columns: 0이면 텍스처 폭에서 유도). solid 정적 콜라이더가 되며(타일 번호뿐 아니라 칠한 문자도 받습니다) 소수의 직사각형으로 탐욕 병합됩니다. (0,0) 칸의 좌상단이 노드 원점에 걸립니다."
1294
1297
  }),
1295
1298
  L("AnimatedSprite2D", {
1296
1299
  en: "Spritesheet animation player.",
@@ -2374,15 +2377,16 @@ function In(e, t, n, r) {
2374
2377
  }
2375
2378
  return s;
2376
2379
  }
2377
- function Ln(e, t) {
2378
- let n = new Set([
2380
+ function Ln(e, t, n = {}) {
2381
+ let r = new Set([
2379
2382
  ".",
2380
2383
  "0",
2381
2384
  "1"
2382
2385
  ]);
2383
- for (let t of e) if (typeof t == "string") for (let e of t) e !== " " && n.add(e);
2384
- for (let e of Object.keys(t)) n.add(e);
2385
- return [...n];
2386
+ for (let t of e) if (typeof t == "string") for (let e of t) e !== " " && r.add(e);
2387
+ for (let e of Object.keys(t)) r.add(e);
2388
+ for (let e of Object.keys(n)) r.add(e);
2389
+ return [...r];
2386
2390
  }
2387
2391
  //#endregion
2388
2392
  //#region src/inspector.ts
@@ -3536,17 +3540,17 @@ function Ir(e, t) {
3536
3540
  r.type = "button", r.className = t.active() ? "paint-toggle on" : "paint-toggle", r.textContent = t.active() ? "🖌 painting — click the canvas" : "🖌 paint tiles", r.addEventListener("click", () => t.toggle()), n.appendChild(r);
3537
3541
  let i = document.createElement("div");
3538
3542
  i.className = "paint-palette";
3539
- let a = e.props?.cells ?? [], o = e.props?.legend ?? {};
3540
- for (let e of Ln(a, o)) {
3543
+ let a = e.props?.cells ?? [], o = e.props?.legend ?? {}, s = e.props?.colors ?? {};
3544
+ for (let e of Ln(a, o, s)) {
3541
3545
  let n = document.createElement("button");
3542
- n.type = "button", n.className = t.brush() === e ? "paint-chip on" : "paint-chip", n.textContent = e === "." ? "␡" : e, n.title = e === "." ? "eraser (empty cell)" : `tile '${e}'`, n.addEventListener("click", () => t.setBrush(e)), i.appendChild(n);
3546
+ n.type = "button", n.className = t.brush() === e ? "paint-chip on" : "paint-chip", n.textContent = e === "." ? "␡" : e, n.title = e === "." ? "eraser (empty cell)" : s[e] ? `tile '${e}' — ${s[e]}` : `tile '${e}'`, s[e] && (n.style.borderColor = s[e]), n.addEventListener("click", () => t.setBrush(e)), i.appendChild(n);
3543
3547
  }
3544
- let s = document.createElement("input");
3545
- s.type = "text", s.maxLength = 1, s.className = "paint-brush-input", s.value = t.brush(), s.title = "brush char (digit or a legend char)", s.addEventListener("change", () => {
3546
- s.value.length === 1 && t.setBrush(s.value);
3547
- }), i.appendChild(s), n.appendChild(i);
3548
- let c = document.createElement("div");
3549
- return c.className = "paint-hint", c.textContent = "grid grows right/down; digits map to atlas tiles, others need legend", n.appendChild(c), n;
3548
+ let c = document.createElement("input");
3549
+ c.type = "text", c.maxLength = 1, c.className = "paint-brush-input", c.value = t.brush(), c.title = "brush char (a digit, or a char from legend/colors)", c.addEventListener("change", () => {
3550
+ c.value.length === 1 && t.setBrush(c.value);
3551
+ }), i.appendChild(c), n.appendChild(i);
3552
+ let l = document.createElement("div");
3553
+ return l.className = "paint-hint", l.textContent = "grid grows right/down; digits map to atlas tiles, others need legend or colors", n.appendChild(l), n;
3550
3554
  }
3551
3555
  function Lr(e, t, n, r = "", i) {
3552
3556
  let a = document.createElement("textarea");
@@ -1,11 +1,11 @@
1
- import { q as diagnose } from "./save-slots-BXVg148r.js";
2
- import { M as translateOn, t as registerCoreNodes } from "./register-6DYnKZcy.js";
1
+ import { Y as diagnose } from "./save-slots-Bvuh2p_r.js";
2
+ import { M as translateOn, t as registerCoreNodes } from "./register-DuVdzxbq.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-CDOMybym.js";
5
5
  import { c as getNodeSchema, h as didYouMean, p as registerNode } from "./touch-BnCyPA0G.js";
6
6
  import { t as createNoise2D } from "./noise-D3nPpmFg.js";
7
7
  import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimations, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-CqR2o3SA.js";
8
- import { B as Area3D, E as effectRng, G as Node3D, H as PhysicsBody3D, I as Water3D, R as WaterCutout3D, U as RigidBody3D, V as CharacterBody3D, W as StaticBody3D, Y as colliderFootDrop, Z as worldPosition, w as tolerateUnknownAction, z as rejectScale } from "./gameplay-BfHkuzVb.js";
8
+ import { B as rejectScale, D as effectRng, G as StaticBody3D, H as CharacterBody3D, K as Node3D, L as Water3D, Q as worldPosition, T as tolerateUnknownAction, U as PhysicsBody3D, V as Area3D, W as RigidBody3D, X as colliderFootDrop, z as WaterCutout3D } from "./gameplay-CNULJvwh.js";
9
9
  import { t as checkSheetGrid } from "./sheet-grid-Cea343VO.js";
10
10
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-BYgD5Edk.js";
11
11
  import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, ClampToEdgeWrapping, Color, ConeGeometry, CylinderGeometry, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, Group, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NoColorSpace, NormalBlending, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector3, Vector4, VectorKeyframeTrack } from "three";
@@ -177,7 +177,7 @@ const pct = (v) => `${(v * 100).toFixed(1)}%`;
177
177
  /** The report a person or an agent reads. Verdict first, numbers after. */
178
178
  function frameText(stats) {
179
179
  const lines = [];
180
- if (stats.hidden) lines.push("THE PAGE IS HIDDEN a browser stops animating a window covered by another, so these pixels are whatever it last drew. Bring it to the front and capture again.");
180
+ if (stats.hidden) lines.push("the page is hidden, so the game is PAUSED these pixels were rendered for this capture (current), but nothing is moving. A `--do` drive needs the window in front.");
181
181
  if (stats.black) lines.push("BLACK SCREEN — nothing was drawn (no light, no camera, or nothing in view)");
182
182
  else if (stats.uniform) lines.push("one flat colour — the camera may be inside geometry, or only the sky is drawn");
183
183
  else if (!stats.subject) lines.push("nothing but background — the camera is pointed away from everything in the scene");