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.
- 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-CtPScDMo.d.ts → behavior-CGSCWOHB.d.ts} +310 -22
- package/dist/{create-game-CFNqoqOX.js → create-game-B0Wfhi2-.js} +6 -5
- package/dist/{create-game-QTfghzwM.js → create-game-BUD89Kqh.js} +20 -6
- package/dist/debug.d.ts +1 -1
- package/dist/{duplicate-DlOvknDO.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-B6WiUsaO.js → environment-presets-TAGvmM3z.js} +240 -74
- package/dist/{errors-1dXlIwoR.d.ts → errors-BY2kL0hv.d.ts} +1 -1
- package/dist/{gameplay-BxFtmfSe.js → gameplay-BvhcQbfJ.js} +75 -11
- package/dist/gameplay.d.ts +12 -5
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +22 -7
- package/dist/index.js +6 -6
- package/dist/{loader-DmXfj6Uv.d.ts → loader-CUcj00M8.d.ts} +19 -2
- package/dist/{loader-BwR0DxeM.js → loader-Mig5fY4n.js} +97 -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-DRCe89SI.d.ts → pathfinding-DgOo2KNF.d.ts} +1 -1
- package/dist/{physics-2d-BuyAwsW5.js → physics-2d-DqAclql-.js} +22 -9
- package/dist/{physics-3d-C8Kn_Nyb.js → physics-3d-DxBH4sIF.js} +27 -11
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/{register-G3edsjJ2.js → register-BFLg0-_i.js} +490 -12
- package/dist/{register-OodmuUMK.js → register-BjbPMA5B.js} +2 -2
- package/dist/{register-BLPC10Mj.js → register-DSmIRAf7.js} +11 -5
- package/dist/{schema-CcoWb32N.d.ts → schema-3ywbdlrv.d.ts} +10 -0
- package/dist/{test-C4B5znap.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-BTODHtdM.js → agent8-D3_GWeuh.js} +1 -1
- package/editor/assets/{debug-CJEz4EwC.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 +109 -0
- 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 +16 -0
- package/skills/incanto-localization.md +132 -0
- package/skills/incanto-node-reference.md +33 -18
- 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-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
|
+
```
|
|
@@ -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.
|
|
@@ -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": "
|
|
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": "
|
|
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": "
|
|
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
|
}
|