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
@@ -0,0 +1,132 @@
1
+ ---
2
+ name: incanto-localization
3
+ description: Ship a game in more than one language: a scene-header `strings` table, `"@t:key"` text props, `engine.t()`, and a ready-made UiLanguageSelect that switches live and persists. English is the base — a key a locale omits falls back to it silently, on purpose. Use whenever a game needs a language other than English, or a language setting.
4
+ ---
5
+
6
+ # Localization — English base, other languages on top
7
+
8
+ Ship a game in more than one language without a second build, a second scene, or
9
+ a page reload.
10
+
11
+ **English is the base and the default.** Every other locale is a partial overlay:
12
+ anything it does not translate falls back to English, silently.
13
+
14
+ ## The rule that matters most
15
+
16
+ > When a translation is ambiguous, when the English term is the more precise one,
17
+ > or when the translation runs so much longer than the English that it breaks the
18
+ > layout — **ship the English.**
19
+
20
+ Leaving a key untranslated is a **decision**, not a gap to fill. `COMBO`,
21
+ `BOSS`, `LV.`, `HP`, `x1.5` are usually clearer in English to players in every
22
+ language, and a "Continue to the next chapter" that becomes three lines in a
23
+ button will break your menu. Omit the key and the English appears. Nothing warns,
24
+ nothing marks it, nothing logs.
25
+
26
+ The only mistake worth reporting is a key **nothing** declares — a typo, which
27
+ renders raw on screen. `bunx incanto-check` tells you at author time.
28
+
29
+ ## Declaring strings
30
+
31
+ One `strings` block in the scene header, locale first:
32
+
33
+ ```json
34
+ {
35
+ "format": 1, "type": "scene", "name": "Menu",
36
+ "strings": {
37
+ "en": {
38
+ "menu.start": "Start Game",
39
+ "menu.options": "Options",
40
+ "settings.language": "Language",
41
+ "hud.wave": "Wave {n}",
42
+ "hud.combo": "COMBO"
43
+ },
44
+ "ko": {
45
+ "menu.start": "게임 시작",
46
+ "menu.options": "설정",
47
+ "settings.language": "언어",
48
+ "hud.wave": "{n} 웨이브"
49
+ }
50
+ },
51
+ "root": { "...": "..." }
52
+ }
53
+ ```
54
+
55
+ `hud.combo` has no Korean entry on purpose — "COMBO" is the term players already
56
+ read. It shows as `COMBO` in both languages, and that is correct.
57
+
58
+ Tables **merge** across scenes: put your shared UI strings in the scene the game
59
+ boots from, and a level that adds three lines of its own keeps them.
60
+
61
+ ## Using them
62
+
63
+ A text prop becomes translatable by naming a key with `@t:` — the same shape as
64
+ the `"$assetKey"` references you already write:
65
+
66
+ ```json
67
+ { "name": "Start", "type": "UiButton", "props": { "text": "@t:menu.start" } }
68
+ { "name": "Wave", "type": "UiText", "props": { "text": "@t:hud.wave" } }
69
+ ```
70
+
71
+ Works on every text-bearing widget: `UiText.text`, `UiButton.text`,
72
+ `UiBar.label`, `UiSelect.label`, `UiBanner.show()`, and `UiDialogue` lines,
73
+ speakers and choices.
74
+
75
+ From a behavior, `engine.t(key, params)`:
76
+
77
+ ```ts
78
+ banner.show(this.engine.t('hud.wave', { n: this.wave }));
79
+ ```
80
+
81
+ `{n}` slots are filled from `params`; a slot with no matching param is left
82
+ alone rather than blanked.
83
+
84
+ ## The language picker
85
+
86
+ One node. Do not build your own.
87
+
88
+ ```json
89
+ { "name": "Language", "type": "UiLanguageSelect",
90
+ "props": { "label": "@t:settings.language" } }
91
+ ```
92
+
93
+ Its options are the locales your scene declares — a game shipping only English
94
+ shows only English — each labeled with its own endonym (`한국어`, not `Korean`),
95
+ because a player who cannot read the language on screen still has to find theirs.
96
+ Picking one switches the game **live** and persists the choice through
97
+ `engine.settings`, so the next visit opens in it.
98
+
99
+ ## What NOT to localize
100
+
101
+ - **Values, not labels.** `UiSelect.options` are values the game compares
102
+ against (`"low,medium,high"`); translating them breaks the comparison.
103
+ - **Node names, group names, asset keys, signal names, action names.** These are
104
+ identifiers.
105
+ - **Anything a behavior parses.** If code does `if (value === 'start')`, that
106
+ string is an identifier wearing a label's clothes.
107
+
108
+ ## Verifying
109
+
110
+ ```bash
111
+ bunx incanto-check src/scenes/menu.scene.json
112
+ ```
113
+
114
+ Reports any `@t:` key that no locale declares. It says nothing about a key one
115
+ locale omits — that is the design, not a warning.
116
+
117
+ To check a language actually renders, set it and read the frame:
118
+
119
+ ```ts
120
+ game.engine.locale.locale = 'ko';
121
+ ```
122
+
123
+ Switching takes effect on the **next frame** — nothing to reload, nothing to
124
+ invalidate. Widgets re-read their text every frame, which is what makes this
125
+ work.
126
+
127
+ ## Fonts
128
+
129
+ The engine's HUD uses the platform font stack. On a machine with no CJK face
130
+ installed, Korean renders as boxes — that is the operating system, not the
131
+ engine. If you ship Korean to an audience you do not control, add a webfont in
132
+ your own page CSS and set it on the HUD layer.
@@ -145,7 +145,7 @@ Signals: `finished`
145
145
  | `renderOrder` | `0` | number |
146
146
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
147
147
  | `snapToGround` | `null` | null |
148
- | `target` | `""` | string |
148
+ | `target` | `""` | node path |
149
149
  | `bone` | `""` | string |
150
150
 
151
151
  ## `BoneLookAt3D` — `incanto/3d`
@@ -160,9 +160,9 @@ Signals: `finished`
160
160
  | `renderOrder` | `0` | number |
161
161
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
162
162
  | `snapToGround` | `null` | null |
163
- | `target` | `""` | string |
163
+ | `target` | `""` | node path |
164
164
  | `bone` | `"Head"` | string |
165
- | `lookAt` | `""` | string |
165
+ | `lookAt` | `""` | node path |
166
166
  | `maxAngleDeg` | `75` | number |
167
167
  | `weight` | `0.85` | number |
168
168
  | `forwardAxis` | `"+z"` | one of: `+x` `-x` `+y` `-y` `+z` `-z` |
@@ -178,7 +178,7 @@ Signals: `finished`
178
178
  | `renderOrder` | `0` | number |
179
179
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
180
180
  | `visible` | `true` | boolean |
181
- | `follow` | `""` | string |
181
+ | `follow` | `""` | node path |
182
182
  | `smoothing` | `0` | number |
183
183
  | `zoom` | `1` | number |
184
184
  | `limits` | `[]` | array |
@@ -289,7 +289,7 @@ Signals: `movementStateChanged(state)`
289
289
  | `moveAction` | `"move"` | string |
290
290
  | `jumpAction` | `"jump"` | string |
291
291
  | `sprintAction` | `"sprint"` | string |
292
- | `skinPath` | `"../Skin"` | string |
292
+ | `skinPath` | `"../Skin"` | node path |
293
293
  | `skinYawOffset` | `0` | number |
294
294
  | `animations` | `{}` | object |
295
295
 
@@ -352,7 +352,7 @@ Signals: `movementStateChanged(state)`
352
352
  | `sway` | `0.5` | number |
353
353
  | `drape` | `null` | null |
354
354
  | `avoidWater` | `true` | boolean |
355
- | `terrain` | `""` | string |
355
+ | `terrain` | `""` | node path |
356
356
 
357
357
  ## `Foliage3D` — `incanto/3d`
358
358
 
@@ -386,7 +386,7 @@ Signals: `movementStateChanged(state)`
386
386
  | `seed` | `1` | number |
387
387
  | `drape` | `null` | null |
388
388
  | `avoidWater` | `true` | boolean |
389
- | `terrain` | `""` | string |
389
+ | `terrain` | `""` | node path |
390
390
 
391
391
  ## `HudLayer` — `incanto`
392
392
 
@@ -394,6 +394,7 @@ Signals: `movementStateChanged(state)`
394
394
  |---|---|---|
395
395
  | `zIndex` | `100` | number |
396
396
  | `visible` | `true` | boolean |
397
+ | `focusNavigation` | `false` | boolean |
397
398
 
398
399
  ## `InstancedMesh3D` — `incanto/3d`
399
400
 
@@ -427,7 +428,7 @@ Signals: `movementStateChanged(state)`
427
428
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
428
429
  | `visible` | `true` | boolean |
429
430
  | `type` | `"revolute"` | one of: `fixed` `revolute` `rope` `spring` |
430
- | `target` | `""` | string |
431
+ | `target` | `""` | node path |
431
432
  | `anchor` | `[0,0]` | array |
432
433
  | `targetAnchor` | `[0,0]` | array |
433
434
  | `length` | `0` | number |
@@ -447,7 +448,7 @@ Signals: `movementStateChanged(state)`
447
448
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
448
449
  | `snapToGround` | `null` | null |
449
450
  | `type` | `"spherical"` | one of: `fixed` `spherical` `rope` `spring` |
450
- | `target` | `""` | string |
451
+ | `target` | `""` | node path |
451
452
  | `anchor` | `[0,0,0]` | array |
452
453
  | `targetAnchor` | `[0,0,0]` | array |
453
454
  | `length` | `0` | number |
@@ -730,7 +731,7 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
730
731
  | `sunDirection` | `[0.5,0.8,0.3]` | array |
731
732
  | `sunColor` | `"#fff6e0"` | string |
732
733
  | `sunIntensity` | `1` | number |
733
- | `terrain` | `""` | string |
734
+ | `terrain` | `""` | node path |
734
735
  | `carve` | `true` | boolean |
735
736
  | `flowForce` | `1` | number |
736
737
  | `spray` | `1` | number |
@@ -909,7 +910,7 @@ Signals: `timeout`
909
910
  | `leafFadeEnd` | `0` | number |
910
911
  | `leafShadows` | `true` | boolean |
911
912
  | `drape` | `null` | null |
912
- | `terrain` | `""` | string |
913
+ | `terrain` | `""` | node path |
913
914
 
914
915
  ## `UILayer` — `incanto/2d`
915
916
 
@@ -923,10 +924,13 @@ Signals: `timeout`
923
924
  |---|---|---|
924
925
  | `anchor` | `"center"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
925
926
  | `visible` | `true` | boolean |
927
+ | `focusable` | `false` | boolean |
928
+ | `draggable` | `false` | boolean |
929
+ | `dropTarget` | `false` | boolean |
926
930
  | `size` | `42` | number |
927
931
  | `seconds` | `2` | number |
928
932
 
929
- Signals: `bannerShown`
933
+ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `bannerShown`
930
934
 
931
935
  ## `UiBar` — `incanto`
932
936
 
@@ -934,6 +938,9 @@ Signals: `bannerShown`
934
938
  |---|---|---|
935
939
  | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
936
940
  | `visible` | `true` | boolean |
941
+ | `focusable` | `false` | boolean |
942
+ | `draggable` | `false` | boolean |
943
+ | `dropTarget` | `false` | boolean |
937
944
  | `value` | `100` | number |
938
945
  | `max` | `100` | number |
939
946
  | `width` | `180` | number |
@@ -944,19 +951,24 @@ Signals: `bannerShown`
944
951
  | `background` | `"rgba(0,0,0,0.5)"` | string |
945
952
  | `label` | `""` | string |
946
953
 
954
+ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped`
955
+
947
956
  ## `UiButton` — `incanto`
948
957
 
949
958
  | Prop | Default | Kind |
950
959
  |---|---|---|
951
960
  | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
952
961
  | `visible` | `true` | boolean |
962
+ | `focusable` | `true` | boolean |
963
+ | `draggable` | `false` | boolean |
964
+ | `dropTarget` | `false` | boolean |
953
965
  | `text` | `"OK"` | string |
954
966
  | `size` | `16` | number |
955
967
  | `color` | `"#ffffff"` | string |
956
968
  | `background` | `"rgba(255,255,255,0.14)"` | string |
957
969
  | `disabled` | `false` | boolean |
958
970
 
959
- Signals: `pressed`
971
+ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `pressed`
960
972
 
961
973
  ## `UiDialogue` — `incanto`
962
974
 
@@ -964,10 +976,13 @@ Signals: `pressed`
964
976
  |---|---|---|
965
977
  | `anchor` | `"bottom"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
966
978
  | `visible` | `true` | boolean |
979
+ | `focusable` | `false` | boolean |
980
+ | `draggable` | `false` | boolean |
981
+ | `dropTarget` | `false` | boolean |
967
982
  | `charsPerSecond` | `40` | number |
968
983
  | `width` | `520` | number |
969
984
 
970
- Signals: `lineShown` · `choiceMade` · `dialogueFinished`
985
+ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `lineShown` · `choiceMade` · `dialogueFinished`
971
986
 
972
987
  ## `UiImage` — `incanto`
973
988
 
@@ -975,6 +990,9 @@ Signals: `lineShown` · `choiceMade` · `dialogueFinished`
975
990
  |---|---|---|
976
991
  | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
977
992
  | `visible` | `true` | boolean |
993
+ | `focusable` | `false` | boolean |
994
+ | `draggable` | `false` | boolean |
995
+ | `dropTarget` | `false` | boolean |
978
996
  | `src` | `""` | string |
979
997
  | `width` | `48` | number |
980
998
  | `height` | `48` | number |
@@ -982,12 +1000,32 @@ Signals: `lineShown` · `choiceMade` · `dialogueFinished`
982
1000
  | `tint` | `""` | string |
983
1001
  | `opacity` | `1` | number |
984
1002
 
1003
+ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped`
1004
+
1005
+ ## `UiLanguageSelect` — `incanto`
1006
+
1007
+ | Prop | Default | Kind |
1008
+ |---|---|---|
1009
+ | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
1010
+ | `visible` | `true` | boolean |
1011
+ | `focusable` | `true` | boolean |
1012
+ | `draggable` | `false` | boolean |
1013
+ | `dropTarget` | `false` | boolean |
1014
+ | `label` | `"Language"` | string |
1015
+ | `options` | `""` | string |
1016
+ | `value` | `""` | string |
1017
+
1018
+ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
1019
+
985
1020
  ## `UiPanel` — `incanto`
986
1021
 
987
1022
  | Prop | Default | Kind |
988
1023
  |---|---|---|
989
1024
  | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
990
1025
  | `visible` | `true` | boolean |
1026
+ | `focusable` | `false` | boolean |
1027
+ | `draggable` | `false` | boolean |
1028
+ | `dropTarget` | `false` | boolean |
991
1029
  | `layout` | `"column"` | one of: `column` `row` `grid` |
992
1030
  | `columns` | `4` | number |
993
1031
  | `gap` | `8` | number |
@@ -998,17 +1036,22 @@ Signals: `lineShown` · `choiceMade` · `dialogueFinished`
998
1036
  | `height` | `0` | number |
999
1037
  | `border` | `""` | string |
1000
1038
 
1039
+ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped`
1040
+
1001
1041
  ## `UiSelect` — `incanto`
1002
1042
 
1003
1043
  | Prop | Default | Kind |
1004
1044
  |---|---|---|
1005
1045
  | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
1006
1046
  | `visible` | `true` | boolean |
1047
+ | `focusable` | `true` | boolean |
1048
+ | `draggable` | `false` | boolean |
1049
+ | `dropTarget` | `false` | boolean |
1007
1050
  | `label` | `""` | string |
1008
1051
  | `options` | `""` | string |
1009
1052
  | `value` | `""` | string |
1010
1053
 
1011
- Signals: `changed`
1054
+ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
1012
1055
 
1013
1056
  ## `UiSlider` — `incanto`
1014
1057
 
@@ -1016,6 +1059,9 @@ Signals: `changed`
1016
1059
  |---|---|---|
1017
1060
  | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
1018
1061
  | `visible` | `true` | boolean |
1062
+ | `focusable` | `true` | boolean |
1063
+ | `draggable` | `false` | boolean |
1064
+ | `dropTarget` | `false` | boolean |
1019
1065
  | `label` | `""` | string |
1020
1066
  | `value` | `0.5` | number |
1021
1067
  | `min` | `0` | number |
@@ -1024,7 +1070,7 @@ Signals: `changed`
1024
1070
  | `width` | `180` | number |
1025
1071
  | `color` | `"#6ee7dc"` | string |
1026
1072
 
1027
- Signals: `changed`
1073
+ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
1028
1074
 
1029
1075
  ## `UiText` — `incanto`
1030
1076
 
@@ -1032,21 +1078,29 @@ Signals: `changed`
1032
1078
  |---|---|---|
1033
1079
  | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
1034
1080
  | `visible` | `true` | boolean |
1081
+ | `focusable` | `false` | boolean |
1082
+ | `draggable` | `false` | boolean |
1083
+ | `dropTarget` | `false` | boolean |
1035
1084
  | `text` | `""` | string |
1036
1085
  | `size` | `16` | number |
1037
1086
  | `color` | `"#ffffff"` | string |
1038
1087
  | `shadow` | `true` | boolean |
1039
1088
 
1089
+ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped`
1090
+
1040
1091
  ## `UiToggle` — `incanto`
1041
1092
 
1042
1093
  | Prop | Default | Kind |
1043
1094
  |---|---|---|
1044
1095
  | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
1045
1096
  | `visible` | `true` | boolean |
1097
+ | `focusable` | `true` | boolean |
1098
+ | `draggable` | `false` | boolean |
1099
+ | `dropTarget` | `false` | boolean |
1046
1100
  | `label` | `""` | string |
1047
1101
  | `value` | `false` | boolean |
1048
1102
 
1049
- Signals: `changed`
1103
+ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
1050
1104
 
1051
1105
  ## `VoxelGrid3D` — `incanto/3d`
1052
1106
 
@@ -1134,7 +1188,7 @@ wire their signals through scene `connections`. Full guide: `incanto-gameplay-be
1134
1188
 
1135
1189
  | Prop | Default | Kind |
1136
1190
  |---|---|---|
1137
- | `water` | `""` | string |
1191
+ | `water` | `""` | node path |
1138
1192
  | `draft` | `0.35` | number |
1139
1193
  | `stiffness` | `3` | number |
1140
1194
  | `damping` | `3.5` | number |
@@ -1155,7 +1209,7 @@ Signals: `submerged` · `surfaced`
1155
1209
 
1156
1210
  | Prop | Default | Kind |
1157
1211
  |---|---|---|
1158
- | `target` | `""` | string |
1212
+ | `target` | `""` | node path |
1159
1213
  | `speed` | `60` | number |
1160
1214
  | `stopRange` | `0` | number |
1161
1215
  | `loseRange` | `0` | number |
@@ -1209,7 +1263,7 @@ Signals: `dayPhaseChanged`
1209
1263
 
1210
1264
  | Prop | Default | Kind |
1211
1265
  |---|---|---|
1212
- | `target` | `""` | string |
1266
+ | `target` | `""` | node path |
1213
1267
  | `offset` | `[]` | array |
1214
1268
  | `smoothing` | `0` | number |
1215
1269
  | `deadzone` | `0` | number |
@@ -1220,7 +1274,7 @@ Signals: `dayPhaseChanged`
1220
1274
  |---|---|---|
1221
1275
  | `restartAction` | `"restart"` | string |
1222
1276
  | `freezeOnEnd` | `true` | boolean |
1223
- | `bannerPath` | `"%Banner"` | string |
1277
+ | `bannerPath` | `"%Banner"` | node path |
1224
1278
 
1225
1279
  Signals: `flowChanged`
1226
1280
 
@@ -1327,7 +1381,7 @@ Signals: `scoreChanged(score)` · `won` · `lost` · `lifeLost(lives)`
1327
1381
 
1328
1382
  | Prop | Default | Kind |
1329
1383
  |---|---|---|
1330
- | `prefab` | `""` | string |
1384
+ | `prefab` | `""` | node path |
1331
1385
  | `interval` | `1` | number |
1332
1386
  | `max` | `0` | number |
1333
1387
  | `at` | `[]` | array |
@@ -1357,14 +1411,14 @@ Signals: `waveStarted(index)` · `waveCleared(index)` · `allCleared`
1357
1411
 
1358
1412
  | Prop | Default | Kind |
1359
1413
  |---|---|---|
1360
- | `aggroTarget` | `""` | string |
1414
+ | `aggroTarget` | `""` | node path |
1361
1415
  | `aggroRange` | `12` | number |
1362
1416
  | `deAggroRange` | `0` | number |
1363
1417
  | `chaseSpeed` | `4` | number |
1364
1418
  | `wanderSpeed` | `1.4` | number |
1365
1419
  | `wanderRadius` | `8` | number |
1366
1420
  | `wanderChangeEvery` | `3` | number |
1367
- | `goalTarget` | `""` | string |
1421
+ | `goalTarget` | `""` | node path |
1368
1422
  | `stopRange` | `1.2` | number |
1369
1423
  | `moveParent` | `false` | boolean |
1370
1424
 
@@ -0,0 +1,152 @@
1
+ ---
2
+ name: incanto-save-slots
3
+ description: Continue where you left off — Behavior serialize()/deserialize(), engine.captureState()/restoreState(), and a SaveSlots layer over the save store. A save carries behavior state keyed by node uid and reloads the scene from source; it does not snapshot the tree. Use for any game with progress worth keeping.
4
+ ---
5
+
6
+ # Save slots
7
+
8
+ Let a player close the tab and come back to their run.
9
+
10
+ ## What a save IS
11
+
12
+ **Which scene, plus every behavior's state, keyed by node uid.**
13
+
14
+ ```json
15
+ {
16
+ "scene": "village",
17
+ "state": { "n_player7x": { "current": 62 }, "n_score2k": { "score": 1400 } },
18
+ "label": "Chapter 2 · Emberwood",
19
+ "savedAt": 1754160000000,
20
+ "playtime": 812,
21
+ "data": { "difficulty": "hard" }
22
+ }
23
+ ```
24
+
25
+ Loading = **load the scene from its file**, then hand each behavior its state
26
+ back by uid.
27
+
28
+ ## What a save is NOT, and why
29
+
30
+ It is not a snapshot of the live tree. That is deliberate, and it is about this
31
+ engine specifically:
32
+
33
+ - `Spawner.onReady` **detaches** its prefab template from the tree. A captured
34
+ tree and a freshly booted one legitimately disagree about which nodes exist,
35
+ on every scene with a spawner.
36
+ - A spawned enemy that carries its own spawner has already lost *its* template,
37
+ so re-adding that subtree runs `onReady` against a node that is gone and
38
+ throws — killing the whole load.
39
+ - Prop deltas cannot be applied without resetting every other prop to default.
40
+
41
+ Reloading from source sidesteps all three. The structure comes from the file
42
+ (authoritative, already validated); the save carries only what the file cannot
43
+ know.
44
+
45
+ **The cost, stated plainly: spawned enemies and mid-level positions are not
46
+ restored.** You resume at the scene's start with stats, inventory, unlocks and
47
+ quest flags intact — a checkpoint save. If your game needs a position, save it:
48
+ `serialize()` returns anything.
49
+
50
+ ## Making a behavior saveable
51
+
52
+ Two optional hooks, exactly like the other five:
53
+
54
+ ```ts
55
+ class QuestLog extends Behavior {
56
+ accepted = false;
57
+ wolvesKilled = 0;
58
+
59
+ override serialize() {
60
+ return { accepted: this.accepted, wolvesKilled: this.wolvesKilled };
61
+ }
62
+
63
+ override deserialize(data: JsonValue) {
64
+ const d = data as { accepted?: unknown; wolvesKilled?: unknown };
65
+ if (typeof d.accepted === 'boolean') this.accepted = d.accepted;
66
+ if (typeof d.wolvesKilled === 'number') this.wolvesKilled = d.wolvesKilled;
67
+ }
68
+ }
69
+ ```
70
+
71
+ **Save only what a fresh `onReady` could not recreate.** `current` health yes;
72
+ `maxHealth` no — that comes back from the scene JSON, and duplicating it makes
73
+ old saves fight your balance patches.
74
+
75
+ `deserialize` is defensive on purpose: that data may come from a build of your
76
+ game that shipped six weeks ago. Check what you read.
77
+
78
+ Built-ins that already save: `Health` (current, dead), `ScoreKeeper` (score,
79
+ lives, won/lost), `Collector` (total).
80
+
81
+ ## Every node you save needs a uid
82
+
83
+ The uid is the join key, because it is the one identifier that survives a rename
84
+ or a reparent. The editor assigns one to every node it touches. A hand-written
85
+ scene may not have them — `engine.captureState()` logs any node that has state to
86
+ save and no uid to key it under.
87
+
88
+ Never hand-craft a uid. Use `newUid()`.
89
+
90
+ ## Saving and loading
91
+
92
+ ```ts
93
+ import { SaveSlots } from 'incanto';
94
+
95
+ const slots = new SaveSlots('emberwood');
96
+
97
+ // save
98
+ slots.write('1', {
99
+ scene: currentSceneKey,
100
+ state: game.engine.captureState(),
101
+ label: 'Chapter 2',
102
+ savedAt: Date.now(),
103
+ playtime: elapsed,
104
+ });
105
+
106
+ // load
107
+ const slot = slots.read('1');
108
+ if (slot) {
109
+ await loadSceneByKey(slot.scene); // your routing — the engine does not route
110
+ const report = game.engine.restoreState(slot.state);
111
+ if (report.missing.length) console.warn('save is older than this build:', report.missing);
112
+ }
113
+ ```
114
+
115
+ `restoreState` runs AFTER the scene has loaded and `onReady` has fired —
116
+ `onReady` is where a behavior sets its starting values, so restoring first would
117
+ be overwritten.
118
+
119
+ It never throws. A save naming a uid this build deleted reports it in
120
+ `report.missing` and restores everything else; refusing to load would mean a
121
+ patch that moves one node deletes everyone's progress.
122
+
123
+ ## A load menu
124
+
125
+ ```ts
126
+ for (const slot of slots.all()) { // newest first
127
+ render(slot.id, slot.label, new Date(slot.savedAt), slot.playtime);
128
+ }
129
+ slots.remove('2');
130
+ slots.clear(); // "delete all data"
131
+ ```
132
+
133
+ ## Checking your coverage
134
+
135
+ ```ts
136
+ import { behaviorsWithoutSave } from 'incanto';
137
+ console.log(behaviorsWithoutSave(game.engine.scene.root));
138
+ ```
139
+
140
+ Names every behavior in the tree that has props and no `serialize`. Not all of
141
+ them are wrong — a behavior that derives everything from time has nothing to
142
+ save — but it is the list to read before shipping.
143
+
144
+ ## Autosave
145
+
146
+ There is no autosave node, on purpose: *when* to save is a design decision
147
+ (checkpoint, level end, every 60s, on quit) and only your game knows. Wire it to
148
+ whatever signal marks the moment:
149
+
150
+ ```json
151
+ { "from": "Level/Exit", "signal": "triggerEnter", "to": "Game", "handler": "onCheckpoint" }
152
+ ```
@@ -14,7 +14,7 @@
14
14
  "@dimforge/rapier2d-compat": "0.19.3",
15
15
  "@dimforge/rapier3d-compat": "0.19.3",
16
16
  "@pixiv/three-vrm": "^3.5.3",
17
- "incanto": "^0.33.0",
17
+ "incanto": "^0.35.0",
18
18
  "three": "^0.184.0"
19
19
  },
20
20
  "devDependencies": {
@@ -13,7 +13,7 @@
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
14
  "@dimforge/rapier3d-compat": "0.19.3",
15
15
  "@pixiv/three-vrm": "^3.5.3",
16
- "incanto": "^0.33.0",
16
+ "incanto": "^0.35.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {
@@ -37,3 +37,19 @@ again button.
37
37
  - Custom code is three behaviors: VillageDirector, GroveDirector, SwordStrike.
38
38
  Movement, camera, animation, NPC proximity, patrol walking, dialogue, HUD —
39
39
  all engine built-ins wired in scene JSON.
40
+
41
+
42
+ ## Localization (added with the 0.35 localization feature)
43
+
44
+ The village HUD ships **English and Korean**, declared in one `strings` block in
45
+ `village.scene.json` and picked with a `UiLanguageSelect` in the top-right
46
+ corner. Switching is live — no reload, no second scene.
47
+
48
+ One string is deliberately **not** translated: `ui.hint`
49
+ (`WASD move · Shift sprint · E talk · click/F strike`). WASD, Shift, E and F are
50
+ the letters printed on the keyboard, and a Korean gloss of them is both longer
51
+ and less clear. It falls back to English in the Korean build, silently, which is
52
+ the documented and intended behaviour — see `incanto-localization.md`.
53
+
54
+ That is the example doing double duty: it shows the feature working AND shows
55
+ the judgement call the feature exists to support.
@@ -13,7 +13,7 @@
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
14
  "@dimforge/rapier3d-compat": "0.19.3",
15
15
  "@pixiv/three-vrm": "^3.5.3",
16
- "incanto": "^0.33.0",
16
+ "incanto": "^0.35.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {