incanto 0.67.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 (70) hide show
  1. package/bin/_pick-server.mjs +48 -0
  2. package/bin/incanto-check.mjs +16 -2
  3. package/bin/incanto-frame.mjs +10 -25
  4. package/bin/incanto-logs.mjs +8 -18
  5. package/dist/2d.d.ts +6 -6
  6. package/dist/2d.js +6 -4
  7. package/dist/3d.d.ts +30 -6
  8. package/dist/3d.js +8 -6
  9. package/dist/{audio-player-BNUHo0JF.d.ts → audio-player-DOrq7sP-.d.ts} +62 -4
  10. package/dist/{behavior-ZnfzKDo3.d.ts → behavior-DoFPYrgo.d.ts} +78 -1
  11. package/dist/boot-failure-CKYrEcGF.js +47 -0
  12. package/dist/{create-game-FAXdHta3.js → create-game-DbWtVTxD.js} +21 -25
  13. package/dist/{create-game-DMetd3vR.js → create-game-IZIydDwI.js} +9 -9
  14. package/dist/debug.d.ts +1 -1
  15. package/dist/{duplicate-B8QnX2-r.js → duplicate-IWIqk0HJ.js} +1 -1
  16. package/dist/{editor-switch-DAvWQeld.d.ts → editor-switch-CnIOiyNJ.d.ts} +1 -1
  17. package/dist/editor.js +11 -5
  18. package/dist/env.d.ts +1 -1
  19. package/dist/{environment-presets-C-5g2pMh.js → environment-presets-CybQXNqS.js} +57 -8
  20. package/dist/{gameplay-CGSv-9XR.js → gameplay-DM1eu_cV.js} +20 -2
  21. package/dist/gameplay.d.ts +51 -3
  22. package/dist/gameplay.js +2 -2
  23. package/dist/index.d.ts +6 -6
  24. package/dist/index.js +8 -7
  25. package/dist/{loader-KZESOZmE.js → loader-BC4PNtJX.js} +26 -1
  26. package/dist/{loader-DyT30ArB.d.ts → loader-CcB533FR.d.ts} +2 -2
  27. package/dist/net.d.ts +2 -2
  28. package/dist/net.js +1 -1
  29. package/dist/{physics-2d-_B6zj3zU.js → physics-2d-B7Y6dPZO.js} +2 -2
  30. package/dist/{physics-3d-i1IeAY0F.js → physics-3d-bG3n70Ky.js} +3 -3
  31. package/dist/react.d.ts +2 -2
  32. package/dist/react.js +1 -1
  33. package/dist/{register-Cs53CY7s.js → register-3ta-2Xig.js} +68 -7
  34. package/dist/{register-vIxU3Xbv.js → register-ibCjm-wH.js} +3 -3
  35. package/dist/{replay-BkpDV51O.js → replay-CYvhVHHN.js} +28 -5
  36. package/dist/{replay-D0GcEHYU.d.ts → replay-Dvn8aeBd.d.ts} +38 -4
  37. package/dist/{schema-CFeioQRE.d.ts → schema-B6ugCV1Q.d.ts} +11 -0
  38. package/dist/{split-screen-DS8RaUV_.js → split-screen-CYwDkbLF.js} +2 -2
  39. package/dist/{split-screen-DD5eYIgW.d.ts → split-screen-DhrSzZIB.d.ts} +3 -3
  40. package/dist/{src-BLlsGJW9.js → src-D7RIqXYF.js} +1 -1
  41. package/dist/{teardown-LB9cTnXJ.js → teardown-D2NEmxPB.js} +2 -2
  42. package/dist/{test-81uZ967a.js → test-8hoHeRmo.js} +26 -21
  43. package/dist/test.d.ts +6 -6
  44. package/dist/test.js +3 -3
  45. package/dist/vite.js +2 -2
  46. package/dist/{rendering-options-ByAdi8PF.js → webgl-unavailable-N9nQqesw.js} +45 -1
  47. package/editor/assets/{agent8-C2jtNKbx.js → agent8-BrrHOjMJ.js} +1 -1
  48. package/editor/assets/{debug-DoU3KeqS.js → debug-BBhKuBNV.js} +1 -1
  49. package/editor/assets/{index-CNqHojcC.js → index-eVd0BToA.js} +92 -92
  50. package/editor/index.html +1 -1
  51. package/package.json +1 -1
  52. package/schemas/scene.schema.json +4 -0
  53. package/skills/incanto-3d-character.md +1 -0
  54. package/skills/incanto-audio.md +49 -0
  55. package/skills/incanto-building-2d-games.md +37 -0
  56. package/skills/incanto-building-3d-games.md +37 -0
  57. package/skills/incanto-gameplay-behaviors.md +28 -1
  58. package/skills/incanto-node-reference.md +1 -0
  59. package/skills/incanto-physics-and-input.md +23 -0
  60. package/skills/incanto-scene-json-authoring.md +12 -1
  61. package/templates-app/beacon-isle-3d/package.json +1 -1
  62. package/templates-app/beacon-isle-3d/src/main.ts +8 -1
  63. package/templates-app/platformer-2d/package.json +1 -1
  64. package/templates-app/platformer-2d/src/main.ts +8 -1
  65. package/templates-app/star-survivor/package.json +1 -1
  66. package/templates-app/star-survivor/src/main.ts +8 -1
  67. package/templates-app/tps-3d/package.json +1 -1
  68. package/templates-app/tps-3d/src/main.ts +8 -1
  69. package/templates-app/village-quest-3d/package.json +1 -1
  70. package/templates-app/village-quest-3d/src/main.ts +8 -1
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Which of the dev servers that answered is THIS project's — the one decision
3
+ * `incanto-frame` and `incanto-logs` must never get wrong, in one place.
4
+ *
5
+ * They each had their own copy, and each copy had the same hatch: when exactly
6
+ * one server answered and it did not say whose it was, take it anyway ("an
7
+ * older incanto that predates the identity field"). A server that will not
8
+ * identify itself is the ONE kind the check cannot clear, so the hatch let
9
+ * through precisely what it was written to stop. Measured in an empty temp
10
+ * directory with one foreign page listening, on a scene with two nodes and no
11
+ * assets:
12
+ *
13
+ * ✓ draws — subject fills 37.5% of the frame
14
+ * ✗ says — asset …/NO-SUCH-TEXTURE.jpg never loaded: 404
15
+ *
16
+ * A ✓ measured off another game, and a ✗ naming a texture that appears nowhere
17
+ * in this project. It surfaced in this repo as an intermittent failure of
18
+ * `incanto-verify.test.ts`, which is a gate that lies in both directions.
19
+ *
20
+ * The real dev server has sent `root` since 0.41 (`frame-endpoint.test.ts`
21
+ * pins it), so nothing is lost; `--port` still names a server on purpose.
22
+ */
23
+
24
+ /** Is `body.root` this directory, an ancestor of it, or inside it? */
25
+ export function sameProject(body, cwd) {
26
+ if (!body?.root) return false;
27
+ const root = String(body.root);
28
+ return root === cwd || cwd.startsWith(`${root}/`) || root.startsWith(`${cwd}/`);
29
+ }
30
+
31
+ /**
32
+ * `{ pick }` when one of them is ours, `{ refusal }` when some answered and
33
+ * none is, `{}` when nothing answered at all (the caller says "no dev server").
34
+ */
35
+ export function pickServer(answered, cwd, what) {
36
+ const mine = answered.filter((a) => sameProject(a, cwd));
37
+ if (mine.length > 0) return { pick: mine[0] };
38
+ if (answered.length === 0) return {};
39
+ const listed = answered
40
+ .map((a) => ` :${a.port} ${a.project || '(unnamed)'} ${a.root ?? "doesn't say"}`)
41
+ .join('\n');
42
+ return {
43
+ refusal:
44
+ `${answered.length} incanto dev server(s) are running and none of them is this ` +
45
+ `directory (${cwd}):\n${listed}\n\n${what}\n` +
46
+ "Start this project's dev server, or pass --port.",
47
+ };
48
+ }
@@ -155,7 +155,9 @@ if (files.length === 0) {
155
155
  process.exit(1);
156
156
  }
157
157
 
158
- const { auditScene, validateScene } = await import(pathToFileURL(join(DIST, 'test.js')).href);
158
+ const { auditScene, instancedScenes, validateScene } = await import(
159
+ pathToFileURL(join(DIST, 'test.js')).href
160
+ );
159
161
 
160
162
  /**
161
163
  * Every `strings` table in the corpus, unioned.
@@ -176,9 +178,16 @@ const declaredElsewhere = {};
176
178
  * part of its project it can.
177
179
  */
178
180
  const declaredAssets = {};
181
+ /**
182
+ * Every scene some OTHER scene embeds — the only honest way to tell a fragment
183
+ * from a first draft. Without it a 3D scene with no camera and no light checked
184
+ * CLEAN, because "has no camera" was doing double duty as "is a fragment".
185
+ */
186
+ const corpus = [];
179
187
  for (const file of files) {
180
188
  try {
181
189
  const doc = JSON.parse(readFileSync(file, 'utf-8'));
190
+ corpus.push({ path: resolve(file), json: doc });
182
191
  for (const [locale, entries] of Object.entries(doc?.strings ?? {})) {
183
192
  if (typeof entries !== 'object' || entries === null) continue;
184
193
  declaredElsewhere[locale] = { ...(declaredElsewhere[locale] ?? {}), ...entries };
@@ -191,6 +200,8 @@ for (const file of files) {
191
200
  }
192
201
  }
193
202
 
203
+ const instanced = instancedScenes(corpus, (from, ref) => resolve(dirname(from), ref));
204
+
194
205
  const results = files.map((file) => {
195
206
  let json;
196
207
  try {
@@ -207,7 +218,10 @@ const results = files.map((file) => {
207
218
  return {
208
219
  file,
209
220
  ok: true,
210
- warnings: [...auditScene(json, { declaredElsewhere }), ...missingArt(file, json)],
221
+ warnings: [
222
+ ...auditScene(json, { declaredElsewhere, instanced: instanced.has(resolve(file)) }),
223
+ ...missingArt(file, json),
224
+ ],
211
225
  };
212
226
  }
213
227
  return {
@@ -20,6 +20,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
20
20
  import { createRequire } from 'node:module';
21
21
  import { dirname, join } from 'node:path';
22
22
  import { fileURLToPath, pathToFileURL } from 'node:url';
23
+ import { pickServer } from './_pick-server.mjs';
23
24
 
24
25
  const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
25
26
  const { listeningPorts, parseProcNetTcp } = await import(
@@ -171,11 +172,6 @@ const answered = [];
171
172
  // is plainly on screen.
172
173
  const HOSTS = ['localhost', '127.0.0.1'];
173
174
  const CWD = process.cwd();
174
- const sameProject = (b) => {
175
- if (!b?.root) return false;
176
- const r = String(b.root);
177
- return r === CWD || CWD.startsWith(`${r}/`) || r.startsWith(`${CWD}/`);
178
- };
179
175
  for (const port of ports) {
180
176
  for (const host of HOSTS) {
181
177
  try {
@@ -197,26 +193,15 @@ for (const port of ports) {
197
193
  if (explicit) {
198
194
  found = answered[0] ?? null;
199
195
  } else {
200
- const mine = answered.filter(sameProject);
201
- if (mine.length > 0) {
202
- found = mine[0];
203
- } else if (answered.length === 1 && answered[0].root === undefined) {
204
- // An older dev server that predates the identity field. Take it, but say so.
205
- found = answered[0];
206
- console.error(
207
- the dev server on :${found.port} does not report which project it is ` +
208
- '(older incanto) — if this reads the wrong game, pass --port.',
209
- );
210
- } else if (answered.length > 0) {
211
- console.error(
212
- `${answered.length} incanto dev server(s) are running and NONE of them is this ` +
213
- `directory (${CWD}):\n` +
214
- answered
215
- .map((a) => ` :${a.port} ${a.project || '(unnamed)'} ${a.root ?? '?'}`)
216
- .join('\n') +
217
- "\n\nMeasuring one project on another project's page reports a number that has\n" +
218
- "nothing to do with this game. Start this project's dev server, or pass --port.",
219
- );
196
+ const { pick, refusal } = pickServer(
197
+ answered,
198
+ CWD,
199
+ "Measuring one project on another project's page reports a number that has\n" +
200
+ 'nothing to do with this game.',
201
+ );
202
+ if (pick) found = pick;
203
+ else if (refusal) {
204
+ console.error(refusal);
220
205
  process.exit(1);
221
206
  }
222
207
  }
@@ -21,6 +21,7 @@ import { readFileSync } from 'node:fs';
21
21
  import { createRequire } from 'node:module';
22
22
  import { dirname, join } from 'node:path';
23
23
  import { fileURLToPath, pathToFileURL } from 'node:url';
24
+ import { pickServer } from './_pick-server.mjs';
24
25
 
25
26
  const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
26
27
  const { listeningPorts } = await import(pathToFileURL(join(PKG, 'dist', 'vite.js')).href);
@@ -80,11 +81,6 @@ let found = null;
80
81
  // project's page answered both the frame and the logs request.
81
82
  const answered = [];
82
83
  const CWD = process.cwd();
83
- const sameProject = (b) => {
84
- if (!b?.root) return false;
85
- const r = String(b.root);
86
- return r === CWD || CWD.startsWith(`${r}/`) || r.startsWith(`${CWD}/`);
87
- };
88
84
  for (const port of ports) {
89
85
  for (const host of ['localhost', '127.0.0.1']) {
90
86
  try {
@@ -105,20 +101,14 @@ for (const port of ports) {
105
101
  if (explicit) {
106
102
  found = answered[0] ? { host: answered[0].host, port: answered[0].port } : null;
107
103
  } else {
108
- const mine = answered.filter(sameProject);
109
- const pick =
110
- mine[0] ?? (answered.length === 1 && answered[0].root === undefined ? answered[0] : null);
104
+ const { pick, refusal } = pickServer(
105
+ answered,
106
+ CWD,
107
+ "Reading another project's log buffer would report its errors as yours.",
108
+ );
111
109
  if (pick) found = { host: pick.host, port: pick.port };
112
- else if (answered.length > 0) {
113
- console.error(
114
- `${answered.length} incanto dev server(s) are running and none is this directory ` +
115
- `(${CWD}):\n` +
116
- answered
117
- .map((a) => ` :${a.port} ${a.project || '(unnamed)'} ${a.root ?? '?'}`)
118
- .join('\n') +
119
- "\n\nReading another project's log buffer would report its errors as yours.\n" +
120
- "Start this project's dev server, or pass --port.",
121
- );
110
+ else if (refusal) {
111
+ console.error(refusal);
122
112
  process.exit(1);
123
113
  }
124
114
  }
package/dist/2d.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, w as GameStats, x as EngineOptions } from "./behavior-ZnfzKDo3.js";
2
- import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DAvWQeld.js";
3
- import { i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
1
+ import { c as showBootFailure, i as ParticleSim, o as ParticleView, r as SpatialPose, s as isWebGLAvailable } from "./audio-player-DOrq7sP-.js";
2
+ import { P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, w as GameStats, x as EngineOptions } from "./behavior-DoFPYrgo.js";
3
+ import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-CnIOiyNJ.js";
4
+ import { i as SceneJson, s as JsonObject } from "./schema-B6ugCV1Q.js";
4
5
  import { r as FrameStats } from "./frame-report-DCnHFmto.js";
5
- import { t as LoadSceneOptions } from "./loader-DyT30ArB.js";
6
+ import { t as LoadSceneOptions } from "./loader-CcB533FR.js";
6
7
  import { n as AnimationEntry } from "./sprite-animation-CMr6f1K2.js";
7
- import { i as ParticleSim, o as ParticleView, r as SpatialPose } from "./audio-player-BNUHo0JF.js";
8
8
  import { Group, Mesh, Object3D, Scene, Texture } from "three";
9
9
  import * as RapierNs from "@dimforge/rapier2d-compat";
10
10
 
@@ -1247,4 +1247,4 @@ declare function parseCells(cells: readonly (string | readonly number[])[], lege
1247
1247
  */
1248
1248
  declare function mergeSolidRects(grid: readonly (readonly number[])[], solid: Set<number>): TileRect[];
1249
1249
  //#endregion
1250
- export { AnimatedSprite2D, Area2D, type AssetStatus, AssetStore2D, Camera2D, CharacterBody2D, CharacterController2D, ColorRect2D, type CreateGame2DOptions, type Game2D, Joint2D, type JointType2D, Label, Node2D, Particles2D, Physics2D, type Physics2DOptions, PhysicsBody2D, Renderer2D, type Renderer2DOptions, type ResolvedSpriteTexture, RigidBody2D, type SheetInfo, Sprite2D, type SpriteFromLibraryResult, StaticBody2D, type Sync2DResult, type TextureLoadCallbacks, TileMap2D, type TileRect, type UIAnchor, UILayer, createGame2D, enablePhysics2D, mergeSolidRects, parseCells, registerNodes2D, spriteFromLibraryMeta, syncTree2D };
1250
+ export { AnimatedSprite2D, Area2D, type AssetStatus, AssetStore2D, Camera2D, CharacterBody2D, CharacterController2D, ColorRect2D, type CreateGame2DOptions, type Game2D, Joint2D, type JointType2D, Label, Node2D, Particles2D, Physics2D, type Physics2DOptions, PhysicsBody2D, Renderer2D, type Renderer2DOptions, type ResolvedSpriteTexture, RigidBody2D, type SheetInfo, Sprite2D, type SpriteFromLibraryResult, StaticBody2D, type Sync2DResult, type TextureLoadCallbacks, TileMap2D, type TileRect, type UIAnchor, UILayer, createGame2D, enablePhysics2D, isWebGLAvailable, mergeSolidRects, parseCells, registerNodes2D, showBootFailure, spriteFromLibraryMeta, syncTree2D };
package/dist/2d.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
- import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-DMetd3vR.js";
3
- import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-vIxU3Xbv.js";
4
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-_B6zj3zU.js";
2
+ import { t as showBootFailure } from "./boot-failure-CKYrEcGF.js";
3
+ import { t as isWebGLAvailable } from "./webgl-unavailable-N9nQqesw.js";
4
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-IZIydDwI.js";
5
+ import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-ibCjm-wH.js";
6
+ import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-B7Y6dPZO.js";
5
7
  //#region src/2d/library-sprite.ts
6
8
  /**
7
9
  * What a `CharacterController2D`/`3D` will ask a skin to play, and the clip in
@@ -95,4 +97,4 @@ function spriteFromLibraryMeta(meta, opts) {
95
97
  };
96
98
  }
97
99
  //#endregion
98
- export { AnimatedSprite2D, Area2D, AssetStore2D, Camera2D, CharacterBody2D, CharacterController2D, ColorRect2D, Joint2D, Label, Node2D, Particles2D, Physics2D, PhysicsBody2D, Renderer2D, RigidBody2D, Sprite2D, StaticBody2D, TileMap2D, UILayer, createGame2D, enablePhysics2D, mergeSolidRects, parseCells, registerNodes2D, spriteFromLibraryMeta, syncTree2D };
100
+ export { AnimatedSprite2D, Area2D, AssetStore2D, Camera2D, CharacterBody2D, CharacterController2D, ColorRect2D, Joint2D, Label, Node2D, Particles2D, Physics2D, PhysicsBody2D, Renderer2D, RigidBody2D, Sprite2D, StaticBody2D, TileMap2D, UILayer, createGame2D, enablePhysics2D, isWebGLAvailable, mergeSolidRects, parseCells, registerNodes2D, showBootFailure, spriteFromLibraryMeta, syncTree2D };
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, x as EngineOptions } from "./behavior-ZnfzKDo3.js";
2
- import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DAvWQeld.js";
3
- import { i as SceneJson$1, s as JsonObject } from "./schema-CFeioQRE.js";
1
+ import { c as showBootFailure, i as ParticleSim, o as ParticleView, r as SpatialPose, s as isWebGLAvailable } from "./audio-player-DOrq7sP-.js";
2
+ import { D as QualityCaps, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, w as GameStats, x as EngineOptions } from "./behavior-DoFPYrgo.js";
3
+ import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-CnIOiyNJ.js";
4
+ import { i as SceneJson$1, s as JsonObject } from "./schema-B6ugCV1Q.js";
4
5
  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-DyT30ArB.js";
6
- import { i as ParticleSim, o as ParticleView, r as SpatialPose } from "./audio-player-BNUHo0JF.js";
6
+ import { t as LoadSceneOptions } from "./loader-CcB533FR.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";
@@ -1404,6 +1404,30 @@ declare class CharacterController3D extends Node3D {
1404
1404
  pitchMax: number;
1405
1405
  /** Spring-arm: pull the orbit camera in front of walls/ground instead of clipping. */
1406
1406
  cameraCollision: boolean;
1407
+ /**
1408
+ * Ride whatever you are standing on.
1409
+ *
1410
+ * `false` restores the old behaviour: the controller steers toward a target
1411
+ * velocity in WORLD space, so standing still on a moving floor brakes you off
1412
+ * it. Turn it off for a conveyor you want to be scenery.
1413
+ */
1414
+ platformCarry: boolean;
1415
+ /** The node the ground probe hit last step, and where it was. */
1416
+ private carriedBy;
1417
+ private carriedFrom;
1418
+ /**
1419
+ * How fast the floor is moving, in world units per second.
1420
+ *
1421
+ * From the POSITION DELTA, not `linearVelocity`, because the documented way
1422
+ * to author a platform is to move its `position` — a behavior, a `PathFollow`,
1423
+ * an `Oscillate` — and that path calls `body.setTranslation`, which teleports
1424
+ * and carries no momentum at all. The delta sees both kinds.
1425
+ *
1426
+ * Only while grounded on the SAME node two steps running: the first step on a
1427
+ * new platform has nothing to compare against, and inventing a velocity there
1428
+ * would fling the player on every step onto one.
1429
+ */
1430
+ private groundCarry;
1407
1431
  turnSpeed: number;
1408
1432
  /** Camera smoothing rate (1-exp(-rate·dt)); side view uses 5. */
1409
1433
  camLerp: number;
@@ -4106,4 +4130,4 @@ declare function createNavDebugNode(nav: TerrainNav, opts?: {
4106
4130
  name?: string;
4107
4131
  }): InstancedMesh3D;
4108
4132
  //#endregion
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 };
4133
+ 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, isWebGLAvailable, keyboardIntensity, modelVerdict, movementState, parseEnvironment3D, projectToRiver, registerNodes3D, resolveFlowerDensity, rigPose, riverCarveChannels, riverStepFor, setEnvironment3D, showBootFailure, smoothCourse, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers, traceDownhillPath, verdictText };
package/dist/3d.js CHANGED
@@ -1,10 +1,12 @@
1
- import { c as parseEnvironment3D, l as sunDirectionFromElevationAzimuth, s as horizonColorFromSky, u as sunDirectionFromSky } from "./loader-KZESOZmE.js";
1
+ import { c as parseEnvironment3D, l as sunDirectionFromElevationAzimuth, s as horizonColorFromSky, u as sunDirectionFromSky } from "./loader-BC4PNtJX.js";
2
+ import { t as showBootFailure } from "./boot-failure-CKYrEcGF.js";
3
+ import { t as isWebGLAvailable } from "./webgl-unavailable-N9nQqesw.js";
2
4
  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-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";
5
+ import { G as StaticBody3D, H as CharacterBody3D, K as Node3D, L as Water3D, R as WATER_CUTOUT_MAX, U as PhysicsBody3D, V as Area3D, W as RigidBody3D, Y as WATER_MAX_RIPPLES, z as WaterCutout3D } from "./gameplay-DM1eu_cV.js";
6
+ import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-CybQXNqS.js";
7
+ import { a as Environment3D, c as AssetStore3D, i as syncTree, o as applyQualityTier3D, r as Renderer3D, s as setEnvironment3D, t as createGame3D } from "./create-game-DbWtVTxD.js";
6
8
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
7
- import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-i1IeAY0F.js";
9
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-bG3n70Ky.js";
8
10
  //#region src/3d/model-verdict.ts
9
11
  /** Mixamo exports every bone as `mixamorigX`; the retargeter binds by that name. */
10
12
  const MIXAMO = /^mixamorig[:_]?/i;
@@ -317,4 +319,4 @@ function createNavDebugNode(nav, opts) {
317
319
  return node;
318
320
  }
319
321
  //#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, 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 };
322
+ 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, isWebGLAvailable, keyboardIntensity, modelVerdict, movementState, parseEnvironment3D, projectToRiver, registerNodes3D, resolveFlowerDensity, rigPose, riverCarveChannels, riverStepFor, setEnvironment3D, showBootFailure, smoothCourse, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers, traceDownhillPath, verdictText };
@@ -1,6 +1,42 @@
1
- import { d as PropSchema, dt as Listener, jt as Node } from "./behavior-ZnfzKDo3.js";
2
- import { a as Rng } from "./schema-CFeioQRE.js";
1
+ import { d as PropSchema, dt as Listener, jt as Node } from "./behavior-DoFPYrgo.js";
2
+ import { a as Rng } from "./schema-B6ugCV1Q.js";
3
3
 
4
+ //#region src/core/boot-failure.d.ts
5
+ /**
6
+ * Put a boot failure on the SCREEN, where the player is looking.
7
+ *
8
+ * Every shipped starter preloads its assets behind a `#loading` overlay and
9
+ * removes that overlay on the line after `await createGame*`. So anything that
10
+ * rejects — no WebGL context, a scene that will not load, a 404 the preload
11
+ * insisted on — leaves the overlay standing at "100%" forever, with the reason
12
+ * in a console the player will never open. Measured on a pristine scaffold with
13
+ * WebGL denied:
14
+ *
15
+ * ```
16
+ * control #loading gone "Talk to the Elder [E] …"
17
+ * no webgl #loading present "Emberwood 100%" … and still there 8.6 s later
18
+ * ```
19
+ *
20
+ * A bar that reaches 100% and stops reads as a hang, which is the one failure
21
+ * mode a player cannot report usefully. This turns it into a sentence.
22
+ *
23
+ * Headless it does nothing but rethrow-safe logging, so a test that boots a
24
+ * game without a document is unaffected.
25
+ */
26
+ declare function showBootFailure(error: unknown, opts?: {
27
+ into?: string;
28
+ }): void;
29
+ //#endregion
30
+ //#region src/core/webgl-unavailable.d.ts
31
+ /**
32
+ * Can this document make a WebGL context at all?
33
+ *
34
+ * The question a game wants BEFORE it boots, so the answer can be a screen
35
+ * instead of an exception. Uses a throwaway canvas and never touches yours;
36
+ * `false` outside a browser, where the question is not meaningful.
37
+ */
38
+ declare function isWebGLAvailable(): boolean;
39
+ //#endregion
4
40
  //#region src/core/particle-sim.d.ts
5
41
  /**
6
42
  * Deterministic particle pool — pure math, renderer-agnostic, headless-
@@ -197,7 +233,29 @@ declare class AudioPlayer extends Node {
197
233
  private spatialElementGain;
198
234
  /** The `spatial` option for the SFX path, or undefined when not spatial. */
199
235
  private spatialPlay;
200
- play(): void;
236
+ /**
237
+ * Start the sound. `at` schedules it on the AUDIO clock instead of now.
238
+ *
239
+ * A frame is a 16.67 ms grid at 60 Hz and 33.33 ms at 30, so a sound fired
240
+ * from `update()` cannot land closer than one frame to where a chart wants
241
+ * it — measured 0 of 64 notes scheduled on the documented path. Queue it a
242
+ * lead ahead of `engine.sfx.now` and the same notes land at |mean| 0.000000
243
+ * ms:
244
+ *
245
+ * ```ts
246
+ * const lead = 0.08; // schedule this far ahead
247
+ * for (const note of dueSoon(engine.sfx.now + lead)) {
248
+ * hit.play(engine.sfx.now + (note.atSec - songSeconds));
249
+ * }
250
+ * ```
251
+ *
252
+ * PRESETS only — a `src` clip goes through an `<audio>` element, which has no
253
+ * scheduling clock, and passing `at` for one is ignored rather than silently
254
+ * approximated. Everything else is unchanged: the bus gain applies, the sound
255
+ * lands in `engine.audio.recent()`, and `muted` still silences it, none of
256
+ * which a hand-rolled `AudioContext` scheduler keeps.
257
+ */
258
+ play(at?: number): void;
201
259
  /**
202
260
  * Procedural-SFX path: synthesize the preset and fire it through WebAudio
203
261
  * (low-latency, overlap-friendly). Headless (no AudioContext) plays nothing —
@@ -222,4 +280,4 @@ declare class AudioPlayer extends Node {
222
280
  override update(_dt: number): void;
223
281
  }
224
282
  //#endregion
225
- export { ParticleSimConfig as a, ParticleSim as i, AudioPlayer as n, ParticleView as o, SpatialPose as r, AudioElementLike as t };
283
+ export { ParticleSimConfig as a, showBootFailure as c, ParticleSim as i, AudioPlayer as n, ParticleView as o, SpatialPose as r, isWebGLAvailable as s, AudioElementLike as t };
@@ -1,4 +1,4 @@
1
- import { a as Rng, c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
1
+ import { a as Rng, c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-B6ugCV1Q.js";
2
2
 
3
3
  //#region src/core/signal.d.ts
4
4
  /**
@@ -419,6 +419,12 @@ interface MusicTrack {
419
419
  play(): void;
420
420
  /** Stop and release the underlying element/source. */
421
421
  stop(): void;
422
+ /**
423
+ * Seconds into the track, or `null` when the backend cannot say.
424
+ *
425
+ * Optional so an existing custom backend keeps compiling.
426
+ */
427
+ playhead?(): number | null;
422
428
  }
423
429
  /** Backend that mints {@link MusicTrack}s — WebAudio-backed in the browser. */
424
430
  interface MusicBackend {
@@ -470,6 +476,22 @@ declare class MusicManager {
470
476
  constructor(buses: AudioBuses);
471
477
  /** Source of the logically-current track, or null when nothing is playing. */
472
478
  get current(): string | null;
479
+ /**
480
+ * Seconds into the current track — the playhead of the thing you can HEAR.
481
+ *
482
+ * `null` when nothing is playing, or when the backend has no clock (headless,
483
+ * or a custom backend that does not implement it).
484
+ *
485
+ * A game charted to a soundtrack needs this and there was no way to get it:
486
+ * `current` gave the src and the backend was private, so the only option was
487
+ * an `engine.time` counter started next to `music.play()` — a different clock
488
+ * from the one the audio is on, and one that keeps counting through a
489
+ * gesture-block, a stall or a seek. This is the audio's own answer.
490
+ *
491
+ * It is the RAW element time, so a looping track wraps to 0 at each pass:
492
+ * that is a loop boundary you can chart against, not a fault.
493
+ */
494
+ get playhead(): number | null;
473
495
  /** Per-manager volume (0..1), on top of the bus gain. */
474
496
  get volume(): number;
475
497
  /**
@@ -694,6 +716,23 @@ interface SfxPlayOptions {
694
716
  seed?: number;
695
717
  /** When set, the sound is spatialized through a PannerNode at this emitter. */
696
718
  spatial?: SpatialPlay;
719
+ /**
720
+ * Start at this point on the AUDIO clock (`engine.sfx.now` + lead), instead
721
+ * of "as soon as this call returns".
722
+ *
723
+ * Nothing in the audio API took a time, so a sound could only be fired from a
724
+ * frame — and a frame is a 16.67 ms grid at 60 Hz, 33.33 ms at 30. Measured
725
+ * on a metronome: firing from `update()` gave 0 of 64 notes a scheduled
726
+ * start; the same notes queued one frame ahead with `when` land at
727
+ * |mean| 0.000000 ms. That difference is the whole of a rhythm game, and
728
+ * anything charted to a soundtrack.
729
+ *
730
+ * A time already past plays immediately (Web Audio's own rule), so a late
731
+ * scheduler degrades instead of going silent. The bus gain and the
732
+ * `engine.audio.recent()` record apply either way — which is what a
733
+ * hand-rolled `AudioContext` scheduler loses.
734
+ */
735
+ when?: number;
697
736
  }
698
737
  /**
699
738
  * One AudioContext + a PCM cache. Instance-scoped (held by the renderer's game
@@ -741,6 +780,14 @@ declare class SfxEngine {
741
780
  /** Resume a gesture-suspended context (wired to the first user gesture). */
742
781
  unlock(): void;
743
782
  /**
783
+ * The AUDIO clock, in seconds — the one a sound is actually scheduled
784
+ * against, and the only clock that does not drift relative to what you hear.
785
+ *
786
+ * `0` headless and before the first sound, since there is no context yet.
787
+ * Schedule with a lead: `sfx.play(params, 1, { when: sfx.now + 0.08 })`.
788
+ */
789
+ get now(): number;
790
+ /**
744
791
  * Play a preset at `gain` (0..1, already bus-multiplied by the caller). Each
745
792
  * call spawns its own source so rapid repeats overlap instead of cutting off.
746
793
  * Headless → no-op.
@@ -1182,6 +1229,25 @@ interface SaveStore {
1182
1229
  remove(key: string): void;
1183
1230
  /** Wipe THIS namespace only. */
1184
1231
  clear(): void;
1232
+ /**
1233
+ * Does a write here survive the page closing?
1234
+ *
1235
+ * `false` means this store is a Map that lives as long as the tab does — a
1236
+ * private window, storage disabled, quota exhausted, or headless. Everything
1237
+ * still works IN the session, which is exactly what made it undetectable:
1238
+ * measured in Safari-private conditions, `set('highScore', 4200)` then
1239
+ * `get('highScore', 0)` returned 4200, zero warnings, and the interface had
1240
+ * no way to ask. On reload the score was 0, the slot list empty, and
1241
+ * `SavePoint` had emitted `saved(1)` for a write that went nowhere.
1242
+ *
1243
+ * Read it once and tell the player their progress will not be kept.
1244
+ *
1245
+ * Optional so a game that injects its OWN store (a cloud backend, a test
1246
+ * double) keeps compiling; absent counts as persistent, which is what a store
1247
+ * somebody wrote on purpose almost always is. Every store the engine makes
1248
+ * says so explicitly.
1249
+ */
1250
+ readonly persistent?: boolean;
1185
1251
  }
1186
1252
  declare function createSaveStore(namespace: string): SaveStore;
1187
1253
  //#endregion
@@ -1261,6 +1327,15 @@ declare function restoreBehaviors(root: Node, state: BehaviorState): RestoreRepo
1261
1327
  declare class SaveSlots {
1262
1328
  private readonly store;
1263
1329
  constructor(namespace?: string, store?: SaveStore);
1330
+ /**
1331
+ * Will these slots survive the page closing?
1332
+ *
1333
+ * `false` in a private window, with storage disabled, or headless — every
1334
+ * write still succeeds and every read still answers, for as long as the tab
1335
+ * lives. A load menu should say so instead of offering a slot that will not
1336
+ * be there tomorrow.
1337
+ */
1338
+ get persistent(): boolean;
1264
1339
  /** Slot ids that have a save, newest first. */
1265
1340
  list(): string[];
1266
1341
  /** Every slot with its contents — what a load menu actually renders. */
@@ -1331,6 +1406,8 @@ declare class Scene {
1331
1406
  readonly orderGroups: Record<string, number> | undefined;
1332
1407
  /** Physics config (gravity etc. — interpreted by the physics modules). */
1333
1408
  readonly physics: JsonObject | undefined;
1409
+ /** Declared piece of another scene — see `SceneJson.fragment`. */
1410
+ readonly fragment: boolean | undefined;
1334
1411
  /** Multiplayer config (room etc. — interpreted by incanto/net). */
1335
1412
  readonly multiplayer: JsonObject | undefined;
1336
1413
  /** Design-resolution viewport (consumed by renderers via resolveViewport). */
@@ -0,0 +1,47 @@
1
+ import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
+ //#region src/core/boot-failure.ts
3
+ /**
4
+ * Put a boot failure on the SCREEN, where the player is looking.
5
+ *
6
+ * Every shipped starter preloads its assets behind a `#loading` overlay and
7
+ * removes that overlay on the line after `await createGame*`. So anything that
8
+ * rejects — no WebGL context, a scene that will not load, a 404 the preload
9
+ * insisted on — leaves the overlay standing at "100%" forever, with the reason
10
+ * in a console the player will never open. Measured on a pristine scaffold with
11
+ * WebGL denied:
12
+ *
13
+ * ```
14
+ * control #loading gone "Talk to the Elder [E] …"
15
+ * no webgl #loading present "Emberwood 100%" … and still there 8.6 s later
16
+ * ```
17
+ *
18
+ * A bar that reaches 100% and stops reads as a hang, which is the one failure
19
+ * mode a player cannot report usefully. This turns it into a sentence.
20
+ *
21
+ * Headless it does nothing but rethrow-safe logging, so a test that boots a
22
+ * game without a document is unaffected.
23
+ */
24
+ function showBootFailure(error, opts = {}) {
25
+ const message = error instanceof IncantoError ? error.message : String(error?.message ?? error);
26
+ const code = error instanceof IncantoError ? error.code : null;
27
+ console.error(`[incanto] the game could not start${code ? ` (${code})` : ""}: ${message}`);
28
+ const doc = globalThis.document;
29
+ if (!doc?.body) return;
30
+ const host = opts.into ? doc.querySelector(opts.into) : doc.querySelector("#loading");
31
+ const panel = host ?? doc.createElement("div");
32
+ if (!host) {
33
+ panel.style.cssText = "position:fixed;inset:0;display:grid;place-content:center;gap:12px;padding:32px;background:#10131a;color:#e6e9ef;z-index:2147483647;font:14px/1.6 system-ui,sans-serif;text-align:center";
34
+ doc.body.appendChild(panel);
35
+ }
36
+ panel.textContent = "";
37
+ const title = doc.createElement("div");
38
+ title.textContent = "This game could not start";
39
+ title.style.cssText = "font-size:18px;font-weight:600;margin-bottom:8px";
40
+ const detail = doc.createElement("div");
41
+ detail.textContent = message;
42
+ detail.style.cssText = "opacity:.85;max-width:52ch;margin:0 auto";
43
+ panel.appendChild(title);
44
+ panel.appendChild(detail);
45
+ }
46
+ //#endregion
47
+ export { showBootFailure as t };