incanto 0.56.0 → 0.58.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 +6 -4
- package/bin/incanto-check.mjs +27 -0
- package/bin/incanto-new.mjs +29 -7
- package/bin/incanto-playtest.mjs +28 -2
- package/bin/incanto-verify.mjs +96 -17
- package/bin/incanto.mjs +106 -0
- package/dist/2d.d.ts +59 -7
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +34 -4
- package/dist/3d.js +5 -5
- package/dist/{behavior-CyQoSu4n.d.ts → behavior-l08AEbq9.d.ts} +5 -0
- package/dist/{create-game-YvaNWhe6.js → create-game-BZwWJIns.js} +146 -7
- package/dist/{create-game-CqJkfMzm.js → create-game-D10bU5_J.js} +83 -78
- package/dist/debug.d.ts +1 -1
- package/dist/{duplicate-MNLMAcbz.js → duplicate-BPLZDZpd.js} +1 -1
- package/dist/{environment-presets-DLHqgNAF.js → environment-presets-CvvQr_bJ.js} +106 -24
- package/dist/{frame-report-njybhZon.js → frame-report-BSMny7oe.js} +1 -1
- package/dist/{frame-report-DZ70IY26.d.ts → frame-report-DCnHFmto.d.ts} +1 -1
- package/dist/{gameplay-C_SPzmUe.js → gameplay-BVphcxmE.js} +89 -13
- package/dist/gameplay.d.ts +26 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +45 -5
- package/dist/index.js +8 -9
- package/dist/{loader-DhI1jFW_.d.ts → loader-BbEMTuWg.d.ts} +1 -1
- package/dist/{loader-BTkHYrQn.js → loader-BcrRSjxB.js} +682 -682
- package/dist/net.d.ts +2 -2
- package/dist/net.js +1 -1
- package/dist/{pathfinding-CXGCpRQe.d.ts → pathfinding-mEN4V1CU.d.ts} +1 -1
- package/dist/{physics-2d-CfWAggJ1.js → physics-2d-EqA-jddf.js} +4 -3
- package/dist/{physics-3d-NzDwWPrF.js → physics-3d-Dnz4fsXX.js} +5 -4
- package/dist/quiet-rapier-BAJ4K94N.js +46 -0
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-en63AEZO.js → register-C6ZBFRjd.js} +63 -60
- package/dist/{register-p48lHE2o.js → register-Ch70uByv.js} +1615 -1502
- package/dist/{replay-ePMz26jw.d.ts → replay-Dw6gMlYA.d.ts} +1 -1
- package/dist/{replay-t1pP0gQg.js → replay-s7I2GstT.js} +31 -11
- package/dist/sheet-grid-BT6N_Bjs.js +59 -0
- package/dist/{split-screen-BsdOHbzP.d.ts → split-screen-B0baBwxI.d.ts} +5 -1
- package/dist/{split-screen-CSb_uZ6W.js → split-screen-DLsUrleX.js} +14 -3
- package/dist/{sprite-animation-7qvUxF6Z.js → sprite-animation-C0wXLBZJ.js} +8 -4
- package/dist/{src-51GA-9m-.js → src-C1J09Op6.js} +55 -2
- package/dist/{teardown-C7uVSJvx.js → teardown-Cs113S9F.js} +74 -2
- package/dist/{test-Bu6SPNsV.js → test-ZBga8kQ9.js} +60 -25
- package/dist/test.d.ts +5 -5
- package/dist/test.js +2 -2
- package/dist/{touch-BnMyy9tr.js → touch-DESwnpOc.js} +173 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +3 -3
- package/editor/assets/{agent8-X0Nesj-k.js → agent8-BQQjE9UQ.js} +1 -1
- package/editor/assets/{debug-TXqu-ftX.js → debug-C9UCsXBS.js} +1 -1
- package/editor/assets/{index-DTdwKKIv.js → index-CBgfM3WD.js} +62 -62
- package/editor/index.html +1 -1
- package/package.json +2 -1
- package/skills/incanto-assets.md +5 -1
- package/skills/incanto-audio.md +7 -1
- package/skills/incanto-building-2d-games.md +13 -0
- package/skills/incanto-building-3d-games.md +3 -3
- package/skills/incanto-gameplay-behaviors.md +2 -1
- package/skills/incanto-hud.md +6 -0
- package/skills/incanto-performance.md +5 -0
- package/skills/incanto-physics-and-input.md +14 -0
- package/skills/incanto-playtesting.md +19 -5
- package/skills/incanto-save-slots.md +2 -1
- package/skills/incanto-verifying-your-game.md +15 -4
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/game.scene.json +7 -6
- package/templates-app/platformer-2d/PROJECT/Context.md +70 -0
- package/templates-app/platformer-2d/PROJECT/Requirements.md +63 -0
- package/templates-app/platformer-2d/PROJECT/Status.md +60 -0
- package/templates-app/platformer-2d/PROJECT/Structure.md +77 -0
- package/templates-app/platformer-2d/docs/project-2d-rules.md +61 -0
- package/templates-app/platformer-2d/index.html +99 -0
- package/templates-app/platformer-2d/package.json +23 -0
- package/templates-app/platformer-2d/src/behaviors.ts +541 -0
- package/templates-app/platformer-2d/src/game.scene.json +2061 -0
- package/templates-app/platformer-2d/src/main.ts +68 -0
- package/templates-app/platformer-2d/tsconfig.json +13 -0
- package/templates-app/platformer-2d/verify.ts +275 -0
- package/templates-app/platformer-2d/vite.config.ts +12 -0
- package/templates-app/star-survivor/PROJECT/Context.md +55 -0
- package/templates-app/star-survivor/PROJECT/Requirements.md +47 -0
- package/templates-app/star-survivor/PROJECT/Status.md +44 -0
- package/templates-app/star-survivor/PROJECT/Structure.md +63 -0
- package/templates-app/star-survivor/docs/project-2d-rules.md +53 -0
- package/templates-app/star-survivor/index.html +232 -0
- package/templates-app/star-survivor/package.json +23 -0
- package/templates-app/star-survivor/src/behaviors.ts +624 -0
- package/templates-app/star-survivor/src/game.scene.json +464 -0
- package/templates-app/star-survivor/src/main.ts +49 -0
- package/templates-app/star-survivor/tsconfig.json +13 -0
- package/templates-app/star-survivor/verify.ts +193 -0
- package/templates-app/star-survivor/vite.config.ts +12 -0
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/game.scene.json +6 -3
- package/templates-app/tps-3d/verify.ts +17 -1
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/src/grove.scene.json +14 -13
- package/templates-app/village-quest-3d/src/village.scene.json +5 -5
- package/dist/log-report-CPFm4OXf.js +0 -173
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { E as behaviorSchema, w as parseNodePath } from "./loader-
|
|
2
|
-
import {
|
|
3
|
-
import { i as getNodeSchema } from "./registry-C7u42TID.js";
|
|
1
|
+
import { E as behaviorSchema, w as parseNodePath } from "./loader-BcrRSjxB.js";
|
|
2
|
+
import { D as HudWidgetBase, F as translationKey } from "./register-Ch70uByv.js";
|
|
3
|
+
import { i as getNodeSchema, o as getNodeType } from "./registry-C7u42TID.js";
|
|
4
4
|
//#region src/core/node-refs.ts
|
|
5
5
|
/**
|
|
6
6
|
* Node-path PROPS, resolved against scene JSON.
|
|
@@ -211,13 +211,33 @@ const BODY_TYPES = new Set([
|
|
|
211
211
|
"Area3D",
|
|
212
212
|
"CharacterBody3D"
|
|
213
213
|
]);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Is this node type a HUD widget — one that renders NOTHING outside a HudLayer?
|
|
216
|
+
*
|
|
217
|
+
* This used to be a hand-written list of five, and the kit has sixteen. It went
|
|
218
|
+
* stale the moment UiPanel landed, so the settings menu in the performance
|
|
219
|
+
* skill and the volume menu in the audio skill — both bare `UiPanel`s of
|
|
220
|
+
* uncovered widgets — drew a blank screen while `incanto-check` printed `ok`.
|
|
221
|
+
* The runtime is silent by design (`hud.ts`: `if (!host) return;`), which is
|
|
222
|
+
* exactly why the lint has to be right.
|
|
223
|
+
*
|
|
224
|
+
* Derived from the class hierarchy instead: anything descending from
|
|
225
|
+
* `HudWidgetBase` is one, whatever it is called and whenever it was added.
|
|
226
|
+
*/
|
|
227
|
+
function isHudWidget(type) {
|
|
228
|
+
if (type.startsWith("Ui")) return true;
|
|
229
|
+
let ctor;
|
|
230
|
+
try {
|
|
231
|
+
ctor = getNodeType(type);
|
|
232
|
+
} catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
for (let c = ctor; c; c = Object.getPrototypeOf(c)) {
|
|
236
|
+
if (c.typeName === "HudWidgetBase") return true;
|
|
237
|
+
if (c === HudWidgetBase) return true;
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
221
241
|
const LIGHT_TYPES = new Set([
|
|
222
242
|
"DirectionalLight3D",
|
|
223
243
|
"OmniLight3D",
|
|
@@ -269,7 +289,7 @@ function auditScene(scene) {
|
|
|
269
289
|
const collider = props.collider;
|
|
270
290
|
if (!collider || collider.shape === void 0) warnings.push(`${path}: ${type} has no collider — physics will skip it (add "collider": { "shape": ... }).`);
|
|
271
291
|
}
|
|
272
|
-
if (
|
|
292
|
+
if (isHudWidget(type) && !insideHud) warnings.push(`${path}: ${type} works only as a descendant of a HudLayer — it will not render.`);
|
|
273
293
|
if (staticRoot && ANIMATED_TYPES.has(type)) warnings.push(`${path}: ${type} is frozen inside static subtree '${staticRoot}' — it will not animate/update on screen.`);
|
|
274
294
|
const nextStatic = staticRoot ?? (props.static === true ? node.name ?? path : null);
|
|
275
295
|
const nextHud = insideHud || type === "HudLayer";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//#region src/core/sheet-grid.ts
|
|
2
|
+
/**
|
|
3
|
+
* Does this spritesheet's grid actually fit the image?
|
|
4
|
+
*
|
|
5
|
+
* `frameWidth`/`frameHeight` are taken on trust, and the frame math floors the
|
|
6
|
+
* division — so a frame size that does not divide the sheet slices every row a
|
|
7
|
+
* little further off, and an animation naming a frame past the end of the grid
|
|
8
|
+
* reads whatever texels are there. Both draw WRONG ART with no error anywhere,
|
|
9
|
+
* which is the one symptom an author cannot tell apart from having picked the
|
|
10
|
+
* wrong sheet in the first place.
|
|
11
|
+
*
|
|
12
|
+
* Pure arithmetic on numbers that are known the moment the image loads, so it
|
|
13
|
+
* costs nothing and it is exact.
|
|
14
|
+
*
|
|
15
|
+
* It lives in core because THREE nodes do this division — AnimatedSprite2D,
|
|
16
|
+
* AnimatedSprite3D and TileMap2D — and for a while the checker sat in `2d/`
|
|
17
|
+
* where only one of them could see it. A checker one directory away from the
|
|
18
|
+
* bug it catches is the same as no checker.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Frame sizes that divide `total` cleanly, biggest first.
|
|
22
|
+
*
|
|
23
|
+
* Bounded to grids of 2–32 cells so the suggestion is a frame size someone
|
|
24
|
+
* might actually have meant, not every factor of the number.
|
|
25
|
+
*/
|
|
26
|
+
function divisors(total) {
|
|
27
|
+
const out = [];
|
|
28
|
+
for (let cells = 2; cells <= 32; cells++) if (total % cells === 0) out.push(total / cells);
|
|
29
|
+
return out.sort((a, b) => b - a).slice(0, 6);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The problem with this grid, or null when there is none.
|
|
33
|
+
*
|
|
34
|
+
* `highestFrame` is the largest frame index the scene asks for, when it is
|
|
35
|
+
* known — an animation range that runs off the end is the other half of the
|
|
36
|
+
* same mistake.
|
|
37
|
+
*/
|
|
38
|
+
function checkSheetGrid(key, frameWidth, frameHeight, imageWidth, imageHeight, highestFrame) {
|
|
39
|
+
if (!imageWidth || !imageHeight || !frameWidth || !frameHeight) return null;
|
|
40
|
+
if (imageWidth % frameWidth !== 0 || imageHeight % frameHeight !== 0) {
|
|
41
|
+
const parts = [];
|
|
42
|
+
if (imageWidth % frameWidth !== 0) {
|
|
43
|
+
const fits = divisors(imageWidth);
|
|
44
|
+
parts.push(`${imageWidth} px wide is not a whole number of ${frameWidth} px frames` + (fits.length > 0 ? ` (${fits.join(", ")} would divide it)` : ""));
|
|
45
|
+
}
|
|
46
|
+
if (imageHeight % frameHeight !== 0) {
|
|
47
|
+
const fits = divisors(imageHeight);
|
|
48
|
+
parts.push(`${imageHeight} px tall is not a whole number of ${frameHeight} px frames` + (fits.length > 0 ? ` (${fits.join(", ")} would divide it)` : ""));
|
|
49
|
+
}
|
|
50
|
+
return `[incanto] spritesheet '${key}' does not fit its frame size: ${parts.join("; ")}. Every row after the first is cut off centre.`;
|
|
51
|
+
}
|
|
52
|
+
if (highestFrame !== void 0) {
|
|
53
|
+
const frames = imageWidth / frameWidth * (imageHeight / frameHeight);
|
|
54
|
+
if (highestFrame >= frames) return `[incanto] spritesheet '${key}' has ${frames} frames (0–${frames - 1}), and an animation asks for frame ${highestFrame}. Those frames draw whatever is at the wrong end of the sheet.`;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { checkSheetGrid as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { At as Node, It as Signal, b as Engine } from "./behavior-
|
|
1
|
+
import { At as Node, It as Signal, b as Engine } from "./behavior-l08AEbq9.js";
|
|
2
2
|
import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
|
|
3
3
|
|
|
4
4
|
//#region src/net/types.d.ts
|
|
@@ -317,6 +317,10 @@ declare class NetworkManager {
|
|
|
317
317
|
private readonly latestGlobalCollections;
|
|
318
318
|
private readonly scenes;
|
|
319
319
|
private readonly lastSent;
|
|
320
|
+
/** Sync keys already reported unreadable, so a per-frame send says it once. */
|
|
321
|
+
private readonly reportedDeadKeys;
|
|
322
|
+
/** Name a sync key the owner cannot read — a typo in the prop or the path. */
|
|
323
|
+
private reportDeadSyncKey;
|
|
320
324
|
private sendAccumulator;
|
|
321
325
|
private detachReplication;
|
|
322
326
|
private lastOwner;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as diagnose, M as Signal,
|
|
2
|
-
import {
|
|
1
|
+
import { C as diagnose, M as Signal, S as Node, n as loadScene, t as buildNodeJson } from "./loader-BcrRSjxB.js";
|
|
2
|
+
import { _ as Engine, t as registerCoreNodes } from "./register-Ch70uByv.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-C7u42TID.js";
|
|
@@ -811,6 +811,14 @@ var NetworkManager = class NetworkManager {
|
|
|
811
811
|
latestGlobalCollections = /* @__PURE__ */ new Map();
|
|
812
812
|
scenes = /* @__PURE__ */ new Map();
|
|
813
813
|
lastSent = /* @__PURE__ */ new Map();
|
|
814
|
+
/** Sync keys already reported unreadable, so a per-frame send says it once. */
|
|
815
|
+
reportedDeadKeys = /* @__PURE__ */ new Set();
|
|
816
|
+
/** Name a sync key the owner cannot read — a typo in the prop or the path. */
|
|
817
|
+
reportDeadSyncKey(owner, key) {
|
|
818
|
+
if (this.reportedDeadKeys.has(key)) return;
|
|
819
|
+
this.reportedDeadKeys.add(key);
|
|
820
|
+
diagnose(owner.tree?.engine ?? null, "error", `[incanto] ${owner.getPath()}: sync key '${key}' reads as undefined — it is never sent, so remote players never see it change. Check the prop name and the node path.`);
|
|
821
|
+
}
|
|
814
822
|
sendAccumulator = 0;
|
|
815
823
|
detachReplication = null;
|
|
816
824
|
lastOwner = null;
|
|
@@ -1000,7 +1008,10 @@ var NetworkManager = class NetworkManager {
|
|
|
1000
1008
|
const patch = {};
|
|
1001
1009
|
for (const key of syncKeys) {
|
|
1002
1010
|
const value = readSyncKey(owner, key);
|
|
1003
|
-
if (value === void 0)
|
|
1011
|
+
if (value === void 0) {
|
|
1012
|
+
this.reportDeadSyncKey(owner, key);
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1004
1015
|
if (!jsonEquals(this.lastSent.get(key) ?? null, value)) patch[key] = jsonClone(value);
|
|
1005
1016
|
}
|
|
1006
1017
|
if (this.sendAccumulator < window) return;
|
|
@@ -306,10 +306,14 @@ function resolveAnimation(animations, name, owner) {
|
|
|
306
306
|
const via = seen.length > 0 ? ` (via ${seen.join(" → ")})` : "";
|
|
307
307
|
throw new IncantoError("UNKNOWN_ANIMATION", `No animation '${key}' on '${owner}'${via}. Available: [${Object.keys(animations).join(", ")}].`);
|
|
308
308
|
}
|
|
309
|
-
if (typeof entry !== "string")
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
309
|
+
if (typeof entry !== "string") {
|
|
310
|
+
const frames = resolveFrames(entry.frames, key);
|
|
311
|
+
if (frames.length > 1 && !(Number.isFinite(entry.fps) && entry.fps > 0)) throw new IncantoError("BAD_FORMAT", `Animation '${key}' on '${owner}' has ${frames.length} frames and no "fps" — it would sit on the first one forever. Add "fps" (the field is fps, not frameRate or speed).`);
|
|
312
|
+
return {
|
|
313
|
+
name: key,
|
|
314
|
+
def: entry
|
|
315
|
+
};
|
|
316
|
+
}
|
|
313
317
|
if (seen.includes(key)) throw new IncantoError("UNKNOWN_ANIMATION", `Animation aliases on '${owner}' point in a circle: ${[...seen, key].join(" → ")}.`);
|
|
314
318
|
seen.push(key);
|
|
315
319
|
key = entry;
|
|
@@ -143,6 +143,59 @@ async function preloadUrls(urls, onProgress) {
|
|
|
143
143
|
function assetUrls(assets) {
|
|
144
144
|
return Object.values(assets ?? {}).map((a) => a.url).filter((u) => typeof u === "string" && !u.startsWith("data:"));
|
|
145
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* The standard boot screen every template hand-rolled (~125 identical lines
|
|
148
|
+
* of HTML+CSS): black overlay, "Loading" title, progress track + percent.
|
|
149
|
+
* Headless it returns inert no-ops.
|
|
150
|
+
*/
|
|
151
|
+
function createLoadingOverlay(title = "Loading") {
|
|
152
|
+
if (typeof document === "undefined") return {
|
|
153
|
+
setProgress: () => {},
|
|
154
|
+
remove: () => {}
|
|
155
|
+
};
|
|
156
|
+
const wrap = document.createElement("div");
|
|
157
|
+
wrap.style.cssText = "position:fixed;inset:0;z-index:10000;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#000;color:#fff;font:600 15px system-ui, -apple-system, 'Segoe UI', sans-serif;transition:opacity .3s;";
|
|
158
|
+
const h = document.createElement("div");
|
|
159
|
+
h.textContent = title;
|
|
160
|
+
h.style.cssText = "font-size:1.5rem;font-weight:700;margin-bottom:1rem;";
|
|
161
|
+
const track = document.createElement("div");
|
|
162
|
+
track.style.cssText = "width:256px;height:8px;background:#374151;border-radius:9999px;overflow:hidden;";
|
|
163
|
+
const fill = document.createElement("div");
|
|
164
|
+
fill.style.cssText = "width:0%;height:100%;background:#3b82f6;transition:width .3s;";
|
|
165
|
+
track.appendChild(fill);
|
|
166
|
+
const pct = document.createElement("div");
|
|
167
|
+
pct.textContent = "0%";
|
|
168
|
+
pct.style.cssText = "margin-top:.5rem;font-size:.9rem;";
|
|
169
|
+
wrap.append(h, track, pct);
|
|
170
|
+
document.body.appendChild(wrap);
|
|
171
|
+
let removed = false;
|
|
172
|
+
return {
|
|
173
|
+
setProgress(done, total) {
|
|
174
|
+
const p = total > 0 ? Math.min(100, Math.round(done / total * 100)) : 100;
|
|
175
|
+
fill.style.width = `${p}%`;
|
|
176
|
+
pct.textContent = `${p}%`;
|
|
177
|
+
},
|
|
178
|
+
remove() {
|
|
179
|
+
if (removed) return;
|
|
180
|
+
removed = true;
|
|
181
|
+
wrap.style.opacity = "0";
|
|
182
|
+
setTimeout(() => wrap.remove(), 320);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Preload every scene-asset URL behind the standard overlay, then remove it:
|
|
188
|
+
*
|
|
189
|
+
* await preloadSceneAssets(sceneJson.assets);
|
|
190
|
+
*/
|
|
191
|
+
async function preloadSceneAssets(assets, title) {
|
|
192
|
+
const overlay = createLoadingOverlay(title);
|
|
193
|
+
try {
|
|
194
|
+
await preloadUrls(assetUrls(assets), (done, total) => overlay.setProgress(done, total));
|
|
195
|
+
} finally {
|
|
196
|
+
overlay.remove();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
146
199
|
//#endregion
|
|
147
200
|
//#region src/core/uid-gen.ts
|
|
148
201
|
/**
|
|
@@ -161,6 +214,6 @@ function newUid() {
|
|
|
161
214
|
//#endregion
|
|
162
215
|
//#region src/index.ts
|
|
163
216
|
/** Engine version. Kept in sync with package.json by the release pipeline. */
|
|
164
|
-
const VERSION = "0.
|
|
217
|
+
const VERSION = "0.58.0";
|
|
165
218
|
//#endregion
|
|
166
|
-
export {
|
|
219
|
+
export { preloadUrls as a, preloadSceneAssets as i, newUid as n, findPath as o, assetUrls as r, gridFromRows as s, VERSION as t };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { g as AudioPlayer } from "./register-Ch70uByv.js";
|
|
2
|
+
import { a as logReport, s as parseDrive } from "./touch-DESwnpOc.js";
|
|
3
|
+
import { a as frameSignature, i as frameImage, o as frameStats } from "./frame-report-BSMny7oe.js";
|
|
2
4
|
//#region src/core/audio-errors.ts
|
|
3
5
|
/** Every AudioPlayer under `root` whose source could not be played. */
|
|
4
6
|
function audioErrors(root) {
|
|
@@ -27,6 +29,76 @@ function claimCanvasGestures(canvas) {
|
|
|
27
29
|
style.webkitTapHighlightColor = "transparent";
|
|
28
30
|
}
|
|
29
31
|
//#endregion
|
|
32
|
+
//#region src/core/dev-channel.ts
|
|
33
|
+
/**
|
|
34
|
+
* Answer the dev server's frame/log requests. Returns the unsubscribes, or an
|
|
35
|
+
* empty list outside a dev server.
|
|
36
|
+
*/
|
|
37
|
+
function wireDevChannel(opts) {
|
|
38
|
+
const hot = import.meta.hot;
|
|
39
|
+
const cleanups = [];
|
|
40
|
+
if (!hot) return cleanups;
|
|
41
|
+
const onFrameRequest = (data) => {
|
|
42
|
+
const request = data;
|
|
43
|
+
const id = request?.id;
|
|
44
|
+
hot.send("incanto:frame-ack", { id });
|
|
45
|
+
const drive = request?.drive ? parseDrive(request.drive) : null;
|
|
46
|
+
if (drive?.error) {
|
|
47
|
+
hot.send("incanto:frame-report", {
|
|
48
|
+
id,
|
|
49
|
+
report: { error: drive.error }
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let droveFrames = null;
|
|
54
|
+
(drive ? opts.applyDrive(drive.steps) : Promise.resolve(null)).then((frames) => {
|
|
55
|
+
droveFrames = frames;
|
|
56
|
+
return opts.captureFramePixels();
|
|
57
|
+
}).then((shot) => hot.send("incanto:frame-report", {
|
|
58
|
+
id,
|
|
59
|
+
report: {
|
|
60
|
+
...frameStats(shot.pixels, shot.width, shot.height, { grid: request?.grid }),
|
|
61
|
+
signature: frameSignature(shot.pixels, shot.width, shot.height),
|
|
62
|
+
...droveFrames === null ? {} : { droveFrames },
|
|
63
|
+
...request?.image ? { image: frameImage(shot.pixels, shot.width, shot.height, (w, h) => Object.assign(document.createElement("canvas"), {
|
|
64
|
+
width: w,
|
|
65
|
+
height: h
|
|
66
|
+
}), request.image) } : {}
|
|
67
|
+
}
|
|
68
|
+
})).catch((error) => hot.send("incanto:frame-report", {
|
|
69
|
+
id,
|
|
70
|
+
report: { error: error instanceof Error ? error.message : String(error) }
|
|
71
|
+
}));
|
|
72
|
+
};
|
|
73
|
+
hot.on("incanto:frame-request", onFrameRequest);
|
|
74
|
+
cleanups.push(() => hot.off?.("incanto:frame-request", onFrameRequest));
|
|
75
|
+
const onLogsRequest = (data) => {
|
|
76
|
+
const id = data?.id;
|
|
77
|
+
try {
|
|
78
|
+
hot.send("incanto:logs-report", {
|
|
79
|
+
id,
|
|
80
|
+
report: logReport({
|
|
81
|
+
entries: opts.engine.log.entries(),
|
|
82
|
+
stats: {
|
|
83
|
+
...opts.engine.stats(),
|
|
84
|
+
...opts.rendererStats?.() ?? {}
|
|
85
|
+
},
|
|
86
|
+
assetErrors: opts.assetErrors?.() ?? [],
|
|
87
|
+
hidden: typeof document !== "undefined" && document.hidden
|
|
88
|
+
})
|
|
89
|
+
});
|
|
90
|
+
} catch (error) {
|
|
91
|
+
hot.send("incanto:logs-report", {
|
|
92
|
+
id,
|
|
93
|
+
report: { error: error instanceof Error ? error.message : String(error) }
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
hot.on("incanto:logs-request", onLogsRequest);
|
|
98
|
+
cleanups.push(() => hot.off?.("incanto:logs-request", onLogsRequest));
|
|
99
|
+
return cleanups;
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
30
102
|
//#region src/core/editor-switch.ts
|
|
31
103
|
/**
|
|
32
104
|
* Cover the page while it changes what it is.
|
|
@@ -324,4 +396,4 @@ function defaultReport(what, error) {
|
|
|
324
396
|
console.error(`[incanto] teardown: ${what} failed (continuing)`, error);
|
|
325
397
|
}
|
|
326
398
|
//#endregion
|
|
327
|
-
export { openBundledEditor as a,
|
|
399
|
+
export { openBundledEditor as a, claimCanvasGestures as c, devServerLibrary as i, audioErrors as l, pauseWhenHidden as n, poseFromRenderer as o, crossFade as r, wireDevChannel as s, teardown as t };
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { A as registerBehavior, n as loadScene, s as resolveViewport } from "./loader-
|
|
2
|
-
import {
|
|
1
|
+
import { A as registerBehavior, n as loadScene, s as resolveViewport } from "./loader-BcrRSjxB.js";
|
|
2
|
+
import { _ as Engine } from "./register-Ch70uByv.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 { i as getNodeSchema, s as mergeStaticProps } from "./registry-C7u42TID.js";
|
|
6
|
-
import { n as startRecording } from "./replay-
|
|
7
|
-
import { n as registerGameplayBehaviors } from "./gameplay-
|
|
8
|
-
import { t as registerNodes2D } from "./register-
|
|
9
|
-
import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-
|
|
10
|
-
import { n as registerNodesNet, t as createSplitScreen } from "./split-screen-
|
|
6
|
+
import { n as startRecording } from "./replay-s7I2GstT.js";
|
|
7
|
+
import { n as registerGameplayBehaviors } from "./gameplay-BVphcxmE.js";
|
|
8
|
+
import { t as registerNodes2D } from "./register-C6ZBFRjd.js";
|
|
9
|
+
import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-CvvQr_bJ.js";
|
|
10
|
+
import { n as registerNodesNet, t as createSplitScreen } from "./split-screen-DLsUrleX.js";
|
|
11
11
|
import { Box3, Euler, Matrix4, PerspectiveCamera, Quaternion, Vector3 } from "three";
|
|
12
12
|
//#region src/test/framing.ts
|
|
13
13
|
/**
|
|
@@ -474,6 +474,24 @@ function framingText(report) {
|
|
|
474
474
|
const FALL_2D_PX = 1e3;
|
|
475
475
|
/** "Reached it", in pixels — one 32 px tile, next to a ~34 px character. */
|
|
476
476
|
const REACH_2D_PX = 32;
|
|
477
|
+
/**
|
|
478
|
+
* How hard this scene pulls, in its own units — 0 only for a scene that says so.
|
|
479
|
+
*
|
|
480
|
+
* Gravity is what makes "left the world downward" mean anything, and a
|
|
481
|
+
* top-down game has none. But ABSENT is not ZERO: the physics adapters default
|
|
482
|
+
* an undeclared scene to [0, 980] px/s² in 2D and [0, -9.81, 0] m/s² in 3D, so
|
|
483
|
+
* a platformer that never wrote a physics header still falls, and reading
|
|
484
|
+
* absent as zero would have stopped reporting the pit it fell into.
|
|
485
|
+
*/
|
|
486
|
+
function gravityMagnitude(physics) {
|
|
487
|
+
const g = physics?.gravity;
|
|
488
|
+
if (g === void 0) return 1;
|
|
489
|
+
if (!Array.isArray(g)) return 1;
|
|
490
|
+
return g.reduce((sum, v) => sum + (typeof v === "number" ? Math.abs(v) : 0), 0);
|
|
491
|
+
}
|
|
492
|
+
/** Furthest the player ever got from its spawn, below which it went nowhere. */
|
|
493
|
+
const STUCK_3D_M = 3;
|
|
494
|
+
const STUCK_2D_PX = 96;
|
|
477
495
|
const CONTROLLERS = new Set(["CharacterController2D", "CharacterController3D"]);
|
|
478
496
|
/**
|
|
479
497
|
* Who the driver is playing as.
|
|
@@ -718,7 +736,8 @@ async function runOnce(json, seed, opts) {
|
|
|
718
736
|
0
|
|
719
737
|
];
|
|
720
738
|
const down = scene.dimension === "2d" ? 1 : -1;
|
|
721
|
-
const
|
|
739
|
+
const pull = gravityMagnitude(scene.physics);
|
|
740
|
+
const floor = opts.fallBelow ?? (pull > 0 ? start[1] + down * (down > 0 ? FALL_2D_PX : 50) : null);
|
|
722
741
|
const radius = opts.reachRadius ?? (down > 0 ? REACH_2D_PX : 2);
|
|
723
742
|
const recorder = startRecording(engine);
|
|
724
743
|
const rand = () => engine.rng.next();
|
|
@@ -726,10 +745,12 @@ async function runOnce(json, seed, opts) {
|
|
|
726
745
|
const reached = /* @__PURE__ */ new Set();
|
|
727
746
|
const stepMs = 1e3 / 60;
|
|
728
747
|
const deadline = (opts.seconds ?? 60) * 1e3;
|
|
729
|
-
let outcome =
|
|
748
|
+
let outcome = null;
|
|
730
749
|
let at = start;
|
|
750
|
+
let wentAsFarAs = 0;
|
|
731
751
|
while (session.timeMs < deadline) {
|
|
732
752
|
at = player ? worldOf(player) : start;
|
|
753
|
+
wentAsFarAs = Math.max(wentAsFarAs, Math.hypot(at[0] - start[0], at[2] - start[2]));
|
|
733
754
|
driver.step(stepMs / 1e3, at);
|
|
734
755
|
session.step(stepMs);
|
|
735
756
|
if (player) for (const [path, node] of targetNodes) {
|
|
@@ -746,15 +767,17 @@ async function runOnce(json, seed, opts) {
|
|
|
746
767
|
outcome = "error";
|
|
747
768
|
break;
|
|
748
769
|
}
|
|
749
|
-
if (player && (at[1] - floor) * down > 0) {
|
|
770
|
+
if (player && floor !== null && (at[1] - floor) * down > 0) {
|
|
750
771
|
outcome = "fell";
|
|
751
772
|
break;
|
|
752
773
|
}
|
|
753
774
|
}
|
|
754
775
|
driver.release();
|
|
776
|
+
const WENT_NOWHERE = scene.dimension === "2d" ? STUCK_2D_PX : STUCK_3D_M;
|
|
777
|
+
outcome ??= wentAsFarAs > WENT_NOWHERE ? "unfinished" : "stuck";
|
|
755
778
|
const heard = engine.audio.recent().map((e) => e.from);
|
|
756
779
|
const shown = engine.effects.recent().map((e) => e.from);
|
|
757
|
-
const bad = outcome !== "won";
|
|
780
|
+
const bad = outcome !== "won" && outcome !== "unfinished";
|
|
758
781
|
const run = {
|
|
759
782
|
seed,
|
|
760
783
|
outcome,
|
|
@@ -857,12 +880,15 @@ function playtestText(report) {
|
|
|
857
880
|
"error",
|
|
858
881
|
"fell",
|
|
859
882
|
"lost",
|
|
860
|
-
"stuck"
|
|
883
|
+
"stuck",
|
|
884
|
+
"unfinished"
|
|
861
885
|
]) {
|
|
862
886
|
const hits = runs.filter((r) => r.outcome === bad);
|
|
863
887
|
if (hits.length === 0) continue;
|
|
864
888
|
const where = bad === "fell" ? ` (last at y=${hits[0].endedAt[1].toFixed(1)})` : "";
|
|
865
|
-
|
|
889
|
+
const mark = bad === "unfinished" ? "·" : "✗";
|
|
890
|
+
const what = bad === "unfinished" ? "ran out of time still playing" : `${bad} in ${pct(hits.length, total)}`;
|
|
891
|
+
lines.push(bad === "unfinished" ? ` ${mark} ${pct(hits.length, total)} ${what}` : ` ${mark} ${what}${where}`);
|
|
866
892
|
}
|
|
867
893
|
const everReached = new Set(runs.flatMap((r) => r.reached));
|
|
868
894
|
const missed = targets.filter((path) => !everReached.has(path));
|
|
@@ -1016,6 +1042,10 @@ function facingText(report) {
|
|
|
1016
1042
|
const STEP_MS = 1e3 / 60;
|
|
1017
1043
|
/** Drop height for the buffer probe — long enough to outlast a coyote window. */
|
|
1018
1044
|
const DROP = 12;
|
|
1045
|
+
/** Metres a 3D probe is lifted/dropped from. */
|
|
1046
|
+
const UNIT_LIFT = 40;
|
|
1047
|
+
/** Pixels a 2D probe is lifted/dropped from — a 2D world is ~30x the numbers. */
|
|
1048
|
+
const PX_LIFT = 1200;
|
|
1019
1049
|
function positionOf(node) {
|
|
1020
1050
|
const p = node.position;
|
|
1021
1051
|
return [
|
|
@@ -1062,11 +1092,14 @@ async function makeProbe(json, opts) {
|
|
|
1062
1092
|
jump
|
|
1063
1093
|
};
|
|
1064
1094
|
}
|
|
1095
|
+
const is2d = session.scene.dimension === "2d";
|
|
1065
1096
|
return {
|
|
1066
1097
|
probe: {
|
|
1067
1098
|
session,
|
|
1068
1099
|
engine: session.engine,
|
|
1069
1100
|
player,
|
|
1101
|
+
up: is2d ? -1 : 1,
|
|
1102
|
+
lift: is2d ? PX_LIFT : UNIT_LIFT,
|
|
1070
1103
|
step: (frames = 1) => session.step(frames * STEP_MS),
|
|
1071
1104
|
pos: () => positionOf(player),
|
|
1072
1105
|
vel: () => velocityOf(player),
|
|
@@ -1159,7 +1192,7 @@ function jumpShape(probe, jump) {
|
|
|
1159
1192
|
let airtime = null;
|
|
1160
1193
|
for (let f = 2; f <= 600; f++) {
|
|
1161
1194
|
probe.step();
|
|
1162
|
-
const h = probe.pos()[1] - floor;
|
|
1195
|
+
const h = (probe.pos()[1] - floor) * probe.up;
|
|
1163
1196
|
if (h > apex) {
|
|
1164
1197
|
apex = h;
|
|
1165
1198
|
toApex = f;
|
|
@@ -1169,7 +1202,8 @@ function jumpShape(probe, jump) {
|
|
|
1169
1202
|
break;
|
|
1170
1203
|
}
|
|
1171
1204
|
}
|
|
1172
|
-
|
|
1205
|
+
const noise = probe.up > 0 ? .02 : 2;
|
|
1206
|
+
if (apex <= noise) return {
|
|
1173
1207
|
apex: null,
|
|
1174
1208
|
toApexMs: null,
|
|
1175
1209
|
airtimeMs: null
|
|
@@ -1197,7 +1231,7 @@ async function measureCoyote(json, opts, jump) {
|
|
|
1197
1231
|
const p = probe.pos();
|
|
1198
1232
|
probe.player.position = [
|
|
1199
1233
|
p[0],
|
|
1200
|
-
p[1] +
|
|
1234
|
+
p[1] + probe.lift * probe.up,
|
|
1201
1235
|
p[2]
|
|
1202
1236
|
];
|
|
1203
1237
|
probe.step(delayFrames);
|
|
@@ -1206,7 +1240,7 @@ async function measureCoyote(json, opts, jump) {
|
|
|
1206
1240
|
probe.step();
|
|
1207
1241
|
const after = probe.vel()[1];
|
|
1208
1242
|
probe.session.dispose();
|
|
1209
|
-
return after - before > 1;
|
|
1243
|
+
return (after - before) * probe.up > 1;
|
|
1210
1244
|
};
|
|
1211
1245
|
if (!await fired(1)) return 0;
|
|
1212
1246
|
let lo = 1;
|
|
@@ -1231,13 +1265,14 @@ async function measureBuffer(json, opts, jump) {
|
|
|
1231
1265
|
if (!probe) return false;
|
|
1232
1266
|
settle$1(probe);
|
|
1233
1267
|
const floor = probe.pos()[1];
|
|
1268
|
+
const drop = (probe.up > 0 ? DROP : probe.lift / 3) * probe.up;
|
|
1234
1269
|
probe.player.position = [
|
|
1235
1270
|
probe.pos()[0],
|
|
1236
|
-
floor +
|
|
1271
|
+
floor + drop,
|
|
1237
1272
|
probe.pos()[2]
|
|
1238
1273
|
];
|
|
1239
1274
|
let frames = 0;
|
|
1240
|
-
while (probe.pos()[1]
|
|
1275
|
+
while ((probe.pos()[1] - floor) * probe.up > .05 && frames < 200) {
|
|
1241
1276
|
probe.step();
|
|
1242
1277
|
frames += 1;
|
|
1243
1278
|
}
|
|
@@ -1249,7 +1284,7 @@ async function measureBuffer(json, opts, jump) {
|
|
|
1249
1284
|
settle$1(p2);
|
|
1250
1285
|
p2.player.position = [
|
|
1251
1286
|
p2.pos()[0],
|
|
1252
|
-
floor +
|
|
1287
|
+
floor + drop,
|
|
1253
1288
|
p2.pos()[2]
|
|
1254
1289
|
];
|
|
1255
1290
|
p2.step(Math.max(0, fallFrames - earlyFrames));
|
|
@@ -1259,7 +1294,7 @@ async function measureBuffer(json, opts, jump) {
|
|
|
1259
1294
|
let rose = false;
|
|
1260
1295
|
for (let f = 0; f < 40; f++) {
|
|
1261
1296
|
p2.step();
|
|
1262
|
-
if (p2.vel()[1] > 1) rose = true;
|
|
1297
|
+
if (p2.vel()[1] * p2.up > 1) rose = true;
|
|
1263
1298
|
}
|
|
1264
1299
|
p2.session.dispose();
|
|
1265
1300
|
return rose;
|
|
@@ -1753,10 +1788,10 @@ async function runScript(json, opts) {
|
|
|
1753
1788
|
engine.setScene(scene);
|
|
1754
1789
|
const physics = opts.physics ?? "auto";
|
|
1755
1790
|
if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
|
|
1756
|
-
const { enablePhysics2D } = await import("./physics-2d-
|
|
1791
|
+
const { enablePhysics2D } = await import("./physics-2d-EqA-jddf.js").then((n) => n.r);
|
|
1757
1792
|
await enablePhysics2D(engine);
|
|
1758
1793
|
} else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
|
|
1759
|
-
const { enablePhysics3D } = await import("./physics-3d-
|
|
1794
|
+
const { enablePhysics3D } = await import("./physics-3d-Dnz4fsXX.js").then((n) => n.r);
|
|
1760
1795
|
await enablePhysics3D(engine);
|
|
1761
1796
|
}
|
|
1762
1797
|
const failures = [];
|
|
@@ -1869,10 +1904,10 @@ async function createPlaySession(json, opts = {}) {
|
|
|
1869
1904
|
engine.setScene(scene);
|
|
1870
1905
|
const physics = opts.physics ?? "auto";
|
|
1871
1906
|
if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
|
|
1872
|
-
const { enablePhysics2D } = await import("./physics-2d-
|
|
1907
|
+
const { enablePhysics2D } = await import("./physics-2d-EqA-jddf.js").then((n) => n.r);
|
|
1873
1908
|
await enablePhysics2D(engine);
|
|
1874
1909
|
} else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
|
|
1875
|
-
const { enablePhysics3D } = await import("./physics-3d-
|
|
1910
|
+
const { enablePhysics3D } = await import("./physics-3d-Dnz4fsXX.js").then((n) => n.r);
|
|
1876
1911
|
await enablePhysics3D(engine);
|
|
1877
1912
|
}
|
|
1878
1913
|
const stepMs = 1e3 / (opts.fixedHz ?? 60);
|
package/dist/test.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { At as Node, Nt as LogEntry, P as Scene, b as Engine, n as BehaviorCtor } from "./behavior-
|
|
1
|
+
import { At as Node, Nt as LogEntry, P as Scene, b as Engine, n as BehaviorCtor } from "./behavior-l08AEbq9.js";
|
|
2
2
|
import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
|
|
3
|
-
import { t as LoadSceneOptions } from "./loader-
|
|
4
|
-
import { l as auditScene, o as IncantoError, r as ReplayJson } from "./replay-
|
|
5
|
-
import { l as LocalGameServerOptions, n as SplitScreenPlayer } from "./split-screen-
|
|
3
|
+
import { t as LoadSceneOptions } from "./loader-BbEMTuWg.js";
|
|
4
|
+
import { l as auditScene, o as IncantoError, r as ReplayJson } from "./replay-Dw6gMlYA.js";
|
|
5
|
+
import { l as LocalGameServerOptions, n as SplitScreenPlayer } from "./split-screen-B0baBwxI.js";
|
|
6
6
|
|
|
7
7
|
//#region src/test/facing.d.ts
|
|
8
8
|
interface FacingOptions {
|
|
@@ -193,7 +193,7 @@ declare function multiplayProblems(detail: MultiplayPlayer[], rooms: string[]):
|
|
|
193
193
|
declare function multiplayText(report: MultiplayReport): string;
|
|
194
194
|
//#endregion
|
|
195
195
|
//#region src/test/playtest.d.ts
|
|
196
|
-
type Outcome = "won" | "lost" | "fell" | "error" | "stuck";
|
|
196
|
+
type Outcome = "won" | "lost" | "fell" | "error" | "stuck" | "unfinished";
|
|
197
197
|
interface PlaytestOptions {
|
|
198
198
|
/** How many independent runs. Each gets its own seed. Default 20. */
|
|
199
199
|
runs?: number;
|
package/dist/test.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { r as auditScene } from "./replay-
|
|
2
|
-
import { S as framingText, _ as failingReplays, a as registerAllNodes, b as playtestText, c as ladderText, d as multiplayText, f as playMultiplayer, g as facingText, h as facingReport, i as findFloatingProps, l as ladderVerdict, m as feelText, n as createPlaySession, o as runScript, p as feelReport, r as describeCapture, s as validateScene, t as captureScene, u as multiplayProblems, v as findPlayer, x as describeFraming, y as playtest } from "./test-
|
|
1
|
+
import { r as auditScene } from "./replay-s7I2GstT.js";
|
|
2
|
+
import { S as framingText, _ as failingReplays, a as registerAllNodes, b as playtestText, c as ladderText, d as multiplayText, f as playMultiplayer, g as facingText, h as facingReport, i as findFloatingProps, l as ladderVerdict, m as feelText, n as createPlaySession, o as runScript, p as feelReport, r as describeCapture, s as validateScene, t as captureScene, u as multiplayProblems, v as findPlayer, x as describeFraming, y as playtest } from "./test-ZBga8kQ9.js";
|
|
3
3
|
export { auditScene, captureScene, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, framingText, ladderText, ladderVerdict, multiplayProblems, multiplayText, playMultiplayer, playtest, playtestText, registerAllNodes, runScript, validateScene };
|