incanto 0.61.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-verify.mjs +134 -55
- package/dist/2d.d.ts +42 -0
- package/dist/2d.js +3 -3
- package/dist/3d.js +5 -5
- package/dist/{create-game-DH7JI5xx.js → create-game-BpunnGPX.js} +8 -8
- package/dist/{create-game-IX5lEH0P.js → create-game-Caut3bqN.js} +6 -6
- package/dist/{duplicate-CGqAmK2h.js → duplicate-E4FUs5Bn.js} +1 -1
- package/dist/editor.js +39 -18
- package/dist/{environment-presets-CNxCuhZF.js → environment-presets-BAWeOeqf.js} +5 -3
- package/dist/{gameplay-Dtzd2itW.js → gameplay-CaHqDiQD.js} +126 -15
- 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-DEe272nY.js} +103 -0
- package/dist/net.js +1 -1
- package/dist/{physics-2d-CBnor8Zf.js → physics-2d-BXmu2i7W.js} +11 -3
- package/dist/{physics-3d-BTUfUSWO.js → physics-3d-ClxP6Uv7.js} +121 -17
- package/dist/react.js +1 -1
- package/dist/{register-DL3izw8j.js → register-CNh4FlbD.js} +60 -9
- package/dist/{register-xuSRyD6b.js → register-D3yx8D4r.js} +158 -2
- package/dist/{replay-D7-yle3s.js → replay-DlgHItNv.js} +15 -2
- package/dist/{split-screen-5Ban4q4n.js → split-screen-CBM9wcMX.js} +2 -2
- package/dist/{src-DozXvyZS.js → src-CH00_JsR.js} +1 -1
- package/dist/{teardown-B6rwJOyS.js → teardown-C7qP-dcC.js} +1 -1
- package/dist/{test-Dch_7VQD.js → test-BMkg8zMV.js} +27 -15
- package/dist/test.d.ts +37 -0
- package/dist/test.js +2 -2
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-CF1JL2tR.js → agent8-m5mtAO_A.js} +1 -1
- package/editor/assets/{debug-3QzYhOPA.js → debug-CPhzCT8f.js} +1 -1
- package/editor/assets/{index-CAD2c5ug.js → index-D422P4kW.js} +91 -91
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +87 -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-gameplay-behaviors.md +15 -1
- package/skills/incanto-node-reference.md +11 -0
- package/skills/incanto-physics-and-input.md +23 -2
- 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/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
|
},
|
|
@@ -5327,6 +5330,90 @@
|
|
|
5327
5330
|
},
|
|
5328
5331
|
"required": ["name", "type"]
|
|
5329
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
|
+
},
|
|
5330
5417
|
"RigidBody2D": {
|
|
5331
5418
|
"type": "object",
|
|
5332
5419
|
"x-signals": ["triggerEnter", "triggerExit"],
|
|
@@ -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`
|
|
@@ -513,6 +513,11 @@ pickups, spinning/pulsing coins. Drives one `axis` of `position`, `rotation`
|
|
|
513
513
|
"script": { "name": "Oscillate", "props": { "axis": "z", "amplitude": 180, "frequency": 0.5, "mode": "rotation" } } }
|
|
514
514
|
```
|
|
515
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
|
+
|
|
516
521
|
## Buoyancy
|
|
517
522
|
|
|
518
523
|
Float on water. Hangs on a `RigidBody3D` and asks the water how high it is at
|
|
@@ -585,7 +590,7 @@ later clones get `EnemyTemplate2`, `EnemyTemplate3`, …)
|
|
|
585
590
|
| `prefab` | `""` | node path of the template to clone (required) |
|
|
586
591
|
| `interval` | `1` | seconds between spawns |
|
|
587
592
|
| `max` | `0` | max LIVE instances (0 = unlimited) |
|
|
588
|
-
| `at` | `[]` | offset
|
|
593
|
+
| `at` | `[]` | where the clone appears, as an offset from the spawner ([x,y(,z)]) |
|
|
589
594
|
| `autoStart` | `true` | begin ticking at ready |
|
|
590
595
|
| `total` | `0` | total LIFETIME spawns (0 = infinite) |
|
|
591
596
|
|
|
@@ -634,6 +639,15 @@ MOVEMENT by design: **compose** it with `DamageOnContact` (deal damage), `Lifeti
|
|
|
634
639
|
`direction` is a vector OR `'forward'` (from the node `rotation`); it's a
|
|
635
640
|
union-typed prop so its default is `null` (= `'forward'`).
|
|
636
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
|
+
|
|
637
651
|
| Prop | Default | Meaning |
|
|
638
652
|
|---|---|---|
|
|
639
653
|
| `speed` | `300` | units per second along `direction` |
|
|
@@ -706,6 +706,17 @@ Signals: `finished`
|
|
|
706
706
|
|
|
707
707
|
Signals: `finished`
|
|
708
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
|
+
|
|
709
720
|
## `RigidBody2D` — `incanto/2d`
|
|
710
721
|
|
|
711
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
|
|
|
@@ -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
|