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
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-Dd7H4bJV.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-D0CxCveZ.js";
4
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-Cji5A6sX.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 } from "./behavior-BXNLfIJk.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-Cga7FVP4.js";
6
- import { i as ParticleSim, o as ParticleView, r as SpatialPose } from "./audio-player-_UAcHxnC.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";
@@ -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
  /**
@@ -1336,6 +1404,30 @@ declare class CharacterController3D extends Node3D {
1336
1404
  pitchMax: number;
1337
1405
  /** Spring-arm: pull the orbit camera in front of walls/ground instead of clipping. */
1338
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;
1339
1431
  turnSpeed: number;
1340
1432
  /** Camera smoothing rate (1-exp(-rate·dt)); side view uses 5. */
1341
1433
  camLerp: number;
@@ -1792,6 +1884,10 @@ declare class MeshInstance3D extends Node3D {
1792
1884
  /** Loader hook: malformed materials fail at LOAD time, not at render. */
1793
1885
  static validateJson(node: Node): void;
1794
1886
  private geometryKey;
1887
+ private materialKey;
1888
+ /** True while this node is using a cache entry it must not dispose. */
1889
+ private sharedGeometry;
1890
+ private sharedMaterial;
1795
1891
  private textureKey;
1796
1892
  private loadedTextures;
1797
1893
  protected override _createObject3D(): Object3D;
@@ -4034,4 +4130,4 @@ declare function createNavDebugNode(nav: TerrainNav, opts?: {
4034
4130
  name?: string;
4035
4131
  }): InstancedMesh3D;
4036
4132
  //#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 };
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-lQDCwNag.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-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";
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-BkHjwJgI.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, 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-BXNLfIJk.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
  /**
@@ -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
@@ -402,6 +419,12 @@ interface MusicTrack {
402
419
  play(): void;
403
420
  /** Stop and release the underlying element/source. */
404
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;
405
428
  }
406
429
  /** Backend that mints {@link MusicTrack}s — WebAudio-backed in the browser. */
407
430
  interface MusicBackend {
@@ -453,6 +476,22 @@ declare class MusicManager {
453
476
  constructor(buses: AudioBuses);
454
477
  /** Source of the logically-current track, or null when nothing is playing. */
455
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;
456
495
  /** Per-manager volume (0..1), on top of the bus gain. */
457
496
  get volume(): number;
458
497
  /**
@@ -677,6 +716,23 @@ interface SfxPlayOptions {
677
716
  seed?: number;
678
717
  /** When set, the sound is spatialized through a PannerNode at this emitter. */
679
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;
680
736
  }
681
737
  /**
682
738
  * One AudioContext + a PCM cache. Instance-scoped (held by the renderer's game
@@ -724,6 +780,14 @@ declare class SfxEngine {
724
780
  /** Resume a gesture-suspended context (wired to the first user gesture). */
725
781
  unlock(): void;
726
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
+ /**
727
791
  * Play a preset at `gain` (0..1, already bus-multiplied by the caller). Each
728
792
  * call spawns its own source so rapid repeats overlap instead of cutting off.
729
793
  * Headless → no-op.
@@ -1165,6 +1229,25 @@ interface SaveStore {
1165
1229
  remove(key: string): void;
1166
1230
  /** Wipe THIS namespace only. */
1167
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;
1168
1251
  }
1169
1252
  declare function createSaveStore(namespace: string): SaveStore;
1170
1253
  //#endregion
@@ -1220,7 +1303,8 @@ interface RestoreReport {
1220
1303
  declare function captureBehaviors(root: Node, source?: JsonValue): {
1221
1304
  state: BehaviorState; /** Paths of nodes that had state to save and no uid to save it under. */
1222
1305
  unaddressable: string[]; /** Behaviors in this tree that have something to save — addressable or not. */
1223
- saveable: number;
1306
+ saveable: number; /** Authored nodes this run consumed that the `#freed` ledger cannot record. */
1307
+ unrecordable: string[];
1224
1308
  };
1225
1309
  /**
1226
1310
  * Hand each behavior its state back.
@@ -1243,6 +1327,15 @@ declare function restoreBehaviors(root: Node, state: BehaviorState): RestoreRepo
1243
1327
  declare class SaveSlots {
1244
1328
  private readonly store;
1245
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;
1246
1339
  /** Slot ids that have a save, newest first. */
1247
1340
  list(): string[];
1248
1341
  /** Every slot with its contents — what a load menu actually renders. */
@@ -1313,6 +1406,8 @@ declare class Scene {
1313
1406
  readonly orderGroups: Record<string, number> | undefined;
1314
1407
  /** Physics config (gravity etc. — interpreted by the physics modules). */
1315
1408
  readonly physics: JsonObject | undefined;
1409
+ /** Declared piece of another scene — see `SceneJson.fragment`. */
1410
+ readonly fragment: boolean | undefined;
1316
1411
  /** Multiplayer config (room etc. — interpreted by incanto/net). */
1317
1412
  readonly multiplayer: JsonObject | undefined;
1318
1413
  /** Design-resolution viewport (consumed by renderers via resolveViewport). */
@@ -1600,6 +1695,26 @@ interface EngineOptions {
1600
1695
  seed?: number;
1601
1696
  /** Frame cap in fps; 0 (default) runs at whatever the display offers. */
1602
1697
  maxFps?: number;
1698
+ /**
1699
+ * Which SETTINGS this game owns — its own, or the origin's.
1700
+ *
1701
+ * Persisted settings are keyed `incanto:<namespace>:<key>` in the browser's
1702
+ * storage, and this defaulted to `settings` with no way to change it. So two
1703
+ * Incanto games served from one domain shared one volume slider, one language
1704
+ * and one quality tier. Measured: game A's player sets
1705
+ * `renderScale 0.25, muted, quality low`; game B — whose source never mentions
1706
+ * settings — boots at a 480×232 drawing buffer, silent.
1707
+ *
1708
+ * `SaveSlots(namespace)` and `createSaveStore(namespace)` have always taken
1709
+ * one and document it as "keeps two games on one origin apart". This is the
1710
+ * third sibling finally saying the same thing.
1711
+ *
1712
+ * Pass a `Settings` instance instead when a game wants to share deliberately
1713
+ * (a launcher and its titles), or drive them from its own store.
1714
+ */
1715
+ settings?: Settings | {
1716
+ namespace: string;
1717
+ };
1603
1718
  }
1604
1719
  /**
1605
1720
  * The game loop: drives a Scene's tree with fixed-timestep `fixedUpdate`
@@ -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 };