incanto 0.47.0 → 0.49.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-frame.mjs +58 -1
- package/bin/incanto-logs.mjs +127 -0
- package/bin/incanto-model.mjs +13 -1
- package/bin/incanto-playtest.mjs +7 -0
- package/bin/incanto-verify.mjs +41 -2
- package/dist/2d.d.ts +21 -12
- package/dist/2d.js +57 -3
- package/dist/3d.d.ts +38 -1
- package/dist/3d.js +114 -4
- package/dist/{create-game-CniOiWzN.js → create-game-BgV6UbVA.js} +5 -5
- package/dist/{create-game-D16MVIPO.js → create-game-DFBjMetZ.js} +83 -6
- package/dist/{duplicate-CRtihGmC.js → duplicate-CI9WF_bg.js} +1 -1
- package/dist/{environment-presets-D1b0ydTS.js → environment-presets-CZOH5TY5.js} +15 -20
- package/dist/{gameplay-BQOeAid6.js → gameplay-02Btmmjn.js} +80 -9
- package/dist/gameplay.d.ts +17 -0
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +139 -1
- package/dist/index.js +8 -7
- package/dist/{loader-r49nDwB4.js → loader-DwazzlQb.js} +36 -6
- package/dist/log-report-lxrQY9cH.js +0 -0
- package/dist/net.js +3 -3
- package/dist/{physics-2d-BmgXBNDB.js → physics-2d-vyCBfACH.js} +3 -3
- package/dist/{physics-3d-CSoGjM8P.js → physics-3d-DpRqw8Mz.js} +4 -4
- package/dist/react.js +1 -1
- package/dist/{register-R2JTnIMw.js → register-BNPZYJmd.js} +23 -22
- package/dist/{register-D651it1J.js → register-CB11yp21.js} +2 -2
- package/dist/{register-BSXV8T9F.js → register-uvaZj1KX.js} +62 -2
- package/dist/{replay-DilbZgQI.js → replay-C0XJIsO7.js} +1 -1
- package/dist/sprite-animation-CMr6f1K2.d.ts +44 -0
- package/dist/{particle-sim-Bw7hB93B.js → sprite-animation-D_p28jwU.js} +63 -1
- package/dist/{src-Ca3oV1fe.js → src-DF4gCsqO.js} +1 -1
- package/dist/{test-E4-otKqK.js → test-DRna_BQU.js} +210 -30
- package/dist/test.d.ts +38 -6
- package/dist/test.js +2 -2
- package/dist/vite.js +41 -4
- package/editor/assets/{agent8-_007gPF8.js → agent8-DCW4TgDt.js} +1 -1
- package/editor/assets/{debug-0DI_MJaq.js → debug-RC6qts6S.js} +1 -1
- package/editor/assets/{index-B-6eYZEi.js → index-5dEIhvsf.js} +92 -92
- package/editor/index.html +1 -1
- package/package.json +3 -2
- package/schemas/scene.schema.json +10 -3
- package/skills/incanto-3d-character.md +13 -2
- package/skills/incanto-3d-models.md +40 -0
- package/skills/incanto-assets.md +15 -0
- package/skills/incanto-building-2d-games.md +57 -5
- package/skills/incanto-building-3d-games.md +7 -0
- package/skills/incanto-gameplay-behaviors.md +18 -1
- package/skills/incanto-hud.md +39 -0
- package/skills/incanto-node-reference.md +5 -3
- package/skills/incanto-physics-and-input.md +1 -1
- package/skills/incanto-playtesting.md +9 -2
- package/skills/incanto-verifying-your-game.md +67 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/tps-3d/package.json +1 -1
- 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-5dEIhvsf.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="./assets/GameServer-C56iOUgF.js">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "incanto",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"description": "Vibe-coding-first web game engine SDK — JSON-driven scenes on three.js",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"game-engine",
|
|
@@ -99,6 +99,7 @@
|
|
|
99
99
|
"incanto-feel": "bin/incanto-feel.mjs",
|
|
100
100
|
"incanto-new": "bin/incanto-new.mjs",
|
|
101
101
|
"incanto-frame": "bin/incanto-frame.mjs",
|
|
102
|
-
"incanto-verify": "./bin/incanto-verify.mjs"
|
|
102
|
+
"incanto-verify": "./bin/incanto-verify.mjs",
|
|
103
|
+
"incanto-logs": "./bin/incanto-logs.mjs"
|
|
103
104
|
}
|
|
104
105
|
}
|
|
@@ -1696,7 +1696,7 @@
|
|
|
1696
1696
|
"maxItems": 2,
|
|
1697
1697
|
"default": [0, 0]
|
|
1698
1698
|
},
|
|
1699
|
-
"
|
|
1699
|
+
"stickToGround": {
|
|
1700
1700
|
"type": "boolean",
|
|
1701
1701
|
"default": true
|
|
1702
1702
|
},
|
|
@@ -1801,8 +1801,7 @@
|
|
|
1801
1801
|
"default": "default"
|
|
1802
1802
|
},
|
|
1803
1803
|
"snapToGround": {
|
|
1804
|
-
"
|
|
1805
|
-
"default": true
|
|
1804
|
+
"default": null
|
|
1806
1805
|
},
|
|
1807
1806
|
"collider": {
|
|
1808
1807
|
"type": "object",
|
|
@@ -1817,6 +1816,10 @@
|
|
|
1817
1816
|
"maxItems": 3,
|
|
1818
1817
|
"default": [0, 0, 0]
|
|
1819
1818
|
},
|
|
1819
|
+
"stickToGround": {
|
|
1820
|
+
"type": "boolean",
|
|
1821
|
+
"default": true
|
|
1822
|
+
},
|
|
1820
1823
|
"slopeLimitDeg": {
|
|
1821
1824
|
"type": "number",
|
|
1822
1825
|
"default": 45
|
|
@@ -7443,6 +7446,10 @@
|
|
|
7443
7446
|
"type": "string",
|
|
7444
7447
|
"default": ""
|
|
7445
7448
|
},
|
|
7449
|
+
"format": {
|
|
7450
|
+
"type": "string",
|
|
7451
|
+
"default": ""
|
|
7452
|
+
},
|
|
7446
7453
|
"size": {
|
|
7447
7454
|
"type": "number",
|
|
7448
7455
|
"default": 16
|
|
@@ -171,6 +171,15 @@ Map movement states straight in JSON — no behavior needed:
|
|
|
171
171
|
The controller writes `skin.animation` on every state change (crossfaded by
|
|
172
172
|
the model). The `movementStateChanged` signal still fires for extras.
|
|
173
173
|
|
|
174
|
+
**Where those clips come from.** Locomotion clips live at
|
|
175
|
+
`https://agent8-games.verse8.io/assets/3d/animations/mixamorig/<name>.glb`
|
|
176
|
+
(`idle-00`, `walk`, `run-medium`, `run-fast`, `jump`) and play on any mixamorig
|
|
177
|
+
model as they are — retargeted through the humanoid map for a VRM. You do not
|
|
178
|
+
have to assemble any of this by hand: run the character URL your asset MCP gave
|
|
179
|
+
you through `bunx incanto-model <url>` and it prints the whole thing — assets,
|
|
180
|
+
input actions, body, controller and skin — ready to paste. See
|
|
181
|
+
`incanto-3d-models.md`.
|
|
182
|
+
|
|
174
183
|
The controller also yaw-rotates the sibling at `skinPath` ('../Skin') toward
|
|
175
184
|
the move direction at `turnSpeed` rad/s (100 = instant snap) — the body
|
|
176
185
|
itself never rotates. The skin MOUNTS at 180° (facing away from the default
|
|
@@ -222,8 +231,10 @@ NPCs feel alive when they track you. One node, zero code:
|
|
|
222
231
|
|
|
223
232
|
## Facing a direction in 3D — the +Z-FORWARD rule (read before turning ANY skin)
|
|
224
233
|
|
|
225
|
-
This trips people up REPEATEDLY, so here is the one rule. agent8's `base-model`
|
|
226
|
-
|
|
234
|
+
This trips people up REPEATEDLY, so here is the one rule. agent8's `base-model`
|
|
235
|
+
(`https://agent8-games.verse8.io/assets/3d/characters/realistic%20style/base-model.glb`
|
|
236
|
+
— note the escaped space; it is the reference rig these docs measure against) and
|
|
237
|
+
any model you give `skinYawOffset: 0` are **+Z-FORWARD**: its face looks down +Z at
|
|
227
238
|
rotation 0. To turn it to face a world heading `(dx, dz)`:
|
|
228
239
|
|
|
229
240
|
```ts
|
|
@@ -8,6 +8,46 @@ description: Loading GLB/glTF and VRM models in Incanto — the ModelInstance3D
|
|
|
8
8
|
> Shipped inside the `incanto` npm package — this document always matches the
|
|
9
9
|
> installed engine version. Sibling skills live in `node_modules/incanto/skills/`.
|
|
10
10
|
|
|
11
|
+
## Where models come from
|
|
12
|
+
|
|
13
|
+
**Search, don't invent.** Model URLs come from your asset MCP (vector search
|
|
14
|
+
over the agent8 library) or from the editor's 📚 button
|
|
15
|
+
(`bunx incanto-editor --token <v8 token>`) — never from a guessed path. There is
|
|
16
|
+
no 3D model in the package's own catalog: `incanto-assets` ships 2D sprites,
|
|
17
|
+
tiles, items, foliage/terrain textures and audio, and characters are the one
|
|
18
|
+
thing you are expected to go and get.
|
|
19
|
+
|
|
20
|
+
Then run the URL through the inspector, which is where a found file becomes a
|
|
21
|
+
scene:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bunx incanto-model "https://…/base-model.glb"
|
|
25
|
+
# mixamorig rig — the 3d/animations clips play on this as they are
|
|
26
|
+
# stands 1.5 units tall
|
|
27
|
+
#
|
|
28
|
+
# paste a playable character into your scene:
|
|
29
|
+
# { "assets": {…model + idle/walk/run/fastRun/airborne clips…},
|
|
30
|
+
# "input": {…move / jump / sprint…},
|
|
31
|
+
# "node": { "type": "RigidBody3D", … Controller + Skin … } }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For a **mixamorig or VRM humanoid** that is the whole character — body,
|
|
35
|
+
controller, skin fitted to the capsule and dropped to its feet, the locomotion
|
|
36
|
+
clips declared, and the input actions without which nothing reads the keyboard.
|
|
37
|
+
Paste it and the character walks. For a prop, you get the model node.
|
|
38
|
+
|
|
39
|
+
### The locomotion clips
|
|
40
|
+
|
|
41
|
+
`https://agent8-games.verse8.io/assets/3d/animations/mixamorig/<name>.glb` —
|
|
42
|
+
`idle-00` · `walk` · `run-medium` · `run-fast` · `jump`, plus
|
|
43
|
+
`melee-attack`, `punch-00/01`, `kick-00/01/02`, `cast`, `swim`,
|
|
44
|
+
`hit-to-body`, `death-backward`, `dance-wave`, and the `rifle-`/`pistol-`
|
|
45
|
+
sets.
|
|
46
|
+
|
|
47
|
+
These are not art you go searching for — they are the rig's vocabulary. They
|
|
48
|
+
play on a mixamorig model as they are, and retarget through the humanoid map
|
|
49
|
+
onto a VRM, so they fit whatever character your asset search returned.
|
|
50
|
+
|
|
11
51
|
## ALWAYS inspect before you place
|
|
12
52
|
|
|
13
53
|
A file name tells you nothing about a model's size or contents. The package ships an
|
package/skills/incanto-assets.md
CHANGED
|
@@ -11,6 +11,13 @@ description: Where game art comes from in Incanto — the FULL built-in catalog
|
|
|
11
11
|
Resolve art in this order: ① built-ins (zero setup) → ② asset MCP servers /
|
|
12
12
|
known URLs → ③ art-free primitives. Never invent asset URLs.
|
|
13
13
|
|
|
14
|
+
**3D characters and props are step ②, always** — the built-in catalog is 2D
|
|
15
|
+
sprites, tiles, items, foliage/terrain textures and audio, with no models in it.
|
|
16
|
+
Search with your asset MCP (or the editor's 📚 library), then run the URL
|
|
17
|
+
through `bunx incanto-model <url>`: for a rigged humanoid it prints a whole
|
|
18
|
+
playable character — model, locomotion clips, body, controller, skin and input
|
|
19
|
+
— ready to paste. See `incanto-3d-models.md`.
|
|
20
|
+
|
|
14
21
|
## 1. Built-in assets (in the package)
|
|
15
22
|
|
|
16
23
|
```bash
|
|
@@ -102,6 +109,14 @@ const { asset, props } = spriteFromLibraryMeta(animJson, {
|
|
|
102
109
|
// asset → scene assets.hero; props → an AnimatedSprite2D's props. Done.
|
|
103
110
|
```
|
|
104
111
|
|
|
112
|
+
The `animations` map it returns (and the one `incanto-assets copy` prints) also
|
|
113
|
+
carries ALIASES for the movement states a character controller emits and the
|
|
114
|
+
sheet does not name — `"run": "move"`, `"fall": "idle"` — because library sheets
|
|
115
|
+
are `idle`/`move`/`attack` and no sheet ships a falling pose. That is what makes
|
|
116
|
+
the printed JSON work when you paste it next to
|
|
117
|
+
`movementStateChanged → play`; repoint any alias once you have the art. Sheets
|
|
118
|
+
with no `idle` (a spinning coin) get none: they are not characters.
|
|
119
|
+
|
|
105
120
|
For 3D files, ALWAYS inspect before placing: `bunx incanto-model file.glb`
|
|
106
121
|
(bounds, animations, rig — see incanto-3d-models).
|
|
107
122
|
|
|
@@ -288,11 +288,32 @@ bunx incanto-check # will it load? is anything lit?
|
|
|
288
288
|
printf 'step 500\nframing\nquit\n' | bunx incanto-play src/game.scene.json
|
|
289
289
|
```
|
|
290
290
|
|
|
291
|
-
`framing` tells you what the camera can actually SEE
|
|
292
|
-
|
|
293
|
-
`
|
|
294
|
-
|
|
295
|
-
|
|
291
|
+
`framing` tells you what the camera can actually SEE, in 2D's own terms: the
|
|
292
|
+
`viewport.design` window centred on the current `Camera2D` **and clamped by its
|
|
293
|
+
`limits`**, which is the view the renderer really draws — a following camera
|
|
294
|
+
sits at the player's y, and in a 544 px world that is hundreds of pixels past
|
|
295
|
+
anything a 540 px window can show.
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
camera /Game/Camera centred [480, 274] showing 960×540px
|
|
299
|
+
11 in view, 2 outside it
|
|
300
|
+
onScreen /Game/Level (TileMap2D) [0, 0] screen [0.333, -0.007] 160px
|
|
301
|
+
onScreen /Game/Gems/Gem1/Icon (Sprite2D) [512, 428] screen [0.067, 0.57] 157px
|
|
302
|
+
offscreen /Game/Goblins/Goblin2/Skin (AnimatedSprite2D) [1002, 512] … 574px
|
|
303
|
+
overlap /Game/Gems/Gem4 ∩ /Game/Level
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
`screen` is −1…1 across the window, so `1.089` means "just past the right
|
|
307
|
+
edge". A node counts as in view when its BOX is — a `ColorRect2D`'s `size`, a
|
|
308
|
+
`TileMap2D`'s whole grid (which hangs from its TOP-LEFT, not its centre), or a
|
|
309
|
+
collider. A bare `Sprite2D` declares no size, so read `offscreen` there as "its
|
|
310
|
+
ORIGIN is outside the view". Nothing is ever "behind" a 2D camera and nothing is
|
|
311
|
+
lit, so neither is reported; `overlap` is real interpenetration, not resting on
|
|
312
|
+
a platform. HUD widgets are screen-space and are left out entirely.
|
|
313
|
+
|
|
314
|
+
Then read `stats().errors` (something threw and got skipped) and `assetErrors()`
|
|
315
|
+
(a texture 404'd). All of these are silent failures otherwise — the screen just
|
|
316
|
+
looks wrong, or empty, and nothing throws.
|
|
296
317
|
|
|
297
318
|
## Platformer game feel (CharacterController2D)
|
|
298
319
|
|
|
@@ -324,6 +345,37 @@ behaves exactly as before.
|
|
|
324
345
|
character from scene JSON instead of a behavior polling velocities. The 3D
|
|
325
346
|
sibling has had this signal since it shipped.
|
|
326
347
|
|
|
348
|
+
```json
|
|
349
|
+
{ "signal": "movementStateChanged", "from": "Player/Ctl",
|
|
350
|
+
"to": "Player/Skin", "handler": "play" }
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**The sheet will not have all six clips — alias the rest.** Library sheets
|
|
354
|
+
(including every packaged character) carry `idle`/`move`/`attack`, and none of
|
|
355
|
+
them ships a falling pose. An `animations` entry may be the NAME of another
|
|
356
|
+
entry, which is what the states you have no artwork for should be:
|
|
357
|
+
|
|
358
|
+
```json
|
|
359
|
+
"animations": {
|
|
360
|
+
"idle": { "frames": [0, 5], "fps": 10, "loop": true },
|
|
361
|
+
"move": { "frames": [6, 11], "fps": 10, "loop": true },
|
|
362
|
+
"attack": { "frames": [12, 17], "fps": 10 },
|
|
363
|
+
"run": "move", "jump": "idle", "fall": "idle", "dash": "move"
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
`incanto-assets copy` and `spriteFromLibraryMeta` already print these aliases,
|
|
368
|
+
so pasting their output next to the wiring above works as-is; point any of them
|
|
369
|
+
somewhere better when you have the art. Aliases may chain (`fall` → `jump` →
|
|
370
|
+
`idle`), a circle is a hard error, and a name that resolves to nothing fails at
|
|
371
|
+
READY — not on the frame the character first jumps. A misspelt clip is still a
|
|
372
|
+
hard `UNKNOWN_ANIMATION`: an alias is a rename you wrote, not a silent fallback.
|
|
373
|
+
|
|
374
|
+
**A cosmetic clip cannot stop the character.** If a `play` wire throws anyway,
|
|
375
|
+
the engine turns THAT WIRE off and reports it — the controller keeps running.
|
|
376
|
+
Read `stats().errors`, because the game will look fine and be missing an
|
|
377
|
+
animation.
|
|
378
|
+
|
|
327
379
|
Wall moves need to know there IS a wall: `CharacterBody2D` now answers
|
|
328
380
|
`isOnWall()`, `wallSide()` (-1 left / +1 right) and `isOnCeiling()` as well as
|
|
329
381
|
`isOnFloor()`. Before, the floor was the only surface a character could report,
|
|
@@ -401,6 +401,13 @@ the same bug.
|
|
|
401
401
|
cloud). The grounding check below skips these, so mark them.
|
|
402
402
|
- omitted — `y` is used exactly as authored.
|
|
403
403
|
|
|
404
|
+
**On a `CharacterBody2D`/`3D` the ground-STICK is `stickToGround`** (default
|
|
405
|
+
true — hold the capsule against the surface while it moves). It used to be
|
|
406
|
+
called `snapToGround`, which shadowed the placement prop above: a character
|
|
407
|
+
body was silently teleported onto the terrain at load, a lift value was
|
|
408
|
+
rejected, and the only escape also turned the ground-stick off. A boolean
|
|
409
|
+
`snapToGround` on a character body is now a hard error naming both props.
|
|
410
|
+
|
|
404
411
|
Resolved at load, in the same frame the terrain is built, and it honours an
|
|
405
412
|
ancestor's yaw, so props under a turned rig land where they look.
|
|
406
413
|
|
|
@@ -90,9 +90,19 @@ dies once.
|
|
|
90
90
|
| `invulnerableFor` | `0` | seconds of immunity after a hit (i-frames; 0 = off) |
|
|
91
91
|
| `freeOnDeath` | `false` | `queueFree()` this node when it dies (clone-safe) |
|
|
92
92
|
|
|
93
|
-
Signals: `damaged(amount, current)` · `healed(amount, current)` ·
|
|
93
|
+
Signals: `damaged(amount, current)` · `healed(amount, current)` ·
|
|
94
|
+
`healthChanged(current, max)` · `died`
|
|
94
95
|
Methods: `damage(n)` · `heal(n)` · `kill()` — state: `current`, `isDead`
|
|
95
96
|
|
|
97
|
+
**`healthChanged` is the one to put on a health BAR.** `damaged`/`healed` lead
|
|
98
|
+
with the DELTA, so wiring either to `UiBar.setValue` paints the damage as the
|
|
99
|
+
health and looks like it works; `healthChanged` leads with the current value and
|
|
100
|
+
carries the max, so `{ "signal": "healthChanged", "from": "Player", "to":
|
|
101
|
+
"HUD/Hp", "handler": "setValue" }` is the whole health bar. It also fires for
|
|
102
|
+
regeneration, which the other two never do — a regen bar wired to `healed` sits
|
|
103
|
+
still while the character heals. Use `damaged`/`healed` for reacting to the HIT
|
|
104
|
+
(sfx, flash, knockback), where the amount is the point.
|
|
105
|
+
|
|
96
106
|
```json
|
|
97
107
|
{ "name": "Player", "type": "CharacterBody2D", "groups": ["player"],
|
|
98
108
|
"props": { "collider": { "shape": "capsule", "radius": 12, "height": 32 } },
|
|
@@ -162,6 +172,13 @@ arg 0) or `→ Collector.collect`.
|
|
|
162
172
|
|
|
163
173
|
Signals: `collected(value, other)`
|
|
164
174
|
|
|
175
|
+
The collector is resolved from the contacted node, its descendants, then its
|
|
176
|
+
ancestor chain — the same search `DamageOnContact` uses for `Health`. So a
|
|
177
|
+
player with a wider `Grab` sensor child still collects, and `other` is the
|
|
178
|
+
ENTITY that owns the group, not whichever part of it touched first. (It stops
|
|
179
|
+
at the entity: past a shared parent a crate standing next to the player would
|
|
180
|
+
otherwise collect the coin.)
|
|
181
|
+
|
|
165
182
|
```json
|
|
166
183
|
{ "name": "Coin", "type": "Area2D",
|
|
167
184
|
"props": { "collider": { "shape": "circle", "radius": 8 } },
|
package/skills/incanto-hud.md
CHANGED
|
@@ -27,6 +27,45 @@ headless tests (core nodes, no three.js). Widgets pick one of 9 anchors:
|
|
|
27
27
|
`topLeft top topRight left center right bottomLeft bottom bottomRight`.
|
|
28
28
|
Widgets stacked on the same anchor form a column.
|
|
29
29
|
|
|
30
|
+
## Driving widgets from the scene JSON (no TypeScript)
|
|
31
|
+
|
|
32
|
+
The widgets take the gameplay signals directly, so a score line and a health bar
|
|
33
|
+
are `connections[]` entries — not a behavior whose whole job is one assignment:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
"connections": [
|
|
37
|
+
{ "signal": "scoreChanged", "from": ".", "to": "HUD/Score", "handler": "setText" },
|
|
38
|
+
{ "signal": "healthChanged", "from": "Player", "to": "HUD/Hp", "handler": "setValue" },
|
|
39
|
+
{ "signal": "lifeLost", "from": ".", "to": "HUD/Lives", "handler": "setText" }
|
|
40
|
+
]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{ "name": "Score", "type": "UiText",
|
|
45
|
+
"props": { "anchor": "topRight", "text": "Gems 0 / 8", "format": "Gems {} / 8" } }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- **`UiText.setText(value)`** — writes the whole line, or fills the `{}` slot in
|
|
49
|
+
`format` when you set one. `format` is resolved at PAINT like `text`, so
|
|
50
|
+
`"@t:hud.gems"` works and switching locale re-reads it. Until the first value
|
|
51
|
+
arrives it shows `text`, which is why you write both (`"Gems 0 / 8"` is the
|
|
52
|
+
opening line, `"Gems {} / 8"` is the template).
|
|
53
|
+
- **`UiBar.setValue(current, max?)`** — takes the max as a second argument
|
|
54
|
+
because that is the shape `healthChanged` already has, so a raised ceiling
|
|
55
|
+
needs no second wire. `UiBar.setMax(max)` moves it alone.
|
|
56
|
+
- **`UiBanner.show(text)`** — already a method, already wireable.
|
|
57
|
+
|
|
58
|
+
**Wire `healthChanged`, not `damaged`.** `damaged(amount, current)` and
|
|
59
|
+
`healed(amount, current)` lead with the DELTA — wire either to `setValue` and
|
|
60
|
+
the bar paints the damage as the health and looks like it works. `setValue`
|
|
61
|
+
refuses a non-number and names the right signal in the message, but a plausible
|
|
62
|
+
wrong NUMBER is the trap worth knowing. `healthChanged(current, max)` also fires
|
|
63
|
+
for regeneration, which the other two never do.
|
|
64
|
+
|
|
65
|
+
Which signal carries what, at a glance: `ScoreKeeper.scoreChanged(score)` ·
|
|
66
|
+
`ScoreKeeper.lifeLost(lives)` · `Collector.totalChanged(total)` ·
|
|
67
|
+
`Health.healthChanged(current, max)` — all value-first, all wireable as-is.
|
|
68
|
+
|
|
30
69
|
## Driving widgets from behaviors
|
|
31
70
|
|
|
32
71
|
Plain node access — same as everything else:
|
|
@@ -214,7 +214,7 @@ Signals: `finished`
|
|
|
214
214
|
| `visible` | `true` | boolean |
|
|
215
215
|
| `collider` | `{}` | object |
|
|
216
216
|
| `velocity` | `[0,0]` | array |
|
|
217
|
-
| `
|
|
217
|
+
| `stickToGround` | `true` | boolean |
|
|
218
218
|
| `slopeLimitDeg` | `45` | number |
|
|
219
219
|
|
|
220
220
|
Signals: `triggerEnter(other)` · `triggerExit(other)`
|
|
@@ -230,9 +230,10 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
|
|
|
230
230
|
| `visible` | `true` | boolean |
|
|
231
231
|
| `renderOrder` | `0` | number |
|
|
232
232
|
| `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
|
|
233
|
-
| `snapToGround` | `
|
|
233
|
+
| `snapToGround` | `null` | null |
|
|
234
234
|
| `collider` | `{}` | object |
|
|
235
235
|
| `velocity` | `[0,0,0]` | array |
|
|
236
|
+
| `stickToGround` | `true` | boolean |
|
|
236
237
|
| `slopeLimitDeg` | `45` | number |
|
|
237
238
|
|
|
238
239
|
Signals: `triggerEnter(other)` · `triggerExit(other)`
|
|
@@ -1133,6 +1134,7 @@ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `change
|
|
|
1133
1134
|
| `draggable` | `false` | boolean |
|
|
1134
1135
|
| `dropTarget` | `false` | boolean |
|
|
1135
1136
|
| `text` | `""` | string |
|
|
1137
|
+
| `format` | `""` | string |
|
|
1136
1138
|
| `size` | `16` | number |
|
|
1137
1139
|
| `color` | `"#ffffff"` | string |
|
|
1138
1140
|
| `shadow` | `true` | boolean |
|
|
@@ -1340,7 +1342,7 @@ Signals: `flowChanged`
|
|
|
1340
1342
|
| `invulnerableFor` | `0` | number |
|
|
1341
1343
|
| `freeOnDeath` | `false` | boolean |
|
|
1342
1344
|
|
|
1343
|
-
Signals: `damaged(amount, current)` · `healed(amount, current)` · `died`
|
|
1345
|
+
Signals: `damaged(amount, current)` · `healed(amount, current)` · `died` · `healthChanged`
|
|
1344
1346
|
|
|
1345
1347
|
### `Interactable`
|
|
1346
1348
|
|
|
@@ -76,7 +76,7 @@ mesh or one model and warns when the body carries more.
|
|
|
76
76
|
Areas overlapping OTHER Areas fire too (e.g. a weapon-hitbox Area over an
|
|
77
77
|
enemy-hitbox Area) — neither side needs to be a Body.
|
|
78
78
|
- **`CharacterBody2D/3D`** — kinematic character (Rapier KCC). Props: `collider`
|
|
79
|
-
(capsule recommended), `velocity`, `
|
|
79
|
+
(capsule recommended), `velocity`, `stickToGround true`, `slopeLimitDeg 45`.
|
|
80
80
|
API: `moveAndSlide()` (call from `fixedUpdate`), `isOnFloor()`.
|
|
81
81
|
|
|
82
82
|
Physics simulates WORLD positions: bodies under offset parents work (offsets compose),
|
|
@@ -48,16 +48,23 @@ two.
|
|
|
48
48
|
|---|---|
|
|
49
49
|
| `reached "won"` | `GameFlow` state `'won'`, or a `won` signal (e.g. `ScoreKeeper`) |
|
|
50
50
|
| `lost` | `GameFlow` `'gameover'`, a `lost` signal, or the player's `Health.died` |
|
|
51
|
-
| `fell` | the player
|
|
51
|
+
| `fell` | the player left the world past `--fall-below` (default: 50 m under the spawn in 3D, 1000 px under it in 2D) |
|
|
52
52
|
| `error` | `stats().errors` went above zero — a behavior threw |
|
|
53
53
|
| `stuck` | none of the above before the clock ran out |
|
|
54
|
-
| `never reached` | destinations the bot never came within 2
|
|
54
|
+
| `never reached` | destinations the bot never came within `--reach-radius` of (default 2 m in 3D, 32 px in 2D) |
|
|
55
55
|
| `never fired` | signals a `connections[]` entry listens to that never happened |
|
|
56
56
|
| `danger` | how many times the player's `Health` emitted `damaged` |
|
|
57
57
|
|
|
58
58
|
**Nothing here is a guess about your design.** The scene already declares what
|
|
59
59
|
winning is; this only listens.
|
|
60
60
|
|
|
61
|
+
**2D and 3D are read in their own units.** A 2D scene is pixels with **+y
|
|
62
|
+
DOWN**, so "left the world" means y grew past the line and a jump is not a fall;
|
|
63
|
+
a 3D scene is metres with +y up. Both defaults scale to the character —
|
|
64
|
+
32 px next to a ~34 px sprite is the same reach as 2 m next to a 1.8 m rig.
|
|
65
|
+
Set `--fall-below` / `--reach-radius` yourself only when your level says
|
|
66
|
+
otherwise (a very tall 2D shaft, a tiny 3D prop).
|
|
67
|
+
|
|
61
68
|
## Destinations are what YOU marked
|
|
62
69
|
|
|
63
70
|
A node counts as somewhere to go when it **runs a behavior**, **carries a
|
|
@@ -252,6 +252,13 @@ Also `incanto-play`'s `framing` command, and `describeFraming(scene)` /
|
|
|
252
252
|
as on screen when its `size` box (or collider) is, not only its origin — so a
|
|
253
253
|
200 m island whose origin sits off to the left is correctly `onScreen`.
|
|
254
254
|
|
|
255
|
+
**A 2D scene gets a 2D report**, in pixels: the `viewport.design` window centred
|
|
256
|
+
on the current `Camera2D` and CLAMPED by its `limits`, which is the view the
|
|
257
|
+
renderer actually draws. Nothing is ever `behind` a 2D camera and nothing is
|
|
258
|
+
lit, so those two lines are absent rather than answered — read
|
|
259
|
+
`report.dimension` if you consume the structured form. A 2D scene with no
|
|
260
|
+
camera is not a failure: the documented view is `(0,0)`–`(design)`.
|
|
261
|
+
|
|
255
262
|
**For actual pixels**, turn on `preserveDrawingBuffer` — WebGL clears the buffer
|
|
256
263
|
the instant a frame composites, so `canvas.toDataURL()` and every screenshot
|
|
257
264
|
built on it come back BLANK without it:
|
|
@@ -278,6 +285,13 @@ stopped. It no longer can.
|
|
|
278
285
|
behavior attached to a `CharacterController3D` stops that behavior — the
|
|
279
286
|
character still walks and the camera still turns. Only a throw from the node's
|
|
280
287
|
own update quarantines the node.
|
|
288
|
+
- **A `connections[]` handler that throws takes down THAT WIRE, and nothing
|
|
289
|
+
else.** The handler runs inside the emit, so its exception used to surface in
|
|
290
|
+
whichever node emitted the signal — a `movementStateChanged → play` wire
|
|
291
|
+
naming a clip the sheet does not have switched off the character controller,
|
|
292
|
+
and the player stopped walking because an animation was missing. The wire is
|
|
293
|
+
reported once (naming the handler, not the emitter), disconnected, and counted
|
|
294
|
+
in `stats().errors`; every other wire on that signal keeps firing.
|
|
281
295
|
- `engine.stats().errors` counts everything swallowed to stay alive. **A game
|
|
282
296
|
that looks fine with `errors: 3` is a broken game** — read `engine.log`.
|
|
283
297
|
- `engine.erroredNodes()` lists them; `engine.resumeErroredNodes()` puts them
|
|
@@ -348,6 +362,32 @@ ceiling — drive `engine.timeScale`, with `0.25× 0.5× 1× 2×` presets:
|
|
|
348
362
|
number is refused and negatives clamp to 0: the scale multiplies every dt in
|
|
349
363
|
the engine, so one NaN would take physics, timers and animation with it.
|
|
350
364
|
|
|
365
|
+
### `bunx incanto-logs` — what the game is SAYING
|
|
366
|
+
|
|
367
|
+
`incanto-frame` gets the pixels out of the browser; this gets the words. The
|
|
368
|
+
engine writes diagnostics for exactly this — and until they had a channel out,
|
|
369
|
+
they reached a human only, in the debug overlay's logs panel.
|
|
370
|
+
|
|
371
|
+
```
|
|
372
|
+
$ bunx incanto-logs
|
|
373
|
+
asset $characters/base never loaded: … responded with 404
|
|
374
|
+
[error] incanto: failed to load model '…/NO-SUCH-MODEL.glb'
|
|
375
|
+
82 nodes · frame rate not measurable — the page is hidden (a window covered by
|
|
376
|
+
another counts), and the browser throttles it. Bring it to the front
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
That game's grass, water and trees all rendered — `incanto-frame` called it
|
|
380
|
+
healthy (`subject fills 84.2%`) because they ARE there. Only the player was
|
|
381
|
+
missing. **Pixels and words answer different questions; a game can pass one and
|
|
382
|
+
fail the other**, which is why both are rungs.
|
|
383
|
+
|
|
384
|
+
It needs no render, so it answers from a page the browser has stopped drawing.
|
|
385
|
+
Exit 1 when something is wrong: an error logged, an error swallowed to keep the
|
|
386
|
+
game alive, or an asset that never loaded.
|
|
387
|
+
|
|
388
|
+
A repeated warning is folded into one line with a count (`×600`) — a game
|
|
389
|
+
shouting the same thing every frame has one problem, not six hundred.
|
|
390
|
+
|
|
351
391
|
### `bunx incanto-frame` — what is actually on screen
|
|
352
392
|
|
|
353
393
|
The rung above framing. `framing` PREDICTS what the camera should see from the
|
|
@@ -386,6 +426,33 @@ browser, not in the dev server.** You do not have to tell it where anything is
|
|
|
386
426
|
it reads the ports that are actually listening, asks each one whether it is an
|
|
387
427
|
incanto dev server, and asks that page for a frame over vite's own HMR channel.
|
|
388
428
|
|
|
429
|
+
#### Drive it somewhere first
|
|
430
|
+
|
|
431
|
+
Every interesting state in a game is downstream of input, and the boot screen is
|
|
432
|
+
the only frame anything could capture before this:
|
|
433
|
+
|
|
434
|
+
```
|
|
435
|
+
$ bunx incanto-frame --do "vector move 0 1; step 3000; vector move 0 0" --out bridge.png
|
|
436
|
+
frame 1280×720 · luminance 0.68
|
|
437
|
+
subject fills 21.4% of frame
|
|
438
|
+
drove 180 frames before capturing
|
|
439
|
+
wrote bridge.png — open it, or read it
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
The same words `incanto-play` takes — `press`/`release ACTION`, `vector ACTION X Y`,
|
|
443
|
+
`key CODE down|up`, `pointer DX DY`, `step MS`, separated by semicolons or
|
|
444
|
+
newlines. `--do-file` reads a longer plan from a file. One vocabulary for the
|
|
445
|
+
headless gamepad and the live one.
|
|
446
|
+
|
|
447
|
+
`step` waits on the REAL clock: this is the running game, with its own loop, its
|
|
448
|
+
own physics and its own animation, and stepping it by hand would fight all three.
|
|
449
|
+
|
|
450
|
+
**`drove N frames` is the number to check.** A browser stops animating a tab it
|
|
451
|
+
considers hidden — including a window merely covered by another — and the inputs
|
|
452
|
+
then land on a game that never advances: the capture comes back UNCHANGED and
|
|
453
|
+
looks perfectly healthy. `drove 0 frames` is refused with an error rather than
|
|
454
|
+
handed over as a picture.
|
|
455
|
+
|
|
389
456
|
#### Look at it
|
|
390
457
|
|
|
391
458
|
```
|