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,4 +1,196 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { E as behaviorSchema, w as parseNodePath } from "./loader-B-Gft32x.js";
|
|
2
|
+
import { D as translationKey } from "./register-DVwlnZAZ.js";
|
|
3
|
+
import { i as getNodeSchema } from "./registry-C7u42TID.js";
|
|
4
|
+
//#region src/core/node-refs.ts
|
|
5
|
+
/**
|
|
6
|
+
* Node-path PROPS, resolved against scene JSON.
|
|
7
|
+
*
|
|
8
|
+
* `Chase.target`, `Camera2D.follow`, `Spawner.prefab`, `Joint3D.target`,
|
|
9
|
+
* `CharacterController3D.skinPath`, `Foliage3D.terrain` … each holds a path to
|
|
10
|
+
* another node, and nothing checked any of them. A connection whose end does not
|
|
11
|
+
* resolve is a hard `DANGLING_CONNECTION` at load; a PROP that does not resolve
|
|
12
|
+
* is nothing at all — `getNodeOrNull` returns null, the scene loads, the enemy
|
|
13
|
+
* never chases, and no error is ever printed.
|
|
14
|
+
*
|
|
15
|
+
* That makes it exactly the class `auditScene` exists for: loads fine, plays
|
|
16
|
+
* wrong. The editor uses the same resolver so the two cannot drift.
|
|
17
|
+
*
|
|
18
|
+
* The grammar comes from `parseNodePath` — the loader's own parser — so a path
|
|
19
|
+
* this module accepts is one the engine accepts.
|
|
20
|
+
*/
|
|
21
|
+
function nodeAt(root, path) {
|
|
22
|
+
let node = root;
|
|
23
|
+
for (const index of path) {
|
|
24
|
+
node = node?.children?.[index];
|
|
25
|
+
if (!node) return null;
|
|
26
|
+
}
|
|
27
|
+
return node ?? null;
|
|
28
|
+
}
|
|
29
|
+
function byName(root, name) {
|
|
30
|
+
const found = [];
|
|
31
|
+
const walk = (node, at) => {
|
|
32
|
+
if (node.name === name) found.push(at);
|
|
33
|
+
(node.children ?? []).forEach((child, i) => {
|
|
34
|
+
walk(child, [...at, i]);
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
walk(root, []);
|
|
38
|
+
return found;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve `value` from the node at `owner`, the way the engine would.
|
|
42
|
+
*
|
|
43
|
+
* Note the asymmetry with connections, whose paths resolve from the SCENE ROOT:
|
|
44
|
+
* a prop's path resolves from the node holding it, which is why `'../Skin'` is
|
|
45
|
+
* the documented default of `CharacterController3D.skinPath` and would mean
|
|
46
|
+
* nothing in a connection.
|
|
47
|
+
*/
|
|
48
|
+
function resolveRefInJson(root, owner, value) {
|
|
49
|
+
if (!root) return {
|
|
50
|
+
ok: false,
|
|
51
|
+
problem: {
|
|
52
|
+
kind: "noMatch",
|
|
53
|
+
name: value
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = parseNodePath(value);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
problem: {
|
|
63
|
+
kind: "grammar",
|
|
64
|
+
message: error instanceof Error ? error.message : String(error)
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (parsed.kind === "unique") {
|
|
69
|
+
const found = byName(root, parsed.name);
|
|
70
|
+
if (found.length === 1) return {
|
|
71
|
+
ok: true,
|
|
72
|
+
path: found[0]
|
|
73
|
+
};
|
|
74
|
+
if (found.length === 0) return {
|
|
75
|
+
ok: false,
|
|
76
|
+
problem: {
|
|
77
|
+
kind: "noMatch",
|
|
78
|
+
name: parsed.name
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
problem: {
|
|
84
|
+
kind: "ambiguous",
|
|
85
|
+
name: parsed.name,
|
|
86
|
+
count: found.length
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
let at;
|
|
91
|
+
let segments = parsed.segments;
|
|
92
|
+
if (parsed.kind === "absolute") {
|
|
93
|
+
const first = segments[0] ?? "";
|
|
94
|
+
if (first !== root.name && first !== "root") return {
|
|
95
|
+
ok: false,
|
|
96
|
+
problem: {
|
|
97
|
+
kind: "wrongRoot",
|
|
98
|
+
first,
|
|
99
|
+
root: String(root.name ?? "")
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
at = [];
|
|
103
|
+
segments = segments.slice(1);
|
|
104
|
+
} else at = [...owner];
|
|
105
|
+
for (const segment of segments) {
|
|
106
|
+
if (segment === "..") {
|
|
107
|
+
if (at.length === 0) return {
|
|
108
|
+
ok: false,
|
|
109
|
+
problem: { kind: "aboveRoot" }
|
|
110
|
+
};
|
|
111
|
+
at.pop();
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const here = nodeAt(root, at);
|
|
115
|
+
const index = (here?.children ?? []).findIndex((child) => child.name === segment);
|
|
116
|
+
if (index < 0) return {
|
|
117
|
+
ok: false,
|
|
118
|
+
problem: {
|
|
119
|
+
kind: "noChild",
|
|
120
|
+
parent: String(here?.name ?? "?"),
|
|
121
|
+
segment
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
at.push(index);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
path: at
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** The problem in English, for `incanto-check` and as the editor's base text. */
|
|
132
|
+
function describeRefProblem(problem, value) {
|
|
133
|
+
switch (problem.kind) {
|
|
134
|
+
case "grammar": return problem.message;
|
|
135
|
+
case "noMatch": return `'%${problem.name}' matches no node in this scene.`;
|
|
136
|
+
case "ambiguous": return `'%${problem.name}' matches ${problem.count} nodes — a %name must be unique.`;
|
|
137
|
+
case "wrongRoot": return `'${value}' starts at '${problem.first}', but the scene root is '${problem.root}' (or write '/root/…').`;
|
|
138
|
+
case "noChild": return `'${value}' leads nowhere — '${problem.parent}' has no child named '${problem.segment}'.`;
|
|
139
|
+
case "aboveRoot": return `'${value}' walks above the scene root.`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Every prop on this node declared `nodePath: true`, per bag. */
|
|
143
|
+
function refProps(node) {
|
|
144
|
+
const collect = (schema) => Object.entries(schema).filter(([, def]) => def?.nodePath).map(([key]) => key);
|
|
145
|
+
let props = [];
|
|
146
|
+
if (node.type) try {
|
|
147
|
+
props = collect(getNodeSchema(node.type));
|
|
148
|
+
} catch {}
|
|
149
|
+
const name = node.script?.name;
|
|
150
|
+
return {
|
|
151
|
+
props,
|
|
152
|
+
script: name ? collect(behaviorSchema(name)) : []
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Scene lints for node-path props that point at nothing.
|
|
157
|
+
*
|
|
158
|
+
* An EMPTY value is not reported: `''` is the default of most of these props and
|
|
159
|
+
* means "not set", which is a choice rather than a typo.
|
|
160
|
+
*/
|
|
161
|
+
function nodeRefWarnings(scene) {
|
|
162
|
+
const root = scene.root;
|
|
163
|
+
if (!root) return [];
|
|
164
|
+
const warnings = [];
|
|
165
|
+
const walk = (node, at, path) => {
|
|
166
|
+
const keys = refProps(node);
|
|
167
|
+
const bags = [[
|
|
168
|
+
node.props,
|
|
169
|
+
keys.props,
|
|
170
|
+
""
|
|
171
|
+
], [
|
|
172
|
+
node.script?.props,
|
|
173
|
+
keys.script,
|
|
174
|
+
`${node.script?.name ?? "script"}.`
|
|
175
|
+
]];
|
|
176
|
+
for (const [bag, propKeys, prefix] of bags) {
|
|
177
|
+
if (!bag) continue;
|
|
178
|
+
for (const key of propKeys) {
|
|
179
|
+
const value = bag[key];
|
|
180
|
+
if (typeof value !== "string" || value === "") continue;
|
|
181
|
+
const result = resolveRefInJson(root, at, value);
|
|
182
|
+
if (result.ok) continue;
|
|
183
|
+
warnings.push(`${path}: ${prefix}${key} — ${describeRefProblem(result.problem, value)} The scene loads and the prop does nothing.`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
(node.children ?? []).forEach((child, i) => {
|
|
187
|
+
walk(child, [...at, i], `${path}/${child.name ?? "?"}`);
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
walk(root, [], String(root.name ?? "root"));
|
|
191
|
+
return warnings;
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
2
194
|
//#region src/core/audit.ts
|
|
3
195
|
const ANIMATED_TYPES = new Set([
|
|
4
196
|
"Particles2D",
|
|
@@ -31,6 +223,21 @@ const LIGHT_TYPES = new Set([
|
|
|
31
223
|
"OmniLight3D",
|
|
32
224
|
"SpotLight3D"
|
|
33
225
|
]);
|
|
226
|
+
/**
|
|
227
|
+
* Built-ins that put something in a save. A save is keyed by node uid, so one
|
|
228
|
+
* of these on a node without one is state that silently goes nowhere: the save
|
|
229
|
+
* comes back `{}` and the load reports a clean bill, because "nothing was
|
|
230
|
+
* missing" is true of an empty save.
|
|
231
|
+
*
|
|
232
|
+
* A JSON walk cannot ask a behavior whether it serializes, so this is the list
|
|
233
|
+
* of the ones that do. Your own behavior with a `serialize()` is caught at
|
|
234
|
+
* runtime instead, by `captureState`.
|
|
235
|
+
*/
|
|
236
|
+
const SAVEABLE_SCRIPTS = new Set([
|
|
237
|
+
"Health",
|
|
238
|
+
"ScoreKeeper",
|
|
239
|
+
"Collector"
|
|
240
|
+
]);
|
|
34
241
|
/** Does the scene header light anything by itself (sky, hdri, ambient, preset)? */
|
|
35
242
|
function isLit(environment) {
|
|
36
243
|
if (!environment) return false;
|
|
@@ -47,9 +254,12 @@ function auditScene(scene) {
|
|
|
47
254
|
let currentCameras = 0;
|
|
48
255
|
let cameras = 0;
|
|
49
256
|
let lights = 0;
|
|
257
|
+
const needUid = [];
|
|
50
258
|
const walk = (node, path, insideHud, staticRoot) => {
|
|
51
259
|
const type = node.type ?? "";
|
|
52
260
|
const props = node.props ?? {};
|
|
261
|
+
const script = node.script?.name;
|
|
262
|
+
if (script && SAVEABLE_SCRIPTS.has(script) && !node.uid) needUid.push(`${path} (${script})`);
|
|
53
263
|
if (type === "Camera2D" || type === "Camera3D") {
|
|
54
264
|
cameras += 1;
|
|
55
265
|
if (props.current === true) currentCameras += 1;
|
|
@@ -66,9 +276,11 @@ function auditScene(scene) {
|
|
|
66
276
|
for (const child of node.children ?? []) walk(child, `${path}/${child.name ?? "?"}`, nextHud, nextStatic);
|
|
67
277
|
};
|
|
68
278
|
walk(root, root.name ?? "root", false, root.props?.static === true ? root.name ?? "root" : null);
|
|
279
|
+
if (needUid.length > 0) warnings.push(`these carry state a save keeps and have no uid to key it under, so the save comes back EMPTY and the load reports no problem: ${needUid.join(", ")}. Give each one a "uid" from newUid().`);
|
|
69
280
|
if (cameras > 0 && currentCameras === 0) warnings.push("no camera has \"current\": true — the screen renders from a default view, probably not what you framed.");
|
|
70
281
|
if (currentCameras > 1) warnings.push(`${currentCameras} cameras claim "current": true — only the first found wins.`);
|
|
71
282
|
if (scene.dimension === "3d" && lights === 0 && !isLit(scene.environment)) warnings.push("nothing lights this 3D scene — no light node, no environment sky/hdri, and no ambient intensity. It will render black. Add a DirectionalLight3D, or an \"environment\": { \"sky\": { \"type\": \"atmosphere\" } } / { \"preset\": \"...\" }.");
|
|
283
|
+
for (const warning of nodeRefWarnings(scene)) warnings.push(warning);
|
|
72
284
|
for (const warning of auditStrings(scene)) warnings.push(warning);
|
|
73
285
|
return warnings;
|
|
74
286
|
}
|
|
@@ -198,4 +410,4 @@ function replay(engine, recording, opts) {
|
|
|
198
410
|
}
|
|
199
411
|
}
|
|
200
412
|
//#endregion
|
|
201
|
-
export { startRecording as n, auditScene as r, replay as t };
|
|
413
|
+
export { nodeRefWarnings as a, describeRefProblem as i, startRecording as n, resolveRefInJson as o, auditScene as r, replay as t };
|