incanto 0.69.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 (147) hide show
  1. package/bin/_behaviors-loader.mjs +22 -0
  2. package/bin/_read-json.mjs +28 -0
  3. package/bin/incanto-assets.mjs +5 -4
  4. package/bin/incanto-check.mjs +58 -31
  5. package/bin/incanto-editor.mjs +128 -5
  6. package/bin/incanto-env.mjs +3 -2
  7. package/bin/incanto-feel.mjs +24 -13
  8. package/bin/incanto-frame.mjs +8 -1
  9. package/bin/incanto-multiplay.mjs +11 -9
  10. package/bin/incanto-new.mjs +105 -10
  11. package/bin/incanto-play.mjs +158 -16
  12. package/bin/incanto-playtest.mjs +66 -23
  13. package/bin/incanto-skills.mjs +14 -2
  14. package/bin/incanto-verify.mjs +121 -102
  15. package/bin/incanto.mjs +3 -2
  16. package/dist/2d.d.ts +296 -34
  17. package/dist/2d.js +4 -4
  18. package/dist/3d.d.ts +157 -9
  19. package/dist/3d.js +8 -8
  20. package/dist/{agent8-CvsfVskX.js → agent8-Cfmd3ar_.js} +61 -8
  21. package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-L8yccdMP.d.ts} +2 -2
  22. package/dist/{behavior-rm8vVzu_.d.ts → behavior-Dcz0fr1S.d.ts} +755 -46
  23. package/dist/{create-game-CwxEDlCm.js → create-game-CH4kt78v.js} +143 -60
  24. package/dist/{create-game-CvJ1wSRP.js → create-game-LCHBLfA4.js} +98 -48
  25. package/dist/debug.d.ts +1 -1
  26. package/dist/debug.js +2 -3
  27. package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
  28. package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
  29. package/dist/editor.js +1088 -882
  30. package/dist/env.d.ts +1 -1
  31. package/dist/env.js +5 -3
  32. package/dist/{environment-presets-BO-7XoFV.js → environment-presets-99BzP_L-.js} +298 -45
  33. package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
  34. package/dist/{gameplay-D1KixWK8.js → gameplay-CNULJvwh.js} +914 -214
  35. package/dist/gameplay.d.ts +254 -10
  36. package/dist/gameplay.js +2 -2
  37. package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
  38. package/dist/index.d.ts +179 -13
  39. package/dist/index.js +10 -12
  40. package/dist/json-CfTjpvW8.js +67 -0
  41. package/dist/{loader-DadV2QCT.d.ts → loader-8-IHvbKD.d.ts} +2 -2
  42. package/dist/net.d.ts +27 -3
  43. package/dist/net.js +2 -2
  44. package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
  45. package/dist/{physics-2d-CF5yyjbj.js → physics-2d-FhrXUc6B.js} +136 -11
  46. package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CFEGxBXZ.js} +104 -23
  47. package/dist/{teardown-CMqFVxO9.js → picking-DVo7fI13.js} +104 -13
  48. package/dist/react.d.ts +2 -2
  49. package/dist/react.js +2 -2
  50. package/dist/{register-DbVArsns.js → register-BYQCBySi.js} +429 -28
  51. package/dist/{register-mmfugpaQ.js → register-DuVdzxbq.js} +594 -831
  52. package/dist/{replay-BEHLDcQV.js → replay-DEvp3kyV.js} +224 -14
  53. package/dist/{replay-BQfkX4x7.d.ts → replay-F7IZHdFR.d.ts} +20 -3
  54. package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
  55. package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
  56. package/dist/{loader-DZE7B07H.js → save-slots-Bvuh2p_r.js} +4619 -2318
  57. package/dist/{split-screen-Cg76MCV5.d.ts → split-screen-CIYf1zSf.d.ts} +36 -4
  58. package/dist/{split-screen-CauMwu6y.js → split-screen-eULetcg-.js} +159 -26
  59. package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
  60. package/dist/{src-CYEu-_S9.js → src-_jk0qLsS.js} +2 -17
  61. package/dist/test-CmN54kWv.js +4264 -0
  62. package/dist/test.d.ts +639 -30
  63. package/dist/test.js +3 -3
  64. package/dist/touch-BnCyPA0G.js +519 -0
  65. package/dist/vite.d.ts +20 -37
  66. package/dist/vite.js +177 -7
  67. package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
  68. package/editor/assets/agent8-yLIEYHbd.js +1 -0
  69. package/editor/assets/{debug-CMULinBW.js → debug-u31w_yhq.js} +2 -2
  70. package/editor/assets/index-DK9xMGpW.js +11046 -0
  71. package/editor/index.html +1 -1
  72. package/package.json +4 -14
  73. package/schemas/scene.schema.json +34 -3
  74. package/skills/incanto-3d-character.md +14 -1
  75. package/skills/incanto-3d-models.md +12 -0
  76. package/skills/incanto-assets.md +9 -1
  77. package/skills/incanto-audio.md +8 -2
  78. package/skills/incanto-behaviors-and-scripts.md +105 -7
  79. package/skills/incanto-building-2d-games.md +97 -10
  80. package/skills/incanto-building-3d-games.md +58 -4
  81. package/skills/incanto-editor.md +46 -7
  82. package/skills/incanto-environment.md +19 -1
  83. package/skills/incanto-game-feel.md +70 -0
  84. package/skills/incanto-gameplay-behaviors.md +172 -20
  85. package/skills/incanto-hud.md +142 -7
  86. package/skills/incanto-localization.md +13 -5
  87. package/skills/incanto-multiplayer.md +83 -3
  88. package/skills/incanto-node-reference.md +239 -59
  89. package/skills/incanto-performance.md +52 -0
  90. package/skills/incanto-physics-and-input.md +123 -24
  91. package/skills/incanto-playtesting.md +112 -2
  92. package/skills/incanto-save-slots.md +217 -7
  93. package/skills/incanto-scene-json-authoring.md +69 -12
  94. package/skills/incanto-verifying-your-game.md +207 -10
  95. package/skills/incanto-web-integration.md +28 -0
  96. package/skills/incanto-your-first-game.md +3 -1
  97. package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
  98. package/templates-app/beacon-isle-3d/package.json +2 -2
  99. package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
  100. package/templates-app/beacon-isle-3d/src/game.scene.json +143 -600
  101. package/templates-app/beacon-isle-3d/src/main.ts +11 -1
  102. package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
  103. package/templates-app/beacon-isle-3d/verify.ts +3 -1
  104. package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
  105. package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
  106. package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
  107. package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
  108. package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
  109. package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
  110. package/templates-app/molehill-2d/index.html +73 -0
  111. package/templates-app/molehill-2d/package.json +23 -0
  112. package/templates-app/molehill-2d/src/behaviors.ts +198 -0
  113. package/templates-app/molehill-2d/src/game.scene.json +1205 -0
  114. package/templates-app/molehill-2d/src/main.ts +41 -0
  115. package/templates-app/molehill-2d/tsconfig.json +13 -0
  116. package/templates-app/molehill-2d/verify.ts +247 -0
  117. package/templates-app/molehill-2d/vite.config.ts +12 -0
  118. package/templates-app/platformer-2d/package.json +2 -2
  119. package/templates-app/platformer-2d/src/game.scene.json +178 -986
  120. package/templates-app/platformer-2d/src/main.ts +11 -1
  121. package/templates-app/platformer-2d/tsconfig.json +1 -1
  122. package/templates-app/star-survivor/package.json +2 -2
  123. package/templates-app/star-survivor/src/game.scene.json +43 -216
  124. package/templates-app/star-survivor/src/main.ts +11 -1
  125. package/templates-app/star-survivor/tsconfig.json +1 -1
  126. package/templates-app/tps-3d/PROJECT/Context.md +1 -1
  127. package/templates-app/tps-3d/package.json +2 -2
  128. package/templates-app/tps-3d/src/behaviors.ts +19 -1
  129. package/templates-app/tps-3d/src/game.scene.json +66 -263
  130. package/templates-app/tps-3d/src/main.ts +13 -2
  131. package/templates-app/tps-3d/tsconfig.json +1 -1
  132. package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
  133. package/templates-app/village-quest-3d/package.json +2 -2
  134. package/templates-app/village-quest-3d/src/behaviors.ts +42 -4
  135. package/templates-app/village-quest-3d/src/grove.scene.json +68 -306
  136. package/templates-app/village-quest-3d/src/main.ts +11 -1
  137. package/templates-app/village-quest-3d/src/village.scene.json +202 -863
  138. package/templates-app/village-quest-3d/tsconfig.json +1 -1
  139. package/templates-app/village-quest-3d/verify.ts +14 -1
  140. package/dist/duplicate-Dj4L-5AA.js +0 -22
  141. package/dist/json-CwwhxQgb.js +0 -36
  142. package/dist/registry-CF70EArN.js +0 -212
  143. package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
  144. package/dist/test-D6ISmuFE.js +0 -2383
  145. package/dist/touch-DEAmqGdf.js +0 -225
  146. package/editor/assets/agent8-BDvaZMQJ.js +0 -1
  147. package/editor/assets/index-CZIRr5EL.js +0 -10958
package/dist/2d.d.ts CHANGED
@@ -1,9 +1,10 @@
1
- import { c as showBootFailure, i as ParticleSim, o as ParticleView, r as SpatialPose, s as isWebGLAvailable } from "./audio-player-C0j2k-yB.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-rm8vVzu_.js";
3
- import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-CnIOiyNJ.js";
4
- import { i as SceneJson, s as JsonObject } from "./schema-B6ugCV1Q.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
+ import { t as DiagnosticSink } from "./diagnostics-Cu85N3tL.js";
4
+ import { a as SceneJson, s as JsonObject } from "./rng-Bb-IutXB.js";
5
+ import { t as EditorSwitchOptions } from "./editor-switch-CFU9mCec.js";
5
6
  import { r as FrameStats } from "./frame-report-DNxDAb1w.js";
6
- import { t as LoadSceneOptions } from "./loader-DadV2QCT.js";
7
+ import { t as LoadSceneOptions } from "./loader-8-IHvbKD.js";
7
8
  import { n as AnimationEntry } from "./sprite-animation-CMr6f1K2.js";
8
9
  import { Group, Mesh, Object3D, Scene, Texture } from "three";
9
10
  import * as RapierNs from "@dimforge/rapier2d-compat";
@@ -53,6 +54,17 @@ declare class AssetStore2D {
53
54
  /** Load status of a declared asset KEY (no '$'), or undefined if unknown. */
54
55
  status(key: string): AssetStatus | undefined;
55
56
  getTexture(ref: string): Texture;
57
+ /**
58
+ * Hand the GPU back.
59
+ *
60
+ * `AssetStore3D` has had this since it was written and the 2D twin never got
61
+ * one, so a game that swapped scenes or unmounted kept every texture it had
62
+ * ever decoded. `Renderer2D.dispose` walked its scene graph and disposed
63
+ * `material.map` instead — which frees only what is CURRENTLY drawn (a sheet
64
+ * loaded and never used stayed resident), and disposes the STORE'S texture
65
+ * out from under a caller who injected their own store.
66
+ */
67
+ dispose(): void;
56
68
  getSheet(ref: string): SheetInfo;
57
69
  private resolve;
58
70
  }
@@ -161,7 +173,7 @@ declare class Area2D extends PhysicsBody2D {
161
173
  * Who is inside RIGHT NOW — the standing answer behind the enter/exit pair.
162
174
  * See `Area3D.overlapping`. Empty in a scene with no physics world.
163
175
  */
164
- overlapping(group?: string): PhysicsBody2D[];
176
+ overlapping(group?: string | undefined): PhysicsBody2D[];
165
177
  }
166
178
  /** Dynamic simulated body. */
167
179
  declare class RigidBody2D extends PhysicsBody2D {
@@ -174,6 +186,38 @@ declare class RigidBody2D extends PhysicsBody2D {
174
186
  restitution: number;
175
187
  /** px/s, y-down. Read back every step; write to launch. */
176
188
  linearVelocity: number[];
189
+ /**
190
+ * rad/s about the z axis — the same sign as `rotation`, which is CLOCKWISE
191
+ * in this y-down space. Read back every step; write to spin.
192
+ *
193
+ * `RigidBody3D` got its `angularVelocity` because "a puzzle builder could
194
+ * neither launch a spinning body nor read how fast a lever was swinging —
195
+ * only its angle, differenced by hand". Same sentence, same defect, other
196
+ * adapter: 2D had `linearVelocity` and no counterpart. `fixedRotation` pins
197
+ * it at zero.
198
+ */
199
+ angularVelocity: number;
200
+ /**
201
+ * The other name for `linearVelocity`, because it is the name every skill,
202
+ * every sibling node and every other engine uses.
203
+ *
204
+ * `body.velocity = [0, -400]` is what `incanto-physics-and-input.md` taught
205
+ * for years. On a plain TS class that assignment SUCCEEDS — it just writes a
206
+ * stray field nothing reads — so a jump that never happened produced no
207
+ * error, no warning and nothing to search for. `CharacterBody2D.velocity`
208
+ * next to `RigidBody2D.linearVelocity` is the kind of near-miss a builder
209
+ * cannot be expected to notice.
210
+ *
211
+ * Scene JSON still names the prop `linearVelocity` and rejects `velocity`
212
+ * loudly, with the valid keys listed — a hard error corrects itself.
213
+ */
214
+ get velocity(): number[];
215
+ set velocity(v: number[]);
216
+ /**
217
+ * Impulse in px·kg/s (y-down) — the 3D twin has had this since it shipped,
218
+ * and calling it here was a TypeError.
219
+ */
220
+ applyImpulse(impulse: [number, number]): void;
177
221
  }
178
222
  /**
179
223
  * Kinematic body on Rapier's character controller. Gravity is NOT applied
@@ -195,6 +239,17 @@ declare class CharacterBody2D extends PhysicsBody2D {
195
239
  stickToGround: boolean;
196
240
  /** Max climbable slope angle. */
197
241
  slopeLimitDeg: number;
242
+ /**
243
+ * How high a step this body walks up without jumping, in PIXELS.
244
+ *
245
+ * Rapier's character controller does not autostep unless it is asked, and in
246
+ * 2D nothing asked: a ledge of ANY height — measured, ONE pixel — stopped a
247
+ * walking body dead, while the mirrored `CharacterBody3D` has cleared 0.35 m
248
+ * by default since 0.63. The default here is that same 0.35 m at the 2D
249
+ * world's own scale (100 px = 1 m, which is why gravity defaults to 980).
250
+ * `0` restores the old behaviour.
251
+ */
252
+ stepHeight: number;
198
253
  /** @internal Updated by Physics2D.moveAndSlide. */
199
254
  _grounded: boolean;
200
255
  moveAndSlide(): void;
@@ -263,6 +318,22 @@ declare class Physics2D {
263
318
  /** Whether the solver runs at all. False = collider outlines only. */
264
319
  private readonly simulate;
265
320
  step(dt: number): void;
321
+ /**
322
+ * May `candidate` carry `rider`?
323
+ *
324
+ * The rule used to be "never a CharacterBody2D", which is the whole CLASS —
325
+ * and a `CharacterBody2D` animated by POSITION WRITES is the documented
326
+ * moving-platform path (`Oscillate`, `MoveTo`, `PathFollow`, or a behaviour of
327
+ * your own). `incanto-physics-and-input.md` promises twice that a character
328
+ * standing on ANY moving body is carried; it was not, and the rider reported
329
+ * `isOnFloor() === true` the whole time while sliding off in world space.
330
+ *
331
+ * What the guard was really for is a CYCLE: two characters standing on each
332
+ * other would carry each other forever. So the test is the rider's own
333
+ * carrier chain, not the type — a platform that is not (transitively) riding
334
+ * you can carry you.
335
+ */
336
+ private canCarry;
266
337
  /** @internal Called by CharacterBody2D.moveAndSlide (during tree fixedUpdate). */
267
338
  moveAndSlide(node: CharacterBody2D): void;
268
339
  /** Rapier debug segments, scaled back to pixels. Null while debugDraw is off. */
@@ -292,7 +363,31 @@ declare class Physics2D {
292
363
  /** The bodies currently overlapping `node`. Freed nodes are dropped on read. */
293
364
  overlapping(node: PhysicsBody2D): PhysicsBody2D[];
294
365
  /**
295
- * World-space raycast in PIXELS (y-down). `exclude` skips that body — a
366
+ * Mass the solver actually uses (collider-derived unless overridden).
367
+ *
368
+ * The 3D twin has had this since `CharacterController3D` needed it to size an
369
+ * impulse; 2D had no way to ask, so a 2D game scaling a push by mass had to
370
+ * read the AUTHORED `mass` prop and hope it was the number Rapier settled on.
371
+ */
372
+ massOf(node: PhysicsBody2D): number;
373
+ /**
374
+ * Current solver velocity in px/s (fresher than the node prop mid-step).
375
+ *
376
+ * `linearVelocity` is written back once per step, so a behavior reading it
377
+ * from inside `fixedUpdate` — after a collision, before the write-back — sees
378
+ * the value from BEFORE the impact. That is exactly when a game wants to know
379
+ * how hard it hit something.
380
+ */
381
+ velocityOf(node: PhysicsBody2D): [number, number];
382
+ /**
383
+ * A world-space impulse in px.kg/s (y-down) — the 2D twin of
384
+ * `Physics3D.applyImpulse`, which the physics skill has documented for both
385
+ * adapters while only one of them had it.
386
+ */
387
+ applyImpulse(node: PhysicsBody2D, impulse: [number, number]): void;
388
+ /**
389
+ * World-space raycast in PIXELS (y-down); `dir` may be any length (it is
390
+ * normalized), so `distance` and `maxLen` are pixels. `exclude` skips that body — a
296
391
  * shooter probing from inside its own collider needs it. Sensors (Area2D)
297
392
  * never block rays. Returns distance (px), the surface normal, and the hit
298
393
  * body — or null.
@@ -303,6 +398,27 @@ declare class Physics2D {
303
398
  distance: number;
304
399
  normal: [number, number];
305
400
  node: PhysicsBody2D | null;
401
+ point: [number, number];
402
+ } | null;
403
+ /**
404
+ * Sweep a CIRCLE of `radius` from `origin` along `dir` — a thick ray, in
405
+ * pixels. The 2D twin of `Physics3D.castSphere`.
406
+ *
407
+ * 3D has had this since the camera spring arm needed it and 2D never got it,
408
+ * so the shared `PhysicsQuery` interface declares it OPTIONAL and every 2D
409
+ * game that wanted a probe with THICKNESS — a shot that must not thread a
410
+ * one-pixel gap between two tiles, a ledge feeler, a camera that should not
411
+ * clip a wall it skims — had to fire several thin rays and hope.
412
+ *
413
+ * Same filters as `castRay`: sensors never hit, `staticOnly` ignores dynamic
414
+ * and kinematic bodies, `dir` may be any length. Returns the travel distance
415
+ * of the circle's CENTRE, or null when the sweep is clear.
416
+ */
417
+ castSphere(origin: [number, number], dir: [number, number], radius: number, maxLen: number, exclude?: PhysicsBody2D, opts?: {
418
+ staticOnly?: boolean;
419
+ }): {
420
+ distance: number;
421
+ node: PhysicsBody2D | null;
306
422
  } | null;
307
423
  dispose(): void;
308
424
  private lastStructure;
@@ -357,6 +473,14 @@ interface CreateGame2DOptions {
357
473
  * Attach pointer input (mouse, finger or pen) to the canvas: positions for
358
474
  * `input.pointerPosition()`, buttons for `input.mousePressed()`.
359
475
  * `{ lockOnClick: true }` captures the cursor — rare in 2D, so it is off.
476
+ *
477
+ * Omitted, it is AUTO: attached when the scene contains a `Clickable`, the
478
+ * same way `physics: 'auto'` turns Rapier on for a scene with bodies. It used
479
+ * to default OFF, so a board of clickable tiles did nothing when clicked —
480
+ * with no error, a clean `incanto-check` and a clean audit, while the skill
481
+ * that introduces the behaviour says it "needs no code at all". Pass `true`
482
+ * when a behaviour of your own reads `pointerPosition()`; nothing here can
483
+ * see that.
360
484
  */
361
485
  pointer?: boolean | {
362
486
  lockOnClick?: boolean;
@@ -412,6 +536,20 @@ interface CreateGame2DOptions {
412
536
  /** Renderer extras (pixelRatio, antialias, custom asset store). */
413
537
  pixelRatio?: number;
414
538
  antialias?: boolean;
539
+ /**
540
+ * Keep the drawing buffer readable so the canvas can be screenshot.
541
+ *
542
+ * WebGL clears it the instant a frame composites, so `toDataURL()` — and
543
+ * every screenshot built on it — is BLANK without this. Off by default: it
544
+ * costs a buffer copy per frame and only a run that means to look at the
545
+ * output should pay for it. `environment.rendering.preserveDrawingBuffer`
546
+ * does the same from the scene.
547
+ *
548
+ * The 2D renderer has always honoured the scene key; only the boot-site
549
+ * override was 3D-only, so a 2D game whose harness wanted a screenshot had to
550
+ * edit its scene file to get one.
551
+ */
552
+ preserveDrawingBuffer?: boolean;
415
553
  resolveScene?: LoadSceneOptions["resolveScene"];
416
554
  /** @internal Test seam — replaces the Renderer2D construction. */
417
555
  _rendererFactory?: (engine: Engine, canvas: HTMLCanvasElement) => GameRenderer;
@@ -569,6 +707,18 @@ declare class Sprite2D extends Node2D {
569
707
  protected resolveTexture(assets: AssetStore2D | null): ResolvedSpriteTexture | null;
570
708
  override _staticReady(assets: AssetStore2D | null): boolean;
571
709
  override _syncObject2D(assets: AssetStore2D | null): void;
710
+ /**
711
+ * The material is PER NODE and nothing freed it.
712
+ *
713
+ * The geometry is shared for the process (`UNIT_PLANE`, on purpose), but
714
+ * every sprite makes its own `MeshBasicMaterial` — and a 2D game that spawns
715
+ * and frees sprites, which is every 2D game, leaked one per node forever.
716
+ * The TEXTURE is not touched: it belongs to the asset store and is shared by
717
+ * every node that names the same key.
718
+ *
719
+ * `AnimatedSprite2D` extends this, so it is covered by the same override.
720
+ */
721
+ override free(): void;
572
722
  }
573
723
  //#endregion
574
724
  //#region src/2d/nodes/animated-sprite-2d.d.ts
@@ -636,6 +786,8 @@ declare class Camera2D extends Node2D {
636
786
  current: boolean;
637
787
  /** `zoom` clamped to a positive floor — 0/negative zoom must never produce NaN views. */
638
788
  get effectiveZoom(): number;
789
+ /** The `follow` path already reported as untrackable — cleared by a new one. */
790
+ private warnedFollow;
639
791
  override update(dt: number): void;
640
792
  /** View center after clamping the (vw×vh)/zoom view rect inside `limits`. */
641
793
  /**
@@ -663,26 +815,6 @@ declare class Camera2D extends Node2D {
663
815
  }
664
816
  //#endregion
665
817
  //#region src/2d/nodes/character-controller-2d.d.ts
666
- /**
667
- * Preset controller: drives its PARENT CharacterBody2D from the engine
668
- * InputMap — JSON-only games get a playable character with zero TypeScript.
669
- *
670
- * - `platformer`: x movement + gravity (from the scene `physics.gravity[1]`,
671
- * default 980) + jump (`jumpHeight` px → impulse √(2·g·h))
672
- * - `topDown`: full-axis movement, no gravity
673
- *
674
- * ## Why the extra platformer props exist
675
- *
676
- * A jump that only works while `isOnFloor()` is true feels BROKEN, and players
677
- * do not report it as "the coyote time is missing" — they report the game as
678
- * unresponsive. The whole schema used to be five props, so a JSON-only game got
679
- * a character that felt wrong and could not drive `AnimatedSprite2D.play('run')`
680
- * either. `examples/platformer-2d` hand-rolls all of this in a 541-line
681
- * behavior; these are those constants, as props.
682
- *
683
- * Every one defaults to OFF (0 / 1 / false), so an existing scene behaves
684
- * exactly as before until it asks for something.
685
- */
686
818
  declare class CharacterController2D extends Node {
687
819
  static override readonly typeName: string;
688
820
  static readonly props: PropSchema;
@@ -718,6 +850,29 @@ declare class CharacterController2D extends Node {
718
850
  * from scene JSON instead of a behavior polling velocities.
719
851
  */
720
852
  private setState;
853
+ /** Actions already reported missing, so a per-frame read says it once. */
854
+ private readonly reportedActions;
855
+ /**
856
+ * Read an input action this controller is allowed to be missing.
857
+ *
858
+ * The 3D controller has done this since `optional-action.ts` was written —
859
+ * whose own docstring says "`CharacterController3D` and `GameFlow` wrap every
860
+ * input read" — and the 2D one did not. So a top-down 2D game, which is the
861
+ * first thing anyone builds with this node and has no jump key, declared
862
+ * `move` and got:
863
+ *
864
+ * ```
865
+ * [incanto] /Game/Player/Ctl (CharacterController2D) threw in fixedUpdate —
866
+ * this node is now SKIPPED. IncantoError: Unknown input action 'jump'.
867
+ * Declared actions: [move].
868
+ * ```
869
+ *
870
+ * SKIPPED: the character could not move either, and every tool reported the
871
+ * game as `✗ error in 1/1` with the reason only in the log. Tolerated because
872
+ * a game with no jump key is the ordinary case; reported because otherwise a
873
+ * TYPO in `"jumpAction"` looks exactly like a deliberate omission.
874
+ */
875
+ private readAction;
721
876
  }
722
877
  //#endregion
723
878
  //#region src/2d/nodes/color-rect-2d.d.ts
@@ -739,6 +894,20 @@ declare class ColorRect2D extends Node2D {
739
894
  /** @internal The drawable quad (lazily created under the backing object). */
740
895
  _quad(): Mesh;
741
896
  override _syncObject2D(assets: AssetStore2D | null): void;
897
+ /**
898
+ * The material is PER NODE and nothing freed it.
899
+ *
900
+ * The geometry is shared for the process (`UNIT_PLANE`, on purpose — a
901
+ * `THREE.Sprite` cannot be instanced and this is the discipline that replaces
902
+ * it), but every rect makes its own `MeshBasicMaterial`, and a 2D game that
903
+ * spawns and frees — bullets, coins, damage flashes, a level swap — leaks one
904
+ * GPU program's worth of state per node, forever.
905
+ *
906
+ * Four 3D nodes had exactly this and were fixed. These two 2D siblings make a
907
+ * material on the same line of the same kind of `_quad()` helper, and were
908
+ * not on that list.
909
+ */
910
+ override free(): void;
742
911
  }
743
912
  //#endregion
744
913
  //#region src/2d/nodes/joint-2d.d.ts
@@ -982,6 +1151,21 @@ declare class TileMap2D extends Node2D {
982
1151
  set cells(value: (string | number[])[]);
983
1152
  get legend(): Record<string, number>;
984
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;
985
1169
  get solid(): number[];
986
1170
  set solid(value: number[]);
987
1171
  opacity: number;
@@ -992,6 +1176,62 @@ declare class TileMap2D extends Node2D {
992
1176
  static validateJson(node: Node): void;
993
1177
  /** The parsed index grid (test/tooling hook). */
994
1178
  grid(): number[][];
1179
+ /**
1180
+ * Which CELL is under this world point — `null` outside the grid.
1181
+ *
1182
+ * The one question a tile game asks constantly and this node could not
1183
+ * answer. `VoxelGrid3D`, the 3D grid, has had `tileAt` since it was written;
1184
+ * the 2D one shipped `grid()` and nothing else, so every mouse-driven tile
1185
+ * game hand-rolled the same arithmetic with the same two traps in it: cell
1186
+ * (0,0) hangs its TOP-LEFT on the origin (so a cell's CENTRE is half a tile
1187
+ * further on), and the origin is the node's WORLD position, not its `position`
1188
+ * prop.
1189
+ *
1190
+ * Composes ancestor POSITIONS, like every other world query in the engine
1191
+ * (`Terrain3D.heightAt`, `Water3D`); `auditScene` reports an ancestor that
1192
+ * scales or rotates one of these, because that moves the picture and not the
1193
+ * answer.
1194
+ */
1195
+ cellAt(worldX: number, worldY: number): [number, number] | null;
1196
+ /**
1197
+ * One cell's size in WORLD pixels — `tileSize` times this node's own scale.
1198
+ *
1199
+ * `scale` is an ordinary `Node2D` prop, the renderer scales the mesh by it,
1200
+ * and `rebuildColliders` has always rebuilt when it changes — so at
1201
+ * `scale: [2, 2]` a tile is 64 px wide on screen AND in physics. These
1202
+ * queries divided by 32 and answered the wrong cell, in silence, which is the
1203
+ * picture and the collider agreeing and the decider disagreeing with both.
1204
+ *
1205
+ * Ancestor scale is not composed here, for the same reason `worldPosition`
1206
+ * does not: every world query in the engine reads its own props and composes
1207
+ * ancestor POSITIONS, and `auditScene` reports an ancestor that scales one.
1208
+ */
1209
+ private scaledTile;
1210
+ /** The atlas index at a cell — `-1` for an empty cell or one off the grid. */
1211
+ tileAt(cx: number, cy: number): number;
1212
+ /**
1213
+ * The CENTRE of a cell, in world pixels — for putting a highlight, a marker
1214
+ * or a spawned enemy on a tile.
1215
+ *
1216
+ * The centre rather than the corner, because that is where a thing goes and
1217
+ * because the corner is the half-tile every hand-rolled version forgets.
1218
+ */
1219
+ worldAt(cx: number, cy: number): [number, number];
1220
+ /**
1221
+ * Write ONE cell, by the character the row holds — dig a tile out, drop a
1222
+ * wall in, flip a switch.
1223
+ *
1224
+ * `cells` are strings and a mutation is not watched, so the documented way to
1225
+ * change a map was to rebuild the whole row by hand
1226
+ * (`row.slice(0, x) + c + row.slice(x + 1)`) and reassign the array. This is
1227
+ * that, once, correctly, and it rebuilds the geometry and the colliders.
1228
+ *
1229
+ * A CHARACTER, not an index: `legend` may map several characters to one tile,
1230
+ * so an index cannot say which of them you meant. `'.'` empties the cell.
1231
+ * Returns false for a cell off the grid, or a numeric row (those hold indices
1232
+ * — assign the row).
1233
+ */
1234
+ setTile(cx: number, cy: number, char: string): boolean;
995
1235
  override onReady(): void;
996
1236
  override update(_dt: number): void;
997
1237
  private rebuildColliders;
@@ -1070,6 +1310,12 @@ interface Renderer2DOptions {
1070
1310
  pixelRatio?: number;
1071
1311
  /** MSAA on the canvas. Defaults to TRUE (Phaser parity). */
1072
1312
  antialias?: boolean;
1313
+ /**
1314
+ * Keep the drawing buffer readable so the canvas can be screenshot — see
1315
+ * `CreateGame2DOptions`. `environment.rendering.preserveDrawingBuffer` does
1316
+ * the same from the scene, and an explicit option here outranks it.
1317
+ */
1318
+ preserveDrawingBuffer?: boolean;
1073
1319
  }
1074
1320
  declare class Renderer2D {
1075
1321
  /**
@@ -1120,6 +1366,7 @@ declare class Renderer2D {
1120
1366
  private readonly engine;
1121
1367
  /** The texture store. Public so `renderer.assets.status(key)` is reachable. */
1122
1368
  readonly assets: AssetStore2D;
1369
+ private readonly ownsAssets;
1123
1370
  private readonly loadedAssetScenes;
1124
1371
  private readonly disconnect;
1125
1372
  private readonly canvas;
@@ -1132,7 +1379,29 @@ declare class Renderer2D {
1132
1379
  /** UI-pass extent from the last render — the space HUD nodes are posed in. */
1133
1380
  private lastUi;
1134
1381
  private lastViewport;
1382
+ /**
1383
+ * The view the LAST render used — and, before there has been one, the
1384
+ * documented default: the whole canvas, centred, at zoom 1.
1385
+ *
1386
+ * It started as `{cx: 0, cy: 0, w: 1, h: 1}`, a one-pixel canvas nobody has,
1387
+ * and `worldFromScreen` answered from it with a straight face. Measured in a
1388
+ * browser on a 960x540 canvas with the camera at (160, 80), before the first
1389
+ * frame: `toWorld(480, 270)` returned `[479.5, 269.5]` — the arithmetic of a
1390
+ * 1x1 window, and a confident number rather than a refusal.
1391
+ */
1135
1392
  private lastView;
1393
+ /**
1394
+ * Has a frame been DRAWN?
1395
+ *
1396
+ * `Renderer3D` refuses every screen-space question until it has one
1397
+ * (`if (!this.lastCamera) return null`, in `pick`, `rayFromScreen` and
1398
+ * `worldFromScreen`); the flat renderer had no equivalent, so the engine-level
1399
+ * `toWorld`/`toScreen` and `pick` answered from a view no frame had used. The
1400
+ * hazard is real in exactly the place agents work: a background tab does not
1401
+ * run rAF, so `await createGame2D(...)` there is followed by any number of
1402
+ * questions with no frame behind them.
1403
+ */
1404
+ private hasRendered;
1136
1405
  /** The world view used by the LAST render (center/zoom/canvas size). */
1137
1406
  view(): View2D;
1138
1407
  /**
@@ -1234,13 +1503,6 @@ interface TileRect {
1234
1503
  w: number;
1235
1504
  h: number;
1236
1505
  }
1237
- /**
1238
- * Normalize the `cells` prop into an index grid. Two author formats:
1239
- * - string rows: `"." `/`" "` = empty, digits `0-9` = tile index, anything
1240
- * else must be in `legend` (char → index). Unknown chars HARD-fail.
1241
- * - number rows: passed through (`-1` = empty).
1242
- * Ragged rows are right-padded with empty.
1243
- */
1244
1506
  declare function parseCells(cells: readonly (string | readonly number[])[], legend: Record<string, number>): number[][];
1245
1507
  /**
1246
1508
  * Greedy rectangle merge over the solid cells: maximal horizontal runs,
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
- import { t as isWebGLAvailable } from "./webgl-unavailable-Z5_g8x9H.js";
4
- import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-CwxEDlCm.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-DbVArsns.js";
6
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-CF5yyjbj.js";
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-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