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.
- package/dist/2d.d.ts +5 -5
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +122 -14
- package/dist/3d.js +5 -5
- package/dist/{audit-C4kmDK0o.js → audit-C5oZGAru.js} +37 -0
- package/dist/{behavior-CKwTCjfR.d.ts → behavior-CGSCWOHB.d.ts} +319 -22
- package/dist/{create-game-D2QU5x7S.js → create-game-B0Wfhi2-.js} +6 -5
- package/dist/{create-game-sFuTLqjD.js → create-game-BUD89Kqh.js} +20 -6
- package/dist/debug.d.ts +1 -1
- package/dist/{duplicate-BgnG1Lqz.js → duplicate-C716f-97.js} +1 -1
- package/dist/{editor-switch-BJb-CWfA.d.ts → editor-switch-DyXEtH36.d.ts} +1 -1
- package/dist/editor.js +1739 -1312
- package/dist/env.d.ts +1 -1
- package/dist/{environment-presets-CQtEGogB.js → environment-presets-TAGvmM3z.js} +240 -74
- package/dist/{errors-1dXlIwoR.d.ts → errors-BY2kL0hv.d.ts} +1 -1
- package/dist/{gameplay-BpQCbABv.js → gameplay-BvhcQbfJ.js} +75 -11
- package/dist/gameplay.d.ts +12 -5
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +72 -7
- package/dist/index.js +6 -6
- package/dist/{loader-COn5fS0o.d.ts → loader-CUcj00M8.d.ts} +19 -2
- package/dist/{loader-Buk8Bu1h.js → loader-Mig5fY4n.js} +110 -18
- package/dist/net.d.ts +2 -2
- package/dist/net.js +3 -3
- package/dist/{particle-sim-CwJ5rI_P.d.ts → particle-sim-B-vZBF5R.d.ts} +1 -1
- package/dist/{pathfinding-DUw9mir9.d.ts → pathfinding-DgOo2KNF.d.ts} +1 -1
- package/dist/{physics-2d-DjXR5DMu.js → physics-2d-DqAclql-.js} +22 -9
- package/dist/{physics-3d-DF8npb1O.js → physics-3d-DxBH4sIF.js} +27 -11
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/{register-CscIzJEO.js → register-BFLg0-_i.js} +697 -21
- package/dist/{register-CtI-itec.js → register-BjbPMA5B.js} +2 -2
- package/dist/{register-FIJtNbub.js → register-DSmIRAf7.js} +11 -5
- package/dist/{schema-CcoWb32N.d.ts → schema-3ywbdlrv.d.ts} +10 -0
- package/dist/{test-BRxLd2jH.js → test-9EokzbRd.js} +10 -10
- package/dist/test.d.ts +4 -4
- package/dist/test.js +2 -2
- package/dist/vite.js +1 -1
- package/editor/assets/{agent8-BdDP3xKW.js → agent8-D3_GWeuh.js} +1 -1
- package/editor/assets/{debug-DbjTyTlC.js → debug-CX0LDd1r.js} +1 -1
- package/editor/assets/index-CMlKFT0C.js +10773 -0
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +251 -6
- package/skills/incanto-assets.md +21 -0
- package/skills/incanto-behaviors-and-scripts.md +16 -0
- package/skills/incanto-editor.md +17 -1
- package/skills/incanto-gameplay-behaviors.md +6 -0
- package/skills/incanto-hud.md +73 -1
- package/skills/incanto-localization.md +132 -0
- package/skills/incanto-node-reference.md +78 -24
- package/skills/incanto-save-slots.md +152 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/PROJECT/Context.md +16 -0
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/src/village.scene.json +25 -3
- 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` | `""` |
|
|
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` | `""` |
|
|
163
|
+
| `target` | `""` | node path |
|
|
164
164
|
| `bone` | `"Head"` | string |
|
|
165
|
-
| `lookAt` | `""` |
|
|
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` | `""` |
|
|
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"` |
|
|
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` | `""` |
|
|
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` | `""` |
|
|
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` | `""` |
|
|
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` | `""` |
|
|
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` | `""` |
|
|
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` | `""` |
|
|
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` | `""` |
|
|
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` | `""` |
|
|
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` | `""` |
|
|
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"` |
|
|
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` | `""` |
|
|
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` | `""` |
|
|
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` | `""` |
|
|
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
|
+
```
|
|
@@ -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.
|