incanto 0.7.0 → 0.8.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 (48) hide show
  1. package/bin/incanto-check.mjs +6 -4
  2. package/dist/2d.d.ts +93 -3
  3. package/dist/2d.js +4 -4
  4. package/dist/3d.d.ts +45 -4
  5. package/dist/3d.js +4 -4
  6. package/dist/{audio-player-VSjk_vG8.d.ts → audio-player-Dyjg5k92.d.ts} +1 -1
  7. package/dist/audit-C6rMyict.js +58 -0
  8. package/dist/{behavior-e3kAmPGC.d.ts → behavior-Bod1AyJS.d.ts} +211 -174
  9. package/dist/{create-game-CEFoFN8d.js → create-game-66c4iYJE.js} +76 -6
  10. package/dist/{create-game-X5iRu6pf.js → create-game-BNDSbhy-.js} +88 -6
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/debug.js +3 -0
  13. package/dist/{errors-BMFaY68Q.d.ts → errors-1dXlIwoR.d.ts} +7 -1
  14. package/dist/{gameplay-CDFgSG6z.js → gameplay-DPMgZk9W.js} +108 -1
  15. package/dist/gameplay.d.ts +44 -3
  16. package/dist/gameplay.js +2 -2
  17. package/dist/index.d.ts +151 -9
  18. package/dist/index.js +189 -4
  19. package/dist/{loader-y_X4zYO0.d.ts → loader-BcUDfNHn.d.ts} +1 -1
  20. package/dist/net.d.ts +46 -2
  21. package/dist/net.js +54 -2
  22. package/dist/particle-sim-CyUU7HVU.js +281 -0
  23. package/dist/{physics-2d-DmQ540uR.js → physics-2d-q2N362ph.js} +45 -4
  24. package/dist/{physics-3d-CDjuhtt_.js → physics-3d-CvHr31d4.js} +1 -1
  25. package/dist/react.d.ts +1 -1
  26. package/dist/react.js +1 -1
  27. package/dist/{register-CscQqB7V.js → register-Cbs7QxoV.js} +139 -4
  28. package/dist/{register-ClKnoILk.js → register-Cqjxrfq0.js} +254 -4
  29. package/dist/{register-C35HDZpm.js → register-CxmuI9FL.js} +1 -1
  30. package/dist/{particle-sim-CrTE7c02.js → register-DRPIjrKG.js} +1185 -278
  31. package/dist/test.d.ts +4 -10
  32. package/dist/test.js +11 -16
  33. package/editor/assets/{agent8-1WJU-yXr.js → agent8-B8QnWCeP.js} +1 -1
  34. package/editor/assets/{index-DzYFgZbl.js → index-CVhnNkb3.js} +81 -81
  35. package/editor/index.html +1 -1
  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-gameplay-behaviors.md +28 -0
  43. package/skills/incanto-hud.md +24 -0
  44. package/skills/incanto-multiplayer.md +27 -0
  45. package/skills/incanto-node-reference.md +73 -0
  46. package/skills/incanto-physics-and-input.md +9 -0
  47. package/skills/incanto-verifying-your-game.md +40 -0
  48. package/dist/register-Dl_ixIJe.js +0 -834
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-DzYFgZbl.js"></script>
8
+ <script type="module" crossorigin src="./assets/index-CVhnNkb3.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="./assets/GameServer-C56iOUgF.js">
10
10
  <link rel="stylesheet" crossorigin href="./assets/index-DF3tMeKJ.css">
11
11
  </head>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incanto",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Vibe-coding-first web game engine SDK — JSON-driven scenes on three.js",
5
5
  "keywords": [
6
6
  "game-engine",
@@ -283,9 +283,15 @@
283
283
  {
284
284
  "$ref": "#/$defs/Terrain3D"
285
285
  },
286
+ {
287
+ "$ref": "#/$defs/TileMap2D"
288
+ },
286
289
  {
287
290
  "$ref": "#/$defs/Timer"
288
291
  },
292
+ {
293
+ "$ref": "#/$defs/Trail3D"
294
+ },
289
295
  {
290
296
  "$ref": "#/$defs/Tree3D"
291
297
  },
@@ -298,6 +304,12 @@
298
304
  {
299
305
  "$ref": "#/$defs/UiBar"
300
306
  },
307
+ {
308
+ "$ref": "#/$defs/UiButton"
309
+ },
310
+ {
311
+ "$ref": "#/$defs/UiDialogue"
312
+ },
301
313
  {
302
314
  "$ref": "#/$defs/UiText"
303
315
  },
@@ -1753,6 +1765,10 @@
1753
1765
  "skinYawOffset": {
1754
1766
  "type": "number",
1755
1767
  "default": 0
1768
+ },
1769
+ "animations": {
1770
+ "type": "object",
1771
+ "default": {}
1756
1772
  }
1757
1773
  },
1758
1774
  "additionalProperties": false
@@ -4842,6 +4858,124 @@
4842
4858
  },
4843
4859
  "required": ["name", "type"]
4844
4860
  },
4861
+ "TileMap2D": {
4862
+ "type": "object",
4863
+ "properties": {
4864
+ "name": {
4865
+ "type": "string"
4866
+ },
4867
+ "uid": {
4868
+ "type": "string"
4869
+ },
4870
+ "type": {
4871
+ "const": "TileMap2D"
4872
+ },
4873
+ "groups": {
4874
+ "type": "array",
4875
+ "items": {
4876
+ "type": "string"
4877
+ }
4878
+ },
4879
+ "tags": {
4880
+ "type": "object"
4881
+ },
4882
+ "props": {
4883
+ "type": "object",
4884
+ "properties": {
4885
+ "position": {
4886
+ "type": "array",
4887
+ "items": {
4888
+ "type": "number"
4889
+ },
4890
+ "minItems": 2,
4891
+ "maxItems": 2,
4892
+ "default": [0, 0]
4893
+ },
4894
+ "rotation": {
4895
+ "type": "number",
4896
+ "default": 0
4897
+ },
4898
+ "static": {
4899
+ "type": "boolean",
4900
+ "default": false
4901
+ },
4902
+ "scale": {
4903
+ "type": "array",
4904
+ "items": {
4905
+ "type": "number"
4906
+ },
4907
+ "minItems": 2,
4908
+ "maxItems": 2,
4909
+ "default": [1, 1]
4910
+ },
4911
+ "renderOrder": {
4912
+ "type": "number",
4913
+ "default": 0
4914
+ },
4915
+ "orderGroup": {
4916
+ "type": "string",
4917
+ "enum": ["background", "terrain", "default", "characters", "effects", "overlay"],
4918
+ "default": "default"
4919
+ },
4920
+ "visible": {
4921
+ "type": "boolean",
4922
+ "default": true
4923
+ },
4924
+ "texture": {
4925
+ "type": "string",
4926
+ "default": ""
4927
+ },
4928
+ "tileSize": {
4929
+ "type": "number",
4930
+ "default": 32
4931
+ },
4932
+ "columns": {
4933
+ "type": "number",
4934
+ "default": 0
4935
+ },
4936
+ "cells": {
4937
+ "type": "array",
4938
+ "default": []
4939
+ },
4940
+ "legend": {
4941
+ "type": "object",
4942
+ "default": {}
4943
+ },
4944
+ "solid": {
4945
+ "type": "array",
4946
+ "default": []
4947
+ },
4948
+ "opacity": {
4949
+ "type": "number",
4950
+ "default": 1
4951
+ }
4952
+ },
4953
+ "additionalProperties": false
4954
+ },
4955
+ "script": {
4956
+ "type": "object",
4957
+ "properties": {
4958
+ "name": {
4959
+ "type": "string"
4960
+ },
4961
+ "props": {
4962
+ "type": "object"
4963
+ }
4964
+ },
4965
+ "required": ["name"]
4966
+ },
4967
+ "network": {
4968
+ "type": "object"
4969
+ },
4970
+ "children": {
4971
+ "type": "array",
4972
+ "items": {
4973
+ "$ref": "#/$defs/node"
4974
+ }
4975
+ }
4976
+ },
4977
+ "required": ["name", "type"]
4978
+ },
4845
4979
  "Timer": {
4846
4980
  "type": "object",
4847
4981
  "x-signals": ["timeout"],
@@ -4906,6 +5040,129 @@
4906
5040
  },
4907
5041
  "required": ["name", "type"]
4908
5042
  },
5043
+ "Trail3D": {
5044
+ "type": "object",
5045
+ "properties": {
5046
+ "name": {
5047
+ "type": "string"
5048
+ },
5049
+ "uid": {
5050
+ "type": "string"
5051
+ },
5052
+ "type": {
5053
+ "const": "Trail3D"
5054
+ },
5055
+ "groups": {
5056
+ "type": "array",
5057
+ "items": {
5058
+ "type": "string"
5059
+ }
5060
+ },
5061
+ "tags": {
5062
+ "type": "object"
5063
+ },
5064
+ "props": {
5065
+ "type": "object",
5066
+ "properties": {
5067
+ "position": {
5068
+ "type": "array",
5069
+ "items": {
5070
+ "type": "number"
5071
+ },
5072
+ "minItems": 3,
5073
+ "maxItems": 3,
5074
+ "default": [0, 0, 0]
5075
+ },
5076
+ "rotation": {
5077
+ "type": "array",
5078
+ "items": {
5079
+ "type": "number"
5080
+ },
5081
+ "minItems": 3,
5082
+ "maxItems": 3,
5083
+ "default": [0, 0, 0]
5084
+ },
5085
+ "static": {
5086
+ "type": "boolean",
5087
+ "default": false
5088
+ },
5089
+ "scale": {
5090
+ "type": "array",
5091
+ "items": {
5092
+ "type": "number"
5093
+ },
5094
+ "minItems": 3,
5095
+ "maxItems": 3,
5096
+ "default": [1, 1, 1]
5097
+ },
5098
+ "visible": {
5099
+ "type": "boolean",
5100
+ "default": true
5101
+ },
5102
+ "renderOrder": {
5103
+ "type": "number",
5104
+ "default": 0
5105
+ },
5106
+ "orderGroup": {
5107
+ "type": "string",
5108
+ "enum": ["background", "terrain", "default", "characters", "effects", "overlay"],
5109
+ "default": "default"
5110
+ },
5111
+ "width": {
5112
+ "type": "number",
5113
+ "default": 0.3
5114
+ },
5115
+ "color": {
5116
+ "type": "string",
5117
+ "default": "#ffffff"
5118
+ },
5119
+ "opacity": {
5120
+ "type": "number",
5121
+ "default": 0.8
5122
+ },
5123
+ "seconds": {
5124
+ "type": "number",
5125
+ "default": 0.6
5126
+ },
5127
+ "emitting": {
5128
+ "type": "boolean",
5129
+ "default": true
5130
+ },
5131
+ "additive": {
5132
+ "type": "boolean",
5133
+ "default": false
5134
+ },
5135
+ "minDistance": {
5136
+ "type": "number",
5137
+ "default": 0.05
5138
+ }
5139
+ },
5140
+ "additionalProperties": false
5141
+ },
5142
+ "script": {
5143
+ "type": "object",
5144
+ "properties": {
5145
+ "name": {
5146
+ "type": "string"
5147
+ },
5148
+ "props": {
5149
+ "type": "object"
5150
+ }
5151
+ },
5152
+ "required": ["name"]
5153
+ },
5154
+ "network": {
5155
+ "type": "object"
5156
+ },
5157
+ "children": {
5158
+ "type": "array",
5159
+ "items": {
5160
+ "$ref": "#/$defs/node"
5161
+ }
5162
+ }
5163
+ },
5164
+ "required": ["name", "type"]
5165
+ },
4909
5166
  "Tree3D": {
4910
5167
  "type": "object",
4911
5168
  "properties": {
@@ -5315,6 +5572,176 @@
5315
5572
  },
5316
5573
  "required": ["name", "type"]
5317
5574
  },
5575
+ "UiButton": {
5576
+ "type": "object",
5577
+ "x-signals": ["pressed"],
5578
+ "properties": {
5579
+ "name": {
5580
+ "type": "string"
5581
+ },
5582
+ "uid": {
5583
+ "type": "string"
5584
+ },
5585
+ "type": {
5586
+ "const": "UiButton"
5587
+ },
5588
+ "groups": {
5589
+ "type": "array",
5590
+ "items": {
5591
+ "type": "string"
5592
+ }
5593
+ },
5594
+ "tags": {
5595
+ "type": "object"
5596
+ },
5597
+ "props": {
5598
+ "type": "object",
5599
+ "properties": {
5600
+ "anchor": {
5601
+ "type": "string",
5602
+ "enum": [
5603
+ "topLeft",
5604
+ "top",
5605
+ "topRight",
5606
+ "left",
5607
+ "center",
5608
+ "right",
5609
+ "bottomLeft",
5610
+ "bottom",
5611
+ "bottomRight"
5612
+ ],
5613
+ "default": "topLeft"
5614
+ },
5615
+ "visible": {
5616
+ "type": "boolean",
5617
+ "default": true
5618
+ },
5619
+ "text": {
5620
+ "type": "string",
5621
+ "default": "OK"
5622
+ },
5623
+ "size": {
5624
+ "type": "number",
5625
+ "default": 16
5626
+ },
5627
+ "color": {
5628
+ "type": "string",
5629
+ "default": "#ffffff"
5630
+ },
5631
+ "background": {
5632
+ "type": "string",
5633
+ "default": "rgba(255,255,255,0.14)"
5634
+ },
5635
+ "disabled": {
5636
+ "type": "boolean",
5637
+ "default": false
5638
+ }
5639
+ },
5640
+ "additionalProperties": false
5641
+ },
5642
+ "script": {
5643
+ "type": "object",
5644
+ "properties": {
5645
+ "name": {
5646
+ "type": "string"
5647
+ },
5648
+ "props": {
5649
+ "type": "object"
5650
+ }
5651
+ },
5652
+ "required": ["name"]
5653
+ },
5654
+ "network": {
5655
+ "type": "object"
5656
+ },
5657
+ "children": {
5658
+ "type": "array",
5659
+ "items": {
5660
+ "$ref": "#/$defs/node"
5661
+ }
5662
+ }
5663
+ },
5664
+ "required": ["name", "type"]
5665
+ },
5666
+ "UiDialogue": {
5667
+ "type": "object",
5668
+ "x-signals": ["lineShown", "choiceMade", "dialogueFinished"],
5669
+ "properties": {
5670
+ "name": {
5671
+ "type": "string"
5672
+ },
5673
+ "uid": {
5674
+ "type": "string"
5675
+ },
5676
+ "type": {
5677
+ "const": "UiDialogue"
5678
+ },
5679
+ "groups": {
5680
+ "type": "array",
5681
+ "items": {
5682
+ "type": "string"
5683
+ }
5684
+ },
5685
+ "tags": {
5686
+ "type": "object"
5687
+ },
5688
+ "props": {
5689
+ "type": "object",
5690
+ "properties": {
5691
+ "anchor": {
5692
+ "type": "string",
5693
+ "enum": [
5694
+ "topLeft",
5695
+ "top",
5696
+ "topRight",
5697
+ "left",
5698
+ "center",
5699
+ "right",
5700
+ "bottomLeft",
5701
+ "bottom",
5702
+ "bottomRight"
5703
+ ],
5704
+ "default": "bottom"
5705
+ },
5706
+ "visible": {
5707
+ "type": "boolean",
5708
+ "default": true
5709
+ },
5710
+ "charsPerSecond": {
5711
+ "type": "number",
5712
+ "default": 40
5713
+ },
5714
+ "width": {
5715
+ "type": "number",
5716
+ "default": 520
5717
+ }
5718
+ },
5719
+ "additionalProperties": false
5720
+ },
5721
+ "script": {
5722
+ "type": "object",
5723
+ "properties": {
5724
+ "name": {
5725
+ "type": "string"
5726
+ },
5727
+ "props": {
5728
+ "type": "object"
5729
+ }
5730
+ },
5731
+ "required": ["name"]
5732
+ },
5733
+ "network": {
5734
+ "type": "object"
5735
+ },
5736
+ "children": {
5737
+ "type": "array",
5738
+ "items": {
5739
+ "$ref": "#/$defs/node"
5740
+ }
5741
+ }
5742
+ },
5743
+ "required": ["name", "type"]
5744
+ },
5318
5745
  "UiText": {
5319
5746
  "type": "object",
5320
5747
  "properties": {
@@ -97,6 +97,20 @@ shambling clip + glowing eyes for an enemy that reads as a different creature.
97
97
  "animation": "$walk", "castShadow": true } }
98
98
  ```
99
99
 
100
+ ## Animations without code
101
+
102
+ Map movement states straight in JSON — no behavior needed:
103
+
104
+ ```json
105
+ { "name": "Controller", "type": "CharacterController3D",
106
+ "props": { "view": "free",
107
+ "animations": { "idle": "$idle", "walk": "$walk", "run": "$run",
108
+ "fastRun": "$sprint", "airborne": "$jump" } } }
109
+ ```
110
+
111
+ The controller writes `skin.animation` on every state change (crossfaded by
112
+ the model). The `movementStateChanged` signal still fires for extras.
113
+
100
114
  The controller also yaw-rotates the sibling at `skinPath` ('../Skin') toward
101
115
  the move direction at `turnSpeed` rad/s (100 = instant snap) — the body
102
116
  itself never rotates. The skin MOUNTS at 180° (facing away from the default
@@ -98,6 +98,29 @@ something up, `hurt` when Health takes damage:
98
98
  > same unlock listener that retries `src` players also resumes the SFX context
99
99
  > (see §2).
100
100
 
101
+ ### Continuous voices — engine hum, wind, thrusters (`startVoice`)
102
+
103
+ One-shot presets can't do a sound that **lasts and retunes** — a motor that
104
+ follows the RPM, wind that rises with speed. `engine.sfx.startVoice(preset)`
105
+ starts a live synth voice you retune every frame:
106
+
107
+ ```ts
108
+ const motor = engine.sfx.startVoice('engine'); // presets: engine | wind | hum | noise
109
+ // per frame (e.g. in a script's update):
110
+ motor.set({ pitch: 0.8 + speed / maxSpeed, volume: throttle * 0.6 });
111
+ // on destroy / scene change:
112
+ motor.stop(); // fades out over 0.15s and frees the WebAudio nodes
113
+ motor.stop(1.2); // …or a slow custom fade
114
+ ```
115
+
116
+ - **`pitch`** — 1 = the preset's base tone; drive it from speed/RPM/height.
117
+ - **`volume`** — 0..1; `startVoice(preset, gain)` sets an overall ceiling.
118
+ - Presets: `engine` (detuned-saw motor growl), `wind` (band-passed rush),
119
+ `hum` (soft pad — shields, auras, reactors), `noise` (thrusters, static).
120
+ - Headless the handle is **inert** (`set`/`stop` are no-ops) — keep the same
121
+ call sites, no `if (browser)` guards. Voices need the same first-gesture
122
+ unlock as everything else.
123
+
101
124
  ---
102
125
 
103
126
  ## 2. The AudioPlayer node (preset vs src file)
@@ -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 —
@@ -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.