incanto 0.67.0 → 0.69.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 (87) hide show
  1. package/assets/catalog.json +9 -5
  2. package/bin/_pick-server.mjs +48 -0
  3. package/bin/incanto-assets.mjs +14 -1
  4. package/bin/incanto-check.mjs +69 -5
  5. package/bin/incanto-frame.mjs +10 -25
  6. package/bin/incanto-logs.mjs +8 -18
  7. package/bin/incanto-new.mjs +29 -1
  8. package/bin/incanto-serve.mjs +160 -0
  9. package/bin/incanto-verify.mjs +40 -0
  10. package/bin/incanto.mjs +1 -0
  11. package/dist/2d.d.ts +10 -8
  12. package/dist/2d.js +6 -4
  13. package/dist/3d.d.ts +31 -7
  14. package/dist/3d.js +9 -7
  15. package/dist/{audio-player-BNUHo0JF.d.ts → audio-player-C0j2k-yB.d.ts} +80 -3
  16. package/dist/{behavior-ZnfzKDo3.d.ts → behavior-rm8vVzu_.d.ts} +145 -1
  17. package/dist/boot-failure-CKYrEcGF.js +47 -0
  18. package/dist/{create-game-FAXdHta3.js → create-game-CvJ1wSRP.js} +27 -27
  19. package/dist/{create-game-DMetd3vR.js → create-game-CwxEDlCm.js} +15 -11
  20. package/dist/debug.d.ts +1 -1
  21. package/dist/{duplicate-B8QnX2-r.js → duplicate-Dj4L-5AA.js} +1 -1
  22. package/dist/{editor-switch-DAvWQeld.d.ts → editor-switch-CnIOiyNJ.d.ts} +1 -1
  23. package/dist/editor.js +12 -6
  24. package/dist/env.d.ts +1 -1
  25. package/dist/{environment-presets-C-5g2pMh.js → environment-presets-BO-7XoFV.js} +58 -9
  26. package/dist/{frame-report-DCnHFmto.d.ts → frame-report-DNxDAb1w.d.ts} +8 -0
  27. package/dist/{frame-report-BSMny7oe.js → frame-report-Dlq13Gyj.js} +1 -0
  28. package/dist/{gameplay-CGSv-9XR.js → gameplay-D1KixWK8.js} +175 -151
  29. package/dist/gameplay.d.ts +51 -3
  30. package/dist/gameplay.js +2 -2
  31. package/dist/index.d.ts +6 -6
  32. package/dist/index.js +8 -7
  33. package/dist/{loader-KZESOZmE.js → loader-DZE7B07H.js} +48 -7
  34. package/dist/{loader-DyT30ArB.d.ts → loader-DadV2QCT.d.ts} +2 -2
  35. package/dist/net.d.ts +2 -2
  36. package/dist/net.js +1 -1
  37. package/dist/{physics-2d-_B6zj3zU.js → physics-2d-CF5yyjbj.js} +2 -2
  38. package/dist/{physics-3d-i1IeAY0F.js → physics-3d-DbYaEcZy.js} +3 -3
  39. package/dist/react.d.ts +2 -2
  40. package/dist/react.js +1 -1
  41. package/dist/{register-vIxU3Xbv.js → register-DbVArsns.js} +19 -10
  42. package/dist/{register-Cs53CY7s.js → register-mmfugpaQ.js} +159 -6
  43. package/dist/{replay-BkpDV51O.js → replay-BEHLDcQV.js} +28 -5
  44. package/dist/{replay-D0GcEHYU.d.ts → replay-BQfkX4x7.d.ts} +38 -4
  45. package/dist/{schema-CFeioQRE.d.ts → schema-B6ugCV1Q.d.ts} +11 -0
  46. package/dist/{sheet-grid-BT6N_Bjs.js → sheet-grid-Cea343VO.js} +6 -2
  47. package/dist/{split-screen-DS8RaUV_.js → split-screen-CauMwu6y.js} +2 -2
  48. package/dist/{split-screen-DD5eYIgW.d.ts → split-screen-Cg76MCV5.d.ts} +3 -3
  49. package/dist/{src-BLlsGJW9.js → src-CYEu-_S9.js} +1 -1
  50. package/dist/{teardown-LB9cTnXJ.js → teardown-CMqFVxO9.js} +5 -4
  51. package/dist/{test-81uZ967a.js → test-D6ISmuFE.js} +73 -25
  52. package/dist/test.d.ts +17 -6
  53. package/dist/test.js +3 -3
  54. package/dist/vite.d.ts +70 -2
  55. package/dist/vite.js +176 -14
  56. package/dist/{rendering-options-ByAdi8PF.js → webgl-unavailable-Z5_g8x9H.js} +57 -2
  57. package/editor/assets/{agent8-C2jtNKbx.js → agent8-BDvaZMQJ.js} +1 -1
  58. package/editor/assets/{debug-DoU3KeqS.js → debug-CMULinBW.js} +1 -1
  59. package/editor/assets/{index-CNqHojcC.js → index-CZIRr5EL.js} +92 -92
  60. package/editor/index.html +1 -1
  61. package/package.json +4 -3
  62. package/schemas/scene.schema.json +4 -0
  63. package/skills/incanto-3d-character.md +1 -0
  64. package/skills/incanto-assets.md +16 -2
  65. package/skills/incanto-audio.md +65 -0
  66. package/skills/incanto-building-2d-games.md +66 -0
  67. package/skills/incanto-building-3d-games.md +97 -2
  68. package/skills/incanto-gameplay-behaviors.md +46 -2
  69. package/skills/incanto-node-reference.md +1 -0
  70. package/skills/incanto-physics-and-input.md +23 -0
  71. package/skills/incanto-save-slots.md +27 -0
  72. package/skills/incanto-scene-json-authoring.md +12 -1
  73. package/skills/incanto-verifying-your-game.md +96 -2
  74. package/skills/incanto-your-first-game.md +2 -1
  75. package/templates-app/beacon-isle-3d/package.json +1 -1
  76. package/templates-app/beacon-isle-3d/src/main.ts +21 -4
  77. package/templates-app/platformer-2d/index.html +0 -23
  78. package/templates-app/platformer-2d/package.json +1 -1
  79. package/templates-app/platformer-2d/src/behaviors.ts +26 -16
  80. package/templates-app/platformer-2d/src/game.scene.json +17 -27
  81. package/templates-app/platformer-2d/src/main.ts +32 -13
  82. package/templates-app/star-survivor/package.json +1 -1
  83. package/templates-app/star-survivor/src/main.ts +25 -7
  84. package/templates-app/tps-3d/package.json +1 -1
  85. package/templates-app/tps-3d/src/main.ts +34 -16
  86. package/templates-app/village-quest-3d/package.json +1 -1
  87. package/templates-app/village-quest-3d/src/main.ts +21 -4
@@ -242,7 +242,8 @@ Things that cost real time, in the order you will meet them.
242
242
  | a 3D character that feels right | `incanto-3d-character.md` |
243
243
  | terrain, water, trees, sky | `incanto-environment.md` |
244
244
  | sound | `incanto-audio.md` |
245
- | shake, flash, hit-stop, particles | `incanto-game-feel.md` |
245
+ | shake, flash, hit-stop, particles | `incanto-gameplay-behaviors.md` (`CameraShake`, `screenFlash`, `hitStop`, `Particles2D/3D`) |
246
+ | is the feel RIGHT? measure it | `incanto-game-feel.md` |
246
247
  | HUD, menus, inventory | `incanto-hud.md` |
247
248
  | proving it works | `incanto-verifying-your-game.md` |
248
249
 
@@ -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.69.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,9 +25,26 @@ 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
- document.querySelector('#loading')?.remove();
31
-
32
- // console-poking handle for playtesting
37
+ // The console handle FIRST, before any wiring of your own can throw.
38
+ // `game.stats()`, `game.assetErrors()`, `game.frame()` and `game.engine.log`
39
+ // are the entire in-page diagnostic surface, and a static build has no other.
40
+ // Assigned after the wiring, one mistake below took all of them with it.
33
41
  (window as unknown as { game: typeof game }).game = game;
42
+
43
+ // The overlay comes down LAST, after any wiring of your own. There is none in
44
+ // this template — when you add some, put it above this line and wrap it:
45
+ //
46
+ // try { …your wiring… } catch (e) { showBootFailure(e); throw e; }
47
+ //
48
+ // A throw between the overlay coming down and the handle going up used to leave
49
+ // a level that renders perfectly with no player and no error of any kind.
50
+ document.querySelector('#loading')?.remove();
@@ -49,28 +49,6 @@
49
49
  transform: rotate(360deg);
50
50
  }
51
51
  }
52
- .round-btn {
53
- position: fixed;
54
- bottom: 2rem;
55
- right: 2rem;
56
- z-index: 1001;
57
- width: 88px;
58
- height: 88px;
59
- border: none;
60
- border-radius: 9999px;
61
- background: rgba(255, 209, 102, 0.45);
62
- color: #1d2a44;
63
- font-weight: bold;
64
- font-size: 0.9rem;
65
- cursor: pointer;
66
- user-select: none;
67
- -webkit-user-select: none;
68
- touch-action: none;
69
- transition: transform 0.1s;
70
- }
71
- .round-btn:active {
72
- transform: scale(0.9);
73
- }
74
52
  </style>
75
53
  </head>
76
54
  <body>
@@ -79,7 +57,6 @@
79
57
  <div class="spinner"></div>
80
58
  </div>
81
59
  <canvas id="game"></canvas>
82
- <button type="button" id="btn-jump" class="round-btn">JUMP</button>
83
60
  <script>
84
61
  // agent8 embed harness: report the game size to a hosting iframe parent.
85
62
  function postGameSize() {
@@ -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.69.0",
15
15
  "three": "^0.184.0"
16
16
  },
17
17
  "devDependencies": {
@@ -19,9 +19,9 @@
19
19
  * HudUpdater — paint ScoreKeeper/hearts/lives into the HUD + win/lose banner.
20
20
  */
21
21
 
22
- import type { Node } from 'incanto';
22
+ import type { Node, UiBanner, UiText } from 'incanto';
23
23
  import { Behavior } from 'incanto';
24
- import type { AnimatedSprite2D, Camera2D, CharacterBody2D, Label, Node2D } from 'incanto/2d';
24
+ import type { AnimatedSprite2D, Camera2D, CharacterBody2D, Node2D } from 'incanto/2d';
25
25
  import { Particles2D } from 'incanto/2d';
26
26
 
27
27
  // ---- tuning (px, y-down; scene gravity is [0, 1800]) --------------------------
@@ -499,17 +499,27 @@ interface HeartState {
499
499
  hearts: number;
500
500
  }
501
501
  export class HudUpdater extends Behavior {
502
- private get coinLabel(): Label {
503
- return this.getNode('CoinLabel') as unknown as Label;
504
- }
505
- private get heartLabel(): Label {
506
- return this.getNode('HeartLabel') as unknown as Label;
507
- }
508
- private get livesLabel(): Label {
509
- return this.getNode('LivesLabel') as unknown as Label;
510
- }
511
- private get banner(): Label {
512
- return this.getNode('/root/BannerLayer/Banner') as unknown as Label;
502
+ /*
503
+ * `UiText` under a `HudLayer`, not `Label` under a `UILayer`.
504
+ *
505
+ * A `Label` lives in the WORLD and scales with `viewport.design`. At
506
+ * 1280x800 this HUD painted at 20 device px; on a 390x844 phone the same
507
+ * nodes painted at 6.1 — an unreadable grey smear — because the viewport
508
+ * scale there is 0.406. The DOM widgets are declared in CSS pixels and are
509
+ * immune, which is why the engine's own volume sliders and touch controls
510
+ * stayed legible in the same capture while the flagship's HUD did not.
511
+ */
512
+ private get coinLabel(): UiText {
513
+ return this.getNode('CoinLabel') as unknown as UiText;
514
+ }
515
+ private get heartLabel(): UiText {
516
+ return this.getNode('HeartLabel') as unknown as UiText;
517
+ }
518
+ private get livesLabel(): UiText {
519
+ return this.getNode('LivesLabel') as unknown as UiText;
520
+ }
521
+ private get banner(): UiBanner {
522
+ return this.getNode('/root/BannerLayer/Banner') as unknown as UiBanner;
513
523
  }
514
524
  private get score(): ScoreLike {
515
525
  return this.getNode('/root').behavior as unknown as ScoreLike;
@@ -534,8 +544,8 @@ export class HudUpdater extends Behavior {
534
544
  this.livesLabel.text = `x ${Math.max(0, this.score.lives)}`;
535
545
  }
536
546
  private show(text: string, color: string): void {
537
- const b = this.banner;
538
- b.text = text;
539
- b.color = color;
547
+ // `UiBanner` owns its own fade and queue; a sticky one stays until the
548
+ // next `show`, which is what a win/lose screen wants.
549
+ this.banner.show(text, { color, seconds: 0 });
540
550
  }
541
551
  }
@@ -1848,52 +1848,49 @@
1848
1848
  },
1849
1849
  {
1850
1850
  "name": "HUD",
1851
- "type": "UILayer",
1852
- "props": {
1853
- "anchor": "top-left"
1854
- },
1851
+ "type": "HudLayer",
1855
1852
  "script": {
1856
1853
  "name": "HudUpdater"
1857
1854
  },
1858
1855
  "children": [
1859
1856
  {
1860
1857
  "name": "CoinLabel",
1861
- "type": "Label",
1858
+ "type": "UiText",
1862
1859
  "props": {
1863
- "position": [20, 30],
1860
+ "anchor": "topLeft",
1864
1861
  "text": "◆ 0",
1865
- "fontSize": 26,
1862
+ "size": 22,
1866
1863
  "color": "#ffd166"
1867
1864
  }
1868
1865
  },
1869
1866
  {
1870
1867
  "name": "HeartLabel",
1871
- "type": "Label",
1868
+ "type": "UiText",
1872
1869
  "props": {
1873
- "position": [20, 64],
1870
+ "anchor": "topLeft",
1874
1871
  "text": "♥♥♥",
1875
- "fontSize": 26,
1872
+ "size": 22,
1876
1873
  "color": "#ff6b81"
1877
1874
  }
1878
1875
  },
1879
1876
  {
1880
1877
  "name": "LivesLabel",
1881
- "type": "Label",
1878
+ "type": "UiText",
1882
1879
  "props": {
1883
- "position": [120, 64],
1880
+ "anchor": "topLeft",
1884
1881
  "text": "x 3",
1885
- "fontSize": 22,
1882
+ "size": 18,
1886
1883
  "color": "#f4f4f4"
1887
1884
  }
1888
1885
  },
1889
1886
  {
1890
1887
  "name": "Hint",
1891
- "type": "Label",
1888
+ "type": "UiText",
1892
1889
  "props": {
1893
- "position": [20, 100],
1890
+ "anchor": "bottom",
1894
1891
  "text": "A/D run · Space jump (double-jump!) · stomp goblins · reach the gold flag",
1895
- "fontSize": 15,
1896
- "color": "#1d2a44"
1892
+ "size": 14,
1893
+ "color": "#f4f4f4"
1897
1894
  }
1898
1895
  }
1899
1896
  ],
@@ -1901,20 +1898,13 @@
1901
1898
  },
1902
1899
  {
1903
1900
  "name": "BannerLayer",
1904
- "type": "UILayer",
1905
- "props": {
1906
- "anchor": "center"
1907
- },
1901
+ "type": "HudLayer",
1908
1902
  "children": [
1909
1903
  {
1910
1904
  "name": "Banner",
1911
- "type": "Label",
1905
+ "type": "UiBanner",
1912
1906
  "props": {
1913
- "position": [0, 0],
1914
- "text": "",
1915
- "fontSize": 64,
1916
- "color": "#ffffff",
1917
- "align": "center"
1907
+ "size": 44
1918
1908
  }
1919
1909
  }
1920
1910
  ]
@@ -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,20 +49,39 @@ 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
- document.querySelector('#loading')?.remove();
61
+ // The console handle FIRST, before any wiring of your own can throw.
62
+ // `game.stats()`, `game.assetErrors()`, `game.frame()` and `game.engine.log`
63
+ // are the entire in-page diagnostic surface, and a static build has no other.
64
+ // Assigned after the wiring, one mistake below took all of them with it.
65
+ (window as unknown as { game: typeof game }).game = game;
55
66
 
56
- if (MUSIC_URL) {
57
- game.engine.music.play(MUSIC_URL, { loop: true, fadeIn: 1.5 });
58
- game.engine.audio.music = 0.3;
59
- }
67
+ // And the loading overlay comes down LAST. Everything between here and there is
68
+ // your code; a throw in it used to leave a level that renders perfectly with no
69
+ // player, no error UI, and no way to ask the page what happened.
70
+ try {
71
+ if (MUSIC_URL) {
72
+ game.engine.music.play(MUSIC_URL, { loop: true, fadeIn: 1.5 });
73
+ game.engine.audio.music = 0.3;
74
+ }
60
75
 
61
- // On-screen JUMP button (mobile + desktop parity); the joystick is auto-shown by
62
- // createGame2D on coarse pointers for the 'move' action.
63
- const jumpBtn = document.querySelector('#btn-jump');
64
- jumpBtn?.addEventListener('pointerdown', () => game.engine.input.handleKey('Space', true));
65
- jumpBtn?.addEventListener('pointerup', () => game.engine.input.handleKey('Space', false));
76
+ // No hand-rolled touch button here: the scene declares `"touch": "button"` on
77
+ // `jump` and `"touch": "joystick"` on `move`, so the ENGINE draws both on a
78
+ // coarse pointer — safe-area aware, off the home-indicator band, and sized to
79
+ // its own rules. This template used to draw an 88 px disc of its own at a flat
80
+ // `bottom: 2rem`, which covered 78% of the engine's jump button and put its
81
+ // lower edge inside the band iOS reserves for the leave-the-app swipe.
82
+ } catch (e) {
83
+ showBootFailure(e);
84
+ throw e;
85
+ }
66
86
 
67
- // Console playground: game.scene.root.getNode('Player').position = [x, y]
68
- Object.assign(window, { game });
87
+ 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.69.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,14 +36,32 @@ 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
- document.querySelector('#loading')?.remove();
48
+ // The console handle FIRST, before any wiring of your own can throw.
49
+ // `game.stats()`, `game.assetErrors()`, `game.frame()` and `game.engine.log`
50
+ // are the entire in-page diagnostic surface, and a static build has no other.
51
+ // Assigned after the wiring, one mistake below took all of them with it.
52
+ (window as unknown as { game: typeof game }).game = game;
42
53
 
43
- if (MUSIC_URL) {
44
- game.engine.music.play(MUSIC_URL, { loop: true, fadeIn: 1.5 });
45
- game.engine.audio.music = 0.3;
54
+ // And the loading overlay comes down LAST. Everything between here and there is
55
+ // your code; a throw in it used to leave a level that renders perfectly with no
56
+ // player, no error UI, and no way to ask the page what happened.
57
+ try {
58
+ if (MUSIC_URL) {
59
+ game.engine.music.play(MUSIC_URL, { loop: true, fadeIn: 1.5 });
60
+ game.engine.audio.music = 0.3;
61
+ }
62
+ } catch (e) {
63
+ showBootFailure(e);
64
+ throw e;
46
65
  }
47
66
 
48
- // Console playground: game.scene.root.getNode('Player').position = [x, y]
49
- Object.assign(window, { game });
67
+ 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.69.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,24 +41,42 @@ 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
- document.querySelector('#loading')?.remove();
53
+ // The console handle FIRST, before any wiring of your own can throw.
54
+ // `game.stats()`, `game.assetErrors()`, `game.frame()` and `game.engine.log`
55
+ // are the entire in-page diagnostic surface, and a static build has no other.
56
+ // Assigned after the wiring, one mistake below took all of them with it.
57
+ (window as unknown as { game: typeof game }).game = game;
47
58
 
48
- // Looping background music on the music bus (a calm tense pad). The engine
49
- // auto-resumes a gesture-blocked track on the first click/keypress.
50
- game.engine.music.play(musicUrl, { loop: true, fadeIn: 1.5 });
51
- game.engine.audio.music = 0.3;
59
+ // And the loading overlay comes down LAST. Everything between here and there is
60
+ // your code; a throw in it used to leave a level that renders perfectly with no
61
+ // player, no error UI, and no way to ask the page what happened.
62
+ try {
63
+ // Looping background music on the music bus (a calm tense pad). The engine
64
+ // auto-resumes a gesture-blocked track on the first click/keypress.
65
+ game.engine.music.play(musicUrl, { loop: true, fadeIn: 1.5 });
66
+ game.engine.audio.music = 0.3;
52
67
 
53
- // On-screen FIRE / JUMP buttons (mobile + desktop parity).
54
- for (const [id, code] of [
55
- ['btn-fire', 'KeyF'],
56
- ['btn-jump', 'Space'],
57
- ] as const) {
58
- const el = document.querySelector(`#${id}`);
59
- el?.addEventListener('pointerdown', () => game.engine.input.handleKey(code, true));
60
- el?.addEventListener('pointerup', () => game.engine.input.handleKey(code, false));
68
+ // On-screen FIRE / JUMP buttons (mobile + desktop parity).
69
+ for (const [id, code] of [
70
+ ['btn-fire', 'KeyF'],
71
+ ['btn-jump', 'Space'],
72
+ ] as const) {
73
+ const el = document.querySelector(`#${id}`);
74
+ el?.addEventListener('pointerdown', () => game.engine.input.handleKey(code, true));
75
+ el?.addEventListener('pointerup', () => game.engine.input.handleKey(code, false));
76
+ }
77
+ } catch (e) {
78
+ showBootFailure(e);
79
+ throw e;
61
80
  }
62
81
 
63
- // Console playground for vibe-coding: game.scene.root.getNode('Player')
64
- Object.assign(window, { game });
82
+ 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.69.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,9 +27,26 @@ 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
- document.querySelector('#loading')?.remove();
33
-
34
- // Console-poking handle for playtesting ("teleport me to the elder").
39
+ // The console handle FIRST, before any wiring of your own can throw.
40
+ // `game.stats()`, `game.assetErrors()`, `game.frame()` and `game.engine.log`
41
+ // are the entire in-page diagnostic surface, and a static build has no other.
42
+ // Assigned after the wiring, one mistake below took all of them with it.
35
43
  (window as unknown as { game: typeof game }).game = game;
44
+
45
+ // The overlay comes down LAST, after any wiring of your own. There is none in
46
+ // this template — when you add some, put it above this line and wrap it:
47
+ //
48
+ // try { …your wiring… } catch (e) { showBootFailure(e); throw e; }
49
+ //
50
+ // A throw between the overlay coming down and the handle going up used to leave
51
+ // a level that renders perfectly with no player and no error of any kind.
52
+ document.querySelector('#loading')?.remove();