incanto 0.34.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-CtPScDMo.d.ts → behavior-CGSCWOHB.d.ts} +310 -22
  7. package/dist/{create-game-CFNqoqOX.js → create-game-B0Wfhi2-.js} +6 -5
  8. package/dist/{create-game-QTfghzwM.js → create-game-BUD89Kqh.js} +20 -6
  9. package/dist/debug.d.ts +1 -1
  10. package/dist/{duplicate-DlOvknDO.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-B6WiUsaO.js → environment-presets-TAGvmM3z.js} +240 -74
  15. package/dist/{errors-1dXlIwoR.d.ts → errors-BY2kL0hv.d.ts} +1 -1
  16. package/dist/{gameplay-BxFtmfSe.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 +22 -7
  20. package/dist/index.js +6 -6
  21. package/dist/{loader-DmXfj6Uv.d.ts → loader-CUcj00M8.d.ts} +19 -2
  22. package/dist/{loader-BwR0DxeM.js → loader-Mig5fY4n.js} +97 -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-DRCe89SI.d.ts → pathfinding-DgOo2KNF.d.ts} +1 -1
  27. package/dist/{physics-2d-BuyAwsW5.js → physics-2d-DqAclql-.js} +22 -9
  28. package/dist/{physics-3d-C8Kn_Nyb.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-G3edsjJ2.js → register-BFLg0-_i.js} +490 -12
  32. package/dist/{register-OodmuUMK.js → register-BjbPMA5B.js} +2 -2
  33. package/dist/{register-BLPC10Mj.js → register-DSmIRAf7.js} +11 -5
  34. package/dist/{schema-CcoWb32N.d.ts → schema-3ywbdlrv.d.ts} +10 -0
  35. package/dist/{test-C4B5znap.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-BTODHtdM.js → agent8-D3_GWeuh.js} +1 -1
  40. package/editor/assets/{debug-CJEz4EwC.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 +109 -0
  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 +16 -0
  50. package/skills/incanto-localization.md +132 -0
  51. package/skills/incanto-node-reference.md +33 -18
  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-BYCso8Bf.js +0 -10696
@@ -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.34.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.34.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.34.0",
16
+ "incanto": "^0.35.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {
@@ -3,6 +3,19 @@
3
3
  "type": "scene",
4
4
  "dimension": "3d",
5
5
  "name": "EmberwoodVillage",
6
+ "strings": {
7
+ "en": {
8
+ "quest.talkElder": "Talk to the Elder [E]",
9
+ "ui.playAgain": "Play again",
10
+ "ui.hint": "WASD move · Shift sprint · E talk · click/F strike",
11
+ "settings.language": "Language"
12
+ },
13
+ "ko": {
14
+ "quest.talkElder": "촌장에게 말을 걸어보세요 [E]",
15
+ "ui.playAgain": "다시 하기",
16
+ "settings.language": "언어"
17
+ }
18
+ },
6
19
  "environment": {
7
20
  "sky": {
8
21
  "type": "atmosphere",
@@ -3435,7 +3448,7 @@
3435
3448
  "type": "UiText",
3436
3449
  "uid": "n_vq_quest",
3437
3450
  "props": {
3438
- "text": "Talk to the Elder [E]",
3451
+ "text": "@t:quest.talkElder",
3439
3452
  "size": 16,
3440
3453
  "anchor": "topLeft",
3441
3454
  "shadow": true
@@ -3463,7 +3476,7 @@
3463
3476
  "type": "UiButton",
3464
3477
  "uid": "n_vq_restart",
3465
3478
  "props": {
3466
- "text": "Play again",
3479
+ "text": "@t:ui.playAgain",
3467
3480
  "anchor": "bottom",
3468
3481
  "visible": false
3469
3482
  }
@@ -3473,12 +3486,21 @@
3473
3486
  "type": "UiText",
3474
3487
  "uid": "n_vq_hint",
3475
3488
  "props": {
3476
- "text": "WASD move · Shift sprint · E talk · click/F strike",
3489
+ "text": "@t:ui.hint",
3477
3490
  "size": 12,
3478
3491
  "color": "#cccccc",
3479
3492
  "anchor": "bottomRight",
3480
3493
  "shadow": true
3481
3494
  }
3495
+ },
3496
+ {
3497
+ "name": "Language",
3498
+ "type": "UiLanguageSelect",
3499
+ "uid": "n_4m6tsfl0og3ekii3",
3500
+ "props": {
3501
+ "label": "@t:settings.language",
3502
+ "anchor": "topRight"
3503
+ }
3482
3504
  }
3483
3505
  ]
3484
3506
  }