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.
- package/assets/catalog.json +9 -5
- package/bin/_pick-server.mjs +48 -0
- package/bin/incanto-assets.mjs +14 -1
- package/bin/incanto-check.mjs +69 -5
- package/bin/incanto-frame.mjs +10 -25
- package/bin/incanto-logs.mjs +8 -18
- package/bin/incanto-new.mjs +29 -1
- package/bin/incanto-serve.mjs +160 -0
- package/bin/incanto-verify.mjs +40 -0
- package/bin/incanto.mjs +1 -0
- package/dist/2d.d.ts +10 -8
- package/dist/2d.js +6 -4
- package/dist/3d.d.ts +31 -7
- package/dist/3d.js +9 -7
- package/dist/{audio-player-BNUHo0JF.d.ts → audio-player-C0j2k-yB.d.ts} +80 -3
- package/dist/{behavior-ZnfzKDo3.d.ts → behavior-rm8vVzu_.d.ts} +145 -1
- package/dist/boot-failure-CKYrEcGF.js +47 -0
- package/dist/{create-game-FAXdHta3.js → create-game-CvJ1wSRP.js} +27 -27
- package/dist/{create-game-DMetd3vR.js → create-game-CwxEDlCm.js} +15 -11
- package/dist/debug.d.ts +1 -1
- package/dist/{duplicate-B8QnX2-r.js → duplicate-Dj4L-5AA.js} +1 -1
- package/dist/{editor-switch-DAvWQeld.d.ts → editor-switch-CnIOiyNJ.d.ts} +1 -1
- package/dist/editor.js +12 -6
- package/dist/env.d.ts +1 -1
- package/dist/{environment-presets-C-5g2pMh.js → environment-presets-BO-7XoFV.js} +58 -9
- package/dist/{frame-report-DCnHFmto.d.ts → frame-report-DNxDAb1w.d.ts} +8 -0
- package/dist/{frame-report-BSMny7oe.js → frame-report-Dlq13Gyj.js} +1 -0
- package/dist/{gameplay-CGSv-9XR.js → gameplay-D1KixWK8.js} +175 -151
- package/dist/gameplay.d.ts +51 -3
- package/dist/gameplay.js +2 -2
- package/dist/index.d.ts +6 -6
- package/dist/index.js +8 -7
- package/dist/{loader-KZESOZmE.js → loader-DZE7B07H.js} +48 -7
- package/dist/{loader-DyT30ArB.d.ts → loader-DadV2QCT.d.ts} +2 -2
- package/dist/net.d.ts +2 -2
- package/dist/net.js +1 -1
- package/dist/{physics-2d-_B6zj3zU.js → physics-2d-CF5yyjbj.js} +2 -2
- package/dist/{physics-3d-i1IeAY0F.js → physics-3d-DbYaEcZy.js} +3 -3
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/{register-vIxU3Xbv.js → register-DbVArsns.js} +19 -10
- package/dist/{register-Cs53CY7s.js → register-mmfugpaQ.js} +159 -6
- package/dist/{replay-BkpDV51O.js → replay-BEHLDcQV.js} +28 -5
- package/dist/{replay-D0GcEHYU.d.ts → replay-BQfkX4x7.d.ts} +38 -4
- package/dist/{schema-CFeioQRE.d.ts → schema-B6ugCV1Q.d.ts} +11 -0
- package/dist/{sheet-grid-BT6N_Bjs.js → sheet-grid-Cea343VO.js} +6 -2
- package/dist/{split-screen-DS8RaUV_.js → split-screen-CauMwu6y.js} +2 -2
- package/dist/{split-screen-DD5eYIgW.d.ts → split-screen-Cg76MCV5.d.ts} +3 -3
- package/dist/{src-BLlsGJW9.js → src-CYEu-_S9.js} +1 -1
- package/dist/{teardown-LB9cTnXJ.js → teardown-CMqFVxO9.js} +5 -4
- package/dist/{test-81uZ967a.js → test-D6ISmuFE.js} +73 -25
- package/dist/test.d.ts +17 -6
- package/dist/test.js +3 -3
- package/dist/vite.d.ts +70 -2
- package/dist/vite.js +176 -14
- package/dist/{rendering-options-ByAdi8PF.js → webgl-unavailable-Z5_g8x9H.js} +57 -2
- package/editor/assets/{agent8-C2jtNKbx.js → agent8-BDvaZMQJ.js} +1 -1
- package/editor/assets/{debug-DoU3KeqS.js → debug-CMULinBW.js} +1 -1
- package/editor/assets/{index-CNqHojcC.js → index-CZIRr5EL.js} +92 -92
- package/editor/index.html +1 -1
- package/package.json +4 -3
- package/schemas/scene.schema.json +4 -0
- package/skills/incanto-3d-character.md +1 -0
- package/skills/incanto-assets.md +16 -2
- package/skills/incanto-audio.md +65 -0
- package/skills/incanto-building-2d-games.md +66 -0
- package/skills/incanto-building-3d-games.md +97 -2
- package/skills/incanto-gameplay-behaviors.md +46 -2
- package/skills/incanto-node-reference.md +1 -0
- package/skills/incanto-physics-and-input.md +23 -0
- package/skills/incanto-save-slots.md +27 -0
- package/skills/incanto-scene-json-authoring.md +12 -1
- package/skills/incanto-verifying-your-game.md +96 -2
- package/skills/incanto-your-first-game.md +2 -1
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/main.ts +21 -4
- package/templates-app/platformer-2d/index.html +0 -23
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/platformer-2d/src/behaviors.ts +26 -16
- package/templates-app/platformer-2d/src/game.scene.json +17 -27
- package/templates-app/platformer-2d/src/main.ts +32 -13
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/star-survivor/src/main.ts +25 -7
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/main.ts +34 -16
- package/templates-app/village-quest-3d/package.json +1 -1
- 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-
|
|
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
|
|
|
@@ -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
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
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() {
|
|
@@ -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,
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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": "
|
|
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": "
|
|
1858
|
+
"type": "UiText",
|
|
1862
1859
|
"props": {
|
|
1863
|
-
"
|
|
1860
|
+
"anchor": "topLeft",
|
|
1864
1861
|
"text": "◆ 0",
|
|
1865
|
-
"
|
|
1862
|
+
"size": 22,
|
|
1866
1863
|
"color": "#ffd166"
|
|
1867
1864
|
}
|
|
1868
1865
|
},
|
|
1869
1866
|
{
|
|
1870
1867
|
"name": "HeartLabel",
|
|
1871
|
-
"type": "
|
|
1868
|
+
"type": "UiText",
|
|
1872
1869
|
"props": {
|
|
1873
|
-
"
|
|
1870
|
+
"anchor": "topLeft",
|
|
1874
1871
|
"text": "♥♥♥",
|
|
1875
|
-
"
|
|
1872
|
+
"size": 22,
|
|
1876
1873
|
"color": "#ff6b81"
|
|
1877
1874
|
}
|
|
1878
1875
|
},
|
|
1879
1876
|
{
|
|
1880
1877
|
"name": "LivesLabel",
|
|
1881
|
-
"type": "
|
|
1878
|
+
"type": "UiText",
|
|
1882
1879
|
"props": {
|
|
1883
|
-
"
|
|
1880
|
+
"anchor": "topLeft",
|
|
1884
1881
|
"text": "x 3",
|
|
1885
|
-
"
|
|
1882
|
+
"size": 18,
|
|
1886
1883
|
"color": "#f4f4f4"
|
|
1887
1884
|
}
|
|
1888
1885
|
},
|
|
1889
1886
|
{
|
|
1890
1887
|
"name": "Hint",
|
|
1891
|
-
"type": "
|
|
1888
|
+
"type": "UiText",
|
|
1892
1889
|
"props": {
|
|
1893
|
-
"
|
|
1890
|
+
"anchor": "bottom",
|
|
1894
1891
|
"text": "A/D run · Space jump (double-jump!) · stomp goblins · reach the gold flag",
|
|
1895
|
-
"
|
|
1896
|
-
"color": "#
|
|
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": "
|
|
1905
|
-
"props": {
|
|
1906
|
-
"anchor": "center"
|
|
1907
|
-
},
|
|
1901
|
+
"type": "HudLayer",
|
|
1908
1902
|
"children": [
|
|
1909
1903
|
{
|
|
1910
1904
|
"name": "Banner",
|
|
1911
|
-
"type": "
|
|
1905
|
+
"type": "UiBanner",
|
|
1912
1906
|
"props": {
|
|
1913
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
Object.assign(window, { game });
|
|
87
|
+
document.querySelector('#loading')?.remove();
|
|
@@ -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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
Object.assign(window, { game });
|
|
67
|
+
document.querySelector('#loading')?.remove();
|
|
@@ -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
|
-
|
|
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
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
] as const) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
Object.assign(window, { game });
|
|
82
|
+
document.querySelector('#loading')?.remove();
|
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
//
|
|
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();
|