incanto 0.52.0 → 0.53.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/dist/2d.d.ts +51 -3
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +9 -3
- package/dist/3d.js +4 -4
- package/dist/{behavior-uPEuZrUB.d.ts → behavior-Do0Da56m.d.ts} +98 -0
- package/dist/{create-game-B_e9hJf7.js → create-game-9D87XaiX.js} +26 -11
- package/dist/{create-game-CGnoypjL.js → create-game-CDJ1lVqK.js} +21 -13
- package/dist/debug.d.ts +1 -1
- package/dist/{duplicate-B-OtSRFL.js → duplicate-MNLMAcbz.js} +1 -1
- package/dist/editor.js +14 -0
- package/dist/{environment-presets-DSZwsKPs.js → environment-presets-SkGanr2s.js} +2 -2
- package/dist/{gameplay-B6jqvYeM.js → gameplay-CRYw_Q-T.js} +2 -2
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +177 -5
- package/dist/index.js +8 -8
- package/dist/{loader-B-Gft32x.js → loader-BTkHYrQn.js} +54 -18
- package/dist/{loader-B9iTqs27.d.ts → loader-Cu_7kJDy.d.ts} +1 -1
- package/dist/{log-report-lxrQY9cH.js → log-report-CPFm4OXf.js} +0 -0
- package/dist/net.d.ts +2 -2
- package/dist/net.js +1 -1
- package/dist/{pathfinding-CAR9DjQQ.d.ts → pathfinding-HNGFqGUZ.d.ts} +24 -1
- package/dist/{physics-2d-Dns-oZlE.js → physics-2d-BhZZ-HAp.js} +2 -2
- package/dist/{physics-3d-BhI0ehpe.js → physics-3d-DmyO2oaN.js} +3 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-DVwlnZAZ.js → register-BTomIiYG.js} +304 -24
- package/dist/{register-Trx7WHnD.js → register-DROK2l7J.js} +2 -2
- package/dist/{replay-BU1CCM15.d.ts → replay-9Fy6C10F.d.ts} +1 -1
- package/dist/{replay-BicPOMX0.js → replay-DYNUL4BU.js} +2 -2
- package/dist/{split-screen-CZ9ccBBQ.js → split-screen-CaF9hO7g.js} +2 -2
- package/dist/{split-screen-paxkQs_q.d.ts → split-screen-DNmcX1Pz.d.ts} +1 -1
- package/dist/{src-5gbZO47I.js → src-C9xyZW7M.js} +1 -1
- package/dist/{teardown-ks3d5W9n.js → teardown-Bw2aeyGI.js} +30 -1
- package/dist/{test-DAojuFdb.js → test-CJRsciFk.js} +11 -11
- package/dist/test.d.ts +4 -4
- package/dist/test.js +2 -2
- package/dist/{touch-BoNg_MnF.js → touch-BnMyy9tr.js} +4 -4
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-CCvckvbw.js → agent8-PlFHzJsh.js} +1 -1
- package/editor/assets/{debug-CLNCOnbc.js → debug-CebV7CDW.js} +1 -1
- package/editor/assets/{index-Cb5Brupb.js → index-CdbsY31G.js} +91 -91
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +213 -0
- package/skills/incanto-audio.md +97 -10
- package/skills/incanto-building-2d-games.md +20 -3
- package/skills/incanto-hud.md +7 -0
- package/skills/incanto-node-reference.md +34 -0
- package/skills/incanto-physics-and-input.md +38 -0
- package/skills/incanto-verifying-your-game.md +19 -1
- package/skills/incanto-web-integration.md +1 -0
- package/templates-app/beacon-isle-3d/index.html +3 -3
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/tps-3d/index.html +3 -3
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/index.html +3 -3
- package/templates-app/village-quest-3d/package.json +1 -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-
|
|
8
|
+
<script type="module" crossorigin src="./assets/index-CdbsY31G.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="./assets/GameServer-C56iOUgF.js">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -352,6 +352,9 @@
|
|
|
352
352
|
{
|
|
353
353
|
"$ref": "#/$defs/UiLanguageSelect"
|
|
354
354
|
},
|
|
355
|
+
{
|
|
356
|
+
"$ref": "#/$defs/UiMuteToggle"
|
|
357
|
+
},
|
|
355
358
|
{
|
|
356
359
|
"$ref": "#/$defs/UiPanel"
|
|
357
360
|
},
|
|
@@ -373,6 +376,9 @@
|
|
|
373
376
|
{
|
|
374
377
|
"$ref": "#/$defs/UiToggle"
|
|
375
378
|
},
|
|
379
|
+
{
|
|
380
|
+
"$ref": "#/$defs/UiVolumeSlider"
|
|
381
|
+
},
|
|
376
382
|
{
|
|
377
383
|
"$ref": "#/$defs/VoxelGrid3D"
|
|
378
384
|
},
|
|
@@ -6870,6 +6876,97 @@
|
|
|
6870
6876
|
},
|
|
6871
6877
|
"required": ["name", "type"]
|
|
6872
6878
|
},
|
|
6879
|
+
"UiMuteToggle": {
|
|
6880
|
+
"type": "object",
|
|
6881
|
+
"x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped", "changed"],
|
|
6882
|
+
"properties": {
|
|
6883
|
+
"name": {
|
|
6884
|
+
"type": "string"
|
|
6885
|
+
},
|
|
6886
|
+
"uid": {
|
|
6887
|
+
"type": "string"
|
|
6888
|
+
},
|
|
6889
|
+
"type": {
|
|
6890
|
+
"const": "UiMuteToggle"
|
|
6891
|
+
},
|
|
6892
|
+
"groups": {
|
|
6893
|
+
"type": "array",
|
|
6894
|
+
"items": {
|
|
6895
|
+
"type": "string"
|
|
6896
|
+
}
|
|
6897
|
+
},
|
|
6898
|
+
"tags": {
|
|
6899
|
+
"type": "object"
|
|
6900
|
+
},
|
|
6901
|
+
"props": {
|
|
6902
|
+
"type": "object",
|
|
6903
|
+
"properties": {
|
|
6904
|
+
"anchor": {
|
|
6905
|
+
"type": "string",
|
|
6906
|
+
"enum": [
|
|
6907
|
+
"topLeft",
|
|
6908
|
+
"top",
|
|
6909
|
+
"topRight",
|
|
6910
|
+
"left",
|
|
6911
|
+
"center",
|
|
6912
|
+
"right",
|
|
6913
|
+
"bottomLeft",
|
|
6914
|
+
"bottom",
|
|
6915
|
+
"bottomRight"
|
|
6916
|
+
],
|
|
6917
|
+
"default": "topLeft"
|
|
6918
|
+
},
|
|
6919
|
+
"visible": {
|
|
6920
|
+
"type": "boolean",
|
|
6921
|
+
"default": true
|
|
6922
|
+
},
|
|
6923
|
+
"focusable": {
|
|
6924
|
+
"type": "boolean",
|
|
6925
|
+
"default": true
|
|
6926
|
+
},
|
|
6927
|
+
"draggable": {
|
|
6928
|
+
"type": "boolean",
|
|
6929
|
+
"default": false
|
|
6930
|
+
},
|
|
6931
|
+
"dropTarget": {
|
|
6932
|
+
"type": "boolean",
|
|
6933
|
+
"default": false
|
|
6934
|
+
},
|
|
6935
|
+
"label": {
|
|
6936
|
+
"type": "string",
|
|
6937
|
+
"default": ""
|
|
6938
|
+
},
|
|
6939
|
+
"value": {
|
|
6940
|
+
"type": "boolean",
|
|
6941
|
+
"default": false
|
|
6942
|
+
}
|
|
6943
|
+
},
|
|
6944
|
+
"additionalProperties": false
|
|
6945
|
+
},
|
|
6946
|
+
"script": {
|
|
6947
|
+
"type": "object",
|
|
6948
|
+
"properties": {
|
|
6949
|
+
"name": {
|
|
6950
|
+
"type": "string"
|
|
6951
|
+
},
|
|
6952
|
+
"props": {
|
|
6953
|
+
"type": "object"
|
|
6954
|
+
}
|
|
6955
|
+
},
|
|
6956
|
+
"required": ["name"]
|
|
6957
|
+
},
|
|
6958
|
+
"network": {
|
|
6959
|
+
"type": "object"
|
|
6960
|
+
},
|
|
6961
|
+
"children": {
|
|
6962
|
+
"type": "array",
|
|
6963
|
+
"items": {
|
|
6964
|
+
"$ref": "#/$defs/node"
|
|
6965
|
+
}
|
|
6966
|
+
}
|
|
6967
|
+
},
|
|
6968
|
+
"required": ["name", "type"]
|
|
6969
|
+
},
|
|
6873
6970
|
"UiPanel": {
|
|
6874
6971
|
"type": "object",
|
|
6875
6972
|
"x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped"],
|
|
@@ -7580,6 +7677,122 @@
|
|
|
7580
7677
|
},
|
|
7581
7678
|
"required": ["name", "type"]
|
|
7582
7679
|
},
|
|
7680
|
+
"UiVolumeSlider": {
|
|
7681
|
+
"type": "object",
|
|
7682
|
+
"x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped", "changed"],
|
|
7683
|
+
"properties": {
|
|
7684
|
+
"name": {
|
|
7685
|
+
"type": "string"
|
|
7686
|
+
},
|
|
7687
|
+
"uid": {
|
|
7688
|
+
"type": "string"
|
|
7689
|
+
},
|
|
7690
|
+
"type": {
|
|
7691
|
+
"const": "UiVolumeSlider"
|
|
7692
|
+
},
|
|
7693
|
+
"groups": {
|
|
7694
|
+
"type": "array",
|
|
7695
|
+
"items": {
|
|
7696
|
+
"type": "string"
|
|
7697
|
+
}
|
|
7698
|
+
},
|
|
7699
|
+
"tags": {
|
|
7700
|
+
"type": "object"
|
|
7701
|
+
},
|
|
7702
|
+
"props": {
|
|
7703
|
+
"type": "object",
|
|
7704
|
+
"properties": {
|
|
7705
|
+
"anchor": {
|
|
7706
|
+
"type": "string",
|
|
7707
|
+
"enum": [
|
|
7708
|
+
"topLeft",
|
|
7709
|
+
"top",
|
|
7710
|
+
"topRight",
|
|
7711
|
+
"left",
|
|
7712
|
+
"center",
|
|
7713
|
+
"right",
|
|
7714
|
+
"bottomLeft",
|
|
7715
|
+
"bottom",
|
|
7716
|
+
"bottomRight"
|
|
7717
|
+
],
|
|
7718
|
+
"default": "topLeft"
|
|
7719
|
+
},
|
|
7720
|
+
"visible": {
|
|
7721
|
+
"type": "boolean",
|
|
7722
|
+
"default": true
|
|
7723
|
+
},
|
|
7724
|
+
"focusable": {
|
|
7725
|
+
"type": "boolean",
|
|
7726
|
+
"default": true
|
|
7727
|
+
},
|
|
7728
|
+
"draggable": {
|
|
7729
|
+
"type": "boolean",
|
|
7730
|
+
"default": false
|
|
7731
|
+
},
|
|
7732
|
+
"dropTarget": {
|
|
7733
|
+
"type": "boolean",
|
|
7734
|
+
"default": false
|
|
7735
|
+
},
|
|
7736
|
+
"label": {
|
|
7737
|
+
"type": "string",
|
|
7738
|
+
"default": ""
|
|
7739
|
+
},
|
|
7740
|
+
"value": {
|
|
7741
|
+
"type": "number",
|
|
7742
|
+
"default": 0.5
|
|
7743
|
+
},
|
|
7744
|
+
"min": {
|
|
7745
|
+
"type": "number",
|
|
7746
|
+
"default": 0
|
|
7747
|
+
},
|
|
7748
|
+
"max": {
|
|
7749
|
+
"type": "number",
|
|
7750
|
+
"default": 1
|
|
7751
|
+
},
|
|
7752
|
+
"step": {
|
|
7753
|
+
"type": "number",
|
|
7754
|
+
"default": 0.01
|
|
7755
|
+
},
|
|
7756
|
+
"width": {
|
|
7757
|
+
"type": "number",
|
|
7758
|
+
"default": 180
|
|
7759
|
+
},
|
|
7760
|
+
"color": {
|
|
7761
|
+
"type": "string",
|
|
7762
|
+
"default": "#6ee7dc"
|
|
7763
|
+
},
|
|
7764
|
+
"bus": {
|
|
7765
|
+
"type": "string",
|
|
7766
|
+
"enum": ["master", "sfx", "music"],
|
|
7767
|
+
"default": "master"
|
|
7768
|
+
}
|
|
7769
|
+
},
|
|
7770
|
+
"additionalProperties": false
|
|
7771
|
+
},
|
|
7772
|
+
"script": {
|
|
7773
|
+
"type": "object",
|
|
7774
|
+
"properties": {
|
|
7775
|
+
"name": {
|
|
7776
|
+
"type": "string"
|
|
7777
|
+
},
|
|
7778
|
+
"props": {
|
|
7779
|
+
"type": "object"
|
|
7780
|
+
}
|
|
7781
|
+
},
|
|
7782
|
+
"required": ["name"]
|
|
7783
|
+
},
|
|
7784
|
+
"network": {
|
|
7785
|
+
"type": "object"
|
|
7786
|
+
},
|
|
7787
|
+
"children": {
|
|
7788
|
+
"type": "array",
|
|
7789
|
+
"items": {
|
|
7790
|
+
"$ref": "#/$defs/node"
|
|
7791
|
+
}
|
|
7792
|
+
}
|
|
7793
|
+
},
|
|
7794
|
+
"required": ["name", "type"]
|
|
7795
|
+
},
|
|
7583
7796
|
"VoxelGrid3D": {
|
|
7584
7797
|
"type": "object",
|
|
7585
7798
|
"x-signals": ["blocksChanged"],
|
package/skills/incanto-audio.md
CHANGED
|
@@ -33,7 +33,9 @@ Set `preset` to one of the names below and call `play()`. No files, no loading.
|
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
```ts
|
|
36
|
-
|
|
36
|
+
import type { AudioPlayer } from 'incanto';
|
|
37
|
+
// from a Behavior — `getNode` returns a `Node`, so name the type you asked for
|
|
38
|
+
const coin = this.node.getNode('Coin') as AudioPlayer;
|
|
37
39
|
coin.play(); // synthesizes + plays instantly; rapid calls OVERLAP (no cutoff)
|
|
38
40
|
```
|
|
39
41
|
|
|
@@ -145,8 +147,20 @@ presets are fire-and-forget one-shots: they do not emit `finished`.
|
|
|
145
147
|
Browsers block audio before the first user gesture. `autoplay: true` (or any
|
|
146
148
|
early `play()`) that gets blocked is marked pending; **`createGame2D` /
|
|
147
149
|
`createGame3D` automatically retry every pending player AND resume the WebAudio
|
|
148
|
-
SFX context on the first
|
|
149
|
-
|
|
150
|
+
SFX context on the first gesture anywhere on the page** — you don't wire
|
|
151
|
+
anything, and a tap on the on-screen touch controls counts (they sit above the
|
|
152
|
+
canvas, not on it). A blocked play is not an error; it just waits.
|
|
153
|
+
|
|
154
|
+
**A file the browser CANNOT play is a different thing, and it says so.** A 404 or
|
|
155
|
+
an undecodable clip used to be filed as "waiting for a gesture" too, so it
|
|
156
|
+
retried forever in silence — and a quiet game looks exactly like a game with no
|
|
157
|
+
sound in it. Now it lands in the engine log and in
|
|
158
|
+
**`game.assetErrors()`** (which 2D games can also ask now), beside the textures
|
|
159
|
+
and models that 404'd:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
[incanto] AudioPlayer 'Boom' could not load '/audio/explosion.mp3' — it is silent.
|
|
163
|
+
```
|
|
150
164
|
|
|
151
165
|
### Music: loop a `src` clip on the music bus
|
|
152
166
|
|
|
@@ -227,7 +241,26 @@ the node declares (default `sfx`); music-ish loops typically set `bus: "music"`.
|
|
|
227
241
|
Changes apply to currently-playing `src` clips on the next frame and to every new
|
|
228
242
|
sound immediately.
|
|
229
243
|
|
|
230
|
-
A settings slider just writes these numbers — no per-node bookkeeping.
|
|
244
|
+
A settings slider just writes these numbers — no per-node bookkeeping. And
|
|
245
|
+
**the slider is a node**, so an audio menu is scene JSON like everything else:
|
|
246
|
+
|
|
247
|
+
```json
|
|
248
|
+
{ "name": "Options", "type": "UiPanel", "props": { "anchor": "center" }, "children": [
|
|
249
|
+
{ "name": "Music", "type": "UiVolumeSlider", "props": { "bus": "music" } },
|
|
250
|
+
{ "name": "Sound", "type": "UiVolumeSlider", "props": { "bus": "sfx" } },
|
|
251
|
+
{ "name": "Mute", "type": "UiMuteToggle" }
|
|
252
|
+
] }
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
| node | prop | writes |
|
|
256
|
+
|---|---|---|
|
|
257
|
+
| `UiVolumeSlider` | `bus`: `master` (default) / `sfx` / `music` | `engine.audio[bus]` |
|
|
258
|
+
| `UiMuteToggle` | — | `engine.audio.muted` (checked = silent) |
|
|
259
|
+
|
|
260
|
+
No behavior, no `connections`, nothing to save: the buses persist themselves
|
|
261
|
+
(below), and each control follows a change made anywhere else, so two menus can
|
|
262
|
+
never disagree. Left unlabelled a slider names its own bus (`Volume` / `Sound` /
|
|
263
|
+
`Music`, translated when the scene declares `settings.volume.*`).
|
|
231
264
|
|
|
232
265
|
---
|
|
233
266
|
|
|
@@ -312,15 +345,44 @@ clean fit; the looping-`src` `AudioPlayer` (§2) stays the right tool for a *fix
|
|
|
312
345
|
background loop authored in scene JSON. Wire `crossfadeTo` from gameplay:
|
|
313
346
|
|
|
314
347
|
```ts
|
|
315
|
-
// e.g. in a behavior when the boss spawns
|
|
316
|
-
this.tree
|
|
348
|
+
// e.g. in a behavior when the boss spawns (`this.engine` is the accessor a
|
|
349
|
+
// Behavior has — there is no `this.tree`):
|
|
350
|
+
this.engine.music.crossfadeTo('/audio/boss.mp3', 3);
|
|
317
351
|
```
|
|
318
352
|
|
|
319
|
-
> Large music files are NOT bundled — reference them by URL (see §4).
|
|
320
|
-
|
|
353
|
+
> Large music files are NOT bundled — reference them by URL (see §4).
|
|
354
|
+
|
|
355
|
+
**Headless the state machine still runs and `current` still updates** — nothing
|
|
356
|
+
plays, but the question a test has is answerable:
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
session.engine.music.crossfadeTo('/audio/boss.wav', 2);
|
|
360
|
+
session.step(100);
|
|
361
|
+
session.engine.music.current; // '/audio/boss.wav'
|
|
362
|
+
session.engine.audio.countOf('/audio/boss.wav'); // 1
|
|
363
|
+
```
|
|
321
364
|
|
|
322
365
|
---
|
|
323
366
|
|
|
367
|
+
## Sound stops when the thing making it goes away
|
|
368
|
+
|
|
369
|
+
Teardown is SILENCE, and you do not wire it:
|
|
370
|
+
|
|
371
|
+
- **A freed node stops its clip.** Swap to level 2 and level 1's looping
|
|
372
|
+
background `AudioPlayer` stops with it — it used to play on over the new level
|
|
373
|
+
forever, with its node freed and no handle left to stop it. Being *moved* is
|
|
374
|
+
not this: a reparented node keeps playing, because reparenting is not
|
|
375
|
+
destroying.
|
|
376
|
+
- **`game.dispose()` stops the music and every continuous voice**, and hands the
|
|
377
|
+
AudioContexts back. An SPA that mounts the game a few times would otherwise
|
|
378
|
+
run out of them (browsers allow only a handful per page).
|
|
379
|
+
|
|
380
|
+
So the one thing you still own is a voice you want to outlive a node — and the
|
|
381
|
+
one thing you must NOT rely on is a sound stopping itself because the scene
|
|
382
|
+
changed under it. `engine.music` deliberately survives a scene swap (it belongs
|
|
383
|
+
to the engine, not the scene): call `engine.music.stop(1)` or `crossfadeTo` when
|
|
384
|
+
the music should change.
|
|
385
|
+
|
|
324
386
|
## Decision guide
|
|
325
387
|
|
|
326
388
|
- **Need a quick game sound (coin/jump/hit/explosion/…)** → set `preset`. Done.
|
|
@@ -332,8 +394,33 @@ this.tree.engine.music.crossfadeTo('incanto/assets/audio/boss.mp3', 3);
|
|
|
332
394
|
- **3D sound that pans + fades with distance** → `spatial:true` on an
|
|
333
395
|
`AudioPlayer` in a 3D scene (§2b); listener = the active `Camera3D`.
|
|
334
396
|
- **Global volume / mute / settings** → `engine.audio.master/sfx/music/muted`.
|
|
335
|
-
- **Verifying headlessly** →
|
|
336
|
-
|
|
397
|
+
- **Verifying headlessly** → read the AUDIO RECORD (below). Audio makes no sound
|
|
398
|
+
in the VM, but the calls still happen, so "did the coin sound fire when the
|
|
399
|
+
coin was collected?" is answerable there.
|
|
400
|
+
|
|
401
|
+
## Verifying sound without hearing it
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
const session = await createPlaySession(gameJson, {});
|
|
405
|
+
session.engine.audio.clearLog();
|
|
406
|
+
collectACoin();
|
|
407
|
+
session.step(200);
|
|
408
|
+
|
|
409
|
+
session.engine.audio.countOf('coin'); // 1
|
|
410
|
+
session.engine.audio.recent();
|
|
411
|
+
// [{ kind: 'preset', name: 'coin', from: '/Game/Player/Coin', bus: 'sfx', at: 1.2 }]
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
`recent()` is the last 200 sounds, oldest first — `kind` is `preset` | `src` |
|
|
415
|
+
`music` | `voice`, `name` is the preset name, the clip url or the track,
|
|
416
|
+
`from` is the node path (or `engine.music` / `engine.sfx`), and `bus` is where
|
|
417
|
+
its volume comes from. `countOf(name)` is the assertion you usually want;
|
|
418
|
+
`clearLog()` resets between steps.
|
|
419
|
+
|
|
420
|
+
This covers every path: `AudioPlayer.play()` on both the procedural and the
|
|
421
|
+
`src` route, `engine.music.play`/`crossfadeTo`, and `engine.sfx.startVoice`. It
|
|
422
|
+
records the INTENT to play — that the wiring fired — not that a speaker moved;
|
|
423
|
+
for "the file is broken" see `assetErrors()` above.
|
|
337
424
|
|
|
338
425
|
## Settings that survive a reload (`engine.settings`)
|
|
339
426
|
|
|
@@ -200,9 +200,26 @@ listing the valid set. With a viewport design, UI coordinates are design px.
|
|
|
200
200
|
**incanto-audio.md**.
|
|
201
201
|
- Reference example: [examples/2d-phaser-sprite-character-gravity](https://github.com/rareboe/Incanto/tree/main/examples/2d-phaser-sprite-character-gravity) —
|
|
202
202
|
spritesheet character with a walk/jump/attack behavior state machine.
|
|
203
|
-
-
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
- **Tap / drag / click**: `createGame2D({ pointer: true })` attaches pointer
|
|
204
|
+
input — MOUSE, FINGER and pen alike — and then
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
game.engine.updated.connect(() => {
|
|
208
|
+
if (!game.engine.input.mouseJustPressed()) return;
|
|
209
|
+
const at = game.engine.input.pointerPosition(); // canvas px, null until
|
|
210
|
+
if (!at) return; // a pointer has been seen
|
|
211
|
+
const world = game.renderer.worldFromScreen(at.x, at.y); // world px
|
|
212
|
+
popAt(world);
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Use `renderer.worldFromScreen` — do NOT hand-roll the projection. It already
|
|
217
|
+
accounts for the design `viewport`, the letterbox bars and the camera's
|
|
218
|
+
clamped centre; a formula written against the DESIGN rect is off by
|
|
219
|
+
`(design − canvas) / 2 / zoom`, which on a 390 px-wide phone showing a 480 px
|
|
220
|
+
design measured **62 world px** — a finger-and-a-half from what the player
|
|
221
|
+
touched. Its inverse, `renderer.screenFromWorld(wx, wy)`, pins DOM to the
|
|
222
|
+
world (see incanto-web-integration).
|
|
206
223
|
|
|
207
224
|
## Game flow recipes
|
|
208
225
|
|
package/skills/incanto-hud.md
CHANGED
|
@@ -151,6 +151,13 @@ so structure in the tree becomes structure on screen. Panels nest.
|
|
|
151
151
|
| `UiSlider` | `label`, `value`, `min`, `max`, `step`, `width`, `color` | `changed(value)` |
|
|
152
152
|
| `UiToggle` | `label`, `value` | `changed(bool)` |
|
|
153
153
|
| `UiSelect` | `label`, `options` (`"low,medium,high"`), `value` | `changed(value)` |
|
|
154
|
+
| `UiVolumeSlider` | `bus` (`master`/`sfx`/`music`), plus every `UiSlider` prop | `changed(value)` |
|
|
155
|
+
| `UiMuteToggle` | `label` | `changed(bool)` |
|
|
156
|
+
|
|
157
|
+
The last two are **already wired**: they read and write `engine.audio`, which
|
|
158
|
+
persists itself. An audio menu is three nodes and no TypeScript — see
|
|
159
|
+
`incanto-audio.md`. The graphics ones (`UiQualitySelect`, `UiFrameCapSelect`,
|
|
160
|
+
`UiRenderScaleSelect`) and `UiLanguageSelect` work the same way.
|
|
154
161
|
|
|
155
162
|
**An inventory is a grid panel**: `"layout": "grid", "columns": 5`, one child per
|
|
156
163
|
slot, each a small `UiPanel` holding a `UiImage` (`tint` greys out what you
|
|
@@ -1039,6 +1039,20 @@ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped`
|
|
|
1039
1039
|
|
|
1040
1040
|
Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
|
|
1041
1041
|
|
|
1042
|
+
## `UiMuteToggle` — `incanto`
|
|
1043
|
+
|
|
1044
|
+
| Prop | Default | Kind |
|
|
1045
|
+
|---|---|---|
|
|
1046
|
+
| `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
|
|
1047
|
+
| `visible` | `true` | boolean |
|
|
1048
|
+
| `focusable` | `true` | boolean |
|
|
1049
|
+
| `draggable` | `false` | boolean |
|
|
1050
|
+
| `dropTarget` | `false` | boolean |
|
|
1051
|
+
| `label` | `""` | string |
|
|
1052
|
+
| `value` | `false` | boolean |
|
|
1053
|
+
|
|
1054
|
+
Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
|
|
1055
|
+
|
|
1042
1056
|
## `UiPanel` — `incanto`
|
|
1043
1057
|
|
|
1044
1058
|
| Prop | Default | Kind |
|
|
@@ -1155,6 +1169,26 @@ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped`
|
|
|
1155
1169
|
|
|
1156
1170
|
Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
|
|
1157
1171
|
|
|
1172
|
+
## `UiVolumeSlider` — `incanto`
|
|
1173
|
+
|
|
1174
|
+
| Prop | Default | Kind |
|
|
1175
|
+
|---|---|---|
|
|
1176
|
+
| `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
|
|
1177
|
+
| `visible` | `true` | boolean |
|
|
1178
|
+
| `focusable` | `true` | boolean |
|
|
1179
|
+
| `draggable` | `false` | boolean |
|
|
1180
|
+
| `dropTarget` | `false` | boolean |
|
|
1181
|
+
| `label` | `""` | string |
|
|
1182
|
+
| `value` | `0.5` | number |
|
|
1183
|
+
| `min` | `0` | number |
|
|
1184
|
+
| `max` | `1` | number |
|
|
1185
|
+
| `step` | `0.01` | number |
|
|
1186
|
+
| `width` | `180` | number |
|
|
1187
|
+
| `color` | `"#6ee7dc"` | string |
|
|
1188
|
+
| `bus` | `"master"` | one of: `master` `sfx` `music` |
|
|
1189
|
+
|
|
1190
|
+
Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
|
|
1191
|
+
|
|
1158
1192
|
## `VoxelGrid3D` — `incanto/3d`
|
|
1159
1193
|
|
|
1160
1194
|
| Prop | Default | Kind |
|
|
@@ -142,6 +142,44 @@ disables; they overlay `touchContainer` — default the canvas's parent, give it
|
|
|
142
142
|
`position: relative`). Manual boots call `attachTouchControls(engine, container,
|
|
143
143
|
{ force? })` from `incanto`. Wrong touch kinds (`"joystick"` on a button) fail at load.
|
|
144
144
|
|
|
145
|
+
**Three things a phone needs that a desktop never shows you**, two of which the
|
|
146
|
+
engine now does for you:
|
|
147
|
+
|
|
148
|
+
- **The canvas owns its touch gestures.** `createGame2D/3D` set
|
|
149
|
+
`touch-action: none` (plus no selection/callout) on the canvas, or the browser
|
|
150
|
+
keeps pan, pinch-zoom and pull-to-refresh over the play surface — a drag meant
|
|
151
|
+
for aim or a virtual stick SCROLLS THE PAGE instead. `pageGestures: true`
|
|
152
|
+
gives them back to the browser (a small canvas inside a scrolling article).
|
|
153
|
+
- **The controls clear the phone's furniture.** They sit
|
|
154
|
+
`calc(24px + env(safe-area-inset-*))` from the edges, off the iPhone home
|
|
155
|
+
indicator — whose band is also the OS's "leave the app" swipe.
|
|
156
|
+
- **Your PAGE has to opt in**, and this part is yours: `env()` is 0 unless the
|
|
157
|
+
document says so, and `100vh` is the toolbar-hidden height, so the bottom strip
|
|
158
|
+
(where the controls are) hides under the browser chrome.
|
|
159
|
+
|
|
160
|
+
```html
|
|
161
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
162
|
+
<style>
|
|
163
|
+
html, body { margin: 0; height: 100%; overflow: hidden; }
|
|
164
|
+
canvas { display: block; width: 100%; height: 100%; } /* NOT 100vw/100vh */
|
|
165
|
+
</style>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Every `incanto-new` template and every runnable example ships exactly this.
|
|
169
|
+
|
|
170
|
+
Two more phone facts worth knowing:
|
|
171
|
+
|
|
172
|
+
- **Audio unlocks on ANY first gesture** — the canvas, a key, or the on-screen
|
|
173
|
+
stick and buttons. (Those controls are siblings of the canvas, so their taps
|
|
174
|
+
never reached it; a game with on-screen controls used to stay silent until the
|
|
175
|
+
player happened to touch the play area.)
|
|
176
|
+
- **2D renders at `pixelRatio: 1` by default** (Phaser parity — the browser
|
|
177
|
+
upscales, edges read soft). Phones are 2–3× denser, so a text-heavy or
|
|
178
|
+
vector-heavy 2D game wants
|
|
179
|
+
`createGame2D({ pixelRatio: Math.min(devicePixelRatio, 2) })`, or
|
|
180
|
+
`"environment": { "rendering": { "pixelRatio": "device" } }` in the scene. 3D
|
|
181
|
+
already defaults to `min(dpr, 2)`; pixel art is usually happier left at 1.
|
|
182
|
+
|
|
145
183
|
Collision layers/masks are not in v0 — everything collides with everything; use groups +
|
|
146
184
|
`triggerEnter` filtering for game logic.
|
|
147
185
|
|
|
@@ -314,7 +314,9 @@ They now go to **`engine.log` as well as the console**, so:
|
|
|
314
314
|
- `runScript().logs` contains them, and `describe()` prints them under the
|
|
315
315
|
failures (`! warn: …`).
|
|
316
316
|
- The debug overlay's **Logs** panel shows them live.
|
|
317
|
-
- `game.assetErrors()` lists every asset that failed **with its reason and ref**
|
|
317
|
+
- `game.assetErrors()` lists every asset that failed **with its reason and ref** —
|
|
318
|
+
textures, models, AND sounds (an `AudioPlayer` whose `src` the browser cannot
|
|
319
|
+
play), in 2D and 3D alike
|
|
318
320
|
— the direct answer to "why is my model not there". The store itself is
|
|
319
321
|
reachable as `renderer.assets` in both dimensions.
|
|
320
322
|
|
|
@@ -661,6 +663,22 @@ never Math.random, dt/`engine.time` never Date.now. Gamepads replay through
|
|
|
661
663
|
the ACTIONS they were bound to, not raw pad state.
|
|
662
664
|
|
|
663
665
|
|
|
666
|
+
## Sound: did the right thing sound?
|
|
667
|
+
|
|
668
|
+
Audio makes no sound in the VM, but the calls still happen — so the wiring is
|
|
669
|
+
checkable where the game can be run a thousand times:
|
|
670
|
+
|
|
671
|
+
```ts
|
|
672
|
+
session.engine.audio.clearLog();
|
|
673
|
+
hitTheEnemy();
|
|
674
|
+
session.step(200);
|
|
675
|
+
expect(session.engine.audio.countOf('hit')).toBe(1);
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
`engine.audio.recent()` is the last 200 sounds with their node path, bus and
|
|
679
|
+
time; `countOf(name)` counts one. A clip whose FILE is broken shows up in
|
|
680
|
+
`assetErrors()` instead. Full guide: `incanto-audio.md`.
|
|
681
|
+
|
|
664
682
|
## Multiplayer: do the players end up in the SAME world?
|
|
665
683
|
|
|
666
684
|
Every rung above asks about one player. A multiplayer game's first question is
|
|
@@ -59,6 +59,7 @@ Rule of thumb: score/menus/dialogs that look like WEB UI → DOM overlay;
|
|
|
59
59
|
anything the game itself must own (and runScript must verify) → UILayer.
|
|
60
60
|
|
|
61
61
|
Pin a DOM element to a world position: `game.renderer.screenFromWorld(wx, wy)`
|
|
62
|
+
(and `worldFromScreen(sx, sy)` the other way, for taps and clicks)
|
|
62
63
|
each frame (subscribe `engine.updated`), then `transform: translate(x, y)`.
|
|
63
64
|
|
|
64
65
|
Perf note: `useNodeProp` deep-compares per frame — subscribe to LEAF values
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><rect width=%22100%22 height=%22100%22 rx=%2220%22 fill=%22%232a4a6b%22/><text x=%2250%22 y=%2268%22 font-size=%2255%22 text-anchor=%22middle%22 font-family=%22Arial%22 font-weight=%22bold%22 fill=%22%23ffd166%22>bi</text></svg>" />
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
7
7
|
<title>Beacon Isle — Incanto 3D template</title>
|
|
8
8
|
<style>
|
|
9
9
|
html,
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
}
|
|
18
18
|
canvas {
|
|
19
19
|
display: block;
|
|
20
|
-
width:
|
|
21
|
-
height:
|
|
20
|
+
width: 100%;
|
|
21
|
+
height: 100%;
|
|
22
22
|
}
|
|
23
23
|
.loading-container {
|
|
24
24
|
position: fixed;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><rect width=%22100%22 height=%22100%22 rx=%2220%22 fill=%22%233b6ea5%22/><text x=%2250%22 y=%2268%22 font-size=%2255%22 text-anchor=%22middle%22 font-family=%22Arial%22 font-weight=%22bold%22 fill=%22%236ee7dc%22>vg</text></svg>" />
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
7
7
|
<title>Vanguard — Incanto TPS</title>
|
|
8
8
|
<style>
|
|
9
9
|
html,
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
}
|
|
18
18
|
canvas {
|
|
19
19
|
display: block;
|
|
20
|
-
width:
|
|
21
|
-
height:
|
|
20
|
+
width: 100%;
|
|
21
|
+
height: 100%;
|
|
22
22
|
}
|
|
23
23
|
.loading-container {
|
|
24
24
|
position: fixed;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><rect width=%22100%22 height=%22100%22 rx=%2220%22 fill=%22%234a6b3a%22/><text x=%2250%22 y=%2268%22 font-size=%2255%22 text-anchor=%22middle%22 font-family=%22Arial%22 font-weight=%22bold%22 fill=%22%23ffd166%22>eb</text></svg>" />
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
7
7
|
<title>Emberwood — Incanto quest</title>
|
|
8
8
|
<style>
|
|
9
9
|
html,
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
}
|
|
18
18
|
canvas {
|
|
19
19
|
display: block;
|
|
20
|
-
width:
|
|
21
|
-
height:
|
|
20
|
+
width: 100%;
|
|
21
|
+
height: 100%;
|
|
22
22
|
}
|
|
23
23
|
.loading-container {
|
|
24
24
|
position: fixed;
|