incanto 0.66.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 (80) 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 +41 -4
  11. package/dist/2d.js +3 -3
  12. package/dist/3d.d.ts +86 -14
  13. package/dist/3d.js +6 -6
  14. package/dist/{audio-player-_UAcHxnC.d.ts → audio-player-BNUHo0JF.d.ts} +1 -1
  15. package/dist/{behavior-BXNLfIJk.d.ts → behavior-ZnfzKDo3.d.ts} +39 -1
  16. package/dist/{create-game-Dd7H4bJV.js → create-game-DMetd3vR.js} +14 -8
  17. package/dist/{create-game-DYJCIzO0.js → create-game-FAXdHta3.js} +77 -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-BlPsEmq6.js → environment-presets-C-5g2pMh.js} +46 -6
  23. package/dist/{gameplay-D1RADWu3.js → gameplay-CGSv-9XR.js} +28 -5
  24. package/dist/gameplay.d.ts +25 -4
  25. package/dist/gameplay.js +1 -1
  26. package/dist/index.d.ts +50 -6
  27. package/dist/index.js +7 -6
  28. package/dist/{loader-Cga7FVP4.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-Cji5A6sX.js → physics-2d-_B6zj3zU.js} +29 -2
  33. package/dist/{physics-3d-BkHjwJgI.js → physics-3d-i1IeAY0F.js} +30 -3
  34. package/dist/react.d.ts +1 -1
  35. package/dist/react.js +1 -1
  36. package/dist/{register-BmuqYTiY.js → register-Cs53CY7s.js} +81 -16
  37. package/dist/{register-D0CxCveZ.js → register-vIxU3Xbv.js} +3 -3
  38. package/dist/rendering-options-ByAdi8PF.js +184 -0
  39. package/dist/{replay-IsZbNu6d.js → replay-BkpDV51O.js} +4 -3
  40. package/dist/{replay-BHoB6fCU.d.ts → replay-D0GcEHYU.d.ts} +1 -1
  41. package/dist/{split-screen-eJUFjhi1.d.ts → split-screen-DD5eYIgW.d.ts} +2 -2
  42. package/dist/{split-screen-D_i7GRcY.js → split-screen-DS8RaUV_.js} +2 -2
  43. package/dist/{src-BTLbXFPZ.js → src-BLlsGJW9.js} +1 -1
  44. package/dist/{teardown-byR9USax.js → teardown-LB9cTnXJ.js} +2 -2
  45. package/dist/{test-DgrD0jHD.js → test-81uZ967a.js} +127 -24
  46. package/dist/test.d.ts +7 -5
  47. package/dist/test.js +2 -2
  48. package/dist/{touch-DESwnpOc.js → touch-DEAmqGdf.js} +30 -186
  49. package/dist/vite.d.ts +0 -7
  50. package/dist/vite.js +13 -3
  51. package/editor/assets/{agent8-9N-Pd_YS.js → agent8-C2jtNKbx.js} +1 -1
  52. package/editor/assets/debug-DoU3KeqS.js +3 -0
  53. package/editor/assets/{index-CeDhIPTC.js → index-CNqHojcC.js} +92 -92
  54. package/editor/index.html +1 -1
  55. package/package.json +1 -1
  56. package/schemas/scene.schema.json +1 -1
  57. package/skills/incanto-building-3d-games.md +19 -0
  58. package/skills/incanto-editor.md +5 -1
  59. package/skills/incanto-environment.md +2 -2
  60. package/skills/incanto-gameplay-behaviors.md +1 -1
  61. package/skills/incanto-hud.md +14 -1
  62. package/skills/incanto-multiplayer.md +14 -0
  63. package/skills/incanto-node-reference.md +1 -1
  64. package/skills/incanto-performance.md +35 -0
  65. package/skills/incanto-playtesting.md +15 -1
  66. package/skills/incanto-save-slots.md +21 -1
  67. package/skills/incanto-verifying-your-game.md +19 -0
  68. package/skills/incanto-your-first-game.md +4 -4
  69. package/templates-app/beacon-isle-3d/package.json +1 -1
  70. package/templates-app/beacon-isle-3d/src/game.scene.json +38 -19
  71. package/templates-app/platformer-2d/package.json +1 -1
  72. package/templates-app/platformer-2d/src/game.scene.json +99 -53
  73. package/templates-app/star-survivor/package.json +1 -1
  74. package/templates-app/star-survivor/src/game.scene.json +31 -17
  75. package/templates-app/tps-3d/package.json +1 -1
  76. package/templates-app/tps-3d/src/game.scene.json +14 -8
  77. package/templates-app/village-quest-3d/package.json +1 -1
  78. package/templates-app/village-quest-3d/src/grove.scene.json +20 -11
  79. package/templates-app/village-quest-3d/src/village.scene.json +10 -7
  80. package/editor/assets/debug-CkbJICYp.js +0 -3
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-BXNLfIJk.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-Cga7FVP4.js";
6
- import { i as ParticleSim, o as ParticleView, r as SpatialPose } from "./audio-player-_UAcHxnC.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;
@@ -578,6 +603,16 @@ declare class Physics3D {
578
603
  private readonly overlaps;
579
604
  private trackOverlap;
580
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
+ /**
581
616
  * The bodies currently overlapping `node` — the standing answer behind
582
617
  * `triggerEnter`/`triggerExit`.
583
618
  *
@@ -683,6 +718,18 @@ interface CreateGame3DOptions {
683
718
  */
684
719
  editor?: boolean | EditorSwitchOptions;
685
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"];
686
733
  fixedHz?: number;
687
734
  pixelRatio?: number;
688
735
  /** Keep the frame rate by rendering fewer pixels while frames run slow
@@ -732,7 +779,22 @@ interface GameRenderer {
732
779
  }
733
780
  interface Game3D {
734
781
  engine: Engine;
735
- 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;
736
798
  /**
737
799
  * The AUTHORED scene this game booted from — the JSON, not the live tree.
738
800
  *
@@ -1028,19 +1090,25 @@ declare class Environment3D {
1028
1090
  }
1029
1091
  //#endregion
1030
1092
  //#region src/3d/environment-runtime.d.ts
1093
+ declare function setEnvironment3D(engine: Engine, patch: JsonObject, opts?: {
1094
+ rebaseTierKeys?: boolean;
1095
+ }): void;
1031
1096
  /**
1032
- * Live environment editing — the renderer re-applies `scene.environment`
1033
- * every frame (cheap when unchanged), so changing the LOOK at runtime is
1034
- * just writing that object. This helper does it safely:
1097
+ * Apply a quality tier — the only caller that may RESET as well as patch.
1035
1098
  *
1036
- * setEnvironment3D(engine, { sky: { elevationDeg: 4 }, exposure: 0.8 });
1037
- * 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.*
1038
1105
  *
1039
- * - one-level deep merge (patching `sky.elevationDeg` keeps the other sky keys)
1040
- * - hard-validates the merged result NOW (in your call stack), so a bad patch
1041
- * 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}`.
1042
1110
  */
1043
- declare function setEnvironment3D(engine: Engine, patch: JsonObject): void;
1111
+ declare function applyQualityTier3D(engine: Engine, patch: JsonObject): void;
1044
1112
  //#endregion
1045
1113
  //#region src/3d/model-verdict.d.ts
1046
1114
  /**
@@ -1792,6 +1860,10 @@ declare class MeshInstance3D extends Node3D {
1792
1860
  /** Loader hook: malformed materials fail at LOAD time, not at render. */
1793
1861
  static validateJson(node: Node): void;
1794
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;
1795
1867
  private textureKey;
1796
1868
  private loadedTextures;
1797
1869
  protected override _createObject3D(): Object3D;
@@ -4034,4 +4106,4 @@ declare function createNavDebugNode(nav: TerrainNav, opts?: {
4034
4106
  name?: string;
4035
4107
  }): InstancedMesh3D;
4036
4108
  //#endregion
4037
- 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 { 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-D1RADWu3.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-BlPsEmq6.js";
5
- import { a as Environment3D, i as syncTree, o as setEnvironment3D, r as Renderer3D, s as AssetStore3D, t as createGame3D } from "./create-game-DYJCIzO0.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-BkHjwJgI.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-BXNLfIJk.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`
@@ -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-BmuqYTiY.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-byR9USax.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-D1RADWu3.js";
9
- import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-D0CxCveZ.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-Cji5A6sX.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,
@@ -1,14 +1,15 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { c as parseEnvironment3D, k as diagnose, n as loadScene, s as horizonColorFromSky, u as sunDirectionFromSky, z as registerBehavior } from "./loader-lQDCwNag.js";
3
- import { C as qualityRendering, _ as AudioPlayer, v as Engine, x as qualityCaps } from "./register-BmuqYTiY.js";
2
+ import { c as parseEnvironment3D, k as diagnose, n as loadScene, s as horizonColorFromSky, u as sunDirectionFromSky, z as registerBehavior } from "./loader-KZESOZmE.js";
3
+ import { C as qualityRendering, _ as AudioPlayer, v as Engine, x as qualityCaps } 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, o as poseFromRenderer, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-byR9USax.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, o as poseFromRenderer, 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 { J as createCausticsQuad, K as Node3D, U as PhysicsBody3D, n as registerGameplayBehaviors } from "./gameplay-D1RADWu3.js";
9
+ import { J as createCausticsQuad, K as Node3D, U as PhysicsBody3D, n as registerGameplayBehaviors } from "./gameplay-CGSv-9XR.js";
9
10
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
10
- import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-BlPsEmq6.js";
11
- import { n as enablePhysics3D } from "./physics-3d-BkHjwJgI.js";
11
+ import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-C-5g2pMh.js";
12
+ import { n as enablePhysics3D } from "./physics-3d-i1IeAY0F.js";
12
13
  import { ACESFilmicToneMapping, AmbientLight, Box3, BufferAttribute, BufferGeometry, Color, DepthTexture, EquirectangularReflectionMapping, FloatType, Fog, HalfFloatType, LineBasicMaterial, LineSegments, Matrix4, Mesh, PCFShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, Quaternion, Raycaster, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
13
14
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
14
15
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
@@ -27,6 +28,31 @@ import { Sky } from "three/examples/jsm/objects/Sky.js";
27
28
  */
28
29
  var AssetStore3D = class {
29
30
  engine;
31
+ /**
32
+ * One geometry and one material per distinct SHAPE and per distinct look.
33
+ *
34
+ * `MeshInstance3D._createObject3D` used to `new BoxGeometry()` and
35
+ * `new MeshPhysicalMaterial()` for every node, even when byte-identical.
36
+ * Three identical nodes reported `geometry shared? false false / material
37
+ * shared? false false`, with distinct uuids. Measured at 3,200 units, all
38
+ * three runs at identical 3,209 draw calls / 1,741,932 triangles and an
39
+ * identical screenshot:
40
+ *
41
+ * ```
42
+ * per-node geometry + material 31.0 ms wall (renderMs 27.3)
43
+ * share one geometry 27.0 ms
44
+ * + share two materials 15.4 ms <- 2x the headroom
45
+ * ```
46
+ *
47
+ * 13.1 ms of that 26.5 ms cliff was GPU state churn from per-node objects,
48
+ * and none of it was visible headlessly: with no renderer `_syncObject3D`
49
+ * never runs, so the bench measured 55 ns/node instead of ~2,000.
50
+ *
51
+ * Renderer-scoped, deliberately — module-level state is the predecessor's
52
+ * fatal flaw and two games in one page must not share a cache.
53
+ */
54
+ meshGeometries = /* @__PURE__ */ new Map();
55
+ meshMaterials = /* @__PURE__ */ new Map();
30
56
  models = /* @__PURE__ */ new Map();
31
57
  animations = /* @__PURE__ */ new Map();
32
58
  modelKeys = /* @__PURE__ */ new Map();
@@ -253,10 +279,27 @@ function yieldToFrame(maxWaitMs = DEFAULT_MAX_WAIT_MS, raf = typeof requestAnima
253
279
  * - hard-validates the merged result NOW (in your call stack), so a bad patch
254
280
  * throws where you wrote it — never inside the render loop
255
281
  */
256
- function setEnvironment3D(engine, patch) {
282
+ /**
283
+ * The environment stages a quality tier is allowed to touch.
284
+ *
285
+ * These are the ones `qualityEnvironment` names, and the ones that must snap
286
+ * back to what the scene file says rather than accumulating.
287
+ */
288
+ const TIER_KEYS = [
289
+ "shadows",
290
+ "bloom",
291
+ "post",
292
+ "clouds"
293
+ ];
294
+ function setEnvironment3D(engine, patch, opts) {
257
295
  const scene = engine.scene;
258
296
  if (!scene) throw new IncantoError("BAD_FORMAT", "setEnvironment3D: the engine has no scene.");
259
297
  const merged = { ...scene.environment ?? {} };
298
+ if (opts?.rebaseTierKeys) {
299
+ const authored = scene.source?.environment ?? {};
300
+ for (const key of TIER_KEYS) if (key in authored) merged[key] = authored[key];
301
+ else delete merged[key];
302
+ }
260
303
  for (const [key, value] of Object.entries(patch)) if (value === null) delete merged[key];
261
304
  else if (typeof value === "object" && !Array.isArray(value) && typeof merged[key] === "object" && merged[key] !== null && !Array.isArray(merged[key])) merged[key] = {
262
305
  ...merged[key],
@@ -266,6 +309,24 @@ function setEnvironment3D(engine, patch) {
266
309
  parseEnvironment3D(merged);
267
310
  scene.environment = merged;
268
311
  }
312
+ /**
313
+ * Apply a quality tier — the only caller that may RESET as well as patch.
314
+ *
315
+ * A tier is re-applied every time the player moves the slider, so it cannot
316
+ * compose with what the last one left behind: `low` strips the author's bloom,
317
+ * and `high` then has nothing to put back. This re-derives the four tier-owned
318
+ * stages from the scene FILE first, so every tier is reversible, and so a stage
319
+ * the author never wrote can never appear — the contract `settings.ts` states
320
+ * and `incanto-performance.md` promises: *the tier can only take things away.*
321
+ *
322
+ * Measured before this, on a scene with no `bloom` and no `post` in its JSON:
323
+ * `high` and `medium` both rendered five GL passes at 3.3 ms where `low`
324
+ * rendered one at 1.24 ms — 62% of the frame on a chain the game never asked
325
+ * for, and `environment.bloom` read back `{strength: 0.8, threshold: 1}`.
326
+ */
327
+ function applyQualityTier3D(engine, patch) {
328
+ setEnvironment3D(engine, patch, { rebaseTierKeys: true });
329
+ }
269
330
  //#endregion
270
331
  //#region src/3d/adaptive-resolution.ts
271
332
  const DEFAULTS = {
@@ -2180,6 +2241,7 @@ async function createGame3D(opts) {
2180
2241
  const engine = new Engine({
2181
2242
  seed: opts.seed,
2182
2243
  fixedHz: opts.fixedHz,
2244
+ ...opts.settings ? { settings: opts.settings } : {},
2183
2245
  ...opts._scheduler ? { scheduler: opts._scheduler } : {}
2184
2246
  });
2185
2247
  const sourceJson = cloneSceneJson(opts.scene);
@@ -2294,7 +2356,7 @@ async function createGame3D(opts) {
2294
2356
  cleanups.push(engine.settings.bindAudio(engine.audio));
2295
2357
  cleanups.push(engine.settings.bindLocale(engine.locale));
2296
2358
  cleanups.push(engine.settings.bindQuality((patch) => {
2297
- setEnvironment3D(engine, patch);
2359
+ applyQualityTier3D(engine, patch);
2298
2360
  }));
2299
2361
  cleanups.push(engine.settings.bindFrameCap((fps) => {
2300
2362
  engine.maxFps = fps;
@@ -2368,7 +2430,11 @@ async function createGame3D(opts) {
2368
2430
  }
2369
2431
  return {
2370
2432
  engine,
2371
- scene,
2433
+ get scene() {
2434
+ const live = engine.scene;
2435
+ if (!live) throw new IncantoError("TREE_VIOLATION", "game.scene: the engine has no scene.");
2436
+ return live;
2437
+ },
2372
2438
  sourceJson,
2373
2439
  renderer,
2374
2440
  physics,
@@ -2454,4 +2520,4 @@ function wireAudioUnlock(engine, root, canvas) {
2454
2520
  return detachAll;
2455
2521
  }
2456
2522
  //#endregion
2457
- export { Environment3D as a, syncTree as i, create_game_exports as n, setEnvironment3D as o, Renderer3D as r, AssetStore3D as s, createGame3D as t };
2523
+ export { Environment3D as a, AssetStore3D as c, syncTree as i, create_game_exports as n, applyQualityTier3D as o, Renderer3D as r, setEnvironment3D as s, createGame3D as t };
package/dist/debug.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Ft as LogLevel, T as RendererStats, b as Engine } from "./behavior-BXNLfIJk.js";
1
+ import { Ft as LogLevel, T as RendererStats, b as Engine } from "./behavior-ZnfzKDo3.js";
2
2
 
3
3
  //#region src/debug/panel.d.ts
4
4
  /** Minimal document surface the overlay needs (injectable for tests). */
package/dist/debug.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { t as jsonClone } from "./json-CwwhxQgb.js";
2
2
  import { s as mergeStaticProps } from "./registry-CF70EArN.js";
3
+ import { r as capturePointer } from "./touch-DEAmqGdf.js";
3
4
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
4
5
  //#region src/debug/capture.ts
5
6
  /**
@@ -369,7 +370,7 @@ var FloatingPanel = class {
369
370
  active = e.pointerId;
370
371
  lastX = e.clientX;
371
372
  lastY = e.clientY;
372
- grip.setPointerCapture?.(e.pointerId);
373
+ capturePointer(grip, e.pointerId);
373
374
  });
374
375
  grip.addEventListener("pointermove", (e) => {
375
376
  if (e.pointerId !== active) return;
@@ -565,7 +566,7 @@ function openRegionSelect(opts) {
565
566
  applyStyle(popup, { display: "none" });
566
567
  applyStyle(box, { cursor: next === "move" ? "grabbing" : "crosshair" });
567
568
  const id = event.pointerId;
568
- if (id !== void 0) layer.setPointerCapture?.(id);
569
+ if (id !== void 0) capturePointer(layer, id);
569
570
  render();
570
571
  }
571
572
  function render() {
@@ -1,4 +1,4 @@
1
- import { a as serializeNode, t as buildNodeJson } from "./loader-lQDCwNag.js";
1
+ import { a as serializeNode, t as buildNodeJson } from "./loader-KZESOZmE.js";
2
2
  //#region src/core/scene/duplicate.ts
3
3
  function stripUids(json) {
4
4
  const { uid: _uid, ...rest } = json;
package/dist/editor.js CHANGED
@@ -2380,7 +2380,8 @@ var Vn = [
2380
2380
  "connections",
2381
2381
  "input",
2382
2382
  "multiplayer",
2383
- "strings"
2383
+ "strings",
2384
+ "environment"
2384
2385
  ];
2385
2386
  function Hn(e, t, n) {
2386
2387
  e.appendChild(Tr("scene")), e.appendChild(Ar("name", String(t.working.name ?? ""), (e) => {
@@ -7842,6 +7843,10 @@ var ss = class {
7842
7843
  return !0;
7843
7844
  }) === !0;
7844
7845
  }
7846
+ resume(e) {
7847
+ let t = this.original;
7848
+ this.reset(e), this.original = t;
7849
+ }
7845
7850
  reset(e) {
7846
7851
  this.working = e, this.working.root && cs(this.working.root), this.original = JSON.stringify(this.working), this.undoStack = [], this.selection = [], this.emit();
7847
7852
  }
@@ -9794,7 +9799,7 @@ async function xc(e = {}) {
9794
9799
  let t = e.target.matches("input, textarea, select");
9795
9800
  (e.metaKey || e.ctrlKey) && e.key === "z" ? (e.preventDefault(), u.undo()) : (e.metaKey || e.ctrlKey) && e.key === "s" ? (e.preventDefault(), p.disabled || p.click()) : !t && e.code === "KeyF" ? P.zoomToFit() : !t && (e.code === "Digit0" || e.code === "Numpad0") ? P.gameView() : !t && e.code === "KeyW" ? P.setMode("move") : !t && e.code === "KeyE" ? P.setMode("rotate") : !t && e.code === "KeyR" && P.setMode("scale");
9796
9801
  };
9797
- return window.addEventListener("keydown", Me), n.push(() => window.removeEventListener("keydown", Me)), F(), e.camera && P.setEditCamera(e.camera.position, e.camera.target), kt.ready(C, w, o.version), {
9802
+ return window.addEventListener("keydown", Me), n.push(() => window.removeEventListener("keydown", Me)), F(), e.scene && u.resume(e.scene), e.camera && P.setEditCamera(e.camera.position, e.camera.target), kt.ready(C, w, o.version), {
9798
9803
  working: () => u.working,
9799
9804
  camera: () => P.editCamera(),
9800
9805
  showError: O,
@@ -1,11 +1,11 @@
1
- import { k as diagnose } from "./loader-lQDCwNag.js";
2
- import { L as translateOn, t as registerCoreNodes } from "./register-BmuqYTiY.js";
1
+ import { k as diagnose } from "./loader-KZESOZmE.js";
2
+ import { L as translateOn, t as registerCoreNodes } from "./register-Cs53CY7s.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
5
  import { i as getNodeSchema, l as registerNode } from "./registry-CF70EArN.js";
6
6
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
7
7
  import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimationAliases, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-CY-mrr1L.js";
8
- import { B as rejectScale, G as StaticBody3D, H as CharacterBody3D, K as Node3D, L as Water3D, U as PhysicsBody3D, V as Area3D, W as RigidBody3D, X as colliderFootDrop, w as tolerateUnknownAction, z as WaterCutout3D } from "./gameplay-D1RADWu3.js";
8
+ import { B as rejectScale, G as StaticBody3D, H as CharacterBody3D, K as Node3D, L as Water3D, U as PhysicsBody3D, V as Area3D, W as RigidBody3D, X as colliderFootDrop, w as tolerateUnknownAction, z as WaterCutout3D } from "./gameplay-CGSv-9XR.js";
9
9
  import { t as checkSheetGrid } from "./sheet-grid-BT6N_Bjs.js";
10
10
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.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";
@@ -387,6 +387,10 @@ var MeshInstance3D = class extends Node3D {
387
387
  validateMeshMaterial(node.material, node.name);
388
388
  }
389
389
  geometryKey = "";
390
+ materialKey = "";
391
+ /** True while this node is using a cache entry it must not dispose. */
392
+ sharedGeometry = false;
393
+ sharedMaterial = false;
390
394
  textureKey = "";
391
395
  loadedTextures = [];
392
396
  _createObject3D() {
@@ -395,12 +399,48 @@ var MeshInstance3D = class extends Node3D {
395
399
  _syncObject3D(alpha = 1, assets = null) {
396
400
  super._syncObject3D(alpha, assets);
397
401
  const m = this._ensureObject3D();
398
- const key = `${this.mesh}:${JSON.stringify(this.size)}`;
402
+ const key = `${this.mesh}:${this.size.join(",")}`;
399
403
  if (key !== this.geometryKey) {
400
- m.geometry.dispose();
401
- m.geometry = buildMeshGeometry(this.mesh, this.size);
404
+ if (!this.sharedGeometry) m.geometry.dispose();
405
+ const cached = assets?.meshGeometries.get(key);
406
+ if (cached) {
407
+ m.geometry = cached;
408
+ this.sharedGeometry = true;
409
+ } else {
410
+ const built = buildMeshGeometry(this.mesh, this.size);
411
+ m.geometry = built;
412
+ this.sharedGeometry = Boolean(assets);
413
+ assets?.meshGeometries.set(key, built);
414
+ }
402
415
  this.geometryKey = key;
403
416
  }
417
+ const look = JSON.stringify(this.material);
418
+ if (assets && look !== this.materialKey) {
419
+ const shared = assets.meshMaterials.get(look);
420
+ if (shared) {
421
+ if (!this.sharedMaterial) m.material.dispose();
422
+ m.material = shared;
423
+ this.sharedMaterial = true;
424
+ this.materialKey = look;
425
+ this.textureKey = "shared";
426
+ m.castShadow = this.castShadow;
427
+ m.receiveShadow = this.receiveShadow;
428
+ return;
429
+ }
430
+ if (this.sharedMaterial) {
431
+ m.material = new MeshPhysicalMaterial();
432
+ this.sharedMaterial = false;
433
+ this.textureKey = "";
434
+ }
435
+ this.materialKey = look;
436
+ const mine = m.material;
437
+ applyMeshMaterial(mine, this.material);
438
+ this.syncTextures(mine, assets);
439
+ assets.meshMaterials.set(look, mine);
440
+ m.castShadow = this.castShadow;
441
+ m.receiveShadow = this.receiveShadow;
442
+ return;
443
+ }
404
444
  const mat = m.material;
405
445
  applyMeshMaterial(mat, this.material);
406
446
  this.syncTextures(mat, assets);