incanto 0.58.0 → 0.59.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-multiplay.mjs +170 -0
- package/bin/incanto-verify.mjs +68 -4
- package/bin/incanto.mjs +1 -0
- package/dist/2d.d.ts +8 -2
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +9 -3
- package/dist/3d.js +4 -4
- package/dist/{behavior-l08AEbq9.d.ts → behavior-DWKTUzKI.d.ts} +10 -0
- package/dist/{create-game-D10bU5_J.js → create-game-BiW8Men_.js} +6 -6
- package/dist/{create-game-BZwWJIns.js → create-game-CHDLDQsQ.js} +6 -6
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +2 -2
- package/dist/{duplicate-BPLZDZpd.js → duplicate-DJQd44CD.js} +1 -1
- package/dist/{environment-presets-CvvQr_bJ.js → environment-presets-DRAz5EV9.js} +12 -10
- package/dist/{gameplay-BVphcxmE.js → gameplay-BBEjPFsR.js} +62 -34
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +36 -8
- package/dist/index.js +8 -8
- package/dist/{json-BLk7H2Qa.js → json-CwwhxQgb.js} +7 -1
- package/dist/{loader-BcrRSjxB.js → loader-D8n7TU8W.js} +142 -5
- package/dist/{loader-BbEMTuWg.d.ts → loader-TvkRFbyL.d.ts} +1 -1
- package/dist/net.d.ts +2 -2
- package/dist/net.js +1 -1
- package/dist/{pathfinding-mEN4V1CU.d.ts → pathfinding-BqWBb0kh.d.ts} +1 -1
- package/dist/{physics-2d-EqA-jddf.js → physics-2d-BaRSRrrZ.js} +12 -2
- package/dist/{physics-3d-Dnz4fsXX.js → physics-3d-CYxjh-HW.js} +13 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.js +2 -2
- package/dist/{register-C6ZBFRjd.js → register-BpFcgdcL.js} +57 -27
- package/dist/{register-Ch70uByv.js → register-CDrAQqPp.js} +90 -41
- package/dist/{registry-C7u42TID.js → registry-WWcQcfMr.js} +1 -1
- package/dist/{replay-s7I2GstT.js → replay-CEPyQtF_.js} +31 -7
- package/dist/{replay-Dw6gMlYA.d.ts → replay-O-yAGM76.d.ts} +1 -1
- package/dist/{split-screen-B0baBwxI.d.ts → split-screen-BQ3tAsf-.d.ts} +38 -1
- package/dist/{split-screen-DLsUrleX.js → split-screen-DDMZutQ6.js} +56 -13
- package/dist/{sprite-animation-C0wXLBZJ.js → sprite-animation-CY-mrr1L.js} +1 -1
- package/dist/{src-C1J09Op6.js → src-CY21B462.js} +1 -1
- package/dist/{teardown-Cs113S9F.js → teardown-RApWnM1G.js} +1 -1
- package/dist/{test-ZBga8kQ9.js → test-DHYuFyAu.js} +260 -25
- package/dist/test.d.ts +103 -5
- package/dist/test.js +3 -3
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-BQQjE9UQ.js → agent8-BoRGtVxK.js} +1 -1
- package/editor/assets/{debug-C9UCsXBS.js → debug-CzdyCg75.js} +1 -1
- package/editor/assets/{index-CBgfM3WD.js → index-VesuVEhe.js} +91 -91
- package/editor/index.html +1 -1
- package/package.json +2 -1
- package/schemas/scene.schema.json +1174 -70
- package/skills/incanto-localization.md +40 -8
- package/skills/incanto-multiplayer.md +57 -4
- package/skills/incanto-node-reference.md +18 -0
- package/skills/incanto-physics-and-input.md +26 -0
- package/skills/incanto-verifying-your-game.md +22 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/package.json +1 -1
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { _ as Engine } from "./register-
|
|
1
|
+
import { c as resolveViewport, j as registerBehavior, n as loadScene } from "./loader-D8n7TU8W.js";
|
|
2
|
+
import { _ as Engine } from "./register-CDrAQqPp.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
|
-
import { n as jsonEquals, t as jsonClone } from "./json-
|
|
5
|
-
import { i as getNodeSchema, s as mergeStaticProps } from "./registry-
|
|
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-
|
|
4
|
+
import { n as jsonEquals, t as jsonClone } from "./json-CwwhxQgb.js";
|
|
5
|
+
import { i as getNodeSchema, s as mergeStaticProps } from "./registry-WWcQcfMr.js";
|
|
6
|
+
import { n as startRecording } from "./replay-CEPyQtF_.js";
|
|
7
|
+
import { n as registerGameplayBehaviors } from "./gameplay-BBEjPFsR.js";
|
|
8
|
+
import { t as registerNodes2D } from "./register-BpFcgdcL.js";
|
|
9
|
+
import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-DRAz5EV9.js";
|
|
10
|
+
import { a as NetworkManager, c as readSyncKey, i as sanitizeName, n as registerNodesNet, r as NetworkSpawner, s as findOwnerNode, t as createSplitScreen } from "./split-screen-DDMZutQ6.js";
|
|
11
11
|
import { Box3, Euler, Matrix4, PerspectiveCamera, Quaternion, Vector3 } from "three";
|
|
12
12
|
//#region src/test/framing.ts
|
|
13
13
|
/**
|
|
@@ -1038,6 +1038,177 @@ function facingText(report) {
|
|
|
1038
1038
|
return `facing: ${report.skin} faces its travel (dot ${report.dot.toFixed(2)})`;
|
|
1039
1039
|
}
|
|
1040
1040
|
//#endregion
|
|
1041
|
+
//#region src/test/agreement.ts
|
|
1042
|
+
/** Absolute slack, for values that are still converging out of a lerp. */
|
|
1043
|
+
const ABS_TOL = .001;
|
|
1044
|
+
/** Relative slack, so a large coordinate is not held to the absolute one. */
|
|
1045
|
+
const REL_TOL = .005;
|
|
1046
|
+
function closeEnough(a, b, absTol, relTol) {
|
|
1047
|
+
return Math.abs(a - b) <= Math.max(absTol, relTol * Math.abs(a));
|
|
1048
|
+
}
|
|
1049
|
+
function numbers(v) {
|
|
1050
|
+
if (typeof v === "number") return [v];
|
|
1051
|
+
if (Array.isArray(v) && v.every((x) => typeof x === "number")) return v;
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Every way the clients disagree, most structural first.
|
|
1056
|
+
*
|
|
1057
|
+
* @param live snapshots taken after the match has quiesced
|
|
1058
|
+
* @param late the same, for a client that joined AFTER everything settled
|
|
1059
|
+
*/
|
|
1060
|
+
function agreementProblems(live, late = [], opts = {}) {
|
|
1061
|
+
const absTol = opts.absTol ?? ABS_TOL;
|
|
1062
|
+
const relTol = opts.relTol ?? REL_TOL;
|
|
1063
|
+
const out = [];
|
|
1064
|
+
const compare = (snapshots, lateJoin) => {
|
|
1065
|
+
const found = [];
|
|
1066
|
+
for (const s of snapshots) for (const [observer, seen] of Object.entries(s.seen)) {
|
|
1067
|
+
if (seen === null) {
|
|
1068
|
+
found.push({
|
|
1069
|
+
kind: "missing",
|
|
1070
|
+
account: s.account,
|
|
1071
|
+
observer,
|
|
1072
|
+
key: "",
|
|
1073
|
+
summary: `${observer} never materialised ${s.account} at all — a NetworkSpawner spawns one instance per OTHER account, so check its \`scene\` key is registered and its \`source\` names a real collection.`
|
|
1074
|
+
});
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
for (const [key, truth] of Object.entries(s.truth)) {
|
|
1078
|
+
const mine = seen[key];
|
|
1079
|
+
if (mine === void 0) {
|
|
1080
|
+
found.push({
|
|
1081
|
+
kind: lateJoin ? "erased" : "absent",
|
|
1082
|
+
account: s.account,
|
|
1083
|
+
observer,
|
|
1084
|
+
key,
|
|
1085
|
+
truth,
|
|
1086
|
+
summary: lateJoin ? `${observer} joined late and never got ${s.account}'s \`${key}\` — the live clients have it, so the value is gone from the AUTHORITATIVE state, not from the wire. A partial patch replaces the stored \`sync\` object.` : `${observer} has no \`${key}\` for ${s.account} — the key path does not resolve on the spawned scene (a renamed child?), or it was never sent.`
|
|
1087
|
+
});
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
const a = numbers(truth);
|
|
1091
|
+
const b = numbers(mine);
|
|
1092
|
+
if (a && b) {
|
|
1093
|
+
if (a.length !== b.length) {
|
|
1094
|
+
found.push({
|
|
1095
|
+
kind: "shape",
|
|
1096
|
+
account: s.account,
|
|
1097
|
+
observer,
|
|
1098
|
+
key,
|
|
1099
|
+
truth,
|
|
1100
|
+
seen: mine,
|
|
1101
|
+
summary: `${observer} holds ${b.length} components of ${s.account}'s \`${key}\` where there are ${a.length} — the interpolator treats a missing component as already correct, so this never converges and never complains.`
|
|
1102
|
+
});
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
if (a.every((v, i) => closeEnough(v, b[i], absTol, relTol))) continue;
|
|
1106
|
+
found.push({
|
|
1107
|
+
kind: "mismatch",
|
|
1108
|
+
account: s.account,
|
|
1109
|
+
observer,
|
|
1110
|
+
key,
|
|
1111
|
+
truth,
|
|
1112
|
+
seen: mine,
|
|
1113
|
+
summary: `${observer} has ${s.account}'s \`${key}\` as ${JSON.stringify(mine)}, not ${JSON.stringify(truth)}.`
|
|
1114
|
+
});
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
if (jsonEquals(truth, mine)) continue;
|
|
1118
|
+
found.push({
|
|
1119
|
+
kind: "mismatch",
|
|
1120
|
+
account: s.account,
|
|
1121
|
+
observer,
|
|
1122
|
+
key,
|
|
1123
|
+
truth,
|
|
1124
|
+
seen: mine,
|
|
1125
|
+
summary: `${observer} has ${s.account}'s \`${key}\` as ${JSON.stringify(mine)}, not ${JSON.stringify(truth)}.`
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return found;
|
|
1130
|
+
};
|
|
1131
|
+
out.push(...compare(live, false));
|
|
1132
|
+
if (late.length > 0) {
|
|
1133
|
+
const alreadyAbsent = new Set(out.filter((d) => d.kind === "absent").map((d) => `${d.account}|${d.key}`));
|
|
1134
|
+
for (const d of compare(late, true)) {
|
|
1135
|
+
if (d.kind === "erased" && alreadyAbsent.has(`${d.account}|${d.key}`)) continue;
|
|
1136
|
+
out.push(d);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
const rank = {
|
|
1140
|
+
missing: 0,
|
|
1141
|
+
erased: 1,
|
|
1142
|
+
absent: 2,
|
|
1143
|
+
shape: 3,
|
|
1144
|
+
mismatch: 4
|
|
1145
|
+
};
|
|
1146
|
+
return out.sort((x, y) => rank[x.kind] - rank[y.kind]);
|
|
1147
|
+
}
|
|
1148
|
+
//#endregion
|
|
1149
|
+
//#region src/test/agreement-snapshot.ts
|
|
1150
|
+
/** The last-dot split of the RECEIVE side, applied to a spawned instance. */
|
|
1151
|
+
function readFromInstance(instance, key) {
|
|
1152
|
+
const lastDot = key.lastIndexOf(".");
|
|
1153
|
+
const target = lastDot === -1 ? instance : instance.getNodeOrNull(key.slice(0, lastDot));
|
|
1154
|
+
if (!target) return void 0;
|
|
1155
|
+
return target[lastDot === -1 ? key : key.slice(lastDot + 1)];
|
|
1156
|
+
}
|
|
1157
|
+
/** Every spawned instance in a client's tree, by the child name a spawner gives it. */
|
|
1158
|
+
function instancesByName(engine) {
|
|
1159
|
+
const out = /* @__PURE__ */ new Map();
|
|
1160
|
+
const root = engine.scene?.tree.root;
|
|
1161
|
+
if (!root) return out;
|
|
1162
|
+
const walk = (node) => {
|
|
1163
|
+
for (const child of node.children) {
|
|
1164
|
+
if (!out.has(child.name)) out.set(child.name, child);
|
|
1165
|
+
walk(child);
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
walk(root);
|
|
1169
|
+
return out;
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* One snapshot per account: its own truth, and what each other client believes.
|
|
1173
|
+
*
|
|
1174
|
+
* @param clients every client in the match
|
|
1175
|
+
* @param observers the clients whose beliefs to record (defaults to `clients`)
|
|
1176
|
+
*/
|
|
1177
|
+
function takeAgreementSnapshots(clients, observers = clients) {
|
|
1178
|
+
const snapshots = [];
|
|
1179
|
+
for (const owner of clients) {
|
|
1180
|
+
const root = owner.engine.scene?.tree.root;
|
|
1181
|
+
if (!root) continue;
|
|
1182
|
+
const node = findOwnerNode(root);
|
|
1183
|
+
if (!node) continue;
|
|
1184
|
+
const keys = Array.isArray(node.network?.sync) ? node.network.sync : [];
|
|
1185
|
+
if (keys.length === 0) continue;
|
|
1186
|
+
const truth = {};
|
|
1187
|
+
for (const key of keys) {
|
|
1188
|
+
const value = readSyncKey(node, key);
|
|
1189
|
+
if (value !== void 0) truth[key] = value;
|
|
1190
|
+
}
|
|
1191
|
+
const seen = {};
|
|
1192
|
+
for (const other of observers) {
|
|
1193
|
+
if (other.account === owner.account) continue;
|
|
1194
|
+
const instance = instancesByName(other.engine).get(sanitizeName(owner.account));
|
|
1195
|
+
if (!instance) {
|
|
1196
|
+
seen[other.account] = null;
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
const held = {};
|
|
1200
|
+
for (const key of Object.keys(truth)) held[key] = readFromInstance(instance, key);
|
|
1201
|
+
seen[other.account] = held;
|
|
1202
|
+
}
|
|
1203
|
+
snapshots.push({
|
|
1204
|
+
account: owner.account,
|
|
1205
|
+
truth,
|
|
1206
|
+
seen
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
return snapshots;
|
|
1210
|
+
}
|
|
1211
|
+
//#endregion
|
|
1041
1212
|
//#region src/test/feel.ts
|
|
1042
1213
|
const STEP_MS = 1e3 / 60;
|
|
1043
1214
|
/** Drop height for the buffer probe — long enough to outlast a coyote window. */
|
|
@@ -1430,10 +1601,12 @@ async function playMultiplayer(opts) {
|
|
|
1430
1601
|
const seconds = opts.seconds ?? 5;
|
|
1431
1602
|
const stepMs = opts.stepMs ?? 1e3 / 60;
|
|
1432
1603
|
const accounts = Array.from({ length: count }, (_, i) => `p${i + 1}`);
|
|
1433
|
-
const { players, dispose } = await createSplitScreen({
|
|
1604
|
+
const { server, players, dispose } = await createSplitScreen({
|
|
1434
1605
|
scene: opts.scene,
|
|
1435
1606
|
...opts.server ? { server: opts.server } : {},
|
|
1436
1607
|
...opts.scenes ? { scenes: opts.scenes } : {},
|
|
1608
|
+
...opts.seed !== void 0 ? { seed: opts.seed } : {},
|
|
1609
|
+
...opts.load ? { load: opts.load } : {},
|
|
1437
1610
|
accounts,
|
|
1438
1611
|
setup: () => {}
|
|
1439
1612
|
});
|
|
@@ -1450,26 +1623,87 @@ async function playMultiplayer(opts) {
|
|
|
1450
1623
|
if (frame % 6 === 0) await settle();
|
|
1451
1624
|
}
|
|
1452
1625
|
await settle();
|
|
1453
|
-
|
|
1626
|
+
for (let frame = 0; frame < 60; frame++) {
|
|
1627
|
+
now += stepMs;
|
|
1628
|
+
for (const player of players) player.engine.tick(now);
|
|
1629
|
+
if (frame % 6 === 0) await settle();
|
|
1630
|
+
}
|
|
1631
|
+
await settle();
|
|
1632
|
+
const live = takeAgreementSnapshots(players);
|
|
1633
|
+
const matchDetail = players.map((p) => ({
|
|
1634
|
+
account: p.account,
|
|
1635
|
+
room: p.manager.roomId,
|
|
1636
|
+
spawned: spawnerCounts(p.engine),
|
|
1637
|
+
sawAccounts: Object.keys(p.manager.latestUserStates ?? {}).filter((a) => a !== p.account),
|
|
1638
|
+
errors: p.engine.stats().errors
|
|
1639
|
+
}));
|
|
1640
|
+
let lateSnapshots = [];
|
|
1641
|
+
let lateJoinChecked = false;
|
|
1642
|
+
if ((opts.lateJoin ?? true) && players.length > 0) {
|
|
1643
|
+
const joiner = await joinLate(server, players[0], opts);
|
|
1644
|
+
if (joiner) {
|
|
1645
|
+
for (let frame = 0; frame < 60; frame++) {
|
|
1646
|
+
now += stepMs;
|
|
1647
|
+
for (const player of [...players, joiner]) player.engine.tick(now);
|
|
1648
|
+
if (frame % 6 === 0) await settle();
|
|
1649
|
+
}
|
|
1650
|
+
await settle();
|
|
1651
|
+
lateSnapshots = takeAgreementSnapshots(players, [joiner]);
|
|
1652
|
+
lateJoinChecked = true;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
const report = summarize(players, seconds, frames, matchDetail, {
|
|
1656
|
+
disagreements: agreementProblems(live, lateSnapshots),
|
|
1657
|
+
keysCompared: live.reduce((n, s) => n + Object.keys(s.truth).length * Object.keys(s.seen).length, 0),
|
|
1658
|
+
lateJoinChecked
|
|
1659
|
+
});
|
|
1454
1660
|
dispose();
|
|
1455
1661
|
return report;
|
|
1456
1662
|
}
|
|
1663
|
+
/**
|
|
1664
|
+
* One more client, against the SAME server and the SAME room.
|
|
1665
|
+
*
|
|
1666
|
+
* `createSplitScreen` builds all its panels up front, so this is the ten lines
|
|
1667
|
+
* of it that matter — and the room id has to come from panel 0 or the joiner
|
|
1668
|
+
* lands in its own auto-assigned room and reports everything as missing.
|
|
1669
|
+
*/
|
|
1670
|
+
async function joinLate(server, first, opts) {
|
|
1671
|
+
try {
|
|
1672
|
+
const { Engine } = await import("./register-CDrAQqPp.js").then((n) => n.v);
|
|
1673
|
+
const { loadScene } = await import("./loader-D8n7TU8W.js").then((n) => n.r);
|
|
1674
|
+
const { jsonClone } = await import("./json-CwwhxQgb.js").then((n) => n.i);
|
|
1675
|
+
const engine = new Engine(opts.seed !== void 0 ? { seed: opts.seed + 999 } : {});
|
|
1676
|
+
engine.setScene(loadScene(jsonClone(opts.scene), opts.load));
|
|
1677
|
+
const manager = await NetworkManager.create(engine, {
|
|
1678
|
+
transport: server.createClient("late"),
|
|
1679
|
+
room: first.manager.roomId
|
|
1680
|
+
});
|
|
1681
|
+
for (const [name, json] of Object.entries(opts.scenes ?? {})) manager.registerScene(name, json);
|
|
1682
|
+
return {
|
|
1683
|
+
account: "late",
|
|
1684
|
+
engine,
|
|
1685
|
+
manager
|
|
1686
|
+
};
|
|
1687
|
+
} catch {
|
|
1688
|
+
return null;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1457
1691
|
/** Give the server's promise queue a turn. */
|
|
1458
1692
|
function settle() {
|
|
1459
1693
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
1460
1694
|
}
|
|
1461
|
-
function summarize(players, seconds, frames) {
|
|
1695
|
+
function summarize(players, seconds, frames, matchDetail, agreement) {
|
|
1462
1696
|
const rooms = players.map((p) => p.manager.roomId);
|
|
1463
|
-
const detail =
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
spawned: spawnerCounts(p.engine),
|
|
1467
|
-
sawAccounts: Object.keys(p.manager.latestUserStates ?? {}).filter((a) => a !== p.account),
|
|
1468
|
-
errors: p.engine.stats().errors
|
|
1697
|
+
const detail = matchDetail.map((d, i) => ({
|
|
1698
|
+
...d,
|
|
1699
|
+
errors: players[i]?.engine.stats().errors ?? d.errors
|
|
1469
1700
|
}));
|
|
1470
|
-
const problems = multiplayProblems(detail, rooms);
|
|
1701
|
+
const problems = [...multiplayProblems(detail, rooms), ...agreement.disagreements.map((d) => d.summary)];
|
|
1471
1702
|
return {
|
|
1472
1703
|
ok: problems.length === 0,
|
|
1704
|
+
disagreements: agreement.disagreements,
|
|
1705
|
+
keysCompared: agreement.keysCompared,
|
|
1706
|
+
lateJoinChecked: agreement.lateJoinChecked,
|
|
1473
1707
|
seconds,
|
|
1474
1708
|
frames,
|
|
1475
1709
|
rooms,
|
|
@@ -1502,7 +1736,7 @@ function spawnerCounts(engine) {
|
|
|
1502
1736
|
const root = engine.scene?.tree.root;
|
|
1503
1737
|
if (!root) return out;
|
|
1504
1738
|
const walk = (node) => {
|
|
1505
|
-
if (node
|
|
1739
|
+
if (node instanceof NetworkSpawner) out[node.name] = node.children.length;
|
|
1506
1740
|
for (const child of node.children) walk(child);
|
|
1507
1741
|
};
|
|
1508
1742
|
walk(root);
|
|
@@ -1511,6 +1745,7 @@ function spawnerCounts(engine) {
|
|
|
1511
1745
|
/** The report as the one paragraph a reader needs. */
|
|
1512
1746
|
function multiplayText(report) {
|
|
1513
1747
|
const lines = [`multiplayer: ${report.players.length} clients, ${report.seconds}s, room ${[...new Set(report.rooms)].join(" + ")}`];
|
|
1748
|
+
lines.push(report.keysCompared === 0 ? " agrees: nothing to compare — no owner node declared any sync keys" : ` agrees: ${report.keysCompared} replicated value(s) compared across clients${report.lateJoinChecked ? " + a late joiner" : ""}, over one in-memory server (not a live wire)`);
|
|
1514
1749
|
for (const p of report.players) {
|
|
1515
1750
|
const spawned = Object.entries(p.spawned).map(([name, n]) => `${name}=${n}`).join(" ");
|
|
1516
1751
|
lines.push(` ${p.account}: sees [${p.sawAccounts.join(", ") || "—"}]` + (spawned ? ` spawned ${spawned}` : "") + (p.errors ? ` · ${p.errors} error(s)` : ""));
|
|
@@ -1788,10 +2023,10 @@ async function runScript(json, opts) {
|
|
|
1788
2023
|
engine.setScene(scene);
|
|
1789
2024
|
const physics = opts.physics ?? "auto";
|
|
1790
2025
|
if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
|
|
1791
|
-
const { enablePhysics2D } = await import("./physics-2d-
|
|
2026
|
+
const { enablePhysics2D } = await import("./physics-2d-BaRSRrrZ.js").then((n) => n.r);
|
|
1792
2027
|
await enablePhysics2D(engine);
|
|
1793
2028
|
} else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
|
|
1794
|
-
const { enablePhysics3D } = await import("./physics-3d-
|
|
2029
|
+
const { enablePhysics3D } = await import("./physics-3d-CYxjh-HW.js").then((n) => n.r);
|
|
1795
2030
|
await enablePhysics3D(engine);
|
|
1796
2031
|
}
|
|
1797
2032
|
const failures = [];
|
|
@@ -1904,10 +2139,10 @@ async function createPlaySession(json, opts = {}) {
|
|
|
1904
2139
|
engine.setScene(scene);
|
|
1905
2140
|
const physics = opts.physics ?? "auto";
|
|
1906
2141
|
if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
|
|
1907
|
-
const { enablePhysics2D } = await import("./physics-2d-
|
|
2142
|
+
const { enablePhysics2D } = await import("./physics-2d-BaRSRrrZ.js").then((n) => n.r);
|
|
1908
2143
|
await enablePhysics2D(engine);
|
|
1909
2144
|
} else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
|
|
1910
|
-
const { enablePhysics3D } = await import("./physics-3d-
|
|
2145
|
+
const { enablePhysics3D } = await import("./physics-3d-CYxjh-HW.js").then((n) => n.r);
|
|
1911
2146
|
await enablePhysics3D(engine);
|
|
1912
2147
|
}
|
|
1913
2148
|
const stepMs = 1e3 / (opts.fixedHz ?? 60);
|
|
@@ -1945,4 +2180,4 @@ async function createPlaySession(json, opts = {}) {
|
|
|
1945
2180
|
};
|
|
1946
2181
|
}
|
|
1947
2182
|
//#endregion
|
|
1948
|
-
export {
|
|
2183
|
+
export { describeFraming as C, playtestText as S, facingReport as _, registerAllNodes as a, findPlayer as b, ladderText as c, multiplayText as d, playMultiplayer as f, agreementProblems as g, takeAgreementSnapshots as h, findFloatingProps as i, ladderVerdict as l, feelText as m, createPlaySession as n, runScript as o, feelReport as p, describeCapture as r, validateScene as s, captureScene as t, multiplayProblems as u, facingText as v, framingText as w, playtest as x, failingReplays as y };
|
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-DWKTUzKI.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-TvkRFbyL.js";
|
|
4
|
+
import { l as auditScene, o as IncantoError, r as ReplayJson } from "./replay-O-yAGM76.js";
|
|
5
|
+
import { l as LocalGameServerOptions, n as SplitScreenPlayer } from "./split-screen-BQ3tAsf-.js";
|
|
6
6
|
|
|
7
7
|
//#region src/test/facing.d.ts
|
|
8
8
|
interface FacingOptions {
|
|
@@ -98,6 +98,84 @@ declare function describeFraming(scene: Scene, opts?: FramingOptions): FramingRe
|
|
|
98
98
|
/** The report as something to read in a terminal. */
|
|
99
99
|
declare function framingText(report: FramingReport): string;
|
|
100
100
|
//#endregion
|
|
101
|
+
//#region src/test/agreement.d.ts
|
|
102
|
+
/**
|
|
103
|
+
* Do two clients hold the same value for the same replicated thing?
|
|
104
|
+
*
|
|
105
|
+
* Nothing in the engine asked. The multiplayer harness measured room
|
|
106
|
+
* MEMBERSHIP and child COUNTS: `sawAccounts` is the room's key list, and the
|
|
107
|
+
* kernel inserts an empty entry at joinRoom, so "saw p2" meant "p2 is in the
|
|
108
|
+
* room" — not "p2's data arrived". A correct scene and four whose replication
|
|
109
|
+
* was completely dead produced byte-identical green reports.
|
|
110
|
+
*
|
|
111
|
+
* A desync is the purest silent failure this engine can have: both clients
|
|
112
|
+
* run, neither errors, and they are playing different games.
|
|
113
|
+
*
|
|
114
|
+
* Pure over snapshots, like `multiplayProblems`, because the test for "the
|
|
115
|
+
* clients disagreed" must not depend on being able to REPRODUCE a
|
|
116
|
+
* disagreement.
|
|
117
|
+
*/
|
|
118
|
+
/** One account's truth, and what every other client believes about it. */
|
|
119
|
+
interface AgreementSnapshot {
|
|
120
|
+
/** The account that OWNS these values. */
|
|
121
|
+
account: string;
|
|
122
|
+
/** Read from the owner node the way the sender reads it: key → value. */
|
|
123
|
+
truth: Record<string, JsonValue>;
|
|
124
|
+
/**
|
|
125
|
+
* Observer account → what its spawned copy holds, or `null` when it never
|
|
126
|
+
* materialised this account at all.
|
|
127
|
+
*/
|
|
128
|
+
seen: Record<string, Record<string, JsonValue | undefined> | null>;
|
|
129
|
+
}
|
|
130
|
+
type DisagreementKind = "missing" | "absent" | "mismatch" | "shape" | "erased";
|
|
131
|
+
interface Disagreement {
|
|
132
|
+
kind: DisagreementKind;
|
|
133
|
+
/** Whose value it is. */
|
|
134
|
+
account: string;
|
|
135
|
+
/** Who is wrong about it. */
|
|
136
|
+
observer: string;
|
|
137
|
+
/** The sync key, or `''` for a whole missing instance. */
|
|
138
|
+
key: string;
|
|
139
|
+
truth?: JsonValue;
|
|
140
|
+
seen?: JsonValue;
|
|
141
|
+
/** One sentence a reader can act on. */
|
|
142
|
+
summary: string;
|
|
143
|
+
}
|
|
144
|
+
interface AgreementOptions {
|
|
145
|
+
absTol?: number;
|
|
146
|
+
relTol?: number;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Every way the clients disagree, most structural first.
|
|
150
|
+
*
|
|
151
|
+
* @param live snapshots taken after the match has quiesced
|
|
152
|
+
* @param late the same, for a client that joined AFTER everything settled
|
|
153
|
+
*/
|
|
154
|
+
declare function agreementProblems(live: AgreementSnapshot[], late?: AgreementSnapshot[], opts?: AgreementOptions): Disagreement[];
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/test/agreement-snapshot.d.ts
|
|
157
|
+
/**
|
|
158
|
+
* Read what every client actually holds, so `agreementProblems` can compare it.
|
|
159
|
+
*
|
|
160
|
+
* Both sides are read through the ENGINE'S OWN accessors — `readSyncKey` for
|
|
161
|
+
* the owner and the receive-side last-dot split for the copy — so the check
|
|
162
|
+
* cannot drift away from the protocol it is checking. Reading the copy the
|
|
163
|
+
* same raw way `setProp` writes it also respects the data-bag design: a
|
|
164
|
+
* replicated `score` that is not a declared prop still round-trips and still
|
|
165
|
+
* gets compared.
|
|
166
|
+
*/
|
|
167
|
+
interface AgreementClient {
|
|
168
|
+
account: string;
|
|
169
|
+
engine: Engine;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* One snapshot per account: its own truth, and what each other client believes.
|
|
173
|
+
*
|
|
174
|
+
* @param clients every client in the match
|
|
175
|
+
* @param observers the clients whose beliefs to record (defaults to `clients`)
|
|
176
|
+
*/
|
|
177
|
+
declare function takeAgreementSnapshots(clients: AgreementClient[], observers?: AgreementClient[]): AgreementSnapshot[];
|
|
178
|
+
//#endregion
|
|
101
179
|
//#region src/test/feel.d.ts
|
|
102
180
|
interface FeelOptions {
|
|
103
181
|
behaviors?: Record<string, BehaviorCtor>;
|
|
@@ -156,6 +234,20 @@ interface MultiplayOptions {
|
|
|
156
234
|
drive?: (player: SplitScreenPlayer, frame: number, seconds: number) => void | Promise<void>;
|
|
157
235
|
/** Called once after every client has joined, before the match runs. */
|
|
158
236
|
start?: (players: SplitScreenPlayer[]) => void | Promise<void>;
|
|
237
|
+
/** Seed every client's RNG (client i gets `seed + i`). Without it, Math.random. */
|
|
238
|
+
seed?: number;
|
|
239
|
+
/** Loader options — `stubMissingBehaviors` when the checker has no TypeScript. */
|
|
240
|
+
load?: LoadSceneOptions;
|
|
241
|
+
/**
|
|
242
|
+
* Bring one more client in AFTER the match has settled, and check it sees
|
|
243
|
+
* what everyone else does. Default true.
|
|
244
|
+
*
|
|
245
|
+
* This half is not optional in spirit: a value that stops changing is erased
|
|
246
|
+
* from the authoritative state by a partial patch, and the clients already in
|
|
247
|
+
* the room never notice — they applied it once and kept it on their node. The
|
|
248
|
+
* late joiner is the only observer that reads the snapshot fresh.
|
|
249
|
+
*/
|
|
250
|
+
lateJoin?: boolean;
|
|
159
251
|
}
|
|
160
252
|
interface MultiplayPlayer {
|
|
161
253
|
account: string;
|
|
@@ -178,6 +270,12 @@ interface MultiplayReport {
|
|
|
178
270
|
roomState: JsonObject;
|
|
179
271
|
/** What is wrong, in the order it matters. Empty when `ok`. */
|
|
180
272
|
problems: string[];
|
|
273
|
+
/** Every way the clients ended up holding DIFFERENT values. */
|
|
274
|
+
disagreements: Disagreement[];
|
|
275
|
+
/** How many replicated keys were compared — 0 means agreement was not tested. */
|
|
276
|
+
keysCompared: number;
|
|
277
|
+
/** Whether a late-joining client was checked too. */
|
|
278
|
+
lateJoinChecked: boolean;
|
|
181
279
|
}
|
|
182
280
|
/** Run a whole match headlessly and report what the clients ended up sharing. */
|
|
183
281
|
declare function playMultiplayer(opts: MultiplayOptions): Promise<MultiplayReport>;
|
|
@@ -528,4 +626,4 @@ interface PlaySession {
|
|
|
528
626
|
*/
|
|
529
627
|
declare function createPlaySession(json: unknown, opts?: PlaySessionOptions): Promise<PlaySession>;
|
|
530
628
|
//#endregion
|
|
531
|
-
export { type FacingOptions, type FacingReport, type FeelOptions, type FeelReport, type FramingEntry, type FramingOptions, type FramingReport, GroundingIssue, GroundingOptions, type LadderVerdict, type MultiplayOptions, type MultiplayPlayer, type MultiplayReport, NodeCapture, type Outcome, PlaySession, PlaySessionOptions, type PlaytestOptions, type PlaytestReport, type PlaytestRun, RunContext, RunFailure, RunResult, RunScriptOptions, type RungResult, type RungStatus, SceneCapture, ScriptStep, ValidateSceneOptions, ValidationResult, type Where, auditScene, captureScene, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, framingText, ladderText, ladderVerdict, multiplayProblems, multiplayText, playMultiplayer, playtest, playtestText, registerAllNodes, runScript, validateScene };
|
|
629
|
+
export { type AgreementSnapshot, type Disagreement, type DisagreementKind, type FacingOptions, type FacingReport, type FeelOptions, type FeelReport, type FramingEntry, type FramingOptions, type FramingReport, GroundingIssue, GroundingOptions, type LadderVerdict, type MultiplayOptions, type MultiplayPlayer, type MultiplayReport, NodeCapture, type Outcome, PlaySession, PlaySessionOptions, type PlaytestOptions, type PlaytestReport, type PlaytestRun, RunContext, RunFailure, RunResult, RunScriptOptions, type RungResult, type RungStatus, SceneCapture, ScriptStep, ValidateSceneOptions, ValidationResult, type Where, agreementProblems, auditScene, captureScene, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, framingText, ladderText, ladderVerdict, multiplayProblems, multiplayText, playMultiplayer, playtest, playtestText, registerAllNodes, runScript, takeAgreementSnapshots, validateScene };
|
package/dist/test.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { r as auditScene } from "./replay-
|
|
2
|
-
import { S as
|
|
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 };
|
|
1
|
+
import { r as auditScene } from "./replay-CEPyQtF_.js";
|
|
2
|
+
import { C as describeFraming, S as playtestText, _ as facingReport, a as registerAllNodes, b as findPlayer, c as ladderText, d as multiplayText, f as playMultiplayer, g as agreementProblems, h as takeAgreementSnapshots, 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 facingText, w as framingText, x as playtest, y as failingReplays } from "./test-DHYuFyAu.js";
|
|
3
|
+
export { agreementProblems, auditScene, captureScene, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, framingText, ladderText, ladderVerdict, multiplayProblems, multiplayText, playMultiplayer, playtest, playtestText, registerAllNodes, runScript, takeAgreementSnapshots, validateScene };
|
package/dist/vite.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as VERSION } from "./src-
|
|
1
|
+
import { t as VERSION } from "./src-CY21B462.js";
|
|
2
2
|
import { n as diffSignatures } from "./frame-report-BSMny7oe.js";
|
|
3
|
-
import { s as validateScene } from "./test-
|
|
3
|
+
import { s as validateScene } from "./test-DHYuFyAu.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
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e}from"./index-
|
|
1
|
+
import{n as e}from"./index-VesuVEhe.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};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{i as e,r as t,t as n}from"./index-
|
|
1
|
+
import{i as e,r as t,t as n}from"./index-VesuVEhe.js";function r(e,t,n,r=1){let i=Math.min(e.x0,e.x1)*r,a=Math.max(e.x0,e.x1)*r,o=Math.min(e.y0,e.y1)*r,s=Math.max(e.y0,e.y1)*r,c=Math.max(0,Math.min(Math.round(i),t-1)),l=Math.max(0,Math.min(Math.round(o),n-1));return{x:c,y:l,w:Math.max(1,Math.min(Math.round(a-i),t-c)),h:Math.max(1,Math.min(Math.round(s-o),n-l))}}function i(e,t,n,r){let{x:i,y:a,w:o,h:s}=r,c=new Uint8ClampedArray(o*s*4);for(let n=0;n<s;n++){let r=((a+n)*t+i)*4,s=n*o*4;c.set(e.subarray(r,r+o*4),s)}return{pixels:c,width:o,height:s}}function a(e){if(!e)return`// no scene loaded`;let t=e.source??e;return JSON.stringify(t,null,2)}async function o(e){let t=globalThis.navigator;if(!t?.clipboard?.writeText)return!1;try{return await t.clipboard.writeText(e),!0}catch{return!1}}async function s(e,t){let n=globalThis,r=n.navigator?.clipboard?.write;if(!r||!n.ImageData||!n.ClipboardItem)return!1;try{let i=e.createElement(`canvas`);i.width=t.width,i.height=t.height;let a=i.getContext(`2d`);if(!a)return!1;a.putImageData(new n.ImageData(t.pixels,t.width,t.height),0,0);let o=await new Promise(e=>i.toBlob(e,`image/png`));return o?(await r.call(n.navigator?.clipboard,[new n.ClipboardItem({"image/png":o})]),!0):!1}catch{return!1}}var c=[`nw`,`n`,`ne`,`e`,`se`,`s`,`sw`,`w`],l={nw:[0,0],n:[.5,0],ne:[1,0],e:[1,.5],se:[1,1],s:[.5,1],sw:[0,1],w:[0,.5]},u={nw:`nwse-resize`,se:`nwse-resize`,ne:`nesw-resize`,sw:`nesw-resize`,n:`ns-resize`,s:`ns-resize`,e:`ew-resize`,w:`ew-resize`},d=(e,t,n)=>Math.max(t,Math.min(n,e));function f(e){return{x:Math.min(e.x0,e.x1),y:Math.min(e.y0,e.y1),w:Math.abs(e.x1-e.x0),h:Math.abs(e.y1-e.y0)}}function p(e,t,n,r,i,a){let o=e.x,s=e.y,c=e.x+e.w,l=e.y+e.h;return t.includes(`w`)&&(o=d(o+n,0,i)),t.includes(`e`)&&(c=d(c+n,0,i)),t.includes(`n`)&&(s=d(s+r,0,a)),t.includes(`s`)&&(l=d(l+r,0,a)),{x:Math.min(o,c),y:Math.min(s,l),w:Math.max(1,Math.abs(c-o)),h:Math.max(1,Math.abs(l-s))}}function m(e,t,n,r,i){return{...e,x:d(e.x+t,0,Math.max(0,r-e.w)),y:d(e.y+n,0,Math.max(0,i-e.h))}}function h(e,t,n,r,i,a=8){let o=d(e.x+e.w/2-t/2,a,Math.max(a,r-t-a)),s=e.y+e.h+a,c=e.y-n-a;return{x:o,y:s+n<=i-a?s:c>=a?c:d(s,a,Math.max(a,i-n-a))}}function g(e,t,n,r,i,a,o=180,s=120){let c=Math.max(o,n),l=Math.max(s,r);return{x:Math.min(Math.max(0,e),Math.max(0,i-c)),y:Math.min(Math.max(0,t),Math.max(0,a-l)),w:c,h:l}}function _(e,t,n,r,i,a=4){let o=Math.min(Math.max(0,e.x),Math.max(0,r-t)),s=e.y+e.h+a,c=e.y-n-a;return s+n<=i?{x:o,y:s}:c>=0?{x:o,y:c}:{x:o,y:Math.min(Math.max(0,s),Math.max(0,i-n))}}function v(e,t,n){let r=e=>{e.key===`Escape`&&n()},i=e=>{let r=e.target;if(r){for(let e of t)if(e&&(e===r||e.contains?.(r)))return;n()}};return e.addEventListener?.(`keydown`,r),e.addEventListener?.(`pointerdown`,i),()=>{e.removeEventListener?.(`keydown`,r),e.removeEventListener?.(`pointerdown`,i)}}function y(e,t){for(let[n,r]of Object.entries(t))e.style[n]=r}var b=`rgba(18, 20, 26, 0.92)`,x=`1px solid rgba(255,255,255,0.14)`,S=`12px ui-monospace, SFMono-Regular, Menlo, monospace`,C=class{host;el;body;onClose=()=>{};x;y;w;h;constructor(e,t,n,r){this.host=t,this.x=r.x,this.y=r.y,this.w=r.w,this.h=r.h,this.el=e.createElement(`div`),y(this.el,{position:`absolute`,background:b,border:x,borderRadius:`8px`,color:`rgba(255,255,255,0.88)`,font:S,display:`flex`,flexDirection:`column`,overflow:`hidden`,zIndex:`40`,pointerEvents:`auto`,boxShadow:`0 8px 28px rgba(0,0,0,0.45)`});let i=e.createElement(`div`);i.textContent=n,y(i,{padding:`6px 28px 6px 10px`,background:`rgba(255,255,255,0.07)`,cursor:`move`,userSelect:`none`,touchAction:`none`,fontWeight:`700`}),this.el.appendChild(i);let a=e.createElement(`div`);a.textContent=`×`,a.title=`close`,y(a,{position:`absolute`,top:`2px`,right:`8px`,cursor:`pointer`,fontSize:`16px`,lineHeight:`20px`,opacity:`0.7`}),a.addEventListener(`click`,()=>this.onClose()),this.el.appendChild(a),this.body=e.createElement(`div`),y(this.body,{flex:`1`,overflow:`auto`,padding:`8px 10px`}),this.el.appendChild(this.body);let o=e.createElement(`div`);o.textContent=`◢`,y(o,{position:`absolute`,right:`2px`,bottom:`0`,cursor:`nwse-resize`,opacity:`0.5`,userSelect:`none`,touchAction:`none`}),this.el.appendChild(o),this.wireDrag(i,(e,t)=>{this.x+=e,this.y+=t,this.layout()}),this.wireDrag(o,(e,t)=>{this.w+=e,this.h+=t,this.layout()}),this.layout(),t.appendChild(this.el)}remove(){this.el.remove()}layout(){let e=this.host.getBoundingClientRect(),t=g(this.x,this.y,this.w,this.h,e.width,e.height);this.x=t.x,this.y=t.y,this.w=t.w,this.h=t.h,y(this.el,{left:`${t.x}px`,top:`${t.y}px`,width:`${t.w}px`,height:`${t.h}px`})}wireDrag(e,t){let n=null,r=0,i=0;e.addEventListener(`pointerdown`,t=>{n=t.pointerId,r=t.clientX,i=t.clientY,e.setPointerCapture?.(t.pointerId)}),e.addEventListener(`pointermove`,e=>{e.pointerId===n&&(t(e.clientX-r,e.clientY-i),r=e.clientX,i=e.clientY)});let a=e=>{e.pointerId===n&&(n=null)};e.addEventListener(`pointerup`,a),e.addEventListener(`pointercancel`,a)}},w=4,T=`#6ee7dc`,E=`rgba(18,20,26,0.94)`,D=`12px ui-monospace, Menlo, monospace`;function O(e){let{doc:t,container:n}=e,r=t.createElement(`div`);y(r,{position:`absolute`,inset:`0`,cursor:`crosshair`,zIndex:`80`,pointerEvents:`auto`,userSelect:`none`,touchAction:`none`});let i=t.createElement(`div`);y(i,{position:`absolute`,inset:`0`,background:`rgba(10,14,20,0.35)`,pointerEvents:`none`}),r.appendChild(i);let a=t.createElement(`div`);y(a,{position:`absolute`,outline:`1px solid ${T}`,boxShadow:`0 0 0 9999px rgba(10,14,20,0.35)`,cursor:`grab`,display:`none`,pointerEvents:`auto`}),r.appendChild(a);let o=t.createElement(`div`);y(o,{position:`absolute`,padding:`2px 6px`,borderRadius:`4px`,background:E,color:T,font:D,pointerEvents:`none`,display:`none`,whiteSpace:`nowrap`}),r.appendChild(o);let s=t.createElement(`div`);s.textContent=`drag to select · Esc to cancel`,y(s,{position:`absolute`,top:`10px`,left:`50%`,transform:`translateX(-50%)`,padding:`4px 10px`,borderRadius:`6px`,background:E,color:`rgba(255,255,255,0.85)`,font:D,pointerEvents:`none`,whiteSpace:`nowrap`}),r.appendChild(s);let d=null,g=`idle`,_={x:0,y:0},v={x:0,y:0,w:0,h:0},b=()=>({w:n.clientWidth||1,h:n.clientHeight||1});for(let e of c){let n=t.createElement(`div`),[r,i]=l[e];y(n,{position:`absolute`,left:`${r*100}%`,top:`${i*100}%`,width:`10px`,height:`10px`,marginLeft:`-5px`,marginTop:`-5px`,borderRadius:`2px`,background:T,border:`1px solid rgba(10,14,20,0.75)`,cursor:u[e],pointerEvents:`auto`}),n.addEventListener(`pointerdown`,t=>{k(t,e)}),a.appendChild(n)}let x=t.createElement(`div`);y(x,{position:`absolute`,display:`none`,gap:`6px`,padding:`6px`,borderRadius:`8px`,background:E,border:`1px solid rgba(255,255,255,0.18)`,font:D,pointerEvents:`auto`,whiteSpace:`nowrap`,boxShadow:`0 4px 16px rgba(0,0,0,0.45)`});let S=(e,n,r)=>{let i=t.createElement(`div`);return i.textContent=e,y(i,{padding:`5px 10px`,borderRadius:`5px`,cursor:`pointer`,color:n?`#08121a`:`rgba(255,255,255,0.85)`,background:n?T:`rgba(255,255,255,0.08)`,userSelect:`none`}),i.addEventListener(`pointerdown`,e=>{C(e),r()}),i};x.appendChild(S(`Copy to clipboard`,!0,()=>N())),x.appendChild(S(`Cancel`,!1,()=>I())),r.appendChild(x);function C(e){e.preventDefault?.(),e.stopPropagation?.()}function O(e){let t=e,r=n.getBoundingClientRect?.()??{left:0,top:0};return{x:(t.clientX??0)-r.left,y:(t.clientY??0)-r.top}}function k(e,t){C(e),_=O(e),g=t,t===`create`?d={x:_.x,y:_.y,w:0,h:0}:d&&(v=d),y(x,{display:`none`}),y(a,{cursor:t===`move`?`grabbing`:`crosshair`});let n=e.pointerId;n!==void 0&&r.setPointerCapture?.(n),A()}function A(){if(!d){y(a,{display:`none`}),y(o,{display:`none`}),y(i,{display:`block`});return}y(i,{display:`none`}),y(a,{display:`block`,left:`${d.x}px`,top:`${d.y}px`,width:`${d.w}px`,height:`${d.h}px`});let t=e.scale();o.textContent=`${Math.round(d.w*t)}×${Math.round(d.h*t)}`;let n=d.y-24;y(o,{display:`block`,left:`${Math.max(2,d.x)}px`,top:`${n>=2?n:d.y+4}px`})}function j(){if(!d||d.w<w||d.h<w)return;y(x,{display:`flex`});let e=b(),t=x.offsetWidth||200,n=x.offsetHeight||36,r=h(d,t,n,e.w,e.h);y(x,{left:`${r.x}px`,top:`${r.y}px`})}r.addEventListener(`pointerdown`,e=>{k(e,`create`)}),a.addEventListener(`pointerdown`,e=>{k(e,`move`)}),r.addEventListener(`pointermove`,e=>{if(g===`idle`||!d)return;let t=O(e),n=b();if(g===`create`){let e=f({x0:_.x,y0:_.y,x1:t.x,y1:t.y});d={x:Math.max(0,Math.min(e.x,n.w)),y:Math.max(0,Math.min(e.y,n.h)),w:Math.min(e.w,n.w-Math.max(0,Math.min(e.x,n.w))),h:Math.min(e.h,n.h-Math.max(0,Math.min(e.y,n.h)))}}else d=g===`move`?m(v,t.x-_.x,t.y-_.y,n.w,n.h):p(v,g,t.x-_.x,t.y-_.y,n.w,n.h);A()});let M=()=>{g!==`idle`&&(g===`create`&&d&&(d.w<w||d.h<w)&&(d=null),g=`idle`,y(a,{cursor:`grab`}),s.textContent=d?`drag the handles or the middle · Enter to copy · Esc to cancel`:`drag to select · Esc to cancel`,A(),j())};r.addEventListener(`pointerup`,M),r.addEventListener(`pointercancel`,M);function N(){let t=d;I(),t&&e.onCopy(t)}let P=e=>{let t=e.key;t===`Escape`?I():t===`Enter`&&d&&N()};t.addEventListener?.(`keydown`,P);let F=!1;function I(){F||(F=!0,t.removeEventListener?.(`keydown`,P),r.remove(),e.onClose())}return n.appendChild(r),{element:r,close:I}}var k=96,A=200,j=8192;function M(e){return Array.isArray(e)?`Array(${e.length})`:`Object(${Object.keys(e).length} keys)`}function N(e,t){if(Array.isArray(e)&&e.some(e=>typeof e==`object`&&!!e)){let t=e.slice(0,A).map(e=>JSON.stringify(e)),n=e.length-A;return t.join(`
|
|
2
2
|
`)+(n>0?`\n… ${n} more`:``)}return t.length>j?`${t.slice(0,j)} … (${t.length-j} more chars)`:t}function P(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}var F=2,I=4;function L(e,t={}){let n=t.doc??(typeof document<`u`?document:null);if(!n)return null;let r=t.container??(typeof document<`u`?document.body:null);return!r||typeof r.appendChild!=`function`?null:new B(e,r,n,t.statsSource,t.actions,t.frameSource)}var R=300,z=[`log`,`info`,`warn`,`error`,`debug`],B=class{engine;container;doc;statsSource;actions;frameSource;panels=new Map;cleanups=[];menuButton;dropdown=null;dismissDropdown=null;menuPos={x:8,y:8};selected=null;collapsedFlags=new Map;statsChip=null;logRows=[];levelEnabled={debug:!0,info:!0,warn:!0,error:!0};consoleCapture=!1;consolePatched=[];frame=0;editing=0;detailOpen=new WeakMap;resumeScale=1;timeEls=null;hovering=!1;constructor(e,t,n,r,i=[],a){this.engine=e,this.container=t,this.doc=n,this.statsSource=r,this.actions=i,this.frameSource=a,this.menuButton=n.createElement(`div`),this.menuButton.textContent=`☰ debug`,y(this.menuButton,{position:`absolute`,top:`8px`,left:`8px`,padding:`4px 10px`,background:`rgba(18,20,26,0.85)`,border:`1px solid rgba(255,255,255,0.18)`,borderRadius:`6px`,color:`rgba(255,255,255,0.85)`,font:`12px ui-monospace, Menlo, monospace`,cursor:`pointer`,userSelect:`none`,zIndex:`50`,pointerEvents:`auto`}),this.bindMenuDrag(),t.style.position||(t.style.position=`relative`),t.appendChild(this.menuButton),this.cleanups.push(e.log.added.connect(e=>{this.pushLog({level:e.level,source:`engine`,text:e.parts.map(U).join(` `)})})),this.cleanups.push(e.sceneChanged.connect(()=>{this.selected=null,this.applyColliderScope(),this.renderExplorer(),this.renderInspector()})),this.cleanups.push(e.updated.connect(()=>{this.frame+=1,this.frame%30==0&&(this.renderExplorer(),this.renderStats(),this.editing===0&&!this.hovering&&this.renderInspector(),this.syncTime())}))}isOpen(e){return e===`copyScene`||e===`captureRegion`?!1:e===`colliders`?this.colliderMode!==`off`:e===`stats`?this.statsChip!==null:this.panels.has(e)}colliderMode=`off`;setColliders(e){this.colliderMode=e,this.applyColliderScope()}applyColliderScope(){let e=this.colliderMode;for(let t of n(`2d`).concat(n(`3d`)))t.debugDraw=e!==`off`,t.debugScope=e===`selected`?this.selected:null}open(e){if(e===`stats`){this.openStatsChip();return}if(this.panels.has(e)){e===`explorer`&&this.renderExplorer(),e===`inspector`&&this.renderInspector(),e===`logs`&&this.renderLogs(),e===`time`&&this.renderTime();return}let t=new C(this.doc,this.container,{explorer:`Explorer`,inspector:`Inspector`,logs:`Logs`,time:`Time`}[e],{explorer:{x:12,y:44,w:240,h:320},inspector:{x:264,y:44,w:280,h:320},logs:{x:12,y:380,w:532,h:200},time:{x:556,y:44,w:260,h:150}}[e]);t.onClose=()=>this.close(e),e===`inspector`&&(t.body.addEventListener(`pointerenter`,()=>{this.hovering=!0}),t.body.addEventListener(`pointerleave`,()=>{this.hovering=!1})),this.panels.set(e,t),e===`explorer`&&this.renderExplorer(),e===`inspector`&&this.renderInspector(),e===`logs`&&this.renderLogs(),e===`time`&&this.renderTime()}close(e){if(e===`inspector`&&(this.hovering=!1),e===`stats`){this.statsChip?.remove(),this.statsChip=null;return}let t=this.panels.get(e);t&&(t.remove(),this.panels.delete(e))}toggle(e){if(e===`copyScene`){this.copyScene();return}if(e===`captureRegion`){this.captureRegion();return}if(e===`colliders`){this.setColliders({off:`all`,all:`selected`,selected:`off`}[this.colliderMode]);return}this.isOpen(e)?this.close(e):this.open(e)}setLevelEnabled(e,t){this.levelEnabled[e]=t,this.renderLogs()}setConsoleCapture(e){if(e!==this.consoleCapture)if(this.consoleCapture=e,e){this.renderLogs();let e=console;for(let t of z){let n=e[t];e[t]=(...e)=>{n.apply(console,e);let r=t===`log`?`info`:t;this.pushLog({level:r,source:`console`,text:e.map(U).join(` `)})},this.consolePatched.push(()=>{e[t]=n})}}else{for(let e of this.consolePatched)e();this.consolePatched=[],this.renderLogs()}}dispose(){this.engine.debugSelection=null,this.setColliders(`off`),this.setConsoleCapture(!1);for(let e of this.cleanups)e();for(let e of[...this.panels.keys()])this.close(e);this.close(`stats`),this.closeDropdown(),this.menuButton.remove()}menuLabel(e,t){let n=e===`colliders`&&this.colliderMode!==`off`?` · ${this.colliderMode}`:``;return`${this.isOpen(e)?`✓ `:``}${t}${n}`}bindMenuDrag(){let e=null,t=!1;this.menuButton.addEventListener(`pointerdown`,n=>{let r=n;e={x:r.clientX??0,y:r.clientY??0,bx:this.menuPos.x,by:this.menuPos.y},t=!1,r.pointerId!==void 0&&this.menuButton.setPointerCapture?.(r.pointerId)}),this.menuButton.addEventListener(`pointermove`,n=>{if(!e)return;let r=n,i=(r.clientX??0)-e.x,a=(r.clientY??0)-e.y;!t&&Math.hypot(i,a)<I||(t||(t=!0,this.closeDropdown(),y(this.menuButton,{cursor:`grabbing`})),this.moveMenuButton(e.bx+i,e.by+a))});let n=()=>{if(!e)return;let n=t;e=null,t=!1,y(this.menuButton,{cursor:`pointer`}),n||this.toggleDropdown()};this.menuButton.addEventListener(`pointerup`,n),this.menuButton.addEventListener(`pointercancel`,n)}moveMenuButton(e,t){let n=this.menuButton,r=g(e,t,n.offsetWidth??0,n.offsetHeight??0,this.container.clientWidth||0,this.container.clientHeight||0,0,0);this.menuPos={x:r.x,y:r.y},y(this.menuButton,{left:`${r.x}px`,top:`${r.y}px`})}closeDropdown(){this.dismissDropdown?.(),this.dismissDropdown=null,this.dropdown?.remove(),this.dropdown=null}toggleDropdown(){if(this.dropdown){this.closeDropdown();return}let e=this.doc.createElement(`div`);y(e,{position:`absolute`,background:`rgba(18,20,26,0.95)`,border:`1px solid rgba(255,255,255,0.18)`,borderRadius:`6px`,font:`12px ui-monospace, Menlo, monospace`,color:`rgba(255,255,255,0.85)`,zIndex:`60`,pointerEvents:`auto`,overflow:`hidden`});for(let[t,n]of[[`explorer`,`Explorer`],[`inspector`,`Inspector`],[`logs`,`Logs`],[`time`,`Time`],[`copyScene`,`Copy scene JSON`],[`captureRegion`,`Capture region`],[`stats`,`Stats`],[`colliders`,`Colliders`]]){let r=this.doc.createElement(`div`);r.textContent=this.menuLabel(t,n),y(r,{padding:`6px 14px`,cursor:`pointer`,userSelect:`none`}),r.addEventListener(`click`,()=>{if(this.toggle(t),t===`colliders`){r.textContent=this.menuLabel(t,n);return}this.closeDropdown()}),e.appendChild(r)}for(let t of this.actions){let n=this.doc.createElement(`div`);n.textContent=t.label,y(n,{padding:`6px 14px`,cursor:`pointer`,userSelect:`none`,borderTop:`1px solid rgba(255,255,255,0.14)`,color:`rgba(158,232,220,0.95)`}),n.addEventListener(`click`,()=>{this.closeDropdown(),t.run()}),e.appendChild(n)}this.container.appendChild(e),this.dropdown=e;let t=e,n=this.menuButton,r=_({x:this.menuPos.x,y:this.menuPos.y,w:n.offsetWidth??0,h:n.offsetHeight??26},t.offsetWidth??0,t.offsetHeight??0,this.container.clientWidth||0,this.container.clientHeight||0);y(e,{left:`${r.x}px`,top:`${r.y}px`}),this.dismissDropdown=v(this.doc,[e,this.menuButton],()=>this.closeDropdown())}openStatsChip(){if(this.statsChip)return;let e=this.doc.createElement(`div`);y(e,{position:`absolute`,top:`8px`,right:`8px`,padding:`4px 10px`,background:`rgba(18,20,26,0.85)`,border:`1px solid rgba(255,255,255,0.18)`,borderRadius:`6px`,color:`rgba(255,255,255,0.85)`,font:`12px ui-monospace, Menlo, monospace`,textAlign:`right`,whiteSpace:`pre`,userSelect:`none`,pointerEvents:`none`,zIndex:`70`}),this.container.appendChild(e),this.statsChip=e,this.renderStats()}renderStats(){let e=this.statsChip;if(!e)return;let t=this.engine.stats(),n=this.statsSource?.()??{},r=[`nodes ${t.nodes}`,...n.triangles===void 0?[]:[`tris ${H(n.triangles)}`],...n.drawCalls===void 0?[]:[`calls ${n.drawCalls}`]].join(` · `),i=t.phases,a=`\nphys ${i.fixedMs.toFixed(1)} · logic ${i.updateMs.toFixed(1)} · draw ${i.renderMs.toFixed(1)} · other ${i.otherMs.toFixed(1)}`;e.textContent=`${Math.round(t.fps)} fps · ${t.frameMs.toFixed(1)} ms\n${r}${a}`}renderExplorer(){let e=this.panels.get(`explorer`);if(!e)return;let t=e.body.scrollTop;V(e.body);let n=this.engine.scene?.root;if(!n){e.body.scrollTop=t;return}let r=(e,t)=>{let n=e.constructor,i=e.children.length>0,a=this.collapsedFlags.get(e)===!0,o=this.doc.createElement(`div`);y(o,{display:`flex`,alignItems:`center`,cursor:`pointer`,padding:`1px 2px`,borderRadius:`3px`,background:e===this.selected?`rgba(110,160,255,0.25)`:`transparent`});let s=this.doc.createElement(`span`);s.textContent=i?a?`▸`:`▾`:`·`,y(s,{width:`14px`,flex:`none`,textAlign:`center`,opacity:i?`0.85`:`0.25`,userSelect:`none`}),i&&s.addEventListener(`click`,t=>{t.stopPropagation?.(),this.collapsedFlags.set(e,!a),this.renderExplorer()}),o.appendChild(s);let c=this.doc.createElement(`span`);c.textContent=e.name,y(c,{whiteSpace:`nowrap`}),o.appendChild(c);let l=this.doc.createElement(`span`);if(l.textContent=` ${n.typeName}`,y(l,{opacity:`0.45`,whiteSpace:`nowrap`,fontSize:`10px`}),o.appendChild(l),i&&a){let t=this.doc.createElement(`span`);t.textContent=` (${e.children.length})`,y(t,{opacity:`0.35`,fontSize:`10px`}),o.appendChild(t)}if(o.addEventListener(`click`,()=>{this.selected=e,this.engine.debugSelection=e,this.applyColliderScope(),this.open(`inspector`),this.renderExplorer(),this.renderInspector()}),t.appendChild(o),i&&!a){let n=this.doc.createElement(`div`);y(n,{marginLeft:`8px`,paddingLeft:`8px`,borderLeft:`1px solid rgba(255,255,255,0.14)`});for(let t of e.children)r(t,n);t.appendChild(n)}};r(n,e.body),e.body.scrollTop=t}renderInspector(){let e=this.panels.get(`inspector`);if(!e)return;let n=e.body.scrollTop;V(e.body);let r=this.selected;if(r&&r.tree===null&&(this.selected=null,this.engine.debugSelection=null,this.applyColliderScope(),r=null),!r){let t=this.doc.createElement(`div`);t.textContent=`select a node in the Explorer`,y(t,{opacity:`0.6`}),e.body.appendChild(t);return}let i=r.constructor,a=this.doc.createElement(`div`);if(a.textContent=`${r.getPath()} · ${i.typeName}${r.uid?` · ${r.uid}`:``}`,y(a,{fontWeight:`700`,marginBottom:`6px`,whiteSpace:`pre-wrap`}),e.body.appendChild(a),r.groups.size>0){let t=this.doc.createElement(`div`);t.textContent=`groups: ${[...r.groups].join(`, `)}`,y(t,{opacity:`0.7`,marginBottom:`6px`}),e.body.appendChild(t)}let o=t(i),s=r;for(let t of Object.keys(o)){let n=o[t];this.renderValueRow(e.body,r,t,{read:()=>s[t],write:e=>{s[t]=e},options:n?.options,variants:n?.variants})}e.body.scrollTop=n}renderValueRow(t,n,r,i){let a=i.read(),o=i.options,s={get[r](){return i.read()},set[r](e){i.write(e)}},c=this.doc.createElement(`div`);y(c,{display:`flex`,gap:`6px`,alignItems:`center`,margin:`2px 0`});let l=this.doc.createElement(`div`);l.textContent=r,y(l,{minWidth:`84px`,opacity:`0.75`}),c.appendChild(l);let u=(e,t)=>{let i=this.doc.createElement(`input`);return i.type=`number`,i.value=String(e),y(i,G(`70px`)),this.trackEditing(i),i.addEventListener(`change`,()=>{let e=Number(i.value);Number.isFinite(e)?t(e):i.value=String(n[r])}),i};if(typeof a==`number`)c.appendChild(u(a,e=>{s[r]=e}));else if(typeof a==`boolean`){let e=this.doc.createElement(`input`);e.type=`checkbox`,e.checked=a,this.trackEditing(e),e.addEventListener(`change`,()=>{s[r]=e.checked}),c.appendChild(e)}else if(typeof a==`string`&&o&&o.length>0){let e=this.doc.createElement(`select`);for(let t of o.includes(a)?o:[a,...o]){let n=this.doc.createElement(`option`);n.value=t,n.textContent=t,t===a&&(n.selected=!0),e.appendChild(n)}e.value=a,this.trackEditing(e),e.addEventListener(`change`,()=>{s[r]=e.value}),y(e,G(`110px`)),c.appendChild(e)}else if(typeof a==`string`){let e=this.doc.createElement(`input`);e.type=`text`,e.value=a,this.trackEditing(e),y(e,G(`140px`)),e.addEventListener(`change`,()=>{s[r]=e.value}),c.appendChild(e)}else if(Array.isArray(a)&&a.length<=8&&a.every(e=>typeof e==`number`))for(let e=0;e<a.length;e++)c.appendChild(u(a[e],t=>{let n=[...s[r]];n[e]=t,s[r]=n}));else if(P(a)){let o=this.doc.createElement(`div`);y(o,{marginLeft:`10px`,paddingLeft:`8px`,borderLeft:`1px solid rgba(255,255,255,0.14)`});let s=i.variants?.tag,l=Object.keys(a);s&&!l.includes(s)&&l.unshift(s);let u=i.path??r;for(let t of l){let r=t===s;this.renderValueRow(o,n,t,{path:`${u}.${t}`,read:()=>i.read()[t]??(r?``:null),write:n=>{if(r&&i.variants){let t=i.variants.byTag[String(n)];if(t!==void 0){i.write(e(t));return}}i.write({...i.read(),[t]:n})},options:r&&i.variants?Object.keys(i.variants.byTag):void 0})}t.appendChild(c),t.appendChild(o);return}else{let o=JSON.stringify(e(a));if(o.length<=k){let e=this.doc.createElement(`div`);e.textContent=o,y(e,{opacity:`0.65`,whiteSpace:`pre-wrap`,wordBreak:`break-all`}),c.appendChild(e)}else{let e=i.path??r,s=this.detailOpen.get(n)?.has(e)??!1,l=this.doc.createElement(`div`);if(l.textContent=`${s?`▾`:`▸`} ${M(a)}`,y(l,{opacity:`0.75`,cursor:`pointer`,userSelect:`none`}),l.addEventListener(`click`,()=>{let t=this.detailOpen.get(n);t||(t=new Set,this.detailOpen.set(n,t)),s?t.delete(e):t.add(e),this.renderInspector()}),c.appendChild(l),t.appendChild(c),s){let e=this.doc.createElement(`div`);e.textContent=N(a,o),y(e,{opacity:`0.6`,whiteSpace:`pre-wrap`,wordBreak:`break-all`,marginLeft:`10px`,paddingLeft:`8px`,borderLeft:`1px solid rgba(255,255,255,0.14)`}),t.appendChild(e)}return}}t.appendChild(c)}trackEditing(e){e.addEventListener(`focus`,()=>{this.editing+=1}),e.addEventListener(`blur`,()=>{this.editing=Math.max(0,this.editing-1)})}pushLog(e){this.logRows.push(e),this.logRows.length>R&&this.logRows.splice(0,this.logRows.length-R),this.renderLogs()}async copyScene(){let e=await o(a(this.engine.scene??null));return this.toast(e?`scene JSON copied`:`could not reach the clipboard`),e}captureRegion(){this.capture||=(this.captureScale=globalThis.devicePixelRatio??1,this.frameSource?.().then(e=>{this.captureScale=e.width/Math.max(1,this.container.clientWidth||e.width)}).catch(()=>{}),O({doc:this.doc,container:this.container,scale:()=>this.captureScale,onCopy:e=>void this.copyRegion({x0:e.x,y0:e.y,x1:e.x+e.w,y1:e.y+e.h}),onClose:()=>{this.capture=null}}))}capture=null;captureScale=1;async copyRegion(e){let t=this.frameSource;if(!t)return this.toast(`no renderer to capture from`),!1;try{let n=await t(),a=n.width/Math.max(1,this.container.clientWidth||n.width),o=r(e,n.width,n.height,a),c=i(n.pixels,n.width,n.height,o),l=await s(this.doc,c);return this.toast(l?`copied ${c.width}×${c.height}`:`could not copy the image`),l}catch(e){return this.toast(`capture failed: ${e instanceof Error?e.message:String(e)}`),!1}}toast(e){let t=this.doc.createElement(`div`);t.textContent=e,y(t,{position:`absolute`,bottom:`16px`,left:`50%`,transform:`translateX(-50%)`,padding:`6px 12px`,borderRadius:`6px`,background:`rgba(18,20,26,0.92)`,color:`rgba(255,255,255,0.9)`,font:`12px ui-monospace, Menlo, monospace`,zIndex:`90`,pointerEvents:`none`}),this.container.appendChild(t),setTimeout(()=>t.remove(),1800)}setTimeScale(e){Number.isFinite(e)&&(this.engine.timeScale=Math.max(0,e),this.syncTime())}setPaused(e){e?(this.engine.timeScale>0&&(this.resumeScale=this.engine.timeScale),this.engine.timeScale=0):this.engine.timeScale=this.resumeScale>0?this.resumeScale:1,this.syncTime()}get paused(){return this.engine.timeScale===0}nextFrame(){this.paused||this.setPaused(!0),this.engine.step({ignorePause:!0}),this.syncTime()}syncTime(){let e=this.timeEls;if(!e)return;let t=this.engine.timeScale,n=String(Math.round(t*1e3)/1e3);this.editing===0&&(e.slider.value=String(Math.min(t,F)),e.box.value=n),e.readout.textContent=`timeScale ${n}${this.paused?` · paused`:``}`,e.pause.textContent=this.paused?`▶ Resume`:`⏸ Pause`}renderTime(){let e=this.panels.get(`time`);if(!e)return;V(e.body),this.timeEls=null;let t=this.doc.createElement(`div`);y(t,{marginBottom:`6px`,opacity:`0.85`}),e.body.appendChild(t);let n=this.doc.createElement(`input`);n.type=`range`,n.min=`0`,n.max=String(F),n.step=`0.05`,n.value=String(Math.min(this.engine.timeScale,F)),y(n,{width:`100%`,marginBottom:`6px`}),this.trackEditing(n),n.addEventListener(`input`,()=>this.setTimeScale(Number(n.value))),e.body.appendChild(n);let r=this.doc.createElement(`div`);y(r,{display:`flex`,gap:`6px`,alignItems:`center`,marginBottom:`8px`});let i=this.doc.createElement(`input`);i.type=`number`,i.min=`0`,i.step=`0.05`,i.value=String(this.engine.timeScale),y(i,G(`72px`)),this.trackEditing(i),i.addEventListener(`change`,()=>{let e=Number(i.value);Number.isFinite(e)&&this.setTimeScale(e),this.syncTimeAfterEdit()}),r.appendChild(i);for(let e of[.25,.5,1,2]){let t=this.doc.createElement(`div`);t.textContent=`${e}×`,y(t,{cursor:`pointer`,opacity:`0.7`,padding:`2px 4px`}),t.addEventListener(`click`,()=>this.setTimeScale(e)),r.appendChild(t)}e.body.appendChild(r);let a=this.doc.createElement(`div`);y(a,{display:`flex`,gap:`8px`});let o=this.doc.createElement(`div`);y(o,W()),o.addEventListener(`click`,()=>this.setPaused(!this.paused)),a.appendChild(o);let s=this.doc.createElement(`div`);s.textContent=`⏭ Next frame`,s.title=`Pauses, then advances one fixed step`,y(s,W()),s.addEventListener(`click`,()=>this.nextFrame()),a.appendChild(s),e.body.appendChild(a),this.timeEls={slider:n,box:i,readout:t,pause:o},this.syncTime()}syncTimeAfterEdit(){let e=this.timeEls;if(!e)return;let t=this.engine.timeScale;e.slider.value=String(Math.min(t,F)),e.box.value=String(Math.round(t*1e3)/1e3)}renderLogs(){let e=this.panels.get(`logs`);if(!e)return;V(e.body);let t=this.doc.createElement(`div`);y(t,{display:`flex`,gap:`8px`,marginBottom:`4px`,flexWrap:`wrap`});for(let e of[`debug`,`info`,`warn`,`error`]){let n=this.doc.createElement(`div`);n.textContent=`${this.levelEnabled[e]?`✓`:`·`}${e}`,y(n,{cursor:`pointer`,opacity:this.levelEnabled[e]?`1`:`0.45`}),n.addEventListener(`click`,()=>this.setLevelEnabled(e,!this.levelEnabled[e])),t.appendChild(n)}let n=this.doc.createElement(`div`);n.textContent=`${this.consoleCapture?`✓`:`·`}console`,y(n,{cursor:`pointer`,marginLeft:`auto`}),n.addEventListener(`click`,()=>this.setConsoleCapture(!this.consoleCapture)),t.appendChild(n),e.body.appendChild(t);let r={debug:`rgba(255,255,255,0.5)`,info:`rgba(255,255,255,0.85)`,warn:`#ffc861`,error:`#ff6b6b`};for(let t of this.logRows){if(!this.levelEnabled[t.level])continue;let n=this.doc.createElement(`div`);n.textContent=`[${t.source===`console`?`console`:t.level}] ${t.text}`,y(n,{color:r[t.level],whiteSpace:`pre-wrap`}),e.body.appendChild(n)}}};function V(e){for(let t of[...e.children])t.remove()}function H(e){return e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e3?`${(e/1e3).toFixed(1)}k`:String(e)}function U(e){if(typeof e==`string`)return e;if(e instanceof Error){let t=e.stack?.split(`
|
|
3
3
|
`)[1]?.trim();return`${e.name}: ${e.message}${t?` (${t})`:``}`}try{let t=JSON.stringify(e);return t===`{}`||t===void 0?String(e):t}catch{return String(e)}}function W(){return{background:`rgba(255,255,255,0.08)`,border:`1px solid rgba(255,255,255,0.2)`,borderRadius:`4px`,padding:`4px 8px`,cursor:`pointer`,userSelect:`none`}}function G(e){return{width:e,background:`rgba(255,255,255,0.08)`,border:`1px solid rgba(255,255,255,0.2)`,borderRadius:`4px`,color:`inherit`,font:`inherit`,padding:`2px 4px`}}export{L as attachDebugOverlay};
|