incanto 0.49.0 → 0.51.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-play.mjs +42 -17
- package/dist/2d.d.ts +3 -2
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +4 -3
- package/dist/3d.js +4 -4
- package/dist/{behavior-62q0HWBO.d.ts → behavior-rZNfzVbH.d.ts} +37 -3
- package/dist/{create-game-DFBjMetZ.js → create-game-BNaOC3Px.js} +5 -5
- package/dist/{create-game-BgV6UbVA.js → create-game-CkzKZ4v5.js} +5 -5
- package/dist/debug.d.ts +1 -1
- package/dist/{duplicate-CI9WF_bg.js → duplicate-EybyYeyL.js} +1 -1
- package/dist/{environment-presets-CZOH5TY5.js → environment-presets-C08pOC6H.js} +2 -2
- package/dist/{gameplay-02Btmmjn.js → gameplay-bStgtZBV.js} +175 -6
- package/dist/gameplay.d.ts +79 -2
- package/dist/gameplay.js +2 -2
- package/dist/index.d.ts +14 -5
- package/dist/index.js +6 -6
- package/dist/{loader-DwazzlQb.js → loader-B2asghWa.js} +350 -1
- package/dist/{loader-CeyU_bm1.d.ts → loader-Dkbn56KC.d.ts} +1 -1
- package/dist/net.d.ts +1 -1
- package/dist/net.js +3 -3
- package/dist/{pathfinding-C49JSNNq.d.ts → pathfinding-Bz34pvQD.d.ts} +1 -1
- package/dist/{physics-2d-vyCBfACH.js → physics-2d-Ccq2L9R0.js} +8 -3
- package/dist/{physics-3d-DpRqw8Mz.js → physics-3d-B2c5KNYh.js} +13 -8
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-BNPZYJmd.js → register-B_BaaGx-.js} +2 -2
- package/dist/{register-uvaZj1KX.js → register-CGq-Hee8.js} +19 -265
- package/dist/{register-CB11yp21.js → register-CLVhzWcI.js} +2 -2
- package/dist/{replay-C0XJIsO7.js → replay-BgKxGcXH.js} +42 -3
- package/dist/{replay-CAphXMyM.d.ts → replay-DIP2_as4.d.ts} +6 -2
- package/dist/{src-DF4gCsqO.js → src-C3UwzYXl.js} +1 -1
- package/dist/{test-DRna_BQU.js → test-D2gRpk5V.js} +48 -21
- package/dist/test.d.ts +3 -3
- package/dist/test.js +2 -2
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-DCW4TgDt.js → agent8-DbX_msaO.js} +1 -1
- package/editor/assets/{debug-RC6qts6S.js → debug-C-kMxdl1.js} +1 -1
- package/editor/assets/{index-5dEIhvsf.js → index-CUc1U7wm.js} +91 -91
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/skills/incanto-building-2d-games.md +6 -1
- package/skills/incanto-gameplay-behaviors.md +11 -0
- package/skills/incanto-node-reference.md +13 -0
- package/skills/incanto-playtesting.md +13 -2
- package/skills/incanto-save-slots.md +134 -12
- package/templates-app/beacon-isle-3d/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-CUc1U7wm.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="./assets/GameServer-C56iOUgF.js">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -213,8 +213,13 @@ listing the valid set. With a viewport design, UI coordinates are design px.
|
|
|
213
213
|
```
|
|
214
214
|
`setScene` frees the old root, clears + redeclares the input map from the new
|
|
215
215
|
scene's `input{}`, and emits `sceneChanged` — `createGame2D`'s touch overlay
|
|
216
|
-
rebuilds itself on that signal,
|
|
216
|
+
rebuilds itself on that signal, the PHYSICS world registers the new scene's
|
|
217
|
+
bodies before its first frame, and the renderer loads the new scene's assets
|
|
217
218
|
on demand. Register any extra behaviors/node types BEFORE the `loadScene` call.
|
|
219
|
+
|
|
220
|
+
The input map is CLEARED by the swap, which matters for a headless drive: an
|
|
221
|
+
injected `setActionVector` does not carry into the next level — set it again
|
|
222
|
+
after the transition, the same way a player's held key is re-read.
|
|
218
223
|
- **Game over / restart**: swap to a fresh load of the SAME JSON —
|
|
219
224
|
`engine.setScene(loadScene(levelJson))`. `loadScene` treats the JSON as
|
|
220
225
|
read-only (everything it keeps is cloned), so reloading the same imported
|
|
@@ -356,6 +356,17 @@ whole entity toward the target. (Distance is measured from the moved parent.)
|
|
|
356
356
|
"script": { "name": "Chase", "props": { "target": "/root/Player", "speed": 120, "stopRange": 20 } } }
|
|
357
357
|
```
|
|
358
358
|
|
|
359
|
+
**A character body chases through the level, not through walls.** When the node
|
|
360
|
+
being moved is a `CharacterBody2D`/`3D`, `Chase` (and `Patrol`) move it with
|
|
361
|
+
`moveAndSlide()`, so it collides and slides like any character. A plain
|
|
362
|
+
`Node2D`/`Node3D` — the example above — has no collider and keeps the direct
|
|
363
|
+
write, which is also what you want for a ghost or a flying marker.
|
|
364
|
+
|
|
365
|
+
This is straight-line homing, not pathfinding: a body can now be STOPPED by
|
|
366
|
+
geometry it used to walk through. When enemies must find their way around a
|
|
367
|
+
level, that is what `buildTerrainNav` + `findPath` + `PathFollow` are for (see
|
|
368
|
+
`incanto-building-3d-games.md`).
|
|
369
|
+
|
|
359
370
|
## Wander
|
|
360
371
|
|
|
361
372
|
Seeded random roaming inside a circle around the spawn point — idle critters,
|
|
@@ -1422,6 +1422,19 @@ Signals: `collected(value, other)`
|
|
|
1422
1422
|
| `direction` | `null` | null |
|
|
1423
1423
|
| `gravity` | `0` | number |
|
|
1424
1424
|
|
|
1425
|
+
### `SavePoint`
|
|
1426
|
+
|
|
1427
|
+
| Prop | Default | Kind |
|
|
1428
|
+
|---|---|---|
|
|
1429
|
+
| `game` | `"game"` | string |
|
|
1430
|
+
| `slot` | `"1"` | string |
|
|
1431
|
+
| `label` | `""` | string |
|
|
1432
|
+
| `scene` | `""` | string |
|
|
1433
|
+
| `restoreOnReady` | `false` | boolean |
|
|
1434
|
+
| `probeOnReady` | `false` | boolean |
|
|
1435
|
+
|
|
1436
|
+
Signals: `saved` · `restored` · `noSave` · `hasSave`
|
|
1437
|
+
|
|
1425
1438
|
### `ScoreKeeper`
|
|
1426
1439
|
|
|
1427
1440
|
| Prop | Default | Kind |
|
|
@@ -98,12 +98,23 @@ far more useful.
|
|
|
98
98
|
## Replays
|
|
99
99
|
|
|
100
100
|
A run that did not win is written out. Because the engine is deterministic
|
|
101
|
-
(seeded RNG, injected clock), a replay reproduces that run **bit-identically
|
|
101
|
+
(seeded RNG, injected clock), a replay reproduces that run **bit-identically** —
|
|
102
|
+
feed the file straight back in, with the seed the run used:
|
|
102
103
|
|
|
103
104
|
```bash
|
|
104
|
-
bunx incanto-play src/game.scene.json --seed
|
|
105
|
+
bunx incanto-play src/game.scene.json --seed 1 --commands .incanto/playtest/fell-seed1.json
|
|
106
|
+
# {"ok":true,"cmd":"replay","frames":804,"events":963}
|
|
107
|
+
# … then the scene as it ended: /Game/Player position=[28.9, -49.46, …]
|
|
105
108
|
```
|
|
106
109
|
|
|
110
|
+
`--commands` takes either a text command script or a replay file and tells them
|
|
111
|
+
apart by content. The seed matters: the world's own randomness (spawners,
|
|
112
|
+
wandering AI) comes from it, and only the PLAYER's input is in the recording.
|
|
113
|
+
|
|
114
|
+
Then read where it ended. The player above left a walled 40×40 arena at
|
|
115
|
+
`x = 28.9` before falling — a thing you cannot learn from "fell in 1/4" and can
|
|
116
|
+
read straight off the replay.
|
|
117
|
+
|
|
107
118
|
One file per *kind* of failure — twenty identical "stuck" replays teach nothing
|
|
108
119
|
the first one does not.
|
|
109
120
|
|
|
@@ -47,6 +47,17 @@ restored.** You resume at the scene's start with stats, inventory, unlocks and
|
|
|
47
47
|
quest flags intact — a checkpoint save. If your game needs a position, save it:
|
|
48
48
|
`serialize()` returns anything.
|
|
49
49
|
|
|
50
|
+
**What the run CONSUMED is remembered.** Reloading from the file brings back
|
|
51
|
+
every gem you already picked up, which would let a collect-five-to-win run
|
|
52
|
+
resume at four with five gems on the map. So the save also carries the authored
|
|
53
|
+
uids that are no longer in the tree, under `#freed`, and the restore frees them
|
|
54
|
+
again — collectibles, opened chests, destroyed crates, a named boss. Spawned
|
|
55
|
+
clones never have uids (`duplicateNode` drops them on purpose), so the ledger is
|
|
56
|
+
exactly the authored world.
|
|
57
|
+
|
|
58
|
+
Those nodes go on the **next frame** (`queueFree`, not an immediate detach), so
|
|
59
|
+
read `report.freed` rather than counting children the instant restore returns.
|
|
60
|
+
|
|
50
61
|
## Making a behavior saveable
|
|
51
62
|
|
|
52
63
|
Two optional hooks, exactly like the other five:
|
|
@@ -82,8 +93,17 @@ lives, won/lost), `Collector` (total).
|
|
|
82
93
|
|
|
83
94
|
The uid is the join key, because it is the one identifier that survives a rename
|
|
84
95
|
or a reparent. The editor assigns one to every node it touches. A hand-written
|
|
85
|
-
scene may not have them
|
|
86
|
-
|
|
96
|
+
scene may not have them, and then the save is silently empty — so
|
|
97
|
+
**`incanto-check` warns about it**, naming each node, long before you write a
|
|
98
|
+
save:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
warn: these carry state a save keeps and have no uid to key it under, so the
|
|
102
|
+
save comes back EMPTY and the load reports no problem: Game (ScoreKeeper),
|
|
103
|
+
Game/Player (Health). Give each one a "uid" from newUid().
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
At runtime `engine.captureState()` logs the same thing per node.
|
|
87
107
|
|
|
88
108
|
Never hand-craft a uid. Use `newUid()`.
|
|
89
109
|
|
|
@@ -120,7 +140,68 @@ It never throws. A save naming a uid this build deleted reports it in
|
|
|
120
140
|
`report.missing` and restores everything else; refusing to load would mean a
|
|
121
141
|
patch that moves one node deletes everyone's progress.
|
|
122
142
|
|
|
123
|
-
##
|
|
143
|
+
## Many levels: the router is three lines, and they are yours
|
|
144
|
+
|
|
145
|
+
The engine does not route scenes — deliberately, because only your game knows
|
|
146
|
+
what a key means. What it does is record the key, so the routing is a lookup:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { createGame2D, loadScene } from 'incanto';
|
|
150
|
+
import level1 from './level1.scene.json';
|
|
151
|
+
import level2 from './level2.scene.json';
|
|
152
|
+
|
|
153
|
+
const SCENES: Record<string, unknown> = { level1, level2 }; // key → scene JSON
|
|
154
|
+
|
|
155
|
+
// New game
|
|
156
|
+
const game = await createGame2D({ canvas, scene: SCENES.level1 });
|
|
157
|
+
|
|
158
|
+
// Next level — the SavePoint in the new scene writes `level2` from here on
|
|
159
|
+
(game.scene.root.getNode('Flow').behavior as GameFlow).goToScene(SCENES.level2);
|
|
160
|
+
|
|
161
|
+
// Continue
|
|
162
|
+
const slot = new SaveSlots('chapters').read('1');
|
|
163
|
+
const scene = SCENES[slot?.scene ?? 'level1'];
|
|
164
|
+
const game = await createGame2D({ canvas, scene });
|
|
165
|
+
// then `restoreOnReady: true` on that scene's SavePoint, or call restore()
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
A `SavePoint` records `scene` as the scene's own `name` unless you set the prop,
|
|
169
|
+
so `level2.scene.json` named `level2` needs no wiring at all. Set `scene`
|
|
170
|
+
explicitly when one file is entered more than one way (`"chapter-2-rescue"`).
|
|
171
|
+
|
|
172
|
+
**The swap clears the input map** (the new scene declares its own `input{}`), and
|
|
173
|
+
physics registers the new bodies before that scene's first frame — so a
|
|
174
|
+
character walks in level two exactly as it did in level one.
|
|
175
|
+
|
|
176
|
+
## A title screen, in JSON
|
|
177
|
+
|
|
178
|
+
A `SavePoint` can ASK without loading. `probeOnReady` fires on the first frame
|
|
179
|
+
and emits `hasSave(label, playtime, scene)` or `noSave`, so the menu wires
|
|
180
|
+
itself:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{ "name": "Save", "type": "Node", "uid": "n_…",
|
|
184
|
+
"script": { "name": "SavePoint",
|
|
185
|
+
"props": { "game": "chapters", "slot": "1", "probeOnReady": true } } }
|
|
186
|
+
```
|
|
187
|
+
```json
|
|
188
|
+
{ "signal": "noSave", "from": "Save", "to": "HUD/Menu/Continue", "handler": "hide" },
|
|
189
|
+
{ "signal": "noSave", "from": "Save", "to": "HUD/Menu/SlotInfo", "handler": "hide" },
|
|
190
|
+
{ "signal": "hasSave", "from": "Save", "to": "HUD/Menu/SlotInfo", "handler": "setText" }
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
With `"format": "Continue: {}"` on that `UiText`, a fresh install shows a menu
|
|
194
|
+
with no Continue button and a save shows `Continue: Chapter 2`. Every HUD widget
|
|
195
|
+
takes `show`/`hide` from a wire (`visible` is a prop, and a connection needs a
|
|
196
|
+
method — the same wall `setText` broke through).
|
|
197
|
+
|
|
198
|
+
The label leads because that is what a menu shows; an unlabelled slot falls back
|
|
199
|
+
to its scene key, so the line is never blank.
|
|
200
|
+
|
|
201
|
+
**The button's press is still yours**, and rightly: `pressed → your router`. See
|
|
202
|
+
the three lines above.
|
|
203
|
+
|
|
204
|
+
## Several slots
|
|
124
205
|
|
|
125
206
|
```ts
|
|
126
207
|
for (const slot of slots.all()) { // newest first
|
|
@@ -130,23 +211,64 @@ slots.remove('2');
|
|
|
130
211
|
slots.clear(); // "delete all data"
|
|
131
212
|
```
|
|
132
213
|
|
|
214
|
+
One `SavePoint` per slot is the declarative version: three nodes with
|
|
215
|
+
`slot: "1" | "2" | "3"`, each probing into its own row of the menu.
|
|
216
|
+
|
|
133
217
|
## Checking your coverage
|
|
134
218
|
|
|
135
219
|
```ts
|
|
136
|
-
import { behaviorsWithoutSave } from 'incanto';
|
|
137
|
-
console.log(behaviorsWithoutSave(game.engine.scene.root));
|
|
220
|
+
import { behaviorsWithoutSave, savesWithoutUid } from 'incanto';
|
|
221
|
+
console.log(behaviorsWithoutSave(game.engine.scene.root)); // forgot serialize?
|
|
222
|
+
console.log(savesWithoutUid(game.engine.scene.root)); // forgot the uid?
|
|
138
223
|
```
|
|
139
224
|
|
|
140
|
-
|
|
141
|
-
them are wrong —
|
|
225
|
+
`behaviorsWithoutSave` names every behavior with props and no `serialize`. Not
|
|
226
|
+
all of them are wrong — one that derives everything from time has nothing to
|
|
142
227
|
save — but it is the list to read before shipping.
|
|
143
228
|
|
|
144
|
-
|
|
229
|
+
`savesWithoutUid` is the other half, and none of it is debatable: a behavior
|
|
230
|
+
that DOES serialize, on a node with no uid, is state that goes nowhere.
|
|
231
|
+
`incanto-check` catches the built-ins it can recognise from the JSON
|
|
232
|
+
(`Health`, `ScoreKeeper`, `Collector`); a scene file cannot be asked whether
|
|
233
|
+
YOUR behavior serializes, so this walks the live tree and names those too.
|
|
145
234
|
|
|
146
|
-
|
|
147
|
-
(checkpoint, level end, every 60s, on quit) and only your game knows. Wire it to
|
|
148
|
-
whatever signal marks the moment:
|
|
235
|
+
## Saving from the scene — `SavePoint`
|
|
149
236
|
|
|
237
|
+
*When* to save is a design decision (checkpoint, level end, on quit) and only
|
|
238
|
+
your game knows — so the scene still chooses, by picking which signal to wire.
|
|
239
|
+
What it does not need any more is a method of your own to wire it to:
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{ "name": "Save", "type": "Node", "uid": "n_…",
|
|
243
|
+
"script": { "name": "SavePoint",
|
|
244
|
+
"props": { "game": "vault", "slot": "1", "label": "Chapter 2" } } }
|
|
245
|
+
```
|
|
150
246
|
```json
|
|
151
|
-
{ "
|
|
247
|
+
{ "signal": "triggerEnter", "from": "Level/Exit", "to": "Save", "handler": "save" },
|
|
248
|
+
{ "signal": "collected", "from": "Gems/Gem1", "to": "Save", "handler": "save" },
|
|
249
|
+
{ "signal": "won", "from": ".", "to": "Save", "handler": "save" }
|
|
152
250
|
```
|
|
251
|
+
|
|
252
|
+
| prop | default | meaning |
|
|
253
|
+
| --- | --- | --- |
|
|
254
|
+
| `game` | `"game"` | slot namespace — keeps two games on one origin apart |
|
|
255
|
+
| `slot` | `"1"` | which slot this node reads and writes |
|
|
256
|
+
| `label` | `""` | shown in a load menu |
|
|
257
|
+
| `scene` | `""` | the key a loader routes back to (empty = this scene's `name`) |
|
|
258
|
+
| `restoreOnReady` | `false` | read the slot on the first frame — a "Continue" boot |
|
|
259
|
+
|
|
260
|
+
Methods: `save()` · `restore()` · `clear()` — and `playtime` / `slotScene()` to
|
|
261
|
+
read. Signals: `saved(slot)` · `restored(count)` · **`noSave`**, which is what
|
|
262
|
+
greys out a Continue button.
|
|
263
|
+
|
|
264
|
+
`restoreOnReady` lands on the **first frame**, not in `onReady`: `onReady` runs
|
|
265
|
+
children-first, so restoring there would hand the score keeper its state back
|
|
266
|
+
and then watch the root's own `onReady` set it to zero.
|
|
267
|
+
|
|
268
|
+
**A checkpoint wired to `collected` counts that pickup.** `Pickup` queues its
|
|
269
|
+
free before it announces, so a save taken from the handler records the world
|
|
270
|
+
without it — otherwise the gem came back on the next run with the score that
|
|
271
|
+
counted it already banked.
|
|
272
|
+
|
|
273
|
+
Still your code when the moment is not a signal — every 60 s, on `visibilitychange`,
|
|
274
|
+
on a quit button: call `engine.captureState()` and `slots.write()` yourself.
|