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
package/editor/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Incanto Scene Editor</title>
7
7
  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><rect width='16' height='16' rx='3' fill='%236ee7dc'/><text x='8' y='12' text-anchor='middle' font-size='11' font-family='monospace' fill='%230e1018'>i</text></svg>" />
8
- <script type="module" crossorigin src="./assets/index-CNqHojcC.js"></script>
8
+ <script type="module" crossorigin src="./assets/index-eVd0BToA.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="./assets/GameServer-C56iOUgF.js">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incanto",
3
- "version": "0.67.0",
3
+ "version": "0.68.0",
4
4
  "description": "Vibe-coding-first web game engine SDK — JSON-driven scenes on three.js",
5
5
  "keywords": [
6
6
  "game-engine",
@@ -2378,6 +2378,10 @@
2378
2378
  "type": "boolean",
2379
2379
  "default": true
2380
2380
  },
2381
+ "platformCarry": {
2382
+ "type": "boolean",
2383
+ "default": true
2384
+ },
2381
2385
  "turnSpeed": {
2382
2386
  "type": "number",
2383
2387
  "default": 100
@@ -54,6 +54,7 @@ did not until now.
54
54
  | `maxJumps` | `1` | `2` = double jump. Extra jumps work in mid-air. |
55
55
  | `airControl` | `0.2` | how much ground control you keep airborne (0 = committed, 1 = full). |
56
56
  | `fallGravity` | `2.5` | gravity multiplier while falling. Higher = snappier arc. |
57
+ | `platformCarry` | `true` | ride whatever you are standing on. Off = the controller steers in WORLD space, so standing still on a moving floor brakes you off it (measured: 95.9% of a moving deck's travel kept, 0.0% with this off). |
57
58
 
58
59
  Every one defaults to OFF (`0` / `1`) or to the previous hard-coded constant, so
59
60
  a scene that asks for nothing behaves exactly as before.
@@ -396,6 +396,55 @@ changed under it. `engine.music` deliberately survives a scene swap (it belongs
396
396
  to the engine, not the scene): call `engine.music.stop(1)` or `crossfadeTo` when
397
397
  the music should change.
398
398
 
399
+ ## Sounds on a GRID: rhythm games and anything charted to a soundtrack
400
+
401
+ A sound fired from `update()` cannot land closer than one frame to where a chart
402
+ wants it — 16.67 ms at 60 Hz, 33.33 ms at 30, and that is the whole difference
403
+ between a rhythm game that feels tight and one that does not. The engine's frame
404
+ clock is exact (3600 steps land on `engine.time` 60.000000000 s and beat error
405
+ never compounds); the last 16 ms is the gap.
406
+
407
+ Schedule on the AUDIO clock instead. Queue a short LOOKAHEAD ahead of now, every
408
+ frame, and let Web Audio place the sound:
409
+
410
+ ```ts
411
+ const LEAD = 0.08; // schedule this far ahead of the speaker
412
+ let cursor = 0; // next un-queued note
413
+
414
+ engine.updated.connect(() => {
415
+ const horizon = engine.sfx.now + LEAD;
416
+ while (cursor < chart.length && startedAt + chart[cursor].atSec <= horizon) {
417
+ hit.play(startedAt + chart[cursor].atSec); // AudioPlayer.play(at)
418
+ cursor++;
419
+ }
420
+ });
421
+ ```
422
+
423
+ - **`engine.sfx.now`** — seconds on the audio clock, the one the sound is
424
+ actually placed on. `0` headless and before the first sound.
425
+ - **`AudioPlayer.play(at)`** and **`engine.sfx.play(params, gain, { when })`** —
426
+ PRESETS only. A `src` clip goes through an `<audio>` element, which has no
427
+ scheduling clock; `at` is ignored for one rather than approximated.
428
+ - A time already past plays immediately (Web Audio's own rule), so a scheduler
429
+ that ran late is late, not silent.
430
+ - Everything else still applies: the bus gain, `engine.audio.recent()`, and
431
+ `muted`. A scheduler hand-rolled on a raw `AudioContext` hits the same
432
+ accuracy and loses all three.
433
+
434
+ Measured: 0 of 64 notes were given a scheduled start on the frame path; the same
435
+ 64 queued this way land at **|mean| 0.000000 ms**.
436
+
437
+ **Charting against a music FILE** needs the playhead of the thing you can hear,
438
+ not a counter you started next to `music.play()` (a different clock, which keeps
439
+ counting through a gesture-block, a stall or a seek):
440
+
441
+ ```ts
442
+ const songSeconds = engine.music.playhead; // null = nothing playing, or no clock
443
+ ```
444
+
445
+ It is the raw element time, so a looping track wraps to 0 each pass — a loop
446
+ boundary to chart against, not a fault.
447
+
399
448
  ## Decision guide
400
449
 
401
450
  - **Need a quick game sound (coin/jump/hit/explosion/…)** → set `preset`. Done.
@@ -485,3 +485,40 @@ which is why a wall jump was not awkward to write but impossible.
485
485
  headless bot sessions and reports what could not be reached, which wires never
486
486
  fired, and whether anything in the level can hurt the player. See
487
487
  `incanto-playtesting.md`.
488
+
489
+ ## When the boot FAILS: say so, or the bar sits at 100% forever
490
+
491
+ `createGame2D`/`createGame2D` reject like any promise, and the loading overlay
492
+ every starter shows is removed on the line *after* the await. So a boot that
493
+ fails leaves the bar at 100% with no text, and the reason in a console the
494
+ player will never open. Measured with WebGL denied on a pristine scaffold: the
495
+ overlay was still reading `Emberwood 100%` 8.6 seconds later.
496
+
497
+ One `.catch` fixes it, and every shipped template now carries it:
498
+
499
+ ```ts
500
+ import { createGame2D, showBootFailure } from 'incanto/2d';
501
+
502
+ const game = await createGame2D({ canvas, scene: sceneJson }).catch((e) => {
503
+ showBootFailure(e); // paints the reason into #loading (or its own panel)
504
+ throw e;
505
+ });
506
+
507
+ document.querySelector('#loading')?.remove();
508
+ ```
509
+
510
+ **No WebGL is the case worth naming** — the 2D renderer is WebGL too. A machine
511
+ with acceleration off, a blocklisted driver, a locked-down or headless browser, or a page that already
512
+ holds too many contexts cannot make one, and three throws
513
+ `Error('Error creating WebGL context.')` with nothing to branch on. The engine
514
+ rethrows it as an `IncantoError` with code **`WEBGL_UNAVAILABLE`** and a message
515
+ naming the likely causes, so you can show a real screen:
516
+
517
+ ```ts
518
+ import { isWebGLAvailable } from 'incanto/2d';
519
+
520
+ if (!isWebGLAvailable()) showMyOwnBrowserTooOldScreen(); // ask BEFORE booting
521
+ ```
522
+
523
+ `isWebGLAvailable()` probes a throwaway canvas and never touches yours; it
524
+ returns `false` outside a browser.
@@ -648,3 +648,40 @@ It stubs unregistered behaviors (it measures geometry, not logic; a scene with
648
648
  any custom behavior used to read as clean — measured 0 floating before
649
649
  `registerBehavior`, 26 after) and throws when it cannot measure. A level built
650
650
  from mesh floors has no terrain height to read: use a physics raycast instead.
651
+
652
+ ## When the boot FAILS: say so, or the bar sits at 100% forever
653
+
654
+ `createGame2D`/`createGame3D` reject like any promise, and the loading overlay
655
+ every starter shows is removed on the line *after* the await. So a boot that
656
+ fails leaves the bar at 100% with no text, and the reason in a console the
657
+ player will never open. Measured with WebGL denied on a pristine scaffold: the
658
+ overlay was still reading `Emberwood 100%` 8.6 seconds later.
659
+
660
+ One `.catch` fixes it, and every shipped template now carries it:
661
+
662
+ ```ts
663
+ import { createGame3D, showBootFailure } from 'incanto/3d';
664
+
665
+ const game = await createGame3D({ canvas, scene: sceneJson }).catch((e) => {
666
+ showBootFailure(e); // paints the reason into #loading (or its own panel)
667
+ throw e;
668
+ });
669
+
670
+ document.querySelector('#loading')?.remove();
671
+ ```
672
+
673
+ **No WebGL is the case worth naming.** A machine with acceleration off, a
674
+ blocklisted driver, a locked-down or headless browser, or a page that already
675
+ holds too many contexts cannot make one, and three throws
676
+ `Error('Error creating WebGL context.')` with nothing to branch on. The engine
677
+ rethrows it as an `IncantoError` with code **`WEBGL_UNAVAILABLE`** and a message
678
+ naming the likely causes, so you can show a real screen:
679
+
680
+ ```ts
681
+ import { isWebGLAvailable } from 'incanto/3d';
682
+
683
+ if (!isWebGLAvailable()) showMyOwnBrowserTooOldScreen(); // ask BEFORE booting
684
+ ```
685
+
686
+ `isWebGLAvailable()` probes a throwaway canvas and never touches yours; it
687
+ returns `false` outside a browser.
@@ -1211,7 +1211,7 @@ spawners, music, or monster aggression to it. The `hour` field (0-24) is
1211
1211
  writable: jump to a time of day in one assignment. For one-off look changes
1212
1212
  use `setEnvironment3D(engine, patch)` from `incanto/3d` directly.
1213
1213
 
1214
- ## Boot loading overlay## Boot loading overlay
1214
+ ## Boot loading overlay
1215
1215
 
1216
1216
  ```ts
1217
1217
  import { preloadSceneAssets } from 'incanto';
@@ -1221,6 +1221,33 @@ await preloadSceneAssets(sceneJson.assets); // black screen + bar + %, auto-re
1221
1221
  Replaces the ~125-line hand-rolled loading screen; `createLoadingOverlay()`
1222
1222
  gives you the raw overlay when you preload something else.
1223
1223
 
1224
+ ## Where a node actually IS (`position` is parent-relative)
1225
+
1226
+ `node.position` is the offset from its PARENT, not the world. A scene nests
1227
+ freely — `Terrain/Clouds/Cloud1/Puff1` four deep is what the engine's own
1228
+ generator writes — so a distance check reading `position` on a nested node
1229
+ measures from the wrong origin and says nothing about it. Measured on a crate
1230
+ parented to a rock: `position` is `[0, 3.02, 0]` and the world point is
1231
+ `[13, 14.31, 80.18]` — **82 m apart**.
1232
+
1233
+ ```ts
1234
+ import { localFromWorld, worldPosition } from 'incanto/gameplay';
1235
+
1236
+ const here = worldPosition(crate); // [13, 14.31, 80.18]
1237
+ crate.position = localFromWorld(crate, [0, 20, 0]); // put it at world [0,20,0]
1238
+ ```
1239
+
1240
+ Both work headless and in both dimensions — arity comes from the node (2 for a
1241
+ 2D node, 3 for a 3D one), and an ancestor with no `position` contributes
1242
+ nothing. Do NOT reach for `_ensureObject3D().matrixWorld`: it is internal, and
1243
+ in a headless verify script there is no renderer to parent the object, so it
1244
+ hands back the LOCAL position with no warning.
1245
+
1246
+ **Translation only.** They sum ancestor positions and ignore ancestor rotation
1247
+ and scale — the same contract physics already uses (`worldPosition2D` /
1248
+ `worldPosition3D`) and that `applyGroundSnap` assumes. A node under a ROTATING
1249
+ parent needs the renderer's matrix, not this.
1250
+
1224
1251
  ## Persistence (high scores, unlocks, settings)
1225
1252
 
1226
1253
  ```ts
@@ -297,6 +297,7 @@ Signals: `movementStateChanged(state)`
297
297
  | `pitchMin` | `-1.3` | number |
298
298
  | `pitchMax` | `1.5` | number |
299
299
  | `cameraCollision` | `true` | boolean |
300
+ | `platformCarry` | `true` | boolean |
300
301
  | `turnSpeed` | `100` | number |
301
302
  | `camLerp` | `25` | number |
302
303
  | `moveAction` | `"move"` | string |
@@ -479,6 +479,29 @@ rotating discs. It is what makes a floating-island game work.
479
479
  Author it the obvious way: move the platform's `position` (a behavior, a
480
480
  `PathFollow`, an `Oscillate`). Nothing else to declare.
481
481
 
482
+ **The 3D PLAYER is a different rig, and it needs one prop.** A
483
+ `CharacterController3D` must sit under a dynamic `RigidBody3D` (hard error
484
+ otherwise), not a `CharacterBody3D` — the character bodies in a 3D game are its
485
+ enemies and NPCs. The controller steers toward a target velocity every fixed
486
+ step, so pressing nothing means "target zero", which in world space is a brake
487
+ aimed at the platform's own motion. `platformCarry` (default **true**) makes
488
+ that target relative to the floor instead. Measured on a 400 m deck moving
489
+ 4 m/s, rider pressing nothing: **95.9%** of the travel kept, **0.0%** with
490
+ `platformCarry: false`.
491
+
492
+ ```json
493
+ { "name": "Ctl", "type": "CharacterController3D",
494
+ "props": { "view": "free", "platformCarry": false } }
495
+ ```
496
+
497
+ Turn it off for a conveyor you want to be scenery. Vertical carry needs no prop
498
+ either way — the hover spring rides whatever surface is under it, which is why
499
+ a 3D elevator always looked right while a conveyor did not.
500
+
501
+ The carry comes from the platform's POSITION DELTA, not its `linearVelocity`,
502
+ because writing `position` calls `setTranslation` — a teleport that transfers no
503
+ momentum to anything. Both authoring styles work.
504
+
482
505
  The carry stops the moment the character is no longer grounded on it, so walking
483
506
  off an edge or jumping is not "sticky", and it is vertical as well as horizontal
484
507
  — an elevator lifts you.
@@ -26,6 +26,7 @@ wiring, and composition are all in the file. TS is for *behavior only* (attached
26
26
  "constants": { "UI": 1000, "Background": -100 },
27
27
  "input": { "<action>": { "...": "...", "touch": "joystick|button (optional — mobile on-screen controls)" } },
28
28
  "multiplayer": { "room": "auto" },
29
+ "fragment": false,
29
30
  "root": { "name": "Level1", "type": "Node", "children": [] },
30
31
  "connections": []
31
32
  }
@@ -33,6 +34,13 @@ wiring, and composition are all in the file. TS is for *behavior only* (attached
33
34
 
34
35
  - `format` MUST be `1`. `type` MUST be `"scene"`. `name` non-empty.
35
36
  - `dimension` is optional: `"2d"` or `"3d"` only.
37
+ - `fragment` (optional, default `false`) says this file is a PIECE of another
38
+ scene — a spawner's prefab, a sub-scene something instances — and is never
39
+ rendered on its own. It changes nothing at runtime; it tells the checker to
40
+ stop asking a prefab where its camera and its sun are. Set it on any scene
41
+ you load from TypeScript and hand to a spawner. (A scene another scene
42
+ embeds with `instance` needs no key — `incanto-check` walks the project and
43
+ can see that for itself.)
36
44
  - `viewport` (optional) makes scene JSON own responsive layout: author the world
37
45
  in fixed `design` pixels (`[width, height]`, positive numbers) and the renderer
38
46
  maps them onto any canvas size. `fit`: `"expand"` (design rect always fully
@@ -158,7 +166,10 @@ level 1's layers.
158
166
  ## Parents and children
159
167
 
160
168
  EVERY node type can hold children — `children` is universal, so any node works
161
- as a grouping container. The inverse is not true: some types demand a SPECIFIC
169
+ as a grouping container. **A child's `position` is relative to its parent**, so
170
+ nesting is also how you move a group: shift the parent and everything under it
171
+ follows. From code, `worldPosition(node)` (`incanto/gameplay`) is where a nested
172
+ node actually is; `node.position` is the local offset. The inverse is not true: some types demand a SPECIFIC
162
173
  parent and are therefore also invalid as the root. Today's rule:
163
174
 
164
175
  - `CharacterController2D` must be a direct child of a `CharacterBody2D` —
@@ -14,7 +14,7 @@
14
14
  "@dimforge/rapier2d-compat": "0.19.3",
15
15
  "@dimforge/rapier3d-compat": "0.19.3",
16
16
  "@pixiv/three-vrm": "^3.5.3",
17
- "incanto": "^0.67.0",
17
+ "incanto": "^0.68.0",
18
18
  "three": "^0.184.0"
19
19
  },
20
20
  "devDependencies": {
@@ -6,7 +6,7 @@
6
6
  * world with `bun run world`, the game logic never changes.
7
7
  */
8
8
  import { assetUrls, preloadUrls } from 'incanto';
9
- import { createGame3D } from 'incanto/3d';
9
+ import { createGame3D, showBootFailure } from 'incanto/3d';
10
10
  import { BEHAVIORS } from './behaviors';
11
11
  import gameJson from './game.scene.json';
12
12
 
@@ -25,6 +25,13 @@ const game = await createGame3D({
25
25
  scene: gameJson,
26
26
  behaviors: BEHAVIORS,
27
27
  pointer: true,
28
+ }).catch((e) => {
29
+ // Without this the player watches the loading overlay sit at 100% forever:
30
+ // `#loading` is removed on the line below, so anything that rejects here (no
31
+ // WebGL context, a scene that will not load) leaves the bar up and the reason
32
+ // in a console nobody opens.
33
+ showBootFailure(e);
34
+ throw e;
28
35
  });
29
36
 
30
37
  document.querySelector('#loading')?.remove();
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
- "incanto": "^0.67.0",
14
+ "incanto": "^0.68.0",
15
15
  "three": "^0.184.0"
16
16
  },
17
17
  "devDependencies": {
@@ -14,7 +14,7 @@
14
14
  * height/stomp/knockback/checkpoint-respawn/platform-carry), GoblinSkin,
15
15
  * FollowCam (follow + screen-shake), ParallaxLayer, HudUpdater.
16
16
  */
17
- import { createGame2D } from 'incanto/2d';
17
+ import { createGame2D, showBootFailure } from 'incanto/2d';
18
18
  import goblinUrl from 'incanto/assets/characters/goblin.png';
19
19
  // Built-in sprite art ships in the package — import the URLs and inject them into
20
20
  // the scene's asset placeholders before boot (the engine clones the scene).
@@ -49,6 +49,13 @@ const game = await createGame2D({
49
49
  canvas,
50
50
  scene,
51
51
  behaviors,
52
+ }).catch((e) => {
53
+ // Without this the player watches the loading overlay sit at 100% forever:
54
+ // `#loading` is removed on the line below, so anything that rejects here (no
55
+ // WebGL context, a scene that will not load) leaves the bar up and the reason
56
+ // in a console nobody opens.
57
+ showBootFailure(e);
58
+ throw e;
52
59
  });
53
60
 
54
61
  document.querySelector('#loading')?.remove();
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
- "incanto": "^0.67.0",
14
+ "incanto": "^0.68.0",
15
15
  "three": "^0.184.0"
16
16
  },
17
17
  "devDependencies": {
@@ -11,7 +11,7 @@
11
11
  * leaves open: `Survivor` (move + auto-attack + XP/level-up/upgrades + HUD),
12
12
  * `Director` (ring-spawn around the player + escalation), `EnemyDrop`, `EnemyFace`.
13
13
  */
14
- import { createGame2D } from 'incanto/2d';
14
+ import { createGame2D, showBootFailure } from 'incanto/2d';
15
15
  import ghostUrl from 'incanto/assets/characters/ghost.png';
16
16
  import goblinUrl from 'incanto/assets/characters/goblin.png';
17
17
  import knightUrl from 'incanto/assets/characters/medieval-knight.png';
@@ -36,6 +36,13 @@ const game = await createGame2D({
36
36
  canvas,
37
37
  scene,
38
38
  behaviors,
39
+ }).catch((e) => {
40
+ // Without this the player watches the loading overlay sit at 100% forever:
41
+ // `#loading` is removed on the line below, so anything that rejects here (no
42
+ // WebGL context, a scene that will not load) leaves the bar up and the reason
43
+ // in a console nobody opens.
44
+ showBootFailure(e);
45
+ throw e;
39
46
  });
40
47
 
41
48
  document.querySelector('#loading')?.remove();
@@ -13,7 +13,7 @@
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
14
  "@dimforge/rapier3d-compat": "0.19.3",
15
15
  "@pixiv/three-vrm": "^3.5.3",
16
- "incanto": "^0.67.0",
16
+ "incanto": "^0.68.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {
@@ -13,7 +13,7 @@
13
13
  * win/lose banner).
14
14
  */
15
15
  import { assetUrls, preloadUrls } from 'incanto';
16
- import { createGame3D } from 'incanto/3d';
16
+ import { createGame3D, showBootFailure } from 'incanto/3d';
17
17
  // A bundled clip looped as low ambience (no large music files ship in the
18
18
  // package — see incanto-audio.md). vite resolves + hashes the real file.
19
19
  import musicUrl from 'incanto/assets/audio/spells_cast.mp3';
@@ -41,6 +41,13 @@ const game = await createGame3D({
41
41
  scene: sceneJson,
42
42
  behaviors: { Shoot, HudUpdater },
43
43
  pointer: true, // pointer-lock mouse look (lockOnClick) — drives the free-view yaw/pitch
44
+ }).catch((e) => {
45
+ // Without this the player watches the loading overlay sit at 100% forever:
46
+ // `#loading` is removed on the line below, so anything that rejects here (no
47
+ // WebGL context, a scene that will not load) leaves the bar up and the reason
48
+ // in a console nobody opens.
49
+ showBootFailure(e);
50
+ throw e;
44
51
  });
45
52
 
46
53
  document.querySelector('#loading')?.remove();
@@ -13,7 +13,7 @@
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
14
  "@dimforge/rapier3d-compat": "0.19.3",
15
15
  "@pixiv/three-vrm": "^3.5.3",
16
- "incanto": "^0.67.0",
16
+ "incanto": "^0.68.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {
@@ -8,7 +8,7 @@
8
8
  * declarative `animations` map — zero animation code.
9
9
  */
10
10
  import { assetUrls, preloadUrls } from 'incanto';
11
- import { createGame3D } from 'incanto/3d';
11
+ import { createGame3D, showBootFailure } from 'incanto/3d';
12
12
  import { BEHAVIORS } from './behaviors';
13
13
  import villageJson from './village.scene.json';
14
14
 
@@ -27,6 +27,13 @@ const game = await createGame3D({
27
27
  scene: villageJson,
28
28
  behaviors: BEHAVIORS,
29
29
  pointer: true, // pointer-lock look (click the canvas)
30
+ }).catch((e) => {
31
+ // Without this the player watches the loading overlay sit at 100% forever:
32
+ // `#loading` is removed on the line below, so anything that rejects here (no
33
+ // WebGL context, a scene that will not load) leaves the bar up and the reason
34
+ // in a console nobody opens.
35
+ showBootFailure(e);
36
+ throw e;
30
37
  });
31
38
 
32
39
  document.querySelector('#loading')?.remove();