incanto 0.51.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 +86 -3
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +9 -3
- package/dist/3d.js +4 -4
- package/dist/{behavior-rZNfzVbH.d.ts → behavior-Do0Da56m.d.ts} +108 -0
- package/dist/{create-game-CkzKZ4v5.js → create-game-9D87XaiX.js} +112 -15
- package/dist/{create-game-BNaOC3Px.js → create-game-CDJ1lVqK.js} +21 -13
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +1 -1
- package/dist/{duplicate-EybyYeyL.js → duplicate-MNLMAcbz.js} +1 -1
- package/dist/editor.js +1931 -1579
- package/dist/{environment-presets-C08pOC6H.js → environment-presets-SkGanr2s.js} +3 -3
- package/dist/{gameplay-bStgtZBV.js → gameplay-CRYw_Q-T.js} +6 -4
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +237 -5
- package/dist/index.js +9 -9
- package/dist/{loader-B2asghWa.js → loader-BTkHYrQn.js} +86 -50
- package/dist/{loader-Dkbn56KC.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 +15 -410
- package/dist/net.js +1 -822
- package/dist/{pathfinding-Bz34pvQD.d.ts → pathfinding-HNGFqGUZ.d.ts} +24 -1
- package/dist/{physics-2d-Ccq2L9R0.js → physics-2d-BhZZ-HAp.js} +2 -2
- package/dist/{physics-3d-B2c5KNYh.js → physics-3d-DmyO2oaN.js} +3 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-CGq-Hee8.js → register-BTomIiYG.js} +305 -25
- package/dist/{register-B_BaaGx-.js → register-DROK2l7J.js} +3 -3
- package/dist/{registry-IyWCGe4q.js → registry-C7u42TID.js} +23 -1
- package/dist/{replay-DIP2_as4.d.ts → replay-9Fy6C10F.d.ts} +1 -1
- package/dist/{replay-BgKxGcXH.js → replay-DYNUL4BU.js} +195 -2
- package/dist/split-screen-CaF9hO7g.js +1267 -0
- package/dist/split-screen-DNmcX1Pz.d.ts +441 -0
- package/dist/{src-C3UwzYXl.js → src-C9xyZW7M.js} +1 -1
- package/dist/{teardown-BKTCzLek.js → teardown-Bw2aeyGI.js} +32 -2
- package/dist/{test-D2gRpk5V.js → test-CJRsciFk.js} +119 -22
- package/dist/test.d.ts +60 -5
- package/dist/test.js +3 -3
- package/dist/{touch-BoNg_MnF.js → touch-BnMyy9tr.js} +4 -4
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-DbX_msaO.js → agent8-PlFHzJsh.js} +1 -1
- package/editor/assets/{debug-C-kMxdl1.js → debug-CebV7CDW.js} +1 -1
- package/editor/assets/index-CdbsY31G.js +10958 -0
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +213 -0
- package/skills/incanto-assets.md +4 -1
- package/skills/incanto-audio.md +97 -10
- package/skills/incanto-building-2d-games.md +20 -3
- package/skills/incanto-editor.md +48 -11
- package/skills/incanto-gameplay-behaviors.md +14 -5
- package/skills/incanto-hud.md +7 -0
- package/skills/incanto-multiplayer.md +39 -3
- package/skills/incanto-node-reference.md +34 -0
- package/skills/incanto-physics-and-input.md +38 -0
- package/skills/incanto-verifying-your-game.md +75 -1
- package/skills/incanto-web-integration.md +1 -0
- package/templates/agent8-server.ts +79 -2
- 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/dist/register-CLVhzWcI.js +0 -374
- package/editor/assets/index-CUc1U7wm.js +0 -10951
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-assets.md
CHANGED
|
@@ -164,4 +164,7 @@ Two consequences worth knowing:
|
|
|
164
164
|
|
|
165
165
|
A texture that 404s now shows up in `game.assetErrors()` alongside models, by the
|
|
166
166
|
URL you wrote — so "why is my sprite invisible" is answerable without opening the
|
|
167
|
-
network tab.
|
|
167
|
+
network tab. In **2D** the same question is `renderer.assets.errors()`
|
|
168
|
+
(`$ref`, url and reason per failed entry), and the scene EDITOR reads it: a
|
|
169
|
+
failed asset is red in the explorer with the url in its tooltip and the
|
|
170
|
+
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
|
|
package/skills/incanto-editor.md
CHANGED
|
@@ -39,13 +39,17 @@ The `scenes` button opens the project as a tree, not a list of paths:
|
|
|
39
39
|
- **The scene you are editing** is marked and revealed, and the cursor starts on it.
|
|
40
40
|
- **A filter box** — type any part of a path; matches show wherever they are hiding.
|
|
41
41
|
Keyboard from that box: `↑↓` move · `→` open a folder · `←` close it · `⏎` load ·
|
|
42
|
-
`Esc` close (backdrop and ✕ work too).
|
|
42
|
+
`Esc` close (backdrop and ✕ work too). The cursor always sits on a SCENE —
|
|
43
|
+
the first match while you filter, the scene you are editing when you open the
|
|
44
|
+
panel — so `⏎` loads something without arrowing first.
|
|
43
45
|
- Each row carries **when it was last written and how big it is**, which is usually
|
|
44
46
|
how you recognise the file you were just in.
|
|
45
47
|
- **create** makes a new scene at the path in the box — left empty it uses the
|
|
46
48
|
placeholder, which tracks the folder you are standing in, so a scene lands beside
|
|
47
49
|
its siblings rather than at the project root. Parent dirs are created.
|
|
48
|
-
- Loading another scene while you have unsaved EDITS asks first
|
|
50
|
+
- Loading another scene while you have unsaved EDITS asks first, and the answer
|
|
51
|
+
is not only *discard*: **save & open** writes the scene you are leaving and
|
|
52
|
+
then opens the other one.
|
|
49
53
|
|
|
50
54
|
**The same browser opens inside a running game.** When the game's dev server serves
|
|
51
55
|
the project's scenes, `☰ debug ▸ ✎ edit this scene` gives you the whole project: the
|
|
@@ -66,7 +70,12 @@ is not there. The editor still edits the scene the game booted with meanwhile.
|
|
|
66
70
|
Opens a local page (default `http://127.0.0.1:5179/`) with three panes:
|
|
67
71
|
|
|
68
72
|
- **Explorer** — two collapsible sections: **ASSETS** on top (icon rows by
|
|
69
|
-
type;
|
|
73
|
+
type; **an asset the renderer could not FETCH turns red**, with the url in its
|
|
74
|
+
tooltip and the consequence spelled out when you select it — *"failed to
|
|
75
|
+
load — every node using $fx/coin draws nothing"* — plus a banner naming all of
|
|
76
|
+
them. A 404'd texture is otherwise the one failure with no symptom: the scene
|
|
77
|
+
is structurally perfect, the tree is full, and the viewport draws nothing where
|
|
78
|
+
the art should be; keys with a `group/` prefix nest under collapsible folders (any depth),
|
|
70
79
|
each showing its recursive asset count — refs are `$group/key`; click the
|
|
71
80
|
icon for a blurb, the row to edit in the inspector; DRAG asset rows onto a
|
|
72
81
|
folder (or the section background = root) to move them — references rewrite
|
|
@@ -79,7 +88,14 @@ Opens a local page (default `http://127.0.0.1:5179/`) with three panes:
|
|
|
79
88
|
to reorder before/after. Dragging a selected row moves the whole selection.
|
|
80
89
|
Illegal drops (engine rules — e.g. a CharacterController2D outside a
|
|
81
90
|
CharacterBody2D) are ROLLED BACK entirely with the error in the banner; the
|
|
82
|
-
tree never shows a state the engine would reject.
|
|
91
|
+
tree never shows a state the engine would reject.
|
|
92
|
+
**A reparent does not MOVE anything**: the node keeps its world transform and
|
|
93
|
+
the editor rewrites the local `position`/`rotation`/`scale` to match (drop a
|
|
94
|
+
ball at `[3, 0.6, 0]` onto a crate at `[-3, 0.5, 0]` and the ball stays put,
|
|
95
|
+
holding `[6, 0.1, 0]`). Rotated and scaled parents are handled the same way,
|
|
96
|
+
and the values that are no longer needed disappear rather than being written
|
|
97
|
+
as defaults. This is the Godot/Unity/Blender behaviour, and it is what keeps a
|
|
98
|
+
tree edit from changing the picture. Right-click for
|
|
83
99
|
duplicate / rename (or double-click the name) / cut / copy / paste-as-child /
|
|
84
100
|
delete — all act on the multi-selection. **Rename REPAIRS references**: every
|
|
85
101
|
`connections[].from/to` and every node-path prop pointing at the node (or into
|
|
@@ -121,6 +137,12 @@ Opens a local page (default `http://127.0.0.1:5179/`) with three panes:
|
|
|
121
137
|
scales to 0.25 steps (the readout shows the snapped value).
|
|
122
138
|
2D additionally supports click-pick, body drag, wheel zoom-at-cursor,
|
|
123
139
|
right/middle/Shift-drag pan, Alt+wheel scale, and collider wireframes.
|
|
140
|
+
**HUD nodes are editable like anything else**: a `UILayer` subtree is posed in
|
|
141
|
+
screen space rather than world space, and the viewport now picks, outlines and
|
|
142
|
+
drags it there — click the widget where you SEE it, and its position moves 1:1
|
|
143
|
+
with the cursor whatever the world zoom is. (Picking used to look only at the
|
|
144
|
+
world pass, so a HUD could not be selected in the viewport at all, and its
|
|
145
|
+
outline was drawn wherever the game camera happened to be looking.)
|
|
124
146
|
**`F` frames the SELECTED node** (its whole subtree; a light or empty node has
|
|
125
147
|
no bounds, so the camera goes to it at a readable distance) — and the whole
|
|
126
148
|
scene when nothing is selected, which is the Maya/Unity/Unreal meaning of the
|
|
@@ -136,9 +158,25 @@ Opens a local page (default `http://127.0.0.1:5179/`) with three panes:
|
|
|
136
158
|
- **Inspector** — schema-driven from the node registry, with STRUCTURED editors for
|
|
137
159
|
the hard parts: `collider` (shape dropdown + per-shape dimensions, mirrored by
|
|
138
160
|
the wireframe), `network` (mode dropdown + sync-key chips + throttle),
|
|
139
|
-
`script`
|
|
140
|
-
|
|
141
|
-
|
|
161
|
+
`script` — **the behaviors the engine SHIPS are a dropdown**, and picking one
|
|
162
|
+
builds a form from its own prop schema (a `Health` gets `max`/`regenPerSec`/
|
|
163
|
+
`invulnerableFor`/`freeOnDeath` with their defaults; a `Patrol`'s `mode` is a
|
|
164
|
+
`loop`/`pingpong` menu), delta-only like every other field. A name that is NOT
|
|
165
|
+
built in is your game's TypeScript: it keeps the raw-JSON props box and says
|
|
166
|
+
so, with copy-paste Behavior boilerplate in its help — and `groups` (tag
|
|
167
|
+
chips). Every one has a `?` help popover with examples. Values equal to the
|
|
168
|
+
default are removed (delta-only, like the serializer).
|
|
169
|
+
**A prop that holds a NODE PATH** (`Chase.target`, `Camera2D.follow`,
|
|
170
|
+
`Spawner.prefab`, `Joint3D.target`, `skinPath`, `terrain`…) offers every node
|
|
171
|
+
in the scene as a list — `%Name` where the name is unique, the absolute path
|
|
172
|
+
where it repeats — and free text still works for the forms a list cannot
|
|
173
|
+
enumerate (`../Skin`, a path into a subtree). A value that resolves to NOTHING
|
|
174
|
+
is marked red with the reason: `'%Playerr' matches no node in this scene. The
|
|
175
|
+
scene still loads — the prop just does nothing.` That last sentence is the
|
|
176
|
+
point: unlike a connection, a dangling path prop is not a load error, so
|
|
177
|
+
nothing else would ever have told you. A behavior's path props are checked
|
|
178
|
+
exactly like the node's own, and `incanto-check` reports the same thing from
|
|
179
|
+
the file.
|
|
142
180
|
|
|
143
181
|
**3D scenes** get full camera navigation: drag orbits, right/middle/Shift-drag pans,
|
|
144
182
|
wheel zooms, `F` frames the contents, and **`0` / the `game cam` button** returns to the
|
|
@@ -353,8 +391,7 @@ get normal click-select back.
|
|
|
353
391
|
The **✦ button** beside the add-node controls opens the Generate dialog — the
|
|
354
392
|
`incanto/env` generators inside the editor, driven by the same `GENERATORS`
|
|
355
393
|
catalog as the `incanto-env` CLI and filtered to the open scene's dimension
|
|
356
|
-
(3D: arena, terrain,
|
|
357
|
-
platforms2d, maze2d, dungeon2d). The param form is built from the catalog
|
|
394
|
+
(3D: arena, terrain, maze; 2D: platforms2d, maze2d, dungeon2d). The param form is built from the catalog
|
|
358
395
|
metadata — numbers clamp to their min/max, option lists become dropdowns — so
|
|
359
396
|
a new generator needs zero editor changes. The seed starts random (↻ rerolls);
|
|
360
397
|
the same seed always generates the same level. **insert** runs the generator
|
|
@@ -395,7 +432,7 @@ trusts the surrounding network — use it only inside containers.
|
|
|
395
432
|
The EDIT view freezes game time — nothing falls or fires until you press play;
|
|
396
433
|
only ambient visuals (model animations, particles, water, foliage sway) keep
|
|
397
434
|
moving. The PLAY view simulates everything engine-native but cannot execute the
|
|
398
|
-
game's TypeScript behaviors.
|
|
399
|
-
viewport
|
|
435
|
+
game's TypeScript behaviors. Both dimensions have the W/E/R gizmos in the
|
|
436
|
+
viewport; 2D additionally has click-pick, body drag and wheel zoom-at-cursor. Runtime-injected textures (asset URLs
|
|
400
437
|
like `"GENERATED_AT_RUNTIME"`) render as a magenta checkerboard — position/size
|
|
401
438
|
stay visible; the real art appears in the running game.
|
|
@@ -58,11 +58,20 @@ Two kinds of node path resolve from DIFFERENT origins — mixing them up is the
|
|
|
58
58
|
enemy (a child of the Spawner) reaches the player with the ABSOLUTE
|
|
59
59
|
`/root/Player`, never a bare `Player` (which would look under the enemy).
|
|
60
60
|
|
|
61
|
-
Renaming a node in the editor
|
|
62
|
-
|
|
63
|
-
still
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
Renaming a node in the editor REWRITES both kinds for you (behavior props
|
|
62
|
+
included) and reports what it changed, and deleting a node lists every reference
|
|
63
|
+
that still points at it before it goes. Hand-edited JSON is covered too:
|
|
64
|
+
**`bunx incanto-check` now reports a node-path prop that leads nowhere** —
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
warn: World/Enemy: Chase.target — '%Playerr' matches no node in this scene.
|
|
68
|
+
The scene loads and the prop does nothing.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
— which is the one failure that used to be completely silent (a prop path
|
|
72
|
+
resolves with `getNodeOrNull`, so the scene opens and the enemy just never
|
|
73
|
+
chases). `Chase.target` and `FollowCamera.target` are `required`: an EMPTY one
|
|
74
|
+
is a load error naming the node, not a surprise in the browser.
|
|
66
75
|
|
|
67
76
|
**Never write `/root/<RootName>/...`.** If your root node is named `Game`, the
|
|
68
77
|
path is `/root/Player` — NOT `/root/Game/Player`: `/root/` already *is* the
|
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
|