incanto 0.61.0 → 0.63.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-verify.mjs +134 -55
- package/dist/2d.d.ts +42 -0
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +22 -0
- package/dist/3d.js +5 -5
- package/dist/{create-game-DH7JI5xx.js → create-game-lLeITaZ5.js} +8 -8
- package/dist/{create-game-IX5lEH0P.js → create-game-viBqXUoZ.js} +6 -6
- package/dist/{duplicate-CGqAmK2h.js → duplicate-BOOKmkQ7.js} +1 -1
- package/dist/editor.js +39 -18
- package/dist/{environment-presets-CNxCuhZF.js → environment-presets-QR7_75KJ.js} +5 -3
- package/dist/{gameplay-Dtzd2itW.js → gameplay-CuqoHHUB.js} +153 -16
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +92 -1
- package/dist/index.js +6 -6
- package/dist/{loader-D7jTvDQv.js → loader-zDynoew_.js} +160 -2
- package/dist/net.js +1 -1
- package/dist/{physics-2d-CBnor8Zf.js → physics-2d-CllJXlic.js} +11 -3
- package/dist/{physics-3d-BTUfUSWO.js → physics-3d-BL_pFJ19.js} +123 -17
- package/dist/react.js +1 -1
- package/dist/{register-DL3izw8j.js → register-1cKM8DEj.js} +60 -9
- package/dist/{register-xuSRyD6b.js → register-Btm7_Emq.js} +158 -2
- package/dist/{replay-D7-yle3s.js → replay-Bhdntkvq.js} +15 -2
- package/dist/{split-screen-5Ban4q4n.js → split-screen-Dx0LvzqS.js} +2 -2
- package/dist/{src-DozXvyZS.js → src-BlV9Sv4m.js} +1 -1
- package/dist/{teardown-B6rwJOyS.js → teardown-BwhkcNt8.js} +1 -1
- package/dist/{test-Dch_7VQD.js → test-Ct1_zf5S.js} +64 -20
- package/dist/test.d.ts +41 -0
- package/dist/test.js +2 -2
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-CF1JL2tR.js → agent8-CLZXBRoM.js} +1 -1
- package/editor/assets/{debug-3QzYhOPA.js → debug-eaSKFAKW.js} +1 -1
- package/editor/assets/{index-CAD2c5ug.js → index-D66AuRwG.js} +91 -91
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +91 -0
- package/skills/README.md +4 -0
- package/skills/incanto-behaviors-and-scripts.md +37 -0
- package/skills/incanto-building-2d-games.md +20 -1
- package/skills/incanto-building-3d-games.md +1 -1
- package/skills/incanto-game-feel.md +3 -2
- package/skills/incanto-gameplay-behaviors.md +45 -6
- package/skills/incanto-node-reference.md +12 -0
- package/skills/incanto-physics-and-input.md +53 -2
- package/skills/incanto-scene-json-authoring.md +15 -1
- package/skills/incanto-verifying-your-game.md +31 -1
- package/skills/incanto-your-first-game.md +237 -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/star-survivor/src/game.scene.json +4 -2
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/game.scene.json +3 -2
- package/templates-app/tps-3d/verify.ts +38 -0
- 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-D66AuRwG.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="./assets/GameServer-C56iOUgF.js">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -295,6 +295,9 @@
|
|
|
295
295
|
{
|
|
296
296
|
"$ref": "#/$defs/Particles3D"
|
|
297
297
|
},
|
|
298
|
+
{
|
|
299
|
+
"$ref": "#/$defs/Respawn"
|
|
300
|
+
},
|
|
298
301
|
{
|
|
299
302
|
"$ref": "#/$defs/RigidBody2D"
|
|
300
303
|
},
|
|
@@ -2040,6 +2043,10 @@
|
|
|
2040
2043
|
"slopeLimitDeg": {
|
|
2041
2044
|
"type": "number",
|
|
2042
2045
|
"default": 45
|
|
2046
|
+
},
|
|
2047
|
+
"stepHeight": {
|
|
2048
|
+
"type": "number",
|
|
2049
|
+
"default": 0.35
|
|
2043
2050
|
}
|
|
2044
2051
|
},
|
|
2045
2052
|
"additionalProperties": false
|
|
@@ -5327,6 +5334,90 @@
|
|
|
5327
5334
|
},
|
|
5328
5335
|
"required": ["name", "type"]
|
|
5329
5336
|
},
|
|
5337
|
+
"Respawn": {
|
|
5338
|
+
"type": "object",
|
|
5339
|
+
"x-signals": ["respawned"],
|
|
5340
|
+
"properties": {
|
|
5341
|
+
"name": {
|
|
5342
|
+
"type": "string"
|
|
5343
|
+
},
|
|
5344
|
+
"uid": {
|
|
5345
|
+
"type": "string"
|
|
5346
|
+
},
|
|
5347
|
+
"type": {
|
|
5348
|
+
"const": "Respawn"
|
|
5349
|
+
},
|
|
5350
|
+
"groups": {
|
|
5351
|
+
"type": "array",
|
|
5352
|
+
"items": {
|
|
5353
|
+
"type": "string"
|
|
5354
|
+
}
|
|
5355
|
+
},
|
|
5356
|
+
"tags": {
|
|
5357
|
+
"type": "object"
|
|
5358
|
+
},
|
|
5359
|
+
"props": {
|
|
5360
|
+
"type": "object",
|
|
5361
|
+
"properties": {
|
|
5362
|
+
"target": {
|
|
5363
|
+
"type": "string",
|
|
5364
|
+
"description": "A node path: '%UniqueName', '/Absolute/From/Root', or relative to this node ('../Skin').",
|
|
5365
|
+
"default": ".."
|
|
5366
|
+
},
|
|
5367
|
+
"below": {
|
|
5368
|
+
"default": null
|
|
5369
|
+
},
|
|
5370
|
+
"to": {
|
|
5371
|
+
"type": "array",
|
|
5372
|
+
"default": []
|
|
5373
|
+
},
|
|
5374
|
+
"resetVelocity": {
|
|
5375
|
+
"type": "boolean",
|
|
5376
|
+
"default": true
|
|
5377
|
+
}
|
|
5378
|
+
},
|
|
5379
|
+
"additionalProperties": false
|
|
5380
|
+
},
|
|
5381
|
+
"script": {
|
|
5382
|
+
"type": "object",
|
|
5383
|
+
"properties": {
|
|
5384
|
+
"name": {
|
|
5385
|
+
"type": "string"
|
|
5386
|
+
},
|
|
5387
|
+
"props": {
|
|
5388
|
+
"type": "object"
|
|
5389
|
+
}
|
|
5390
|
+
},
|
|
5391
|
+
"required": ["name"]
|
|
5392
|
+
},
|
|
5393
|
+
"network": {
|
|
5394
|
+
"type": "object",
|
|
5395
|
+
"properties": {
|
|
5396
|
+
"mode": {
|
|
5397
|
+
"enum": ["owner", "observer"]
|
|
5398
|
+
},
|
|
5399
|
+
"sync": {
|
|
5400
|
+
"type": "array",
|
|
5401
|
+
"items": {
|
|
5402
|
+
"type": "string",
|
|
5403
|
+
"minLength": 1
|
|
5404
|
+
}
|
|
5405
|
+
},
|
|
5406
|
+
"throttleMs": {
|
|
5407
|
+
"type": "number"
|
|
5408
|
+
}
|
|
5409
|
+
},
|
|
5410
|
+
"additionalProperties": false
|
|
5411
|
+
},
|
|
5412
|
+
"children": {
|
|
5413
|
+
"type": "array",
|
|
5414
|
+
"items": {
|
|
5415
|
+
"$ref": "#/$defs/node"
|
|
5416
|
+
}
|
|
5417
|
+
}
|
|
5418
|
+
},
|
|
5419
|
+
"required": ["name", "type"]
|
|
5420
|
+
},
|
|
5330
5421
|
"RigidBody2D": {
|
|
5331
5422
|
"type": "object",
|
|
5332
5423
|
"x-signals": ["triggerEnter", "triggerExit"],
|
package/skills/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# incanto skills (shipped)
|
|
2
2
|
|
|
3
3
|
Full agent skills for building games with this **installed version** of Incanto.
|
|
4
|
+
**New here? `incanto-your-first-game.md` is the walkthrough** — scaffold to a game
|
|
5
|
+
with a real win and lose, the shape of the loop, and the traps that cost a first
|
|
6
|
+
author an hour. Then come back to this list.
|
|
7
|
+
|
|
4
8
|
An AI agent should start with `incanto-scene-json-authoring.md`, then the domain skill
|
|
5
9
|
for the task at hand, and **finish every task with `incanto-verifying-your-game.md`**
|
|
6
10
|
— you cannot see the game, and that skill is how you find out whether it works.
|
|
@@ -145,6 +145,43 @@ declared by their classes. Escape hatch for runtime one-offs: `node.declareSigna
|
|
|
145
145
|
the emitting node. For per-emitter logic, attach a small behavior to the emitter itself
|
|
146
146
|
(see the `Pickup` pattern: a coin's own `triggerEnter` → its own `onTaken` → `queueFree`).
|
|
147
147
|
|
|
148
|
+
## Respawn (core node — catch a player who leaves the world)
|
|
149
|
+
|
|
150
|
+
Hang it off the thing it guards. That is the whole feature:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{ "name": "Player", "type": "RigidBody3D", "props": { "…": "…" },
|
|
154
|
+
"children": [
|
|
155
|
+
{ "name": "Controller", "type": "CharacterController3D" },
|
|
156
|
+
{ "name": "Catch", "type": "Respawn" }
|
|
157
|
+
] }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
With no props it guards its parent, catches it 50 m under the spawn (1000 px in
|
|
161
|
+
2D — the same line `incanto-playtest` calls `fell`), puts it back where it
|
|
162
|
+
started and zeroes the velocity the fall built up. Works in both dimensions.
|
|
163
|
+
|
|
164
|
+
| Prop | Default | Meaning |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| `target` | `".."` | who is being caught — the parent, normally |
|
|
167
|
+
| `below` | `null` | the line, in the scene's own down. `null` = auto (see above) |
|
|
168
|
+
| `to` | `[]` | where to put it back. Empty = wherever it started |
|
|
169
|
+
| `resetVelocity` | `true` | drop the speed the fall built up — off and it falls straight back through |
|
|
170
|
+
|
|
171
|
+
Signal `respawned(target, y)`, and a `falls` counter. It deliberately does NOT
|
|
172
|
+
decide what falling COSTS: wire `respawned` to a `ScoreKeeper.loseLife` or a
|
|
173
|
+
`Health.damage` if it should hurt, and leave it alone if it should not.
|
|
174
|
+
|
|
175
|
+
**A node, not a behavior**, because a node holds one behavior and the player's
|
|
176
|
+
is already spoken for.
|
|
177
|
+
|
|
178
|
+
Nothing in the engine did this before 0.62, and five shipped examples proved
|
|
179
|
+
what that cost: with the `plays` rung finally counting defects,
|
|
180
|
+
`basic-3d-sideview` failed 8 seeded runs of 8, `water-lake-3d` 7,
|
|
181
|
+
`water-ocean-3d` 5, `water-pool-3d` 4, `water-river-3d` 1 — every one of them
|
|
182
|
+
the player walking off the terrain and falling forever. One `Respawn` node each,
|
|
183
|
+
no TypeScript, and all five are clean.
|
|
184
|
+
|
|
148
185
|
## Timer (core node — never setTimeout in game logic)
|
|
149
186
|
|
|
150
187
|
```json
|
|
@@ -170,7 +170,16 @@ ALWAYS confirm in the browser (`bun run dev`) that the feet touch the surface.
|
|
|
170
170
|
| `zoom` | `1` | 2 = world pixels doubled |
|
|
171
171
|
| `limits` | `[]` | `[minX,minY,maxX,maxY]` — view rect clamped inside |
|
|
172
172
|
| `current` | `false` | mark exactly one |
|
|
173
|
-
Camera `position` is the view CENTER
|
|
173
|
+
Camera `position` is the view CENTER, and it composes with its ancestors like any
|
|
174
|
+
other node — so parenting the camera to the player (the Godot/Phaser idiom) works,
|
|
175
|
+
and so does putting it inside the container that holds the level. `follow` and the
|
|
176
|
+
`FollowCamera` behavior compare WORLD positions at both ends, so the target can live
|
|
177
|
+
under a different parent than the camera. With no camera, the view shows (0,0)–(w,h).
|
|
178
|
+
|
|
179
|
+
(Before 0.62 the view centre was the camera's LOCAL prop. A camera under a player at
|
|
180
|
+
`[1400, 900]` framed the world origin — and `framing`, which composes full world
|
|
181
|
+
matrices, reported `centred [1400, 900] … 1 in view`, certifying a frame the renderer
|
|
182
|
+
never drew.)
|
|
174
183
|
|
|
175
184
|
### `Label`
|
|
176
185
|
`text`, `fontSize 16`, `color '#ffffff'`, `font 'monospace'`, `align 'left'|'center'|'right'`,
|
|
@@ -357,6 +366,16 @@ Mark unchanging subtrees (tile decor, backgrounds) `static: true` — the
|
|
|
357
366
|
renderer syncs them once and skips them every frame. Flip to `false` to
|
|
358
367
|
resume live syncing.
|
|
359
368
|
|
|
369
|
+
The freeze waits for the node to have something worth freezing: a `Sprite2D` or
|
|
370
|
+
`TileMap2D` whose texture has not decoded yet keeps syncing until it has, and a
|
|
371
|
+
texture that never arrives never freezes at all (`assetErrors()` is what reports
|
|
372
|
+
that one). Before 0.62 the latch fired on frame one regardless — and a
|
|
373
|
+
`TextureLoader` fills `.image` asynchronously while the renderer loads and syncs
|
|
374
|
+
in the same frame, so the state frozen was `visible: false`, permanently. A
|
|
375
|
+
static backdrop was invisible for the life of the game with nothing reporting
|
|
376
|
+
it: the texture loaded fine, `stats().errors` was 0, and `framing` reads props
|
|
377
|
+
rather than pixels and still called the node on-screen.
|
|
378
|
+
|
|
360
379
|
## Before you hand it back
|
|
361
380
|
|
|
362
381
|
You cannot see this game. `incanto-verifying-your-game.md` is how you find out
|
|
@@ -69,7 +69,7 @@ All 3D nodes extend `Node3D` and therefore have the transform props:
|
|
|
69
69
|
|---|---|---|
|
|
70
70
|
| `position` | `[0,0,0]` | meters; +Y up |
|
|
71
71
|
| `rotation` | `[0,0,0]` | **degrees**, Euler XYZ |
|
|
72
|
-
| `scale` | `[1,1,1]` | |
|
|
72
|
+
| `scale` | `[1,1,1]` | resizes the DRAWN mesh. `Terrain3D`/`Water3D`/`River3D` reject a non-unit scale at load — they keep a CPU twin (`heightAt`, the heightfield collider, the water surface query) built from `size`, so scaling would move the picture and not the ground. Resize those with `size` |
|
|
73
73
|
| `visible` | `true` | hides the whole subtree's rendering |
|
|
74
74
|
|
|
75
75
|
### `MeshInstance3D`
|
|
@@ -20,7 +20,8 @@ feel of /Game/Player
|
|
|
20
20
|
turnaround 367 ms
|
|
21
21
|
input → motion 1 frame(s)
|
|
22
22
|
|
|
23
|
-
jump apex 0.262 u
|
|
23
|
+
jump apex 0.262 u (tapped)
|
|
24
|
+
held apex 0.981 u ← size your level against THIS
|
|
24
25
|
time to apex 133 ms
|
|
25
26
|
airtime 267 ms
|
|
26
27
|
coyote window 100 ms
|
|
@@ -47,7 +48,7 @@ reports **0 ms**. That was a real bug in this engine's own 3D controller until
|
|
|
47
48
|
| `to a stop` | how much you slide. A platformer that overshoots ledges has a long one. |
|
|
48
49
|
| `turnaround` | the cost of changing your mind. The number players feel most and name least. |
|
|
49
50
|
| `input → motion` | 1 frame is immediate. More than 2 and something is buffering. |
|
|
50
|
-
| `jump apex` / `airtime` | compare against the gaps and platform heights in your level. |
|
|
51
|
+
| `jump apex` / `airtime` | compare against the gaps and platform heights in your level. **Use the HELD apex** — the plain one is a one-frame TAP, and with `jumpCutMultiplier` (the variable-height jump the 3D character skill recommends at `0.45`) that is a fraction of the real jump: measured 0.478 u tapped against 1.831 u held on the same scene. The held line only appears when the two differ. |
|
|
51
52
|
| `coyote window` | **0 reads to players as "the controls are broken"**, not as a missing feature. |
|
|
52
53
|
| `jump buffer` | 0 throws away a press made a few frames before landing. |
|
|
53
54
|
|
|
@@ -222,18 +222,41 @@ Signals: `totalChanged(total)` — Methods: `collect(value)` — state: `total`
|
|
|
222
222
|
Deals damage to whatever it touches — projectiles, spikes, lava, enemy hitboxes.
|
|
223
223
|
**Goes on an Area.** On `triggerEnter`, finds the contacted entity's `Health` and
|
|
224
224
|
calls `damage(amount)`, then emits `dealtDamage(amount, healthOwnerNode)`.
|
|
225
|
-
`oncePerTarget` (default) prevents re-hitting a resting body every frame;
|
|
226
|
-
`destroySelf` frees the hazard after a hit (single-use projectiles).
|
|
227
225
|
|
|
228
226
|
| Prop | Default | Meaning |
|
|
229
227
|
|---|---|---|
|
|
230
228
|
| `amount` | `10` | HP removed per contact |
|
|
231
229
|
| `targetGroup` | `""` | only damage targets whose Health-owner is in this group (`""` = any) |
|
|
232
|
-
| `oncePerTarget` | `true` | damage each target at most once |
|
|
230
|
+
| `oncePerTarget` | `true` | damage each target at most **once, ever** |
|
|
231
|
+
| `repeatEvery` | `0` | seconds between re-hits while still overlapping (`0` = entry only) |
|
|
233
232
|
| `destroySelf` | `false` | `queueFree()` after the first hit |
|
|
234
233
|
|
|
235
234
|
Signals: `dealtDamage(amount, target)`
|
|
236
235
|
|
|
236
|
+
### `repeatEvery` is the "and it keeps hurting" knob
|
|
237
|
+
|
|
238
|
+
**Contact events fire on ENTRY and EXIT, not per frame.** So `oncePerTarget:
|
|
239
|
+
false` does not mean "hurt every frame" — it means "hurt again on RE-entry". An
|
|
240
|
+
enemy that closes on you and stops moving has entered once, and deals one hit
|
|
241
|
+
and then nothing. Measured, 6 seconds of unbroken overlap at `amount: 10`:
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
oncePerTarget=false repeatEvery=0 → hp 90 ← one hit, then nothing
|
|
245
|
+
oncePerTarget=false repeatEvery=0.5 → hp 0
|
|
246
|
+
oncePerTarget=true repeatEvery=0.5 → hp 0
|
|
247
|
+
oncePerTarget=true repeatEvery=0 → hp 90
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`repeatEvery` is the only prop that makes a resting overlap keep hurting, and it
|
|
251
|
+
overrides `oncePerTarget` — lava pools, poison clouds, an enemy standing on you.
|
|
252
|
+
Set it on **every enemy contact hitbox**; without it a game plays as though the
|
|
253
|
+
enemies are harmless the moment one of them corners you, and every check comes
|
|
254
|
+
back green.
|
|
255
|
+
|
|
256
|
+
Pair it with `Health.invulnerableFor`, which is the real "don't drain me every
|
|
257
|
+
frame" guard: with `invulnerableFor: 0.6`, a `repeatEvery` under 0.6 s just
|
|
258
|
+
lands on i-frames, so 0.5–1 s is the useful range.
|
|
259
|
+
|
|
237
260
|
**`targetGroup` — stop enemies killing each other.** With no collision layers in
|
|
238
261
|
v0 (everything overlaps everything), an enemy's contact hitbox would damage any
|
|
239
262
|
`Health` it touches, including other enemies clustered at the spawn point. Gate
|
|
@@ -513,6 +536,11 @@ pickups, spinning/pulsing coins. Drives one `axis` of `position`, `rotation`
|
|
|
513
536
|
"script": { "name": "Oscillate", "props": { "axis": "z", "amplitude": 180, "frequency": 0.5, "mode": "rotation" } } }
|
|
514
537
|
```
|
|
515
538
|
|
|
539
|
+
On a physics BODY, `mode: "rotation"` turns the collider too — a spinning blade
|
|
540
|
+
cuts, a swinging gate blocks. (Before 0.62 it turned only the mesh: a body's
|
|
541
|
+
angle was applied once at creation and never followed again, so every spinner
|
|
542
|
+
built this way was decoration.)
|
|
543
|
+
|
|
516
544
|
## Buoyancy
|
|
517
545
|
|
|
518
546
|
Float on water. Hangs on a `RigidBody3D` and asks the water how high it is at
|
|
@@ -585,7 +613,7 @@ later clones get `EnemyTemplate2`, `EnemyTemplate3`, …)
|
|
|
585
613
|
| `prefab` | `""` | node path of the template to clone (required) |
|
|
586
614
|
| `interval` | `1` | seconds between spawns |
|
|
587
615
|
| `max` | `0` | max LIVE instances (0 = unlimited) |
|
|
588
|
-
| `at` | `[]` | offset
|
|
616
|
+
| `at` | `[]` | where the clone appears, as an offset from the spawner ([x,y(,z)]) |
|
|
589
617
|
| `autoStart` | `true` | begin ticking at ready |
|
|
590
618
|
| `total` | `0` | total LIFETIME spawns (0 = infinite) |
|
|
591
619
|
|
|
@@ -634,6 +662,15 @@ MOVEMENT by design: **compose** it with `DamageOnContact` (deal damage), `Lifeti
|
|
|
634
662
|
`direction` is a vector OR `'forward'` (from the node `rotation`); it's a
|
|
635
663
|
union-typed prop so its default is `null` (= `'forward'`).
|
|
636
664
|
|
|
665
|
+
In 3D, `'forward'` is the engine's ONE facing convention — **+Z**, the same
|
|
666
|
+
`atan2(dx, dz)` basis `CharacterController3D` uses and `incanto-3d-character.md`
|
|
667
|
+
spells out under "the +Z-FORWARD rule". So aim the bullet (or its shooter) with
|
|
668
|
+
that formula and it flies at what you aimed at; do not add 180°. (Before 0.62
|
|
669
|
+
this returned three.js's −Z instead, so every `'forward'` shot left the muzzle
|
|
670
|
+
pointing at the shooter's back — at the right speed, with no error, which reads
|
|
671
|
+
as a gun that simply never hits anything.) In 2D `rotation` is a scalar and
|
|
672
|
+
`'forward'` is +x turned clockwise by it, unchanged.
|
|
673
|
+
|
|
637
674
|
| Prop | Default | Meaning |
|
|
638
675
|
|---|---|---|
|
|
639
676
|
| `speed` | `300` | units per second along `direction` |
|
|
@@ -679,7 +716,8 @@ interaction is a `connection`.
|
|
|
679
716
|
"script": { "name": "Pickup", "props": { "value": 1 } } },
|
|
680
717
|
{ "name": "Spike", "type": "Area2D", "props": { "position": [120, 80],
|
|
681
718
|
"collider": { "shape": "rect", "size": [24, 24] } },
|
|
682
|
-
"script": { "name": "DamageOnContact",
|
|
719
|
+
"script": { "name": "DamageOnContact",
|
|
720
|
+
"props": { "amount": 40, "oncePerTarget": false, "repeatEvery": 0.5 } } }
|
|
683
721
|
]
|
|
684
722
|
},
|
|
685
723
|
"connections": [
|
|
@@ -764,7 +802,8 @@ glue an early sim had to hand-write):
|
|
|
764
802
|
{ "name": "Hit", "type": "Area2D",
|
|
765
803
|
"props": { "collider": { "shape": "circle", "radius": 12 } },
|
|
766
804
|
"script": { "name": "DamageOnContact",
|
|
767
|
-
"props": { "amount": 10, "targetGroup": "player",
|
|
805
|
+
"props": { "amount": 10, "targetGroup": "player",
|
|
806
|
+
"oncePerTarget": false, "repeatEvery": 0.5 } } }
|
|
768
807
|
] }
|
|
769
808
|
] }
|
|
770
809
|
]
|
|
@@ -239,6 +239,7 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
|
|
|
239
239
|
| `velocity` | `[0,0,0]` | array |
|
|
240
240
|
| `stickToGround` | `true` | boolean |
|
|
241
241
|
| `slopeLimitDeg` | `45` | number |
|
|
242
|
+
| `stepHeight` | `0.35` | number |
|
|
242
243
|
|
|
243
244
|
Signals: `triggerEnter(other)` · `triggerExit(other)`
|
|
244
245
|
|
|
@@ -706,6 +707,17 @@ Signals: `finished`
|
|
|
706
707
|
|
|
707
708
|
Signals: `finished`
|
|
708
709
|
|
|
710
|
+
## `Respawn` — `incanto`
|
|
711
|
+
|
|
712
|
+
| Prop | Default | Kind |
|
|
713
|
+
|---|---|---|
|
|
714
|
+
| `target` | `".."` | node path |
|
|
715
|
+
| `below` | `null` | null |
|
|
716
|
+
| `to` | `[]` | array |
|
|
717
|
+
| `resetVelocity` | `true` | boolean |
|
|
718
|
+
|
|
719
|
+
Signals: `respawned`
|
|
720
|
+
|
|
709
721
|
## `RigidBody2D` — `incanto/2d`
|
|
710
722
|
|
|
711
723
|
| Prop | Default | Kind |
|
|
@@ -50,6 +50,16 @@ the corners. Give the body its mesh as a CHILD and let the shape follow:
|
|
|
50
50
|
"props": { "mesh": "box", "size": [14, 0.22, 2.2] } } ] }
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
The child's OWN `position`, `rotation` and `scale` are part of the shape: raise the plank on
|
|
54
|
+
the child instead of the body and the collider goes up with it; scale the crate 4× and it is
|
|
55
|
+
solid at 4×. (It was not before 0.62 — the child transform was dropped and the collider sat
|
|
56
|
+
at the body's origin at the authored size, so you walked through the deck you could see and
|
|
57
|
+
landed on an invisible slab at ground level. Measured: a plank drawn with its top at y=3.2
|
|
58
|
+
had its surface at y=0.2.) `collider.offset` stacks on top of that.
|
|
59
|
+
|
|
60
|
+
A ball, a capsule and a cylinder have ONE radius between them, so a child scaled unevenly in
|
|
61
|
+
x and z has no exact shape — the widest of the two is used and the engine says so by name.
|
|
62
|
+
|
|
53
63
|
Bodies ignore an ANCESTOR's rotation, so a yawed structure puts each body at top level with
|
|
54
64
|
its own `rotation` — the child mesh then turns with it, visual and collider together.
|
|
55
65
|
|
|
@@ -143,7 +153,13 @@ engine.input.isPressed('jump'); // held
|
|
|
143
153
|
engine.input.justPressed('jump'); // one-frame edge (settled per tick)
|
|
144
154
|
engine.input.getVector('move'); // normalized {x, y}, y-down (up = -y)
|
|
145
155
|
```
|
|
146
|
-
Keys are `KeyboardEvent.code` strings (`KeyW`, `Space`, `ArrowLeft`)
|
|
156
|
+
Keys are `KeyboardEvent.code` strings (`KeyW`, `Space`, `ArrowLeft`) — plus `Mouse0..4`
|
|
157
|
+
and `Pad0..16`, which live in the same space. **A value outside that set is a load
|
|
158
|
+
error** naming the one you probably meant (`'W' → "KeyW"`, `'space' → "Space"`,
|
|
159
|
+
`'Shift' → "ShiftLeft" or "ShiftRight"`): the map is looked up by exact string, so a
|
|
160
|
+
misspelled code binds to nothing and the player silently does not have that control.
|
|
161
|
+
An EMPTY key list is still fine — `{"keys": [], "touch": "button"}` is a touch-only
|
|
162
|
+
control. Unknown actions and
|
|
147
163
|
wrong-kind queries (getVector on a button) are hard errors listing valid names.
|
|
148
164
|
Action declarations RESET on every `setScene` (no keybind bleed between scenes).
|
|
149
165
|
|
|
@@ -239,7 +255,12 @@ Injected state combines with key state (vectors clamped to unit length).
|
|
|
239
255
|
- Pickups: `Area2D` in a group + `triggerEnter` → check `other.isInGroup('player')` →
|
|
240
256
|
`queueFree()` — wire it via JSON `connections` to a behavior method (the `Pickup`
|
|
241
257
|
pattern in `incanto-behaviors-and-scripts.md`), or imperatively with `node.on(...)`.
|
|
242
|
-
- Teleport: just write `node.position` — the body follows.
|
|
258
|
+
- Teleport: just write `node.position` — the body follows. **`node.rotation` follows
|
|
259
|
+
too**: turn a static or kinematic body at runtime and its COLLIDER turns with it, so
|
|
260
|
+
a spinning blade cuts and a swinging gate blocks. (It did not before 0.62 — the
|
|
261
|
+
angle was applied once at creation, the mesh turned and the collider stayed put.)
|
|
262
|
+
A free dynamic body's rotation belongs to the solver and is left alone.
|
|
263
|
+
Launch: write `linearVelocity`.
|
|
243
264
|
- Reference: [examples/2d-phaser-sprite-character-gravity](https://github.com/rareboe/Incanto/tree/main/examples/2d-phaser-sprite-character-gravity) — gravity, jump, attack lockout, custom
|
|
244
265
|
`Player` node type. Verified in Chromium end-to-end.
|
|
245
266
|
|
|
@@ -336,6 +357,36 @@ the tree says, without your scene falling over while you look at it. Use it for
|
|
|
336
357
|
any "show me the collision geometry" tool. Do NOT use it to freeze a running
|
|
337
358
|
game — that is `engine.timeScale = 0`, which keeps the world coherent.
|
|
338
359
|
|
|
360
|
+
## A walking body climbs a step (`stepHeight`, 3D)
|
|
361
|
+
|
|
362
|
+
`CharacterBody3D.stepHeight` (default `0.35` m, `0` = off) is how high a ledge
|
|
363
|
+
the body walks UP without jumping. It is Rapier's autostep, and **Rapier does not
|
|
364
|
+
autostep unless it is asked** — before 0.63 nothing asked, so any walking body
|
|
365
|
+
was stopped dead by a curb.
|
|
366
|
+
|
|
367
|
+
You will not see this on the player: `CharacterController3D` rides a hover spring
|
|
368
|
+
rather than the character controller, so it floats over small ledges already.
|
|
369
|
+
It is the ENEMIES that walk, and a chaser walks a straight line — it cannot go
|
|
370
|
+
around. Measured on the shipped `tps-3d` template, whose arena has a 0.6 m ramp
|
|
371
|
+
between the spawn and the player, standing still for 45 seconds:
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
before hits=0 hp=100 closest an enemy ever got = 33.91m
|
|
375
|
+
after hits=9 hp=.. closest = 1.80m (exactly Chase.stopRange)
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
They chased at full speed the whole time and piled up against the near face of a
|
|
379
|
+
knee-high box. Raise `stepHeight` for a world with real stairs; set it to `0` for
|
|
380
|
+
something that genuinely should be blocked by a curb.
|
|
381
|
+
|
|
382
|
+
**It loses to a large downward velocity.** Autostep happens inside the movement
|
|
383
|
+
solve, so a body driven with a big constant gravity term every frame never
|
|
384
|
+
climbs. Same 0.3 m ledge, same `stepHeight: 0.35`, only the `velocity[1]`
|
|
385
|
+
differs: `0` → cleared, `−0.1` → cleared, `−2` → stopped at the face. If you are
|
|
386
|
+
integrating your own gravity, apply it as a falling SPEED that resets on the
|
|
387
|
+
ground, not as a constant push. (`moveBody`, which `Chase` and `Patrol` use,
|
|
388
|
+
derives velocity from a position delta and carries no such term.)
|
|
389
|
+
|
|
339
390
|
## Moving platforms carry their riders (2D **and** 3D)
|
|
340
391
|
|
|
341
392
|
A `CharacterBody2D`/`CharacterBody3D` standing on a body that moves is dragged
|
|
@@ -299,7 +299,21 @@ that do exist at the failing spot).
|
|
|
299
299
|
- `signal` must be DECLARED on the from node (the class's or its behavior's
|
|
300
300
|
`static signals`) — validated at load → `UNKNOWN_SIGNAL` otherwise.
|
|
301
301
|
- `handler` must be a method on the target node OR its behavior (script) — both are
|
|
302
|
-
validated hard at load. Otherwise → `UNKNOWN_HANDLER`.
|
|
302
|
+
validated hard at load. Otherwise → `UNKNOWN_HANDLER`.
|
|
303
|
+
- **The ARGUMENTS are checked when the wire fires**, not at load — nothing declares
|
|
304
|
+
how many a signal carries, so the pair can only be compared at the moment both
|
|
305
|
+
are known. A handler given fewer than it needs is reported once, by name:
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
[incanto] 'died' from 'Enemy' carries 0 argument(s) and 'Keeper.addScore' needs 1.
|
|
309
|
+
The missing one(s) arrive as undefined — a number handler gets NaN and
|
|
310
|
+
never recovers.
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
`died → ScoreKeeper.addScore` is the natural way to score a kill and it sets the
|
|
314
|
+
score to `NaN` on the first one, with the win condition then permanently out of
|
|
315
|
+
reach. A handler that DEFAULTS what it is not given is correct and stays silent:
|
|
316
|
+
`won → GameFlow.win` works, because `win(text = 'YOU WIN')` needs nothing. On **both**, it is
|
|
303
317
|
`AMBIGUOUS_HANDLER`: the node's method wins, so the script's would never run
|
|
304
318
|
and nothing would say so. A core node answers to 62 public methods before any
|
|
305
319
|
adapter adds more (`stop` `play` `show` `clear` `say` `start` `free` …), which
|
|
@@ -32,6 +32,7 @@ failures — but `framing` and `assetErrors()` you have to ASK for.
|
|
|
32
32
|
$ bunx incanto verify # finds your scene AND your behaviours
|
|
33
33
|
· behaviours: src/behaviors.ts (found, not named)
|
|
34
34
|
✓ loads — the scene is legal and its assets resolve
|
|
35
|
+
! /Game/Chest: script Chest — target '%Ke' matches nothing
|
|
35
36
|
? plays — 8 runs played without reaching a win (4 lost, 4 ran out the clock)
|
|
36
37
|
✓ feels — 5 of 7 fired — silent: /Game/Boss/Roar, /Game/Boss/Boom
|
|
37
38
|
· agrees — not run — this scene has no `multiplayer` header
|
|
@@ -67,11 +68,40 @@ encodes so you do not have to remember them:
|
|
|
67
68
|
|
|
68
69
|
- a **failed** rung makes the ones above it meaningless, so only the first is
|
|
69
70
|
worth reading — the rest are marked `·` skipped;
|
|
71
|
+
- a rung that PASSED can still have something to say. Anything its own tool
|
|
72
|
+
warned about is printed under it with a `!` — `loads` runs `incanto-check`,
|
|
73
|
+
and the ladder used to read only whether it passed and drop the warnings, so a
|
|
74
|
+
scene the check describes as "It will render black" came back as
|
|
75
|
+
`✓ loads — the scene is legal and its assets resolve`;
|
|
70
76
|
- an **unmeasured** rung (`?`) is not a failure. "No dev server" means the
|
|
71
77
|
question was never asked; treating that as a broken game sends you editing a
|
|
72
78
|
scene that is fine;
|
|
73
79
|
- a scene that declares **no win** is not a scene that cannot be won. A
|
|
74
|
-
walkabout has no end, and that is reported as unmeasured, not failed
|
|
80
|
+
walkabout has no end, and that is reported as unmeasured, not failed;
|
|
81
|
+
- but a **defect** fails the rung whatever the scene declares. `error` (a
|
|
82
|
+
behaviour threw) and `fell` (the player left the world) are counted BEFORE
|
|
83
|
+
anything else is decided:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
✗ plays — 8 of 8 runs ended in a defect (8 fell out of the world)
|
|
87
|
+
next: the player left the world with nothing catching them — give the level a
|
|
88
|
+
floor, walls, or a respawn
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The routed answer to that last one is a `Respawn` node under the player
|
|
92
|
+
(`incanto-behaviors-and-scripts.md`) — one line of JSON, no per-frame `y`
|
|
93
|
+
check in your own code.
|
|
94
|
+
|
|
95
|
+
This used to be the sentence `8 runs played without error` — printed for a
|
|
96
|
+
scene with no declared win no matter what happened in it, including 8/8
|
|
97
|
+
`fell`, and exit 0. The child `incanto-playtest` had just printed
|
|
98
|
+
`✗ fell in 8/8`; the parent said the opposite of it.
|
|
99
|
+
|
|
100
|
+
`stuck` — the player went nowhere — is the third defect and the one that needs
|
|
101
|
+
a fair chance before it counts. It is only a failure when there WAS someone to
|
|
102
|
+
move and your behaviours were loaded. A scene with no character controller, no
|
|
103
|
+
`player` group and no node named `Player` reports `nothing here is drivable`,
|
|
104
|
+
and a run without `--behaviors` reports that instead: neither is a broken game.
|
|
75
105
|
|
|
76
106
|
`feels` is the sound-and-effects rung: it lists what the scene DECLARES against
|
|
77
107
|
what actually fired during the playtest. A game whose feedback is wired but
|