incanto 0.52.0 → 0.54.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/bin/incanto-check.mjs +56 -1
- package/dist/2d.d.ts +95 -4
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +111 -6
- package/dist/3d.js +10 -6
- package/dist/{behavior-uPEuZrUB.d.ts → behavior-CyQoSu4n.d.ts} +212 -10
- package/dist/{create-game-B_e9hJf7.js → create-game-BRt6XKmP.js} +31 -12
- package/dist/{create-game-CGnoypjL.js → create-game-Czzp6ZuE.js} +26 -14
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +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-BQ_QsIBY.js} +222 -11
- package/dist/{gameplay-B6jqvYeM.js → gameplay-CZ2yq37J.js} +32 -23
- package/dist/gameplay.d.ts +8 -3
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +178 -6
- package/dist/index.js +9 -9
- package/dist/{loader-B-Gft32x.js → loader-BTkHYrQn.js} +54 -18
- package/dist/{loader-B9iTqs27.d.ts → loader-DhI1jFW_.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/{particle-sim-BzJ1yxoE.d.ts → particle-sim-C5OfBbmU.d.ts} +11 -0
- package/dist/{pathfinding-CAR9DjQQ.d.ts → pathfinding-CXGCpRQe.d.ts} +24 -1
- package/dist/{physics-2d-Dns-oZlE.js → physics-2d-CfWAggJ1.js} +2 -2
- package/dist/{physics-3d-BhI0ehpe.js → physics-3d-Bes-GIuR.js} +3 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-Trx7WHnD.js → register-en63AEZO.js} +163 -4
- package/dist/{register-DVwlnZAZ.js → register-p48lHE2o.js} +433 -36
- package/dist/{replay-BU1CCM15.d.ts → replay-ePMz26jw.d.ts} +1 -1
- package/dist/{replay-BicPOMX0.js → replay-t1pP0gQg.js} +2 -2
- package/dist/{split-screen-paxkQs_q.d.ts → split-screen-BsdOHbzP.d.ts} +1 -1
- package/dist/{split-screen-CZ9ccBBQ.js → split-screen-CSb_uZ6W.js} +2 -2
- package/dist/{sprite-animation-D_p28jwU.js → sprite-animation-7qvUxF6Z.js} +19 -0
- package/dist/{src-5gbZO47I.js → src-vNPQeQ1W.js} +1 -1
- package/dist/{teardown-ks3d5W9n.js → teardown-C7uVSJvx.js} +30 -1
- package/dist/{test-DAojuFdb.js → test-Tnf1nQl3.js} +25 -13
- 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-BWaW-D85.js} +1 -1
- package/editor/assets/{debug-CLNCOnbc.js → debug-DLF8rUtr.js} +2 -2
- package/editor/assets/{index-Cb5Brupb.js → index-BYfiwbQx.js} +91 -91
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +221 -0
- package/skills/incanto-assets.md +45 -2
- package/skills/incanto-audio.md +97 -10
- package/skills/incanto-building-2d-games.md +55 -3
- package/skills/incanto-building-3d-games.md +3 -1
- package/skills/incanto-environment.md +3 -1
- package/skills/incanto-gameplay-behaviors.md +51 -4
- package/skills/incanto-hud.md +7 -0
- package/skills/incanto-node-reference.md +36 -0
- package/skills/incanto-performance.md +47 -0
- package/skills/incanto-physics-and-input.md +38 -0
- package/skills/incanto-verifying-your-game.md +50 -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-BYfiwbQx.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
|
},
|
|
@@ -4229,6 +4235,10 @@
|
|
|
4229
4235
|
"type": "boolean",
|
|
4230
4236
|
"default": true
|
|
4231
4237
|
},
|
|
4238
|
+
"worldSpace": {
|
|
4239
|
+
"type": "boolean",
|
|
4240
|
+
"default": false
|
|
4241
|
+
},
|
|
4232
4242
|
"rate": {
|
|
4233
4243
|
"type": "number",
|
|
4234
4244
|
"default": 40
|
|
@@ -4437,6 +4447,10 @@
|
|
|
4437
4447
|
"type": "boolean",
|
|
4438
4448
|
"default": true
|
|
4439
4449
|
},
|
|
4450
|
+
"worldSpace": {
|
|
4451
|
+
"type": "boolean",
|
|
4452
|
+
"default": false
|
|
4453
|
+
},
|
|
4440
4454
|
"rate": {
|
|
4441
4455
|
"type": "number",
|
|
4442
4456
|
"default": 40
|
|
@@ -6870,6 +6884,97 @@
|
|
|
6870
6884
|
},
|
|
6871
6885
|
"required": ["name", "type"]
|
|
6872
6886
|
},
|
|
6887
|
+
"UiMuteToggle": {
|
|
6888
|
+
"type": "object",
|
|
6889
|
+
"x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped", "changed"],
|
|
6890
|
+
"properties": {
|
|
6891
|
+
"name": {
|
|
6892
|
+
"type": "string"
|
|
6893
|
+
},
|
|
6894
|
+
"uid": {
|
|
6895
|
+
"type": "string"
|
|
6896
|
+
},
|
|
6897
|
+
"type": {
|
|
6898
|
+
"const": "UiMuteToggle"
|
|
6899
|
+
},
|
|
6900
|
+
"groups": {
|
|
6901
|
+
"type": "array",
|
|
6902
|
+
"items": {
|
|
6903
|
+
"type": "string"
|
|
6904
|
+
}
|
|
6905
|
+
},
|
|
6906
|
+
"tags": {
|
|
6907
|
+
"type": "object"
|
|
6908
|
+
},
|
|
6909
|
+
"props": {
|
|
6910
|
+
"type": "object",
|
|
6911
|
+
"properties": {
|
|
6912
|
+
"anchor": {
|
|
6913
|
+
"type": "string",
|
|
6914
|
+
"enum": [
|
|
6915
|
+
"topLeft",
|
|
6916
|
+
"top",
|
|
6917
|
+
"topRight",
|
|
6918
|
+
"left",
|
|
6919
|
+
"center",
|
|
6920
|
+
"right",
|
|
6921
|
+
"bottomLeft",
|
|
6922
|
+
"bottom",
|
|
6923
|
+
"bottomRight"
|
|
6924
|
+
],
|
|
6925
|
+
"default": "topLeft"
|
|
6926
|
+
},
|
|
6927
|
+
"visible": {
|
|
6928
|
+
"type": "boolean",
|
|
6929
|
+
"default": true
|
|
6930
|
+
},
|
|
6931
|
+
"focusable": {
|
|
6932
|
+
"type": "boolean",
|
|
6933
|
+
"default": true
|
|
6934
|
+
},
|
|
6935
|
+
"draggable": {
|
|
6936
|
+
"type": "boolean",
|
|
6937
|
+
"default": false
|
|
6938
|
+
},
|
|
6939
|
+
"dropTarget": {
|
|
6940
|
+
"type": "boolean",
|
|
6941
|
+
"default": false
|
|
6942
|
+
},
|
|
6943
|
+
"label": {
|
|
6944
|
+
"type": "string",
|
|
6945
|
+
"default": ""
|
|
6946
|
+
},
|
|
6947
|
+
"value": {
|
|
6948
|
+
"type": "boolean",
|
|
6949
|
+
"default": false
|
|
6950
|
+
}
|
|
6951
|
+
},
|
|
6952
|
+
"additionalProperties": false
|
|
6953
|
+
},
|
|
6954
|
+
"script": {
|
|
6955
|
+
"type": "object",
|
|
6956
|
+
"properties": {
|
|
6957
|
+
"name": {
|
|
6958
|
+
"type": "string"
|
|
6959
|
+
},
|
|
6960
|
+
"props": {
|
|
6961
|
+
"type": "object"
|
|
6962
|
+
}
|
|
6963
|
+
},
|
|
6964
|
+
"required": ["name"]
|
|
6965
|
+
},
|
|
6966
|
+
"network": {
|
|
6967
|
+
"type": "object"
|
|
6968
|
+
},
|
|
6969
|
+
"children": {
|
|
6970
|
+
"type": "array",
|
|
6971
|
+
"items": {
|
|
6972
|
+
"$ref": "#/$defs/node"
|
|
6973
|
+
}
|
|
6974
|
+
}
|
|
6975
|
+
},
|
|
6976
|
+
"required": ["name", "type"]
|
|
6977
|
+
},
|
|
6873
6978
|
"UiPanel": {
|
|
6874
6979
|
"type": "object",
|
|
6875
6980
|
"x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped"],
|
|
@@ -7580,6 +7685,122 @@
|
|
|
7580
7685
|
},
|
|
7581
7686
|
"required": ["name", "type"]
|
|
7582
7687
|
},
|
|
7688
|
+
"UiVolumeSlider": {
|
|
7689
|
+
"type": "object",
|
|
7690
|
+
"x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped", "changed"],
|
|
7691
|
+
"properties": {
|
|
7692
|
+
"name": {
|
|
7693
|
+
"type": "string"
|
|
7694
|
+
},
|
|
7695
|
+
"uid": {
|
|
7696
|
+
"type": "string"
|
|
7697
|
+
},
|
|
7698
|
+
"type": {
|
|
7699
|
+
"const": "UiVolumeSlider"
|
|
7700
|
+
},
|
|
7701
|
+
"groups": {
|
|
7702
|
+
"type": "array",
|
|
7703
|
+
"items": {
|
|
7704
|
+
"type": "string"
|
|
7705
|
+
}
|
|
7706
|
+
},
|
|
7707
|
+
"tags": {
|
|
7708
|
+
"type": "object"
|
|
7709
|
+
},
|
|
7710
|
+
"props": {
|
|
7711
|
+
"type": "object",
|
|
7712
|
+
"properties": {
|
|
7713
|
+
"anchor": {
|
|
7714
|
+
"type": "string",
|
|
7715
|
+
"enum": [
|
|
7716
|
+
"topLeft",
|
|
7717
|
+
"top",
|
|
7718
|
+
"topRight",
|
|
7719
|
+
"left",
|
|
7720
|
+
"center",
|
|
7721
|
+
"right",
|
|
7722
|
+
"bottomLeft",
|
|
7723
|
+
"bottom",
|
|
7724
|
+
"bottomRight"
|
|
7725
|
+
],
|
|
7726
|
+
"default": "topLeft"
|
|
7727
|
+
},
|
|
7728
|
+
"visible": {
|
|
7729
|
+
"type": "boolean",
|
|
7730
|
+
"default": true
|
|
7731
|
+
},
|
|
7732
|
+
"focusable": {
|
|
7733
|
+
"type": "boolean",
|
|
7734
|
+
"default": true
|
|
7735
|
+
},
|
|
7736
|
+
"draggable": {
|
|
7737
|
+
"type": "boolean",
|
|
7738
|
+
"default": false
|
|
7739
|
+
},
|
|
7740
|
+
"dropTarget": {
|
|
7741
|
+
"type": "boolean",
|
|
7742
|
+
"default": false
|
|
7743
|
+
},
|
|
7744
|
+
"label": {
|
|
7745
|
+
"type": "string",
|
|
7746
|
+
"default": ""
|
|
7747
|
+
},
|
|
7748
|
+
"value": {
|
|
7749
|
+
"type": "number",
|
|
7750
|
+
"default": 0.5
|
|
7751
|
+
},
|
|
7752
|
+
"min": {
|
|
7753
|
+
"type": "number",
|
|
7754
|
+
"default": 0
|
|
7755
|
+
},
|
|
7756
|
+
"max": {
|
|
7757
|
+
"type": "number",
|
|
7758
|
+
"default": 1
|
|
7759
|
+
},
|
|
7760
|
+
"step": {
|
|
7761
|
+
"type": "number",
|
|
7762
|
+
"default": 0.01
|
|
7763
|
+
},
|
|
7764
|
+
"width": {
|
|
7765
|
+
"type": "number",
|
|
7766
|
+
"default": 180
|
|
7767
|
+
},
|
|
7768
|
+
"color": {
|
|
7769
|
+
"type": "string",
|
|
7770
|
+
"default": "#6ee7dc"
|
|
7771
|
+
},
|
|
7772
|
+
"bus": {
|
|
7773
|
+
"type": "string",
|
|
7774
|
+
"enum": ["master", "sfx", "music"],
|
|
7775
|
+
"default": "master"
|
|
7776
|
+
}
|
|
7777
|
+
},
|
|
7778
|
+
"additionalProperties": false
|
|
7779
|
+
},
|
|
7780
|
+
"script": {
|
|
7781
|
+
"type": "object",
|
|
7782
|
+
"properties": {
|
|
7783
|
+
"name": {
|
|
7784
|
+
"type": "string"
|
|
7785
|
+
},
|
|
7786
|
+
"props": {
|
|
7787
|
+
"type": "object"
|
|
7788
|
+
}
|
|
7789
|
+
},
|
|
7790
|
+
"required": ["name"]
|
|
7791
|
+
},
|
|
7792
|
+
"network": {
|
|
7793
|
+
"type": "object"
|
|
7794
|
+
},
|
|
7795
|
+
"children": {
|
|
7796
|
+
"type": "array",
|
|
7797
|
+
"items": {
|
|
7798
|
+
"$ref": "#/$defs/node"
|
|
7799
|
+
}
|
|
7800
|
+
}
|
|
7801
|
+
},
|
|
7802
|
+
"required": ["name", "type"]
|
|
7803
|
+
},
|
|
7583
7804
|
"VoxelGrid3D": {
|
|
7584
7805
|
"type": "object",
|
|
7585
7806
|
"x-signals": ["blocksChanged"],
|
package/skills/incanto-assets.md
CHANGED
|
@@ -146,6 +146,47 @@ Swap in real art later by changing ONLY the `assets{}` entry — node props
|
|
|
146
146
|
stay untouched.
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
## Art you named and never copied
|
|
150
|
+
|
|
151
|
+
`bunx incanto-check` warns when a scene asset points at a local file the project
|
|
152
|
+
does not contain:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
ok src/game.scene.json
|
|
156
|
+
warn: $hero → assets/hero.png is not in the project (looked in public/,
|
|
157
|
+
./, and beside the scene). It will draw nothing.
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
That used to be a browser-only failure — visible in `assetErrors()`, which needs
|
|
161
|
+
a running game and someone to look at it — and it is the single most common way
|
|
162
|
+
a scene draws nothing. Remote (`https:`) and inline (`data:`) urls are left
|
|
163
|
+
alone: the command cannot know, and guessing would be worse than the bug.
|
|
164
|
+
|
|
165
|
+
## A spritesheet grid that does not fit says so
|
|
166
|
+
|
|
167
|
+
`frameWidth`/`frameHeight` are the two numbers nothing could check for you, and
|
|
168
|
+
getting them wrong does not fail — it draws the wrong art. A size that does not
|
|
169
|
+
divide the sheet slices every row a little further off centre; an animation
|
|
170
|
+
naming a frame past the end of the grid reads whatever is at the wrong end of
|
|
171
|
+
the image.
|
|
172
|
+
|
|
173
|
+
Both are reported now, once, the first time the sprite draws:
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
[incanto] spritesheet 'hero' does not fit its frame size: 1344 px wide is not a
|
|
177
|
+
whole number of 100 px frames (672, 448, 336, 224, 192, 112 would divide it).
|
|
178
|
+
Every row after the first is cut off centre.
|
|
179
|
+
|
|
180
|
+
[incanto] spritesheet 'hero' has 35 frames (0–34), and an animation asks for
|
|
181
|
+
frame 40. Those frames draw whatever is at the wrong end of the sheet.
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The suggested sizes are the ones that actually divide your image, so the fix is
|
|
185
|
+
usually in the message. `bunx incanto-assets info <name>` prints the real frame
|
|
186
|
+
size for a built-in, and the editor's 📚 picker fills it in from the catalog's
|
|
187
|
+
metadata — or leaves it BLANK when the catalog does not carry it, because an
|
|
188
|
+
invented frame size is exactly this bug.
|
|
189
|
+
|
|
149
190
|
## Textures are shared automatically (3D)
|
|
150
191
|
|
|
151
192
|
A hundred `Sprite3D`s pointing at one atlas cost **one** fetch, one decode and
|
|
@@ -162,9 +203,11 @@ Two consequences worth knowing:
|
|
|
162
203
|
- **`pixelArt: true` also turns mipmaps off**, which is what makes pixel art stay
|
|
163
204
|
crisp at distance instead of blurring into mud.
|
|
164
205
|
|
|
165
|
-
A texture that 404s
|
|
206
|
+
A texture that 404s shows up in `game.assetErrors()` alongside models, by the
|
|
166
207
|
URL you wrote — so "why is my sprite invisible" is answerable without opening the
|
|
167
|
-
network tab.
|
|
208
|
+
network tab. **That includes vegetation**: a `Tree3D` leaf or bark URL that fails
|
|
209
|
+
used to leave a grove of bare branches and say nothing anywhere, because that
|
|
210
|
+
node keeps its own texture cache. In **2D** the same question is `renderer.assets.errors()`
|
|
168
211
|
(`$ref`, url and reason per failed entry), and the scene EDITOR reads it: a
|
|
169
212
|
failed asset is red in the explorer with the url in its tooltip and the
|
|
170
213
|
consequence in its inspector.
|
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
|
|
|
@@ -274,6 +291,41 @@ solid core carries the colour). `rate: 0` +
|
|
|
274
291
|
{ "signal": "finished", "from": "Boom", "to": ".", "handler": "onBoomDone" }
|
|
275
292
|
```
|
|
276
293
|
|
|
294
|
+
### Where a particle LIVES: `worldSpace`
|
|
295
|
+
|
|
296
|
+
A particle is born in the emitter's LOCAL space by default, so the whole plume
|
|
297
|
+
moves with the node — right for a torch on a moving platform, wrong for almost
|
|
298
|
+
everything else:
|
|
299
|
+
|
|
300
|
+
- dust under a running player is GLUED to the player instead of left behind;
|
|
301
|
+
- and the standard way to place a one-shot — move the emitter, `replay()` —
|
|
302
|
+
drags the PREVIOUS burst across the level with it.
|
|
303
|
+
|
|
304
|
+
`"worldSpace": true` makes a particle keep the place it was born:
|
|
305
|
+
|
|
306
|
+
```jsonc
|
|
307
|
+
{ "name": "Dust", "type": "Particles2D",
|
|
308
|
+
"props": { "preset": "smoke", "worldSpace": true } }
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
It follows a moving PARENT too, not just the node's own position. Default is
|
|
312
|
+
`false` so existing scenes look exactly as they did.
|
|
313
|
+
|
|
314
|
+
### Armed, but not yet: `emitting: false`
|
|
315
|
+
|
|
316
|
+
A one-shot with `burst` fires on its FIRST FRAME — which is what you want for a
|
|
317
|
+
firework that is meant to go off as the level opens, and not at all what you
|
|
318
|
+
want for the twenty explosions authored on twenty destructible crates. Set
|
|
319
|
+
`"emitting": false` to arm one and hold it:
|
|
320
|
+
|
|
321
|
+
```jsonc
|
|
322
|
+
{ "name": "Boom", "type": "Particles2D",
|
|
323
|
+
"props": { "preset": "explosion", "rate": 0, "burst": 60, "emitting": false } }
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
`node.replay()` fires it — that is the game asking, so `emitting: false` does
|
|
327
|
+
not hold it back — and re-arms it for the next time.
|
|
328
|
+
|
|
277
329
|
`node.replay()` re-arms a one-shot. Simulation is deterministic under the
|
|
278
330
|
engine seed — runScript verifies particle gameplay reproducibly.
|
|
279
331
|
|
|
@@ -463,7 +463,9 @@ block.
|
|
|
463
463
|
## Particles
|
|
464
464
|
|
|
465
465
|
`Particles3D` mirrors `Particles2D` (same presets — `fire`, `explosion`,
|
|
466
|
-
`magic`…
|
|
466
|
+
`magic`… the same one-shot `rate: 0` + `burst` → `finished` pattern, and the
|
|
467
|
+
same `worldSpace` prop, which is what stops a moved one-shot dragging the
|
|
468
|
+
previous explosion across the level) as
|
|
467
469
|
point sprites in meters: preset distances auto-scale ÷100 (presets are authored
|
|
468
470
|
in 2D px). See incanto-building-2d-games for the full prop list.
|
|
469
471
|
|
|
@@ -1042,7 +1042,9 @@ emitter only when you want a specific custom burst:
|
|
|
1042
1042
|
|
|
1043
1043
|
(`replay` re-arms the one-shot burst; a small behavior can also move the
|
|
1044
1044
|
emitter to the body first — the signal hands you the node:
|
|
1045
|
-
`onEntered(body) { splash.position = body.position; splash.replay(); }
|
|
1045
|
+
`onEntered(body) { splash.position = body.position; splash.replay(); }` — give
|
|
1046
|
+
that emitter `"worldSpace": true`, or the second splash drags the first one to
|
|
1047
|
+
the new spot.)
|
|
1046
1048
|
|
|
1047
1049
|
## River3D — running water
|
|
1048
1050
|
|
|
@@ -815,12 +815,59 @@ import { CameraShake, Cooldown, hitStop, screenFlash } from 'incanto/gameplay';
|
|
|
815
815
|
meters). For cameras WITHOUT a follow script attach the standalone
|
|
816
816
|
`CameraShake` behavior — it composes with any other position writer and
|
|
817
817
|
returns the camera exactly to base.
|
|
818
|
-
- **screenFlash(color?, opacity?, seconds?)** — full-screen damage/
|
|
819
|
-
flash
|
|
818
|
+
- **screenFlash(engine, color?, opacity?, seconds?)** — full-screen damage/
|
|
819
|
+
pickup flash. DOM, so it draws nothing headless — but it REPORTS (below), so
|
|
820
|
+
you can still check that it fired.
|
|
820
821
|
- **hitStop(engine, seconds?)** — freezes `engine.timeScale` for REAL
|
|
821
|
-
seconds then restores; stacked calls extend. Sells melee impacts.
|
|
822
|
+
seconds then restores; stacked calls extend. Sells melee impacts. It measures
|
|
823
|
+
those seconds on `engine.unscaledTime`, so it thaws in a headless session
|
|
824
|
+
exactly as it does in a browser.
|
|
822
825
|
- **engine.timeScale** — 1 realtime, 0.5 slow motion, 0 pause: scales
|
|
823
|
-
variable AND fixed updates together (physics, timers, behaviors)
|
|
826
|
+
variable AND fixed updates together (physics, timers, behaviors) — and it
|
|
827
|
+
means the same thing headless, so a pause menu and a slow-motion finish can be
|
|
828
|
+
tested. (A stepped frame still HAPPENS while paused: `update` runs with
|
|
829
|
+
`dt = 0`, which is how the pause menu's own key polling keeps working.)
|
|
830
|
+
|
|
831
|
+
## Did the effect fire?
|
|
832
|
+
|
|
833
|
+
Sound has `engine.audio`; vision has **`engine.effects`**, and the question is
|
|
834
|
+
the same one. `framing()` says where an emitter IS — a particle system that
|
|
835
|
+
never fired and one that fired a hundred times sit at the same coordinates — and
|
|
836
|
+
shake, flash and hit-stop draw nothing at all in a run with no screen.
|
|
837
|
+
|
|
838
|
+
```ts
|
|
839
|
+
session.engine.effects.clearLog();
|
|
840
|
+
smashTheCrystal();
|
|
841
|
+
session.step(200);
|
|
842
|
+
|
|
843
|
+
session.engine.effects.countOf('explosion'); // 1
|
|
844
|
+
session.engine.effects.countFrom('/Game/Crystal/Boom'); // 1
|
|
845
|
+
session.engine.effects.recent();
|
|
846
|
+
// [{ kind: 'burst', name: 'explosion', from: '/Game/Crystal/Boom', amount: 60, at: 1.2 }]
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
- `kind` — `burst` (a one-shot fired) · `emit` (a continuous emitter STARTED —
|
|
850
|
+
once, not once per particle) · `trail` (a `Trail3D` started laying ribbon) ·
|
|
851
|
+
`shake` · `flash` · `hitstop`.
|
|
852
|
+
- `name` — the particle preset, or the flash colour.
|
|
853
|
+
- `from` — the node path, or `engine` for the screen-wide ones.
|
|
854
|
+
- `amount` — particles in the burst, shake magnitude, seconds frozen.
|
|
855
|
+
- Bounded at 200, `clearLog()` between assertions.
|
|
856
|
+
|
|
857
|
+
A `Trail3D`'s ribbon is a special case worth knowing: it is laid in the RENDER
|
|
858
|
+
pass, because each point needs the node's real world transform (a sword arc is a
|
|
859
|
+
rotating parent). So `trail.pointCount` is **0 headless** — that is the verify VM
|
|
860
|
+
working, not a broken trail — and the `trail` event above is what tells you the
|
|
861
|
+
wiring fired.
|
|
862
|
+
|
|
863
|
+
It records that the effect was ASKED FOR, not that a pixel moved — the burst
|
|
864
|
+
that never happened because the signal was never connected looks exactly like
|
|
865
|
+
the burst that happened off-screen, and this tells them apart. For "is it
|
|
866
|
+
running right now", a particle node also has **`aliveCount`**.
|
|
867
|
+
|
|
868
|
+
A shake suppressed by `reduceMotion` records NOTHING, on purpose: it did not
|
|
869
|
+
happen, and saying it did would send you hunting a camera bug that is an
|
|
870
|
+
accessibility setting.
|
|
824
871
|
|
|
825
872
|
## Game flow (win / lose / restart)
|
|
826
873
|
|
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
|