incanto 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/incanto-check.mjs +6 -4
- package/dist/2d.d.ts +91 -3
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +43 -4
- package/dist/3d.js +4 -4
- package/dist/{audio-player-C3bH_eZ5.d.ts → audio-player-Dyjg5k92.d.ts} +1 -1
- package/dist/audit-C6rMyict.js +58 -0
- package/dist/{behavior-q3cejJgs.d.ts → behavior-Bod1AyJS.d.ts} +31 -1
- package/dist/{create-game-rFulaS3s.js → create-game-Bydey3CZ.js} +4 -5
- package/dist/{create-game-DuDkqnjl.js → create-game-C7Zsqbko.js} +4 -5
- package/dist/debug.d.ts +1 -1
- package/dist/{errors-BMFaY68Q.d.ts → errors-1dXlIwoR.d.ts} +7 -1
- package/dist/{gameplay-CDFgSG6z.js → gameplay-DPMgZk9W.js} +108 -1
- package/dist/gameplay.d.ts +44 -3
- package/dist/gameplay.js +2 -2
- package/dist/index.d.ts +151 -9
- package/dist/index.js +189 -4
- package/dist/{loader-DnLMMRy8.d.ts → loader-BcUDfNHn.d.ts} +1 -1
- package/dist/net.d.ts +46 -2
- package/dist/net.js +54 -2
- package/dist/particle-sim-CyUU7HVU.js +281 -0
- package/dist/{physics-2d-CZM5Y90X.js → physics-2d-DweTURFt.js} +45 -4
- package/dist/{physics-3d-DT5e8izo.js → physics-3d-Clv1uFzk.js} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-Cl3pAxX-.js → register-BD6zgrv_.js} +254 -4
- package/dist/{register-C35HDZpm.js → register-Cwc-yCYx.js} +1 -1
- package/dist/{particle-sim-m3CdTGSl.js → register-DJZXxwAn.js} +1178 -278
- package/dist/{register-CpXcMiEk.js → register-iUPtts7T.js} +141 -4
- package/dist/test.d.ts +4 -10
- package/dist/test.js +11 -16
- package/editor/assets/{agent8-N4bwVH7C.js → agent8-B0ZGx-N1.js} +1 -1
- package/editor/assets/index-BLzp8moy.js +7417 -0
- package/editor/assets/{index-DF3tMeKJ.css → index-D8QvwvOm.css} +1 -1
- package/editor/index.html +2 -2
- package/package.json +1 -1
- package/schemas/scene.schema.json +427 -0
- package/skills/incanto-3d-character.md +14 -0
- package/skills/incanto-audio.md +23 -0
- package/skills/incanto-building-2d-games.md +31 -0
- package/skills/incanto-building-3d-games.md +21 -0
- package/skills/incanto-editor.md +13 -0
- package/skills/incanto-gameplay-behaviors.md +28 -0
- package/skills/incanto-hud.md +24 -0
- package/skills/incanto-multiplayer.md +27 -0
- package/skills/incanto-node-reference.md +73 -0
- package/skills/incanto-physics-and-input.md +9 -0
- package/skills/incanto-verifying-your-game.md +31 -0
- package/dist/register-Dl_ixIJe.js +0 -834
- package/editor/assets/index-CXFNanuR.js +0 -7417
|
@@ -61,6 +61,37 @@ A solid-colored rectangle — no texture, no asset file. Props: `size [100,100]`
|
|
|
61
61
|
`color '#ffffff'`, `opacity 1`, `anchor [0.5,0.5]`. The prototyping workhorse
|
|
62
62
|
(paddles, walls, platforms, flashes); swap in a `Sprite2D` when art arrives.
|
|
63
63
|
|
|
64
|
+
### `TileMap2D` (a whole tile level in ONE node)
|
|
65
|
+
Grid render in a single draw call **plus** greedy-merged static colliders —
|
|
66
|
+
stop hand-placing dozens of ColorRect walls:
|
|
67
|
+
|
|
68
|
+
```jsonc
|
|
69
|
+
{ "name": "Level", "type": "TileMap2D", "props": {
|
|
70
|
+
"texture": "$tiles", // atlas: tileSize×tileSize squares, L→R, T→B
|
|
71
|
+
"tileSize": 32,
|
|
72
|
+
"cells": [
|
|
73
|
+
"..................",
|
|
74
|
+
"......GGG.........",
|
|
75
|
+
"000000000000000000"
|
|
76
|
+
],
|
|
77
|
+
"legend": { "G": 12 }, // chars → atlas tile index (digits 0-9 are free)
|
|
78
|
+
"solid": [0, 12] // these tile indices become static colliders
|
|
79
|
+
} }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- `.` / space = empty; digits `0-9` = atlas tile index directly; any OTHER char
|
|
83
|
+
must be in `legend` — unknown chars **hard-fail at load** (fix the map, not
|
|
84
|
+
the runtime).
|
|
85
|
+
- `solid` tiles are merged into a handful of `StaticBody2D` rect colliders
|
|
86
|
+
(greedy rectangles), so a 100×50 level costs a few bodies, not thousands.
|
|
87
|
+
- Cell (0,0) hangs its TOP-LEFT on the node's origin; position the node to
|
|
88
|
+
place the level. `columns: 0` (default) derives atlas columns from the
|
|
89
|
+
texture width.
|
|
90
|
+
- No texture yet? Colliders still work — pair with ColorRect2D placeholders or
|
|
91
|
+
just leave it invisible while you block out the level.
|
|
92
|
+
- Change the map at runtime by REPLACING `cells` (mutations are not watched):
|
|
93
|
+
`map.cells = [...rows]` — geometry and colliders rebuild next frame.
|
|
94
|
+
|
|
64
95
|
### `Sprite2D`
|
|
65
96
|
| Prop | Default | Notes |
|
|
66
97
|
|---|---|---|
|
|
@@ -358,6 +358,27 @@ paletteColors:[…saturated…], shimmer:8`), a lingering ember glitter
|
|
|
358
358
|
spreadDeg:180`) with `renderOrder:100`, each parented to a stable
|
|
359
359
|
node (NOT the collectible's container), self-removing on `finished`.
|
|
360
360
|
|
|
361
|
+
## Trails (`Trail3D` — motion ribbons)
|
|
362
|
+
|
|
363
|
+
A fading world-space ribbon behind whatever it's parented to — wingtip trails,
|
|
364
|
+
sword arcs, tyre streaks, dash effects. Cheaper and crisper than particles for
|
|
365
|
+
"where did it move" lines:
|
|
366
|
+
|
|
367
|
+
```jsonc
|
|
368
|
+
{ "name": "WingTrail", "type": "Trail3D",
|
|
369
|
+
"props": { "width": 0.4, "color": "#aaddff", "seconds": 0.8, "additive": true } }
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Parent it to the MOVING node (it records that node's world position). The strip
|
|
373
|
+
faces the camera and tapers + fades toward the tail over `seconds`.
|
|
374
|
+
|
|
375
|
+
- `width` (m, default 0.3), `color`, `opacity` (0.8), `seconds` (0.6 lifetime).
|
|
376
|
+
- `additive: true` for glowing energy trails (dark backdrops); default normal
|
|
377
|
+
blending reads better in daylight.
|
|
378
|
+
- `emitting: false` stops laying NEW ribbon while the old tail fades out — flip
|
|
379
|
+
it from script for dash/boost effects (`trail.emitting = boosting`).
|
|
380
|
+
- `minDistance` (0.05 m) filters jitter when the parent is near-stationary.
|
|
381
|
+
|
|
361
382
|
## Terrain
|
|
362
383
|
|
|
363
384
|
`Terrain3D` is procedural heightfield terrain with biome texture splatting —
|
package/skills/incanto-editor.md
CHANGED
|
@@ -110,6 +110,19 @@ Save activates only when the working scene differs from the original
|
|
|
110
110
|
(`Ctrl/Cmd+S`); undo is `Ctrl/Cmd+Z`. Saving writes pretty-printed JSON to
|
|
111
111
|
`--output` (default: the input file).
|
|
112
112
|
|
|
113
|
+
## Painting TileMap2D levels
|
|
114
|
+
|
|
115
|
+
Select a `TileMap2D` node and the inspector grows a **🖌 paint tiles** tool:
|
|
116
|
+
toggle it on and left-clicks (and drags) on the canvas paint cells with the
|
|
117
|
+
active brush. The palette chips offer every char already used in `cells`, the
|
|
118
|
+
`legend` chars, and an eraser (`␡` = `.`); the small text box takes any other
|
|
119
|
+
single char (digits map straight to atlas tile indices, other chars need a
|
|
120
|
+
`legend` entry). The grid grows right/down automatically when you paint past
|
|
121
|
+
the edge — to grow left/up, move the node instead (cell (0,0) hangs on the
|
|
122
|
+
node origin). Merged solid colliders re-derive live as you paint. Each painted
|
|
123
|
+
cell is one undo step; toggle paint off (or select another node) to get normal
|
|
124
|
+
click-select back.
|
|
125
|
+
|
|
113
126
|
## Generate environments
|
|
114
127
|
|
|
115
128
|
The **✦ button** beside the add-node controls opens the Generate dialog — the
|
|
@@ -745,6 +745,10 @@ Terminal states ignore further transitions; the restart action (or
|
|
|
745
745
|
physics, input. Listen to the `flowChanged(state)` signal for custom UI.
|
|
746
746
|
`restartScene(engine)` is exported standalone.
|
|
747
747
|
|
|
748
|
+
Multi-scene games: `flow.goToScene(nextSceneJson, { fadeSeconds: 0.4 })`
|
|
749
|
+
fades to black, swaps, fades back (headless = instant). Title → level →
|
|
750
|
+
next level is three JSON files and this one call.
|
|
751
|
+
|
|
748
752
|
## Boot loading overlay
|
|
749
753
|
|
|
750
754
|
```ts
|
|
@@ -765,3 +769,27 @@ save.set('highScore', Math.max(score, save.get('highScore', 0)));
|
|
|
765
769
|
|
|
766
770
|
JSON round-tripped; headless/private-mode falls back to in-memory (never
|
|
767
771
|
throws). `clear()` wipes only your namespace.
|
|
772
|
+
|
|
773
|
+
## Pathfinding (grid A* + PathFollow)
|
|
774
|
+
|
|
775
|
+
```ts
|
|
776
|
+
import { findPath, gridFromRows } from 'incanto';
|
|
777
|
+
|
|
778
|
+
const grid = gridFromRows([ // or { width, height, solid(x,y) } over your map data
|
|
779
|
+
'..........',
|
|
780
|
+
'..######..',
|
|
781
|
+
'..........',
|
|
782
|
+
]);
|
|
783
|
+
const cells = findPath(grid, [0, 1], [9, 1]); // null = unreachable
|
|
784
|
+
(npc.behavior as PathFollow).setPath(
|
|
785
|
+
(cells ?? []).map(([cx, cy]) => [cx * TILE, cy * TILE]),
|
|
786
|
+
);
|
|
787
|
+
npc.on('arrived', () => attack());
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
`findPath` is pure and dimension-free (cells in, cells out; diagonals on by
|
|
791
|
+
default, corner-cutting forbidden; `{ diagonal: false }` for 4-way).
|
|
792
|
+
`PathFollow` (registered behavior) walks waypoints at constant `speed`
|
|
793
|
+
(units/sec — px in 2D, meters in 3D), emits `waypointReached(i)` /
|
|
794
|
+
`arrived`, and `loop: true` patrols. Corners never eat frame budget — the
|
|
795
|
+
speed is exact across bends.
|
package/skills/incanto-hud.md
CHANGED
|
@@ -56,3 +56,27 @@ banner.show('YOU DIED', { color: '#ef4444', seconds: 0 }); // sticky until next
|
|
|
56
56
|
|
|
57
57
|
Pair with the `Health` / `ScoreKeeper` gameplay behaviors: listen to their
|
|
58
58
|
signals and write the widget props — that is the whole HUD wiring.
|
|
59
|
+
|
|
60
|
+
## Interactive widgets: UiButton & UiDialogue
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{ "name": "Talk", "type": "UiDialogue" },
|
|
64
|
+
{ "name": "Start", "type": "UiButton",
|
|
65
|
+
"props": { "anchor": "center", "text": "START" } }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const talk = this.node.getNode('%Talk') as UiDialogue;
|
|
70
|
+
talk.say('Elder', 'Welcome to Lumina Village...');
|
|
71
|
+
talk.say('Elder', 'Will you help us?', ['Yes', 'No']);
|
|
72
|
+
talk.on('choiceMade', (i) => { if (i === 0) startQuest(); });
|
|
73
|
+
talk.on('dialogueFinished', () => player.frozen = false);
|
|
74
|
+
// while talk.active, skip player input — the box eats clicks to advance
|
|
75
|
+
|
|
76
|
+
(this.node.getNode('%Start') as UiButton).on('pressed', () => flow.restart());
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Typewriter reveal at `charsPerSecond` (0 = instant); clicking the box
|
|
80
|
+
reveals the line then advances; choice lines render buttons and wait for
|
|
81
|
+
`choose(i)`. Buttons opt into pointer events — the rest of the HUD stays
|
|
82
|
+
click-through.
|
|
@@ -140,6 +140,33 @@ const manager = await NetworkManager.create(engine, { transport: local.createCli
|
|
|
140
140
|
engine.updated.connect((dt) => void local.tick(Math.min(dt, 0.05) * 1000));
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
### Split-screen preview in one call (`createSplitScreen`)
|
|
144
|
+
|
|
145
|
+
Testing multiplayer alone means playing BOTH sides. `createSplitScreen` wires
|
|
146
|
+
the whole N-panel harness — one LocalGameServer, one engine + NetworkManager
|
|
147
|
+
per player — so you only supply the per-panel renderer/input:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { createSplitScreen, registerNodesNet } from 'incanto/net';
|
|
151
|
+
|
|
152
|
+
const canvases = [document.getElementById('p1'), document.getElementById('p2')];
|
|
153
|
+
const { players, server, dispose } = await createSplitScreen({
|
|
154
|
+
scene: gameJson, // shared scene (each panel gets its own copy)
|
|
155
|
+
server: Server, // your v2 Server class (optional)
|
|
156
|
+
scenes: { 'remote-player': remoteJson }, // NetworkSpawner scenes, every panel
|
|
157
|
+
accounts: ['p1', 'p2'], // one panel per account (default)
|
|
158
|
+
setup: ({ engine }, i) => { // finish wiring each panel
|
|
159
|
+
new Renderer2D({ canvas: canvases[i], engine });
|
|
160
|
+
engine.input.attachKeyboard(i === 0 ? window : canvases[i]); // split inputs!
|
|
161
|
+
engine.start();
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The first panel's clock pumps `server.tick` (so `$roomTick` runs) — don't add
|
|
167
|
+
your own. `dispose()` tears every panel down. Going live is unchanged: ONE
|
|
168
|
+
client per browser with `createAgent8Server()` as the transport.
|
|
169
|
+
|
|
143
170
|
It runs the SAME class body the cloud runs: the v2 globals (`$sender`/`$global`/
|
|
144
171
|
`$room`/`$lock`) are injected per call, a FRESH `Server` instance runs per request
|
|
145
172
|
(so `this.*` never persists), and calls are serialized (no global leaks across
|
|
@@ -241,6 +241,7 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
|
|
|
241
241
|
| `sprintAction` | `"sprint"` | string |
|
|
242
242
|
| `skinPath` | `"../Skin"` | string |
|
|
243
243
|
| `skinYawOffset` | `0` | number |
|
|
244
|
+
| `animations` | `{}` | object |
|
|
244
245
|
|
|
245
246
|
Signals: `movementStateChanged(state)`
|
|
246
247
|
|
|
@@ -711,6 +712,25 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
|
|
|
711
712
|
| `textureBase` | `"https://agent8-games.verse8.io/assets/3D/default/textures/terrain"` | string |
|
|
712
713
|
| `basins` | `[]` | array |
|
|
713
714
|
|
|
715
|
+
## `TileMap2D` — `incanto/2d`
|
|
716
|
+
|
|
717
|
+
| Prop | Default | Kind |
|
|
718
|
+
|---|---|---|
|
|
719
|
+
| `position` | `[0,0]` | array |
|
|
720
|
+
| `rotation` | `0` | number |
|
|
721
|
+
| `static` | `false` | boolean |
|
|
722
|
+
| `scale` | `[1,1]` | array |
|
|
723
|
+
| `renderOrder` | `0` | number |
|
|
724
|
+
| `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
|
|
725
|
+
| `visible` | `true` | boolean |
|
|
726
|
+
| `texture` | `""` | string |
|
|
727
|
+
| `tileSize` | `32` | number |
|
|
728
|
+
| `columns` | `0` | number |
|
|
729
|
+
| `cells` | `[]` | array |
|
|
730
|
+
| `legend` | `{}` | object |
|
|
731
|
+
| `solid` | `[]` | array |
|
|
732
|
+
| `opacity` | `1` | number |
|
|
733
|
+
|
|
714
734
|
## `Timer` — `incanto`
|
|
715
735
|
|
|
716
736
|
| Prop | Default | Kind |
|
|
@@ -721,6 +741,25 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
|
|
|
721
741
|
|
|
722
742
|
Signals: `timeout`
|
|
723
743
|
|
|
744
|
+
## `Trail3D` — `incanto/3d`
|
|
745
|
+
|
|
746
|
+
| Prop | Default | Kind |
|
|
747
|
+
|---|---|---|
|
|
748
|
+
| `position` | `[0,0,0]` | array |
|
|
749
|
+
| `rotation` | `[0,0,0]` | array |
|
|
750
|
+
| `static` | `false` | boolean |
|
|
751
|
+
| `scale` | `[1,1,1]` | array |
|
|
752
|
+
| `visible` | `true` | boolean |
|
|
753
|
+
| `renderOrder` | `0` | number |
|
|
754
|
+
| `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
|
|
755
|
+
| `width` | `0.3` | number |
|
|
756
|
+
| `color` | `"#ffffff"` | string |
|
|
757
|
+
| `opacity` | `0.8` | number |
|
|
758
|
+
| `seconds` | `0.6` | number |
|
|
759
|
+
| `emitting` | `true` | boolean |
|
|
760
|
+
| `additive` | `false` | boolean |
|
|
761
|
+
| `minDistance` | `0.05` | number |
|
|
762
|
+
|
|
724
763
|
## `Tree3D` — `incanto/3d`
|
|
725
764
|
|
|
726
765
|
| Prop | Default | Kind |
|
|
@@ -780,6 +819,31 @@ Signals: `bannerShown`
|
|
|
780
819
|
| `background` | `"rgba(0,0,0,0.5)"` | string |
|
|
781
820
|
| `label` | `""` | string |
|
|
782
821
|
|
|
822
|
+
## `UiButton` — `incanto`
|
|
823
|
+
|
|
824
|
+
| Prop | Default | Kind |
|
|
825
|
+
|---|---|---|
|
|
826
|
+
| `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
|
|
827
|
+
| `visible` | `true` | boolean |
|
|
828
|
+
| `text` | `"OK"` | string |
|
|
829
|
+
| `size` | `16` | number |
|
|
830
|
+
| `color` | `"#ffffff"` | string |
|
|
831
|
+
| `background` | `"rgba(255,255,255,0.14)"` | string |
|
|
832
|
+
| `disabled` | `false` | boolean |
|
|
833
|
+
|
|
834
|
+
Signals: `pressed`
|
|
835
|
+
|
|
836
|
+
## `UiDialogue` — `incanto`
|
|
837
|
+
|
|
838
|
+
| Prop | Default | Kind |
|
|
839
|
+
|---|---|---|
|
|
840
|
+
| `anchor` | `"bottom"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
|
|
841
|
+
| `visible` | `true` | boolean |
|
|
842
|
+
| `charsPerSecond` | `40` | number |
|
|
843
|
+
| `width` | `520` | number |
|
|
844
|
+
|
|
845
|
+
Signals: `lineShown` · `choiceMade` · `dialogueFinished`
|
|
846
|
+
|
|
783
847
|
## `UiText` — `incanto`
|
|
784
848
|
|
|
785
849
|
| Prop | Default | Kind |
|
|
@@ -951,6 +1015,15 @@ Signals: `arrived`
|
|
|
951
1015
|
| `frequency` | `1` | number |
|
|
952
1016
|
| `mode` | `"position"` | one of: `position` `rotation` `scale` |
|
|
953
1017
|
|
|
1018
|
+
### `PathFollow`
|
|
1019
|
+
|
|
1020
|
+
| Prop | Default | Kind |
|
|
1021
|
+
|---|---|---|
|
|
1022
|
+
| `speed` | `120` | number |
|
|
1023
|
+
| `loop` | `false` | boolean |
|
|
1024
|
+
|
|
1025
|
+
Signals: `waypointReached` · `arrived`
|
|
1026
|
+
|
|
954
1027
|
### `Patrol`
|
|
955
1028
|
|
|
956
1029
|
| Prop | Default | Kind |
|
|
@@ -133,6 +133,15 @@ Injected state combines with key state (vectors clamped to unit length).
|
|
|
133
133
|
- 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
|
|
134
134
|
`Player` node type. Verified in Chromium end-to-end.
|
|
135
135
|
|
|
136
|
+
## Platformer staples (2D)
|
|
137
|
+
|
|
138
|
+
- **One-way platforms**: `"collider": { "shape": "rect", "size": [300, 20],
|
|
139
|
+
"oneWay": true }` on a StaticBody2D — characters jump up THROUGH it and
|
|
140
|
+
land ON it (solid only when falling from above). The Mario ledge.
|
|
141
|
+
- **Moving platforms carry riders automatically**: a CharacterBody2D
|
|
142
|
+
standing on ANY body follows that body's movement (elevators, patrol
|
|
143
|
+
platforms — just animate the platform's `position`; the character rides).
|
|
144
|
+
|
|
136
145
|
## Joints (Joint2D / Joint3D)
|
|
137
146
|
|
|
138
147
|
Link two bodies: put the joint node as a CHILD of body A, point `target` at
|
|
@@ -198,3 +198,34 @@ depth-test off, so it shows through walls). Extent-less nodes get a small
|
|
|
198
198
|
marker cube at their position. That is the fastest answer to "which thing
|
|
199
199
|
on screen IS this node?" — and the reverse of it: walk the tree clicking
|
|
200
200
|
rows until the orange box lands on the thing you're hunting.
|
|
201
|
+
|
|
202
|
+
## Semantic warnings (audit)
|
|
203
|
+
|
|
204
|
+
`incanto-check` now WARNS about scenes that load fine but play wrong: no
|
|
205
|
+
`current` camera, physics bodies without colliders, animated nodes frozen
|
|
206
|
+
inside a `static` subtree, HUD widgets outside a HudLayer. Programmatic:
|
|
207
|
+
`auditScene(json)` from `incanto` or `incanto/test` returns the warnings.
|
|
208
|
+
|
|
209
|
+
## Deterministic replay (record once, regression-test forever)
|
|
210
|
+
|
|
211
|
+
The engine is fully deterministic under a seed + injected clock, so a
|
|
212
|
+
recording of (tick timestamps + input events) replays BIT-identically:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { Engine, loadScene, replay, startRecording } from 'incanto';
|
|
216
|
+
|
|
217
|
+
// while playing (dev build):
|
|
218
|
+
const rec = startRecording(engine);
|
|
219
|
+
// ... play the level ...
|
|
220
|
+
save.set('replays/level1', rec.stop());
|
|
221
|
+
|
|
222
|
+
// in a headless test:
|
|
223
|
+
const engine = new Engine({ seed: SAME_SEED });
|
|
224
|
+
engine.setScene(loadScene(sceneJson));
|
|
225
|
+
replay(engine, recording, { onTick(i) { /* mid-run asserts */ } });
|
|
226
|
+
expect(player.position).toEqual(expected); // exact, not approximate
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Rules that make it hold (the engine's own rules anyway): use `engine.rng`
|
|
230
|
+
never Math.random, dt/`engine.time` never Date.now. Gamepads replay through
|
|
231
|
+
the ACTIONS they were bound to, not raw pad state.
|