incanto 0.33.0 → 0.35.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 (58) hide show
  1. package/dist/2d.d.ts +5 -5
  2. package/dist/2d.js +3 -3
  3. package/dist/3d.d.ts +122 -14
  4. package/dist/3d.js +5 -5
  5. package/dist/{audit-C4kmDK0o.js → audit-C5oZGAru.js} +37 -0
  6. package/dist/{behavior-CKwTCjfR.d.ts → behavior-CGSCWOHB.d.ts} +319 -22
  7. package/dist/{create-game-D2QU5x7S.js → create-game-B0Wfhi2-.js} +6 -5
  8. package/dist/{create-game-sFuTLqjD.js → create-game-BUD89Kqh.js} +20 -6
  9. package/dist/debug.d.ts +1 -1
  10. package/dist/{duplicate-BgnG1Lqz.js → duplicate-C716f-97.js} +1 -1
  11. package/dist/{editor-switch-BJb-CWfA.d.ts → editor-switch-DyXEtH36.d.ts} +1 -1
  12. package/dist/editor.js +1739 -1312
  13. package/dist/env.d.ts +1 -1
  14. package/dist/{environment-presets-CQtEGogB.js → environment-presets-TAGvmM3z.js} +240 -74
  15. package/dist/{errors-1dXlIwoR.d.ts → errors-BY2kL0hv.d.ts} +1 -1
  16. package/dist/{gameplay-BpQCbABv.js → gameplay-BvhcQbfJ.js} +75 -11
  17. package/dist/gameplay.d.ts +12 -5
  18. package/dist/gameplay.js +1 -1
  19. package/dist/index.d.ts +72 -7
  20. package/dist/index.js +6 -6
  21. package/dist/{loader-COn5fS0o.d.ts → loader-CUcj00M8.d.ts} +19 -2
  22. package/dist/{loader-Buk8Bu1h.js → loader-Mig5fY4n.js} +110 -18
  23. package/dist/net.d.ts +2 -2
  24. package/dist/net.js +3 -3
  25. package/dist/{particle-sim-CwJ5rI_P.d.ts → particle-sim-B-vZBF5R.d.ts} +1 -1
  26. package/dist/{pathfinding-DUw9mir9.d.ts → pathfinding-DgOo2KNF.d.ts} +1 -1
  27. package/dist/{physics-2d-DjXR5DMu.js → physics-2d-DqAclql-.js} +22 -9
  28. package/dist/{physics-3d-DF8npb1O.js → physics-3d-DxBH4sIF.js} +27 -11
  29. package/dist/react.d.ts +2 -2
  30. package/dist/react.js +1 -1
  31. package/dist/{register-CscIzJEO.js → register-BFLg0-_i.js} +697 -21
  32. package/dist/{register-CtI-itec.js → register-BjbPMA5B.js} +2 -2
  33. package/dist/{register-FIJtNbub.js → register-DSmIRAf7.js} +11 -5
  34. package/dist/{schema-CcoWb32N.d.ts → schema-3ywbdlrv.d.ts} +10 -0
  35. package/dist/{test-BRxLd2jH.js → test-9EokzbRd.js} +10 -10
  36. package/dist/test.d.ts +4 -4
  37. package/dist/test.js +2 -2
  38. package/dist/vite.js +1 -1
  39. package/editor/assets/{agent8-BdDP3xKW.js → agent8-D3_GWeuh.js} +1 -1
  40. package/editor/assets/{debug-DbjTyTlC.js → debug-CX0LDd1r.js} +1 -1
  41. package/editor/assets/index-CMlKFT0C.js +10773 -0
  42. package/editor/index.html +1 -1
  43. package/package.json +1 -1
  44. package/schemas/scene.schema.json +251 -6
  45. package/skills/incanto-assets.md +21 -0
  46. package/skills/incanto-behaviors-and-scripts.md +16 -0
  47. package/skills/incanto-editor.md +17 -1
  48. package/skills/incanto-gameplay-behaviors.md +6 -0
  49. package/skills/incanto-hud.md +73 -1
  50. package/skills/incanto-localization.md +132 -0
  51. package/skills/incanto-node-reference.md +78 -24
  52. package/skills/incanto-save-slots.md +152 -0
  53. package/templates-app/beacon-isle-3d/package.json +1 -1
  54. package/templates-app/tps-3d/package.json +1 -1
  55. package/templates-app/village-quest-3d/PROJECT/Context.md +16 -0
  56. package/templates-app/village-quest-3d/package.json +1 -1
  57. package/templates-app/village-quest-3d/src/village.scene.json +25 -3
  58. package/editor/assets/index-BWCudoz1.js +0 -10696
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-BWCudoz1.js"></script>
8
+ <script type="module" crossorigin src="./assets/index-CMlKFT0C.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="./assets/GameServer-C56iOUgF.js">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incanto",
3
- "version": "0.33.0",
3
+ "version": "0.35.0",
4
4
  "description": "Vibe-coding-first web game engine SDK — JSON-driven scenes on three.js",
5
5
  "keywords": [
6
6
  "game-engine",
@@ -325,6 +325,9 @@
325
325
  {
326
326
  "$ref": "#/$defs/UiImage"
327
327
  },
328
+ {
329
+ "$ref": "#/$defs/UiLanguageSelect"
330
+ },
328
331
  {
329
332
  "$ref": "#/$defs/UiPanel"
330
333
  },
@@ -1202,6 +1205,7 @@
1202
1205
  },
1203
1206
  "target": {
1204
1207
  "type": "string",
1208
+ "description": "A node path: '%UniqueName', '/Absolute/From/Root', or relative to this node ('../Skin').",
1205
1209
  "default": ""
1206
1210
  },
1207
1211
  "bone": {
@@ -1308,6 +1312,7 @@
1308
1312
  },
1309
1313
  "target": {
1310
1314
  "type": "string",
1315
+ "description": "A node path: '%UniqueName', '/Absolute/From/Root', or relative to this node ('../Skin').",
1311
1316
  "default": ""
1312
1317
  },
1313
1318
  "bone": {
@@ -1316,6 +1321,7 @@
1316
1321
  },
1317
1322
  "lookAt": {
1318
1323
  "type": "string",
1324
+ "description": "A node path: '%UniqueName', '/Absolute/From/Root', or relative to this node ('../Skin').",
1319
1325
  "default": ""
1320
1326
  },
1321
1327
  "maxAngleDeg": {
@@ -1423,6 +1429,7 @@
1423
1429
  },
1424
1430
  "follow": {
1425
1431
  "type": "string",
1432
+ "description": "A node path: '%UniqueName', '/Absolute/From/Root', or relative to this node ('../Skin').",
1426
1433
  "default": ""
1427
1434
  },
1428
1435
  "smoothing": {
@@ -2076,6 +2083,7 @@
2076
2083
  },
2077
2084
  "skinPath": {
2078
2085
  "type": "string",
2086
+ "description": "A node path: '%UniqueName', '/Absolute/From/Root', or relative to this node ('../Skin').",
2079
2087
  "default": "../Skin"
2080
2088
  },
2081
2089
  "skinYawOffset": {
@@ -2469,6 +2477,7 @@
2469
2477
  },
2470
2478
  "terrain": {
2471
2479
  "type": "string",
2480
+ "description": "A node path: '%UniqueName', '/Absolute/From/Root', or relative to this node ('../Skin').",
2472
2481
  "default": ""
2473
2482
  }
2474
2483
  },
@@ -2663,6 +2672,7 @@
2663
2672
  },
2664
2673
  "terrain": {
2665
2674
  "type": "string",
2675
+ "description": "A node path: '%UniqueName', '/Absolute/From/Root', or relative to this node ('../Skin').",
2666
2676
  "default": ""
2667
2677
  }
2668
2678
  },
@@ -2723,6 +2733,10 @@
2723
2733
  "visible": {
2724
2734
  "type": "boolean",
2725
2735
  "default": true
2736
+ },
2737
+ "focusNavigation": {
2738
+ "type": "boolean",
2739
+ "default": false
2726
2740
  }
2727
2741
  },
2728
2742
  "additionalProperties": false
@@ -2952,6 +2966,7 @@
2952
2966
  },
2953
2967
  "target": {
2954
2968
  "type": "string",
2969
+ "description": "A node path: '%UniqueName', '/Absolute/From/Root', or relative to this node ('../Skin').",
2955
2970
  "default": ""
2956
2971
  },
2957
2972
  "anchor": {
@@ -3089,6 +3104,7 @@
3089
3104
  },
3090
3105
  "target": {
3091
3106
  "type": "string",
3107
+ "description": "A node path: '%UniqueName', '/Absolute/From/Root', or relative to this node ('../Skin').",
3092
3108
  "default": ""
3093
3109
  },
3094
3110
  "anchor": {
@@ -4869,6 +4885,7 @@
4869
4885
  },
4870
4886
  "terrain": {
4871
4887
  "type": "string",
4888
+ "description": "A node path: '%UniqueName', '/Absolute/From/Root', or relative to this node ('../Skin').",
4872
4889
  "default": ""
4873
4890
  },
4874
4891
  "carve": {
@@ -5990,6 +6007,7 @@
5990
6007
  },
5991
6008
  "terrain": {
5992
6009
  "type": "string",
6010
+ "description": "A node path: '%UniqueName', '/Absolute/From/Root', or relative to this node ('../Skin').",
5993
6011
  "default": ""
5994
6012
  }
5995
6013
  },
@@ -6087,7 +6105,7 @@
6087
6105
  },
6088
6106
  "UiBanner": {
6089
6107
  "type": "object",
6090
- "x-signals": ["bannerShown"],
6108
+ "x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped", "bannerShown"],
6091
6109
  "properties": {
6092
6110
  "name": {
6093
6111
  "type": "string"
@@ -6129,6 +6147,18 @@
6129
6147
  "type": "boolean",
6130
6148
  "default": true
6131
6149
  },
6150
+ "focusable": {
6151
+ "type": "boolean",
6152
+ "default": false
6153
+ },
6154
+ "draggable": {
6155
+ "type": "boolean",
6156
+ "default": false
6157
+ },
6158
+ "dropTarget": {
6159
+ "type": "boolean",
6160
+ "default": false
6161
+ },
6132
6162
  "size": {
6133
6163
  "type": "number",
6134
6164
  "default": 42
@@ -6166,6 +6196,7 @@
6166
6196
  },
6167
6197
  "UiBar": {
6168
6198
  "type": "object",
6199
+ "x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped"],
6169
6200
  "properties": {
6170
6201
  "name": {
6171
6202
  "type": "string"
@@ -6207,6 +6238,18 @@
6207
6238
  "type": "boolean",
6208
6239
  "default": true
6209
6240
  },
6241
+ "focusable": {
6242
+ "type": "boolean",
6243
+ "default": false
6244
+ },
6245
+ "draggable": {
6246
+ "type": "boolean",
6247
+ "default": false
6248
+ },
6249
+ "dropTarget": {
6250
+ "type": "boolean",
6251
+ "default": false
6252
+ },
6210
6253
  "value": {
6211
6254
  "type": "number",
6212
6255
  "default": 100
@@ -6272,7 +6315,7 @@
6272
6315
  },
6273
6316
  "UiButton": {
6274
6317
  "type": "object",
6275
- "x-signals": ["pressed"],
6318
+ "x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped", "pressed"],
6276
6319
  "properties": {
6277
6320
  "name": {
6278
6321
  "type": "string"
@@ -6314,6 +6357,18 @@
6314
6357
  "type": "boolean",
6315
6358
  "default": true
6316
6359
  },
6360
+ "focusable": {
6361
+ "type": "boolean",
6362
+ "default": true
6363
+ },
6364
+ "draggable": {
6365
+ "type": "boolean",
6366
+ "default": false
6367
+ },
6368
+ "dropTarget": {
6369
+ "type": "boolean",
6370
+ "default": false
6371
+ },
6317
6372
  "text": {
6318
6373
  "type": "string",
6319
6374
  "default": "OK"
@@ -6363,7 +6418,15 @@
6363
6418
  },
6364
6419
  "UiDialogue": {
6365
6420
  "type": "object",
6366
- "x-signals": ["lineShown", "choiceMade", "dialogueFinished"],
6421
+ "x-signals": [
6422
+ "dragStarted",
6423
+ "dragCancelled",
6424
+ "droppedOn",
6425
+ "dropped",
6426
+ "lineShown",
6427
+ "choiceMade",
6428
+ "dialogueFinished"
6429
+ ],
6367
6430
  "properties": {
6368
6431
  "name": {
6369
6432
  "type": "string"
@@ -6405,6 +6468,18 @@
6405
6468
  "type": "boolean",
6406
6469
  "default": true
6407
6470
  },
6471
+ "focusable": {
6472
+ "type": "boolean",
6473
+ "default": false
6474
+ },
6475
+ "draggable": {
6476
+ "type": "boolean",
6477
+ "default": false
6478
+ },
6479
+ "dropTarget": {
6480
+ "type": "boolean",
6481
+ "default": false
6482
+ },
6408
6483
  "charsPerSecond": {
6409
6484
  "type": "number",
6410
6485
  "default": 40
@@ -6442,6 +6517,7 @@
6442
6517
  },
6443
6518
  "UiImage": {
6444
6519
  "type": "object",
6520
+ "x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped"],
6445
6521
  "properties": {
6446
6522
  "name": {
6447
6523
  "type": "string"
@@ -6483,6 +6559,18 @@
6483
6559
  "type": "boolean",
6484
6560
  "default": true
6485
6561
  },
6562
+ "focusable": {
6563
+ "type": "boolean",
6564
+ "default": false
6565
+ },
6566
+ "draggable": {
6567
+ "type": "boolean",
6568
+ "default": false
6569
+ },
6570
+ "dropTarget": {
6571
+ "type": "boolean",
6572
+ "default": false
6573
+ },
6486
6574
  "src": {
6487
6575
  "type": "string",
6488
6576
  "default": ""
@@ -6535,8 +6623,104 @@
6535
6623
  },
6536
6624
  "required": ["name", "type"]
6537
6625
  },
6626
+ "UiLanguageSelect": {
6627
+ "type": "object",
6628
+ "x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped", "changed"],
6629
+ "properties": {
6630
+ "name": {
6631
+ "type": "string"
6632
+ },
6633
+ "uid": {
6634
+ "type": "string"
6635
+ },
6636
+ "type": {
6637
+ "const": "UiLanguageSelect"
6638
+ },
6639
+ "groups": {
6640
+ "type": "array",
6641
+ "items": {
6642
+ "type": "string"
6643
+ }
6644
+ },
6645
+ "tags": {
6646
+ "type": "object"
6647
+ },
6648
+ "props": {
6649
+ "type": "object",
6650
+ "properties": {
6651
+ "anchor": {
6652
+ "type": "string",
6653
+ "enum": [
6654
+ "topLeft",
6655
+ "top",
6656
+ "topRight",
6657
+ "left",
6658
+ "center",
6659
+ "right",
6660
+ "bottomLeft",
6661
+ "bottom",
6662
+ "bottomRight"
6663
+ ],
6664
+ "default": "topLeft"
6665
+ },
6666
+ "visible": {
6667
+ "type": "boolean",
6668
+ "default": true
6669
+ },
6670
+ "focusable": {
6671
+ "type": "boolean",
6672
+ "default": true
6673
+ },
6674
+ "draggable": {
6675
+ "type": "boolean",
6676
+ "default": false
6677
+ },
6678
+ "dropTarget": {
6679
+ "type": "boolean",
6680
+ "default": false
6681
+ },
6682
+ "label": {
6683
+ "type": "string",
6684
+ "default": "Language"
6685
+ },
6686
+ "options": {
6687
+ "type": "string",
6688
+ "default": ""
6689
+ },
6690
+ "value": {
6691
+ "type": "string",
6692
+ "default": ""
6693
+ }
6694
+ },
6695
+ "additionalProperties": false
6696
+ },
6697
+ "script": {
6698
+ "type": "object",
6699
+ "properties": {
6700
+ "name": {
6701
+ "type": "string"
6702
+ },
6703
+ "props": {
6704
+ "type": "object"
6705
+ }
6706
+ },
6707
+ "required": ["name"]
6708
+ },
6709
+ "network": {
6710
+ "type": "object"
6711
+ },
6712
+ "children": {
6713
+ "type": "array",
6714
+ "items": {
6715
+ "$ref": "#/$defs/node"
6716
+ }
6717
+ }
6718
+ },
6719
+ "required": ["name", "type"]
6720
+ },
6538
6721
  "UiPanel": {
6539
6722
  "type": "object",
6723
+ "x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped"],
6540
6724
  "properties": {
6541
6725
  "name": {
6542
6726
  "type": "string"
@@ -6578,6 +6762,18 @@
6578
6762
  "type": "boolean",
6579
6763
  "default": true
6580
6764
  },
6765
+ "focusable": {
6766
+ "type": "boolean",
6767
+ "default": false
6768
+ },
6769
+ "draggable": {
6770
+ "type": "boolean",
6771
+ "default": false
6772
+ },
6773
+ "dropTarget": {
6774
+ "type": "boolean",
6775
+ "default": false
6776
+ },
6581
6777
  "layout": {
6582
6778
  "type": "string",
6583
6779
  "enum": ["column", "row", "grid"],
@@ -6644,7 +6840,7 @@
6644
6840
  },
6645
6841
  "UiSelect": {
6646
6842
  "type": "object",
6647
- "x-signals": ["changed"],
6843
+ "x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped", "changed"],
6648
6844
  "properties": {
6649
6845
  "name": {
6650
6846
  "type": "string"
@@ -6686,6 +6882,18 @@
6686
6882
  "type": "boolean",
6687
6883
  "default": true
6688
6884
  },
6885
+ "focusable": {
6886
+ "type": "boolean",
6887
+ "default": true
6888
+ },
6889
+ "draggable": {
6890
+ "type": "boolean",
6891
+ "default": false
6892
+ },
6893
+ "dropTarget": {
6894
+ "type": "boolean",
6895
+ "default": false
6896
+ },
6689
6897
  "label": {
6690
6898
  "type": "string",
6691
6899
  "default": ""
@@ -6727,7 +6935,7 @@
6727
6935
  },
6728
6936
  "UiSlider": {
6729
6937
  "type": "object",
6730
- "x-signals": ["changed"],
6938
+ "x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped", "changed"],
6731
6939
  "properties": {
6732
6940
  "name": {
6733
6941
  "type": "string"
@@ -6769,6 +6977,18 @@
6769
6977
  "type": "boolean",
6770
6978
  "default": true
6771
6979
  },
6980
+ "focusable": {
6981
+ "type": "boolean",
6982
+ "default": true
6983
+ },
6984
+ "draggable": {
6985
+ "type": "boolean",
6986
+ "default": false
6987
+ },
6988
+ "dropTarget": {
6989
+ "type": "boolean",
6990
+ "default": false
6991
+ },
6772
6992
  "label": {
6773
6993
  "type": "string",
6774
6994
  "default": ""
@@ -6826,6 +7046,7 @@
6826
7046
  },
6827
7047
  "UiText": {
6828
7048
  "type": "object",
7049
+ "x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped"],
6829
7050
  "properties": {
6830
7051
  "name": {
6831
7052
  "type": "string"
@@ -6867,6 +7088,18 @@
6867
7088
  "type": "boolean",
6868
7089
  "default": true
6869
7090
  },
7091
+ "focusable": {
7092
+ "type": "boolean",
7093
+ "default": false
7094
+ },
7095
+ "draggable": {
7096
+ "type": "boolean",
7097
+ "default": false
7098
+ },
7099
+ "dropTarget": {
7100
+ "type": "boolean",
7101
+ "default": false
7102
+ },
6870
7103
  "text": {
6871
7104
  "type": "string",
6872
7105
  "default": ""
@@ -6912,7 +7145,7 @@
6912
7145
  },
6913
7146
  "UiToggle": {
6914
7147
  "type": "object",
6915
- "x-signals": ["changed"],
7148
+ "x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped", "changed"],
6916
7149
  "properties": {
6917
7150
  "name": {
6918
7151
  "type": "string"
@@ -6954,6 +7187,18 @@
6954
7187
  "type": "boolean",
6955
7188
  "default": true
6956
7189
  },
7190
+ "focusable": {
7191
+ "type": "boolean",
7192
+ "default": true
7193
+ },
7194
+ "draggable": {
7195
+ "type": "boolean",
7196
+ "default": false
7197
+ },
7198
+ "dropTarget": {
7199
+ "type": "boolean",
7200
+ "default": false
7201
+ },
6957
7202
  "label": {
6958
7203
  "type": "string",
6959
7204
  "default": ""
@@ -129,3 +129,24 @@ scene built that way stays plain JSON you can also hand-write.
129
129
 
130
130
  Swap in real art later by changing ONLY the `assets{}` entry — node props
131
131
  stay untouched.
132
+
133
+
134
+ ## Textures are shared automatically (3D)
135
+
136
+ A hundred `Sprite3D`s pointing at one atlas cost **one** fetch, one decode and
137
+ one GPU upload. The renderer's asset store keys textures by URL plus their
138
+ sampler settings, so you never need to hoist a texture yourself or worry that
139
+ spawning enemies re-downloads their sheet.
140
+
141
+ Two consequences worth knowing:
142
+
143
+ - **Do not mutate a texture you got from a node.** `Sprite3D` and `Terrain3D`
144
+ hold the shared master. The two nodes with per-node UV state
145
+ (`AnimatedSprite3D`'s frame window, `MeshInstance3D`'s `material.repeat`) get
146
+ a private clone automatically — you do not have to ask for it.
147
+ - **`pixelArt: true` also turns mipmaps off**, which is what makes pixel art stay
148
+ crisp at distance instead of blurring into mud.
149
+
150
+ A texture that 404s now shows up in `game.assetErrors()` alongside models, by the
151
+ URL you wrote — so "why is my sprite invisible" is answerable without opening the
152
+ network tab.
@@ -38,6 +38,22 @@ class CoinCounter extends Behavior {
38
38
  registerBehavior('CoinCounter', CoinCounter); // EXPLICIT — before loadScene
39
39
  ```
40
40
 
41
+ **A behavior that holds progress should be able to save it.** `serialize()` /
42
+ `deserialize(data)` are two more optional hooks in the same list, and they are
43
+ what makes "continue where you left off" possible — see
44
+ `incanto-save-slots.md`. Save only what a fresh `onReady` could not recreate.
45
+
46
+ **A prop that holds a node path must say so.** Mark it `nodePath: true` and the
47
+ editor rewrites it when the target is renamed; leave it off and the rename
48
+ silently breaks the link (`getNodeOrNull` returns null, nothing throws).
49
+
50
+ ```ts
51
+ static readonly props = {
52
+ target: { default: '', nodePath: true }, // fed to getNode/getNodeOrNull
53
+ damage: { default: 10 },
54
+ };
55
+ ```
56
+
41
57
  ## The three-artifact contract (keep them in sync!)
42
58
 
43
59
  1. scene JSON `"script": {"name": "CoinCounter"}`
@@ -81,9 +81,25 @@ Opens a local page (default `http://127.0.0.1:5179/`) with three panes:
81
81
  CharacterBody2D) are ROLLED BACK entirely with the error in the banner; the
82
82
  tree never shows a state the engine would reject. Right-click for
83
83
  duplicate / rename (or double-click the name) / cut / copy / paste-as-child /
84
- delete — all act on the multi-selection. `+` adds a child of any registered
84
+ delete — all act on the multi-selection. **Rename REPAIRS references**: every
85
+ `connections[].from/to` and every node-path prop pointing at the node (or into
86
+ its subtree) is rewritten in the same edit, across all three path forms
87
+ (`%Unique`, `/root/…`, relative `../Skin`), and the editor reports what it
88
+ changed. A prop is rewritten only if its type declares `nodePath: true`, so a
89
+ text prop that merely reads "Player" is never touched. `+` adds a child of any registered
85
90
  type, `✕` deletes the selection. Selecting the ⚙ scene row edits the header
86
91
  (dimension, gravity with real inputs, environment/input/assets/multiplayer).
92
+ - **CONNECTIONS on the selected node** — the inspector's last section shows what
93
+ the node **emits** and what it **receives**, each row being signal → target →
94
+ handler with a ✕ to remove and `+ connect` to add. Signals come from the node
95
+ type merged with its behavior's `static signals`; the target picker lists every
96
+ node in the scene; the handler is a dropdown of real methods when the target
97
+ has no script, and free text when it does (those handlers live in your
98
+ TypeScript, which the editor never loads). An end that no longer resolves is
99
+ marked red with the reason. For what a row cannot express — `filter`, `once`,
100
+ bound args — the scene header still has `connections` as raw JSON.
101
+ **Validation now runs WITH connections**: the editor used to strip them before
102
+ loading, so it reported green for a scene the game hard-fails to open.
87
103
  - **Viewport with DIRECT MANIPULATION** — a mode toolbar (top left) + the
88
104
  Unity/Godot keys: **W move · E rotate · R scale** (active mode highlighted).
89
105
  A collider-visibility toggle sits beside the mode tools, and it shows the
@@ -58,6 +58,12 @@ Two kinds of node path resolve from DIFFERENT origins — mixing them up is the
58
58
  enemy (a child of the Spawner) reaches the player with the ABSOLUTE
59
59
  `/root/Player`, never a bare `Player` (which would look under the enemy).
60
60
 
61
+ Renaming a node in the editor now REWRITES both kinds for you (and reports what
62
+ it changed), so the paths above stay valid across a rename. Hand-edited JSON
63
+ still has to be kept in sync yourself — `bunx incanto-check` catches the
64
+ connections, but a behavior prop pointing at a name that no longer exists
65
+ resolves to null in silence.
66
+
61
67
  **Never write `/root/<RootName>/...`.** If your root node is named `Game`, the
62
68
  path is `/root/Player` — NOT `/root/Game/Player`: `/root/` already *is* the
63
69
  `Game` root, so `/root/Game/...` double-counts it and dangles at load. Likewise
@@ -101,7 +101,7 @@ so structure in the tree becomes structure on screen. Panels nest.
101
101
  { "name": "Volume", "type": "UiSlider", "props": { "label": "volume", "value": 0.8 } },
102
102
  { "name": "Invert", "type": "UiToggle", "props": { "label": "invert Y" } },
103
103
  { "name": "Quality", "type": "UiSelect", "props": { "options": "low,medium,high", "value": "high" } },
104
- { "name": "Resume", "type": "UiButton", "props": { "label": "Resume" } }
104
+ { "name": "Resume", "type": "UiButton", "props": { "text": "Resume" } }
105
105
  ] }
106
106
  ```
107
107
 
@@ -121,3 +121,75 @@ widget use `UiButton`'s `pressed`.
121
121
  Setting `.value` from a behavior updates the control and does **NOT** re-emit
122
122
  `changed` — restoring a saved setting must not fire the handler that saved it.
123
123
 
124
+ ## Playable on a controller (focus navigation)
125
+
126
+ A menu you can only click is not playable on a gamepad, and "add controller
127
+ support" was not something a JSON scene could express at all.
128
+
129
+ ```json
130
+ { "name": "Hud", "type": "HudLayer", "props": { "focusNavigation": true }, "children": [ … ] }
131
+ ```
132
+
133
+ Arrow keys / d-pad move the focus between the **focusable** widgets under that
134
+ layer, `Enter` / `A` activates, and the focused one wears a ring.
135
+ `UiButton`/`UiSlider`/`UiToggle`/`UiSelect` are focusable by default;
136
+ `UiText`/`UiBar`/`UiImage`/`UiPanel` are not, so arrowing never lands on a label.
137
+
138
+ What activation MEANS is per widget: a button presses, a toggle flips (left/right
139
+ sets it explicitly, which reads better on a pad), a select walks its list, a
140
+ slider nudges by one `step`.
141
+
142
+ **It is OFF by default**, deliberately: a game whose HUD happens to contain a
143
+ button must not lose its arrow keys the moment one exists. Turn it on for the
144
+ screens that ARE menus and off again when play resumes —
145
+ `hud.focusNavigation = false`. `hud.focus(widget)` sets the starting item, since
146
+ opening a menu should land somewhere rather than nowhere.
147
+
148
+ The gamepad codes are the ones the engine already produces from a pad
149
+ (`Pad12`–`Pad15` d-pad, `Pad0` = A), so nothing extra is declared.
150
+
151
+ ## Dragging things between slots (inventory)
152
+
153
+ An inventory is the one screen where "click it" is not enough, and every game
154
+ that wanted one dropped out of scene JSON to hand-roll pointer handlers. Two
155
+ props and four signals, on the widgets you already have — no new node type:
156
+
157
+ ```json
158
+ { "name": "SlotA", "type": "UiPanel", "props": { "dropTarget": true }, "children": [
159
+ { "name": "Potion", "type": "UiImage", "props": { "src": "$potion", "draggable": true } }
160
+ ] },
161
+ { "name": "SlotB", "type": "UiPanel", "props": { "dropTarget": true } }
162
+ ```
163
+
164
+ | signal | on | args |
165
+ | --- | --- | --- |
166
+ | `dragStarted` | the dragged widget | itself |
167
+ | `droppedOn` | the dragged widget | the target it landed on |
168
+ | `dropped` | the drop target | the widget that landed |
169
+ | `dragCancelled` | the dragged widget | itself (landed on nothing) |
170
+
171
+ Both ends are told, because both usually have work to do: the item leaves its old
172
+ slot, the slot takes it. **Who owns the item is your game's business** — these
173
+ report the GESTURE, not a model, so an inventory that stacks, swaps or refuses is
174
+ your `connections` and a behavior, not a prop nobody could have guessed.
175
+
176
+ A drop on a CHILD of a slot counts as a drop on the slot (the cursor lands on the
177
+ icon inside it, which is the normal case), a drop on nothing cancels, and a
178
+ widget never drops onto itself.
179
+
180
+
181
+
182
+ ## More than one language
183
+
184
+ Any text prop here can name a translation key instead of a literal:
185
+
186
+ ```json
187
+ { "name": "Score", "type": "UiText", "props": { "text": "@t:hud.score" } }
188
+ ```
189
+
190
+ Strings live in the scene header's `strings` block, English is the base, and a
191
+ key a locale omits falls back to English silently — which is the RIGHT answer
192
+ whenever the English term is more precise or the translation would not fit.
193
+ `UiLanguageSelect` is the picker, already wired.
194
+
195
+ Read `incanto-localization.md` before adding a second language.