incanto 0.66.0 → 0.68.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 (96) hide show
  1. package/bin/_behaviors-loader.mjs +87 -0
  2. package/bin/_pick-server.mjs +48 -0
  3. package/bin/incanto-check.mjs +16 -2
  4. package/bin/incanto-editor.mjs +39 -2
  5. package/bin/incanto-feel.mjs +4 -1
  6. package/bin/incanto-frame.mjs +37 -3
  7. package/bin/incanto-logs.mjs +23 -3
  8. package/bin/incanto-multiplay.mjs +50 -1
  9. package/bin/incanto-play.mjs +3 -1
  10. package/bin/incanto-playtest.mjs +3 -25
  11. package/bin/incanto-verify.mjs +15 -2
  12. package/dist/2d.d.ts +44 -7
  13. package/dist/2d.js +6 -4
  14. package/dist/3d.d.ts +112 -16
  15. package/dist/3d.js +8 -6
  16. package/dist/{audio-player-_UAcHxnC.d.ts → audio-player-DOrq7sP-.d.ts} +62 -4
  17. package/dist/{behavior-BXNLfIJk.d.ts → behavior-DoFPYrgo.d.ts} +117 -2
  18. package/dist/boot-failure-CKYrEcGF.js +47 -0
  19. package/dist/{create-game-DYJCIzO0.js → create-game-DbWtVTxD.js} +91 -29
  20. package/dist/{create-game-Dd7H4bJV.js → create-game-IZIydDwI.js} +16 -10
  21. package/dist/debug.d.ts +1 -1
  22. package/dist/debug.js +3 -2
  23. package/dist/{duplicate-CqSAtdrh.js → duplicate-IWIqk0HJ.js} +1 -1
  24. package/dist/{editor-switch-DAvWQeld.d.ts → editor-switch-CnIOiyNJ.d.ts} +1 -1
  25. package/dist/editor.js +18 -7
  26. package/dist/env.d.ts +1 -1
  27. package/dist/{environment-presets-BlPsEmq6.js → environment-presets-CybQXNqS.js} +100 -11
  28. package/dist/{gameplay-D1RADWu3.js → gameplay-DM1eu_cV.js} +46 -5
  29. package/dist/gameplay.d.ts +75 -6
  30. package/dist/gameplay.js +2 -2
  31. package/dist/index.d.ts +52 -8
  32. package/dist/index.js +9 -7
  33. package/dist/{loader-lQDCwNag.js → loader-BC4PNtJX.js} +173 -1
  34. package/dist/{loader-Cga7FVP4.d.ts → loader-CcB533FR.d.ts} +2 -2
  35. package/dist/net.d.ts +2 -2
  36. package/dist/net.js +1 -1
  37. package/dist/{physics-2d-Cji5A6sX.js → physics-2d-B7Y6dPZO.js} +29 -2
  38. package/dist/{physics-3d-BkHjwJgI.js → physics-3d-bG3n70Ky.js} +30 -3
  39. package/dist/react.d.ts +2 -2
  40. package/dist/react.js +1 -1
  41. package/dist/{register-BmuqYTiY.js → register-3ta-2Xig.js} +148 -22
  42. package/dist/{register-D0CxCveZ.js → register-ibCjm-wH.js} +3 -3
  43. package/dist/{replay-IsZbNu6d.js → replay-CYvhVHHN.js} +28 -4
  44. package/dist/{replay-BHoB6fCU.d.ts → replay-Dvn8aeBd.d.ts} +38 -4
  45. package/dist/{schema-CFeioQRE.d.ts → schema-B6ugCV1Q.d.ts} +11 -0
  46. package/dist/{split-screen-D_i7GRcY.js → split-screen-CYwDkbLF.js} +2 -2
  47. package/dist/{split-screen-eJUFjhi1.d.ts → split-screen-DhrSzZIB.d.ts} +3 -3
  48. package/dist/{src-BTLbXFPZ.js → src-D7RIqXYF.js} +1 -1
  49. package/dist/{teardown-byR9USax.js → teardown-D2NEmxPB.js} +2 -2
  50. package/dist/{test-DgrD0jHD.js → test-8hoHeRmo.js} +140 -32
  51. package/dist/test.d.ts +9 -7
  52. package/dist/test.js +3 -3
  53. package/dist/{touch-DESwnpOc.js → touch-DEAmqGdf.js} +30 -186
  54. package/dist/vite.d.ts +0 -7
  55. package/dist/vite.js +13 -3
  56. package/dist/webgl-unavailable-N9nQqesw.js +228 -0
  57. package/editor/assets/{agent8-9N-Pd_YS.js → agent8-BrrHOjMJ.js} +1 -1
  58. package/editor/assets/debug-BBhKuBNV.js +3 -0
  59. package/editor/assets/{index-CeDhIPTC.js → index-eVd0BToA.js} +92 -92
  60. package/editor/index.html +1 -1
  61. package/package.json +1 -1
  62. package/schemas/scene.schema.json +5 -1
  63. package/skills/incanto-3d-character.md +1 -0
  64. package/skills/incanto-audio.md +49 -0
  65. package/skills/incanto-building-2d-games.md +37 -0
  66. package/skills/incanto-building-3d-games.md +56 -0
  67. package/skills/incanto-editor.md +5 -1
  68. package/skills/incanto-environment.md +2 -2
  69. package/skills/incanto-gameplay-behaviors.md +29 -2
  70. package/skills/incanto-hud.md +14 -1
  71. package/skills/incanto-multiplayer.md +14 -0
  72. package/skills/incanto-node-reference.md +2 -1
  73. package/skills/incanto-performance.md +35 -0
  74. package/skills/incanto-physics-and-input.md +23 -0
  75. package/skills/incanto-playtesting.md +15 -1
  76. package/skills/incanto-save-slots.md +21 -1
  77. package/skills/incanto-scene-json-authoring.md +12 -1
  78. package/skills/incanto-verifying-your-game.md +19 -0
  79. package/skills/incanto-your-first-game.md +4 -4
  80. package/templates-app/beacon-isle-3d/package.json +1 -1
  81. package/templates-app/beacon-isle-3d/src/game.scene.json +38 -19
  82. package/templates-app/beacon-isle-3d/src/main.ts +8 -1
  83. package/templates-app/platformer-2d/package.json +1 -1
  84. package/templates-app/platformer-2d/src/game.scene.json +99 -53
  85. package/templates-app/platformer-2d/src/main.ts +8 -1
  86. package/templates-app/star-survivor/package.json +1 -1
  87. package/templates-app/star-survivor/src/game.scene.json +31 -17
  88. package/templates-app/star-survivor/src/main.ts +8 -1
  89. package/templates-app/tps-3d/package.json +1 -1
  90. package/templates-app/tps-3d/src/game.scene.json +14 -8
  91. package/templates-app/tps-3d/src/main.ts +8 -1
  92. package/templates-app/village-quest-3d/package.json +1 -1
  93. package/templates-app/village-quest-3d/src/grove.scene.json +20 -11
  94. package/templates-app/village-quest-3d/src/main.ts +8 -1
  95. package/templates-app/village-quest-3d/src/village.scene.json +10 -7
  96. package/editor/assets/debug-CkbJICYp.js +0 -3
@@ -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-BC4PNtJX.js";
3
+ import { C as qualityRendering, _ as AudioPlayer, v as Engine, x as qualityCaps } from "./register-3ta-2Xig.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 { n as withWebGLContext, r as resolveRendering } from "./webgl-unavailable-N9nQqesw.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-D2NEmxPB.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-DM1eu_cV.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-CybQXNqS.js";
12
+ import { n as enablePhysics3D } from "./physics-3d-bG3n70Ky.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();
@@ -61,22 +87,18 @@ var AssetStore3D = class {
61
87
  url,
62
88
  error
63
89
  });
64
- for (const [ref, url] of this.modelKeys) {
65
- const entry = this.models.get(url);
66
- if (entry?.error) out.push({
67
- ref,
68
- url,
69
- error: String(entry.error)
70
- });
71
- }
72
- for (const [ref, decl] of this.animationKeys) {
73
- const entry = this.animations.get(decl.url);
74
- if (entry?.error) out.push({
75
- ref,
76
- url: decl.url,
77
- error: String(entry.error)
78
- });
79
- }
90
+ const modelRefs = new Map([...this.modelKeys].map(([ref, url]) => [url, ref]));
91
+ for (const [url, entry] of this.models) if (entry.error) out.push({
92
+ ref: modelRefs.get(url) ?? url,
93
+ url,
94
+ error: String(entry.error)
95
+ });
96
+ const clipRefs = new Map([...this.animationKeys].map(([ref, decl]) => [decl.url, ref]));
97
+ for (const [url, entry] of this.animations) if (entry.error) out.push({
98
+ ref: clipRefs.get(url) ?? url,
99
+ url,
100
+ error: String(entry.error)
101
+ });
80
102
  return out;
81
103
  }
82
104
  /** Map a scene's `assets` header. */
@@ -253,10 +275,27 @@ function yieldToFrame(maxWaitMs = DEFAULT_MAX_WAIT_MS, raf = typeof requestAnima
253
275
  * - hard-validates the merged result NOW (in your call stack), so a bad patch
254
276
  * throws where you wrote it — never inside the render loop
255
277
  */
256
- function setEnvironment3D(engine, patch) {
278
+ /**
279
+ * The environment stages a quality tier is allowed to touch.
280
+ *
281
+ * These are the ones `qualityEnvironment` names, and the ones that must snap
282
+ * back to what the scene file says rather than accumulating.
283
+ */
284
+ const TIER_KEYS = [
285
+ "shadows",
286
+ "bloom",
287
+ "post",
288
+ "clouds"
289
+ ];
290
+ function setEnvironment3D(engine, patch, opts) {
257
291
  const scene = engine.scene;
258
292
  if (!scene) throw new IncantoError("BAD_FORMAT", "setEnvironment3D: the engine has no scene.");
259
293
  const merged = { ...scene.environment ?? {} };
294
+ if (opts?.rebaseTierKeys) {
295
+ const authored = scene.source?.environment ?? {};
296
+ for (const key of TIER_KEYS) if (key in authored) merged[key] = authored[key];
297
+ else delete merged[key];
298
+ }
260
299
  for (const [key, value] of Object.entries(patch)) if (value === null) delete merged[key];
261
300
  else if (typeof value === "object" && !Array.isArray(value) && typeof merged[key] === "object" && merged[key] !== null && !Array.isArray(merged[key])) merged[key] = {
262
301
  ...merged[key],
@@ -266,6 +305,24 @@ function setEnvironment3D(engine, patch) {
266
305
  parseEnvironment3D(merged);
267
306
  scene.environment = merged;
268
307
  }
308
+ /**
309
+ * Apply a quality tier — the only caller that may RESET as well as patch.
310
+ *
311
+ * A tier is re-applied every time the player moves the slider, so it cannot
312
+ * compose with what the last one left behind: `low` strips the author's bloom,
313
+ * and `high` then has nothing to put back. This re-derives the four tier-owned
314
+ * stages from the scene FILE first, so every tier is reversible, and so a stage
315
+ * the author never wrote can never appear — the contract `settings.ts` states
316
+ * and `incanto-performance.md` promises: *the tier can only take things away.*
317
+ *
318
+ * Measured before this, on a scene with no `bloom` and no `post` in its JSON:
319
+ * `high` and `medium` both rendered five GL passes at 3.3 ms where `low`
320
+ * rendered one at 1.24 ms — 62% of the frame on a chain the game never asked
321
+ * for, and `environment.bloom` read back `{strength: 0.8, threshold: 1}`.
322
+ */
323
+ function applyQualityTier3D(engine, patch) {
324
+ setEnvironment3D(engine, patch, { rebaseTierKeys: true });
325
+ }
269
326
  //#endregion
270
327
  //#region src/3d/adaptive-resolution.ts
271
328
  const DEFAULTS = {
@@ -1430,11 +1487,11 @@ var Renderer3D = class {
1430
1487
  pixelRatio: opts.pixelRatio,
1431
1488
  preserveDrawingBuffer: opts.preserveDrawingBuffer
1432
1489
  });
1433
- this.webgl = new WebGLRenderer({
1490
+ this.webgl = withWebGLContext(() => new WebGLRenderer({
1434
1491
  canvas: opts.canvas,
1435
1492
  antialias: rendering.antialias,
1436
1493
  preserveDrawingBuffer: rendering.preserveDrawingBuffer
1437
- });
1494
+ }));
1438
1495
  this.webgl.info.autoReset = false;
1439
1496
  this.engine.picker = (x, y) => this.pick(x, y);
1440
1497
  this.basePixelRatio = rendering.pixelRatio;
@@ -2180,6 +2237,7 @@ async function createGame3D(opts) {
2180
2237
  const engine = new Engine({
2181
2238
  seed: opts.seed,
2182
2239
  fixedHz: opts.fixedHz,
2240
+ ...opts.settings ? { settings: opts.settings } : {},
2183
2241
  ...opts._scheduler ? { scheduler: opts._scheduler } : {}
2184
2242
  });
2185
2243
  const sourceJson = cloneSceneJson(opts.scene);
@@ -2294,7 +2352,7 @@ async function createGame3D(opts) {
2294
2352
  cleanups.push(engine.settings.bindAudio(engine.audio));
2295
2353
  cleanups.push(engine.settings.bindLocale(engine.locale));
2296
2354
  cleanups.push(engine.settings.bindQuality((patch) => {
2297
- setEnvironment3D(engine, patch);
2355
+ applyQualityTier3D(engine, patch);
2298
2356
  }));
2299
2357
  cleanups.push(engine.settings.bindFrameCap((fps) => {
2300
2358
  engine.maxFps = fps;
@@ -2368,7 +2426,11 @@ async function createGame3D(opts) {
2368
2426
  }
2369
2427
  return {
2370
2428
  engine,
2371
- scene,
2429
+ get scene() {
2430
+ const live = engine.scene;
2431
+ if (!live) throw new IncantoError("TREE_VIOLATION", "game.scene: the engine has no scene.");
2432
+ return live;
2433
+ },
2372
2434
  sourceJson,
2373
2435
  renderer,
2374
2436
  physics,
@@ -2454,4 +2516,4 @@ function wireAudioUnlock(engine, root, canvas) {
2454
2516
  return detachAll;
2455
2517
  }
2456
2518
  //#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 };
2519
+ 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 };
@@ -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-BC4PNtJX.js";
3
+ import { _ as AudioPlayer, v as Engine } from "./register-3ta-2Xig.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 { n as withWebGLContext, r as resolveRendering } from "./webgl-unavailable-N9nQqesw.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-D2NEmxPB.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-DM1eu_cV.js";
10
+ import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-ibCjm-wH.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-B7Y6dPZO.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
  /**
@@ -444,11 +445,11 @@ var Renderer2D = class {
444
445
  pixelRatio: 1,
445
446
  preserveDrawingBuffer: false
446
447
  }, globalThis.devicePixelRatio ?? 1, opts);
447
- this.webgl = new WebGLRenderer({
448
+ this.webgl = withWebGLContext(() => new WebGLRenderer({
448
449
  canvas: opts.canvas,
449
450
  antialias: rendering.antialias,
450
451
  preserveDrawingBuffer: rendering.preserveDrawingBuffer
451
- });
452
+ }));
452
453
  this.webgl.info.autoReset = false;
453
454
  this.engine.picker = (x, y) => this.pick(x, y);
454
455
  this.basePixelRatio = rendering.pixelRatio;
@@ -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,
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-DoFPYrgo.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-BC4PNtJX.js";
2
2
  //#region src/core/scene/duplicate.ts
3
3
  function stripUids(json) {
4
4
  const { uid: _uid, ...rest } = json;
@@ -1,4 +1,4 @@
1
- import { s as JsonObject } from "./schema-CFeioQRE.js";
1
+ import { s as JsonObject } from "./schema-B6ugCV1Q.js";
2
2
 
3
3
  //#region src/core/diagnostics.d.ts
4
4
  /** The minimum an engine has to look like to receive a diagnostic. */
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) => {
@@ -2397,19 +2398,25 @@ function Hn(e, t, n) {
2397
2398
  t.mutate(() => {
2398
2399
  t.working.dimension = r.value;
2399
2400
  });
2400
- }), e.appendChild(B("dimension", r)), e.appendChild(Tr("physics", "physics"));
2401
- let i = (t.working.physics ?? {}).gravity ?? (t.working.dimension === "3d" ? [
2401
+ }), e.appendChild(B("dimension", r));
2402
+ let i = document.createElement("input");
2403
+ i.type = "checkbox", i.checked = t.working.fragment === !0, i.addEventListener("change", () => {
2404
+ t.mutate(() => {
2405
+ i.checked ? t.working.fragment = !0 : delete t.working.fragment;
2406
+ });
2407
+ }), e.appendChild(B("fragment", i)), e.appendChild(Tr("physics", "physics"));
2408
+ let a = (t.working.physics ?? {}).gravity ?? (t.working.dimension === "3d" ? [
2402
2409
  0,
2403
2410
  -9.81,
2404
2411
  0
2405
- ] : [0, 980]), a = t.working.dimension === "3d" ? [
2412
+ ] : [0, 980]), o = t.working.dimension === "3d" ? [
2406
2413
  0,
2407
2414
  -9.81,
2408
2415
  0
2409
2416
  ] : [0, 980];
2410
- e.appendChild(Mr("gravity", i, (e) => {
2417
+ e.appendChild(Mr("gravity", a, (e) => {
2411
2418
  t.mutate(() => {
2412
- if (JSON.stringify(e) === JSON.stringify(a)) {
2419
+ if (JSON.stringify(e) === JSON.stringify(o)) {
2413
2420
  let e = { ...t.working.physics };
2414
2421
  delete e.gravity, Object.keys(e).length === 0 ? delete t.working.physics : t.working.physics = e;
2415
2422
  } else t.working.physics = {
@@ -7842,6 +7849,10 @@ var ss = class {
7842
7849
  return !0;
7843
7850
  }) === !0;
7844
7851
  }
7852
+ resume(e) {
7853
+ let t = this.original;
7854
+ this.reset(e), this.original = t;
7855
+ }
7845
7856
  reset(e) {
7846
7857
  this.working = e, this.working.root && cs(this.working.root), this.original = JSON.stringify(this.working), this.undoStack = [], this.selection = [], this.emit();
7847
7858
  }
@@ -9794,7 +9805,7 @@ async function xc(e = {}) {
9794
9805
  let t = e.target.matches("input, textarea, select");
9795
9806
  (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
9807
  };
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), {
9808
+ 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
9809
  working: () => u.working,
9799
9810
  camera: () => P.editCamera(),
9800
9811
  showError: O,
package/dist/env.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as Rng, i as SceneJson, n as NodeJson, s as JsonObject } from "./schema-CFeioQRE.js";
1
+ import { a as Rng, i as SceneJson, n as NodeJson, s as JsonObject } from "./schema-B6ugCV1Q.js";
2
2
 
3
3
  //#region src/env/arena.d.ts
4
4
  /** Valid `generateArena` themes — drives the catalog options AND validation. */
@@ -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-BC4PNtJX.js";
2
+ import { L as translateOn, t as registerCoreNodes } from "./register-3ta-2Xig.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, I as worldPosition, 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-DM1eu_cV.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);
@@ -2674,6 +2714,7 @@ var CharacterController3D = class extends Node3D {
2674
2714
  pitchMin: { default: -1.3 },
2675
2715
  pitchMax: { default: 1.5 },
2676
2716
  cameraCollision: { default: true },
2717
+ platformCarry: { default: true },
2677
2718
  turnSpeed: { default: 100 },
2678
2719
  camLerp: { default: 25 },
2679
2720
  moveAction: { default: "move" },
@@ -2726,6 +2767,43 @@ var CharacterController3D = class extends Node3D {
2726
2767
  pitchMax = 1.5;
2727
2768
  /** Spring-arm: pull the orbit camera in front of walls/ground instead of clipping. */
2728
2769
  cameraCollision = true;
2770
+ /**
2771
+ * Ride whatever you are standing on.
2772
+ *
2773
+ * `false` restores the old behaviour: the controller steers toward a target
2774
+ * velocity in WORLD space, so standing still on a moving floor brakes you off
2775
+ * it. Turn it off for a conveyor you want to be scenery.
2776
+ */
2777
+ platformCarry = true;
2778
+ /** The node the ground probe hit last step, and where it was. */
2779
+ carriedBy = null;
2780
+ carriedFrom = [0, 0];
2781
+ /**
2782
+ * How fast the floor is moving, in world units per second.
2783
+ *
2784
+ * From the POSITION DELTA, not `linearVelocity`, because the documented way
2785
+ * to author a platform is to move its `position` — a behavior, a `PathFollow`,
2786
+ * an `Oscillate` — and that path calls `body.setTranslation`, which teleports
2787
+ * and carries no momentum at all. The delta sees both kinds.
2788
+ *
2789
+ * Only while grounded on the SAME node two steps running: the first step on a
2790
+ * new platform has nothing to compare against, and inventing a velocity there
2791
+ * would fling the player on every step onto one.
2792
+ */
2793
+ groundCarry(ground, dt) {
2794
+ if (!this.platformCarry || !this.grounded || !ground || dt <= 0) {
2795
+ this.carriedBy = ground;
2796
+ if (ground) this.carriedFrom = groundXZ(ground);
2797
+ return [0, 0];
2798
+ }
2799
+ const at = groundXZ(ground);
2800
+ const same = this.carriedBy === ground;
2801
+ const was = this.carriedFrom;
2802
+ this.carriedBy = ground;
2803
+ this.carriedFrom = at;
2804
+ if (!same) return [0, 0];
2805
+ return [(at[0] - was[0]) / dt, (at[1] - was[1]) / dt];
2806
+ }
2729
2807
  turnSpeed = 100;
2730
2808
  /** Camera smoothing rate (1-exp(-rate·dt)); side view uses 5. */
2731
2809
  camLerp = 25;
@@ -2790,6 +2868,8 @@ var CharacterController3D = class extends Node3D {
2790
2868
  const rayLen = radius + 2;
2791
2869
  const originY = pos[1] - halfHeight;
2792
2870
  let toi = null;
2871
+ /** What the probe landed on — the platform, when there is one. */
2872
+ let ground = null;
2793
2873
  for (const [ox, oz] of [
2794
2874
  [0, 0],
2795
2875
  [radius, 0],
@@ -2808,11 +2888,15 @@ var CharacterController3D = class extends Node3D {
2808
2888
  ], rayLen, body);
2809
2889
  if (hit) {
2810
2890
  const d = hit.distance;
2811
- toi = toi === null ? d : Math.min(toi, d);
2891
+ if (toi === null || d < toi) {
2892
+ toi = d;
2893
+ ground = hit.node;
2894
+ }
2812
2895
  if (ox === 0 && oz === 0) break;
2813
2896
  }
2814
2897
  }
2815
2898
  this.grounded = toi !== null && toi < floatingDis * 2;
2899
+ const carry = this.groundCarry(ground, dt);
2816
2900
  const footed = toi !== null && toi < floatingDis * 1.3 && (vel[1] ?? 0) <= .5;
2817
2901
  let mx = 0;
2818
2902
  let mz = 0;
@@ -2837,17 +2921,17 @@ var CharacterController3D = class extends Node3D {
2837
2921
  const speedMultiplier = 1 + (this.sprintMultiplier - 1) * intensity;
2838
2922
  const targetSpeed = moving ? this.maxSpeed * speedMultiplier : 0;
2839
2923
  const control = this.grounded ? 1 : this.airControl;
2840
- const ax = (mx * targetSpeed - vel[0]) / ACC_DELTA_TIME * control;
2841
- const az = (mz * targetSpeed - vel[2]) / ACC_DELTA_TIME * control;
2924
+ const ax = (mx * targetSpeed + carry[0] - vel[0]) / ACC_DELTA_TIME * control;
2925
+ const az = (mz * targetSpeed + carry[1] - vel[2]) / ACC_DELTA_TIME * control;
2842
2926
  body.applyImpulse([
2843
2927
  ax * mass,
2844
2928
  0,
2845
2929
  az * mass
2846
2930
  ]);
2847
2931
  if (!moving && this.grounded) body.applyImpulse([
2848
- -vel[0] * DRAG_DAMPING * mass,
2932
+ -(vel[0] - carry[0]) * DRAG_DAMPING * mass,
2849
2933
  0,
2850
- -vel[2] * DRAG_DAMPING * mass
2934
+ -(vel[2] - carry[1]) * DRAG_DAMPING * mass
2851
2935
  ]);
2852
2936
  let springToi = toi;
2853
2937
  if (this.stepHeight > 0 && moving && toi !== null && !this.jumping && (vel[1] ?? 0) <= .5) {
@@ -3097,6 +3181,11 @@ function worldOf(node) {
3097
3181
  * coyote time incoherent (a held button would re-fire through the whole
3098
3182
  * window). Undeclared actions are tolerated, and reported once.
3099
3183
  */
3184
+ /** A node's world x/z — the platform may itself be parented to something. */
3185
+ function groundXZ(node) {
3186
+ const world = worldPosition(node);
3187
+ return [world[0] ?? 0, world[2] ?? 0];
3188
+ }
3100
3189
  //#endregion
3101
3190
  //#region src/3d/vegetation/grass-placement.ts
3102
3191
  const NOISE_SCALE = .15;
@@ -1,7 +1,7 @@
1
- import { O as Node, P as Behavior, _ as SaveSlots, k as diagnose, n as loadScene, w as effectiveOrder, z as registerBehavior } from "./loader-lQDCwNag.js";
1
+ import { O as Node, P as Behavior, _ as SaveSlots, k as diagnose, n as loadScene, w as effectiveOrder, z as registerBehavior } from "./loader-BC4PNtJX.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { t as jsonClone } from "./json-CwwhxQgb.js";
4
- import { t as duplicateNode } from "./duplicate-CqSAtdrh.js";
4
+ import { t as duplicateNode } from "./duplicate-IWIqk0HJ.js";
5
5
  import { Color, CubeCamera, DepthTexture, DoubleSide, Euler, FloatType, Frustum, HalfFloatType, LinearMipmapLinearFilter, MathUtils, Matrix4, Mesh, MeshDepthMaterial, Object3D, PerspectiveCamera, Plane, PlaneGeometry, Quaternion, ShaderMaterial, Sphere, Vector2, Vector3, Vector4, WebGLCubeRenderTarget, WebGLRenderTarget } from "three";
6
6
  //#region src/3d/frustum.ts
7
7
  const scratchFrustum = new Frustum();
@@ -5016,12 +5016,20 @@ var Health = class extends Behavior {
5016
5016
  * unlosable. Every checker was green on it, and `incanto-playtest` even
5017
5017
  * reported `lost in 19/20` because a single `died` reads as a loss.
5018
5018
  *
5019
- * Wire it where the life is spent:
5019
+ * **Wire `reviveFull`, not this, to `lifeLost`.** `ScoreKeeper.loseLife()`
5020
+ * emits `lifeLost` WITH the remaining life count, and this method's first
5021
+ * parameter is the HP to come back at — so the documented recipe revived at
5022
+ * 2 HP, then 1, then 0, and nothing warned (the wire-arity guard fires only
5023
+ * when a handler is UNDER-fed, and `hp` is optional):
5020
5024
  *
5021
- * ```json
5022
- * { "signal": "lifeLost", "from": "Score", "to": "Player", "handler": "revive" }
5025
+ * ```
5026
+ * death 1: lives=2 hp=2
5027
+ * death 2: lives=1 hp=1
5028
+ * death 3: lives=0 hp=0 dead=false <- alive at zero HP
5023
5029
  * ```
5024
5030
  *
5031
+ * Reach for `revive(hp)` when your game genuinely wants a partial comeback.
5032
+ *
5025
5033
  * A no-op on the living — reviving someone who never died would silently
5026
5034
  * refill their health, which is a different feature and not this one.
5027
5035
  */
@@ -5033,6 +5041,21 @@ var Health = class extends Behavior {
5033
5041
  this.invulnTimer = this.invulnerableFor;
5034
5042
  this.emit("revived", this.current);
5035
5043
  }
5044
+ /**
5045
+ * Bring a dead one back at FULL health — the handler a life-lost wire wants.
5046
+ *
5047
+ * It exists because `revive` takes an optional `hp` and `lifeLost` carries the
5048
+ * life count, so wiring the two silently made "you have 2 lives left" mean
5049
+ * "come back at 2 HP". This takes no arguments, so no signal's payload can
5050
+ * ever change what it means.
5051
+ *
5052
+ * ```json
5053
+ * { "signal": "lifeLost", "from": "Score", "to": "Player", "handler": "reviveFull" }
5054
+ * ```
5055
+ */
5056
+ reviveFull() {
5057
+ this.revive();
5058
+ }
5036
5059
  /** Drop to 0 and die immediately (ignores i-frames). */
5037
5060
  kill() {
5038
5061
  if (this.dead) return;
@@ -6652,6 +6675,20 @@ var SavePoint = class extends Behavior {
6652
6675
  this.slots ??= new SaveSlots(this.game);
6653
6676
  return this.slots;
6654
6677
  }
6678
+ /**
6679
+ * Has this run already said the saves are going nowhere?
6680
+ *
6681
+ * `saved(slot)` is the engine's own instrument for "it saved", and it fired
6682
+ * identically in a private window where the write landed in a Map that dies
6683
+ * with the tab. In-session everything looked healthy — the slot read back,
6684
+ * `slotScene()` answered — and across the reload the score went 5 to 0 and
6685
+ * `list()` went `["1"]` to `[]`, with `engine.log.entries().length = 0` and
6686
+ * `stats().errors = 0` in every phase. Byte-identical to a healthy control.
6687
+ *
6688
+ * So it goes through `engine.log`, not just the console: that is what
6689
+ * `incanto-logs` reads and what the `says` rung reports.
6690
+ */
6691
+ warnedVolatile = false;
6655
6692
  pendingRestore = false;
6656
6693
  pendingProbe = false;
6657
6694
  onReady() {
@@ -6711,6 +6748,10 @@ var SavePoint = class extends Behavior {
6711
6748
  savedAt: Date.now(),
6712
6749
  playtime: Math.round(this.elapsed)
6713
6750
  });
6751
+ if (!this.store().persistent && !this.warnedVolatile) {
6752
+ this.warnedVolatile = true;
6753
+ engine.log.warn(`SavePoint wrote slot '${this.slot}' to memory, not to disk — this browser refused localStorage (private window, storage disabled, or quota exhausted), so the run is lost when the page closes. \`SaveSlots.persistent\` is how a game asks before promising the player anything.`);
6754
+ }
6714
6755
  this.emit("saved", this.slot);
6715
6756
  }
6716
6757
  /**