incanto 0.50.0 → 0.52.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 +38 -2
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +4 -3
- package/dist/3d.js +4 -4
- package/dist/{behavior-62q0HWBO.d.ts → behavior-uPEuZrUB.d.ts} +47 -3
- package/dist/{create-game-BLDjy_PW.js → create-game-B_e9hJf7.js} +92 -10
- package/dist/{create-game-DqqxEax1.js → create-game-CGnoypjL.js} +6 -6
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +1 -1
- package/dist/{duplicate-CI9WF_bg.js → duplicate-B-OtSRFL.js} +1 -1
- package/dist/editor.js +1917 -1579
- package/dist/{environment-presets-XFuqu5jv.js → environment-presets-DSZwsKPs.js} +3 -3
- package/dist/{gameplay-DbaI313d.js → gameplay-B6jqvYeM.js} +163 -6
- package/dist/gameplay.d.ts +79 -2
- package/dist/gameplay.js +2 -2
- package/dist/index.d.ts +74 -5
- package/dist/index.js +7 -7
- package/dist/{loader-DwazzlQb.js → loader-B-Gft32x.js} +372 -23
- package/dist/{loader-CeyU_bm1.d.ts → loader-B9iTqs27.d.ts} +1 -1
- package/dist/net.d.ts +15 -410
- package/dist/net.js +1 -822
- package/dist/{pathfinding-C49JSNNq.d.ts → pathfinding-CAR9DjQQ.d.ts} +1 -1
- package/dist/{physics-2d-_9VBOHn6.js → physics-2d-Dns-oZlE.js} +7 -2
- package/dist/{physics-3d-DrpF5hcG.js → physics-3d-BhI0ehpe.js} +8 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-uvaZj1KX.js → register-DVwlnZAZ.js} +20 -266
- package/dist/{register-BNPZYJmd.js → register-Trx7WHnD.js} +3 -3
- package/dist/{registry-IyWCGe4q.js → registry-C7u42TID.js} +23 -1
- package/dist/{replay-DYdy1wb0.d.ts → replay-BU1CCM15.d.ts} +1 -1
- package/dist/{replay-j-m6lJ4W.js → replay-BicPOMX0.js} +214 -2
- package/dist/split-screen-CZ9ccBBQ.js +1267 -0
- package/dist/split-screen-paxkQs_q.d.ts +441 -0
- package/dist/{src-B3HrKuAi.js → src-5gbZO47I.js} +1 -1
- package/dist/{teardown-BKTCzLek.js → teardown-ks3d5W9n.js} +2 -1
- package/dist/{test-D16igj4C.js → test-DAojuFdb.js} +119 -22
- package/dist/test.d.ts +60 -5
- package/dist/test.js +3 -3
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-t3kl5q9K.js → agent8-CCvckvbw.js} +1 -1
- package/editor/assets/{debug-Bu3eeAlO.js → debug-CLNCOnbc.js} +1 -1
- package/editor/assets/{index-Df5g8ofT.js → index-Cb5Brupb.js} +99 -92
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/skills/incanto-assets.md +4 -1
- package/skills/incanto-building-2d-games.md +6 -1
- package/skills/incanto-editor.md +48 -11
- package/skills/incanto-gameplay-behaviors.md +14 -5
- package/skills/incanto-multiplayer.md +39 -3
- package/skills/incanto-node-reference.md +13 -0
- package/skills/incanto-save-slots.md +134 -12
- package/skills/incanto-verifying-your-game.md +56 -0
- package/templates/agent8-server.ts +79 -2
- 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/package.json +1 -1
- package/dist/register-CB11yp21.js +0 -374
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
2
2
|
import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
|
|
3
|
-
import { c as mergeStaticSignals, i as getNodeSchema, r as createNode, s as mergeStaticProps, t as applySchemaProps } from "./registry-
|
|
3
|
+
import { c as mergeStaticSignals, i as getNodeSchema, r as createNode, s as mergeStaticProps, t as applySchemaProps } from "./registry-C7u42TID.js";
|
|
4
4
|
//#region src/core/signal.ts
|
|
5
5
|
var Signal = class {
|
|
6
6
|
connections = [];
|
|
@@ -135,6 +135,36 @@ function createBehavior(name, props) {
|
|
|
135
135
|
return behavior;
|
|
136
136
|
}
|
|
137
137
|
//#endregion
|
|
138
|
+
//#region src/core/node-path.ts
|
|
139
|
+
function parseNodePath(path) {
|
|
140
|
+
if (path === "") throw new IncantoError("BAD_NODE_PATH", "Node path must not be empty.");
|
|
141
|
+
if (path.startsWith("%")) {
|
|
142
|
+
const name = path.slice(1);
|
|
143
|
+
if (name === "" || name.includes("/") || name.includes("%")) throw new IncantoError("BAD_NODE_PATH", `Invalid unique-name path '${path}'. Expected '%NodeName'.`);
|
|
144
|
+
return {
|
|
145
|
+
kind: "unique",
|
|
146
|
+
name
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (path === ".") return {
|
|
150
|
+
kind: "relative",
|
|
151
|
+
segments: []
|
|
152
|
+
};
|
|
153
|
+
const absolute = path.startsWith("/");
|
|
154
|
+
const body = absolute ? path.slice(1) : path;
|
|
155
|
+
if (body === "" || body.split("/").some((s) => s === "")) throw new IncantoError("BAD_NODE_PATH", `Invalid node path '${path}'. Segments must be non-empty (no leading/trailing/double '/').`);
|
|
156
|
+
const segments = body.split("/");
|
|
157
|
+
for (const seg of segments) {
|
|
158
|
+
if (seg.includes("%")) throw new IncantoError("BAD_NODE_PATH", `Invalid node path '${path}'. '%' is only allowed as a whole-path prefix ('%Name').`);
|
|
159
|
+
if (seg === ".") throw new IncantoError("BAD_NODE_PATH", `Invalid node path '${path}'. '.' is only valid as the whole path — drop the './' prefix.`);
|
|
160
|
+
}
|
|
161
|
+
if (absolute && segments.includes("..")) throw new IncantoError("BAD_NODE_PATH", `Invalid node path '${path}'. Absolute paths must not contain '..'.`);
|
|
162
|
+
return {
|
|
163
|
+
kind: absolute ? "absolute" : "relative",
|
|
164
|
+
segments
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
138
168
|
//#region src/core/diagnostics.ts
|
|
139
169
|
function diagnose(engine, level, ...parts) {
|
|
140
170
|
engine?.log[level](...parts);
|
|
@@ -640,35 +670,344 @@ function effectiveOrder(group, renderOrder, table = ORDER_GROUP_BASE) {
|
|
|
640
670
|
return (table[group] ?? 0) + renderOrder;
|
|
641
671
|
}
|
|
642
672
|
//#endregion
|
|
643
|
-
//#region src/core/
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
673
|
+
//#region src/core/save.ts
|
|
674
|
+
const memoryStores = /* @__PURE__ */ new Map();
|
|
675
|
+
function createSaveStore(namespace) {
|
|
676
|
+
const prefix = `incanto:${namespace}:`;
|
|
677
|
+
const local = storageOrNull();
|
|
678
|
+
if (!local) {
|
|
679
|
+
let mem = memoryStores.get(namespace);
|
|
680
|
+
if (!mem) {
|
|
681
|
+
mem = /* @__PURE__ */ new Map();
|
|
682
|
+
memoryStores.set(namespace, mem);
|
|
683
|
+
}
|
|
684
|
+
const backing = mem;
|
|
649
685
|
return {
|
|
650
|
-
|
|
651
|
-
|
|
686
|
+
get: (key, fallback) => parse(backing.get(key), fallback),
|
|
687
|
+
set: (key, value) => void backing.set(key, JSON.stringify(value)),
|
|
688
|
+
remove: (key) => void backing.delete(key),
|
|
689
|
+
clear: () => backing.clear()
|
|
652
690
|
};
|
|
653
691
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
692
|
+
return {
|
|
693
|
+
get: (key, fallback) => parse(local.getItem(prefix + key) ?? void 0, fallback),
|
|
694
|
+
set: (key, value) => local.setItem(prefix + key, JSON.stringify(value)),
|
|
695
|
+
remove: (key) => local.removeItem(prefix + key),
|
|
696
|
+
clear: () => {
|
|
697
|
+
const doomed = [];
|
|
698
|
+
for (let i = 0; i < local.length; i++) {
|
|
699
|
+
const k = local.key(i);
|
|
700
|
+
if (k?.startsWith(prefix)) doomed.push(k);
|
|
701
|
+
}
|
|
702
|
+
for (const k of doomed) local.removeItem(k);
|
|
703
|
+
}
|
|
657
704
|
};
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if (
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
705
|
+
}
|
|
706
|
+
function parse(raw, fallback) {
|
|
707
|
+
if (raw === void 0 || raw === null) return fallback;
|
|
708
|
+
try {
|
|
709
|
+
return JSON.parse(raw);
|
|
710
|
+
} catch {
|
|
711
|
+
return fallback;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
function storageOrNull() {
|
|
715
|
+
try {
|
|
716
|
+
if (typeof localStorage === "undefined") return null;
|
|
717
|
+
const probe = "__incanto_probe__";
|
|
718
|
+
localStorage.setItem(probe, "1");
|
|
719
|
+
localStorage.removeItem(probe);
|
|
720
|
+
return localStorage;
|
|
721
|
+
} catch {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
//#endregion
|
|
726
|
+
//#region src/core/save-slots.ts
|
|
727
|
+
/**
|
|
728
|
+
* Save slots — "continue where you left off", which no game on this engine could
|
|
729
|
+
* do.
|
|
730
|
+
*
|
|
731
|
+
* `createSaveStore` has been here a long time and `Scene.toJSON()` round-trips a
|
|
732
|
+
* live tree, so two thirds of the work existed. The missing third was that
|
|
733
|
+
* BEHAVIOR state had nowhere to go: `Behavior` declared `onEnterTree`, `onReady`,
|
|
734
|
+
* `onExitTree`, `update` and `fixedUpdate` and nothing for "write yourself down",
|
|
735
|
+
* so `Health.current`, `ScoreKeeper.score` and every quest flag were
|
|
736
|
+
* unrecoverable. Both quest templates this repo ships were RPGs you could not
|
|
737
|
+
* resume.
|
|
738
|
+
*
|
|
739
|
+
* ## What a save is, and what it deliberately is NOT
|
|
740
|
+
*
|
|
741
|
+
* A save is **which scene, plus every behavior's state, keyed by node uid**. It
|
|
742
|
+
* is not a snapshot of the live tree.
|
|
743
|
+
*
|
|
744
|
+
* That is a design decision, not a shortcut. Restoring a live tree by walking
|
|
745
|
+
* paths breaks on this engine specifically:
|
|
746
|
+
*
|
|
747
|
+
* - `Spawner.onReady` DETACHES its prefab template from the tree (see
|
|
748
|
+
* `SpawnSource.resolveTemplate`), so a captured tree and a freshly booted one
|
|
749
|
+
* legitimately disagree about which nodes exist, every time.
|
|
750
|
+
* Every scene with a spawner would report phantom removals.
|
|
751
|
+
* - A spawned enemy carrying its own spawner has already lost ITS template, so
|
|
752
|
+
* re-adding that subtree runs `onReady` against a node that is not there and
|
|
753
|
+
* throws — and `onReady` is not quarantined, so it kills the whole load.
|
|
754
|
+
* - Prop deltas cannot be applied with `applySchemaProps`, which resets every
|
|
755
|
+
* prop to its default before writing.
|
|
756
|
+
*
|
|
757
|
+
* Reloading the scene from source sidesteps all three: the structure comes from
|
|
758
|
+
* the file (which is authoritative and already validated), and the save carries
|
|
759
|
+
* only what the file cannot know. A fresh boot detaches the same templates the
|
|
760
|
+
* captured one did, so there is nothing to reconcile.
|
|
761
|
+
*
|
|
762
|
+
* The cost, stated plainly so nobody discovers it: **spawned enemies and
|
|
763
|
+
* mid-level positions are not restored.** You resume at the scene's start with
|
|
764
|
+
* your stats, inventory, unlocks and quest flags intact. That is what a
|
|
765
|
+
* checkpoint save is, and it is what both quest templates need. A game that
|
|
766
|
+
* wants a position saves it — `serialize()` can return anything.
|
|
767
|
+
*
|
|
768
|
+
* Node uids are the join key because they are the one identifier the engine
|
|
769
|
+
* already promises survives a rename or a reparent.
|
|
770
|
+
*/
|
|
771
|
+
/**
|
|
772
|
+
* The one key in a save that is not a uid: authored nodes the run consumed.
|
|
773
|
+
*
|
|
774
|
+
* A save reloads the scene from its FILE, so every collectible comes back —
|
|
775
|
+
* resume a collect-five-to-win run at four and there are five gems on the map
|
|
776
|
+
* again with your four still banked. No behavior can fix that from the inside:
|
|
777
|
+
* the node that knew is the node that was freed.
|
|
778
|
+
*
|
|
779
|
+
* The engine can answer it without anyone's help. A uid in the scene file that
|
|
780
|
+
* is not in the live tree is a node this run consumed. Spawned clones never
|
|
781
|
+
* carry uids (`duplicateNode` drops them so a template can never mint colliding
|
|
782
|
+
* ids), so the ledger is exactly the authored world — the part the file
|
|
783
|
+
* rebuilds. `n_` prefixes every uid, so `#` can never collide with one.
|
|
784
|
+
*/
|
|
785
|
+
const FREED_KEY = "#freed";
|
|
786
|
+
/**
|
|
787
|
+
* Every behavior in the tree that has something to say, by uid.
|
|
788
|
+
*
|
|
789
|
+
* A behavior with no `serialize` contributes nothing — the hook is optional in
|
|
790
|
+
* exactly the way the other five are, so adding save support to a game is
|
|
791
|
+
* additive and a game that never saves pays nothing.
|
|
792
|
+
*
|
|
793
|
+
* A node with no `uid` is skipped and reported, because state keyed by nothing
|
|
794
|
+
* cannot be given back. The editor assigns a uid to every node it touches; a
|
|
795
|
+
* hand-written scene may not have.
|
|
796
|
+
*/
|
|
797
|
+
function captureBehaviors(root, source) {
|
|
798
|
+
const state = {};
|
|
799
|
+
const unaddressable = [];
|
|
800
|
+
let saveable = 0;
|
|
801
|
+
const walk = (node) => {
|
|
802
|
+
const behavior = node.behavior;
|
|
803
|
+
if (behavior && typeof behavior.serialize === "function") {
|
|
804
|
+
const value = behavior.serialize();
|
|
805
|
+
if (value !== void 0) {
|
|
806
|
+
saveable += 1;
|
|
807
|
+
if (node.uid) state[node.uid] = value;
|
|
808
|
+
else unaddressable.push(node.getPath());
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
for (const child of node.children) walk(child);
|
|
812
|
+
};
|
|
813
|
+
walk(root);
|
|
814
|
+
if (source !== void 0) {
|
|
815
|
+
const alive = /* @__PURE__ */ new Set();
|
|
816
|
+
const mark = (node) => {
|
|
817
|
+
const leaving = node.tree?._isQueuedFree(node) ?? false;
|
|
818
|
+
if (node.uid && !leaving) alive.add(node.uid);
|
|
819
|
+
for (const child of node.children) mark(child);
|
|
820
|
+
};
|
|
821
|
+
mark(root);
|
|
822
|
+
const freed = authoredUids(source).filter((uid) => !alive.has(uid));
|
|
823
|
+
if (freed.length > 0) state[FREED_KEY] = freed;
|
|
665
824
|
}
|
|
666
|
-
if (absolute && segments.includes("..")) throw new IncantoError("BAD_NODE_PATH", `Invalid node path '${path}'. Absolute paths must not contain '..'.`);
|
|
667
825
|
return {
|
|
668
|
-
|
|
669
|
-
|
|
826
|
+
state,
|
|
827
|
+
unaddressable,
|
|
828
|
+
saveable
|
|
670
829
|
};
|
|
671
830
|
}
|
|
831
|
+
/** Every uid the scene FILE declares, in document order. */
|
|
832
|
+
function authoredUids(source) {
|
|
833
|
+
const out = [];
|
|
834
|
+
const walk = (node) => {
|
|
835
|
+
if (typeof node !== "object" || node === null) return;
|
|
836
|
+
const n = node;
|
|
837
|
+
if (typeof n.uid === "string" && n.uid !== "") out.push(n.uid);
|
|
838
|
+
if (Array.isArray(n.children)) for (const child of n.children) walk(child);
|
|
839
|
+
};
|
|
840
|
+
walk(source.root);
|
|
841
|
+
return out;
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Hand each behavior its state back.
|
|
845
|
+
*
|
|
846
|
+
* Call this AFTER the scene has loaded and `onReady` has run: `onReady` is where
|
|
847
|
+
* a behavior sets its starting values, so restoring first would be overwritten
|
|
848
|
+
* by the fresh start.
|
|
849
|
+
*
|
|
850
|
+
* Never throws on a mismatch. A save from an older build of the game will name
|
|
851
|
+
* uids that no longer exist, and refusing to load it would mean a patch that
|
|
852
|
+
* moves one node deletes everyone's progress. It reports instead.
|
|
853
|
+
*/
|
|
854
|
+
function restoreBehaviors(root, state) {
|
|
855
|
+
const report = {
|
|
856
|
+
missing: [],
|
|
857
|
+
skipped: [],
|
|
858
|
+
restored: 0,
|
|
859
|
+
expected: captureBehaviors(root).saveable,
|
|
860
|
+
freed: 0
|
|
861
|
+
};
|
|
862
|
+
for (const [uid, value] of Object.entries(state)) {
|
|
863
|
+
if (uid === "#freed") {
|
|
864
|
+
for (const gone of value ?? []) {
|
|
865
|
+
const node = root.getNodeByUid(gone);
|
|
866
|
+
if (node) {
|
|
867
|
+
node.queueFree();
|
|
868
|
+
report.freed += 1;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
const node = root.getNodeByUid(uid);
|
|
874
|
+
if (!node) {
|
|
875
|
+
report.missing.push(uid);
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
const behavior = node.behavior;
|
|
879
|
+
if (!behavior || typeof behavior.deserialize !== "function") {
|
|
880
|
+
report.skipped.push(uid);
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
try {
|
|
884
|
+
behavior.deserialize(value);
|
|
885
|
+
report.restored += 1;
|
|
886
|
+
} catch {
|
|
887
|
+
report.skipped.push(uid);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return report;
|
|
891
|
+
}
|
|
892
|
+
/** Slot ids, so a "3 save slots" menu is a list and not a naming convention. */
|
|
893
|
+
const INDEX_KEY = "slots";
|
|
894
|
+
/**
|
|
895
|
+
* The slot layer: a named list of saves over the existing store.
|
|
896
|
+
*
|
|
897
|
+
* Slot ids are yours (`'auto'`, `'1'`, `'2'`, `'3'`) — the engine does not
|
|
898
|
+
* decide how many you have or whether one of them autosaves.
|
|
899
|
+
*/
|
|
900
|
+
var SaveSlots = class {
|
|
901
|
+
store;
|
|
902
|
+
constructor(namespace = "saves", store) {
|
|
903
|
+
this.store = store ?? createSaveStore(namespace);
|
|
904
|
+
}
|
|
905
|
+
/** Slot ids that have a save, newest first. */
|
|
906
|
+
list() {
|
|
907
|
+
const rows = this.store.get(INDEX_KEY, []).map((id) => ({
|
|
908
|
+
id,
|
|
909
|
+
slot: this.read(id)
|
|
910
|
+
})).filter((row) => row.slot !== null);
|
|
911
|
+
rows.sort((a, b) => b.slot.savedAt - a.slot.savedAt);
|
|
912
|
+
return rows.map((row) => row.id);
|
|
913
|
+
}
|
|
914
|
+
/** Every slot with its contents — what a load menu actually renders. */
|
|
915
|
+
all() {
|
|
916
|
+
return this.list().map((id) => ({
|
|
917
|
+
id,
|
|
918
|
+
...this.read(id)
|
|
919
|
+
}));
|
|
920
|
+
}
|
|
921
|
+
read(id) {
|
|
922
|
+
const raw = this.store.get(key(id), null);
|
|
923
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
924
|
+
const slot = raw;
|
|
925
|
+
if (typeof slot.scene !== "string" || typeof slot.state !== "object" || slot.state === null) return null;
|
|
926
|
+
return {
|
|
927
|
+
scene: slot.scene,
|
|
928
|
+
state: slot.state,
|
|
929
|
+
label: typeof slot.label === "string" ? slot.label : "",
|
|
930
|
+
savedAt: typeof slot.savedAt === "number" ? slot.savedAt : 0,
|
|
931
|
+
playtime: typeof slot.playtime === "number" ? slot.playtime : 0,
|
|
932
|
+
data: slot.data ?? {}
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Write a slot.
|
|
937
|
+
*
|
|
938
|
+
* `savedAt` is a parameter rather than `Date.now()` so a test can assert an
|
|
939
|
+
* ordering without sleeping, and so a game that wants server time can use it.
|
|
940
|
+
*/
|
|
941
|
+
write(id, slot) {
|
|
942
|
+
const full = {
|
|
943
|
+
scene: slot.scene,
|
|
944
|
+
state: slot.state,
|
|
945
|
+
label: slot.label ?? "",
|
|
946
|
+
savedAt: slot.savedAt ?? 0,
|
|
947
|
+
playtime: slot.playtime ?? 0,
|
|
948
|
+
data: slot.data ?? {}
|
|
949
|
+
};
|
|
950
|
+
this.store.set(key(id), full);
|
|
951
|
+
const ids = this.store.get(INDEX_KEY, []);
|
|
952
|
+
if (!ids.includes(id)) this.store.set(INDEX_KEY, [...ids, id]);
|
|
953
|
+
}
|
|
954
|
+
remove(id) {
|
|
955
|
+
this.store.remove(key(id));
|
|
956
|
+
this.store.set(INDEX_KEY, this.store.get(INDEX_KEY, []).filter((other) => other !== id));
|
|
957
|
+
}
|
|
958
|
+
/** Wipe every slot — a "delete all data" button, and test isolation. */
|
|
959
|
+
clear() {
|
|
960
|
+
for (const id of this.store.get(INDEX_KEY, [])) this.store.remove(key(id));
|
|
961
|
+
this.store.set(INDEX_KEY, []);
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
function key(id) {
|
|
965
|
+
return `slot:${id}`;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Behaviors in this tree that hold state and cannot save it — the audit's
|
|
969
|
+
* question, answerable before a player loses an hour.
|
|
970
|
+
*
|
|
971
|
+
* A behavior with props but no `serialize` is not necessarily wrong (an
|
|
972
|
+
* `Oscillate` derives everything from time), so this reports rather than warns,
|
|
973
|
+
* and the caller decides.
|
|
974
|
+
*/
|
|
975
|
+
function behaviorsWithoutSave(root) {
|
|
976
|
+
const out = [];
|
|
977
|
+
const walk = (node) => {
|
|
978
|
+
const behavior = node.behavior;
|
|
979
|
+
const name = node.script?.name;
|
|
980
|
+
if (behavior && typeof name === "string" && typeof behavior.serialize !== "function") {
|
|
981
|
+
if (Object.keys(behaviorSchema(name)).length > 0) out.push(`${node.getPath()} (${name})`);
|
|
982
|
+
}
|
|
983
|
+
for (const child of node.children) walk(child);
|
|
984
|
+
};
|
|
985
|
+
walk(root);
|
|
986
|
+
return out;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Behaviors that DO save, on nodes with no uid to save them under — the state
|
|
990
|
+
* that goes nowhere.
|
|
991
|
+
*
|
|
992
|
+
* `incanto-check` warns about the built-ins it can recognise from the JSON
|
|
993
|
+
* (Health, ScoreKeeper, Collector), but a scene file cannot be asked whether
|
|
994
|
+
* YOUR behavior has a `serialize`. This can: it walks the live tree, so a quest
|
|
995
|
+
* log or a wallet of your own is named too.
|
|
996
|
+
*
|
|
997
|
+
* The pair to read before shipping: `behaviorsWithoutSave` is "did you forget
|
|
998
|
+
* to write serialize", this is "did you forget the uid that makes it count".
|
|
999
|
+
*/
|
|
1000
|
+
function savesWithoutUid(root) {
|
|
1001
|
+
const out = [];
|
|
1002
|
+
const walk = (node) => {
|
|
1003
|
+
const behavior = node.behavior;
|
|
1004
|
+
const name = node.script?.name;
|
|
1005
|
+
if (behavior && typeof behavior.serialize === "function" && !node.uid) out.push(`${node.getPath()} (${name ?? "behavior"})`);
|
|
1006
|
+
for (const child of node.children) walk(child);
|
|
1007
|
+
};
|
|
1008
|
+
walk(root);
|
|
1009
|
+
return out;
|
|
1010
|
+
}
|
|
672
1011
|
//#endregion
|
|
673
1012
|
//#region src/core/node.ts
|
|
674
1013
|
function validateName(name) {
|
|
@@ -1274,6 +1613,16 @@ var SceneTree = class SceneTree {
|
|
|
1274
1613
|
_detachRoot(node) {
|
|
1275
1614
|
if (this._root === node) this._root = null;
|
|
1276
1615
|
}
|
|
1616
|
+
/**
|
|
1617
|
+
* @internal Is this node already on its way out?
|
|
1618
|
+
*
|
|
1619
|
+
* A save taken from a `collected` handler runs BEFORE the deferred free, so
|
|
1620
|
+
* the pickup that triggered the checkpoint is still in the tree — and came
|
|
1621
|
+
* back on the next run while its score did not.
|
|
1622
|
+
*/
|
|
1623
|
+
_isQueuedFree(node) {
|
|
1624
|
+
return this.freeQueue.has(node);
|
|
1625
|
+
}
|
|
1277
1626
|
flushFreeQueue() {
|
|
1278
1627
|
if (this.freeQueue.size === 0) return;
|
|
1279
1628
|
const items = [...this.freeQueue];
|
|
@@ -1681,4 +2030,4 @@ function validateUniqueUids(root) {
|
|
|
1681
2030
|
walk(root);
|
|
1682
2031
|
}
|
|
1683
2032
|
//#endregion
|
|
1684
|
-
export {
|
|
2033
|
+
export { registerBehavior as A, diagnose as C, behaviorSignals as D, behaviorSchema as E, Signal as M, clearBehaviors as O, InputMap as S, Behavior as T, savesWithoutUid as _, SCENE_FORMAT as a, effectiveOrder as b, SceneTree as c, resolveConstants as d, Node as f, restoreBehaviors as g, captureBehaviors as h, serializeNode as i, registeredBehaviors as j, getBehavior as k, CONST_REF_KEY as l, behaviorsWithoutSave as m, loadScene as n, computeViewport as o, SaveSlots as p, Scene as r, resolveViewport as s, buildNodeJson as t, isConstRef as u, createSaveStore as v, parseNodePath as w, resolveOrderGroups as x, ORDER_GROUP_BASE as y };
|