incanto 0.47.0 → 0.49.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/bin/incanto-frame.mjs +58 -1
- package/bin/incanto-logs.mjs +127 -0
- package/bin/incanto-model.mjs +13 -1
- package/bin/incanto-playtest.mjs +7 -0
- package/bin/incanto-verify.mjs +41 -2
- package/dist/2d.d.ts +21 -12
- package/dist/2d.js +57 -3
- package/dist/3d.d.ts +38 -1
- package/dist/3d.js +114 -4
- package/dist/{create-game-CniOiWzN.js → create-game-BgV6UbVA.js} +5 -5
- package/dist/{create-game-D16MVIPO.js → create-game-DFBjMetZ.js} +83 -6
- package/dist/{duplicate-CRtihGmC.js → duplicate-CI9WF_bg.js} +1 -1
- package/dist/{environment-presets-D1b0ydTS.js → environment-presets-CZOH5TY5.js} +15 -20
- package/dist/{gameplay-BQOeAid6.js → gameplay-02Btmmjn.js} +80 -9
- package/dist/gameplay.d.ts +17 -0
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +139 -1
- package/dist/index.js +8 -7
- package/dist/{loader-r49nDwB4.js → loader-DwazzlQb.js} +36 -6
- package/dist/log-report-lxrQY9cH.js +0 -0
- package/dist/net.js +3 -3
- package/dist/{physics-2d-BmgXBNDB.js → physics-2d-vyCBfACH.js} +3 -3
- package/dist/{physics-3d-CSoGjM8P.js → physics-3d-DpRqw8Mz.js} +4 -4
- package/dist/react.js +1 -1
- package/dist/{register-R2JTnIMw.js → register-BNPZYJmd.js} +23 -22
- package/dist/{register-D651it1J.js → register-CB11yp21.js} +2 -2
- package/dist/{register-BSXV8T9F.js → register-uvaZj1KX.js} +62 -2
- package/dist/{replay-DilbZgQI.js → replay-C0XJIsO7.js} +1 -1
- package/dist/sprite-animation-CMr6f1K2.d.ts +44 -0
- package/dist/{particle-sim-Bw7hB93B.js → sprite-animation-D_p28jwU.js} +63 -1
- package/dist/{src-Ca3oV1fe.js → src-DF4gCsqO.js} +1 -1
- package/dist/{test-E4-otKqK.js → test-DRna_BQU.js} +210 -30
- package/dist/test.d.ts +38 -6
- package/dist/test.js +2 -2
- package/dist/vite.js +41 -4
- package/editor/assets/{agent8-_007gPF8.js → agent8-DCW4TgDt.js} +1 -1
- package/editor/assets/{debug-0DI_MJaq.js → debug-RC6qts6S.js} +1 -1
- package/editor/assets/{index-B-6eYZEi.js → index-5dEIhvsf.js} +92 -92
- package/editor/index.html +1 -1
- package/package.json +3 -2
- package/schemas/scene.schema.json +10 -3
- package/skills/incanto-3d-character.md +13 -2
- package/skills/incanto-3d-models.md +40 -0
- package/skills/incanto-assets.md +15 -0
- package/skills/incanto-building-2d-games.md +57 -5
- package/skills/incanto-building-3d-games.md +7 -0
- package/skills/incanto-gameplay-behaviors.md +18 -1
- package/skills/incanto-hud.md +39 -0
- package/skills/incanto-node-reference.md +5 -3
- package/skills/incanto-physics-and-input.md +1 -1
- package/skills/incanto-playtesting.md +9 -2
- package/skills/incanto-verifying-your-game.md +67 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
1
2
|
import { n as jsonEquals } from "./json-BLk7H2Qa.js";
|
|
2
3
|
//#region src/core/particle-presets.ts
|
|
3
4
|
const PARTICLE_PRESETS = {
|
|
@@ -264,4 +265,65 @@ var ParticleSim = class {
|
|
|
264
265
|
}
|
|
265
266
|
};
|
|
266
267
|
//#endregion
|
|
267
|
-
|
|
268
|
+
//#region src/core/sprite-animation.ts
|
|
269
|
+
/**
|
|
270
|
+
* The animation MAP a spritesheet node is driven by — shared by 2D and 3D.
|
|
271
|
+
*
|
|
272
|
+
* Core, not an adapter: no three.js, and the two sprite nodes had grown
|
|
273
|
+
* byte-identical copies of the frame reader.
|
|
274
|
+
*/
|
|
275
|
+
/**
|
|
276
|
+
* Follow aliases to the clip that actually plays.
|
|
277
|
+
*
|
|
278
|
+
* Chains are allowed (`fall → jump → idle`) because they are how a fallback
|
|
279
|
+
* ladder reads, and a cycle is a hard error rather than a hang.
|
|
280
|
+
*/
|
|
281
|
+
function resolveAnimation(animations, name, owner) {
|
|
282
|
+
const seen = [];
|
|
283
|
+
let key = name;
|
|
284
|
+
for (;;) {
|
|
285
|
+
const entry = animations[key];
|
|
286
|
+
if (entry === void 0) {
|
|
287
|
+
const via = seen.length > 0 ? ` (via ${seen.join(" → ")})` : "";
|
|
288
|
+
throw new IncantoError("UNKNOWN_ANIMATION", `No animation '${key}' on '${owner}'${via}. Available: [${Object.keys(animations).join(", ")}].`);
|
|
289
|
+
}
|
|
290
|
+
if (typeof entry !== "string") return {
|
|
291
|
+
name: key,
|
|
292
|
+
def: entry
|
|
293
|
+
};
|
|
294
|
+
if (seen.includes(key)) throw new IncantoError("UNKNOWN_ANIMATION", `Animation aliases on '${owner}' point in a circle: ${[...seen, key].join(" → ")}.`);
|
|
295
|
+
seen.push(key);
|
|
296
|
+
key = entry;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Every alias resolves — checked once, at ready, rather than on the frame the
|
|
301
|
+
* character first jumps.
|
|
302
|
+
*
|
|
303
|
+
* A cosmetic map that only fails the moment a state is first entered is the
|
|
304
|
+
* shape of bug that reaches players: the game loads, plays, and dies on a
|
|
305
|
+
* ledge twenty minutes in.
|
|
306
|
+
*/
|
|
307
|
+
function validateAnimationAliases(animations, owner) {
|
|
308
|
+
for (const [name, entry] of Object.entries(animations)) if (typeof entry === "string") resolveAnimation(animations, name, owner);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* `[start, end]` is a RANGE; anything else is an explicit list.
|
|
312
|
+
*
|
|
313
|
+
* Two ascending numbers are overwhelmingly a range in every sheet format, and
|
|
314
|
+
* a two-frame flip-book is spelled `[3, 3, 4, 4]` or a descending pair.
|
|
315
|
+
*/
|
|
316
|
+
function resolveFrames(frames, name) {
|
|
317
|
+
if (!Array.isArray(frames) || frames.length === 0) throw new IncantoError("BAD_FORMAT", `Animation '${name}': "frames" must be a non-empty array.`);
|
|
318
|
+
if (frames.length === 2) {
|
|
319
|
+
const [start, end] = frames;
|
|
320
|
+
if (end >= start) {
|
|
321
|
+
const out = [];
|
|
322
|
+
for (let f = start; f <= end; f++) out.push(f);
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return [...frames];
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
export { PARTICLE_PRESETS as a, ParticleSim as i, resolveFrames as n, PARTICLE_PRESET_NAMES as o, validateAnimationAliases as r, applyParticlePreset as s, resolveAnimation as t };
|
|
@@ -161,6 +161,6 @@ function newUid() {
|
|
|
161
161
|
//#endregion
|
|
162
162
|
//#region src/index.ts
|
|
163
163
|
/** Engine version. Kept in sync with package.json by the release pipeline. */
|
|
164
|
-
const VERSION = "0.
|
|
164
|
+
const VERSION = "0.49.0";
|
|
165
165
|
//#endregion
|
|
166
166
|
export { findPath as a, preloadUrls as i, newUid as n, gridFromRows as o, assetUrls as r, VERSION as t };
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { n as loadScene, w as registerBehavior } from "./loader-
|
|
2
|
-
import { h as Engine } from "./register-
|
|
1
|
+
import { n as loadScene, s as resolveViewport, w as registerBehavior } from "./loader-DwazzlQb.js";
|
|
2
|
+
import { h as Engine } from "./register-uvaZj1KX.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
|
-
import { n as startRecording } from "./replay-
|
|
4
|
+
import { n as startRecording } from "./replay-C0XJIsO7.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-IyWCGe4q.js";
|
|
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 { t as registerNodesNet } from "./register-
|
|
7
|
+
import { n as registerGameplayBehaviors } from "./gameplay-02Btmmjn.js";
|
|
8
|
+
import { t as registerNodes2D } from "./register-BNPZYJmd.js";
|
|
9
|
+
import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-CZOH5TY5.js";
|
|
10
|
+
import { t as registerNodesNet } from "./register-CB11yp21.js";
|
|
11
11
|
import { Box3, Euler, Matrix4, PerspectiveCamera, Quaternion, Vector3 } from "three";
|
|
12
12
|
//#region src/test/framing.ts
|
|
13
13
|
/**
|
|
@@ -38,6 +38,13 @@ const LIGHT_TYPES = new Set([
|
|
|
38
38
|
"OmniLight3D",
|
|
39
39
|
"SpotLight3D"
|
|
40
40
|
]);
|
|
41
|
+
/** Screen-space subtrees: they ignore the camera, so framing has nothing to say. */
|
|
42
|
+
const SCREEN_SPACE = new Set(["UILayer", "HudLayer"]);
|
|
43
|
+
/** How far two colliders must actually interpenetrate to be a finding. */
|
|
44
|
+
const TOUCH_EPS_2D = 1;
|
|
45
|
+
const TOUCH_EPS_3D = .01;
|
|
46
|
+
/** What a 2D scene shows when it declares neither a viewport nor a canvas. */
|
|
47
|
+
const DEFAULT_VIEW_2D = [960, 540];
|
|
41
48
|
/**
|
|
42
49
|
* World transform for every node, composed from the tree.
|
|
43
50
|
*
|
|
@@ -50,7 +57,11 @@ function place(scene) {
|
|
|
50
57
|
const type = node.constructor.typeName ?? "Node";
|
|
51
58
|
const n = node;
|
|
52
59
|
const local = new Matrix4();
|
|
53
|
-
if (Array.isArray(n.position))
|
|
60
|
+
if (Array.isArray(n.position)) {
|
|
61
|
+
const r = n.rotation;
|
|
62
|
+
const euler = typeof r === "number" ? new Euler(0, 0, r * DEG) : new Euler((r?.[0] ?? 0) * DEG, (r?.[1] ?? 0) * DEG, (r?.[2] ?? 0) * DEG, "XYZ");
|
|
63
|
+
local.compose(new Vector3(n.position[0] ?? 0, n.position[1] ?? 0, n.position[2] ?? 0), new Quaternion().setFromEuler(euler), new Vector3(n.scale?.[0] ?? 1, n.scale?.[1] ?? 1, n.scale?.[2] ?? 1));
|
|
64
|
+
}
|
|
54
65
|
const matrix = new Matrix4().multiplyMatrices(parent, local);
|
|
55
66
|
out.push({
|
|
56
67
|
node,
|
|
@@ -58,6 +69,7 @@ function place(scene) {
|
|
|
58
69
|
matrix,
|
|
59
70
|
position: new Vector3().setFromMatrixPosition(matrix)
|
|
60
71
|
});
|
|
72
|
+
if (SCREEN_SPACE.has(type)) return;
|
|
61
73
|
for (const child of node.children) walk(child, matrix);
|
|
62
74
|
};
|
|
63
75
|
walk(scene.root, new Matrix4());
|
|
@@ -74,6 +86,54 @@ function draws(node) {
|
|
|
74
86
|
return proto._createObject3D !== base._createObject3D;
|
|
75
87
|
}
|
|
76
88
|
/**
|
|
89
|
+
* A 2D node draws something if its class puts pixels on its backing object.
|
|
90
|
+
*
|
|
91
|
+
* NOT `_createObject2D` — the 3D test's counterpart, and wrong here. 2D
|
|
92
|
+
* drawables all inherit the plain container and hang a quad off it in
|
|
93
|
+
* `_syncObject2D`, so the create hook is identical for a sprite and for a bare
|
|
94
|
+
* `Node2D`, and testing it reports a scene full of sprites as drawing nothing.
|
|
95
|
+
*/
|
|
96
|
+
function draws2D(node) {
|
|
97
|
+
const proto = Object.getPrototypeOf(node);
|
|
98
|
+
let base = proto;
|
|
99
|
+
while (base && base.constructor?.typeName !== "Node2D") {
|
|
100
|
+
base = Object.getPrototypeOf(base);
|
|
101
|
+
if (!base) return false;
|
|
102
|
+
}
|
|
103
|
+
return proto._syncObject2D !== base._syncObject2D;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A 2D node's half-size in pixels, and where its box hangs off the origin.
|
|
107
|
+
*
|
|
108
|
+
* Most 2D nodes are centred on their origin, but a `TileMap2D` hangs cell (0,0)
|
|
109
|
+
* by its TOP-LEFT corner — a whole level reported as a point at its corner is
|
|
110
|
+
* how you conclude the map is off screen when you are standing on it.
|
|
111
|
+
*/
|
|
112
|
+
function box2D(node) {
|
|
113
|
+
if ((node.constructor.typeName ?? "") === "TileMap2D") {
|
|
114
|
+
const map = node;
|
|
115
|
+
const rows = Array.isArray(map.cells) ? map.cells : [];
|
|
116
|
+
const cols = rows.reduce((n, r) => Math.max(n, String(r).length), 0);
|
|
117
|
+
const ts = map.tileSize ?? 32;
|
|
118
|
+
if (rows.length === 0 || cols === 0) return null;
|
|
119
|
+
const half = [cols * ts / 2, rows.length * ts / 2];
|
|
120
|
+
return {
|
|
121
|
+
half,
|
|
122
|
+
centre: half
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const size = node.size;
|
|
126
|
+
if (Array.isArray(size) && typeof size[0] === "number" && typeof size[1] === "number") return {
|
|
127
|
+
half: [Math.abs(size[0]) / 2, Math.abs(size[1]) / 2],
|
|
128
|
+
centre: [0, 0]
|
|
129
|
+
};
|
|
130
|
+
const half = halfExtents(node);
|
|
131
|
+
return half ? {
|
|
132
|
+
half: [half.x, half.y],
|
|
133
|
+
centre: [0, 0]
|
|
134
|
+
} : null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
77
137
|
* How big the node is on screen, from whatever it declares.
|
|
78
138
|
*
|
|
79
139
|
* `size` first (a MeshInstance3D's box, a Terrain3D's ground), then a collider.
|
|
@@ -118,10 +178,110 @@ function halfExtents(node) {
|
|
|
118
178
|
const r = collider.radius ?? .5;
|
|
119
179
|
return new Vector3(r, (collider.height ?? 1) / 2 + r, r);
|
|
120
180
|
}
|
|
181
|
+
if (shape === "rect") {
|
|
182
|
+
const size = collider.size ?? [1, 1];
|
|
183
|
+
return new Vector3((size[0] ?? 1) / 2, (size[1] ?? 1) / 2, .5);
|
|
184
|
+
}
|
|
185
|
+
if (shape === "circle") {
|
|
186
|
+
const r = collider.radius ?? .5;
|
|
187
|
+
return new Vector3(r, r, .5);
|
|
188
|
+
}
|
|
121
189
|
return null;
|
|
122
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* The same question in 2D: an orthographic window of design pixels, centred on
|
|
193
|
+
* the Camera2D and CLAMPED by its limits.
|
|
194
|
+
*
|
|
195
|
+
* The clamp is not a detail — it is the whole point of `limits`, and a report
|
|
196
|
+
* that skips it describes a view the renderer never draws. A following camera
|
|
197
|
+
* sits at the player's y, which in a 544 px world is hundreds of pixels past
|
|
198
|
+
* anything a 540 px window can show.
|
|
199
|
+
*
|
|
200
|
+
* With no camera at all a 2D scene still draws: the documented default view is
|
|
201
|
+
* `(0,0)`–`(w,h)`, so "no camera" is not "nothing is framed" the way it is in 3D.
|
|
202
|
+
*/
|
|
203
|
+
function describeFraming2D(scene, placed, opts) {
|
|
204
|
+
const design = resolveViewport(scene.viewport)?.design;
|
|
205
|
+
const [vw, vh] = opts.viewport ?? design ?? DEFAULT_VIEW_2D;
|
|
206
|
+
const cameras = placed.filter((p) => p.type === "Camera2D");
|
|
207
|
+
const chosen = cameras.find((p) => p.node.current === true) ?? cameras[0] ?? null;
|
|
208
|
+
let camera = null;
|
|
209
|
+
let view = {
|
|
210
|
+
minX: 0,
|
|
211
|
+
minY: 0,
|
|
212
|
+
maxX: vw,
|
|
213
|
+
maxY: vh
|
|
214
|
+
};
|
|
215
|
+
if (chosen) {
|
|
216
|
+
const cam = chosen.node;
|
|
217
|
+
const zoom = Math.max(.01, cam.zoom ?? 1);
|
|
218
|
+
const halfW = vw / (2 * zoom);
|
|
219
|
+
const halfH = vh / (2 * zoom);
|
|
220
|
+
let cx = chosen.position.x;
|
|
221
|
+
let cy = chosen.position.y;
|
|
222
|
+
const limits = cam.limits ?? [];
|
|
223
|
+
if (limits.length === 4) {
|
|
224
|
+
const [minX, minY, maxX, maxY] = limits;
|
|
225
|
+
cx = clampCentred(cx, minX + halfW, maxX - halfW);
|
|
226
|
+
cy = clampCentred(cy, minY + halfH, maxY - halfH);
|
|
227
|
+
}
|
|
228
|
+
view = {
|
|
229
|
+
minX: cx - halfW,
|
|
230
|
+
minY: cy - halfH,
|
|
231
|
+
maxX: cx + halfW,
|
|
232
|
+
maxY: cy + halfH
|
|
233
|
+
};
|
|
234
|
+
camera = {
|
|
235
|
+
path: chosen.node.getPath(),
|
|
236
|
+
at: [
|
|
237
|
+
round(cx),
|
|
238
|
+
round(cy),
|
|
239
|
+
0
|
|
240
|
+
],
|
|
241
|
+
view: [vw, vh],
|
|
242
|
+
zoom
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const wanted = opts.types ? new Set(opts.types) : null;
|
|
246
|
+
const centre = [(view.minX + view.maxX) / 2, (view.minY + view.maxY) / 2];
|
|
247
|
+
const entries = [];
|
|
248
|
+
for (const p of placed) {
|
|
249
|
+
if (p.type === "Camera2D") continue;
|
|
250
|
+
if (!(wanted ? wanted.has(p.type) : draws2D(p.node))) continue;
|
|
251
|
+
const b = box2D(p.node);
|
|
252
|
+
const cx = p.position.x + (b?.centre[0] ?? 0);
|
|
253
|
+
const cy = p.position.y + (b?.centre[1] ?? 0);
|
|
254
|
+
const hx = b?.half[0] ?? 0;
|
|
255
|
+
const hy = b?.half[1] ?? 0;
|
|
256
|
+
const onScreen = cx + hx >= view.minX && cx - hx <= view.maxX && cy + hy >= view.minY && cy - hy <= view.maxY;
|
|
257
|
+
entries.push({
|
|
258
|
+
path: p.node.getPath(),
|
|
259
|
+
type: p.type,
|
|
260
|
+
at: [
|
|
261
|
+
round(p.position.x),
|
|
262
|
+
round(p.position.y),
|
|
263
|
+
0
|
|
264
|
+
],
|
|
265
|
+
where: onScreen ? "onScreen" : "offscreen",
|
|
266
|
+
screen: [round((cx - centre[0]) / ((view.maxX - view.minX) / 2)), round((cy - centre[1]) / ((view.maxY - view.minY) / 2))],
|
|
267
|
+
distance: round(Math.hypot(cx - centre[0], cy - centre[1]))
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
dimension: "2d",
|
|
272
|
+
camera,
|
|
273
|
+
entries,
|
|
274
|
+
lights: [],
|
|
275
|
+
overlaps: findOverlaps(placed, TOUCH_EPS_2D)
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function clampCentred(v, lo, hi) {
|
|
279
|
+
if (lo > hi) return (lo + hi) / 2;
|
|
280
|
+
return Math.min(hi, Math.max(lo, v));
|
|
281
|
+
}
|
|
123
282
|
function describeFraming(scene, opts = {}) {
|
|
124
283
|
const placed = place(scene);
|
|
284
|
+
if (scene.dimension === "2d") return describeFraming2D(scene, placed, opts);
|
|
125
285
|
const cameras = placed.filter((p) => CAMERA_TYPES.has(p.type));
|
|
126
286
|
const chosen = cameras.find((p) => p.node.current === true) ?? cameras[0] ?? null;
|
|
127
287
|
const lights = [];
|
|
@@ -185,10 +345,11 @@ function describeFraming(scene, opts = {}) {
|
|
|
185
345
|
}
|
|
186
346
|
}
|
|
187
347
|
return {
|
|
348
|
+
dimension: "3d",
|
|
188
349
|
camera,
|
|
189
350
|
entries,
|
|
190
351
|
lights,
|
|
191
|
-
overlaps: findOverlaps(placed)
|
|
352
|
+
overlaps: findOverlaps(placed, TOUCH_EPS_3D)
|
|
192
353
|
};
|
|
193
354
|
}
|
|
194
355
|
function cornersOnScreen(box, cam) {
|
|
@@ -207,14 +368,16 @@ function cornersOnScreen(box, cam) {
|
|
|
207
368
|
* no loaded model — and a prop buried inside a platform is the thing you were
|
|
208
369
|
* trying to see anyway.
|
|
209
370
|
*/
|
|
210
|
-
function findOverlaps(placed) {
|
|
371
|
+
function findOverlaps(placed, epsilon) {
|
|
211
372
|
const boxes = [];
|
|
212
373
|
for (const p of placed) {
|
|
213
374
|
const half = halfExtents(p.node);
|
|
214
375
|
if (!half) continue;
|
|
376
|
+
const shrunk = half.clone().multiplyScalar(2).addScalar(-2 * epsilon);
|
|
215
377
|
boxes.push({
|
|
216
378
|
path: p.node.getPath(),
|
|
217
|
-
|
|
379
|
+
internal: p.node.name.startsWith("__"),
|
|
380
|
+
box: new Box3().setFromCenterAndSize(p.position, shrunk.max(new Vector3(0, 0, 0)))
|
|
218
381
|
});
|
|
219
382
|
}
|
|
220
383
|
const out = [];
|
|
@@ -223,6 +386,7 @@ function findOverlaps(placed) {
|
|
|
223
386
|
const b = boxes[j];
|
|
224
387
|
if (!a || !b) continue;
|
|
225
388
|
if (a.path.startsWith(`${b.path}/`) || b.path.startsWith(`${a.path}/`)) continue;
|
|
389
|
+
if (a.internal && b.internal) continue;
|
|
226
390
|
if (a.box.intersectsBox(b.box)) out.push({
|
|
227
391
|
a: a.path,
|
|
228
392
|
b: b.path
|
|
@@ -236,26 +400,41 @@ function round(n) {
|
|
|
236
400
|
/** The report as something to read in a terminal. */
|
|
237
401
|
function framingText(report) {
|
|
238
402
|
const lines = [];
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
403
|
+
const is2d = report.dimension === "2d";
|
|
404
|
+
const unit = is2d ? "px" : "m";
|
|
405
|
+
const c = report.camera;
|
|
406
|
+
if (c && is2d) {
|
|
407
|
+
const [vw = 0, vh = 0] = c.view ?? [];
|
|
408
|
+
const zoom = c.zoom === 1 ? "" : ` zoom ${c.zoom}`;
|
|
409
|
+
lines.push(`camera ${c.path} centred [${c.at[0]}, ${c.at[1]}] showing ${vw}×${vh}px${zoom}`);
|
|
410
|
+
} else if (c) lines.push(`camera ${c.path} at [${c.at.map(round).join(", ")}] looking [${(c.forward ?? []).join(", ")}] fov ${c.fovDeg}`);
|
|
411
|
+
else if (is2d) lines.push("camera: NONE — the view is (0,0)–(design), which may be intentional");
|
|
412
|
+
else lines.push("camera: NONE — this scene frames nothing (no Camera3D)");
|
|
413
|
+
if (!is2d) lines.push(report.lights.length ? `lit by: ${report.lights.join(", ")}` : "lit by: NOTHING — this scene renders black");
|
|
245
414
|
const on = report.entries.filter((e) => e.where === "onScreen");
|
|
246
415
|
const off = report.entries.filter((e) => e.where === "offscreen");
|
|
247
416
|
const behind = report.entries.filter((e) => e.where === "behind");
|
|
248
|
-
lines.push(`${on.length} on screen, ${off.length} off screen, ${behind.length} behind the camera`);
|
|
417
|
+
lines.push(is2d ? `${on.length} in view, ${off.length} outside it` : `${on.length} on screen, ${off.length} off screen, ${behind.length} behind the camera`);
|
|
249
418
|
for (const e of report.entries) {
|
|
250
|
-
const at = `[${e.at.map(round).join(", ")}]`;
|
|
419
|
+
const at = `[${(is2d ? e.at.slice(0, 2) : e.at).map(round).join(", ")}]`;
|
|
251
420
|
const screen = e.screen ? ` screen [${e.screen.join(", ")}]` : "";
|
|
252
|
-
lines.push(` ${e.where.padEnd(9)} ${e.path} (${e.type}) ${at}${screen} ${e.distance}
|
|
421
|
+
lines.push(` ${e.where.padEnd(9)} ${e.path} (${e.type}) ${at}${screen} ${e.distance}${unit}`);
|
|
253
422
|
}
|
|
254
423
|
for (const o of report.overlaps) lines.push(` overlap ${o.a} ∩ ${o.b}`);
|
|
255
424
|
return lines.join("\n");
|
|
256
425
|
}
|
|
257
426
|
//#endregion
|
|
258
427
|
//#region src/test/playtest.ts
|
|
428
|
+
/**
|
|
429
|
+
* Out of the world, in pixels — about two screens under the spawn.
|
|
430
|
+
*
|
|
431
|
+
* Generous on purpose: a player who has actually left the level is falling
|
|
432
|
+
* forever, so a far line costs a few frames and never mistakes a long drop
|
|
433
|
+
* down a shaft for one.
|
|
434
|
+
*/
|
|
435
|
+
const FALL_2D_PX = 1e3;
|
|
436
|
+
/** "Reached it", in pixels — one 32 px tile, next to a ~34 px character. */
|
|
437
|
+
const REACH_2D_PX = 32;
|
|
259
438
|
const CONTROLLERS = new Set(["CharacterController2D", "CharacterController3D"]);
|
|
260
439
|
/**
|
|
261
440
|
* Who the driver is playing as.
|
|
@@ -499,8 +678,9 @@ async function runOnce(json, seed, opts) {
|
|
|
499
678
|
0,
|
|
500
679
|
0
|
|
501
680
|
];
|
|
502
|
-
const
|
|
503
|
-
const
|
|
681
|
+
const down = scene.dimension === "2d" ? 1 : -1;
|
|
682
|
+
const floor = opts.fallBelow ?? start[1] + down * (down > 0 ? FALL_2D_PX : 50);
|
|
683
|
+
const radius = opts.reachRadius ?? (down > 0 ? REACH_2D_PX : 2);
|
|
504
684
|
const recorder = startRecording(engine);
|
|
505
685
|
const rand = () => engine.rng.next();
|
|
506
686
|
const driver = new Driver(engine, rand);
|
|
@@ -527,7 +707,7 @@ async function runOnce(json, seed, opts) {
|
|
|
527
707
|
outcome = "error";
|
|
528
708
|
break;
|
|
529
709
|
}
|
|
530
|
-
if (player && at[1]
|
|
710
|
+
if (player && (at[1] - floor) * down > 0) {
|
|
531
711
|
outcome = "fell";
|
|
532
712
|
break;
|
|
533
713
|
}
|
|
@@ -584,7 +764,8 @@ async function playtest(json, opts = {}) {
|
|
|
584
764
|
signals: [...new Set(signals)],
|
|
585
765
|
actions,
|
|
586
766
|
inertActions: [],
|
|
587
|
-
declaresWin
|
|
767
|
+
declaresWin,
|
|
768
|
+
seconds: opts.seconds ?? 60
|
|
588
769
|
};
|
|
589
770
|
}
|
|
590
771
|
function pct(n, total) {
|
|
@@ -605,8 +786,7 @@ function playtestText(report) {
|
|
|
605
786
|
const { runs, targets, signals } = report;
|
|
606
787
|
const total = runs.length;
|
|
607
788
|
const lines = [];
|
|
608
|
-
|
|
609
|
-
lines.push(`${total} runs × up to ${secs}s`);
|
|
789
|
+
lines.push(`${total} runs × up to ${report.seconds}s`);
|
|
610
790
|
lines.push("");
|
|
611
791
|
const won = runs.filter((r) => r.outcome === "won");
|
|
612
792
|
if (won.length > 0) lines.push(` ✓ reached "won" in ${pct(won.length, total)} median ${Math.round(median(won.map((r) => r.timeMs)) / 1e3)}s`);
|
|
@@ -1414,10 +1594,10 @@ async function runScript(json, opts) {
|
|
|
1414
1594
|
engine.setScene(scene);
|
|
1415
1595
|
const physics = opts.physics ?? "auto";
|
|
1416
1596
|
if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
|
|
1417
|
-
const { enablePhysics2D } = await import("./physics-2d-
|
|
1597
|
+
const { enablePhysics2D } = await import("./physics-2d-vyCBfACH.js").then((n) => n.r);
|
|
1418
1598
|
await enablePhysics2D(engine);
|
|
1419
1599
|
} else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
|
|
1420
|
-
const { enablePhysics3D } = await import("./physics-3d-
|
|
1600
|
+
const { enablePhysics3D } = await import("./physics-3d-DpRqw8Mz.js").then((n) => n.r);
|
|
1421
1601
|
await enablePhysics3D(engine);
|
|
1422
1602
|
}
|
|
1423
1603
|
const failures = [];
|
|
@@ -1530,10 +1710,10 @@ async function createPlaySession(json, opts = {}) {
|
|
|
1530
1710
|
engine.setScene(scene);
|
|
1531
1711
|
const physics = opts.physics ?? "auto";
|
|
1532
1712
|
if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
|
|
1533
|
-
const { enablePhysics2D } = await import("./physics-2d-
|
|
1713
|
+
const { enablePhysics2D } = await import("./physics-2d-vyCBfACH.js").then((n) => n.r);
|
|
1534
1714
|
await enablePhysics2D(engine);
|
|
1535
1715
|
} else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
|
|
1536
|
-
const { enablePhysics3D } = await import("./physics-3d-
|
|
1716
|
+
const { enablePhysics3D } = await import("./physics-3d-DpRqw8Mz.js").then((n) => n.r);
|
|
1537
1717
|
await enablePhysics3D(engine);
|
|
1538
1718
|
}
|
|
1539
1719
|
const stepMs = 1e3 / (opts.fixedHz ?? 60);
|
package/dist/test.d.ts
CHANGED
|
@@ -52,12 +52,23 @@ interface FramingEntry {
|
|
|
52
52
|
distance: number;
|
|
53
53
|
}
|
|
54
54
|
interface FramingReport {
|
|
55
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Which report this is. The question is the same — can the camera see it —
|
|
57
|
+
* but the machinery and the UNITS are not: 3D projects a perspective frustum
|
|
58
|
+
* and measures metres, 2D clips an orthographic window and measures pixels.
|
|
59
|
+
*/
|
|
60
|
+
dimension: "2d" | "3d";
|
|
61
|
+
/**
|
|
62
|
+
* 3D: null when the scene declares no camera — nothing frames anything.
|
|
63
|
+
* 2D: null means the documented default view, `(0,0)`–`(design)`.
|
|
64
|
+
*/
|
|
56
65
|
camera: {
|
|
57
66
|
path: string;
|
|
58
|
-
at: [number, number, number]; /**
|
|
59
|
-
forward
|
|
60
|
-
fovDeg
|
|
67
|
+
at: [number, number, number]; /** 3D: the unit vector it points along. Absent in 2D. */
|
|
68
|
+
forward?: [number, number, number]; /** 3D only. */
|
|
69
|
+
fovDeg?: number; /** 2D only: world pixels visible, after `zoom`. */
|
|
70
|
+
view?: [number, number]; /** 2D only. */
|
|
71
|
+
zoom?: number;
|
|
61
72
|
} | null;
|
|
62
73
|
entries: FramingEntry[];
|
|
63
74
|
/**
|
|
@@ -76,6 +87,11 @@ interface FramingOptions {
|
|
|
76
87
|
aspect?: number;
|
|
77
88
|
/** Report only these node types (default: everything that draws). */
|
|
78
89
|
types?: readonly string[];
|
|
90
|
+
/**
|
|
91
|
+
* 2D only: the window in world pixels. Defaults to the scene's
|
|
92
|
+
* `viewport.design`, then 960×540 — headless there is no canvas to ask.
|
|
93
|
+
*/
|
|
94
|
+
viewport?: readonly [number, number];
|
|
79
95
|
}
|
|
80
96
|
declare function describeFraming(scene: Scene, opts?: FramingOptions): FramingReport;
|
|
81
97
|
/** The report as something to read in a terminal. */
|
|
@@ -133,9 +149,16 @@ interface PlaytestOptions {
|
|
|
133
149
|
behaviors?: Record<string, BehaviorCtor>;
|
|
134
150
|
stubMissingBehaviors?: boolean;
|
|
135
151
|
resolveScene?: LoadSceneOptions["resolveScene"];
|
|
136
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* The y past which the player has left the world, in the SCENE's own
|
|
154
|
+
* direction: 3D counts down (−y), 2D counts down the screen (+y).
|
|
155
|
+
* Default: 50 m under the start in 3D, 1000 px under it in 2D.
|
|
156
|
+
*/
|
|
137
157
|
fallBelow?: number;
|
|
138
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* How close counts as "reached", in the scene's own units. Default 2 (metres)
|
|
160
|
+
* in 3D, 32 (pixels — one tile) in 2D.
|
|
161
|
+
*/
|
|
139
162
|
reachRadius?: number;
|
|
140
163
|
}
|
|
141
164
|
interface PlaytestRun {
|
|
@@ -174,6 +197,15 @@ interface PlaytestReport {
|
|
|
174
197
|
* author looking for a bug that was never there.
|
|
175
198
|
*/
|
|
176
199
|
declaresWin: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* The per-run clock the caller asked for, in simulated seconds.
|
|
202
|
+
*
|
|
203
|
+
* The header used to read this off the FIRST run's length, which is the one
|
|
204
|
+
* number guaranteed to be wrong when something ends runs early: a 60-second
|
|
205
|
+
* playtest whose every run died at a second announced itself as "20 runs ×
|
|
206
|
+
* up to 1s", hiding the very fact it was reporting.
|
|
207
|
+
*/
|
|
208
|
+
seconds: number;
|
|
177
209
|
}
|
|
178
210
|
/**
|
|
179
211
|
* Who the driver is playing as.
|
package/dist/test.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { r as auditScene } from "./replay-
|
|
2
|
-
import { _ as playtestText, a as registerAllNodes, c as ladderText, d as feelText, f as facingReport, g as playtest, h as findPlayer, i as findFloatingProps, l as ladderVerdict, m as failingReplays, n as createPlaySession, o as runScript, p as facingText, r as describeCapture, s as validateScene, t as captureScene, u as feelReport, v as describeFraming, y as framingText } from "./test-
|
|
1
|
+
import { r as auditScene } from "./replay-C0XJIsO7.js";
|
|
2
|
+
import { _ as playtestText, a as registerAllNodes, c as ladderText, d as feelText, f as facingReport, g as playtest, h as findPlayer, i as findFloatingProps, l as ladderVerdict, m as failingReplays, n as createPlaySession, o as runScript, p as facingText, r as describeCapture, s as validateScene, t as captureScene, u as feelReport, v as describeFraming, y as framingText } from "./test-DRna_BQU.js";
|
|
3
3
|
export { auditScene, captureScene, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, framingText, ladderText, ladderVerdict, playtest, playtestText, registerAllNodes, runScript, validateScene };
|
package/dist/vite.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as VERSION } from "./src-
|
|
1
|
+
import { t as VERSION } from "./src-DF4gCsqO.js";
|
|
2
2
|
import { n as diffSignatures } from "./frame-report-njybhZon.js";
|
|
3
|
-
import { s as validateScene } from "./test-
|
|
3
|
+
import { s as validateScene } from "./test-DRna_BQU.js";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, join, normalize, relative, resolve, sep } from "node:path";
|
|
6
6
|
//#region src/vite/frame-endpoint.ts
|
|
@@ -60,6 +60,7 @@ function serveFrameEndpoints(host, version, opts = {}) {
|
|
|
60
60
|
for (const c of channels) {
|
|
61
61
|
c.on("incanto:frame-report", onReport);
|
|
62
62
|
c.on("incanto:frame-ack", onAck);
|
|
63
|
+
c.on("incanto:logs-report", onReport);
|
|
63
64
|
}
|
|
64
65
|
host.middlewares.use("/__incanto/ping", (_req, res) => {
|
|
65
66
|
json$1(res, 200, {
|
|
@@ -67,6 +68,7 @@ function serveFrameEndpoints(host, version, opts = {}) {
|
|
|
67
68
|
frame: Boolean(channel)
|
|
68
69
|
});
|
|
69
70
|
});
|
|
71
|
+
serveLogs(host, channels, waiting, () => nextId++, timeoutMs);
|
|
70
72
|
host.middlewares.use("/__incanto/frame", (req, res) => {
|
|
71
73
|
if (!channel) {
|
|
72
74
|
json$1(res, 503, { error: "no-hmr-channel" });
|
|
@@ -78,11 +80,13 @@ function serveFrameEndpoints(host, version, opts = {}) {
|
|
|
78
80
|
const threshold = Number(params.get("threshold"));
|
|
79
81
|
const grid = parseGrid(params.get("grid"));
|
|
80
82
|
const image = Math.max(0, Math.min(2048, Number(params.get("image")) || 0));
|
|
83
|
+
const drive = params.get("drive") ?? "";
|
|
84
|
+
const driveMs = Math.max(0, Math.min(12e4, Number(params.get("driveMs")) || 0));
|
|
81
85
|
const id = nextId++;
|
|
82
86
|
const timer = setTimeout(() => {
|
|
83
87
|
waiting.delete(id);
|
|
84
88
|
json$1(res, 504, { error: acked.delete(id) ? "page-not-drawing" : "no-page-connected" });
|
|
85
|
-
}, timeoutMs);
|
|
89
|
+
}, timeoutMs + (driveMs > 0 ? driveMs * 2 + 2e3 : 0));
|
|
86
90
|
waiting.set(id, (value) => {
|
|
87
91
|
clearTimeout(timer);
|
|
88
92
|
acked.delete(id);
|
|
@@ -113,11 +117,44 @@ function serveFrameEndpoints(host, version, opts = {}) {
|
|
|
113
117
|
data: {
|
|
114
118
|
id,
|
|
115
119
|
grid,
|
|
116
|
-
image
|
|
120
|
+
image,
|
|
121
|
+
...drive ? { drive } : {}
|
|
117
122
|
}
|
|
118
123
|
});
|
|
119
124
|
});
|
|
120
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* `/__incanto/logs` — what the game is SAYING, not what it looks like.
|
|
128
|
+
*
|
|
129
|
+
* Same round trip as a frame and a different question. It needs no render, so
|
|
130
|
+
* unlike a frame it answers from a tab the browser has stopped drawing.
|
|
131
|
+
*/
|
|
132
|
+
function serveLogs(host, channels, waiting, nextId, timeoutMs) {
|
|
133
|
+
host.middlewares.use("/__incanto/logs", (_req, res) => {
|
|
134
|
+
if (channels.length === 0) {
|
|
135
|
+
json$1(res, 503, { error: "no-hmr-channel" });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const id = nextId();
|
|
139
|
+
const timer = setTimeout(() => {
|
|
140
|
+
waiting.delete(id);
|
|
141
|
+
json$1(res, 504, { error: "no-page-connected" });
|
|
142
|
+
}, timeoutMs);
|
|
143
|
+
waiting.set(id, (value) => {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
if (!value.ok) {
|
|
146
|
+
json$1(res, 500, { error: "report-failed" });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
json$1(res, 200, { report: value.report });
|
|
150
|
+
});
|
|
151
|
+
for (const c of channels) c.send({
|
|
152
|
+
type: "custom",
|
|
153
|
+
event: "incanto:logs-request",
|
|
154
|
+
data: { id }
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
121
158
|
function query(req) {
|
|
122
159
|
const url = req?.originalUrl ?? req?.url ?? "";
|
|
123
160
|
const q = url.indexOf("?");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e}from"./index-
|
|
1
|
+
import{n as e}from"./index-5dEIhvsf.js";async function t(t){return new n((await e(()=>import(`./GameServer-C56iOUgF.js`),[],import.meta.url)).GameServer,t)}var n=class{raw;active=new Map;reconnecting=!1;disposed=!1;constructor(e,t){this.raw=new e({...t})}get account(){return this.raw.account}get connected(){return this.raw.connected}connect(){return this.raw.connected?Promise.resolve(!0):(this.disposed=!1,this.rawConnect())}rawConnect(){return this.raw.connect({onDisconnect:()=>void this.reconnect()})}disconnect(){this.disposed=!0;for(let e of this.active.values())e.off();return this.active.clear(),this.raw.disconnect()}remoteFunction(e,t,n){return this.raw.remoteFunction(e,t,n)}track(e){let t=Symbol(`sub`),n={make:e,off:e()};return this.active.set(t,n),()=>{n.off(),this.active.delete(t)}}async reconnect(){if(!this.disposed&&!this.reconnecting){this.reconnecting=!0;try{await this.rawConnect();for(let e of this.active.values())e.off(),e.off=e.make()}finally{this.reconnecting=!1}}}subscribeRoomState(e,t){return this.track(()=>this.raw.subscribeRoomState(e,t))}subscribeRoomMyState(e,t){return this.track(()=>this.raw.subscribeRoomMyState(e,t))}subscribeRoomAllUserStates(e,t){return this.track(()=>this.raw.subscribeRoomAllUserStates(e,e=>{let n={};for(let t of e??[]){if(!t||typeof t.account!=`string`||t.__leaved)continue;let{account:e,__updated:r,__leaved:i,...a}=t;n[e]=a}t(n)}))}subscribeRoomCollection(e,t,n){return this.track(()=>this.raw.subscribeRoomCollection(e,t,({items:e})=>{let t={};for(let n of e??[])n&&typeof n.__id==`string`&&(t[n.__id]=n);n(t)}))}onRoomMessage(e,t,n){return this.track(()=>this.raw.onRoomMessage(e,t,n))}onRoomUserJoin(e,t){return this.track(()=>this.raw.onRoomUserJoin(e,t))}onRoomUserLeave(e,t){return this.track(()=>this.raw.onRoomUserLeave(e,t))}subscribeGlobalState(e){return this.track(()=>this.raw.subscribeGlobalState(e))}subscribeGlobalMyState(e){return this.track(()=>this.raw.subscribeGlobalMyState(e))}subscribeGlobalUserState(e,t){return this.track(()=>this.raw.subscribeGlobalUserState(e,t))}subscribeGlobalCollection(e,t){return this.track(()=>this.raw.subscribeGlobalCollection(e,({items:e})=>{let n={};for(let t of e??[])t&&typeof t.__id==`string`&&(n[t.__id]=t);t(n)}))}subscribeAsset(e,t){return this.track(()=>this.raw.subscribeAsset(e,t))}onGlobalMessage(e,t){return this.track(()=>this.raw.onGlobalMessage(e,t))}};export{t as createAgent8Server};
|