incanto 0.28.0 → 0.30.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/README.md +44 -0
- package/bin/incanto-editor.mjs +219 -29
- package/dist/2d.d.ts +54 -6
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +353 -16
- package/dist/3d.js +5 -5
- package/dist/{behavior-CPibUfnH.d.ts → behavior-BAc0erXF.d.ts} +21 -0
- package/dist/{create-game-71TJjW1T.js → create-game-5z_QVtLx.js} +59 -16
- package/dist/{create-game-BRgWpNsa.js → create-game-DuBTv2zI.js} +276 -49
- package/dist/debug-draw-BM3DsvtT.js +18 -0
- package/dist/debug.d.ts +51 -6
- package/dist/debug.js +242 -19
- package/dist/editor-switch-B0wB_DSr.d.ts +59 -0
- package/dist/editor.d.ts +49 -0
- package/dist/editor.js +8554 -0
- package/dist/{gameplay-DEG-TP7D.js → gameplay-Cfr6aFZ1.js} +219 -40
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +31 -3
- package/dist/index.js +2 -2
- package/dist/{loader-DILt9PGC.d.ts → loader-B242FF6N.d.ts} +1 -1
- package/dist/net.d.ts +1 -1
- package/dist/net.js +2 -2
- package/dist/{pathfinding-RWYkNKx9.d.ts → pathfinding-BwD974Ss.d.ts} +1 -1
- package/dist/{physics-2d-DiVFFlH3.js → physics-2d-3kOQCtgd.js} +88 -3
- package/dist/{physics-3d--y5clE2j.js → physics-3d-CeRH-Ff_.js} +475 -9
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-nObreUQR.js → register-BTg0EM7s.js} +35 -3
- package/dist/{register-6R75AC7-.js → register-DJ0SByQg.js} +5550 -5073
- package/dist/{register-CvpSUU3O.js → register-DWcWq4QG.js} +22 -2
- package/dist/{register-BFFE1Mh1.js → register-MelqEdza.js} +1 -1
- package/dist/teardown-ByzfDPyu.js +240 -0
- package/dist/test.d.ts +35 -3
- package/dist/test.js +104 -10
- package/dist/vite.d.ts +139 -1
- package/dist/vite.js +355 -4
- package/editor/assets/{agent8-CojUfCXN.js → agent8-CAp0i5qn.js} +1 -1
- package/editor/assets/debug-BoEYfbqK.js +2 -0
- package/editor/assets/index-BO6WU8by.js +10696 -0
- package/editor/index.html +3 -157
- package/package.json +3 -2
- package/schemas/scene.schema.json +102 -4
- package/skills/incanto-3d-models.md +38 -0
- package/skills/incanto-assets.md +13 -0
- package/skills/incanto-building-3d-games.md +83 -6
- package/skills/incanto-editor.md +212 -8
- package/skills/incanto-environment.md +65 -1
- package/skills/incanto-node-reference.md +33 -1
- package/skills/incanto-physics-and-input.md +60 -6
- package/skills/incanto-verifying-your-game.md +57 -2
- package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +5 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/vite.config.ts +7 -0
- package/templates-app/tps-3d/docs/project-3d-rules.md +5 -0
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/vite.config.ts +7 -0
- package/templates-app/village-quest-3d/docs/project-3d-rules.md +5 -0
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/vite.config.ts +7 -0
- package/dist/debug-draw-CZmOYjL2.js +0 -13
- package/editor/assets/index-D6RQgROR.js +0 -8330
- package/editor/assets/index-D8QvwvOm.css +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { f as Node } from "./loader-BZqOKfI2.js";
|
|
2
|
-
import { t as registerCoreNodes } from "./register-
|
|
2
|
+
import { t as registerCoreNodes } from "./register-BTg0EM7s.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
4
|
import { t as Rng } from "./rng-DP-SR7eg.js";
|
|
5
5
|
import { i as getNodeSchema, l as registerNode } from "./registry-BVJ2HbCn.js";
|
|
@@ -115,7 +115,27 @@ var Node2D = class extends Node {
|
|
|
115
115
|
* `{shape:'capsule', radius, height}` — never child shape nodes.
|
|
116
116
|
*/
|
|
117
117
|
var PhysicsBody2D = class extends Node2D {
|
|
118
|
-
static props = { collider: {
|
|
118
|
+
static props = { collider: {
|
|
119
|
+
default: {},
|
|
120
|
+
variants: {
|
|
121
|
+
tag: "shape",
|
|
122
|
+
byTag: {
|
|
123
|
+
rect: {
|
|
124
|
+
shape: "rect",
|
|
125
|
+
size: [32, 32]
|
|
126
|
+
},
|
|
127
|
+
circle: {
|
|
128
|
+
shape: "circle",
|
|
129
|
+
radius: 16
|
|
130
|
+
},
|
|
131
|
+
capsule: {
|
|
132
|
+
shape: "capsule",
|
|
133
|
+
radius: 12,
|
|
134
|
+
height: 32
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} };
|
|
119
139
|
/** The unified collision model: every collider participant can emit these. */
|
|
120
140
|
static signals = ["triggerEnter", "triggerExit"];
|
|
121
141
|
_collider = {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { f as Node, t as buildNodeJson, y as Signal } from "./loader-BZqOKfI2.js";
|
|
2
|
-
import { t as registerCoreNodes } from "./register-
|
|
2
|
+
import { t as registerCoreNodes } from "./register-BTg0EM7s.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
4
|
import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
|
|
5
5
|
import { l as registerNode } from "./registry-BVJ2HbCn.js";
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
//#region src/core/editor-switch.ts
|
|
2
|
+
/**
|
|
3
|
+
* Cover the page while it changes what it is.
|
|
4
|
+
*
|
|
5
|
+
* A mode switch tears down a whole game and builds a whole IDE in its place;
|
|
6
|
+
* between those two the window shows a dead canvas, a flash of white body, and
|
|
7
|
+
* then a fully-formed editor. Every one of those frames is honest and none of
|
|
8
|
+
* them is pleasant. A veil makes the crossing read as one movement: fade to the
|
|
9
|
+
* editor's own background, do the work behind it, fade back.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately CSS-only and dependency-free — it has to work in a game page
|
|
12
|
+
* whose styles we know nothing about, so it sits at the top of the stacking
|
|
13
|
+
* order, eats pointer events (nobody clicks a UI that is being replaced), and
|
|
14
|
+
* removes itself completely. In a headless boot there is no document and this
|
|
15
|
+
* is a pair of no-ops.
|
|
16
|
+
*/
|
|
17
|
+
const VEIL_MS = 160;
|
|
18
|
+
function raiseVeil(label = "", background = "#0b0d14") {
|
|
19
|
+
if (typeof document === "undefined") return { lift: () => {} };
|
|
20
|
+
const el = document.createElement("div");
|
|
21
|
+
el.dataset.incantoSwitch = "";
|
|
22
|
+
el.style.cssText = `position:fixed;inset:0;z-index:2147483646;background:${background};display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity ${VEIL_MS}ms ease;pointer-events:auto;`;
|
|
23
|
+
if (label) {
|
|
24
|
+
const text = document.createElement("div");
|
|
25
|
+
text.textContent = label;
|
|
26
|
+
text.style.cssText = "font:13px/1 ui-monospace,SFMono-Regular,Menlo,monospace;color:#5d6680;letter-spacing:0.04em;";
|
|
27
|
+
el.appendChild(text);
|
|
28
|
+
}
|
|
29
|
+
document.body.appendChild(el);
|
|
30
|
+
requestAnimationFrame(() => {
|
|
31
|
+
el.style.opacity = "1";
|
|
32
|
+
});
|
|
33
|
+
let lifted = false;
|
|
34
|
+
return { lift() {
|
|
35
|
+
if (lifted) return;
|
|
36
|
+
lifted = true;
|
|
37
|
+
requestAnimationFrame(() => {
|
|
38
|
+
el.style.opacity = "0";
|
|
39
|
+
setTimeout(() => el.remove(), 200);
|
|
40
|
+
});
|
|
41
|
+
} };
|
|
42
|
+
}
|
|
43
|
+
/** Hold the veil up for at least one paint so the fade is visible at all. */
|
|
44
|
+
async function settle(ms = VEIL_MS) {
|
|
45
|
+
if (typeof requestAnimationFrame !== "function") return;
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The `/api/library` route `incanto/vite`'s `incantoLibrary()` (and the
|
|
50
|
+
* `incanto-editor` server) serve. Used when a game passes `library: true`.
|
|
51
|
+
*/
|
|
52
|
+
function devServerLibrary(token) {
|
|
53
|
+
return async (query) => {
|
|
54
|
+
const params = new URLSearchParams({
|
|
55
|
+
tags: query.tags.join(","),
|
|
56
|
+
page: String(query.page ?? 1),
|
|
57
|
+
limit: String(query.limit ?? 24)
|
|
58
|
+
});
|
|
59
|
+
if (query.keyword?.trim()) params.set("keyword", query.keyword.trim());
|
|
60
|
+
const res = await fetch(`/api/library?${params}`, { headers: token ? { "x-incanto-v8-token": token } : {} });
|
|
61
|
+
if (res.status === 401) {
|
|
62
|
+
const body = await res.json().catch(() => ({}));
|
|
63
|
+
const error = new Error(body.error ?? "the asset library needs a Verse8 access token");
|
|
64
|
+
error.needsToken = true;
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
if (!res.ok) throw new Error(`asset library: HTTP ${res.status}`);
|
|
68
|
+
return await res.json();
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Does the page's own dev server serve the project's scenes?
|
|
73
|
+
*
|
|
74
|
+
* `incantoScenes()` from `incanto/vite` (the plugin that already validates
|
|
75
|
+
* scenes on save; `incanto-new` puts it in every scaffolded project) answers
|
|
76
|
+
* `GET /api/scenes`. When it does, the
|
|
77
|
+
* editor a game switches into is a PROJECT editor: it can list, open, create
|
|
78
|
+
* and save every scene in the project, and play whichever one you loaded.
|
|
79
|
+
* When it does not (a built game, someone else's server), the editor still
|
|
80
|
+
* edits the scene the game is running, exactly as before.
|
|
81
|
+
*/
|
|
82
|
+
async function projectScenes() {
|
|
83
|
+
try {
|
|
84
|
+
const res = await fetch("/api/scenes");
|
|
85
|
+
if (!res.ok) return null;
|
|
86
|
+
const body = await res.json();
|
|
87
|
+
return Array.isArray(body) ? body : null;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** What to say when the dev server has no idea what scenes this project has. */
|
|
93
|
+
const NO_SCENE_ROUTES = "this dev server does not serve the project's scenes yet. Add incantoScenes() to vite.config.ts and restart:\n\n import { incantoScenes } from 'incanto/vite';\n export default defineConfig({ plugins: [incantoScenes()] });\n\nIt also validates every scene you save, before the page reloads into a broken one.";
|
|
94
|
+
async function readProjectScene(file) {
|
|
95
|
+
const res = await fetch(`/api/scene?file=${encodeURIComponent(file)}`);
|
|
96
|
+
if (res.ok) return await res.json();
|
|
97
|
+
const body = await res.json().catch(() => null);
|
|
98
|
+
throw new Error(body?.error ?? `could not open ${file}: HTTP ${res.status}`);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The default opener: pull `incanto/editor` in on demand and mount it.
|
|
102
|
+
*
|
|
103
|
+
* It is a dynamic import so the editor — an IDE's worth of UI — never enters a
|
|
104
|
+
* production bundle. A build that cannot resolve it fails HERE, with the two
|
|
105
|
+
* ways to fix it, rather than silently offering a menu item that does nothing.
|
|
106
|
+
*/
|
|
107
|
+
async function openBundledEditor(opts) {
|
|
108
|
+
let mod;
|
|
109
|
+
try {
|
|
110
|
+
mod = await import("incanto/editor");
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new Error(`createGame3D editor: could not load 'incanto/editor'. If this app aliases incanto to the engine SOURCE (the examples and playground do), add an alias for 'incanto/editor' → packages/editor/src/main.ts — or pass editor: { open } and load it yourself. (${error instanceof Error ? error.message : String(error)})`);
|
|
113
|
+
}
|
|
114
|
+
let handle = null;
|
|
115
|
+
const project = await projectScenes();
|
|
116
|
+
const format = mod.formatSceneJson ?? ((json) => JSON.stringify(json, null, 2));
|
|
117
|
+
const writeProjectScene = async (file, json) => {
|
|
118
|
+
const res = await fetch(`/api/scene?file=${encodeURIComponent(file)}`, {
|
|
119
|
+
method: "PUT",
|
|
120
|
+
body: format(json)
|
|
121
|
+
});
|
|
122
|
+
if (!res.ok) throw new Error(`could not save ${file}: HTTP ${res.status} ${await res.text()}`);
|
|
123
|
+
};
|
|
124
|
+
handle = await mod.mountEditor({
|
|
125
|
+
...opts.camera ? { camera: opts.camera } : {},
|
|
126
|
+
api: {
|
|
127
|
+
meta: async () => ({
|
|
128
|
+
version: "live",
|
|
129
|
+
mode: "project",
|
|
130
|
+
root: "",
|
|
131
|
+
input: null,
|
|
132
|
+
output: null
|
|
133
|
+
}),
|
|
134
|
+
loadScene: async (file) => file ? await readProjectScene(file) : opts.scene,
|
|
135
|
+
...opts.save || project ? { saveScene: async (json, file) => {
|
|
136
|
+
if (file) return writeProjectScene(file, json);
|
|
137
|
+
await opts.save?.(json);
|
|
138
|
+
} } : {},
|
|
139
|
+
scenes: async () => {
|
|
140
|
+
const found = await projectScenes();
|
|
141
|
+
if (!found) throw new Error(NO_SCENE_ROUTES);
|
|
142
|
+
return found;
|
|
143
|
+
},
|
|
144
|
+
createScene: async (path) => {
|
|
145
|
+
const res = await fetch("/api/scenes", {
|
|
146
|
+
method: "POST",
|
|
147
|
+
body: JSON.stringify({ path })
|
|
148
|
+
});
|
|
149
|
+
if (res.status === 404) throw new Error(NO_SCENE_ROUTES);
|
|
150
|
+
if (!res.ok) throw new Error(`could not create ${path}: ${await res.text()}`);
|
|
151
|
+
return await res.json();
|
|
152
|
+
},
|
|
153
|
+
...opts.library ? { library: opts.library } : {}
|
|
154
|
+
},
|
|
155
|
+
onPlay: (choice, working) => {
|
|
156
|
+
if (choice !== "debug") return void 0;
|
|
157
|
+
const camera = handle?.camera();
|
|
158
|
+
return crossFade(async () => {
|
|
159
|
+
handle?.dispose();
|
|
160
|
+
handle = null;
|
|
161
|
+
await opts.play(working, camera);
|
|
162
|
+
}, "starting the game…");
|
|
163
|
+
},
|
|
164
|
+
...opts.exit ? { onExit: () => crossFade(async () => {
|
|
165
|
+
handle?.dispose();
|
|
166
|
+
handle = null;
|
|
167
|
+
await opts.exit?.();
|
|
168
|
+
}, "back to the game…") } : {}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Run a mode change behind the veil: cover, swap, reveal.
|
|
173
|
+
*
|
|
174
|
+
* The reveal waits for the new page to have PAINTED (two frames — one for the
|
|
175
|
+
* layout the swap just produced, one for it to reach the screen), otherwise the
|
|
176
|
+
* fade-out races the boot and shows the very flash it exists to hide.
|
|
177
|
+
*/
|
|
178
|
+
async function crossFade(swap, label = "") {
|
|
179
|
+
const veil = raiseVeil(label);
|
|
180
|
+
await settle();
|
|
181
|
+
try {
|
|
182
|
+
await swap();
|
|
183
|
+
} finally {
|
|
184
|
+
await settle(0);
|
|
185
|
+
veil.lift();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The camera the player is actually looking through, as a pose.
|
|
190
|
+
*
|
|
191
|
+
* Reads the renderer's LAST rendered camera rather than the scene's Camera3D:
|
|
192
|
+
* a third-person rig, a cutscene camera and a debug free-fly all end up in the
|
|
193
|
+
* same place, and it is the view on screen — the thing continuity is about.
|
|
194
|
+
*/
|
|
195
|
+
function poseFromRenderer(renderer, fallbackDistance = 8) {
|
|
196
|
+
const basis = renderer.cameraBasis;
|
|
197
|
+
const posOf = renderer.lastCameraPosition;
|
|
198
|
+
if (typeof basis !== "function" || typeof posOf !== "function") return void 0;
|
|
199
|
+
const eye = posOf.call(renderer);
|
|
200
|
+
if (!eye) return void 0;
|
|
201
|
+
const { forward } = basis.call(renderer);
|
|
202
|
+
return {
|
|
203
|
+
position: [
|
|
204
|
+
eye.x,
|
|
205
|
+
eye.y,
|
|
206
|
+
eye.z
|
|
207
|
+
],
|
|
208
|
+
target: [
|
|
209
|
+
eye.x + forward.x * fallbackDistance,
|
|
210
|
+
eye.y + forward.y * fallbackDistance,
|
|
211
|
+
eye.z + forward.z * fallbackDistance
|
|
212
|
+
]
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/core/teardown.ts
|
|
217
|
+
function teardown(steps, onError = defaultReport) {
|
|
218
|
+
const failures = [];
|
|
219
|
+
for (const step of steps) {
|
|
220
|
+
if (!step) continue;
|
|
221
|
+
const [what, run] = step;
|
|
222
|
+
try {
|
|
223
|
+
run();
|
|
224
|
+
} catch (error) {
|
|
225
|
+
failures.push({
|
|
226
|
+
what,
|
|
227
|
+
error
|
|
228
|
+
});
|
|
229
|
+
try {
|
|
230
|
+
onError(what, error);
|
|
231
|
+
} catch {}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { failures };
|
|
235
|
+
}
|
|
236
|
+
function defaultReport(what, error) {
|
|
237
|
+
console.error(`[incanto] teardown: ${what} failed (continuing)`, error);
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
export { poseFromRenderer as a, openBundledEditor as i, crossFade as n, devServerLibrary as r, teardown as t };
|
package/dist/test.d.ts
CHANGED
|
@@ -1,9 +1,41 @@
|
|
|
1
1
|
import { c as JsonValue, i as SceneJson } from "./schema-CcoWb32N.js";
|
|
2
|
-
import { $ as Node, T as LogEntry, n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-
|
|
3
|
-
import { t as LoadSceneOptions } from "./loader-
|
|
2
|
+
import { $ as Node, T as LogEntry, n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-BAc0erXF.js";
|
|
3
|
+
import { t as LoadSceneOptions } from "./loader-B242FF6N.js";
|
|
4
4
|
import { i as auditScene, t as IncantoError } from "./errors-1dXlIwoR.js";
|
|
5
5
|
|
|
6
6
|
//#region src/test/index.d.ts
|
|
7
|
+
/** One thing that is not standing where it should be. */
|
|
8
|
+
interface GroundingIssue {
|
|
9
|
+
/** Scene path, e.g. `/World/Footbridge/PostL0`. */
|
|
10
|
+
path: string;
|
|
11
|
+
/** Metres between the node's underside and the ground: + floats, − buried. */
|
|
12
|
+
gap: number;
|
|
13
|
+
}
|
|
14
|
+
interface GroundingOptions {
|
|
15
|
+
/** How far a foot may sit off the ground before it is a bug (default 0.1 m). */
|
|
16
|
+
tolerance?: number;
|
|
17
|
+
/** Node types to check (default: the ones that read as props on a map). */
|
|
18
|
+
types?: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Every prop that is floating or buried.
|
|
22
|
+
*
|
|
23
|
+
* THE most repeated mistake in scene authoring, by humans and agents alike, is
|
|
24
|
+
* a hand-computed Y: correct when it is written and wrong the moment anything
|
|
25
|
+
* reshapes the ground under it — a river carving its bed, a new seed, a
|
|
26
|
+
* generator that mirrors a carve slightly differently. This is the check that
|
|
27
|
+
* catches it, and `snapToGround` is the fix that stops it happening.
|
|
28
|
+
*
|
|
29
|
+
* Anything explicitly marked `snapToGround: false` is skipped: that is the
|
|
30
|
+
* author saying "yes, this hangs in the air", which a bridge deck, a lantern
|
|
31
|
+
* and a cloud all legitimately do.
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* const off = findFloatingProps(gameJson);
|
|
35
|
+
* ok(`nothing floats (${off.length})`, off.length === 0);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
declare function findFloatingProps(json: unknown, opts?: GroundingOptions): GroundingIssue[];
|
|
7
39
|
declare function registerAllNodes(): void;
|
|
8
40
|
interface NodeCapture {
|
|
9
41
|
/** Absolute path ('/Root/Player') — stable, grep-able. */
|
|
@@ -149,4 +181,4 @@ interface PlaySession {
|
|
|
149
181
|
*/
|
|
150
182
|
declare function createPlaySession(json: unknown, opts?: PlaySessionOptions): Promise<PlaySession>;
|
|
151
183
|
//#endregion
|
|
152
|
-
export { NodeCapture, PlaySession, PlaySessionOptions, RunContext, RunFailure, RunResult, RunScriptOptions, SceneCapture, ScriptStep, ValidateSceneOptions, ValidationResult, auditScene, captureScene, createPlaySession, describeCapture, registerAllNodes, runScript, validateScene };
|
|
184
|
+
export { GroundingIssue, GroundingOptions, NodeCapture, PlaySession, PlaySessionOptions, RunContext, RunFailure, RunResult, RunScriptOptions, SceneCapture, ScriptStep, ValidateSceneOptions, ValidationResult, auditScene, captureScene, createPlaySession, describeCapture, findFloatingProps, registerAllNodes, runScript, validateScene };
|
package/dist/test.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { _ as registerBehavior, n as loadScene } from "./loader-BZqOKfI2.js";
|
|
2
|
-
import { u as Engine } from "./register-
|
|
2
|
+
import { u as Engine } from "./register-BTg0EM7s.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
4
|
import { t as auditScene } from "./audit-C6rMyict.js";
|
|
5
5
|
import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
|
|
6
6
|
import { i as getNodeSchema, s as mergeStaticProps } from "./registry-BVJ2HbCn.js";
|
|
7
|
-
import { n as registerGameplayBehaviors } from "./gameplay-
|
|
8
|
-
import { t as registerNodes2D } from "./register-
|
|
9
|
-
import { t as registerNodes3D } from "./register-
|
|
10
|
-
import { t as registerNodesNet } from "./register-
|
|
7
|
+
import { n as registerGameplayBehaviors } from "./gameplay-Cfr6aFZ1.js";
|
|
8
|
+
import { t as registerNodes2D } from "./register-DWcWq4QG.js";
|
|
9
|
+
import { t as registerNodes3D } from "./register-DJ0SByQg.js";
|
|
10
|
+
import { t as registerNodesNet } from "./register-MelqEdza.js";
|
|
11
11
|
//#region src/test/index.ts
|
|
12
12
|
/**
|
|
13
13
|
* incanto/test — the browserless verification harness.
|
|
@@ -25,6 +25,100 @@ import { t as registerNodesNet } from "./register-BFFE1Mh1.js";
|
|
|
25
25
|
* Deterministic by construction: fixed timestep, seeded `engine.rng`,
|
|
26
26
|
* no requestAnimationFrame.
|
|
27
27
|
*/
|
|
28
|
+
/** Node types whose whole job is to sit on the ground. Characters, water,
|
|
29
|
+
* clouds, birds and lights are all legitimately off it. */
|
|
30
|
+
const GROUNDED_TYPES = ["MeshInstance3D", "InstancedMesh3D"];
|
|
31
|
+
/** Under one of these, the BODY owns where the thing stands — a character's
|
|
32
|
+
* skin hangs at whatever offset its rig wants, and is not a floating prop. */
|
|
33
|
+
const BODY_OWNED = new Set([
|
|
34
|
+
"CharacterBody3D",
|
|
35
|
+
"RigidBody3D",
|
|
36
|
+
"StaticBody3D",
|
|
37
|
+
"AreaBody3D"
|
|
38
|
+
]);
|
|
39
|
+
/**
|
|
40
|
+
* Every prop that is floating or buried.
|
|
41
|
+
*
|
|
42
|
+
* THE most repeated mistake in scene authoring, by humans and agents alike, is
|
|
43
|
+
* a hand-computed Y: correct when it is written and wrong the moment anything
|
|
44
|
+
* reshapes the ground under it — a river carving its bed, a new seed, a
|
|
45
|
+
* generator that mirrors a carve slightly differently. This is the check that
|
|
46
|
+
* catches it, and `snapToGround` is the fix that stops it happening.
|
|
47
|
+
*
|
|
48
|
+
* Anything explicitly marked `snapToGround: false` is skipped: that is the
|
|
49
|
+
* author saying "yes, this hangs in the air", which a bridge deck, a lantern
|
|
50
|
+
* and a cloud all legitimately do.
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* const off = findFloatingProps(gameJson);
|
|
54
|
+
* ok(`nothing floats (${off.length})`, off.length === 0);
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
function findFloatingProps(json, opts = {}) {
|
|
58
|
+
registerAllNodes();
|
|
59
|
+
const tolerance = opts.tolerance ?? .1;
|
|
60
|
+
const types = new Set(opts.types ?? GROUNDED_TYPES);
|
|
61
|
+
let scene;
|
|
62
|
+
try {
|
|
63
|
+
scene = loadScene(json);
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
let ground = null;
|
|
68
|
+
const findGround = (node) => {
|
|
69
|
+
if (!ground && node.constructor.typeName === "Terrain3D" && typeof node.heightAt === "function") ground = node;
|
|
70
|
+
for (const child of node.children) findGround(child);
|
|
71
|
+
};
|
|
72
|
+
findGround(scene.root);
|
|
73
|
+
if (!ground) return [];
|
|
74
|
+
const out = [];
|
|
75
|
+
const walk = (node, path, ox, oy, oz, yaw, bodyOwned) => {
|
|
76
|
+
const n = node;
|
|
77
|
+
let wx = ox;
|
|
78
|
+
let wy = oy;
|
|
79
|
+
let wz = oz;
|
|
80
|
+
let ownYaw = yaw;
|
|
81
|
+
if (Array.isArray(n.position)) {
|
|
82
|
+
const c = Math.cos(yaw);
|
|
83
|
+
const s = Math.sin(yaw);
|
|
84
|
+
const lx = n.position[0] ?? 0;
|
|
85
|
+
const lz = n.position[2] ?? 0;
|
|
86
|
+
wx = ox + lx * c + lz * s;
|
|
87
|
+
wz = oz - lx * s + lz * c;
|
|
88
|
+
wy = oy + (n.position[1] ?? 0);
|
|
89
|
+
ownYaw = yaw + (n.rotation?.[1] ?? 0) * Math.PI / 180;
|
|
90
|
+
}
|
|
91
|
+
const type = node.constructor.typeName ?? "";
|
|
92
|
+
const soil = ground;
|
|
93
|
+
if (types.has(type) && n.visible !== false && n.snapToGround !== false && !bodyOwned) {
|
|
94
|
+
const rows = node.transforms;
|
|
95
|
+
if (Array.isArray(rows) && rows.length > 0) {
|
|
96
|
+
let worst = null;
|
|
97
|
+
for (const row of rows) {
|
|
98
|
+
const scale = row[4] ?? 1;
|
|
99
|
+
const half = (n.size?.[1] ?? 0) * scale / 2;
|
|
100
|
+
const gap = wy + (row[1] ?? 0) - half - soil.heightAt(wx + (row[0] ?? 0), wz + (row[2] ?? 0));
|
|
101
|
+
if (Math.abs(gap) > tolerance && (!worst || Math.abs(gap) > Math.abs(worst.gap))) worst = {
|
|
102
|
+
path,
|
|
103
|
+
gap: Math.round(gap * 100) / 100
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (worst) out.push(worst);
|
|
107
|
+
} else {
|
|
108
|
+
const half = (n.size?.[1] ?? 0) / 2;
|
|
109
|
+
const gap = wy - half - soil.heightAt(wx, wz);
|
|
110
|
+
if (Math.abs(gap) > tolerance) out.push({
|
|
111
|
+
path,
|
|
112
|
+
gap: Math.round(gap * 100) / 100
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const childBodyOwned = bodyOwned || BODY_OWNED.has(type);
|
|
117
|
+
for (const child of node.children) walk(child, `${path}/${child.name}`, wx, wy, wz, ownYaw, childBodyOwned);
|
|
118
|
+
};
|
|
119
|
+
walk(scene.root, `/${scene.root.name}`, 0, 0, 0, 0, false);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
28
122
|
function registerAllNodes() {
|
|
29
123
|
registerNodes2D();
|
|
30
124
|
registerNodes3D();
|
|
@@ -124,10 +218,10 @@ async function runScript(json, opts) {
|
|
|
124
218
|
engine.setScene(scene);
|
|
125
219
|
const physics = opts.physics ?? "auto";
|
|
126
220
|
if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
|
|
127
|
-
const { enablePhysics2D } = await import("./physics-2d-
|
|
221
|
+
const { enablePhysics2D } = await import("./physics-2d-3kOQCtgd.js").then((n) => n.r);
|
|
128
222
|
await enablePhysics2D(engine);
|
|
129
223
|
} else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
|
|
130
|
-
const { enablePhysics3D } = await import("./physics-3d
|
|
224
|
+
const { enablePhysics3D } = await import("./physics-3d-CeRH-Ff_.js").then((n) => n.r);
|
|
131
225
|
await enablePhysics3D(engine);
|
|
132
226
|
}
|
|
133
227
|
const failures = [];
|
|
@@ -229,10 +323,10 @@ async function createPlaySession(json, opts = {}) {
|
|
|
229
323
|
engine.setScene(scene);
|
|
230
324
|
const physics = opts.physics ?? "auto";
|
|
231
325
|
if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
|
|
232
|
-
const { enablePhysics2D } = await import("./physics-2d-
|
|
326
|
+
const { enablePhysics2D } = await import("./physics-2d-3kOQCtgd.js").then((n) => n.r);
|
|
233
327
|
await enablePhysics2D(engine);
|
|
234
328
|
} else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
|
|
235
|
-
const { enablePhysics3D } = await import("./physics-3d
|
|
329
|
+
const { enablePhysics3D } = await import("./physics-3d-CeRH-Ff_.js").then((n) => n.r);
|
|
236
330
|
await enablePhysics3D(engine);
|
|
237
331
|
}
|
|
238
332
|
const stepMs = 1e3 / (opts.fixedHz ?? 60);
|
|
@@ -267,4 +361,4 @@ async function createPlaySession(json, opts = {}) {
|
|
|
267
361
|
};
|
|
268
362
|
}
|
|
269
363
|
//#endregion
|
|
270
|
-
export { auditScene, captureScene, createPlaySession, describeCapture, registerAllNodes, runScript, validateScene };
|
|
364
|
+
export { auditScene, captureScene, createPlaySession, describeCapture, findFloatingProps, registerAllNodes, runScript, validateScene };
|
package/dist/vite.d.ts
CHANGED
|
@@ -17,10 +17,148 @@ interface HotUpdateContext {
|
|
|
17
17
|
interface IncantoScenesOptions {
|
|
18
18
|
/** Hard-fail on unregistered behaviors too (default: structure-only). */
|
|
19
19
|
strictBehaviors?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Where scenes are found and written. Default: vite's `root` (your project).
|
|
22
|
+
* Nothing outside it is readable or writable, whatever a request asks for.
|
|
23
|
+
*/
|
|
24
|
+
root?: string;
|
|
20
25
|
}
|
|
21
26
|
declare function incantoScenes(opts?: IncantoScenesOptions): {
|
|
22
27
|
name: string;
|
|
23
28
|
handleHotUpdate(ctx: HotUpdateContext): Promise<undefined | never[]>;
|
|
29
|
+
configureServer(server: SceneDevServer): void;
|
|
24
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* `GET /api/scenes` → every `*.scene.json` under the project root
|
|
33
|
+
* `POST /api/scenes` `{path}` → create a minimal scene there
|
|
34
|
+
* `GET /api/scene?file=<rel>` → that scene's JSON
|
|
35
|
+
* `PUT /api/scene?file=<rel>` → write it back (bytes verbatim)
|
|
36
|
+
*
|
|
37
|
+
* These are the routes `incanto-editor` serves, on YOUR dev server, so the
|
|
38
|
+
* editor a game switches into is a project editor rather than a single-file
|
|
39
|
+
* one: it can list, open, create and save any scene in the project, and play
|
|
40
|
+
* whichever one you loaded.
|
|
41
|
+
*
|
|
42
|
+
* Everything is bounded to the root and to `*.scene.json`. A dev server is
|
|
43
|
+
* still a server: `?file=../../.ssh/id_rsa` has to be a 400, not a read.
|
|
44
|
+
*/
|
|
45
|
+
interface SceneDevServer {
|
|
46
|
+
config?: {
|
|
47
|
+
root?: string;
|
|
48
|
+
};
|
|
49
|
+
middlewares: {
|
|
50
|
+
use(path: string, handler: (req: ApiReq, res: LibraryRes) => void): void;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
interface ApiReq extends LibraryReq {
|
|
54
|
+
on(event: "data" | "end" | "error", cb: (chunk?: unknown) => void): void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* One JSON file in the project, and whether it is a scene.
|
|
58
|
+
*
|
|
59
|
+
* A file browser that only shows `*.scene.json` cannot show you the scene you
|
|
60
|
+
* saved as `level.json` — and, worse, gives no reason for its absence. So the
|
|
61
|
+
* list is every `.json` under the project (a scene is JSON by definition, so
|
|
62
|
+
* nothing else can be one), and `scene` says which of them actually are.
|
|
63
|
+
*/
|
|
64
|
+
interface ProjectFileEntry {
|
|
65
|
+
rel: string;
|
|
66
|
+
abs: string;
|
|
67
|
+
size: number;
|
|
68
|
+
modified: number;
|
|
69
|
+
/** Present only when the file IS a scene — read from the file, not the name. */
|
|
70
|
+
scene?: {
|
|
71
|
+
name: string;
|
|
72
|
+
dimension?: "2d" | "3d";
|
|
73
|
+
nodes: number;
|
|
74
|
+
};
|
|
75
|
+
/** Why it is not one, in a form a person can act on. */
|
|
76
|
+
notScene?: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Is this JSON an incanto scene? The file itself answers.
|
|
80
|
+
*
|
|
81
|
+
* A scene declares what it is — `{"format": 1, "type": "scene", "name", "root"}`
|
|
82
|
+
* — and those are exactly the four keys the loader demands before it will build
|
|
83
|
+
* anything (`validateHeader`). So "is this a scene" is not a guess from the file
|
|
84
|
+
* name; it is a fact readable from the first object in the file. That is what
|
|
85
|
+
* lets the browser mark a row ✓ instead of hoping.
|
|
86
|
+
*/
|
|
87
|
+
declare function sceneFacts(json: unknown): {
|
|
88
|
+
ok: true;
|
|
89
|
+
name: string;
|
|
90
|
+
dimension?: "2d" | "3d";
|
|
91
|
+
nodes: number;
|
|
92
|
+
} | {
|
|
93
|
+
ok: false;
|
|
94
|
+
why: string;
|
|
95
|
+
};
|
|
96
|
+
/** @internal Exported for tests. Root-bounded walk, deepest 6 levels. */
|
|
97
|
+
declare function discoverScenes(root: string): ProjectFileEntry[];
|
|
98
|
+
/**
|
|
99
|
+
* @internal The one place a request's path becomes a file path.
|
|
100
|
+
*
|
|
101
|
+
* `.json` under the project, nothing else — the file browser offers every JSON
|
|
102
|
+
* and the loader decides, but a dev server still must not be a way to read
|
|
103
|
+
* `.env` or somebody's key file. Content is gated separately: a JSON that is
|
|
104
|
+
* not a scene is never sent to the page, only the reason it is not one.
|
|
105
|
+
*/
|
|
106
|
+
declare function resolveSceneFile(root: string, rel: unknown): string | null;
|
|
107
|
+
/** @internal Exported for tests — `GET`/`POST /api/scenes`. */
|
|
108
|
+
declare function serveSceneList(req: ApiReq, res: LibraryRes, root: string): Promise<void>;
|
|
109
|
+
/** @internal Exported for tests — `GET`/`PUT /api/scene?file=`. */
|
|
110
|
+
declare function serveSceneFile(req: ApiReq, res: LibraryRes, root: string): Promise<void>;
|
|
111
|
+
interface DevServer {
|
|
112
|
+
middlewares: {
|
|
113
|
+
use(path: string, handler: (req: LibraryReq, res: LibraryRes) => void): void;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
interface LibraryReq {
|
|
117
|
+
url?: string | undefined;
|
|
118
|
+
method?: string | undefined;
|
|
119
|
+
headers: Record<string, string | string[] | undefined>;
|
|
120
|
+
}
|
|
121
|
+
interface LibraryRes {
|
|
122
|
+
statusCode: number;
|
|
123
|
+
setHeader(name: string, value: string): void;
|
|
124
|
+
end(body?: string): void;
|
|
125
|
+
}
|
|
126
|
+
interface IncantoLibraryOptions {
|
|
127
|
+
/**
|
|
128
|
+
* Verse8 access token. Defaults to INCANTO_V8_TOKEN / V8_ACCESS_TOKEN.
|
|
129
|
+
* Read at request time so a token exported after the server started works.
|
|
130
|
+
*/
|
|
131
|
+
token?: string;
|
|
132
|
+
/** Upstream catalog (default: the agent8 backend). */
|
|
133
|
+
url?: string;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Serve the agent8 asset catalog at `/api/library` on the dev server.
|
|
137
|
+
*
|
|
138
|
+
* The catalog wants a bearer token and does not CORS-allow localhost, so a page
|
|
139
|
+
* cannot call it — but the dev server can, and it is already running. This is
|
|
140
|
+
* the same proxy `incanto-editor` ships, so the editor's 📚 works identically
|
|
141
|
+
* whether it opened as a page or over a running game.
|
|
142
|
+
*
|
|
143
|
+
* DEV ONLY (vite `configureServer`): nothing about it reaches a build, and the
|
|
144
|
+
* token stays in the terminal that started the server.
|
|
145
|
+
*
|
|
146
|
+
* ```ts
|
|
147
|
+
* // vite.config.ts
|
|
148
|
+
* import { incantoLibrary, incantoScenes } from 'incanto/vite';
|
|
149
|
+
* export default defineConfig({ plugins: [incantoScenes(), incantoLibrary()] });
|
|
150
|
+
* ```
|
|
151
|
+
* ```ts
|
|
152
|
+
* // main.ts — the editor then offers the library
|
|
153
|
+
* createGame3D({ ..., debug: DEV, editor: { library: true } });
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
declare function incantoLibrary(opts?: IncantoLibraryOptions): {
|
|
157
|
+
name: string;
|
|
158
|
+
apply: "serve";
|
|
159
|
+
configureServer(server: DevServer): void;
|
|
160
|
+
};
|
|
161
|
+
/** @internal Exported for tests — the whole request/response behaviour. */
|
|
162
|
+
declare function serveLibrary(req: LibraryReq, res: LibraryRes, opts?: IncantoLibraryOptions, doFetch?: typeof fetch): Promise<void>;
|
|
25
163
|
//#endregion
|
|
26
|
-
export { IncantoScenesOptions, incantoScenes };
|
|
164
|
+
export { IncantoLibraryOptions, IncantoScenesOptions, ProjectFileEntry, discoverScenes, incantoLibrary, incantoScenes, resolveSceneFile, sceneFacts, serveLibrary, serveSceneFile, serveSceneList };
|