incanto 0.65.0 → 0.67.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 (88) hide show
  1. package/bin/_behaviors-loader.mjs +87 -0
  2. package/bin/incanto-editor.mjs +39 -2
  3. package/bin/incanto-feel.mjs +4 -1
  4. package/bin/incanto-frame.mjs +52 -3
  5. package/bin/incanto-logs.mjs +33 -3
  6. package/bin/incanto-multiplay.mjs +50 -1
  7. package/bin/incanto-play.mjs +3 -1
  8. package/bin/incanto-playtest.mjs +3 -25
  9. package/bin/incanto-verify.mjs +15 -2
  10. package/dist/2d.d.ts +76 -4
  11. package/dist/2d.js +3 -3
  12. package/dist/3d.d.ts +213 -14
  13. package/dist/3d.js +6 -6
  14. package/dist/{audio-player-BrEHwbK1.d.ts → audio-player-BNUHo0JF.d.ts} +1 -1
  15. package/dist/{behavior-CmMuQ6CL.d.ts → behavior-ZnfzKDo3.d.ts} +58 -1
  16. package/dist/{create-game-DIyh5Zy9.js → create-game-DMetd3vR.js} +14 -8
  17. package/dist/{create-game-W2xzTJhs.js → create-game-FAXdHta3.js} +138 -11
  18. package/dist/debug.d.ts +1 -1
  19. package/dist/debug.js +3 -2
  20. package/dist/{duplicate-CqSAtdrh.js → duplicate-B8QnX2-r.js} +1 -1
  21. package/dist/editor.js +7 -2
  22. package/dist/{environment-presets-BvHs6HEk.js → environment-presets-C-5g2pMh.js} +113 -9
  23. package/dist/{gameplay-D-H986A4.js → gameplay-CGSv-9XR.js} +295 -12
  24. package/dist/gameplay.d.ts +102 -6
  25. package/dist/gameplay.js +2 -2
  26. package/dist/index.d.ts +50 -6
  27. package/dist/index.js +7 -6
  28. package/dist/{loader-CTB441z4.d.ts → loader-DyT30ArB.d.ts} +1 -1
  29. package/dist/{loader-lQDCwNag.js → loader-KZESOZmE.js} +148 -1
  30. package/dist/net.d.ts +2 -2
  31. package/dist/net.js +1 -1
  32. package/dist/{physics-2d-CmnunaR2.js → physics-2d-_B6zj3zU.js} +57 -4
  33. package/dist/{physics-3d-DULB1lD1.js → physics-3d-i1IeAY0F.js} +74 -5
  34. package/dist/{quiet-rapier-BAJ4K94N.js → quiet-rapier-C6fW4zcW.js} +14 -1
  35. package/dist/react.d.ts +1 -1
  36. package/dist/react.js +1 -1
  37. package/dist/{register-CgeR4ckF.js → register-Cs53CY7s.js} +110 -16
  38. package/dist/{register-DO5_Qp0Q.js → register-vIxU3Xbv.js} +38 -4
  39. package/dist/rendering-options-ByAdi8PF.js +184 -0
  40. package/dist/{replay-CEw2xAhG.js → replay-BkpDV51O.js} +4 -3
  41. package/dist/{replay-CThvwkcQ.d.ts → replay-D0GcEHYU.d.ts} +1 -1
  42. package/dist/{split-screen-DxZzC1nm.d.ts → split-screen-DD5eYIgW.d.ts} +2 -2
  43. package/dist/{split-screen-gb0c3xdb.js → split-screen-DS8RaUV_.js} +2 -2
  44. package/dist/{src-DYffzHbd.js → src-BLlsGJW9.js} +1 -1
  45. package/dist/{teardown-BordHL9o.js → teardown-LB9cTnXJ.js} +2 -2
  46. package/dist/{test-CXZuXs8k.js → test-81uZ967a.js} +127 -24
  47. package/dist/test.d.ts +7 -5
  48. package/dist/test.js +2 -2
  49. package/dist/{touch-DESwnpOc.js → touch-DEAmqGdf.js} +30 -186
  50. package/dist/vite.d.ts +0 -7
  51. package/dist/vite.js +13 -3
  52. package/editor/assets/{agent8-DO5qzIOD.js → agent8-C2jtNKbx.js} +1 -1
  53. package/editor/assets/debug-DoU3KeqS.js +3 -0
  54. package/editor/assets/{index-CqEUVaPN.js → index-CNqHojcC.js} +92 -92
  55. package/editor/index.html +1 -1
  56. package/package.json +1 -1
  57. package/schemas/scene.schema.json +20 -1
  58. package/skills/incanto-building-3d-games.md +19 -0
  59. package/skills/incanto-editor.md +5 -1
  60. package/skills/incanto-environment.md +2 -2
  61. package/skills/incanto-gameplay-behaviors.md +86 -7
  62. package/skills/incanto-hud.md +14 -1
  63. package/skills/incanto-multiplayer.md +14 -0
  64. package/skills/incanto-node-reference.md +60 -2
  65. package/skills/incanto-performance.md +35 -0
  66. package/skills/incanto-physics-and-input.md +69 -0
  67. package/skills/incanto-playtesting.md +15 -1
  68. package/skills/incanto-save-slots.md +21 -1
  69. package/skills/incanto-verifying-your-game.md +19 -0
  70. package/skills/incanto-web-integration.md +24 -3
  71. package/skills/incanto-your-first-game.md +4 -4
  72. package/templates-app/beacon-isle-3d/package.json +1 -1
  73. package/templates-app/beacon-isle-3d/src/game.scene.json +38 -19
  74. package/templates-app/platformer-2d/package.json +1 -1
  75. package/templates-app/platformer-2d/src/game.scene.json +99 -53
  76. package/templates-app/star-survivor/package.json +1 -1
  77. package/templates-app/star-survivor/src/game.scene.json +31 -17
  78. package/templates-app/tps-3d/package.json +1 -1
  79. package/templates-app/tps-3d/src/game.scene.json +14 -8
  80. package/templates-app/village-quest-3d/package.json +1 -1
  81. package/templates-app/village-quest-3d/src/grove.scene.json +20 -11
  82. package/templates-app/village-quest-3d/src/village.scene.json +10 -7
  83. package/editor/assets/debug-DntG9CVD.js +0 -3
  84. package/templates-app/beacon-isle-3d/coverage.json +0 -9
  85. package/templates-app/platformer-2d/coverage.json +0 -5
  86. package/templates-app/star-survivor/coverage.json +0 -5
  87. package/templates-app/tps-3d/coverage.json +0 -5
  88. package/templates-app/village-quest-3d/coverage.json +0 -9
package/dist/3d.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { D as QualityCaps, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, w as GameStats } from "./behavior-CmMuQ6CL.js";
1
+ import { D as QualityCaps, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, w as GameStats, x as EngineOptions } from "./behavior-ZnfzKDo3.js";
2
2
  import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DAvWQeld.js";
3
3
  import { i as SceneJson$1, s as JsonObject } from "./schema-CFeioQRE.js";
4
4
  import { a as GridCell, c as diffText, d as frameText, i as FrameStatsOptions, l as frameSignature, n as FrameSignature, o as SIGNATURE_GRID, r as FrameStats, s as diffSignatures, t as FrameDiff, u as frameStats } from "./frame-report-DCnHFmto.js";
5
- import { t as LoadSceneOptions } from "./loader-CTB441z4.js";
6
- import { i as ParticleSim, o as ParticleView, r as SpatialPose } from "./audio-player-BrEHwbK1.js";
5
+ import { t as LoadSceneOptions } from "./loader-DyT30ArB.js";
6
+ import { i as ParticleSim, o as ParticleView, r as SpatialPose } from "./audio-player-BNUHo0JF.js";
7
7
  import { n as PathGrid } from "./pathfinding-_fGrCFmH.js";
8
8
  import { AnimationClip, AnimationMixer, BufferGeometry, Color, DirectionalLight, Group, InstancedMesh, Mesh, MeshPhysicalMaterial, Object3D, PerspectiveCamera, Scene, ShaderMaterial, Texture, Vector3, WebGLRenderer } from "three";
9
9
  import { VRM } from "@pixiv/three-vrm";
@@ -133,6 +133,31 @@ interface AnimationEntry {
133
133
  */
134
134
  declare class AssetStore3D {
135
135
  private readonly engine;
136
+ /**
137
+ * One geometry and one material per distinct SHAPE and per distinct look.
138
+ *
139
+ * `MeshInstance3D._createObject3D` used to `new BoxGeometry()` and
140
+ * `new MeshPhysicalMaterial()` for every node, even when byte-identical.
141
+ * Three identical nodes reported `geometry shared? false false / material
142
+ * shared? false false`, with distinct uuids. Measured at 3,200 units, all
143
+ * three runs at identical 3,209 draw calls / 1,741,932 triangles and an
144
+ * identical screenshot:
145
+ *
146
+ * ```
147
+ * per-node geometry + material 31.0 ms wall (renderMs 27.3)
148
+ * share one geometry 27.0 ms
149
+ * + share two materials 15.4 ms <- 2x the headroom
150
+ * ```
151
+ *
152
+ * 13.1 ms of that 26.5 ms cliff was GPU state churn from per-node objects,
153
+ * and none of it was visible headlessly: with no renderer `_syncObject3D`
154
+ * never runs, so the bench measured 55 ns/node instead of ~2,000.
155
+ *
156
+ * Renderer-scoped, deliberately — module-level state is the predecessor's
157
+ * fatal flaw and two games in one page must not share a cache.
158
+ */
159
+ readonly meshGeometries: Map<string, unknown>;
160
+ readonly meshMaterials: Map<string, unknown>;
136
161
  private readonly models;
137
162
  private readonly animations;
138
163
  private readonly modelKeys;
@@ -358,6 +383,33 @@ declare class StaticBody3D extends PhysicsBody3D {
358
383
  /** Sensor volume emitting `triggerEnter(other)` / `triggerExit(other)`. */
359
384
  declare class Area3D extends PhysicsBody3D {
360
385
  static override readonly typeName: string;
386
+ /**
387
+ * Who is inside RIGHT NOW — the standing answer behind the enter/exit pair.
388
+ *
389
+ * Enter and exit tell you about crossings; a pressure plate, a capture point,
390
+ * a "how many enemies are in the blast" count and a shop trigger all want
391
+ * OCCUPANCY. Without this every one of them kept a private `Set` fed by the
392
+ * two signals, and had to guard it, because a body that is teleported or
393
+ * freed inside a sensor does not reliably announce its exit. Physics already
394
+ * knows; now it says.
395
+ *
396
+ * ```ts
397
+ * const load = this.area.overlapping()
398
+ * .reduce((kg, b) => kg + ((b as RigidBody3D).mass ?? 0), 0);
399
+ * door.open = load >= 60;
400
+ * ```
401
+ *
402
+ * It reports everything the solver says is inside, STATIC WORLD INCLUDED — a
403
+ * plate laid into the floor genuinely contains the floor. Pass a group to ask
404
+ * the question you usually mean:
405
+ *
406
+ * ```ts
407
+ * area.overlapping('crate') // only things tagged `crate`
408
+ * ```
409
+ *
410
+ * Empty in a scene with no physics world.
411
+ */
412
+ overlapping(group?: string): PhysicsBody3D[];
361
413
  }
362
414
  /** Dynamic simulated body. */
363
415
  declare class RigidBody3D extends PhysicsBody3D {
@@ -370,6 +422,14 @@ declare class RigidBody3D extends PhysicsBody3D {
370
422
  restitution: number;
371
423
  /** m/s, y-up. Read back every step; write to launch. */
372
424
  linearVelocity: number[];
425
+ /**
426
+ * rad/s about each world axis. Read back every step; write to spin.
427
+ *
428
+ * A puzzle builder could neither launch a spinning body nor read how fast a
429
+ * lever was swinging — only its angle, differenced by hand — because this had
430
+ * no counterpart to `linearVelocity`. `fixedRotation` pins it at zero.
431
+ */
432
+ angularVelocity: number[];
373
433
  /** @internal set by Physics3D */
374
434
  _physics3d: Physics3D | null;
375
435
  /** World-space impulse (kg·m/s) — the floating-capsule controller's verb. */
@@ -539,6 +599,27 @@ declare class Physics3D {
539
599
  velocityOf(node: PhysicsBody3D): [number, number, number];
540
600
  /** Create newly-arrived joints; tear down departed ones. */
541
601
  private syncJoints;
602
+ /** Who is currently inside each sensor/body, maintained from the event drain. */
603
+ private readonly overlaps;
604
+ private trackOverlap;
605
+ /**
606
+ * Forget every overlap whose node has left the tree.
607
+ *
608
+ * The lazy drop inside `overlapping()` only cleans the SETS of a key someone
609
+ * asks about, and only when they ask. This clears the KEYS, which is where the
610
+ * dead nodes were pinned.
611
+ */
612
+ /** Drop one node from the overlap map, both as a key and as a member. */
613
+ private forgetOverlaps;
614
+ private pruneOverlaps;
615
+ /**
616
+ * The bodies currently overlapping `node` — the standing answer behind
617
+ * `triggerEnter`/`triggerExit`.
618
+ *
619
+ * Freed nodes are dropped on read rather than tracked, so a hazard that
620
+ * despawns inside a plate cannot leave a ghost holding a door open.
621
+ */
622
+ overlapping(node: PhysicsBody3D): PhysicsBody3D[];
542
623
  /**
543
624
  * World-space raycast (meters). `exclude` skips that body's collider —
544
625
  * ground probes from inside a capsule need it. `opts.staticOnly` hits ONLY the
@@ -637,6 +718,18 @@ interface CreateGame3DOptions {
637
718
  */
638
719
  editor?: boolean | EditorSwitchOptions;
639
720
  seed?: number;
721
+ /**
722
+ * Namespace this game's persisted settings — volume, language, quality tier.
723
+ *
724
+ * They are keyed `incanto:<namespace>:<key>` in the browser's storage and
725
+ * defaulted to `settings` for everyone, so two Incanto games on one domain
726
+ * shared one options screen. Measured: game A's player sets `renderScale
727
+ * 0.25, muted, quality low`; game B, whose source never mentions settings,
728
+ * boots at a 480x232 drawing buffer and silent.
729
+ *
730
+ * `SaveSlots(namespace)` already takes one for exactly this reason.
731
+ */
732
+ settings?: EngineOptions["settings"];
640
733
  fixedHz?: number;
641
734
  pixelRatio?: number;
642
735
  /** Keep the frame rate by rendering fewer pixels while frames run slow
@@ -686,7 +779,22 @@ interface GameRenderer {
686
779
  }
687
780
  interface Game3D {
688
781
  engine: Engine;
689
- scene: Scene$1;
782
+ /**
783
+ * The LIVE scene — a getter over `engine.scene`, not a snapshot.
784
+ *
785
+ * This used to be the scene captured at boot. `Engine.setScene` calls
786
+ * `this._scene?.root.free()`, so after any swap the old handle was not merely
787
+ * stale, it was EMPTIED — and a single-scene game reaches that through
788
+ * `flow.restart()`, which `incanto-hud.md` wires to a Start button. Measured:
789
+ *
790
+ * before swap game.scene.root.children = 2 engine.scene = 2
791
+ * after swap game.scene.root.children = 0 engine.scene = 2
792
+ * game.scene.root.getNode('Flow') -> No node at 'Flow'. Children here: [].
793
+ *
794
+ * `game.engine.scene` always worked and three skills use it, so there were
795
+ * two handles with one name and no note saying which was which.
796
+ */
797
+ get scene(): Scene$1;
690
798
  /**
691
799
  * The AUTHORED scene this game booted from — the JSON, not the live tree.
692
800
  *
@@ -982,19 +1090,25 @@ declare class Environment3D {
982
1090
  }
983
1091
  //#endregion
984
1092
  //#region src/3d/environment-runtime.d.ts
1093
+ declare function setEnvironment3D(engine: Engine, patch: JsonObject, opts?: {
1094
+ rebaseTierKeys?: boolean;
1095
+ }): void;
985
1096
  /**
986
- * Live environment editing — the renderer re-applies `scene.environment`
987
- * every frame (cheap when unchanged), so changing the LOOK at runtime is
988
- * just writing that object. This helper does it safely:
1097
+ * Apply a quality tier — the only caller that may RESET as well as patch.
989
1098
  *
990
- * setEnvironment3D(engine, { sky: { elevationDeg: 4 }, exposure: 0.8 });
991
- * setEnvironment3D(engine, { fog: null }); // null DELETES a key
1099
+ * A tier is re-applied every time the player moves the slider, so it cannot
1100
+ * compose with what the last one left behind: `low` strips the author's bloom,
1101
+ * and `high` then has nothing to put back. This re-derives the four tier-owned
1102
+ * stages from the scene FILE first, so every tier is reversible, and so a stage
1103
+ * the author never wrote can never appear — the contract `settings.ts` states
1104
+ * and `incanto-performance.md` promises: *the tier can only take things away.*
992
1105
  *
993
- * - one-level deep merge (patching `sky.elevationDeg` keeps the other sky keys)
994
- * - hard-validates the merged result NOW (in your call stack), so a bad patch
995
- * throws where you wrote itnever inside the render loop
1106
+ * Measured before this, on a scene with no `bloom` and no `post` in its JSON:
1107
+ * `high` and `medium` both rendered five GL passes at 3.3 ms where `low`
1108
+ * rendered one at 1.24 ms 62% of the frame on a chain the game never asked
1109
+ * for, and `environment.bloom` read back `{strength: 0.8, threshold: 1}`.
996
1110
  */
997
- declare function setEnvironment3D(engine: Engine, patch: JsonObject): void;
1111
+ declare function applyQualityTier3D(engine: Engine, patch: JsonObject): void;
998
1112
  //#endregion
999
1113
  //#region src/3d/model-verdict.d.ts
1000
1114
  /**
@@ -1264,6 +1378,25 @@ declare class CharacterController3D extends Node3D {
1264
1378
  airControl: number;
1265
1379
  fallGravity: number;
1266
1380
  floatHeight: number;
1381
+ /**
1382
+ * How high a step the PLAYER walks up without jumping (0 = off).
1383
+ *
1384
+ * The hover spring holds the capsule a fixed distance above whatever is
1385
+ * DIRECTLY below it, so a vertical face stopped the player dead at about
1386
+ * 0.2 m — while `CharacterBody3D.stepHeight` let every enemy climb 0.35 m.
1387
+ * The docs said the player did not need this ("it floats over small ledges
1388
+ * already"); it floats over 18 cm ones.
1389
+ *
1390
+ * This probes just AHEAD of the capsule in the direction of travel: if the
1391
+ * ground there is higher, but by no more than `stepHeight`, the spring aims
1392
+ * at THAT surface and lifts the character onto it. A wall taller than the
1393
+ * step is not a step and still stops you.
1394
+ *
1395
+ * Do not reach for `floatHeight` instead. It does raise the character over
1396
+ * ledges, and it silently makes it WEIGHTLESS: at `floatHeight: 0.28` a
1397
+ * 200 kg character does not move a seesaw a 70 kg crate tips.
1398
+ */
1399
+ stepHeight: number;
1267
1400
  mouseLook: boolean;
1268
1401
  zoomMin: number;
1269
1402
  zoomMax: number;
@@ -1727,6 +1860,10 @@ declare class MeshInstance3D extends Node3D {
1727
1860
  /** Loader hook: malformed materials fail at LOAD time, not at render. */
1728
1861
  static validateJson(node: Node): void;
1729
1862
  private geometryKey;
1863
+ private materialKey;
1864
+ /** True while this node is using a cache entry it must not dispose. */
1865
+ private sharedGeometry;
1866
+ private sharedMaterial;
1730
1867
  private textureKey;
1731
1868
  private loadedTextures;
1732
1869
  protected override _createObject3D(): Object3D;
@@ -1836,6 +1973,31 @@ declare class Joint3D extends Node3D {
1836
1973
  length: number;
1837
1974
  stiffness: number;
1838
1975
  damping: number;
1976
+ /**
1977
+ * Do the two linked bodies still collide with EACH OTHER?
1978
+ *
1979
+ * `null` (default) decides from the joint type, because the two families want
1980
+ * opposite answers:
1981
+ *
1982
+ * - **pivots** (`fixed`, `spherical`, `revolute`) → **off**. A hinge's bodies
1983
+ * overlap at the pivot by construction, so the contact solver fights the
1984
+ * joint and flings them. A puzzle builder's first hinge produced a bar that
1985
+ * spun chaotically forever and threw a 40 kg box thirty metres, and they
1986
+ * nearly filed "spherical joints inject energy":
1987
+ *
1988
+ * ```
1989
+ * t= 0 Plank r=[0,0,0] Box [ 0.00, 5.50, 2.40]
1990
+ * t=3000 Plank r=[-66,24,64] Box [-29.70, 0.40, -4.91]
1991
+ * ```
1992
+ *
1993
+ * - **tethers** (`rope`, `spring`) → **on**. These anchor a thing to the world,
1994
+ * and the thing has to rest on what it is tethered to. Defaulting these off
1995
+ * drops a barrel roped to the floor straight through it — measured, in this
1996
+ * repo's own conformance room.
1997
+ *
1998
+ * Set `true`/`false` to say it outright.
1999
+ */
2000
+ collide: boolean | null;
1839
2001
  override onEnterTree(): void;
1840
2002
  /** @internal The two bodies, resolved (throws on a bad target at step time). */
1841
2003
  _resolveBodies(): {
@@ -3719,6 +3881,43 @@ declare class Renderer3D {
3719
3881
  behind: boolean;
3720
3882
  };
3721
3883
  /**
3884
+ * The WORLD POINT under canvas pixel (sx, sy) — the other half of
3885
+ * `screenFromWorld`, and the call `incanto-web-integration.md` has always
3886
+ * offered "for taps and clicks".
3887
+ *
3888
+ * It did not exist in 3D. `Renderer2D` has had it since the beginning, the
3889
+ * doc covers both runtimes, and an agent building a tower defense went looking
3890
+ * for it and measured `typeof game.renderer.worldFromScreen === "undefined"`.
3891
+ * `pick()` gives you the NODE under a pixel but never the POINT, so "put the
3892
+ * thing where the player clicked on the ground" was not expressible and that
3893
+ * game discretised its whole map into clickable pads instead.
3894
+ *
3895
+ * A pixel in 3D is a RAY, not a point, so this needs a surface to land on and
3896
+ * takes the horizontal plane at `planeY` (default 0 — the ground). Returns
3897
+ * null when the ray runs parallel to that plane or would only meet it behind
3898
+ * the camera, which is what "the sky" looks like from here.
3899
+ *
3900
+ * For anything not flat — terrain, a hillside, a stack of crates — raycast
3901
+ * physics instead: `rayFromScreen` gives you the origin and direction to feed
3902
+ * `physics.castRay`.
3903
+ */
3904
+ worldFromScreen(sx: number, sy: number, planeY?: number): {
3905
+ x: number;
3906
+ y: number;
3907
+ z: number;
3908
+ } | null;
3909
+ /**
3910
+ * The camera ray through canvas pixel (sx, sy) — origin and unit direction,
3911
+ * ready for `physics.castRay(origin, dir, maxLen)`.
3912
+ *
3913
+ * This is the honest 3D answer to "where did the player click": on uneven
3914
+ * ground the only thing that knows is the collision world.
3915
+ */
3916
+ rayFromScreen(sx: number, sy: number): {
3917
+ origin: [number, number, number];
3918
+ dir: [number, number, number];
3919
+ } | null;
3920
+ /**
3722
3921
  * The node rendered under canvas pixel (sx, sy), or null — raycasts the last
3723
3922
  * rendered camera into the scene and resolves the hit up to its `incantoNode`
3724
3923
  * (every Node3D stamps that in `_ensureObject3D`). 3D depth-sorts, so the
@@ -3907,4 +4106,4 @@ declare function createNavDebugNode(nav: TerrainNav, opts?: {
3907
4106
  name?: string;
3908
4107
  }): InstancedMesh3D;
3909
4108
  //#endregion
3910
- export { Area3D, AssetStore3D, type BedSampler, Billboard3D, type BillboardGroupMode, BoneAttachment3D, BoneLookAt3D, Camera3D, CharacterBody3D, CharacterController3D, type CreateGame3DOptions, DEFAULT_TERRAIN_TEXTURE_BASE, DirectionalLight3D, type DownhillTraceOptions, Environment3D, type Environment3DConfig, DENSITY_PRESETS as FLOWER_DENSITY_PRESETS, FLOWER_VARIETIES, type FlowerVariety, Flowers3D, type FogEnvironment, Foliage3D, type FoliageKind, type FoliageStyle, type FrameDiff, type FrameSignature, type FrameStats, type FrameStatsOptions, type Game3D, type GridCell, type HeightSampler, type Heightmap, type HeightmapOptions, InstancedMesh3D, Joint3D, type JointType3D, LoftMesh3D, type LoftSection, MeshInstance3D, type MeshKind, type MeshMaterialProps, type ModelEntry, type ModelFacts, ModelInstance3D, type ModelVerdict, Node3D, OmniLight3D, Particles3D, Physics3D, type Physics3DOptions, PhysicsBody3D, QUARTER_PITCH, type RenderContext3D, type RenderHook3D, Renderer3D, type Renderer3DOptions, type Rig, type RigView, RigidBody3D, type Ripple, River3D, type RiverCarveOptions, type RiverCoverageGap, type RiverHit, type RiverRing, type RiverRingOptions, SIGNATURE_GRID, type SceneJson, type ShadowsEnvironment, type SkyEnvironment, StaticBody3D, type SunConsumer3D, type SyncOptions, type SyncResult, TERRAIN_THEMES, Terrain3D, type TerrainLayer, type TerrainNav, type TerrainNavOptions, type TerrainTheme, TextureCache3D, type TextureSpec, Trail3D, Tree3D, type TreeTier, type TreeType, VOXEL_PALETTE, type VoxelBlock, VoxelGrid3D, WATER_CUTOUT_MAX, WATER_MAX_RIPPLES, Water3D, WaterCutout3D, acquireOwnTexture, acquireTexture, buildHeightmap, buildRiverRings, buildTerrainNav, cameraRelative, createGame3D, createNavDebugNode, diffSignatures, diffText, enablePhysics3D, findRiverCoverageGaps, frameSignature, frameStats, frameText, horizonColorFromSky, keyboardIntensity, modelVerdict, movementState, parseEnvironment3D, projectToRiver, registerNodes3D, resolveFlowerDensity, rigPose, riverCarveChannels, riverStepFor, setEnvironment3D, smoothCourse, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers, traceDownhillPath, verdictText };
4109
+ export { Area3D, AssetStore3D, type BedSampler, Billboard3D, type BillboardGroupMode, BoneAttachment3D, BoneLookAt3D, Camera3D, CharacterBody3D, CharacterController3D, type CreateGame3DOptions, DEFAULT_TERRAIN_TEXTURE_BASE, DirectionalLight3D, type DownhillTraceOptions, Environment3D, type Environment3DConfig, DENSITY_PRESETS as FLOWER_DENSITY_PRESETS, FLOWER_VARIETIES, type FlowerVariety, Flowers3D, type FogEnvironment, Foliage3D, type FoliageKind, type FoliageStyle, type FrameDiff, type FrameSignature, type FrameStats, type FrameStatsOptions, type Game3D, type GridCell, type HeightSampler, type Heightmap, type HeightmapOptions, InstancedMesh3D, Joint3D, type JointType3D, LoftMesh3D, type LoftSection, MeshInstance3D, type MeshKind, type MeshMaterialProps, type ModelEntry, type ModelFacts, ModelInstance3D, type ModelVerdict, Node3D, OmniLight3D, Particles3D, Physics3D, type Physics3DOptions, PhysicsBody3D, QUARTER_PITCH, type RenderContext3D, type RenderHook3D, Renderer3D, type Renderer3DOptions, type Rig, type RigView, RigidBody3D, type Ripple, River3D, type RiverCarveOptions, type RiverCoverageGap, type RiverHit, type RiverRing, type RiverRingOptions, SIGNATURE_GRID, type SceneJson, type ShadowsEnvironment, type SkyEnvironment, StaticBody3D, type SunConsumer3D, type SyncOptions, type SyncResult, TERRAIN_THEMES, Terrain3D, type TerrainLayer, type TerrainNav, type TerrainNavOptions, type TerrainTheme, TextureCache3D, type TextureSpec, Trail3D, Tree3D, type TreeTier, type TreeType, VOXEL_PALETTE, type VoxelBlock, VoxelGrid3D, WATER_CUTOUT_MAX, WATER_MAX_RIPPLES, Water3D, WaterCutout3D, acquireOwnTexture, acquireTexture, applyQualityTier3D, buildHeightmap, buildRiverRings, buildTerrainNav, cameraRelative, createGame3D, createNavDebugNode, diffSignatures, diffText, enablePhysics3D, findRiverCoverageGaps, frameSignature, frameStats, frameText, horizonColorFromSky, keyboardIntensity, modelVerdict, movementState, parseEnvironment3D, projectToRiver, registerNodes3D, resolveFlowerDensity, rigPose, riverCarveChannels, riverStepFor, setEnvironment3D, smoothCourse, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers, traceDownhillPath, verdictText };
package/dist/3d.js CHANGED
@@ -1,10 +1,10 @@
1
- import { c as parseEnvironment3D, l as sunDirectionFromElevationAzimuth, s as horizonColorFromSky, u as sunDirectionFromSky } from "./loader-lQDCwNag.js";
1
+ import { c as parseEnvironment3D, l as sunDirectionFromElevationAzimuth, s as horizonColorFromSky, u as sunDirectionFromSky } from "./loader-KZESOZmE.js";
2
2
  import { a as frameSignature, n as diffSignatures, o as frameStats, r as diffText, s as frameText, t as SIGNATURE_GRID } from "./frame-report-BSMny7oe.js";
3
- import { B as CharacterBody3D, F as Water3D, H as RigidBody3D, I as WATER_CUTOUT_MAX, L as WaterCutout3D, U as StaticBody3D, V as PhysicsBody3D, W as Node3D, q as WATER_MAX_RIPPLES, z as Area3D } from "./gameplay-D-H986A4.js";
4
- import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-BvHs6HEk.js";
5
- import { a as Environment3D, i as syncTree, o as setEnvironment3D, r as Renderer3D, s as AssetStore3D, t as createGame3D } from "./create-game-W2xzTJhs.js";
3
+ 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-CGSv-9XR.js";
4
+ import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-C-5g2pMh.js";
5
+ 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-FAXdHta3.js";
6
6
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
7
- import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-DULB1lD1.js";
7
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-i1IeAY0F.js";
8
8
  //#region src/3d/model-verdict.ts
9
9
  /** Mixamo exports every bone as `mixamorigX`; the retargeter binds by that name. */
10
10
  const MIXAMO = /^mixamorig[:_]?/i;
@@ -317,4 +317,4 @@ function createNavDebugNode(nav, opts) {
317
317
  return node;
318
318
  }
319
319
  //#endregion
320
- export { Area3D, AssetStore3D, Billboard3D, BoneAttachment3D, BoneLookAt3D, Camera3D, CharacterBody3D, CharacterController3D, DEFAULT_TERRAIN_TEXTURE_BASE, DirectionalLight3D, Environment3D, DENSITY_PRESETS as FLOWER_DENSITY_PRESETS, FLOWER_VARIETIES, Flowers3D, Foliage3D, InstancedMesh3D, Joint3D, LoftMesh3D, MeshInstance3D, ModelInstance3D, Node3D, OmniLight3D, Particles3D, Physics3D, PhysicsBody3D, QUARTER_PITCH, Renderer3D, RigidBody3D, River3D, SIGNATURE_GRID, StaticBody3D, TERRAIN_THEMES, Terrain3D, TextureCache3D, Trail3D, Tree3D, VOXEL_PALETTE, VoxelGrid3D, WATER_CUTOUT_MAX, WATER_MAX_RIPPLES, Water3D, WaterCutout3D, acquireOwnTexture, acquireTexture, buildHeightmap, buildRiverRings, buildTerrainNav, cameraRelative, createGame3D, createNavDebugNode, diffSignatures, diffText, enablePhysics3D, findRiverCoverageGaps, frameSignature, frameStats, frameText, horizonColorFromSky, keyboardIntensity, modelVerdict, movementState, parseEnvironment3D, projectToRiver, registerNodes3D, resolveFlowerDensity, rigPose, riverCarveChannels, riverStepFor, setEnvironment3D, smoothCourse, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers, traceDownhillPath, verdictText };
320
+ export { Area3D, AssetStore3D, Billboard3D, BoneAttachment3D, BoneLookAt3D, Camera3D, CharacterBody3D, CharacterController3D, DEFAULT_TERRAIN_TEXTURE_BASE, DirectionalLight3D, Environment3D, DENSITY_PRESETS as FLOWER_DENSITY_PRESETS, FLOWER_VARIETIES, Flowers3D, Foliage3D, InstancedMesh3D, Joint3D, LoftMesh3D, MeshInstance3D, ModelInstance3D, Node3D, OmniLight3D, Particles3D, Physics3D, PhysicsBody3D, QUARTER_PITCH, Renderer3D, RigidBody3D, River3D, SIGNATURE_GRID, StaticBody3D, TERRAIN_THEMES, Terrain3D, TextureCache3D, Trail3D, Tree3D, VOXEL_PALETTE, VoxelGrid3D, WATER_CUTOUT_MAX, WATER_MAX_RIPPLES, Water3D, WaterCutout3D, acquireOwnTexture, acquireTexture, applyQualityTier3D, buildHeightmap, buildRiverRings, buildTerrainNav, cameraRelative, createGame3D, createNavDebugNode, diffSignatures, diffText, enablePhysics3D, findRiverCoverageGaps, frameSignature, frameStats, frameText, horizonColorFromSky, keyboardIntensity, modelVerdict, movementState, parseEnvironment3D, projectToRiver, registerNodes3D, resolveFlowerDensity, rigPose, riverCarveChannels, riverStepFor, setEnvironment3D, smoothCourse, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers, traceDownhillPath, verdictText };
@@ -1,4 +1,4 @@
1
- import { d as PropSchema, dt as Listener, jt as Node } from "./behavior-CmMuQ6CL.js";
1
+ import { d as PropSchema, dt as Listener, jt as Node } from "./behavior-ZnfzKDo3.js";
2
2
  import { a as Rng } from "./schema-CFeioQRE.js";
3
3
 
4
4
  //#region src/core/particle-sim.d.ts
@@ -16,6 +16,15 @@ declare class Signal<Args extends unknown[] = unknown[]> {
16
16
  }): () => void;
17
17
  disconnect(fn: SignalListener<Args>): void;
18
18
  disconnectAll(): void;
19
+ /**
20
+ * How many listeners are attached.
21
+ *
22
+ * A leak is a SLOPE, and a game had no way to see one: every check in this
23
+ * engine is a snapshot. A handler connected per frame and never disconnected,
24
+ * or a `once` that armed and never fired, accumulates invisibly and is
25
+ * perfectly healthy for the first thirty seconds of every playthrough.
26
+ */
27
+ get count(): number;
19
28
  emit(...args: Args): void;
20
29
  get connectionCount(): number;
21
30
  }
@@ -247,6 +256,14 @@ declare class Node implements NodeLifecycle {
247
256
  once?: boolean;
248
257
  }): () => void;
249
258
  off(signal: string, fn: SignalListener<unknown[]>): void;
259
+ /**
260
+ * How many listeners this node has on `signal` — 0 for one nobody watches.
261
+ *
262
+ * The engine's own leak test reads it, and so can a game: a handler connected
263
+ * every frame and never disconnected is invisible to every other check here,
264
+ * because every other check is a snapshot and a leak is a slope.
265
+ */
266
+ listenerCount(signal: string): number;
250
267
  emit(signal: string, ...args: unknown[]): void;
251
268
  /**
252
269
  * Defer destruction to the end of the current update pass (flushed by the
@@ -1220,7 +1237,8 @@ interface RestoreReport {
1220
1237
  declare function captureBehaviors(root: Node, source?: JsonValue): {
1221
1238
  state: BehaviorState; /** Paths of nodes that had state to save and no uid to save it under. */
1222
1239
  unaddressable: string[]; /** Behaviors in this tree that have something to save — addressable or not. */
1223
- saveable: number;
1240
+ saveable: number; /** Authored nodes this run consumed that the `#freed` ledger cannot record. */
1241
+ unrecordable: string[];
1224
1242
  };
1225
1243
  /**
1226
1244
  * Hand each behavior its state back.
@@ -1600,6 +1618,26 @@ interface EngineOptions {
1600
1618
  seed?: number;
1601
1619
  /** Frame cap in fps; 0 (default) runs at whatever the display offers. */
1602
1620
  maxFps?: number;
1621
+ /**
1622
+ * Which SETTINGS this game owns — its own, or the origin's.
1623
+ *
1624
+ * Persisted settings are keyed `incanto:<namespace>:<key>` in the browser's
1625
+ * storage, and this defaulted to `settings` with no way to change it. So two
1626
+ * Incanto games served from one domain shared one volume slider, one language
1627
+ * and one quality tier. Measured: game A's player sets
1628
+ * `renderScale 0.25, muted, quality low`; game B — whose source never mentions
1629
+ * settings — boots at a 480×232 drawing buffer, silent.
1630
+ *
1631
+ * `SaveSlots(namespace)` and `createSaveStore(namespace)` have always taken
1632
+ * one and document it as "keeps two games on one origin apart". This is the
1633
+ * third sibling finally saying the same thing.
1634
+ *
1635
+ * Pass a `Settings` instance instead when a game wants to share deliberately
1636
+ * (a launcher and its titles), or drive them from its own store.
1637
+ */
1638
+ settings?: Settings | {
1639
+ namespace: string;
1640
+ };
1603
1641
  }
1604
1642
  /**
1605
1643
  * The game loop: drives a Scene's tree with fixed-timestep `fixedUpdate`
@@ -1649,6 +1687,25 @@ declare class Engine {
1649
1687
  * and cleared on dispose; null when nothing renders (headless).
1650
1688
  */
1651
1689
  picker: ((x: number, y: number) => Node | null) | null;
1690
+ /** Frames the engine has stepped — monotonic, and the pick cache's key. */
1691
+ frameNumber: number;
1692
+ /** @internal frame number + answer, so N Clickables cost ONE raycast. */
1693
+ private pickCacheFrame;
1694
+ private pickCacheKey;
1695
+ private pickCacheHit;
1696
+ /**
1697
+ * The node under (x, y), answered at most ONCE per frame per cursor position.
1698
+ *
1699
+ * `Clickable` asks every frame, and a board game has one per cell: 42 pads
1700
+ * measured **420 picker calls over ten frames** — one GPU raycast per pad per
1701
+ * frame, all answering the same question about the same pixel. At 60 fps that
1702
+ * is ~2500 raycasts a second to find out what the cursor is on, and a 200-tile
1703
+ * board was unusable.
1704
+ *
1705
+ * Cached on (frame, x, y), so a game that picks at two different points in one
1706
+ * frame still gets two real answers.
1707
+ */
1708
+ pickAt(x: number, y: number): Node | null;
1652
1709
  /** Global volume buses — `master` × `sfx`/`music` gain + `muted`. AudioPlayer
1653
1710
  * routes its volume through this; games set it for global volume/mute. */
1654
1711
  readonly audio: AudioBuses;
@@ -1,14 +1,15 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { d as computeViewport, f as resolveViewport, k as diagnose, n as loadScene, z as registerBehavior } from "./loader-lQDCwNag.js";
3
- import { _ as AudioPlayer, v as Engine } from "./register-CgeR4ckF.js";
2
+ import { d as computeViewport, f as resolveViewport, k as diagnose, n as loadScene, z as registerBehavior } from "./loader-KZESOZmE.js";
3
+ import { _ as AudioPlayer, v as Engine } from "./register-Cs53CY7s.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
- import { i as resolveRendering, n as attachTouchControls } from "./touch-DESwnpOc.js";
6
- import { a as openBundledEditor, c as claimCanvasGestures, i as devServerLibrary, l as audioErrors, n as pauseWhenHidden, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-BordHL9o.js";
5
+ import { n as attachTouchControls } from "./touch-DEAmqGdf.js";
6
+ import { t as resolveRendering } from "./rendering-options-ByAdi8PF.js";
7
+ import { a as openBundledEditor, c as claimCanvasGestures, i as devServerLibrary, l as audioErrors, n as pauseWhenHidden, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-LB9cTnXJ.js";
7
8
  import { o as frameStats } from "./frame-report-BSMny7oe.js";
8
- import { n as registerGameplayBehaviors } from "./gameplay-D-H986A4.js";
9
- import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-DO5_Qp0Q.js";
9
+ import { n as registerGameplayBehaviors } from "./gameplay-CGSv-9XR.js";
10
+ import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-vIxU3Xbv.js";
10
11
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
11
- import { n as enablePhysics2D } from "./physics-2d-CmnunaR2.js";
12
+ import { n as enablePhysics2D } from "./physics-2d-_B6zj3zU.js";
12
13
  import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
13
14
  //#region src/2d/assets.ts
14
15
  /**
@@ -756,6 +757,7 @@ async function createGame2D(opts) {
756
757
  const engine = new Engine({
757
758
  seed: opts.seed,
758
759
  fixedHz: opts.fixedHz,
760
+ ...opts.settings ? { settings: opts.settings } : {},
759
761
  ...opts._scheduler ? { scheduler: opts._scheduler } : {}
760
762
  });
761
763
  const sourceJson = cloneSceneJson(opts.scene);
@@ -918,7 +920,11 @@ async function createGame2D(opts) {
918
920
  }
919
921
  return {
920
922
  engine,
921
- scene,
923
+ get scene() {
924
+ const live = engine.scene;
925
+ if (!live) throw new IncantoError("TREE_VIOLATION", "game.scene: the engine has no scene.");
926
+ return live;
927
+ },
922
928
  sourceJson,
923
929
  renderer,
924
930
  physics,