incanto 0.70.0 → 0.71.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 +36 -98
- package/dist/2d.d.ts +18 -3
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +3 -3
- package/dist/3d.js +6 -6
- package/dist/{agent8-CmNF01gA.js → agent8-Cfmd3ar_.js} +1 -1
- package/dist/{audio-player-DaMxqfNE.d.ts → audio-player-L8yccdMP.d.ts} +1 -1
- package/dist/{behavior-DZExDn9o.d.ts → behavior-Dcz0fr1S.d.ts} +11 -0
- package/dist/{create-game-Bwvh6q8A.js → create-game-CH4kt78v.js} +7 -7
- package/dist/{create-game-C7ffQWW7.js → create-game-LCHBLfA4.js} +7 -7
- package/dist/debug.d.ts +1 -1
- package/dist/editor.js +29 -25
- package/dist/{environment-presets-D6Q5BxeE.js → environment-presets-99BzP_L-.js} +3 -3
- package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
- package/dist/{gameplay-BfHkuzVb.js → gameplay-CNULJvwh.js} +108 -6
- package/dist/gameplay.d.ts +68 -4
- package/dist/gameplay.js +2 -2
- package/dist/index.d.ts +27 -4
- package/dist/index.js +4 -4
- package/dist/{loader-Cff09LMm.d.ts → loader-8-IHvbKD.d.ts} +1 -1
- package/dist/net.d.ts +2 -2
- package/dist/net.js +2 -2
- package/dist/{physics-2d-CE0Qvy3V.js → physics-2d-FhrXUc6B.js} +2 -2
- package/dist/{physics-3d-CpH-2gn5.js → physics-3d-CFEGxBXZ.js} +3 -3
- package/dist/{picking-CQJ_PJKh.js → picking-DVo7fI13.js} +3 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-Bkk0wSDB.js → register-BYQCBySi.js} +100 -8
- package/dist/{register-6DYnKZcy.js → register-DuVdzxbq.js} +49 -15
- package/dist/{replay-DjAkAzMq.js → replay-DEvp3kyV.js} +2 -2
- package/dist/{replay-Dmw-PKQu.d.ts → replay-F7IZHdFR.d.ts} +1 -1
- package/dist/{save-slots-BXVg148r.js → save-slots-Bvuh2p_r.js} +56 -3
- package/dist/{split-screen--k-XpBjr.d.ts → split-screen-CIYf1zSf.d.ts} +2 -2
- package/dist/{split-screen-PL78oVXP.js → split-screen-eULetcg-.js} +3 -3
- package/dist/{src-Cxfiv1Hg.js → src-_jk0qLsS.js} +1 -1
- package/dist/{test-iHYVUcDK.js → test-CmN54kWv.js} +252 -24
- package/dist/test.d.ts +113 -5
- package/dist/test.js +3 -3
- package/dist/vite.js +3 -3
- package/editor/assets/{agent8-D0MS174y.js → agent8-yLIEYHbd.js} +1 -1
- package/editor/assets/{debug-BnXkKuYu.js → debug-u31w_yhq.js} +1 -1
- package/editor/assets/{index-CIu3uc3l.js → index-DK9xMGpW.js} +94 -94
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +4 -0
- package/skills/incanto-behaviors-and-scripts.md +30 -9
- package/skills/incanto-building-2d-games.md +20 -2
- package/skills/incanto-gameplay-behaviors.md +63 -1
- package/skills/incanto-hud.md +14 -0
- package/skills/incanto-node-reference.md +18 -2
- package/skills/incanto-playtesting.md +34 -0
- package/skills/incanto-save-slots.md +56 -1
- package/skills/incanto-verifying-your-game.md +11 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/game.scene.json +44 -226
- package/templates-app/molehill-2d/package.json +1 -1
- package/templates-app/molehill-2d/src/game.scene.json +10 -60
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/platformer-2d/src/game.scene.json +80 -416
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/star-survivor/src/game.scene.json +5 -24
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/game.scene.json +13 -71
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/src/behaviors.ts +21 -4
- package/templates-app/village-quest-3d/src/grove.scene.json +16 -87
- package/templates-app/village-quest-3d/src/village.scene.json +5 -27
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-DK9xMGpW.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="./assets/GameServer-C56iOUgF.js">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -177,7 +177,7 @@ What behavior code actually calls at runtime — all instance methods, no global
|
|
|
177
177
|
| `engine.stop()` / `engine.start()` | pause / resume the loop — stop resets the clock and accumulator, so no banked sim time leaks into the resume |
|
|
178
178
|
| `engine.step()` | advance exactly ONE fixed step + one update (both dt = the fixed step) — the unit of time for headless tests |
|
|
179
179
|
| `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 |
|
|
180
|
-
| `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 |
|
|
180
|
+
| `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. It fires with **null** at `dispose()` too — a handler that reads `scene.root` has to check, or teardown says so and continues without it |
|
|
181
181
|
| `node.off(signal, fn)` | disconnect ONE listener you connected with `on` — pass the same function reference. (`free()` disconnects everything a node owns, so this is for a listener that must stop while the node lives on) |
|
|
182
182
|
| `node.signal('died')` | the `Signal` object itself, for `connect`/`disconnect` by hand; undeclared names throw, like `emit` |
|
|
183
183
|
| `node.listenerCount('died')` | how many are listening — a test's way to prove a wire was made, or dropped |
|
|
@@ -187,19 +187,40 @@ The recurring traps: `duplicateNode` does NOT insert the clone anywhere — a
|
|
|
187
187
|
"spawner that does nothing" usually forgot `addChild`.
|
|
188
188
|
|
|
189
189
|
**The template you clone FROM is a live node.** Its behaviors run, its timers
|
|
190
|
-
tick, its turret shoots — `visible: false` hides a
|
|
191
|
-
off. `Spawner` and `WaveSpawner` sidestep this by
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
tick, its turret shoots, its `autoplay` audio plays — `visible: false` hides a
|
|
191
|
+
node, it does not switch it off. `Spawner` and `WaveSpawner` sidestep this by
|
|
192
|
+
DETACHING their `prefab` at enter. For a shelf of templates you clone yourself,
|
|
193
|
+
put `PrefabShelf` on the shelf node and it does the same for all of them:
|
|
194
194
|
|
|
195
|
+
```json
|
|
196
|
+
{ "name": "Prefabs", "type": "Node2D", "script": { "name": "PrefabShelf" },
|
|
197
|
+
"children": [ { "name": "Tower", "type": "Node2D", "children": [] } ] }
|
|
198
|
+
```
|
|
195
199
|
```ts
|
|
196
|
-
const
|
|
197
|
-
tower.
|
|
200
|
+
const shelf = this.node.getNode('/Game/Prefabs').behavior as PrefabShelf;
|
|
201
|
+
const tower = shelf.make('Tower'); // detached, awake all the way down
|
|
202
|
+
tower.position = at; // set it up BEFORE it readies
|
|
198
203
|
this.node.getNode('/Game/Towers').addChild(tower);
|
|
199
204
|
```
|
|
200
205
|
|
|
201
|
-
|
|
202
|
-
|
|
206
|
+
Its children never ENTER the tree, so nothing has to be hidden and nothing has
|
|
207
|
+
to be asleep. `make()` returns the clone DETACHED, like `duplicateNode` — not a
|
|
208
|
+
formality: `onReady` fires on attach, and a behavior that banks its node's
|
|
209
|
+
position there (`FloatAway`) would bank the wrong one. `names()` lists what the
|
|
210
|
+
shelf holds, and asking for anything else fails saying so.
|
|
211
|
+
|
|
212
|
+
**Why not just author the templates `"enabled": false`?** That was the old
|
|
213
|
+
advice and it half-works: `enabled` is a per-behavior pause switch, so
|
|
214
|
+
`clone.behavior?.enable()` wakes ONE node — while the shape this same skill
|
|
215
|
+
teaches (one behavior per node, so a prefab's parts live on children) leaves
|
|
216
|
+
every child asleep, silently and for good. Measured on a shipped tower defense:
|
|
217
|
+
the bolt's root `Projectile` was woken and its sibling `Lifetime` was not, so
|
|
218
|
+
every bolt that MISSED flew forever — five still in the tree at 69 seconds. And
|
|
219
|
+
`enabled` never silenced the template's `autoplay` audio at all, which is how
|
|
220
|
+
that game's `feels` rung read `every one of the 4 emitters fired` while two of
|
|
221
|
+
the four were templates going off on the shelf. `enabled: false` is for a state
|
|
222
|
+
a node deliberately starts in (a `Chase` waiting on `spotted`), not for hiding a
|
|
223
|
+
template from the tree. **And look the template up by PATH, not `%Name`**: a clone keeps its
|
|
203
224
|
template's name, so `%Tower` is unambiguous exactly until the first one is
|
|
204
225
|
placed, and then it throws `DUPLICATE_UNIQUE_NAME` from inside your build
|
|
205
226
|
handler — a game that works once. `engine.tick(16)` does
|
|
@@ -105,8 +105,26 @@ stop hand-placing dozens of ColorRect walls:
|
|
|
105
105
|
- Cell (0,0) hangs its TOP-LEFT on the node's origin; position the node to
|
|
106
106
|
place the level. `columns: 0` (default) derives atlas columns from the
|
|
107
107
|
texture width.
|
|
108
|
-
- No
|
|
109
|
-
|
|
108
|
+
- **No art yet? Paint with `colors`.** A char → `#rrggbb` map draws that cell as
|
|
109
|
+
a flat quad, in the same single draw call, with no atlas at all:
|
|
110
|
+
|
|
111
|
+
```jsonc
|
|
112
|
+
{ "name": "Walls", "type": "TileMap2D", "props": {
|
|
113
|
+
"tileSize": 56,
|
|
114
|
+
"cells": ["########", "#......#", "########"],
|
|
115
|
+
"colors": { "#": "#3a4356" },
|
|
116
|
+
"solid": ["#"]
|
|
117
|
+
} }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Exclusive with `texture` (one material, one draw call — a half-textured level
|
|
121
|
+
that loads clean is exactly the silence this engine fails loudly about), and
|
|
122
|
+
`solid` takes the CHAR you painted as well as an atlas index, because that is
|
|
123
|
+
the thing an author knows. Everything else is unchanged: `cellAt`, `tileAt`,
|
|
124
|
+
`setTile`, `worldAt` and the merged colliders work the same. It exists because
|
|
125
|
+
a sokoban built from the published package came out as 24 hand-placed
|
|
126
|
+
`ColorRect2D` walls — the one node that answers every question a grid game
|
|
127
|
+
asks could not be SEEN.
|
|
110
128
|
- Change the map at runtime by REPLACING `cells` (mutations are not watched):
|
|
111
129
|
`map.cells = [...rows]` — geometry and colliders rebuild next frame. For ONE
|
|
112
130
|
cell use `setTile` below, which does that correctly.
|
|
@@ -803,6 +803,44 @@ a `draft` that puts the waterline on it:
|
|
|
803
803
|
For the water's own height anywhere (jetties, spawn heights, an AI deciding
|
|
804
804
|
where the shore is), `Water3D.heightAt(x, z)` answers the same surface.
|
|
805
805
|
|
|
806
|
+
## PrefabShelf
|
|
807
|
+
|
|
808
|
+
The shelf of templates the GAME clones itself — a tower the player places, a
|
|
809
|
+
bolt that tower fires, the `+14` a kill pays. `Spawner` owns its one prefab and
|
|
810
|
+
detaches it; this owns a whole shelf of them and detaches all of them.
|
|
811
|
+
|
|
812
|
+
```json
|
|
813
|
+
{ "name": "Prefabs", "type": "Node2D", "script": { "name": "PrefabShelf" },
|
|
814
|
+
"children": [
|
|
815
|
+
{ "name": "Tower", "type": "Node2D", "children": [] },
|
|
816
|
+
{ "name": "Bolt", "type": "Node2D", "children": [] }
|
|
817
|
+
] }
|
|
818
|
+
```
|
|
819
|
+
```ts
|
|
820
|
+
const shelf = this.node.getNode('/Game/Prefabs').behavior as PrefabShelf;
|
|
821
|
+
const tower = shelf.make('Tower'); // detached, awake all the way down
|
|
822
|
+
tower.position = at; // set it up BEFORE it readies
|
|
823
|
+
this.node.getNode('/Game/Towers').addChild(tower);
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
| Prop | Default | Meaning |
|
|
827
|
+
|---|---|---|
|
|
828
|
+
| `prefabs` | `""` | comma-separated child names to take; empty takes them ALL |
|
|
829
|
+
|
|
830
|
+
Methods: `make(name)` · `names()`
|
|
831
|
+
|
|
832
|
+
- **Its children never enter the tree.** Claimed in `onEnterTree` (parent-first,
|
|
833
|
+
so a template carrying its own shelf is taken intact), so nothing has to be
|
|
834
|
+
`visible: false` and nothing has to be `"enabled": false`. A template left in
|
|
835
|
+
the tree is a live node: its behaviors tick, and its `autoplay` audio plays.
|
|
836
|
+
- **`make()` returns the clone DETACHED**, like `duplicateNode` — `onReady`
|
|
837
|
+
fires on attach, and a behavior that banks its node's position there
|
|
838
|
+
(`FloatAway`) would bank the wrong one. Set it up, then `addChild`.
|
|
839
|
+
- **A clone is reported as the template it came from.** `wiringPath()` answers
|
|
840
|
+
with the authored address wherever the clone lands, so a hundred towers are
|
|
841
|
+
one emitter in the audio/effects logs and in the `feels` rung — the promise
|
|
842
|
+
those logs already made for a spawner's waves, now true for a shelf too.
|
|
843
|
+
|
|
806
844
|
## Spawner
|
|
807
845
|
|
|
808
846
|
Drip-feed clones of a TEMPLATE into the scene on a timer — enemy generators,
|
|
@@ -854,7 +892,17 @@ also frees timed spawns.
|
|
|
854
892
|
| `waves` | `[]` | array of `{ prefab, count, interval, delayBefore }` (required) |
|
|
855
893
|
| `autoStart` | `true` | begin the first wave at ready |
|
|
856
894
|
|
|
857
|
-
Signals: `waveStarted(index)` · `waveCleared(index)` ·
|
|
895
|
+
Signals: `spawned(node)` · `waveStarted(index)` · `waveCleared(index)` ·
|
|
896
|
+
`allCleared`
|
|
897
|
+
|
|
898
|
+
`spawned` hands you the clone itself, before it has been stepped — the route to
|
|
899
|
+
anything PER-ENEMY that the template cannot say: a difficulty setting that
|
|
900
|
+
raises health, an elite that gets a tint, a list your own code keeps, a target
|
|
901
|
+
handed over on arrival.
|
|
902
|
+
|
|
903
|
+
```json
|
|
904
|
+
{ "signal": "spawned", "from": "Waves", "to": "Game", "handler": "onRaider" }
|
|
905
|
+
```
|
|
858
906
|
|
|
859
907
|
```json
|
|
860
908
|
{ "name": "Waves", "type": "Node2D",
|
|
@@ -1290,6 +1338,20 @@ swap into a physics scene run unsimulated — imperceptible, and the alternative
|
|
|
1290
1338
|
was a game that never simulated. `physics: false` stays off across swaps, and a
|
|
1291
1339
|
scene with no bodies still pays for nothing.
|
|
1292
1340
|
|
|
1341
|
+
**The line after a swap runs in a node that no longer exists.** `goToScene` and
|
|
1342
|
+
`setScene` FREE the tree they replace, and `this.engine` is
|
|
1343
|
+
`this.node.tree.engine` — so a Continue button that swaps and then talks to what
|
|
1344
|
+
arrived throws on its second line. Hold the engine first:
|
|
1345
|
+
|
|
1346
|
+
```ts
|
|
1347
|
+
const engine = this.engine; // BEFORE the swap
|
|
1348
|
+
goToScene(engine, villageJson, { fadeSeconds: 0 });
|
|
1349
|
+
(engine.scene?.root.behavior as Hold).continueGame();
|
|
1350
|
+
```
|
|
1351
|
+
|
|
1352
|
+
The engine says so by name (`has been FREED — a scene swap frees the tree it
|
|
1353
|
+
replaces`), and `node.freed` answers the same question in code.
|
|
1354
|
+
|
|
1293
1355
|
## DayNight (live 24-hour cycle)
|
|
1294
1356
|
|
|
1295
1357
|
Drives the scene `environment` (needs an atmosphere sky): sun elevation/
|
package/skills/incanto-hud.md
CHANGED
|
@@ -259,6 +259,20 @@ widget use `UiButton`'s `pressed`.
|
|
|
259
259
|
Setting `.value` from a behavior updates the control and does **NOT** re-emit
|
|
260
260
|
`changed` — restoring a saved setting must not fire the handler that saved it.
|
|
261
261
|
|
|
262
|
+
**`choose(value)` is the other half**: set it as a PERSON would, and say so.
|
|
263
|
+
`UiButton` has always had `press()`; the value widgets had nothing, so a preset
|
|
264
|
+
button, a "reset to defaults", a tutorial that moves a slider for you, or a
|
|
265
|
+
harness checking its own options screen had no public route at all.
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
(hud.getNode('%Volume') as UiSlider).choose(0.25); // clamped, emits changed
|
|
269
|
+
(hud.getNode('%Invert') as UiToggle).choose(true);
|
|
270
|
+
(hud.getNode('%Quality') as UiSelect).choose('high'); // an option it does not
|
|
271
|
+
// offer is refused
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Both are silent when nothing changes.
|
|
275
|
+
|
|
262
276
|
**Every prop is live.** `color`, `size`, `width`, `background`, `label` and
|
|
263
277
|
`anchor` are re-read each frame, so flashing the score red, growing a health bar
|
|
264
278
|
or moving a widget to another corner all work from a behavior — and from the
|
|
@@ -656,7 +656,7 @@ _No props (structural fields only)._
|
|
|
656
656
|
|
|
657
657
|
Methods: `addChild(child)` · `removeChild(child)` · `reparent(newParent)` · `findChild(name, recursive?)` · `getRoot()` · `getNodeByUid(uid)` · `getNodesByName(name)` · `getPath()` · `wiringPath()` · `getNode(path)` · `getNodeOrNull(path)` · `addToGroup(group)` · `removeFromGroup(group)` · `isInGroup(group)` · `declareSignal(name)` · `declaredSignalNames()` · `signal(name)` · `on(signal, fn, opts?)` · `off(signal, fn)` · `listenerCount(signal)` · `emit(signal, ...args)` · `queueFree()` · `free()`
|
|
658
658
|
|
|
659
|
-
Reads: `uid: string | null` · `name: string` · `parent: Node | null` · `children: readonly Node[]` · `groups: ReadonlySet<string>` · `tree: SceneTree | null` · `isReady: boolean`
|
|
659
|
+
Reads: `uid: string | null` · `name: string` · `parent: Node | null` · `children: readonly Node[]` · `groups: ReadonlySet<string>` · `tree: SceneTree | null` · `isReady: boolean` · `freed: boolean`
|
|
660
660
|
|
|
661
661
|
## `Node2D` — `incanto/2d`
|
|
662
662
|
|
|
@@ -1009,6 +1009,7 @@ Methods: `heightAt(x, z)` · `resolvedLayers()` · `free()`
|
|
|
1009
1009
|
| `columns` | `0` | number |
|
|
1010
1010
|
| `cells` | `[]` | array |
|
|
1011
1011
|
| `legend` | `{}` | object |
|
|
1012
|
+
| `colors` | `{}` | object |
|
|
1012
1013
|
| `solid` | `[]` | array |
|
|
1013
1014
|
| `opacity` | `1` | number |
|
|
1014
1015
|
|
|
@@ -1295,6 +1296,8 @@ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `change
|
|
|
1295
1296
|
|
|
1296
1297
|
Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
|
|
1297
1298
|
|
|
1299
|
+
Methods: `choose(value)`
|
|
1300
|
+
|
|
1298
1301
|
## `UiSlider` — `incanto`
|
|
1299
1302
|
|
|
1300
1303
|
| Prop | Default | Kind |
|
|
@@ -1314,6 +1317,8 @@ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `change
|
|
|
1314
1317
|
|
|
1315
1318
|
Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
|
|
1316
1319
|
|
|
1320
|
+
Methods: `choose(value)`
|
|
1321
|
+
|
|
1317
1322
|
## `UiText` — `incanto`
|
|
1318
1323
|
|
|
1319
1324
|
| Prop | Default | Kind |
|
|
@@ -1349,6 +1354,8 @@ Reads: `shown: string`
|
|
|
1349
1354
|
|
|
1350
1355
|
Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
|
|
1351
1356
|
|
|
1357
|
+
Methods: `choose(value)`
|
|
1358
|
+
|
|
1352
1359
|
## `UiVolumeSlider` — `incanto`
|
|
1353
1360
|
|
|
1354
1361
|
| Prop | Default | Kind |
|
|
@@ -1712,6 +1719,15 @@ Signals: `reachedPoint(index)`
|
|
|
1712
1719
|
|
|
1713
1720
|
Signals: `collected(value, other)`
|
|
1714
1721
|
|
|
1722
|
+
### `PrefabShelf`
|
|
1723
|
+
|
|
1724
|
+
| Prop | Default | Kind |
|
|
1725
|
+
|---|---|---|
|
|
1726
|
+
| `enabled` | `true` | boolean |
|
|
1727
|
+
| `prefabs` | `""` | string |
|
|
1728
|
+
|
|
1729
|
+
Methods: `names()` · `make(name)`
|
|
1730
|
+
|
|
1715
1731
|
### `Projectile`
|
|
1716
1732
|
|
|
1717
1733
|
| Prop | Default | Kind |
|
|
@@ -1789,7 +1805,7 @@ Reads: `liveCount: number`
|
|
|
1789
1805
|
| `waves` | `[]` | array · **required** |
|
|
1790
1806
|
| `autoStart` | `true` | boolean |
|
|
1791
1807
|
|
|
1792
|
-
Signals: `waveStarted(index)` · `waveCleared(index)` · `allCleared`
|
|
1808
|
+
Signals: `spawned(node)` · `waveStarted(index)` · `waveCleared(index)` · `allCleared`
|
|
1793
1809
|
|
|
1794
1810
|
Methods: `start()`
|
|
1795
1811
|
|
|
@@ -42,6 +42,24 @@ find a door, not enough to solve a puzzle.
|
|
|
42
42
|
No renderer is involved, so twenty minutes of simulated play takes a second or
|
|
43
43
|
two.
|
|
44
44
|
|
|
45
|
+
**Two buttons it will stop pressing.** The bot drives every declared button
|
|
46
|
+
action, then notices what a player would notice. If pressing one STOPS THE
|
|
47
|
+
CLOCK, that is a pause menu, and a run spent inside one measures nothing — it
|
|
48
|
+
lets go and never presses that button again. If pressing one replaces the scene
|
|
49
|
+
with THAT SAME SCENE, that is a restart, and it does not just waste the run, it
|
|
50
|
+
ends it: the bot is bound to the tree it started in. One run of the set pays for
|
|
51
|
+
that lesson and the rest play the game. Both are reported (`the bot stopped
|
|
52
|
+
pressing …`), both are OBSERVED rather than read off the words "pause" and
|
|
53
|
+
"restart" — your game may call them anything, or nothing — and neither is a
|
|
54
|
+
defect in your game.
|
|
55
|
+
|
|
56
|
+
The second one matters more than it sounds: mid-run retry is ordinary in puzzle,
|
|
57
|
+
racing and tower-defense games, and `GameFlow` restarts only from a terminal
|
|
58
|
+
state, so the games that reload mid-play are the ones that call `restartScene`
|
|
59
|
+
themselves. Every one of them used to report `8 runs played without reaching a
|
|
60
|
+
win (8 changed scene)` — and each run died a second in, which took the emitters,
|
|
61
|
+
the hazards and the destinations down with it.
|
|
62
|
+
|
|
45
63
|
## What it reports, and where each answer comes from
|
|
46
64
|
|
|
47
65
|
| line | source |
|
|
@@ -138,6 +156,22 @@ lifted out of the tree at load, so the check reads the scene FILE as well as the
|
|
|
138
156
|
loaded tree, and a wave of forty enemies is reported as its one authored
|
|
139
157
|
emitter, not forty.
|
|
140
158
|
|
|
159
|
+
**`made and still held`** counts the clones alive when the run ended, grouped by
|
|
160
|
+
the TEMPLATE they came from, with what the count was at the halfway mark:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
· made and still held: 12 × /Game/Prefabs/Bolt (3 at halfway),
|
|
164
|
+
6 × /Game/Prefabs/Tower (3 at halfway)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Six towers from three is a player who kept building; twelve projectiles from
|
|
168
|
+
three is a projectile that never learned to stop. **Furniture holds steady; a
|
|
169
|
+
leak climbs** — and nothing in the engine can tell them apart, so both are
|
|
170
|
+
printed and the judgement is yours. It exists because a shipped tower defense
|
|
171
|
+
kept every bolt that MISSED alive forever (its `Lifetime` was asleep on the
|
|
172
|
+
prefab shelf) and no instrument said a word: the playtest was green, the audit
|
|
173
|
+
was green, the harness was green, every rung of the ladder was green.
|
|
174
|
+
|
|
141
175
|
**`nothing here can hurt you`** is a design report, not an error. Sometimes it
|
|
142
176
|
is exactly right (a walking simulator). Usually it means the hazards are not
|
|
143
177
|
wired up.
|
|
@@ -292,6 +292,49 @@ explicitly when one file is entered more than one way (`"chapter-2-rescue"`).
|
|
|
292
292
|
physics registers the new bodies before that scene's first frame — so a
|
|
293
293
|
character walks in level two exactly as it did in level one.
|
|
294
294
|
|
|
295
|
+
## State that outlives a SCENE, not just a session
|
|
296
|
+
|
|
297
|
+
A save and a scene swap are different mechanisms and the word "persist" covers
|
|
298
|
+
both, so this is the one thing a two-scene quest game has to be told: **the
|
|
299
|
+
three hooks above carry state across a SAVE and nothing across a SWAP.**
|
|
300
|
+
|
|
301
|
+
`goToScene` replaces the tree. The new scene's behaviors are new objects with
|
|
302
|
+
`onReady` starting values — a quest flag set in the village is gone when the
|
|
303
|
+
cellar loads, and nothing reports it, because nothing went wrong.
|
|
304
|
+
|
|
305
|
+
A quest that has to survive both is three parts, and each one alone fails
|
|
306
|
+
differently. Measured on a two-scene game built for this:
|
|
307
|
+
|
|
308
|
+
| | swap | save |
|
|
309
|
+
| --- | --- | --- |
|
|
310
|
+
| a module object in YOUR code | ✅ crosses | ❌ `captureState()` cannot see it — `restored: 0`, and the report is clean |
|
|
311
|
+
| `serialize`/`deserialize` alone | ❌ the new scene's behavior starts fresh | ✅ `restored: 1` |
|
|
312
|
+
| …plus `announce()` | — | the HUD, which otherwise reads the old value over restored state |
|
|
313
|
+
|
|
314
|
+
So: keep the truth in a module object, and give ONE behavior that exists in
|
|
315
|
+
every scene the job of writing it in and out.
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
/** Not engine state — YOUR game's, and a module is where a swap cannot reach. */
|
|
319
|
+
export const quest = { accepted: false, carrying: false, done: false };
|
|
320
|
+
|
|
321
|
+
export class Hold extends Behavior {
|
|
322
|
+
static signals = ['questChanged'];
|
|
323
|
+
override serialize() { return { ...quest }; }
|
|
324
|
+
override deserialize(d: JsonValue) { Object.assign(quest, d as object); }
|
|
325
|
+
override announce() { this.emit('questChanged', quest.stage); } // the screen
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**Give that node the same `uid` in every scene it appears in.** The save is keyed
|
|
330
|
+
by uid, so `village.scene.json` and `cellar.scene.json` sharing one uid on their
|
|
331
|
+
root is what lets a save taken in either one restore into either one. Generate it
|
|
332
|
+
once with `newUid()` and paste it into both files — this is the single case where
|
|
333
|
+
one uid legitimately appears in two scenes, because it is one thing.
|
|
334
|
+
|
|
335
|
+
`behaviorsWithoutSave()` finds the behavior you forgot: on the game above it
|
|
336
|
+
named `/Village (Hold)` first, out of 46% of the scripted nodes it lists.
|
|
337
|
+
|
|
295
338
|
## A title screen, in JSON
|
|
296
339
|
|
|
297
340
|
A `SavePoint` can ASK without loading. `probeOnReady` fires on the first frame
|
|
@@ -318,7 +361,19 @@ The label leads because that is what a menu shows; an unlabelled slot falls back
|
|
|
318
361
|
to its scene key, so the line is never blank.
|
|
319
362
|
|
|
320
363
|
**The button's press is still yours**, and rightly: `pressed → your router`. See
|
|
321
|
-
the three lines above.
|
|
364
|
+
the three lines above. `examples/lanternhold-2d` ships this exact menu — a fresh
|
|
365
|
+
install with no Continue, and a returning player whose slot line reads what the
|
|
366
|
+
game SAVED (`last saved: Village` — the SavePoint's own label, not something the
|
|
367
|
+
title screen guessed).
|
|
368
|
+
|
|
369
|
+
Two things that cost a run each to learn there:
|
|
370
|
+
|
|
371
|
+
- **A hand-written slot with `state: {}` restores nothing.** The engine says so
|
|
372
|
+
— `restored 0 of 2 saveable behaviour(s)` — but a menu built on one looks
|
|
373
|
+
perfectly green. Write the slot the way the game writes it.
|
|
374
|
+
- **Hold the engine before you route.** `goToScene` frees the tree the button
|
|
375
|
+
lives in, so `this.engine` on the next line throws. See "Game flow" in
|
|
376
|
+
`incanto-gameplay-behaviors.md`.
|
|
322
377
|
|
|
323
378
|
## Several slots
|
|
324
379
|
|
|
@@ -685,6 +685,17 @@ then land on a game that never advances: the capture comes back UNCHANGED and
|
|
|
685
685
|
looks perfectly healthy. `drove 0 frames` is refused with an error rather than
|
|
686
686
|
handed over as a picture.
|
|
687
687
|
|
|
688
|
+
**A PLAIN capture works on a hidden page.** Only a DRIVE needs the window in
|
|
689
|
+
front. `captureFrame` waits ~120 ms for the game loop and then draws a frame
|
|
690
|
+
itself, exactly because a hidden tab gets none — so the pixels are current, the
|
|
691
|
+
world is simply paused. Measured, hiding every node from the console and
|
|
692
|
+
capturing again: 2D `luminance 0.09 → 0.00 BLACK SCREEN`, 3D `0.64 → 0.96,
|
|
693
|
+
subject 44.3% → 22.0%`.
|
|
694
|
+
|
|
695
|
+
That is the whole point for an agent: **your terminal is in front and the game
|
|
696
|
+
is behind it, and `draws` still answers.** The report says the world is paused,
|
|
697
|
+
and the `draws` rung carries that as a note under a pass.
|
|
698
|
+
|
|
688
699
|
#### Look at it
|
|
689
700
|
|
|
690
701
|
```
|