incanto 0.69.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/_behaviors-loader.mjs +22 -0
- package/bin/_read-json.mjs +28 -0
- package/bin/incanto-assets.mjs +5 -4
- package/bin/incanto-check.mjs +58 -31
- package/bin/incanto-editor.mjs +128 -5
- package/bin/incanto-env.mjs +3 -2
- package/bin/incanto-feel.mjs +24 -13
- package/bin/incanto-frame.mjs +8 -1
- package/bin/incanto-multiplay.mjs +11 -9
- package/bin/incanto-new.mjs +105 -10
- package/bin/incanto-play.mjs +158 -16
- package/bin/incanto-playtest.mjs +66 -23
- package/bin/incanto-skills.mjs +14 -2
- package/bin/incanto-verify.mjs +121 -102
- package/bin/incanto.mjs +3 -2
- package/dist/2d.d.ts +296 -34
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +157 -9
- package/dist/3d.js +8 -8
- package/dist/{agent8-CvsfVskX.js → agent8-Cfmd3ar_.js} +61 -8
- package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-L8yccdMP.d.ts} +2 -2
- package/dist/{behavior-rm8vVzu_.d.ts → behavior-Dcz0fr1S.d.ts} +755 -46
- package/dist/{create-game-CwxEDlCm.js → create-game-CH4kt78v.js} +143 -60
- package/dist/{create-game-CvJ1wSRP.js → create-game-LCHBLfA4.js} +98 -48
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +2 -3
- package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
- package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
- package/dist/editor.js +1088 -882
- package/dist/env.d.ts +1 -1
- package/dist/env.js +5 -3
- package/dist/{environment-presets-BO-7XoFV.js → environment-presets-99BzP_L-.js} +298 -45
- package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
- package/dist/{gameplay-D1KixWK8.js → gameplay-CNULJvwh.js} +914 -214
- package/dist/gameplay.d.ts +254 -10
- package/dist/gameplay.js +2 -2
- package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
- package/dist/index.d.ts +179 -13
- package/dist/index.js +10 -12
- package/dist/json-CfTjpvW8.js +67 -0
- package/dist/{loader-DadV2QCT.d.ts → loader-8-IHvbKD.d.ts} +2 -2
- package/dist/net.d.ts +27 -3
- package/dist/net.js +2 -2
- package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
- package/dist/{physics-2d-CF5yyjbj.js → physics-2d-FhrXUc6B.js} +136 -11
- package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CFEGxBXZ.js} +104 -23
- package/dist/{teardown-CMqFVxO9.js → picking-DVo7fI13.js} +104 -13
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/dist/{register-DbVArsns.js → register-BYQCBySi.js} +429 -28
- package/dist/{register-mmfugpaQ.js → register-DuVdzxbq.js} +594 -831
- package/dist/{replay-BEHLDcQV.js → replay-DEvp3kyV.js} +224 -14
- package/dist/{replay-BQfkX4x7.d.ts → replay-F7IZHdFR.d.ts} +20 -3
- package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
- package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
- package/dist/{loader-DZE7B07H.js → save-slots-Bvuh2p_r.js} +4619 -2318
- package/dist/{split-screen-Cg76MCV5.d.ts → split-screen-CIYf1zSf.d.ts} +36 -4
- package/dist/{split-screen-CauMwu6y.js → split-screen-eULetcg-.js} +159 -26
- package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
- package/dist/{src-CYEu-_S9.js → src-_jk0qLsS.js} +2 -17
- package/dist/test-CmN54kWv.js +4264 -0
- package/dist/test.d.ts +639 -30
- package/dist/test.js +3 -3
- package/dist/touch-BnCyPA0G.js +519 -0
- package/dist/vite.d.ts +20 -37
- package/dist/vite.js +177 -7
- package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
- package/editor/assets/agent8-yLIEYHbd.js +1 -0
- package/editor/assets/{debug-CMULinBW.js → debug-u31w_yhq.js} +2 -2
- package/editor/assets/index-DK9xMGpW.js +11046 -0
- package/editor/index.html +1 -1
- package/package.json +4 -14
- package/schemas/scene.schema.json +34 -3
- package/skills/incanto-3d-character.md +14 -1
- package/skills/incanto-3d-models.md +12 -0
- package/skills/incanto-assets.md +9 -1
- package/skills/incanto-audio.md +8 -2
- package/skills/incanto-behaviors-and-scripts.md +105 -7
- package/skills/incanto-building-2d-games.md +97 -10
- package/skills/incanto-building-3d-games.md +58 -4
- package/skills/incanto-editor.md +46 -7
- package/skills/incanto-environment.md +19 -1
- package/skills/incanto-game-feel.md +70 -0
- package/skills/incanto-gameplay-behaviors.md +172 -20
- package/skills/incanto-hud.md +142 -7
- package/skills/incanto-localization.md +13 -5
- package/skills/incanto-multiplayer.md +83 -3
- package/skills/incanto-node-reference.md +239 -59
- package/skills/incanto-performance.md +52 -0
- package/skills/incanto-physics-and-input.md +123 -24
- package/skills/incanto-playtesting.md +112 -2
- package/skills/incanto-save-slots.md +217 -7
- package/skills/incanto-scene-json-authoring.md +69 -12
- package/skills/incanto-verifying-your-game.md +207 -10
- package/skills/incanto-web-integration.md +28 -0
- package/skills/incanto-your-first-game.md +3 -1
- package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
- package/templates-app/beacon-isle-3d/package.json +2 -2
- package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
- package/templates-app/beacon-isle-3d/src/game.scene.json +143 -600
- package/templates-app/beacon-isle-3d/src/main.ts +11 -1
- package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
- package/templates-app/beacon-isle-3d/verify.ts +3 -1
- package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
- package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
- package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
- package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
- package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
- package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
- package/templates-app/molehill-2d/index.html +73 -0
- package/templates-app/molehill-2d/package.json +23 -0
- package/templates-app/molehill-2d/src/behaviors.ts +198 -0
- package/templates-app/molehill-2d/src/game.scene.json +1205 -0
- package/templates-app/molehill-2d/src/main.ts +41 -0
- package/templates-app/molehill-2d/tsconfig.json +13 -0
- package/templates-app/molehill-2d/verify.ts +247 -0
- package/templates-app/molehill-2d/vite.config.ts +12 -0
- package/templates-app/platformer-2d/package.json +2 -2
- package/templates-app/platformer-2d/src/game.scene.json +178 -986
- package/templates-app/platformer-2d/src/main.ts +11 -1
- package/templates-app/platformer-2d/tsconfig.json +1 -1
- package/templates-app/star-survivor/package.json +2 -2
- package/templates-app/star-survivor/src/game.scene.json +43 -216
- package/templates-app/star-survivor/src/main.ts +11 -1
- package/templates-app/star-survivor/tsconfig.json +1 -1
- package/templates-app/tps-3d/PROJECT/Context.md +1 -1
- package/templates-app/tps-3d/package.json +2 -2
- package/templates-app/tps-3d/src/behaviors.ts +19 -1
- package/templates-app/tps-3d/src/game.scene.json +66 -263
- package/templates-app/tps-3d/src/main.ts +13 -2
- package/templates-app/tps-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
- package/templates-app/village-quest-3d/package.json +2 -2
- package/templates-app/village-quest-3d/src/behaviors.ts +42 -4
- package/templates-app/village-quest-3d/src/grove.scene.json +68 -306
- package/templates-app/village-quest-3d/src/main.ts +11 -1
- package/templates-app/village-quest-3d/src/village.scene.json +202 -863
- package/templates-app/village-quest-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/verify.ts +14 -1
- package/dist/duplicate-Dj4L-5AA.js +0 -22
- package/dist/json-CwwhxQgb.js +0 -36
- package/dist/registry-CF70EArN.js +0 -212
- package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
- package/dist/test-D6ISmuFE.js +0 -2383
- package/dist/touch-DEAmqGdf.js +0 -225
- package/editor/assets/agent8-BDvaZMQJ.js +0 -1
- package/editor/assets/index-CZIRr5EL.js +0 -10958
|
@@ -42,10 +42,17 @@ Reloading from source sidesteps all three. The structure comes from the file
|
|
|
42
42
|
(authoritative, already validated); the save carries only what the file cannot
|
|
43
43
|
know.
|
|
44
44
|
|
|
45
|
-
**The cost, stated plainly: spawned enemies
|
|
46
|
-
restored.** You resume at the scene's start
|
|
47
|
-
quest flags intact — a checkpoint save. If
|
|
48
|
-
`serialize()` returns anything.
|
|
45
|
+
**The cost, stated plainly: spawned enemies, mid-level positions and anything
|
|
46
|
+
else that lives in the TREE are not restored.** You resume at the scene's start
|
|
47
|
+
with stats, inventory, unlocks and quest flags intact — a checkpoint save. If
|
|
48
|
+
your game needs a position, save it: `serialize()` returns anything.
|
|
49
|
+
|
|
50
|
+
"Inventory" there means a behavior's DATA — a list of item ids, a wallet, a
|
|
51
|
+
quest flag. An inventory made of WIDGETS MOVED BETWEEN SLOTS
|
|
52
|
+
(`incanto-hud.md`'s drag recipe) is structure, so it reloads exactly as the
|
|
53
|
+
file has it: measured on a bench with one item dragged across, the save
|
|
54
|
+
mentions no slot and the reload puts the item back on its shelf. That skill
|
|
55
|
+
prints the `serialize` that keeps the slot map as data.
|
|
49
56
|
|
|
50
57
|
**What the run CONSUMED is remembered.** Reloading from the file brings back
|
|
51
58
|
every gem you already picked up, which would let a collect-five-to-win run
|
|
@@ -83,11 +90,59 @@ class QuestLog extends Behavior {
|
|
|
83
90
|
`maxHealth` no — that comes back from the scene JSON, and duplicating it makes
|
|
84
91
|
old saves fight your balance patches.
|
|
85
92
|
|
|
93
|
+
**Randomness is carried for you.** `captureState()` records where
|
|
94
|
+
`engine.rng` has got to and `restoreState()` puts it back, because without that
|
|
95
|
+
a load replays the seeded sequence from the top — the first "random" event after
|
|
96
|
+
a save is the one the RUN opened with, and every loot roll, wander and spawn
|
|
97
|
+
wobble repeats the opening of the game. Nothing to do; `engine.rng.position` is
|
|
98
|
+
readable if you want it for something else.
|
|
99
|
+
|
|
86
100
|
`deserialize` is defensive on purpose: that data may come from a build of your
|
|
87
101
|
game that shipped six weeks ago. Check what you read.
|
|
88
102
|
|
|
89
|
-
Built-ins that already save: `
|
|
90
|
-
|
|
103
|
+
Built-ins that already save: `Collector` (total), `Currency` (amount),
|
|
104
|
+
`DayNight` (hour, paused), `Health` (current, dead), `SavePoint` (playtime),
|
|
105
|
+
`ScoreKeeper` (score, lives, won/lost), `WaveSpawner` (which wave).
|
|
106
|
+
|
|
107
|
+
`WaveSpawner` RESTARTS the wave you were on rather than resuming mid-spawn —
|
|
108
|
+
spawned entities are never restored, so resuming "four enemies into wave 3"
|
|
109
|
+
would resume a wave whose enemies do not exist. `waveStarted` fires again, so a
|
|
110
|
+
HUD wired to it catches up on its own.
|
|
111
|
+
|
|
112
|
+
Every one of them needs a `uid` on its node, and `incanto-check` says so if it
|
|
113
|
+
has none — that check reads the same list.
|
|
114
|
+
|
|
115
|
+
## Tell the screen: `announce()`
|
|
116
|
+
|
|
117
|
+
`deserialize` writes fields. Your HUD is wired to **signals** — that is what
|
|
118
|
+
`incanto-hud.md` teaches and the only thing the editor can wire — so a restore
|
|
119
|
+
that only writes fields leaves the screen showing a fresh start:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
HUD score="0" hp=100/100 gems="0"
|
|
123
|
+
TRUTH score=1400 hp=38 gems=7
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
with no error anywhere and a restore report of `{missing: [], restored: 3}`.
|
|
127
|
+
|
|
128
|
+
So there is a third hook, called once after the whole restore pass:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
override announce() {
|
|
132
|
+
this.emit('questChanged', this.stage); // what the screen SHOWS
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Emit only what displays.** Not `died`, not `won`, not `levelUp` — a save is
|
|
137
|
+
being read, nothing just happened, and re-firing an outcome signal on load is
|
|
138
|
+
how a Continue lands straight on the game-over screen it was loaded to escape.
|
|
139
|
+
It runs after every `deserialize` in the pass, so a handler that reads a sibling
|
|
140
|
+
sees restored values there too.
|
|
141
|
+
|
|
142
|
+
`Health`, `ScoreKeeper`, `Collector` and `Currency` implement it. `ScoreKeeper`
|
|
143
|
+
gained **`livesChanged`** for this: `lifeLost` is the EVENT (flash, sound,
|
|
144
|
+
respawn) and fires only on a real loss, while `livesChanged` is the COUNT and
|
|
145
|
+
fires on both. Wire a lives counter to `livesChanged`.
|
|
91
146
|
|
|
92
147
|
## Every node you save — and every node that VANISHES — needs a uid
|
|
93
148
|
|
|
@@ -109,6 +164,20 @@ it just wrote is already wrong. It does not guess a replacement: a uid survives
|
|
|
109
164
|
a rename and a reparent, and a saved PATH would point at whatever node moved
|
|
110
165
|
into that slot after your next edit.
|
|
111
166
|
|
|
167
|
+
**Put the uid on the node that OWNS the state, not on its art.** A collectible
|
|
168
|
+
is normally one node with the script and a plain child for the sprite:
|
|
169
|
+
|
|
170
|
+
```jsonc
|
|
171
|
+
{ "name": "Sword", "type": "Area2D", "uid": "n_sr9scvhygy01mgxr",
|
|
172
|
+
"script": { "name": "ItemPickup" },
|
|
173
|
+
"children": [ { "name": "Icon", "type": "Sprite2D" } ] } // no uid needed
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Freeing the sword takes the icon with it, so the parent's uid in `#freed`
|
|
177
|
+
records both — the audit says nothing about the child, and adding a uid to it
|
|
178
|
+
changes nothing. What the audit still catches is the child that goes while its
|
|
179
|
+
parent LIVES: that one really does come back.
|
|
180
|
+
|
|
112
181
|
|
|
113
182
|
|
|
114
183
|
The uid is the join key, because it is the one identifier that survives a rename
|
|
@@ -156,6 +225,35 @@ if (slot) {
|
|
|
156
225
|
`onReady` is where a behavior sets its starting values, so restoring first would
|
|
157
226
|
be overwritten.
|
|
158
227
|
|
|
228
|
+
**Not from `onReady` itself, either**, which is the tempting place to put
|
|
229
|
+
"continue my run" and the one that cannot work: the ready pass runs while the
|
|
230
|
+
scene is still loading, and `engine.scene` — which both halves of this API read
|
|
231
|
+
— is not assigned until `setScene`. Both say so now rather than answering
|
|
232
|
+
quietly:
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
[incanto] restoreState() ran before the engine had a scene, and restored NOTHING.
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Before that it reported every uid in the save as `missing`, which is the
|
|
239
|
+
signature of a save format change, so the hunt started in the wrong place
|
|
240
|
+
entirely.
|
|
241
|
+
|
|
242
|
+
**To keep the continue INSIDE the game** (where a harness can reach it —
|
|
243
|
+
`runScript` boots the scene, never your `main.ts`), do it on the first frame:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
class Game extends Behavior {
|
|
247
|
+
private continued = false;
|
|
248
|
+
override update(): void {
|
|
249
|
+
if (this.continued) return;
|
|
250
|
+
this.continued = true;
|
|
251
|
+
const slot = this.slots.read('1');
|
|
252
|
+
if (slot) this.engine.restoreState(slot.state);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
159
257
|
It never throws. A save naming a uid this build deleted reports it in
|
|
160
258
|
`report.missing` and restores everything else; refusing to load would mean a
|
|
161
259
|
patch that moves one node deletes everyone's progress.
|
|
@@ -194,6 +292,49 @@ explicitly when one file is entered more than one way (`"chapter-2-rescue"`).
|
|
|
194
292
|
physics registers the new bodies before that scene's first frame — so a
|
|
195
293
|
character walks in level two exactly as it did in level one.
|
|
196
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
|
+
|
|
197
338
|
## A title screen, in JSON
|
|
198
339
|
|
|
199
340
|
A `SavePoint` can ASK without loading. `probeOnReady` fires on the first frame
|
|
@@ -220,7 +361,19 @@ The label leads because that is what a menu shows; an unlabelled slot falls back
|
|
|
220
361
|
to its scene key, so the line is never blank.
|
|
221
362
|
|
|
222
363
|
**The button's press is still yours**, and rightly: `pressed → your router`. See
|
|
223
|
-
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`.
|
|
224
377
|
|
|
225
378
|
## Several slots
|
|
226
379
|
|
|
@@ -235,6 +388,63 @@ slots.clear(); // "delete all data"
|
|
|
235
388
|
One `SavePoint` per slot is the declarative version: three nodes with
|
|
236
389
|
`slot: "1" | "2" | "3"`, each probing into its own row of the menu.
|
|
237
390
|
|
|
391
|
+
### Restore into a scene that has been PLAYED
|
|
392
|
+
|
|
393
|
+
A save is restored INTO a freshly loaded scene. The structure comes from the
|
|
394
|
+
file; `#freed` then takes away the nodes that run had consumed — and nothing
|
|
395
|
+
puts any back. So a pause-menu Load, a slot menu, or death wired straight to
|
|
396
|
+
`SavePoint.restore()` leaves THIS run's collectibles deleted:
|
|
397
|
+
|
|
398
|
+
```
|
|
399
|
+
CONTROL restart the scene from source, then restore → gems [Gem2,Gem3], won
|
|
400
|
+
death wired straight to restore (no reload) → gems [Gem3], lost
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
…and the next autosave writes that hybrid back to the slot, so the unwinnable
|
|
404
|
+
run survives a page reload. Reload first — `GameFlow.restart` with
|
|
405
|
+
`restoreOnReady`, or `restartScene(engine)` and then restore. After a restart
|
|
406
|
+
the SavePoint node is a NEW one, so a held behavior reference is stale.
|
|
407
|
+
|
|
408
|
+
`report.stale` names the authored nodes this tree consumed that the save does
|
|
409
|
+
not account for, and the engine warns when it is non-empty. It is a warning
|
|
410
|
+
rather than an error because a node that freed ITSELF on a timer lands in the
|
|
411
|
+
same set, and only your game knows which of its nodes are transient.
|
|
412
|
+
|
|
413
|
+
### A save hook that throws
|
|
414
|
+
|
|
415
|
+
`serialize()`, `deserialize()` and `announce()` are your code, and your code
|
|
416
|
+
throws. All three are caught now, and each says something different:
|
|
417
|
+
|
|
418
|
+
- a **`serialize()`** that throws leaves a HOLE in the save — that behavior's
|
|
419
|
+
whole run is missing — so `SavePoint.save()` **refuses to write** and emits
|
|
420
|
+
`saveFailed` instead of `saved`. Overwriting the previous slot with a holed
|
|
421
|
+
save destroys the progress the player actually had. `engine.lastCaptureFailures`
|
|
422
|
+
names them if you call `captureState()` yourself.
|
|
423
|
+
- a **`deserialize()`** that throws is in `report.skipped` as before, and now
|
|
424
|
+
also in **`report.refused`** with the reason — a save this build cannot read
|
|
425
|
+
and a node with no `deserialize` at all are different problems.
|
|
426
|
+
- an **`announce()`** that throws means the state IS restored and the SCREEN was
|
|
427
|
+
not told: **`report.unannounced`**. It reported `restored: 2, expected: 2`
|
|
428
|
+
over a HUD showing zeros, and it does not take an override to reach — a
|
|
429
|
+
`getNode('HUD/ScoreLabel')` on a renamed node lands there.
|
|
430
|
+
|
|
431
|
+
### A save that cannot be read
|
|
432
|
+
|
|
433
|
+
A truncated write (a power cut, a tab closed mid-save) or a slot from a build
|
|
434
|
+
that predates this one is not offered by `all()` — but it is not *gone* either,
|
|
435
|
+
and a load menu that quietly shows one fewer row than the player remembers is
|
|
436
|
+
the worst thing a save system can do:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
for (const { id, why } of slots.problems()) {
|
|
440
|
+
render(`slot ${id}: ${why === 'corrupt' ? 'damaged' : 'from an older version'}`);
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
The slot INDEX is rebuilt from storage when it cannot be read, so one corrupt
|
|
445
|
+
byte in a derived list no longer hides every save on the machine — nor lets the
|
|
446
|
+
next `write()` orphan them. It says so on the console when it does.
|
|
447
|
+
|
|
238
448
|
## Checking your coverage
|
|
239
449
|
|
|
240
450
|
```ts
|
|
@@ -33,14 +33,22 @@ wiring, and composition are all in the file. TS is for *behavior only* (attached
|
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
- `format` MUST be `1`. `type` MUST be `"scene"`. `name` non-empty.
|
|
36
|
-
- `dimension` is optional: `"2d"` or `"3d"` only.
|
|
36
|
+
- `dimension` is optional: `"2d"` or `"3d"` only. Left out, it is INFERRED from
|
|
37
|
+
the node types in the tree (anything ending `3D` makes the scene 3D) — write
|
|
38
|
+
it anyway, because it is what the environment validator, the audit's
|
|
39
|
+
no-camera/no-light warnings and `physics: "auto"` all read.
|
|
37
40
|
- `fragment` (optional, default `false`) says this file is a PIECE of another
|
|
38
41
|
scene — a spawner's prefab, a sub-scene something instances — and is never
|
|
39
42
|
rendered on its own. It changes nothing at runtime; it tells the checker to
|
|
40
|
-
stop asking a prefab where its camera and its sun are
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
stop asking a prefab where its camera and its sun are, and to stop answering
|
|
44
|
+
the two questions only the HOST can answer: a node path that leaves the file
|
|
45
|
+
(`/root/Bolts` is the fragment's own root only while the fragment is alone)
|
|
46
|
+
and a `DamageOnContact.targetGroup` whose victims live in the host scene.
|
|
47
|
+
Those two are still NAMED, with a reworded verdict — a typo is most likely in
|
|
48
|
+
exactly this file, written away from the scene that gives it meaning. Set it
|
|
49
|
+
on any scene you load from TypeScript and hand to a spawner. (A scene another
|
|
50
|
+
scene embeds with `instance` needs no key — `incanto-check` walks the project
|
|
51
|
+
and can see that for itself.)
|
|
44
52
|
- `viewport` (optional) makes scene JSON own responsive layout: author the world
|
|
45
53
|
in fixed `design` pixels (`[width, height]`, positive numbers) and the renderer
|
|
46
54
|
maps them onto any canvas size. `fit`: `"expand"` (design rect always fully
|
|
@@ -96,12 +104,24 @@ Rules the engine enforces with **hard errors** (it never warns silently):
|
|
|
96
104
|
3. Prop keys and value kinds are validated against the type's schema:
|
|
97
105
|
unknown key → `UNKNOWN_PROP` (message lists valid keys); wrong JSON kind
|
|
98
106
|
(e.g. string where the default is boolean) → `PROP_TYPE_MISMATCH`.
|
|
107
|
+
A few props accept TWO kinds — `Flowers3D.density` takes a preset name or
|
|
108
|
+
plants per m², `Water3D.underwater` takes `true`/`false` or a settings
|
|
109
|
+
object — and `incanto-node-reference.md` names both in its Kind column
|
|
110
|
+
(`one of: lush sparse none, or number`). The error names them too, so a
|
|
111
|
+
value the loader refuses tells you every shape it would have taken.
|
|
99
112
|
4. Sibling names must be unique. Colliding names are auto-renamed by incrementing a
|
|
100
113
|
trailing number (`Enemy` → `Enemy2` → `Enemy3`) — **write unique names yourself** so your
|
|
101
114
|
connection paths stay valid (connections resolve after renaming).
|
|
102
115
|
5. Node names must not contain `/` or `%` and must be non-empty.
|
|
103
116
|
6. `groups` are string tags for queries (`getNodesInGroup`, connection filters).
|
|
104
117
|
`tags` is free-form JSON for game-logic identity (`{"kind": "ITEM", "value": 10}`).
|
|
118
|
+
**`groups` is a LIST and `tags` is an OBJECT**, and the loader now says so:
|
|
119
|
+
`"groups": "player"` used to load clean and spread the string one CHARACTER
|
|
120
|
+
at a time (`["p","l","a","y","e","r"]`), so `player` was in no group and
|
|
121
|
+
every query and `filter: {group: "player"}` silently matched nothing. Every
|
|
122
|
+
node key is checked for its KIND now — `props`/`overrides` an object,
|
|
123
|
+
`children` a list, `uid` a non-empty string from `newUid()` — as are the
|
|
124
|
+
header's `constants`, `multiplayer` (objects) and `connections` (a list).
|
|
105
125
|
7. `script` resolves to a registered Behavior at load (`incanto-behaviors-and-scripts.md`);
|
|
106
126
|
`network` drives multiplayer replication (`incanto-multiplayer.md`). Both round-trip
|
|
107
127
|
losslessly through export.
|
|
@@ -321,10 +341,21 @@ that do exist at the failing spot).
|
|
|
321
341
|
never recovers.
|
|
322
342
|
```
|
|
323
343
|
|
|
324
|
-
`died → ScoreKeeper.addScore` is the natural way to score a kill and it
|
|
325
|
-
score to `NaN` on the first one, with the win condition then
|
|
326
|
-
|
|
327
|
-
|
|
344
|
+
`died → ScoreKeeper.addScore` is the natural way to score a kill and it used to
|
|
345
|
+
set the score to `NaN` on the first one, with the win condition then
|
|
346
|
+
permanently out of reach. The numeric handlers REFUSE a non-number now, so the
|
|
347
|
+
same wire fails loudly instead — and so does the one the arity check cannot
|
|
348
|
+
see: `clicked → addScore` hands the NODE, which is one argument into one
|
|
349
|
+
parameter, and `score + node` is a STRING that grows forever. A handler that DEFAULTS what it is not given is correct and stays silent:
|
|
350
|
+
`won → GameFlow.win` works, because `win(text = 'YOU WIN')` needs nothing.
|
|
351
|
+
|
|
352
|
+
Writing your OWN handler, the same rule applies to you: an optional parameter
|
|
353
|
+
is a **default**, not a `?`. `hurt(amount: number, from?: Node)` counts as
|
|
354
|
+
needing 2, because `?` is a TypeScript annotation that does not survive to
|
|
355
|
+
runtime — write `from: Node | undefined = undefined` and the wire is judged on
|
|
356
|
+
what it actually needs. Every engine method a wire can name has been corrected
|
|
357
|
+
to this; before that, four of five documented HUD wires were reported as
|
|
358
|
+
broken while working perfectly. On **both**, it is
|
|
328
359
|
`AMBIGUOUS_HANDLER`: the node's method wins, so the script's would never run
|
|
329
360
|
and nothing would say so. A core node answers to 62 public methods before any
|
|
330
361
|
adapter adds more (`stop` `play` `show` `clear` `say` `start` `free` …), which
|
|
@@ -333,7 +364,11 @@ that do exist at the failing spot).
|
|
|
333
364
|
- Unresolvable `from`/`to` → `DANGLING_CONNECTION` at load. Renaming a node breaks its
|
|
334
365
|
connections **loudly** — update paths in the same edit.
|
|
335
366
|
- `filter` gates firing on the first emitted argument: it must be a node in `filter.group`
|
|
336
|
-
and/or match every `filter.tag` entry.
|
|
367
|
+
and/or match every `filter.tag` entry. **A filter's keys are `group` and `tag`, and a
|
|
368
|
+
connection's are `[signal, from, to, handler, once, filter]`** — anything else is a load
|
|
369
|
+
error, because a filter the matcher cannot read (`{"gruop": "player"}`, or the bare
|
|
370
|
+
string `"player"`) passes EVERYTHING, which is the opposite of what a filter is for, and
|
|
371
|
+
`"once": "no"` is truthy so it fires exactly once. With `once: true`, the connection is consumed only
|
|
337
372
|
when the filter matches.
|
|
338
373
|
|
|
339
374
|
## Sub-scene composition
|
|
@@ -345,7 +380,20 @@ that do exist at the failing spot).
|
|
|
345
380
|
- `instance` embeds another scene's tree; `overrides` deep-merge onto the sub-scene root's
|
|
346
381
|
props. At the instancing site, `groups` and `children` COMPOSE (union/append), while
|
|
347
382
|
`tags`/`script`/`network` REPLACE the sub-scene root's values when declared (omitted = kept).
|
|
348
|
-
- The sub-scene's own `connections` are wired inside its subtree automatically
|
|
383
|
+
- The sub-scene's own `connections` are wired inside its subtree automatically,
|
|
384
|
+
and so are its `assets`, `input`, `strings`, `constants` and `orderGroups` —
|
|
385
|
+
**a prefab brings everything it declares**. (Its `orderGroups` used to come
|
|
386
|
+
through as NAMES only, so `"orderGroup": "loot"` validated and the band's
|
|
387
|
+
number was dropped: the sprite sorted in the default band and the scene loaded
|
|
388
|
+
clean.) A `coin.scene.json` with its own
|
|
389
|
+
spritesheet, or a `player.scene.json` with its own `move`/`jump` bindings, is
|
|
390
|
+
a complete, reusable thing.
|
|
391
|
+
- The HOST wins any asset key, action name or string it declares itself, so a
|
|
392
|
+
level can re-point a prefab's art or rebind its controls without editing the
|
|
393
|
+
prefab. Two INSTANCED scenes declaring one asset key with different urls — or
|
|
394
|
+
one action name with different keys — is a hard error naming both:
|
|
395
|
+
first-writer-wins would hand the second prefab the first one's art in silence.
|
|
396
|
+
The same key with the same value is just two prefabs agreeing.
|
|
349
397
|
- There is **no** scene inheritance — composition only.
|
|
350
398
|
- Current limitation: exporting expands instances into full trees (the `instance` reference is
|
|
351
399
|
not preserved on export yet).
|
|
@@ -373,6 +421,12 @@ that do exist at the failing spot).
|
|
|
373
421
|
| `DUPLICATE_BEHAVIOR` | two classes registered under one behavior name | rename one, or `{ replace: true }` for hot reload |
|
|
374
422
|
| `TREE_VIOLATION` | invalid name, re-parenting without detach, cycles, double root | follow the rule in the message |
|
|
375
423
|
|
|
424
|
+
Every one of these lists what WOULD have worked, and names the nearest
|
|
425
|
+
candidate when there is one — `Unknown node type 'Label2D'. Did you mean
|
|
426
|
+
"Label"?`, `Unknown prop 'size' on 'Label'. Did you mean "fontSize"?`. A name
|
|
427
|
+
that resembles nothing gets the list alone, because a suggestion that is not the
|
|
428
|
+
answer is worse than none.
|
|
429
|
+
|
|
376
430
|
Scene-load and registry errors carry structured `details` (`path`, `uid`, `nodeType`,
|
|
377
431
|
`prop`, `signal`, `validOptions`) mirroring the prose — prefer those over regexing the
|
|
378
432
|
message (a few runtime errors still carry prose only). Scene-load
|
|
@@ -384,7 +438,10 @@ errors append the offending node's path: `… (at '/Level/Enemies/Slime3')`.
|
|
|
384
438
|
import { loadScene, registerCoreNodes, duplicateNode } from 'incanto';
|
|
385
439
|
|
|
386
440
|
registerCoreNodes(); // explicit — never an import side effect
|
|
387
|
-
|
|
441
|
+
// `{ engine }` on a manual boot: `onReady` runs during the LOAD, so a behaviour
|
|
442
|
+
// that reads `this.engine` or `this.rng` there throws without it. `createGame2D`
|
|
443
|
+
// /`3D` and `runScript` pass it for you.
|
|
444
|
+
const scene = loadScene(json, { engine, resolveScene }); // throws IncantoError on any problem
|
|
388
445
|
scene.root.getNode('Player').emit('hit', 10);
|
|
389
446
|
const copy = duplicateNode(scene.root.getNode('Coin'));
|
|
390
447
|
const exported = scene.toJSON(); // lossless (delta-only props)
|