incanto 0.60.0 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/incanto-check.mjs +13 -0
- package/bin/incanto-verify.mjs +134 -55
- package/dist/2d.d.ts +64 -3
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +5 -5
- package/dist/3d.js +5 -4
- package/dist/{pathfinding-BqWBb0kh.d.ts → audio-player-D5GJgb_x.d.ts} +103 -38
- package/dist/{behavior-DWKTUzKI.d.ts → behavior-DsgayMsH.d.ts} +79 -1
- package/dist/{create-game-ClnIb_M5.js → create-game-BpunnGPX.js} +78 -13
- package/dist/{create-game-BCm38FJV.js → create-game-Caut3bqN.js} +21 -294
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +1 -1
- package/dist/{duplicate-DJQd44CD.js → duplicate-E4FUs5Bn.js} +1 -1
- package/dist/editor.js +41 -19
- package/dist/{environment-presets-8cjF3t6w.js → environment-presets-BAWeOeqf.js} +50 -13
- package/dist/{gameplay-BBEjPFsR.js → gameplay-CaHqDiQD.js} +177 -24
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +97 -6
- package/dist/index.js +7 -7
- package/dist/{loader-D8n7TU8W.js → loader-DEe272nY.js} +900 -2
- package/dist/{loader-TvkRFbyL.d.ts → loader-DolLJWJn.d.ts} +13 -1
- package/dist/net.d.ts +2 -2
- package/dist/net.js +1 -1
- package/dist/pathfinding-_fGrCFmH.d.ts +28 -0
- package/dist/{physics-2d-DqdVp1bt.js → physics-2d-BXmu2i7W.js} +11 -3
- package/dist/{physics-3d-BP0DZb_1.js → physics-3d-ClxP6Uv7.js} +121 -17
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-BSu2dWGC.js → register-CNh4FlbD.js} +104 -16
- package/dist/{register-Da3hXh2H.js → register-D3yx8D4r.js} +228 -16
- package/dist/{registry-WWcQcfMr.js → registry-CF70EArN.js} +55 -3
- package/dist/{replay-BCMK_VRP.d.ts → replay-C5x2vPF5.d.ts} +2 -2
- package/dist/{replay-BlNuIDdg.js → replay-DlgHItNv.js} +57 -197
- package/dist/{split-screen-CL5Yvxse.js → split-screen-CBM9wcMX.js} +3 -3
- package/dist/{split-screen-BQ3tAsf-.d.ts → split-screen-D7OopelJ.d.ts} +2 -2
- package/dist/{src-DFpXBMJN.js → src-CH00_JsR.js} +1 -1
- package/dist/{teardown-CTTwhWSe.js → teardown-C7qP-dcC.js} +1 -1
- package/dist/{test-BeZ95pqw.js → test-BMkg8zMV.js} +29 -16
- package/dist/test.d.ts +45 -4
- package/dist/test.js +2 -2
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-DSJries_.js → agent8-m5mtAO_A.js} +1 -1
- package/editor/assets/{debug-D15Wi5TO.js → debug-CPhzCT8f.js} +1 -1
- package/editor/assets/{index-BjC88k97.js → index-D422P4kW.js} +92 -92
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +91 -0
- package/skills/incanto-3d-models.md +1 -1
- package/skills/incanto-assets.md +10 -1
- package/skills/incanto-audio.md +21 -13
- package/skills/incanto-behaviors-and-scripts.md +44 -3
- package/skills/incanto-building-2d-games.md +32 -4
- package/skills/incanto-building-3d-games.md +1 -1
- package/skills/incanto-editor.md +1 -1
- package/skills/incanto-gameplay-behaviors.md +39 -2
- package/skills/incanto-hud.md +5 -3
- package/skills/incanto-localization.md +7 -0
- package/skills/incanto-node-reference.md +12 -0
- package/skills/incanto-physics-and-input.md +23 -2
- package/skills/incanto-scene-json-authoring.md +6 -1
- package/skills/incanto-verifying-your-game.md +31 -1
- 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
- package/dist/particle-sim-C5OfBbmU.d.ts +0 -77
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-D422P4kW.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
|
},
|
|
@@ -3696,6 +3699,10 @@
|
|
|
3696
3699
|
"align": {
|
|
3697
3700
|
"type": "string",
|
|
3698
3701
|
"default": "left"
|
|
3702
|
+
},
|
|
3703
|
+
"opacity": {
|
|
3704
|
+
"type": "number",
|
|
3705
|
+
"default": 1
|
|
3699
3706
|
}
|
|
3700
3707
|
},
|
|
3701
3708
|
"additionalProperties": false
|
|
@@ -5323,6 +5330,90 @@
|
|
|
5323
5330
|
},
|
|
5324
5331
|
"required": ["name", "type"]
|
|
5325
5332
|
},
|
|
5333
|
+
"Respawn": {
|
|
5334
|
+
"type": "object",
|
|
5335
|
+
"x-signals": ["respawned"],
|
|
5336
|
+
"properties": {
|
|
5337
|
+
"name": {
|
|
5338
|
+
"type": "string"
|
|
5339
|
+
},
|
|
5340
|
+
"uid": {
|
|
5341
|
+
"type": "string"
|
|
5342
|
+
},
|
|
5343
|
+
"type": {
|
|
5344
|
+
"const": "Respawn"
|
|
5345
|
+
},
|
|
5346
|
+
"groups": {
|
|
5347
|
+
"type": "array",
|
|
5348
|
+
"items": {
|
|
5349
|
+
"type": "string"
|
|
5350
|
+
}
|
|
5351
|
+
},
|
|
5352
|
+
"tags": {
|
|
5353
|
+
"type": "object"
|
|
5354
|
+
},
|
|
5355
|
+
"props": {
|
|
5356
|
+
"type": "object",
|
|
5357
|
+
"properties": {
|
|
5358
|
+
"target": {
|
|
5359
|
+
"type": "string",
|
|
5360
|
+
"description": "A node path: '%UniqueName', '/Absolute/From/Root', or relative to this node ('../Skin').",
|
|
5361
|
+
"default": ".."
|
|
5362
|
+
},
|
|
5363
|
+
"below": {
|
|
5364
|
+
"default": null
|
|
5365
|
+
},
|
|
5366
|
+
"to": {
|
|
5367
|
+
"type": "array",
|
|
5368
|
+
"default": []
|
|
5369
|
+
},
|
|
5370
|
+
"resetVelocity": {
|
|
5371
|
+
"type": "boolean",
|
|
5372
|
+
"default": true
|
|
5373
|
+
}
|
|
5374
|
+
},
|
|
5375
|
+
"additionalProperties": false
|
|
5376
|
+
},
|
|
5377
|
+
"script": {
|
|
5378
|
+
"type": "object",
|
|
5379
|
+
"properties": {
|
|
5380
|
+
"name": {
|
|
5381
|
+
"type": "string"
|
|
5382
|
+
},
|
|
5383
|
+
"props": {
|
|
5384
|
+
"type": "object"
|
|
5385
|
+
}
|
|
5386
|
+
},
|
|
5387
|
+
"required": ["name"]
|
|
5388
|
+
},
|
|
5389
|
+
"network": {
|
|
5390
|
+
"type": "object",
|
|
5391
|
+
"properties": {
|
|
5392
|
+
"mode": {
|
|
5393
|
+
"enum": ["owner", "observer"]
|
|
5394
|
+
},
|
|
5395
|
+
"sync": {
|
|
5396
|
+
"type": "array",
|
|
5397
|
+
"items": {
|
|
5398
|
+
"type": "string",
|
|
5399
|
+
"minLength": 1
|
|
5400
|
+
}
|
|
5401
|
+
},
|
|
5402
|
+
"throttleMs": {
|
|
5403
|
+
"type": "number"
|
|
5404
|
+
}
|
|
5405
|
+
},
|
|
5406
|
+
"additionalProperties": false
|
|
5407
|
+
},
|
|
5408
|
+
"children": {
|
|
5409
|
+
"type": "array",
|
|
5410
|
+
"items": {
|
|
5411
|
+
"$ref": "#/$defs/node"
|
|
5412
|
+
}
|
|
5413
|
+
}
|
|
5414
|
+
},
|
|
5415
|
+
"required": ["name", "type"]
|
|
5416
|
+
},
|
|
5326
5417
|
"RigidBody2D": {
|
|
5327
5418
|
"type": "object",
|
|
5328
5419
|
"x-signals": ["triggerEnter", "triggerExit"],
|
|
@@ -94,7 +94,7 @@ meta (name/authors) + humanoid bone count. Decision rules:
|
|
|
94
94
|
|
|
95
95
|
`ModelInstance3D` props: `model` (`$key` or a direct URL), `targetHeight` (>0 uniformly
|
|
96
96
|
scales the model to stand that many units tall — composes with the node `scale`, reactive
|
|
97
|
-
at runtime; see "Sizing" below), `animation` (see below), `tint` (
|
|
97
|
+
at runtime; see "Sizing" below), `animation` (see below), `tint` (any CSS colour — `"#5f8f4a"`, `"chartreuse"`; `""`=off — multiplies into
|
|
98
98
|
every material to RESKIN one shared GLB into many variants, e.g. base human → green
|
|
99
99
|
zombie; clones materials per instance so other instances are untouched),
|
|
100
100
|
`metalness`/`roughness` (0..1 overrides applied to EVERY material; `-1`=keep authored —
|
package/skills/incanto-assets.md
CHANGED
|
@@ -86,7 +86,16 @@ Every catalog entry has a `url` that is directly usable; there are two classes:
|
|
|
86
86
|
`copy` does this for you and prints the ready-to-paste JSON — for animated
|
|
87
87
|
sheets it includes the full `animations` map (idle/move/attack…) derived from
|
|
88
88
|
the sheet metadata. `Sprite2D.texture` / `AnimatedSprite2D.sheet` take a
|
|
89
|
-
`"$assetKey"` ref
|
|
89
|
+
`"$assetKey"` ref, and the engine hard-fails at LOAD on anything else — a raw
|
|
90
|
+
URL, a bare catalog id, or a `$key` the scene does not declare:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
[UNKNOWN_ASSET] 'Coin.texture' names asset '$coinn', which the scene does not
|
|
94
|
+
declare. Declared: [coin, gem]. (at '/Room/Coin')
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
(It genuinely did not, until 0.61: the check lived in the renderer, so only a
|
|
98
|
+
browser ever reached it and a typo'd ref simply drew nothing.)
|
|
90
99
|
|
|
91
100
|
The scene **composer (incanto-editor)** wires all of this for you: the inspector
|
|
92
101
|
shows an asset PICKER on every texture/sheet/map prop — browse the built-ins by
|
package/skills/incanto-audio.md
CHANGED
|
@@ -138,9 +138,13 @@ motor.stop(1.2); // …or a slow custom fade
|
|
|
138
138
|
} }
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
-
Methods: **`play()`**, **`stop()`**. Signal: **`finished`**
|
|
142
|
-
non-looping `src` clip ends
|
|
143
|
-
|
|
141
|
+
Methods: **`play()`**, **`stop()`**. Signal: **`finished`** — connect it to clean
|
|
142
|
+
up or chain sounds. It fires when a non-looping `src` clip ends AND when a
|
|
143
|
+
procedural preset does: a preset's length is `attack + sustain + decay`, known
|
|
144
|
+
exactly, so `player.playing` is true for that long and `finished` arrives at the
|
|
145
|
+
end. It is measured on the unscaled clock, because a paused game still hears the
|
|
146
|
+
tail of the hit that paused it, and it fires headlessly too — the length is
|
|
147
|
+
arithmetic, not a device. `stop()` is not a finish and does not emit.
|
|
144
148
|
|
|
145
149
|
### Autoplay + the browser gesture-unlock
|
|
146
150
|
|
|
@@ -175,14 +179,14 @@ and models that 404'd:
|
|
|
175
179
|
|
|
176
180
|
---
|
|
177
181
|
|
|
178
|
-
## 2b. Spatial (
|
|
182
|
+
## 2b. Spatial (positional) audio
|
|
179
183
|
|
|
180
|
-
Set **`spatial: true`** on an `AudioPlayer`
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
The
|
|
184
|
-
moving the emitter or the camera updates the sound live. Works
|
|
185
|
-
(procedural `preset` and a `src` file).
|
|
184
|
+
Set **`spatial: true`** on an `AudioPlayer` and the sound pans (left/right) and
|
|
185
|
+
attenuates by **distance** — the emitter is the node's world position, the
|
|
186
|
+
**listener is the active camera** (`Camera3D` or `Camera2D`, the one marked
|
|
187
|
+
`current`). The adapter feeds the emitter + listener pose to the panner every
|
|
188
|
+
frame, so moving the emitter or the camera updates the sound live. Works in both
|
|
189
|
+
dimensions and for BOTH paths (procedural `preset` and a `src` file).
|
|
186
190
|
|
|
187
191
|
```jsonc
|
|
188
192
|
// Attach under a moving Node3D — it emits from THAT node's world position.
|
|
@@ -214,9 +218,13 @@ listener driven by the camera's world position + orientation. The pure
|
|
|
214
218
|
distance-gain + pan-sign math (`spatialGain`, `spatialPan`, exported) is what the
|
|
215
219
|
non-WebAudio paths use, so headless gameplay is unaffected.
|
|
216
220
|
|
|
217
|
-
> **2D scenes
|
|
218
|
-
>
|
|
219
|
-
>
|
|
221
|
+
> **2D scenes** work the same way: the listener is the active `Camera2D` (its
|
|
222
|
+
> world position is the view centre, and its own rotation turns the stereo image
|
|
223
|
+
> with it). The one difference is the UNIT — a 2D scene measures in PIXELS,
|
|
224
|
+
> and `refDistance: 1` / `maxDistance: 50` are metre-shaped defaults that put a
|
|
225
|
+
> sound at its quietest about one sprite away. Set them to your level's scale
|
|
226
|
+
> (`refDistance: 100`, `maxDistance: 900` is a reasonable start); `incanto check`
|
|
227
|
+
> warns when it sees `spatial: true` in a 2D scene with the defaults left alone.
|
|
220
228
|
|
|
221
229
|
> **Headless / verify VM:** spatial is a no-op like all audio — `play()` won't
|
|
222
230
|
> throw and your gameplay logic is unaffected.
|
|
@@ -102,7 +102,7 @@ What behavior code actually calls at runtime — all instance methods, no global
|
|
|
102
102
|
| `engine.stop()` / `engine.start()` | pause / resume the loop — stop resets the clock and accumulator, so no banked sim time leaks into the resume |
|
|
103
103
|
| `engine.step()` | advance exactly ONE fixed step + one update (both dt = the fixed step) — the unit of time for headless tests |
|
|
104
104
|
| `engine.tick(timestampMs)` | manual frame advance — takes an **absolute** ms timestamp (rAF-style), NOT a dt; the first call after (re)start only primes the clock |
|
|
105
|
-
| `engine.setScene(scene)` | swap scenes: the previous root is freed, the input map is cleared and redeclared from the new scene's `input{}`, then `sceneChanged` fires |
|
|
105
|
+
| `engine.setScene(scene)` | swap scenes: the previous root is freed, the input map is cleared and redeclared from the new scene's `input{}`, the clock resets (`time`, `unscaledTime`, and **`timeScale` back to 1** — a level restarted out of a frozen game-over must not boot frozen), then `sceneChanged` fires |
|
|
106
106
|
| `engine.stats()` | live perf counters `{ fps, frameMs, nodes, running }` — fps/frameMs average the last ~60 REAL `tick` frames (headless `step()` runs report 0), nodes is the current tree size. GPU counters (triangles/draw calls) live on `renderer.stats()` / the merged `game.stats()` |
|
|
107
107
|
|
|
108
108
|
The recurring traps: `duplicateNode` does NOT insert the clone anywhere — a
|
|
@@ -138,11 +138,50 @@ declared by their classes. Escape hatch for runtime one-offs: `node.declareSigna
|
|
|
138
138
|
]
|
|
139
139
|
```
|
|
140
140
|
- `handler` must exist on the target NODE or its BEHAVIOR — hard `UNKNOWN_HANDLER`
|
|
141
|
-
otherwise
|
|
141
|
+
otherwise, and hard `AMBIGUOUS_HANDLER` when BOTH have it (node methods take
|
|
142
|
+
precedence on invocation, so the script's would never run). Prefix handlers
|
|
143
|
+
with `on` and the question never comes up.
|
|
142
144
|
- Handlers receive the EMITTED args only (e.g. the other body for `triggerEnter`) — not
|
|
143
145
|
the emitting node. For per-emitter logic, attach a small behavior to the emitter itself
|
|
144
146
|
(see the `Pickup` pattern: a coin's own `triggerEnter` → its own `onTaken` → `queueFree`).
|
|
145
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
|
+
|
|
146
185
|
## Timer (core node — never setTimeout in game logic)
|
|
147
186
|
|
|
148
187
|
```json
|
|
@@ -166,7 +205,9 @@ Child of a `CharacterBody2D` (hard error otherwise):
|
|
|
166
205
|
- Defaults: `mode 'platformer'`, `maxSpeed 220`, `jumpHeight 64`, `moveAction 'move'`,
|
|
167
206
|
`jumpAction 'jump'`. Timer defaults: `waitTime 1`, `oneShot false`, `autostart false`.
|
|
168
207
|
- ⚠️ Connection handler names must not collide with Node API methods (`emit`, `update`,
|
|
169
|
-
`on`, `queueFree`, …) — the NODE method wins
|
|
208
|
+
`on`, `queueFree`, …) — the NODE method wins. That is `AMBIGUOUS_HANDLER` at load
|
|
209
|
+
now, not a silent wrong call; `incanto check` warns on the same pair from the
|
|
210
|
+
file alone. Prefix handlers with `on`.
|
|
170
211
|
|
|
171
212
|
Reference: [examples/2d-phaser-sprite-character-gravity](https://github.com/rareboe/Incanto/tree/main/examples/2d-phaser-sprite-character-gravity) — engine nodes + one small PlayerControl behavior
|
|
172
213
|
(`CoinCounter`, `Pickup`) wired entirely through JSON connections. Verified in Chromium.
|
|
@@ -170,10 +170,21 @@ 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
|
-
`text`, `fontSize 16`, `color '#ffffff'`, `font 'monospace'`, `align 'left'|'center'|'right'
|
|
185
|
+
`text`, `fontSize 16`, `color '#ffffff'`, `font 'monospace'`, `align 'left'|'center'|'right'`,
|
|
186
|
+
`opacity 1` (0..1, like `Sprite2D`/`ColorRect2D` — a `Label` can fade now, which is
|
|
187
|
+
what makes `FloatAway` work on one).
|
|
177
188
|
CanvasTexture-rendered; mutate `label.text` freely (re-rasterizes only on change).
|
|
178
189
|
|
|
179
190
|
### `UILayer`
|
|
@@ -250,14 +261,21 @@ listing the valid set. With a viewport design, UI coordinates are design px.
|
|
|
250
261
|
The input map is CLEARED by the swap, which matters for a headless drive: an
|
|
251
262
|
injected `setActionVector` does not carry into the next level — set it again
|
|
252
263
|
after the transition, the same way a player's held key is re-read.
|
|
264
|
+
|
|
265
|
+
So is the CLOCK: `time`, `unscaledTime` and `timeScale` all start over. That
|
|
266
|
+
last one is why the game-over recipe below is safe — a swap out of a frozen
|
|
267
|
+
screen boots the next level running, not frozen.
|
|
253
268
|
- **Game over / restart**: swap to a fresh load of the SAME JSON —
|
|
254
269
|
`engine.setScene(loadScene(levelJson))`. `loadScene` treats the JSON as
|
|
255
270
|
read-only (everything it keeps is cloned), so reloading the same imported
|
|
256
271
|
object yields a clean run; only clone (`structuredClone(levelJson)`) first if
|
|
257
272
|
your own code mutated that object. (`createGame`'s `scene` option already
|
|
258
273
|
structuredClones internally.)
|
|
259
|
-
- **Pause / resume**: `engine.
|
|
260
|
-
|
|
274
|
+
- **Pause / resume**: `engine.timeScale = 0` is the pause you want — the loop
|
|
275
|
+
keeps running, so menus, banners and the key that closes them stay live (this
|
|
276
|
+
is what `GameFlow.pause()` does). `engine.stop()` halts the loop entirely
|
|
277
|
+
(clock and accumulator reset — no banked time on resume); `engine.start()`
|
|
278
|
+
resumes. Either way a `setScene` puts `timeScale` back to 1.
|
|
261
279
|
- **Save / load** (a behavior, localStorage):
|
|
262
280
|
```ts
|
|
263
281
|
localStorage.setItem('save', JSON.stringify({ score: this.score }));
|
|
@@ -348,6 +366,16 @@ Mark unchanging subtrees (tile decor, backgrounds) `static: true` — the
|
|
|
348
366
|
renderer syncs them once and skips them every frame. Flip to `false` to
|
|
349
367
|
resume live syncing.
|
|
350
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
|
+
|
|
351
379
|
## Before you hand it back
|
|
352
380
|
|
|
353
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`
|
package/skills/incanto-editor.md
CHANGED
|
@@ -104,7 +104,7 @@ Opens a local page (default `http://127.0.0.1:5179/`) with three panes:
|
|
|
104
104
|
changed. A prop is rewritten only if its type declares `nodePath: true`, so a
|
|
105
105
|
text prop that merely reads "Player" is never touched. `+` adds a child of any registered
|
|
106
106
|
type, `✕` deletes the selection. Selecting the ⚙ scene row edits the header
|
|
107
|
-
(dimension, gravity with real inputs, environment/input/assets/multiplayer).
|
|
107
|
+
(dimension, gravity with real inputs, environment/input/assets/multiplayer/strings).
|
|
108
108
|
- **CONNECTIONS on the selected node** — the inspector's last section shows what
|
|
109
109
|
the node **emits** and what it **receives**, each row being signal → target →
|
|
110
110
|
handler with a ✕ to remove and `+ connect` to add. Signals come from the node
|
|
@@ -315,6 +315,15 @@ toward `target.position + offset`. Works in 2D and 3D.
|
|
|
315
315
|
"script": { "name": "FollowCamera", "props": { "target": "/root/Player", "smoothing": 0.9 } } }
|
|
316
316
|
```
|
|
317
317
|
|
|
318
|
+
**Not together with `CharacterController3D`.** That controller drives the
|
|
319
|
+
scene's current `Camera3D` itself, in every one of its views — so a
|
|
320
|
+
`FollowCamera` on the same camera is a second writer, and the two fight for the
|
|
321
|
+
position every frame. Measured on a scene with both, target at (10, 0, 0): the
|
|
322
|
+
camera settled at (6.59, 0.22, 1.36), which is neither answer, with no error
|
|
323
|
+
anywhere. Use the controller's own `view` / `camDistance` for a third-person
|
|
324
|
+
camera, and keep `FollowCamera` for the cameras nothing else drives.
|
|
325
|
+
`incanto check` reports the pair.
|
|
326
|
+
|
|
318
327
|
## Patrol
|
|
319
328
|
|
|
320
329
|
Walk a node along fixed waypoints at constant `speed` — guards, moving platforms.
|
|
@@ -504,6 +513,11 @@ pickups, spinning/pulsing coins. Drives one `axis` of `position`, `rotation`
|
|
|
504
513
|
"script": { "name": "Oscillate", "props": { "axis": "z", "amplitude": 180, "frequency": 0.5, "mode": "rotation" } } }
|
|
505
514
|
```
|
|
506
515
|
|
|
516
|
+
On a physics BODY, `mode: "rotation"` turns the collider too — a spinning blade
|
|
517
|
+
cuts, a swinging gate blocks. (Before 0.62 it turned only the mesh: a body's
|
|
518
|
+
angle was applied once at creation and never followed again, so every spinner
|
|
519
|
+
built this way was decoration.)
|
|
520
|
+
|
|
507
521
|
## Buoyancy
|
|
508
522
|
|
|
509
523
|
Float on water. Hangs on a `RigidBody3D` and asks the water how high it is at
|
|
@@ -576,7 +590,7 @@ later clones get `EnemyTemplate2`, `EnemyTemplate3`, …)
|
|
|
576
590
|
| `prefab` | `""` | node path of the template to clone (required) |
|
|
577
591
|
| `interval` | `1` | seconds between spawns |
|
|
578
592
|
| `max` | `0` | max LIVE instances (0 = unlimited) |
|
|
579
|
-
| `at` | `[]` | offset
|
|
593
|
+
| `at` | `[]` | where the clone appears, as an offset from the spawner ([x,y(,z)]) |
|
|
580
594
|
| `autoStart` | `true` | begin ticking at ready |
|
|
581
595
|
| `total` | `0` | total LIFETIME spawns (0 = infinite) |
|
|
582
596
|
|
|
@@ -625,6 +639,15 @@ MOVEMENT by design: **compose** it with `DamageOnContact` (deal damage), `Lifeti
|
|
|
625
639
|
`direction` is a vector OR `'forward'` (from the node `rotation`); it's a
|
|
626
640
|
union-typed prop so its default is `null` (= `'forward'`).
|
|
627
641
|
|
|
642
|
+
In 3D, `'forward'` is the engine's ONE facing convention — **+Z**, the same
|
|
643
|
+
`atan2(dx, dz)` basis `CharacterController3D` uses and `incanto-3d-character.md`
|
|
644
|
+
spells out under "the +Z-FORWARD rule". So aim the bullet (or its shooter) with
|
|
645
|
+
that formula and it flies at what you aimed at; do not add 180°. (Before 0.62
|
|
646
|
+
this returned three.js's −Z instead, so every `'forward'` shot left the muzzle
|
|
647
|
+
pointing at the shooter's back — at the right speed, with no error, which reads
|
|
648
|
+
as a gun that simply never hits anything.) In 2D `rotation` is a scalar and
|
|
649
|
+
`'forward'` is +x turned clockwise by it, unchanged.
|
|
650
|
+
|
|
628
651
|
| Prop | Default | Meaning |
|
|
629
652
|
|---|---|---|
|
|
630
653
|
| `speed` | `300` | units per second along `direction` |
|
|
@@ -822,7 +845,9 @@ import { CameraShake, Cooldown, hitStop, screenFlash } from 'incanto/gameplay';
|
|
|
822
845
|
- **hitStop(engine, seconds?)** — freezes `engine.timeScale` for REAL
|
|
823
846
|
seconds then restores; stacked calls extend. Sells melee impacts. It measures
|
|
824
847
|
those seconds on `engine.unscaledTime`, so it thaws in a headless session
|
|
825
|
-
exactly as it does in a browser
|
|
848
|
+
exactly as it does in a browser, and a `setScene` under a live freeze drops it
|
|
849
|
+
(the swap resets that clock, and the killing blow — hit-stop, then restart —
|
|
850
|
+
is exactly when the two meet).
|
|
826
851
|
- **engine.timeScale** — 1 realtime, 0.5 slow motion, 0 pause: scales
|
|
827
852
|
variable AND fixed updates together (physics, timers, behaviors) — and it
|
|
828
853
|
means the same thing headless, so a pause menu and a slow-motion finish can be
|
|
@@ -848,6 +873,13 @@ puff, a 2D `Label`. It reads the start position ONCE, so the ease does not
|
|
|
848
873
|
compound, and it frees the node a frame after it is invisible rather than on the
|
|
849
874
|
frame it gets there (which reads as a flicker).
|
|
850
875
|
|
|
876
|
+
**`opacity` is not optional.** A node without one still rises and still frees
|
|
877
|
+
itself, and skips the fade entirely — the number pops out at full brightness,
|
|
878
|
+
which is the thing this behavior exists to prevent. It says so at ready, naming
|
|
879
|
+
the node, rather than leaving you to spot a missing fade by eye. (The 2D `Label`
|
|
880
|
+
in the list above is the reason: it had no `opacity` prop until 0.57, so the one
|
|
881
|
+
2D node the doc named was the one it could not fade.)
|
|
882
|
+
|
|
851
883
|
## Did the effect fire?
|
|
852
884
|
|
|
853
885
|
Sound has `engine.audio`; vision has **`engine.effects`**, and the question is
|
|
@@ -933,6 +965,11 @@ Terminal states ignore further transitions; the restart action (or
|
|
|
933
965
|
physics, input. Listen to the `flowChanged(state)` signal for custom UI.
|
|
934
966
|
`restartScene(engine)` is exported standalone.
|
|
935
967
|
|
|
968
|
+
**Any scene swap thaws.** The freeze lives on the engine, not on the flow, so
|
|
969
|
+
leaving a game-over screen by your own route — `engine.setScene(loadScene(next))`
|
|
970
|
+
from a menu button — puts `timeScale` back to 1 as surely as `flow.restart()`
|
|
971
|
+
does. You cannot restart into a frozen level.
|
|
972
|
+
|
|
936
973
|
Multi-scene games: `flow.goToScene(nextSceneJson, { fadeSeconds: 0.4 })`
|
|
937
974
|
fades to black, swaps, fades back (headless = instant). Title → level →
|
|
938
975
|
next level is three JSON files and this one call.
|
package/skills/incanto-hud.md
CHANGED
|
@@ -225,9 +225,11 @@ slot, the slot takes it. **Who owns the item is your game's business** — these
|
|
|
225
225
|
report the GESTURE, not a model, so an inventory that stacks, swaps or refuses is
|
|
226
226
|
your `connections` and a behavior, not a prop nobody could have guessed.
|
|
227
227
|
|
|
228
|
-
A drop on a CHILD of a slot counts as a drop on the slot
|
|
229
|
-
icon inside it, which is the normal case
|
|
230
|
-
|
|
228
|
+
A drop on a CHILD of a slot counts as a drop on the slot — the cursor lands on
|
|
229
|
+
the icon inside it, which is the normal case, and the icon is itself a widget.
|
|
230
|
+
A drop on nothing cancels, and so does putting a thing back into the slot it
|
|
231
|
+
came from: that is not a move, and firing `dropped` for it would make every
|
|
232
|
+
mis-grab look like a transfer.
|
|
231
233
|
|
|
232
234
|
|
|
233
235
|
|
|
@@ -99,6 +99,13 @@ banner.show(this.engine.t('hud.wave', { n: this.wave }));
|
|
|
99
99
|
slot; a scene-JSON prop has no params to fill it from. A slot with no matching
|
|
100
100
|
param is left alone rather than blanked.
|
|
101
101
|
|
|
102
|
+
### Authoring the table
|
|
103
|
+
|
|
104
|
+
The scene header's **advanced** section has a `strings` field — the same
|
|
105
|
+
raw-JSON editor `input` and `multiplayer` use. Until 0.61 it was the one of
|
|
106
|
+
those three the panel did not show, so a localized game could be read by the
|
|
107
|
+
editor and never authored in it.
|
|
108
|
+
|
|
102
109
|
## Checking a translation without a browser
|
|
103
110
|
|
|
104
111
|
No headless check could see a translated string: a capture printed
|
|
@@ -482,6 +482,7 @@ Signals: `movementStateChanged(state)`
|
|
|
482
482
|
| `color` | `"#ffffff"` | string |
|
|
483
483
|
| `font` | `"monospace"` | string |
|
|
484
484
|
| `align` | `"left"` | string |
|
|
485
|
+
| `opacity` | `1` | number |
|
|
485
486
|
|
|
486
487
|
## `Label3D` — `incanto`
|
|
487
488
|
|
|
@@ -705,6 +706,17 @@ Signals: `finished`
|
|
|
705
706
|
|
|
706
707
|
Signals: `finished`
|
|
707
708
|
|
|
709
|
+
## `Respawn` — `incanto`
|
|
710
|
+
|
|
711
|
+
| Prop | Default | Kind |
|
|
712
|
+
|---|---|---|
|
|
713
|
+
| `target` | `".."` | node path |
|
|
714
|
+
| `below` | `null` | null |
|
|
715
|
+
| `to` | `[]` | array |
|
|
716
|
+
| `resetVelocity` | `true` | boolean |
|
|
717
|
+
|
|
718
|
+
Signals: `respawned`
|
|
719
|
+
|
|
708
720
|
## `RigidBody2D` — `incanto/2d`
|
|
709
721
|
|
|
710
722
|
| 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
|
|
|
@@ -299,7 +299,11 @@ 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`. On **both**, it is
|
|
303
|
+
`AMBIGUOUS_HANDLER`: the node's method wins, so the script's would never run
|
|
304
|
+
and nothing would say so. A core node answers to 62 public methods before any
|
|
305
|
+
adapter adds more (`stop` `play` `show` `clear` `say` `start` `free` …), which
|
|
306
|
+
is why the collision is easy to write — rename the script's method. Script names themselves must be
|
|
303
307
|
registered (`registerBehavior`) before `loadScene` → `UNKNOWN_BEHAVIOR` otherwise.
|
|
304
308
|
- Unresolvable `from`/`to` → `DANGLING_CONNECTION` at load. Renaming a node breaks its
|
|
305
309
|
connections **loudly** — update paths in the same edit.
|
|
@@ -336,6 +340,7 @@ that do exist at the failing spot).
|
|
|
336
340
|
| `DUPLICATE_UNIQUE_NAME` | `%Name` matches ≥2 nodes | rename one, or use an explicit path |
|
|
337
341
|
| `DANGLING_CONNECTION` | connection `from`/`to` unresolvable | fix the path after renames |
|
|
338
342
|
| `UNKNOWN_HANDLER` | handler missing on the node AND its behavior | fix the method name |
|
|
343
|
+
| `AMBIGUOUS_HANDLER` | handler is a method on the node AND on its behavior — the node's wins silently | rename the behavior's method |
|
|
339
344
|
| `UNKNOWN_SIGNAL` | connection (or emit/on) names a signal the from node never declares | use a declared signal, or declare it (`static signals` / `declareSignal`) |
|
|
340
345
|
| `UNKNOWN_BEHAVIOR` | `script.name` not registered | `registerBehavior(name, Class)` before `loadScene` |
|
|
341
346
|
| `UNRESOLVED_INSTANCE` | no resolver, unknown path, or instance cycle | provide/fix `resolveScene`, break the cycle |
|
|
@@ -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
|