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.
Files changed (50) hide show
  1. package/bin/incanto-check.mjs +6 -4
  2. package/dist/2d.d.ts +91 -3
  3. package/dist/2d.js +4 -4
  4. package/dist/3d.d.ts +43 -4
  5. package/dist/3d.js +4 -4
  6. package/dist/{audio-player-C3bH_eZ5.d.ts → audio-player-Dyjg5k92.d.ts} +1 -1
  7. package/dist/audit-C6rMyict.js +58 -0
  8. package/dist/{behavior-q3cejJgs.d.ts → behavior-Bod1AyJS.d.ts} +31 -1
  9. package/dist/{create-game-rFulaS3s.js → create-game-Bydey3CZ.js} +4 -5
  10. package/dist/{create-game-DuDkqnjl.js → create-game-C7Zsqbko.js} +4 -5
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/{errors-BMFaY68Q.d.ts → errors-1dXlIwoR.d.ts} +7 -1
  13. package/dist/{gameplay-CDFgSG6z.js → gameplay-DPMgZk9W.js} +108 -1
  14. package/dist/gameplay.d.ts +44 -3
  15. package/dist/gameplay.js +2 -2
  16. package/dist/index.d.ts +151 -9
  17. package/dist/index.js +189 -4
  18. package/dist/{loader-DnLMMRy8.d.ts → loader-BcUDfNHn.d.ts} +1 -1
  19. package/dist/net.d.ts +46 -2
  20. package/dist/net.js +54 -2
  21. package/dist/particle-sim-CyUU7HVU.js +281 -0
  22. package/dist/{physics-2d-CZM5Y90X.js → physics-2d-DweTURFt.js} +45 -4
  23. package/dist/{physics-3d-DT5e8izo.js → physics-3d-Clv1uFzk.js} +1 -1
  24. package/dist/react.d.ts +1 -1
  25. package/dist/react.js +1 -1
  26. package/dist/{register-Cl3pAxX-.js → register-BD6zgrv_.js} +254 -4
  27. package/dist/{register-C35HDZpm.js → register-Cwc-yCYx.js} +1 -1
  28. package/dist/{particle-sim-m3CdTGSl.js → register-DJZXxwAn.js} +1178 -278
  29. package/dist/{register-CpXcMiEk.js → register-iUPtts7T.js} +141 -4
  30. package/dist/test.d.ts +4 -10
  31. package/dist/test.js +11 -16
  32. package/editor/assets/{agent8-N4bwVH7C.js → agent8-B0ZGx-N1.js} +1 -1
  33. package/editor/assets/index-BLzp8moy.js +7417 -0
  34. package/editor/assets/{index-DF3tMeKJ.css → index-D8QvwvOm.css} +1 -1
  35. package/editor/index.html +2 -2
  36. package/package.json +1 -1
  37. package/schemas/scene.schema.json +427 -0
  38. package/skills/incanto-3d-character.md +14 -0
  39. package/skills/incanto-audio.md +23 -0
  40. package/skills/incanto-building-2d-games.md +31 -0
  41. package/skills/incanto-building-3d-games.md +21 -0
  42. package/skills/incanto-editor.md +13 -0
  43. package/skills/incanto-gameplay-behaviors.md +28 -0
  44. package/skills/incanto-hud.md +24 -0
  45. package/skills/incanto-multiplayer.md +27 -0
  46. package/skills/incanto-node-reference.md +73 -0
  47. package/skills/incanto-physics-and-input.md +9 -0
  48. package/skills/incanto-verifying-your-game.md +31 -0
  49. package/dist/register-Dl_ixIJe.js +0 -834
  50. 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 —
@@ -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.
@@ -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.