incanto 0.58.0 → 0.59.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-multiplay.mjs +170 -0
- package/bin/incanto-verify.mjs +68 -4
- package/bin/incanto.mjs +1 -0
- package/dist/2d.d.ts +8 -2
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +9 -3
- package/dist/3d.js +4 -4
- package/dist/{behavior-l08AEbq9.d.ts → behavior-DWKTUzKI.d.ts} +10 -0
- package/dist/{create-game-D10bU5_J.js → create-game-BiW8Men_.js} +6 -6
- package/dist/{create-game-BZwWJIns.js → create-game-CHDLDQsQ.js} +6 -6
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +2 -2
- package/dist/{duplicate-BPLZDZpd.js → duplicate-DJQd44CD.js} +1 -1
- package/dist/{environment-presets-CvvQr_bJ.js → environment-presets-DRAz5EV9.js} +12 -10
- package/dist/{gameplay-BVphcxmE.js → gameplay-BBEjPFsR.js} +62 -34
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +36 -8
- package/dist/index.js +8 -8
- package/dist/{json-BLk7H2Qa.js → json-CwwhxQgb.js} +7 -1
- package/dist/{loader-BcrRSjxB.js → loader-D8n7TU8W.js} +142 -5
- package/dist/{loader-BbEMTuWg.d.ts → loader-TvkRFbyL.d.ts} +1 -1
- package/dist/net.d.ts +2 -2
- package/dist/net.js +1 -1
- package/dist/{pathfinding-mEN4V1CU.d.ts → pathfinding-BqWBb0kh.d.ts} +1 -1
- package/dist/{physics-2d-EqA-jddf.js → physics-2d-BaRSRrrZ.js} +12 -2
- package/dist/{physics-3d-Dnz4fsXX.js → physics-3d-CYxjh-HW.js} +13 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.js +2 -2
- package/dist/{register-C6ZBFRjd.js → register-BpFcgdcL.js} +57 -27
- package/dist/{register-Ch70uByv.js → register-CDrAQqPp.js} +90 -41
- package/dist/{registry-C7u42TID.js → registry-WWcQcfMr.js} +1 -1
- package/dist/{replay-s7I2GstT.js → replay-CEPyQtF_.js} +31 -7
- package/dist/{replay-Dw6gMlYA.d.ts → replay-O-yAGM76.d.ts} +1 -1
- package/dist/{split-screen-B0baBwxI.d.ts → split-screen-BQ3tAsf-.d.ts} +38 -1
- package/dist/{split-screen-DLsUrleX.js → split-screen-DDMZutQ6.js} +56 -13
- package/dist/{sprite-animation-C0wXLBZJ.js → sprite-animation-CY-mrr1L.js} +1 -1
- package/dist/{src-C1J09Op6.js → src-CY21B462.js} +1 -1
- package/dist/{teardown-Cs113S9F.js → teardown-RApWnM1G.js} +1 -1
- package/dist/{test-ZBga8kQ9.js → test-DHYuFyAu.js} +260 -25
- package/dist/test.d.ts +103 -5
- package/dist/test.js +3 -3
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-BQQjE9UQ.js → agent8-BoRGtVxK.js} +1 -1
- package/editor/assets/{debug-C9UCsXBS.js → debug-CzdyCg75.js} +1 -1
- package/editor/assets/{index-CBgfM3WD.js → index-VesuVEhe.js} +91 -91
- package/editor/index.html +1 -1
- package/package.json +2 -1
- package/schemas/scene.schema.json +1174 -70
- package/skills/incanto-localization.md +40 -8
- package/skills/incanto-multiplayer.md +57 -4
- package/skills/incanto-node-reference.md +18 -0
- package/skills/incanto-physics-and-input.md +26 -0
- package/skills/incanto-verifying-your-game.md +22 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/package.json +1 -1
|
@@ -65,12 +65,29 @@ the `"$assetKey"` references you already write:
|
|
|
65
65
|
|
|
66
66
|
```json
|
|
67
67
|
{ "name": "Start", "type": "UiButton", "props": { "text": "@t:menu.start" } }
|
|
68
|
-
{ "name": "Wave", "type": "UiText", "props": { "
|
|
68
|
+
{ "name": "Wave", "type": "UiText", "props": { "format": "@t:hud.wave" } }
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
Works on every text-bearing widget: `UiText.text
|
|
72
|
-
`UiBar.label`, `UiSelect.label
|
|
73
|
-
speakers and
|
|
71
|
+
Works on every text-bearing widget: `UiText.text` and `.format`,
|
|
72
|
+
`UiButton.text`, `UiBar.label`, `UiSelect.label` **and `.options`**,
|
|
73
|
+
`UiBanner.show()`, `UiDialogue` lines/speakers/choices — and `Label3D` / 2D
|
|
74
|
+
`Label`, the text that lives in the world.
|
|
75
|
+
|
|
76
|
+
**A key with a `{}` slot needs `format`, not `text`.** A prop resolves the key
|
|
77
|
+
and paints the result verbatim: `"hud.wave": "Wave {n}"` in `text` puts the
|
|
78
|
+
characters `Wave {n}` on screen. `format` is the template `setText` fills:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{ "name": "Wave", "type": "UiText", "props": { "format": "@t:hud.wave" } }
|
|
82
|
+
```
|
|
83
|
+
```ts
|
|
84
|
+
wave.setText(String(n)); // "Wave 3" — and it re-reads on a language switch
|
|
85
|
+
```
|
|
86
|
+
```json
|
|
87
|
+
"strings": { "en": { "hud.wave": "Wave {}" }, "ko": { "hud.wave": "{} 웨이브" } }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The slot moves with the language, which is the whole reason it is a slot.
|
|
74
91
|
|
|
75
92
|
From a behavior, `engine.t(key, params)`:
|
|
76
93
|
|
|
@@ -78,8 +95,9 @@ From a behavior, `engine.t(key, params)`:
|
|
|
78
95
|
banner.show(this.engine.t('hud.wave', { n: this.wave }));
|
|
79
96
|
```
|
|
80
97
|
|
|
81
|
-
`{n}` slots are filled from `params
|
|
82
|
-
|
|
98
|
+
`{n}` slots are filled from `params` — this is the ONLY path that fills a NAMED
|
|
99
|
+
slot; a scene-JSON prop has no params to fill it from. A slot with no matching
|
|
100
|
+
param is left alone rather than blanked.
|
|
83
101
|
|
|
84
102
|
## The language picker
|
|
85
103
|
|
|
@@ -96,10 +114,24 @@ because a player who cannot read the language on screen still has to find theirs
|
|
|
96
114
|
Picking one switches the game **live** and persists the choice through
|
|
97
115
|
`engine.settings`, so the next visit opens in it.
|
|
98
116
|
|
|
117
|
+
### Text that lives in the world
|
|
118
|
+
|
|
119
|
+
`Label3D` and the 2D `Label` resolve `@t:` too, and re-bake their texture when
|
|
120
|
+
the language changes — a sign over a shop door, a nameplate, a damage number.
|
|
121
|
+
The prop keeps the marker; only the painted words change.
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{ "name": "Sign", "type": "Label3D",
|
|
125
|
+
"props": { "text": "@t:sign.welcome", "height": 0.4 } }
|
|
126
|
+
```
|
|
127
|
+
|
|
99
128
|
## What NOT to localize
|
|
100
129
|
|
|
101
|
-
- **
|
|
102
|
-
|
|
130
|
+
- **Option VALUES stay identifiers — but you can still translate what is shown.**
|
|
131
|
+
`UiSelect.options` are what the game compares against, and `@t:` in an option
|
|
132
|
+
changes only the words on screen: the value `changed` emits is exactly what
|
|
133
|
+
you wrote. So `"options": "@t:diff.easy,@t:diff.hard"` displays 쉬움/어려움
|
|
134
|
+
and still hands your handler `@t:diff.easy`.
|
|
103
135
|
- **Node names, group names, asset keys, signal names, action names.** These are
|
|
104
136
|
identifiers.
|
|
105
137
|
- **Anything a behavior parses.** If code does `if (value === 'start')`, that
|
|
@@ -49,9 +49,32 @@ Authority rules:
|
|
|
49
49
|
] }
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
**The `network` block is validated at LOAD.** It used to be the one node-level
|
|
53
|
+
block the loader cloned without looking at, and its typos are invisible at
|
|
54
|
+
runtime — a string `sync`, a capitalised `mode`, the plural `syncs` each mean
|
|
55
|
+
"nothing replicates and nothing says so", and produce a report identical to a
|
|
56
|
+
working game's. All three are hard `BAD_FORMAT` errors now, and
|
|
57
|
+
`bunx incanto check` catches them before you ever open a browser:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
[BAD_FORMAT] "network.sync" must be an ARRAY of prop names, not "position"
|
|
61
|
+
— write ["position"] (on 'Player')
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Valid keys: `mode` (`owner` | `observer`), `sync` (array of prop names),
|
|
65
|
+
`throttleMs` (number). An `owner` with an empty `sync` is an error too — it is
|
|
66
|
+
a half-finished edit that behaves exactly like a broken one.
|
|
67
|
+
|
|
52
68
|
- **ONE owner node per player.** Its `sync` keys are relative to ITSELF
|
|
53
|
-
(`position` = own prop, `Skin.animation` = child path + prop).
|
|
54
|
-
|
|
69
|
+
(`position` = own prop, `Skin.animation` = child path + prop). A change to ANY
|
|
70
|
+
of them sends ALL of them, once per throttle window — the payload is the whole
|
|
71
|
+
set, deliberately: backends shallow-merge one level down, so a partial patch
|
|
72
|
+
would REPLACE the stored `sync` object and erase every key that had stopped
|
|
73
|
+
changing (a team colour, a skin, a name). Players already in the room would
|
|
74
|
+
never notice — they applied it once and kept it — while the next joiner
|
|
75
|
+
rendered the default forever. An idle owner still sends nothing until the
|
|
76
|
+
keyframe (`keyframeMs`, default 2 s) comes round, which is also what heals a
|
|
77
|
+
send that never reached the wire. Spawned entities (bullets,
|
|
55
78
|
pickups) go through **collections**, never extra owner nodes.
|
|
56
79
|
- **`NetworkSpawner`** (register with `registerNodesNet()`): `source: "users"` spawns one
|
|
57
80
|
instance of the registered scene per OTHER account (self skipped); the flat `sync` patch
|
|
@@ -193,8 +216,38 @@ It runs the SAME class body the cloud runs: the v2 globals (`$sender`/`$global`/
|
|
|
193
216
|
**Verifying a whole match**: `playMultiplayer` from `incanto/test` runs N clients
|
|
194
217
|
against one in-memory server for a fixed number of simulated seconds and reports
|
|
195
218
|
what they ended up sharing — rooms, who saw whom, what each `NetworkSpawner`
|
|
196
|
-
materialised, per-client frame errors, the final room state
|
|
197
|
-
|
|
219
|
+
materialised, per-client frame errors, the final room state, and **whether the
|
|
220
|
+
clients hold the same VALUES**.
|
|
221
|
+
|
|
222
|
+
That last part is the one that matters and the one that used to be missing.
|
|
223
|
+
"Saw p2" only ever meant "p2 is in the room" — the kernel puts an empty entry
|
|
224
|
+
there at join — so a game whose replication was completely dead reported exactly
|
|
225
|
+
what a working one did. After the match quiesces, every key in every owner's
|
|
226
|
+
`network.sync` is read on the sender and on each other client's spawned copy and
|
|
227
|
+
compared:
|
|
228
|
+
|
|
229
|
+
| | |
|
|
230
|
+
|---|---|
|
|
231
|
+
| `missing` | that client never materialised the account at all |
|
|
232
|
+
| `absent` | the key path does not resolve on the spawned scene — a renamed child |
|
|
233
|
+
| `shape` | different array lengths; a componentwise lerp calls that "already correct" |
|
|
234
|
+
| `mismatch` | the values differ beyond the interpolation tolerance |
|
|
235
|
+
| `erased` | the live clients agree, and a LATE JOINER never got it |
|
|
236
|
+
|
|
237
|
+
`erased` is why the harness brings one more client in after everything settles.
|
|
238
|
+
Players already in the room applied a value once and kept it on their node, so
|
|
239
|
+
they agree with each other while the authoritative snapshot is already wrong —
|
|
240
|
+
a two-browser test cannot see it, and the next person to join sees the default
|
|
241
|
+
forever. Set `lateJoin: false` to skip that half; `seed` makes the whole match
|
|
242
|
+
reproducible.
|
|
243
|
+
|
|
244
|
+
**What it does not prove.** One in-memory server means no latency, no loss, no
|
|
245
|
+
reordering: this measures the apply path and the protocol shape, never the live
|
|
246
|
+
wire. A reconnect that never re-joins, a throttle that discards a payload, a
|
|
247
|
+
batch dropped on a closed socket — none of those are in reach, and the report
|
|
248
|
+
says so on its own summary line.
|
|
249
|
+
|
|
250
|
+
See `incanto-verifying-your-game.md`.
|
|
198
251
|
|
|
199
252
|
**Driving one by hand**: server calls are QUEUED and only run when the event loop
|
|
200
253
|
turns, so a synchronous frame loop enqueues a thousand ticks that never execute —
|
|
@@ -81,6 +81,7 @@ Signals: `animationFinished(name)`
|
|
|
81
81
|
| `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
|
|
82
82
|
| `visible` | `true` | boolean |
|
|
83
83
|
| `collider` | `{}` | object |
|
|
84
|
+
| `enabled` | `true` | boolean |
|
|
84
85
|
|
|
85
86
|
Signals: `triggerEnter(other)` · `triggerExit(other)`
|
|
86
87
|
|
|
@@ -97,6 +98,7 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
|
|
|
97
98
|
| `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
|
|
98
99
|
| `snapToGround` | `null` | null |
|
|
99
100
|
| `collider` | `{}` | object |
|
|
101
|
+
| `enabled` | `true` | boolean |
|
|
100
102
|
|
|
101
103
|
Signals: `triggerEnter(other)` · `triggerExit(other)`
|
|
102
104
|
|
|
@@ -213,6 +215,7 @@ Signals: `finished`
|
|
|
213
215
|
| `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
|
|
214
216
|
| `visible` | `true` | boolean |
|
|
215
217
|
| `collider` | `{}` | object |
|
|
218
|
+
| `enabled` | `true` | boolean |
|
|
216
219
|
| `velocity` | `[0,0]` | array |
|
|
217
220
|
| `stickToGround` | `true` | boolean |
|
|
218
221
|
| `slopeLimitDeg` | `45` | number |
|
|
@@ -232,6 +235,7 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
|
|
|
232
235
|
| `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
|
|
233
236
|
| `snapToGround` | `null` | null |
|
|
234
237
|
| `collider` | `{}` | object |
|
|
238
|
+
| `enabled` | `true` | boolean |
|
|
235
239
|
| `velocity` | `[0,0,0]` | array |
|
|
236
240
|
| `stickToGround` | `true` | boolean |
|
|
237
241
|
| `slopeLimitDeg` | `45` | number |
|
|
@@ -713,6 +717,7 @@ Signals: `finished`
|
|
|
713
717
|
| `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
|
|
714
718
|
| `visible` | `true` | boolean |
|
|
715
719
|
| `collider` | `{}` | object |
|
|
720
|
+
| `enabled` | `true` | boolean |
|
|
716
721
|
| `mass` | `1` | number |
|
|
717
722
|
| `gravityScale` | `1` | number |
|
|
718
723
|
| `fixedRotation` | `false` | boolean |
|
|
@@ -735,6 +740,7 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
|
|
|
735
740
|
| `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
|
|
736
741
|
| `snapToGround` | `null` | null |
|
|
737
742
|
| `collider` | `{}` | object |
|
|
743
|
+
| `enabled` | `true` | boolean |
|
|
738
744
|
| `mass` | `1` | number |
|
|
739
745
|
| `gravityScale` | `1` | number |
|
|
740
746
|
| `fixedRotation` | `false` | boolean |
|
|
@@ -828,6 +834,7 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
|
|
|
828
834
|
| `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
|
|
829
835
|
| `visible` | `true` | boolean |
|
|
830
836
|
| `collider` | `{}` | object |
|
|
837
|
+
| `enabled` | `true` | boolean |
|
|
831
838
|
|
|
832
839
|
Signals: `triggerEnter(other)` · `triggerExit(other)`
|
|
833
840
|
|
|
@@ -844,6 +851,7 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
|
|
|
844
851
|
| `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
|
|
845
852
|
| `snapToGround` | `null` | null |
|
|
846
853
|
| `collider` | `{}` | object |
|
|
854
|
+
| `enabled` | `true` | boolean |
|
|
847
855
|
|
|
848
856
|
Signals: `triggerEnter(other)` · `triggerExit(other)`
|
|
849
857
|
|
|
@@ -1377,6 +1385,16 @@ Signals: `dealtDamage(amount, target)`
|
|
|
1377
1385
|
|
|
1378
1386
|
Signals: `dayPhaseChanged`
|
|
1379
1387
|
|
|
1388
|
+
### `FloatAway`
|
|
1389
|
+
|
|
1390
|
+
| Prop | Default | Kind |
|
|
1391
|
+
|---|---|---|
|
|
1392
|
+
| `rise` | `1` | number |
|
|
1393
|
+
| `seconds` | `0.7` | number |
|
|
1394
|
+
| `hold` | `0.3` | number |
|
|
1395
|
+
| `drift` | `0` | number |
|
|
1396
|
+
| `freeOnEnd` | `true` | boolean |
|
|
1397
|
+
|
|
1380
1398
|
### `FollowCamera`
|
|
1381
1399
|
|
|
1382
1400
|
| Prop | Default | Kind |
|
|
@@ -79,6 +79,32 @@ mesh or one model and warns when the body carries more.
|
|
|
79
79
|
(capsule recommended), `velocity`, `stickToGround true`, `slopeLimitDeg 45`.
|
|
80
80
|
API: `moveAndSlide()` (call from `fixedUpdate`), `isOnFloor()`.
|
|
81
81
|
|
|
82
|
+
### `enabled` — a collider that is off for now
|
|
83
|
+
|
|
84
|
+
Every physics body takes **`enabled`** (default `true`). Off means no contacts
|
|
85
|
+
and no `triggerEnter`/`triggerExit`; the body stays, so nothing is rebuilt and
|
|
86
|
+
re-arming is one write.
|
|
87
|
+
|
|
88
|
+
```jsonc
|
|
89
|
+
// a melee hitbox: real collider, authored OFF
|
|
90
|
+
{ "name": "Sword", "type": "Area2D",
|
|
91
|
+
"props": { "collider": { "shape": "circle", "radius": 30 }, "enabled": false } }
|
|
92
|
+
```
|
|
93
|
+
```ts
|
|
94
|
+
sword.enabled = true; // the active window of the swing
|
|
95
|
+
sword.enabled = false; // …and done
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Do not arm a hitbox by swapping `collider` in and out.** Replacing the
|
|
99
|
+
collider prop tears the rigid body down and builds a new one — twice per swing —
|
|
100
|
+
and a scene authored `"collider": {}` warns `has no collider — physics skips it`
|
|
101
|
+
on every boot, which is true and unactionable and teaches its reader to ignore
|
|
102
|
+
warnings.
|
|
103
|
+
|
|
104
|
+
Good for anything that is sometimes solid: a door that opens, a platform that
|
|
105
|
+
phases, a shield that is only up while blocking, a trigger that fires once and
|
|
106
|
+
retires.
|
|
107
|
+
|
|
82
108
|
Physics simulates WORLD positions: bodies under offset parents work (offsets compose),
|
|
83
109
|
but ancestor ROTATION/SCALE are not supported for physics bodies — keep body ancestors
|
|
84
110
|
untransformed or translation-only.
|
|
@@ -34,12 +34,34 @@ $ bunx incanto verify # finds your scene AND your behaviou
|
|
|
34
34
|
✓ loads — the scene is legal and its assets resolve
|
|
35
35
|
? plays — 8 runs played without reaching a win (4 lost, 4 ran out the clock)
|
|
36
36
|
✓ feels — 5 of 7 fired — silent: /Game/Boss/Roar, /Game/Boss/Boom
|
|
37
|
+
· agrees — not run — this scene has no `multiplayer` header
|
|
37
38
|
? draws — the dev server is running on :5173, but no page answered
|
|
38
39
|
|
|
39
40
|
passes what was measured — plays, draws not measured.
|
|
40
41
|
next: nothing here is broken — a win that takes skill or a sequence is out of reach of random play. Judge it with a scripted run: `bun run verify`
|
|
41
42
|
```
|
|
42
43
|
|
|
44
|
+
### `agrees` — do two clients hold the same values?
|
|
45
|
+
|
|
46
|
+
Skipped outright unless the scene has a `multiplayer` header. When it does,
|
|
47
|
+
`incanto multiplay` runs a real match headlessly, lets it QUIESCE, then reads
|
|
48
|
+
every key in the owner's `network.sync` on the sender and on each other
|
|
49
|
+
client's spawned copy and compares them — plus one client that joins AFTER
|
|
50
|
+
everything settles, which is the only observer that reads the authoritative
|
|
51
|
+
snapshot fresh.
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
✓ agrees — 8 replicated value(s) match across clients and a late joiner
|
|
55
|
+
— over one in-memory server, not a live wire
|
|
56
|
+
✗ agrees — p2 has no `Skin.rotation` for p1 — the key path does not resolve on
|
|
57
|
+
the spawned scene (a renamed child?), or it was never sent.
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
That trailing clause is not modesty, it is the measurement's boundary: one
|
|
61
|
+
in-memory server has no latency, no loss and no reordering, so this proves the
|
|
62
|
+
apply path and says nothing about a reconnect that never re-joins or a batch
|
|
63
|
+
dropped on a closed socket.
|
|
64
|
+
|
|
43
65
|
Runs the rungs below in order and says the ONE thing to do next. Three rules it
|
|
44
66
|
encodes so you do not have to remember them:
|
|
45
67
|
|