@adatechnology/game-test-room 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/index.d.ts +55 -2
- package/dist/index.global.js +861 -123
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +789 -97
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createDestructionRenderer, specFromObject3D } from "@adatechnology/game
|
|
|
4
4
|
import { createArsenal, DEFAULT_LOADOUT } from "@adatechnology/game-weapons";
|
|
5
5
|
import { createFluids, createItemUse, ITEM_USES, resolveStrings as resolveFluidStrings } from "@adatechnology/game-fluids";
|
|
6
6
|
import { createFluidRenderer } from "@adatechnology/game-fluids-three";
|
|
7
|
-
import { checkAnimation, checkDestructible, checkSpec } from "@adatechnology/game-contracts";
|
|
7
|
+
import { checkAnimation, checkBench, checkDestructible, checkSpec } from "@adatechnology/game-contracts";
|
|
8
8
|
|
|
9
9
|
// src/items.ts
|
|
10
10
|
import { buildFrame } from "@adatechnology/game-destruction";
|
|
@@ -219,6 +219,21 @@ var en = {
|
|
|
219
219
|
"ui.animations": "Animations",
|
|
220
220
|
"ui.animations_aim": "Aim at an item or hold it to see its animations.",
|
|
221
221
|
"ui.animations_none": "This item brings no animations.",
|
|
222
|
+
"mod.bench_ok": "{name}: bench ok ({controls} controls, {layers} layers, {metrics} metrics)",
|
|
223
|
+
"mod.bench_fail": "{name}: bench broken: {problems}",
|
|
224
|
+
"mod.metric_failed": '{name}: metric "{metric}" threw and shows n/a: {error}',
|
|
225
|
+
"mod.listener_failed": "{name}: a bench listener threw: {error}",
|
|
226
|
+
"ui.bench": "Bench",
|
|
227
|
+
"ui.layers": "Layers",
|
|
228
|
+
"ui.metrics": "Counters",
|
|
229
|
+
"metric.fps": "FPS",
|
|
230
|
+
"metric.frame_ms": "ms/frame",
|
|
231
|
+
"metric.draw_calls": "Draw calls",
|
|
232
|
+
"metric.triangles": "Triangles",
|
|
233
|
+
"metric.objects": "Objects",
|
|
234
|
+
"metric.fragments": "Fragments",
|
|
235
|
+
"metric.particles": "Particles",
|
|
236
|
+
"metric.decals": "Decals",
|
|
222
237
|
"act.spawn": "Spawned {name} at x {x} \xB7 z {z}",
|
|
223
238
|
"act.clear": "Cleared the room",
|
|
224
239
|
"act.reset": "Repaired every object",
|
|
@@ -291,6 +306,9 @@ var en = {
|
|
|
291
306
|
"ui.quality.medium": "medium",
|
|
292
307
|
"ui.quality.high": "high",
|
|
293
308
|
"ui.equip": "Equip as a tool",
|
|
309
|
+
"ui.search_items": "Search items (all groups)",
|
|
310
|
+
"ui.search_tests": "Search tests",
|
|
311
|
+
"ui.no_results": "Nothing found",
|
|
294
312
|
"ui.objects": "{n} objects",
|
|
295
313
|
"ui.tests": "Tests",
|
|
296
314
|
"ui.run_all": "Run all",
|
|
@@ -508,6 +526,21 @@ var ptBR = {
|
|
|
508
526
|
"ui.animations": "Anima\xE7\xF5es",
|
|
509
527
|
"ui.animations_aim": "Mire num item ou segure-o para ver as anima\xE7\xF5es.",
|
|
510
528
|
"ui.animations_none": "Este item n\xE3o traz anima\xE7\xF5es.",
|
|
529
|
+
"mod.bench_ok": "{name}: bancada ok ({controls} controles, {layers} camadas, {metrics} contadores)",
|
|
530
|
+
"mod.bench_fail": "{name}: bancada quebrada: {problems}",
|
|
531
|
+
"mod.metric_failed": '{name}: o contador "{metric}" lan\xE7ou erro e mostra n/a: {error}',
|
|
532
|
+
"mod.listener_failed": "{name}: um ouvinte da bancada lan\xE7ou erro: {error}",
|
|
533
|
+
"ui.bench": "Bancada",
|
|
534
|
+
"ui.layers": "Camadas",
|
|
535
|
+
"ui.metrics": "Contadores",
|
|
536
|
+
"metric.fps": "FPS",
|
|
537
|
+
"metric.frame_ms": "ms/quadro",
|
|
538
|
+
"metric.draw_calls": "Chamadas de desenho",
|
|
539
|
+
"metric.triangles": "Tri\xE2ngulos",
|
|
540
|
+
"metric.objects": "Objetos",
|
|
541
|
+
"metric.fragments": "Fragmentos",
|
|
542
|
+
"metric.particles": "Part\xEDculas",
|
|
543
|
+
"metric.decals": "Decalques",
|
|
511
544
|
"act.spawn": "Criou {name} em x {x} \xB7 z {z}",
|
|
512
545
|
"act.clear": "Limpou a sala",
|
|
513
546
|
"act.reset": "Consertou todos os objetos",
|
|
@@ -580,6 +613,9 @@ var ptBR = {
|
|
|
580
613
|
"ui.quality.medium": "m\xE9dia",
|
|
581
614
|
"ui.quality.high": "alta",
|
|
582
615
|
"ui.equip": "Equipar como ferramenta",
|
|
616
|
+
"ui.search_items": "Buscar item (todas as abas)",
|
|
617
|
+
"ui.search_tests": "Buscar teste",
|
|
618
|
+
"ui.no_results": "Nada encontrado",
|
|
583
619
|
"ui.objects": "{n} objetos",
|
|
584
620
|
"ui.tests": "Testes",
|
|
585
621
|
"ui.run_all": "Rodar todos",
|
|
@@ -1116,6 +1152,154 @@ function createArena(THREE, size, scene) {
|
|
|
1116
1152
|
};
|
|
1117
1153
|
}
|
|
1118
1154
|
|
|
1155
|
+
// src/bench.ts
|
|
1156
|
+
var snap = (control, value) => {
|
|
1157
|
+
const clamped = Math.min(control.max, Math.max(control.min, value));
|
|
1158
|
+
if (!control.step) return clamped;
|
|
1159
|
+
const stepped = control.min + Math.round((clamped - control.min) / control.step) * control.step;
|
|
1160
|
+
return Number(Math.min(control.max, stepped).toFixed(10));
|
|
1161
|
+
};
|
|
1162
|
+
function fitValue(control, value) {
|
|
1163
|
+
if (control.kind === "number") return typeof value === "number" && Number.isFinite(value) ? snap(control, value) : void 0;
|
|
1164
|
+
if (control.kind === "boolean") return typeof value === "boolean" ? value : void 0;
|
|
1165
|
+
if (control.kind === "choice") return typeof value === "string" && control.options.some((option) => option.id === value) ? value : void 0;
|
|
1166
|
+
return void 0;
|
|
1167
|
+
}
|
|
1168
|
+
var fromText = (control, text) => {
|
|
1169
|
+
if (control.kind === "number") return text.trim() === "" ? Number.NaN : Number(text);
|
|
1170
|
+
if (control.kind === "boolean") return text === "1" ? true : text === "0" ? false : void 0;
|
|
1171
|
+
return text;
|
|
1172
|
+
};
|
|
1173
|
+
var toText = (value) => typeof value === "boolean" ? value ? "1" : "0" : String(value);
|
|
1174
|
+
function encodeBenchState(controls, layers) {
|
|
1175
|
+
const pairs = [];
|
|
1176
|
+
for (const control of controls) if (control.value !== void 0) pairs.push(`${control.id}=${encodeURIComponent(toText(control.value))}`);
|
|
1177
|
+
for (const layer of layers) pairs.push(`${layer.id}=${layer.on ? "1" : "0"}`);
|
|
1178
|
+
return pairs.join(";");
|
|
1179
|
+
}
|
|
1180
|
+
function decodeBenchState(encoded, controls, layers) {
|
|
1181
|
+
const out = /* @__PURE__ */ new Map();
|
|
1182
|
+
for (const pair of encoded.split(";")) {
|
|
1183
|
+
const at = pair.indexOf("=");
|
|
1184
|
+
if (at <= 0) continue;
|
|
1185
|
+
const id = pair.slice(0, at);
|
|
1186
|
+
let text;
|
|
1187
|
+
try {
|
|
1188
|
+
text = decodeURIComponent(pair.slice(at + 1));
|
|
1189
|
+
} catch {
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
const control = controls.find((c) => c.id === id);
|
|
1193
|
+
if (control) {
|
|
1194
|
+
const value = fitValue(control, fromText(control, text));
|
|
1195
|
+
if (value !== void 0) out.set(id, value);
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
if (layers.some((layer) => layer.id === id) && (text === "1" || text === "0")) out.set(id, text === "1");
|
|
1199
|
+
}
|
|
1200
|
+
return out;
|
|
1201
|
+
}
|
|
1202
|
+
function createBenchStore(module, locale, onListenerError) {
|
|
1203
|
+
const declared = { controls: module.controls?.(locale) ?? [], layers: module.layers?.(locale) ?? [], metrics: module.metrics?.(locale) ?? [] };
|
|
1204
|
+
const controls = declared.controls.map((control) => ({ ...control, disabled: false }));
|
|
1205
|
+
const values = /* @__PURE__ */ new Map();
|
|
1206
|
+
for (const control of controls) if (control.kind !== "action") values.set(control.id, control.default);
|
|
1207
|
+
const layers = new Map(declared.layers.map((layer) => [layer.id, layer.default !== false]));
|
|
1208
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1209
|
+
const watchers = /* @__PURE__ */ new Set();
|
|
1210
|
+
const emit = (change) => {
|
|
1211
|
+
for (const listener of listeners) {
|
|
1212
|
+
try {
|
|
1213
|
+
listener(change);
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
onListenerError?.(error);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
const changed = () => {
|
|
1220
|
+
for (const watcher of watchers) watcher();
|
|
1221
|
+
};
|
|
1222
|
+
const controlOf = (id) => controls.find((control) => control.id === id);
|
|
1223
|
+
const store = {
|
|
1224
|
+
moduleId: module.id,
|
|
1225
|
+
metrics: declared.metrics,
|
|
1226
|
+
declared,
|
|
1227
|
+
controls: () => controls.map((control) => ({ ...control, value: values.get(control.id) })),
|
|
1228
|
+
layers: () => declared.layers.map((layer) => ({ ...layer, on: layers.get(layer.id) === true })),
|
|
1229
|
+
value: (id) => values.get(id),
|
|
1230
|
+
layer: (id) => layers.get(id) === true,
|
|
1231
|
+
set(id, value) {
|
|
1232
|
+
const control = controlOf(id);
|
|
1233
|
+
if (!control || control.disabled) return false;
|
|
1234
|
+
const fitted = fitValue(control, value);
|
|
1235
|
+
if (fitted === void 0) return false;
|
|
1236
|
+
values.set(id, fitted);
|
|
1237
|
+
emit({ kind: "control", id, value: fitted });
|
|
1238
|
+
changed();
|
|
1239
|
+
return true;
|
|
1240
|
+
},
|
|
1241
|
+
setLayer(id, on) {
|
|
1242
|
+
if (!layers.has(id)) return false;
|
|
1243
|
+
layers.set(id, on);
|
|
1244
|
+
emit({ kind: "layer", id, on });
|
|
1245
|
+
changed();
|
|
1246
|
+
return true;
|
|
1247
|
+
},
|
|
1248
|
+
act(id) {
|
|
1249
|
+
const control = controlOf(id);
|
|
1250
|
+
if (!control || control.kind !== "action" || control.disabled) return false;
|
|
1251
|
+
emit({ kind: "action", id });
|
|
1252
|
+
return true;
|
|
1253
|
+
},
|
|
1254
|
+
encode: () => encodeBenchState(store.controls(), store.layers()),
|
|
1255
|
+
onChange(listener) {
|
|
1256
|
+
listeners.add(listener);
|
|
1257
|
+
return () => listeners.delete(listener);
|
|
1258
|
+
},
|
|
1259
|
+
watch(listener) {
|
|
1260
|
+
watchers.add(listener);
|
|
1261
|
+
return () => watchers.delete(listener);
|
|
1262
|
+
},
|
|
1263
|
+
patch(id, patch) {
|
|
1264
|
+
const index = controls.findIndex((control2) => control2.id === id);
|
|
1265
|
+
if (index < 0) return;
|
|
1266
|
+
let control = controls[index];
|
|
1267
|
+
if (patch.hidden !== void 0) control = { ...control, hidden: patch.hidden };
|
|
1268
|
+
if (patch.disabled !== void 0) control = { ...control, disabled: patch.disabled };
|
|
1269
|
+
if (patch.options && control.kind === "choice" && patch.options.length > 0) {
|
|
1270
|
+
control = { ...control, options: [...patch.options] };
|
|
1271
|
+
const offered = control.options;
|
|
1272
|
+
if (!offered.some((option) => option.id === values.get(id))) values.set(id, offered[0].id);
|
|
1273
|
+
}
|
|
1274
|
+
controls[index] = control;
|
|
1275
|
+
if (patch.value !== void 0) {
|
|
1276
|
+
const fitted = fitValue(control, patch.value);
|
|
1277
|
+
if (fitted !== void 0) values.set(id, fitted);
|
|
1278
|
+
}
|
|
1279
|
+
changed();
|
|
1280
|
+
},
|
|
1281
|
+
sendInitial() {
|
|
1282
|
+
for (const control of controls) {
|
|
1283
|
+
const value = values.get(control.id);
|
|
1284
|
+
if (value !== void 0) emit({ kind: "control", id: control.id, value });
|
|
1285
|
+
}
|
|
1286
|
+
for (const layer of declared.layers) emit({ kind: "layer", id: layer.id, on: layers.get(layer.id) === true });
|
|
1287
|
+
changed();
|
|
1288
|
+
},
|
|
1289
|
+
restore(encoded) {
|
|
1290
|
+
for (const [id, value] of decodeBenchState(encoded, controls, declared.layers)) {
|
|
1291
|
+
if (typeof value === "boolean" && layers.has(id)) layers.set(id, value);
|
|
1292
|
+
else values.set(id, value);
|
|
1293
|
+
}
|
|
1294
|
+
},
|
|
1295
|
+
dispose() {
|
|
1296
|
+
listeners.clear();
|
|
1297
|
+
watchers.clear();
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
return store;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1119
1303
|
// src/log.ts
|
|
1120
1304
|
var MAX_ENTRIES = 5e3;
|
|
1121
1305
|
var pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
@@ -1244,6 +1428,56 @@ function createLog(options = {}) {
|
|
|
1244
1428
|
return log;
|
|
1245
1429
|
}
|
|
1246
1430
|
|
|
1431
|
+
// src/metrics.ts
|
|
1432
|
+
var ROOM_METRICS = ["fps", "frame_ms", "draw_calls", "triangles", "objects", "fragments", "particles", "decals"];
|
|
1433
|
+
var WINDOW_S = 0.5;
|
|
1434
|
+
function createRoomMetrics(sources) {
|
|
1435
|
+
const wanted = new Set(sources.ids);
|
|
1436
|
+
const gl = sources.gl;
|
|
1437
|
+
let windowS = 0;
|
|
1438
|
+
let frames = 0;
|
|
1439
|
+
let fps;
|
|
1440
|
+
let frameMs;
|
|
1441
|
+
let calls;
|
|
1442
|
+
let triangles;
|
|
1443
|
+
let ownsRender = false;
|
|
1444
|
+
return {
|
|
1445
|
+
frame(dt) {
|
|
1446
|
+
frames++;
|
|
1447
|
+
windowS += dt;
|
|
1448
|
+
if (windowS < WINDOW_S) return;
|
|
1449
|
+
fps = frames / windowS;
|
|
1450
|
+
frameMs = windowS / frames * 1e3;
|
|
1451
|
+
frames = 0;
|
|
1452
|
+
windowS = 0;
|
|
1453
|
+
},
|
|
1454
|
+
rendered() {
|
|
1455
|
+
ownsRender = true;
|
|
1456
|
+
calls = gl.info?.render?.calls;
|
|
1457
|
+
triangles = gl.info?.render?.triangles;
|
|
1458
|
+
},
|
|
1459
|
+
sample() {
|
|
1460
|
+
const out = {};
|
|
1461
|
+
const put = (id, value) => {
|
|
1462
|
+
if (wanted.has(id) && value !== void 0) out[id] = value;
|
|
1463
|
+
};
|
|
1464
|
+
put("fps", fps);
|
|
1465
|
+
put("frame_ms", frameMs);
|
|
1466
|
+
const render = ownsRender ? { calls, triangles } : gl.info?.render;
|
|
1467
|
+
put("draw_calls", render?.calls);
|
|
1468
|
+
put("triangles", render?.triangles);
|
|
1469
|
+
if (!["objects", "fragments", "particles", "decals"].some((id) => wanted.has(id))) return out;
|
|
1470
|
+
const destruction = sources.destruction.stats();
|
|
1471
|
+
const fluids = sources.fluids.stats();
|
|
1472
|
+
put("objects", destruction.pieces);
|
|
1473
|
+
put("fragments", destruction.fragments);
|
|
1474
|
+
put("particles", destruction.particles + fluids.particles);
|
|
1475
|
+
put("decals", destruction.decals + fluids.stains);
|
|
1476
|
+
return out;
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1247
1481
|
// src/narrate.ts
|
|
1248
1482
|
var SKIP_DAMAGE = /* @__PURE__ */ new Set(["fire", "heat"]);
|
|
1249
1483
|
function createNarrator(c) {
|
|
@@ -2314,11 +2548,218 @@ function scenariosFor(locale = "en") {
|
|
|
2314
2548
|
}
|
|
2315
2549
|
var SCENARIOS = scenariosFor("en");
|
|
2316
2550
|
|
|
2551
|
+
// src/bench-panel.ts
|
|
2552
|
+
var SEGMENTED_MAX = 4;
|
|
2553
|
+
var mk = (tag, cls, text) => {
|
|
2554
|
+
const e = document.createElement(tag);
|
|
2555
|
+
if (cls) e.className = cls;
|
|
2556
|
+
if (text !== void 0) e.textContent = text;
|
|
2557
|
+
return e;
|
|
2558
|
+
};
|
|
2559
|
+
var shapeOf = (view) => JSON.stringify([view.title, view.controls.map((c) => ({ ...c, value: void 0 })), view.layers.map((l) => [l.id, l.name])]);
|
|
2560
|
+
function createBenchPanel(o) {
|
|
2561
|
+
const { t } = o;
|
|
2562
|
+
const element = mk("div", "tr-panel tr-bench");
|
|
2563
|
+
element.style.display = "none";
|
|
2564
|
+
element.addEventListener("keydown", (e) => e.stopPropagation());
|
|
2565
|
+
const roomLine = mk("div", "sub tr-bench-room");
|
|
2566
|
+
const body = mk("div");
|
|
2567
|
+
const metricTitle = mk("div", "sub", t("ui.metrics"));
|
|
2568
|
+
metricTitle.style.display = "none";
|
|
2569
|
+
const metricList = mk("dl", "tr-metrics");
|
|
2570
|
+
element.append(mk("h3", void 0, t("ui.bench")), roomLine, body, metricTitle, metricList);
|
|
2571
|
+
let shape = "";
|
|
2572
|
+
let hasRoom = false;
|
|
2573
|
+
let hasBench = false;
|
|
2574
|
+
const refreshers = [];
|
|
2575
|
+
const metricNodes = /* @__PURE__ */ new Map();
|
|
2576
|
+
let metricKey = "";
|
|
2577
|
+
const syncDisplay = () => {
|
|
2578
|
+
element.style.display = hasRoom || hasBench ? "" : "none";
|
|
2579
|
+
};
|
|
2580
|
+
const numberControl = (control) => {
|
|
2581
|
+
const wrap = mk("label", "tr-ctl");
|
|
2582
|
+
const input = mk("input");
|
|
2583
|
+
input.type = "range";
|
|
2584
|
+
input.min = String(control.min);
|
|
2585
|
+
input.max = String(control.max);
|
|
2586
|
+
input.step = control.step ? String(control.step) : "any";
|
|
2587
|
+
input.disabled = control.disabled;
|
|
2588
|
+
const reading = mk("output");
|
|
2589
|
+
input.oninput = () => o.onControl(control.id, Number(input.value));
|
|
2590
|
+
wrap.append(mk("span", void 0, control.name), input, reading);
|
|
2591
|
+
refreshers.push((view) => {
|
|
2592
|
+
const value = view.controls.find((c) => c.id === control.id)?.value;
|
|
2593
|
+
if (typeof value !== "number") return;
|
|
2594
|
+
if (Number(input.value) !== value) input.value = String(value);
|
|
2595
|
+
reading.textContent = `${o.num(value, control.step && control.step >= 1 ? 0 : 2)}${control.unit ? ` ${control.unit}` : ""}`;
|
|
2596
|
+
});
|
|
2597
|
+
return wrap;
|
|
2598
|
+
};
|
|
2599
|
+
const booleanControl = (control) => {
|
|
2600
|
+
const wrap = mk("label", "tr-ctl");
|
|
2601
|
+
const box2 = mk("input");
|
|
2602
|
+
box2.type = "checkbox";
|
|
2603
|
+
box2.disabled = control.disabled;
|
|
2604
|
+
box2.onchange = () => o.onControl(control.id, box2.checked);
|
|
2605
|
+
wrap.append(box2, document.createTextNode(` ${control.name}`));
|
|
2606
|
+
refreshers.push((view) => {
|
|
2607
|
+
box2.checked = view.controls.find((c) => c.id === control.id)?.value === true;
|
|
2608
|
+
});
|
|
2609
|
+
return wrap;
|
|
2610
|
+
};
|
|
2611
|
+
const choiceControl = (control) => {
|
|
2612
|
+
const wrap = mk("div", "tr-ctl");
|
|
2613
|
+
wrap.appendChild(mk("span", void 0, control.name));
|
|
2614
|
+
if (control.options.length <= SEGMENTED_MAX) {
|
|
2615
|
+
const group = mk("div", "tr-seg");
|
|
2616
|
+
group.setAttribute("role", "group");
|
|
2617
|
+
const buttons = control.options.map((option) => {
|
|
2618
|
+
const b = mk("button", "tr-btn", option.name);
|
|
2619
|
+
b.type = "button";
|
|
2620
|
+
b.disabled = control.disabled;
|
|
2621
|
+
b.onclick = () => o.onControl(control.id, option.id);
|
|
2622
|
+
group.appendChild(b);
|
|
2623
|
+
return { b, id: option.id };
|
|
2624
|
+
});
|
|
2625
|
+
wrap.appendChild(group);
|
|
2626
|
+
refreshers.push((view) => {
|
|
2627
|
+
const value = view.controls.find((c) => c.id === control.id)?.value;
|
|
2628
|
+
for (const { b, id } of buttons) {
|
|
2629
|
+
b.setAttribute("aria-pressed", String(id === value));
|
|
2630
|
+
b.classList.toggle("on", id === value);
|
|
2631
|
+
}
|
|
2632
|
+
});
|
|
2633
|
+
return wrap;
|
|
2634
|
+
}
|
|
2635
|
+
const select = mk("select");
|
|
2636
|
+
select.disabled = control.disabled;
|
|
2637
|
+
for (const option of control.options) {
|
|
2638
|
+
const op = mk("option", void 0, option.name);
|
|
2639
|
+
op.value = option.id;
|
|
2640
|
+
select.appendChild(op);
|
|
2641
|
+
}
|
|
2642
|
+
select.onchange = () => o.onControl(control.id, select.value);
|
|
2643
|
+
wrap.appendChild(select);
|
|
2644
|
+
refreshers.push((view) => {
|
|
2645
|
+
const value = view.controls.find((c) => c.id === control.id)?.value;
|
|
2646
|
+
if (typeof value === "string") select.value = value;
|
|
2647
|
+
});
|
|
2648
|
+
return wrap;
|
|
2649
|
+
};
|
|
2650
|
+
const actionControl = (control) => {
|
|
2651
|
+
const b = mk("button", "tr-btn", control.name);
|
|
2652
|
+
b.type = "button";
|
|
2653
|
+
b.disabled = control.disabled;
|
|
2654
|
+
b.onclick = () => o.onAction(control.id);
|
|
2655
|
+
return b;
|
|
2656
|
+
};
|
|
2657
|
+
const controlElement = (control) => {
|
|
2658
|
+
const el = control.kind === "number" ? numberControl(control) : control.kind === "boolean" ? booleanControl(control) : control.kind === "choice" ? choiceControl(control) : actionControl(control);
|
|
2659
|
+
if (control.hint) el.title = control.hint;
|
|
2660
|
+
return el;
|
|
2661
|
+
};
|
|
2662
|
+
const layerList = (layers) => {
|
|
2663
|
+
const wrap = mk("div", "tr-layers");
|
|
2664
|
+
wrap.appendChild(mk("div", "sub", t("ui.layers")));
|
|
2665
|
+
for (const layer of layers) {
|
|
2666
|
+
const label = mk("label", "tr-ctl");
|
|
2667
|
+
const box2 = mk("input");
|
|
2668
|
+
box2.type = "checkbox";
|
|
2669
|
+
box2.onchange = () => o.onLayer(layer.id, box2.checked);
|
|
2670
|
+
label.append(box2, document.createTextNode(` ${layer.name}`));
|
|
2671
|
+
wrap.appendChild(label);
|
|
2672
|
+
refreshers.push((view) => {
|
|
2673
|
+
box2.checked = view.layers.find((l) => l.id === layer.id)?.on === true;
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
return wrap;
|
|
2677
|
+
};
|
|
2678
|
+
const rebuild = (view) => {
|
|
2679
|
+
body.textContent = "";
|
|
2680
|
+
refreshers.length = 0;
|
|
2681
|
+
body.appendChild(mk("div", "sub", view.title));
|
|
2682
|
+
const list = mk("div", "tr-ctls");
|
|
2683
|
+
for (const control of view.controls) if (!control.hidden) list.appendChild(controlElement(control));
|
|
2684
|
+
body.appendChild(list);
|
|
2685
|
+
if (view.layers.length) body.appendChild(layerList(view.layers));
|
|
2686
|
+
};
|
|
2687
|
+
const renderMetrics = (rows) => {
|
|
2688
|
+
metricList.textContent = "";
|
|
2689
|
+
metricNodes.clear();
|
|
2690
|
+
metricTitle.style.display = rows.length ? "" : "none";
|
|
2691
|
+
for (const row of rows) {
|
|
2692
|
+
const dd = mk("dd");
|
|
2693
|
+
const text = mk("span", void 0, row.text);
|
|
2694
|
+
dd.appendChild(text);
|
|
2695
|
+
let bar = null;
|
|
2696
|
+
if (row.ratio !== void 0) {
|
|
2697
|
+
bar = mk("div", "tr-bar");
|
|
2698
|
+
bar.appendChild(mk("i"));
|
|
2699
|
+
dd.appendChild(bar);
|
|
2700
|
+
}
|
|
2701
|
+
metricList.append(mk("dt", void 0, row.name), dd);
|
|
2702
|
+
metricNodes.set(row.id, { text, bar });
|
|
2703
|
+
}
|
|
2704
|
+
};
|
|
2705
|
+
const fillBar = (bar, ratio) => {
|
|
2706
|
+
bar.className = `tr-bar${ratio > 1 ? " bad" : ratio >= 0.8 ? " warn" : ""}`;
|
|
2707
|
+
const fill = bar.firstElementChild;
|
|
2708
|
+
fill.style.width = `${Math.round(Math.max(0, Math.min(1, ratio)) * 100)}%`;
|
|
2709
|
+
};
|
|
2710
|
+
return {
|
|
2711
|
+
element,
|
|
2712
|
+
show(view) {
|
|
2713
|
+
hasBench = view !== null;
|
|
2714
|
+
syncDisplay();
|
|
2715
|
+
if (!view) {
|
|
2716
|
+
shape = "";
|
|
2717
|
+
body.textContent = "";
|
|
2718
|
+
refreshers.length = 0;
|
|
2719
|
+
renderMetrics([]);
|
|
2720
|
+
metricKey = "";
|
|
2721
|
+
return;
|
|
2722
|
+
}
|
|
2723
|
+
const next = shapeOf(view);
|
|
2724
|
+
if (next !== shape) {
|
|
2725
|
+
shape = next;
|
|
2726
|
+
rebuild(view);
|
|
2727
|
+
}
|
|
2728
|
+
for (const refresh of refreshers) refresh(view);
|
|
2729
|
+
},
|
|
2730
|
+
metrics(rows) {
|
|
2731
|
+
hasRoom = rows.room.length > 0;
|
|
2732
|
+
syncDisplay();
|
|
2733
|
+
roomLine.textContent = rows.room.map((row) => `${row.name} ${row.text}`).join(" \xB7 ");
|
|
2734
|
+
const key = rows.module.map((row) => `${row.id}:${row.ratio !== void 0}`).join("|");
|
|
2735
|
+
if (key !== metricKey) {
|
|
2736
|
+
metricKey = key;
|
|
2737
|
+
renderMetrics(rows.module);
|
|
2738
|
+
}
|
|
2739
|
+
for (const row of rows.module) {
|
|
2740
|
+
const node = metricNodes.get(row.id);
|
|
2741
|
+
if (!node) continue;
|
|
2742
|
+
node.text.textContent = row.text;
|
|
2743
|
+
if (node.bar && row.ratio !== void 0) fillBar(node.bar, row.ratio);
|
|
2744
|
+
}
|
|
2745
|
+
},
|
|
2746
|
+
get visible() {
|
|
2747
|
+
if (element.style.display === "none") return false;
|
|
2748
|
+
const parent = element.parentElement;
|
|
2749
|
+
return !parent || getComputedStyle(parent).display !== "none";
|
|
2750
|
+
}
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2317
2754
|
// src/ui.ts
|
|
2318
2755
|
var CSS = `
|
|
2319
2756
|
.tr-root{position:absolute;inset:0;pointer-events:none;font:12px/1.35 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;color:#e8eaee;overflow:hidden}
|
|
2320
2757
|
.tr-root *{box-sizing:border-box}
|
|
2321
|
-
.tr-root button,.tr-root select,.tr-root label{pointer-events:auto}
|
|
2758
|
+
.tr-root button,.tr-root select,.tr-root label,.tr-root input{pointer-events:auto}
|
|
2759
|
+
.tr-search{width:100%;margin:0 0 6px;padding:5px 8px;border-radius:5px;border:1px solid #3a404c;background:#0f1116;color:#e8eaee;font:inherit}
|
|
2760
|
+
.tr-search::placeholder{color:#7d8594}
|
|
2761
|
+
.tr-item small{color:#7d8594;margin-left:4px}
|
|
2762
|
+
.tr-empty{color:#9aa3b2;padding:4px 2px;grid-column:1/-1}
|
|
2322
2763
|
.tr-cross{position:absolute;left:50%;top:50%;width:18px;height:18px;margin:-9px 0 0 -9px}
|
|
2323
2764
|
.tr-cross:before,.tr-cross:after{content:"";position:absolute;background:#fff;opacity:.85;box-shadow:0 0 2px #000}
|
|
2324
2765
|
.tr-cross:before{left:8px;top:0;width:2px;height:18px}.tr-cross:after{top:8px;left:0;height:2px;width:18px}
|
|
@@ -2362,6 +2803,13 @@ var CSS = `
|
|
|
2362
2803
|
.tr-cat{flex:0 1 auto;max-height:40%}
|
|
2363
2804
|
.tr-anim{flex:0 0 auto;max-height:18%}
|
|
2364
2805
|
.tr-anim .list{display:flex;flex-wrap:wrap;gap:4px}.tr-anim .sub{color:#9aa3b2;margin-bottom:4px}
|
|
2806
|
+
.tr-bench{flex:0 1 auto;max-height:34%}
|
|
2807
|
+
.tr-bench .sub{color:#9aa3b2;margin-bottom:4px}
|
|
2808
|
+
.tr-ctls{display:flex;flex-direction:column;gap:5px}
|
|
2809
|
+
.tr-ctl{display:flex;flex-wrap:wrap;align-items:center;gap:6px}.tr-ctl input[type=range]{flex:1;min-width:90px}.tr-ctl output{min-width:48px;text-align:right;color:#cfd3da}
|
|
2810
|
+
.tr-seg{display:inline-flex;flex-wrap:wrap;gap:2px}
|
|
2811
|
+
.tr-layers{margin-top:6px;display:flex;flex-direction:column;gap:2px}
|
|
2812
|
+
.tr-metrics{display:grid;grid-template-columns:auto 1fr;gap:2px 8px;margin:6px 0 0}.tr-metrics dt{color:#9aa3b2}.tr-metrics dd{margin:0}
|
|
2365
2813
|
.tr-tests{flex:1 1 auto;min-height:100px}
|
|
2366
2814
|
.tr-tests .list{display:flex;flex-direction:column;gap:3px;margin-top:6px}.tr-tests .list .tr-btn{text-align:left}
|
|
2367
2815
|
.tr-row{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;align-items:center}
|
|
@@ -2369,7 +2817,8 @@ var CSS = `
|
|
|
2369
2817
|
.tr-summary{margin-top:6px;color:#9aa3b2}
|
|
2370
2818
|
@media (max-width:980px){.tr-left,.tr-right{display:none}.tr-root.open .tr-left,.tr-root.open .tr-right{display:flex}.tr-toggle{display:block;pointer-events:auto}.tr-root.open .tr-toggle{right:min(310px,34vw)}.tr-left{width:min(400px,46vw)}.tr-help{display:none}}
|
|
2371
2819
|
`;
|
|
2372
|
-
var
|
|
2820
|
+
var fold = (text) => text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
|
2821
|
+
var mk2 = (tag, cls, text) => {
|
|
2373
2822
|
const e = document.createElement(tag);
|
|
2374
2823
|
if (cls) e.className = cls;
|
|
2375
2824
|
if (text !== void 0) e.textContent = text;
|
|
@@ -2377,42 +2826,42 @@ var mk = (tag, cls, text) => {
|
|
|
2377
2826
|
};
|
|
2378
2827
|
function createUi(o) {
|
|
2379
2828
|
const { t } = o;
|
|
2380
|
-
const style =
|
|
2829
|
+
const style = mk2("style");
|
|
2381
2830
|
style.textContent = CSS;
|
|
2382
|
-
const root =
|
|
2831
|
+
const root = mk2("div", "tr-root");
|
|
2383
2832
|
const pos = getComputedStyle?.(o.container)?.position;
|
|
2384
2833
|
if (!pos || pos === "static") o.container.style.position = "relative";
|
|
2385
2834
|
root.appendChild(style);
|
|
2386
2835
|
o.container.appendChild(root);
|
|
2387
|
-
const cross =
|
|
2388
|
-
cross.appendChild(
|
|
2389
|
-
const top =
|
|
2390
|
-
const toolBox =
|
|
2391
|
-
const status =
|
|
2392
|
-
const prog =
|
|
2393
|
-
const progLabel =
|
|
2394
|
-
const progBar =
|
|
2395
|
-
const progFill =
|
|
2836
|
+
const cross = mk2("div", "tr-cross");
|
|
2837
|
+
cross.appendChild(mk2("i"));
|
|
2838
|
+
const top = mk2("div", "tr-top");
|
|
2839
|
+
const toolBox = mk2("div", "tr-tool");
|
|
2840
|
+
const status = mk2("div", "tr-status");
|
|
2841
|
+
const prog = mk2("div", "tr-prog");
|
|
2842
|
+
const progLabel = mk2("b");
|
|
2843
|
+
const progBar = mk2("div", "tr-bar");
|
|
2844
|
+
const progFill = mk2("i");
|
|
2396
2845
|
progBar.appendChild(progFill);
|
|
2397
2846
|
prog.append(progLabel, progBar);
|
|
2398
2847
|
top.append(toolBox, status, prog);
|
|
2399
|
-
const bottom =
|
|
2400
|
-
const held =
|
|
2401
|
-
const hotbar =
|
|
2848
|
+
const bottom = mk2("div", "tr-bottom");
|
|
2849
|
+
const held = mk2("div", "tr-held");
|
|
2850
|
+
const hotbar = mk2("div", "tr-hotbar");
|
|
2402
2851
|
bottom.append(held, hotbar);
|
|
2403
|
-
const help =
|
|
2852
|
+
const help = mk2("div", "tr-help");
|
|
2404
2853
|
help.textContent = t("ui.help");
|
|
2405
|
-
const left =
|
|
2406
|
-
const insp =
|
|
2407
|
-
const logPanel =
|
|
2854
|
+
const left = mk2("div", "tr-left");
|
|
2855
|
+
const insp = mk2("div", "tr-panel tr-insp");
|
|
2856
|
+
const logPanel = mk2("div", "tr-panel tr-log");
|
|
2408
2857
|
left.append(insp, logPanel);
|
|
2409
|
-
const logTitle =
|
|
2410
|
-
const logBar =
|
|
2411
|
-
const logBody =
|
|
2858
|
+
const logTitle = mk2("h3", void 0, t("ui.log"));
|
|
2859
|
+
const logBar = mk2("div", "tr-logbar");
|
|
2860
|
+
const logBody = mk2("div", "tr-logbody");
|
|
2412
2861
|
const filters = { action: true, event: true, check: true };
|
|
2413
2862
|
const filterBtns = [];
|
|
2414
2863
|
for (const ch of ["action", "event", "check"]) {
|
|
2415
|
-
const b =
|
|
2864
|
+
const b = mk2("button", "tr-btn on", t(`ui.filter.${ch}`));
|
|
2416
2865
|
b.type = "button";
|
|
2417
2866
|
b.onclick = () => {
|
|
2418
2867
|
filters[ch] = !filters[ch];
|
|
@@ -2423,8 +2872,8 @@ function createUi(o) {
|
|
|
2423
2872
|
logBar.appendChild(b);
|
|
2424
2873
|
}
|
|
2425
2874
|
let autoscroll = true;
|
|
2426
|
-
const asLabel =
|
|
2427
|
-
const asBox =
|
|
2875
|
+
const asLabel = mk2("label");
|
|
2876
|
+
const asBox = mk2("input");
|
|
2428
2877
|
asBox.type = "checkbox";
|
|
2429
2878
|
asBox.checked = true;
|
|
2430
2879
|
asBox.onchange = () => {
|
|
@@ -2433,7 +2882,7 @@ function createUi(o) {
|
|
|
2433
2882
|
asLabel.append(asBox, document.createTextNode(" " + t("ui.autoscroll")));
|
|
2434
2883
|
logBar.appendChild(asLabel);
|
|
2435
2884
|
const logBtn = (label, fn) => {
|
|
2436
|
-
const b =
|
|
2885
|
+
const b = mk2("button", "tr-btn", label);
|
|
2437
2886
|
b.type = "button";
|
|
2438
2887
|
b.onclick = fn;
|
|
2439
2888
|
logBar.appendChild(b);
|
|
@@ -2441,7 +2890,7 @@ function createUi(o) {
|
|
|
2441
2890
|
logBtn(t("ui.copy_json"), () => {
|
|
2442
2891
|
const text = o.log.toJSON();
|
|
2443
2892
|
const select = () => {
|
|
2444
|
-
const ta =
|
|
2893
|
+
const ta = mk2("textarea");
|
|
2445
2894
|
ta.value = text;
|
|
2446
2895
|
ta.setAttribute("readonly", "");
|
|
2447
2896
|
ta.style.cssText = "position:fixed;left:8px;bottom:8px;width:min(480px,80vw);height:120px;z-index:99999";
|
|
@@ -2475,12 +2924,12 @@ function createUi(o) {
|
|
|
2475
2924
|
const fill = (n, e) => {
|
|
2476
2925
|
n.className = lineClass(e);
|
|
2477
2926
|
n.textContent = "";
|
|
2478
|
-
const tm =
|
|
2927
|
+
const tm = mk2("time", void 0, fmtTime(e.t));
|
|
2479
2928
|
n.append(tm, document.createTextNode(e.text));
|
|
2480
2929
|
};
|
|
2481
2930
|
const addNode = (e) => {
|
|
2482
2931
|
if (!filters[e.channel]) return;
|
|
2483
|
-
const n =
|
|
2932
|
+
const n = mk2("div");
|
|
2484
2933
|
fill(n, e);
|
|
2485
2934
|
nodes.set(e.seq, n);
|
|
2486
2935
|
logBody.appendChild(n);
|
|
@@ -2502,27 +2951,40 @@ function createUi(o) {
|
|
|
2502
2951
|
if (n) fill(n, e);
|
|
2503
2952
|
} else addNode(e);
|
|
2504
2953
|
});
|
|
2505
|
-
const right =
|
|
2506
|
-
const cat =
|
|
2507
|
-
const anim =
|
|
2508
|
-
const tests =
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
const
|
|
2513
|
-
|
|
2954
|
+
const right = mk2("div", "tr-right");
|
|
2955
|
+
const cat = mk2("div", "tr-panel tr-cat");
|
|
2956
|
+
const anim = mk2("div", "tr-panel tr-anim");
|
|
2957
|
+
const tests = mk2("div", "tr-panel tr-tests");
|
|
2958
|
+
const benchPanel = createBenchPanel({ t, num: (n, d) => num(n, d), onControl: o.onControl, onLayer: o.onLayer, onAction: o.onAction });
|
|
2959
|
+
right.append(cat, anim, benchPanel.element, tests);
|
|
2960
|
+
cat.appendChild(mk2("h3", void 0, t("ui.catalog")));
|
|
2961
|
+
const itemSearch = mk2("input", "tr-search");
|
|
2962
|
+
itemSearch.type = "search";
|
|
2963
|
+
itemSearch.placeholder = t("ui.search_items");
|
|
2964
|
+
const tabs = mk2("div", "tr-tabs");
|
|
2965
|
+
const items = mk2("div", "tr-items");
|
|
2966
|
+
cat.append(itemSearch, tabs, items);
|
|
2514
2967
|
let activeGroup = "";
|
|
2968
|
+
const visibleItems = () => {
|
|
2969
|
+
const term = fold(itemSearch.value.trim());
|
|
2970
|
+
if (!term) return (o.groups().get(activeGroup) ?? []).map((item) => ({ item, group: activeGroup }));
|
|
2971
|
+
return [...o.groups()].flatMap(([group, list2]) => list2.filter((item) => fold(`${item.name} ${item.id} ${group}`).includes(term)).map((item) => ({ item, group })));
|
|
2972
|
+
};
|
|
2515
2973
|
const renderItems = () => {
|
|
2516
2974
|
items.textContent = "";
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2975
|
+
const searching = itemSearch.value.trim() !== "";
|
|
2976
|
+
const found = visibleItems();
|
|
2977
|
+
if (!found.length) items.appendChild(mk2("div", "tr-empty", t("ui.no_results")));
|
|
2978
|
+
for (const { item: it, group } of found) {
|
|
2979
|
+
const wrap = mk2("div", "tr-item");
|
|
2980
|
+
const b = mk2("button", "tr-btn", it.name);
|
|
2981
|
+
if (searching) b.appendChild(mk2("small", void 0, group));
|
|
2520
2982
|
b.type = "button";
|
|
2521
2983
|
b.title = it.note ?? it.name;
|
|
2522
2984
|
b.onclick = () => o.onSpawn(it.id);
|
|
2523
2985
|
wrap.appendChild(b);
|
|
2524
2986
|
if (it.weapon) {
|
|
2525
|
-
const w =
|
|
2987
|
+
const w = mk2("button", "tr-btn", "\u2694");
|
|
2526
2988
|
w.type = "button";
|
|
2527
2989
|
w.title = t("ui.equip");
|
|
2528
2990
|
w.onclick = () => o.onEquip(it.id);
|
|
@@ -2536,7 +2998,7 @@ function createUi(o) {
|
|
|
2536
2998
|
const groupNames = [...o.groups().keys()];
|
|
2537
2999
|
if (!groupNames.includes(activeGroup)) activeGroup = groupNames[0] ?? "";
|
|
2538
3000
|
for (const g of groupNames) {
|
|
2539
|
-
const b =
|
|
3001
|
+
const b = mk2("button", g === activeGroup ? "tr-btn on" : "tr-btn", g);
|
|
2540
3002
|
b.type = "button";
|
|
2541
3003
|
b.onclick = () => {
|
|
2542
3004
|
activeGroup = g;
|
|
@@ -2547,9 +3009,11 @@ function createUi(o) {
|
|
|
2547
3009
|
renderItems();
|
|
2548
3010
|
};
|
|
2549
3011
|
renderTabs();
|
|
2550
|
-
|
|
3012
|
+
itemSearch.oninput = renderItems;
|
|
3013
|
+
for (const box2 of [itemSearch]) box2.addEventListener("keydown", (e) => e.stopPropagation());
|
|
3014
|
+
const controls = mk2("div", "tr-row");
|
|
2551
3015
|
const ctl = (label, fn) => {
|
|
2552
|
-
const b =
|
|
3016
|
+
const b = mk2("button", "tr-btn", label);
|
|
2553
3017
|
b.type = "button";
|
|
2554
3018
|
b.onclick = () => fn(b);
|
|
2555
3019
|
controls.appendChild(b);
|
|
@@ -2570,26 +3034,34 @@ function createUi(o) {
|
|
|
2570
3034
|
o.onFast(fast);
|
|
2571
3035
|
});
|
|
2572
3036
|
const tapBtn = ctl(t("ui.tap"), () => o.onTap());
|
|
2573
|
-
const qSel =
|
|
3037
|
+
const qSel = mk2("select");
|
|
2574
3038
|
for (const q of ["low", "medium", "high"]) {
|
|
2575
|
-
const op =
|
|
3039
|
+
const op = mk2("option", void 0, t(`ui.quality.${q}`));
|
|
2576
3040
|
op.value = q;
|
|
2577
3041
|
qSel.appendChild(op);
|
|
2578
3042
|
}
|
|
2579
3043
|
qSel.value = o.quality;
|
|
2580
3044
|
qSel.onchange = () => o.onQuality(qSel.value);
|
|
2581
|
-
const qLabel =
|
|
3045
|
+
const qLabel = mk2("label", void 0, t("ui.quality") + " ");
|
|
2582
3046
|
qLabel.appendChild(qSel);
|
|
2583
3047
|
controls.appendChild(qLabel);
|
|
2584
|
-
const count =
|
|
3048
|
+
const count = mk2("span");
|
|
2585
3049
|
controls.appendChild(count);
|
|
2586
3050
|
cat.appendChild(controls);
|
|
2587
|
-
tests.appendChild(
|
|
2588
|
-
const runAll =
|
|
3051
|
+
tests.appendChild(mk2("h3", void 0, t("ui.tests")));
|
|
3052
|
+
const runAll = mk2("button", "tr-btn", t("ui.run_all"));
|
|
2589
3053
|
runAll.type = "button";
|
|
2590
|
-
const summary =
|
|
2591
|
-
const
|
|
2592
|
-
|
|
3054
|
+
const summary = mk2("div", "tr-summary");
|
|
3055
|
+
const testSearch = mk2("input", "tr-search");
|
|
3056
|
+
testSearch.type = "search";
|
|
3057
|
+
testSearch.placeholder = t("ui.search_tests");
|
|
3058
|
+
testSearch.addEventListener("keydown", (e) => e.stopPropagation());
|
|
3059
|
+
const list = mk2("div", "list");
|
|
3060
|
+
tests.append(runAll, summary, testSearch, list);
|
|
3061
|
+
testSearch.oninput = () => {
|
|
3062
|
+
const term = fold(testSearch.value.trim());
|
|
3063
|
+
for (const b of scBtns.values()) b.style.display = !term || fold(b.textContent ?? "").includes(term) ? "" : "none";
|
|
3064
|
+
};
|
|
2593
3065
|
const scBtns = /* @__PURE__ */ new Map();
|
|
2594
3066
|
let busy = false;
|
|
2595
3067
|
const mark = (rep) => {
|
|
@@ -2607,7 +3079,7 @@ function createUi(o) {
|
|
|
2607
3079
|
list.textContent = "";
|
|
2608
3080
|
scBtns.clear();
|
|
2609
3081
|
for (const s of o.scenarios()) {
|
|
2610
|
-
const b =
|
|
3082
|
+
const b = mk2("button", "tr-btn", s.name);
|
|
2611
3083
|
b.type = "button";
|
|
2612
3084
|
b.onclick = async () => {
|
|
2613
3085
|
if (busy) return;
|
|
@@ -2633,7 +3105,7 @@ function createUi(o) {
|
|
|
2633
3105
|
busy = false;
|
|
2634
3106
|
}
|
|
2635
3107
|
};
|
|
2636
|
-
const toggle =
|
|
3108
|
+
const toggle = mk2("button", "tr-btn tr-toggle", t("ui.panel"));
|
|
2637
3109
|
toggle.type = "button";
|
|
2638
3110
|
const togglePanels = () => {
|
|
2639
3111
|
root.classList.toggle("open");
|
|
@@ -2642,15 +3114,15 @@ function createUi(o) {
|
|
|
2642
3114
|
toggle.onclick = togglePanels;
|
|
2643
3115
|
root.append(cross, top, bottom, help, left, right, toggle);
|
|
2644
3116
|
const bar = (value, max, cls) => {
|
|
2645
|
-
const b =
|
|
2646
|
-
const i =
|
|
3117
|
+
const b = mk2("div", `tr-bar ${cls}`);
|
|
3118
|
+
const i = mk2("i");
|
|
2647
3119
|
i.style.width = `${Math.max(0, Math.min(100, value / Math.max(1e-9, max) * 100))}%`;
|
|
2648
3120
|
b.appendChild(i);
|
|
2649
3121
|
return b;
|
|
2650
3122
|
};
|
|
2651
3123
|
const row = (k, v) => {
|
|
2652
|
-
const r =
|
|
2653
|
-
r.append(
|
|
3124
|
+
const r = mk2("div", "r");
|
|
3125
|
+
r.append(mk2("span", void 0, k), mk2("span", void 0, v));
|
|
2654
3126
|
return r;
|
|
2655
3127
|
};
|
|
2656
3128
|
const num = (n, d = 1) => {
|
|
@@ -2659,12 +3131,12 @@ function createUi(o) {
|
|
|
2659
3131
|
};
|
|
2660
3132
|
const renderInspector = (i) => {
|
|
2661
3133
|
insp.textContent = "";
|
|
2662
|
-
insp.appendChild(
|
|
3134
|
+
insp.appendChild(mk2("h3", void 0, t("ui.inspector")));
|
|
2663
3135
|
if (!i) {
|
|
2664
|
-
insp.appendChild(
|
|
3136
|
+
insp.appendChild(mk2("div", void 0, t("ui.aim_at_something")));
|
|
2665
3137
|
return;
|
|
2666
3138
|
}
|
|
2667
|
-
insp.appendChild(
|
|
3139
|
+
insp.appendChild(mk2("div", "t", `${i.object} \u203A ${i.part}`));
|
|
2668
3140
|
insp.appendChild(row(t("ins.material"), i.material));
|
|
2669
3141
|
insp.appendChild(row(t("ins.status"), i.status + (i.burning ? ` \xB7 ${t("ins.burning")}` : "")));
|
|
2670
3142
|
insp.appendChild(row(t("ins.hp"), `${num(i.hp, 0)} / ${num(i.maxHp, 0)}`));
|
|
@@ -2683,30 +3155,30 @@ function createUi(o) {
|
|
|
2683
3155
|
insp.appendChild(bar(Math.min(i.stress, 1.2), 1.2, i.stress > 1 ? "bad" : i.stress > 0.7 ? "warn" : "hp"));
|
|
2684
3156
|
}
|
|
2685
3157
|
if (i.joints.length) {
|
|
2686
|
-
insp.appendChild(
|
|
3158
|
+
insp.appendChild(mk2("div", void 0, t("ins.joints") + ":"));
|
|
2687
3159
|
for (const j of i.joints) {
|
|
2688
3160
|
const tools = j.tools.length ? j.tools.join(", ") : t("tool.none");
|
|
2689
|
-
insp.appendChild(
|
|
3161
|
+
insp.appendChild(mk2("div", void 0, `\u2022 ${j.kind} (${j.id})${j.open ? ` \xB7 ${t("ins.open")}` : ` \u2192 ${tools}`}`));
|
|
2690
3162
|
}
|
|
2691
3163
|
}
|
|
2692
|
-
if (i.note) insp.appendChild(
|
|
3164
|
+
if (i.note) insp.appendChild(mk2("div", void 0, i.note));
|
|
2693
3165
|
};
|
|
2694
3166
|
let lastAnim = "";
|
|
2695
3167
|
const renderAnimations = (a) => {
|
|
2696
3168
|
anim.textContent = "";
|
|
2697
|
-
anim.appendChild(
|
|
3169
|
+
anim.appendChild(mk2("h3", void 0, t("ui.animations")));
|
|
2698
3170
|
if (!a) {
|
|
2699
|
-
anim.appendChild(
|
|
3171
|
+
anim.appendChild(mk2("div", "sub", t("ui.animations_aim")));
|
|
2700
3172
|
return;
|
|
2701
3173
|
}
|
|
2702
|
-
anim.appendChild(
|
|
3174
|
+
anim.appendChild(mk2("div", "sub", a.name));
|
|
2703
3175
|
if (a.clips.length === 0) {
|
|
2704
|
-
anim.appendChild(
|
|
3176
|
+
anim.appendChild(mk2("div", "sub", t("ui.animations_none")));
|
|
2705
3177
|
return;
|
|
2706
3178
|
}
|
|
2707
|
-
const l =
|
|
3179
|
+
const l = mk2("div", "list");
|
|
2708
3180
|
for (const c of a.clips) {
|
|
2709
|
-
const b =
|
|
3181
|
+
const b = mk2("button", a.playing === c.id ? "tr-btn on" : "tr-btn", `\u25B6 ${t(`anim.${c.id}`) === `anim.${c.id}` ? c.id : t(`anim.${c.id}`)} (${num(c.duration, 1)} s)`);
|
|
2710
3182
|
b.type = "button";
|
|
2711
3183
|
b.onclick = () => o.onPlay(a.objectId, c.id);
|
|
2712
3184
|
l.appendChild(b);
|
|
@@ -2723,7 +3195,7 @@ function createUi(o) {
|
|
|
2723
3195
|
hud(s) {
|
|
2724
3196
|
toolBox.textContent = "";
|
|
2725
3197
|
toolBox.append(document.createTextNode(s.toolName));
|
|
2726
|
-
if (s.ammo) toolBox.append(
|
|
3198
|
+
if (s.ammo) toolBox.append(mk2("small", void 0, s.ammo));
|
|
2727
3199
|
if (s.status) status.textContent = s.status;
|
|
2728
3200
|
else if (!toastTimer) status.textContent = "";
|
|
2729
3201
|
if (s.progress) {
|
|
@@ -2744,7 +3216,7 @@ function createUi(o) {
|
|
|
2744
3216
|
];
|
|
2745
3217
|
if (s.held.weapon) keys.push(["H", t("hud.equip", { weapon: s.held.weapon })]);
|
|
2746
3218
|
for (const [k, label] of keys) {
|
|
2747
|
-
held.append(
|
|
3219
|
+
held.append(mk2("kbd", void 0, k), document.createTextNode(` ${label} `));
|
|
2748
3220
|
}
|
|
2749
3221
|
} else held.style.display = "none";
|
|
2750
3222
|
const key = s.slots.join("|") + s.page;
|
|
@@ -2754,9 +3226,9 @@ function createUi(o) {
|
|
|
2754
3226
|
slotsEl.length = 0;
|
|
2755
3227
|
hudPage = s.page - 1;
|
|
2756
3228
|
s.slots.forEach((name, i) => {
|
|
2757
|
-
const b =
|
|
3229
|
+
const b = mk2("button", "tr-slot");
|
|
2758
3230
|
b.type = "button";
|
|
2759
|
-
b.append(
|
|
3231
|
+
b.append(mk2("b", void 0, String((i + 1) % 10)), document.createTextNode(name));
|
|
2760
3232
|
b.title = name;
|
|
2761
3233
|
b.onclick = () => o.onTool(hudPage * 10 + i);
|
|
2762
3234
|
hotbar.appendChild(b);
|
|
@@ -2788,7 +3260,17 @@ function createUi(o) {
|
|
|
2788
3260
|
renderTabs();
|
|
2789
3261
|
renderScenarios();
|
|
2790
3262
|
},
|
|
3263
|
+
showGroup(group) {
|
|
3264
|
+
activeGroup = group;
|
|
3265
|
+
itemSearch.value = "";
|
|
3266
|
+
renderTabs();
|
|
3267
|
+
},
|
|
2791
3268
|
togglePanels,
|
|
3269
|
+
bench: benchPanel.show,
|
|
3270
|
+
metrics: benchPanel.metrics,
|
|
3271
|
+
get benchVisible() {
|
|
3272
|
+
return benchPanel.visible;
|
|
3273
|
+
},
|
|
2792
3274
|
dispose() {
|
|
2793
3275
|
offLog();
|
|
2794
3276
|
if (toastTimer) clearTimeout(toastTimer);
|
|
@@ -2801,6 +3283,7 @@ function createUi(o) {
|
|
|
2801
3283
|
|
|
2802
3284
|
// src/room.ts
|
|
2803
3285
|
var FIXED_DT = 1 / 60;
|
|
3286
|
+
var METRICS_INTERVAL_S = 0.25;
|
|
2804
3287
|
var v3 = (v) => [v.x, v.y, v.z];
|
|
2805
3288
|
var dist3 = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
2806
3289
|
function createTestRoom(options) {
|
|
@@ -2858,7 +3341,13 @@ function createTestRoom(options) {
|
|
|
2858
3341
|
const p = world.part(arena.table.id, arena.table.topPart);
|
|
2859
3342
|
return !!p && p.status !== "broken" && p.status !== "melted" && p.status !== "ashes";
|
|
2860
3343
|
};
|
|
2861
|
-
const
|
|
3344
|
+
const restingTops = [];
|
|
3345
|
+
const restingTopAt = (x, z) => {
|
|
3346
|
+
let top = 0;
|
|
3347
|
+
for (const c of restingTops) if (x > c.minX && x < c.maxX && z > c.minZ && z < c.maxZ && world.has(c.objectId)) top = Math.max(top, c.top);
|
|
3348
|
+
return top;
|
|
3349
|
+
};
|
|
3350
|
+
const groundAt = (x, z) => Math.max(x > arena.table.minX && x < arena.table.maxX && z > arena.table.minZ && z < arena.table.maxZ && tableOk() ? arena.table.top : 0, restingTopAt(x, z));
|
|
2862
3351
|
const ownFluids = !options.fluids;
|
|
2863
3352
|
const fluids = options.fluids ?? createFluids({ seed: 1, locale, liquids: options.liquids, groundAt });
|
|
2864
3353
|
const pristineFluids = ownFluids ? fluids.serialize() : null;
|
|
@@ -2991,16 +3480,16 @@ function createTestRoom(options) {
|
|
|
2991
3480
|
const puncture = (ev) => {
|
|
2992
3481
|
const e = entryOf(ev.object);
|
|
2993
3482
|
if (!e?.containerId || e.fluidPart !== ev.part) return;
|
|
2994
|
-
const
|
|
2995
|
-
|
|
2996
|
-
tmpM.copy(
|
|
3483
|
+
const host2 = hostOf(e);
|
|
3484
|
+
host2.root.updateMatrixWorld(true);
|
|
3485
|
+
tmpM.copy(host2.root.matrixWorld).invert();
|
|
2997
3486
|
const local = tmpV.set(ev.point[0], ev.point[1], ev.point[2]).applyMatrix4(tmpM);
|
|
2998
3487
|
local.x -= e.fluidOffset[0];
|
|
2999
3488
|
local.y -= e.fluidOffset[1];
|
|
3000
3489
|
local.z -= e.fluidOffset[2];
|
|
3001
|
-
|
|
3490
|
+
host2.root.getWorldQuaternion(tmpQ).invert();
|
|
3002
3491
|
const n = tmpV2.set(ev.direction[0], ev.direction[1], ev.direction[2]).applyQuaternion(tmpQ);
|
|
3003
|
-
const part = world.part(
|
|
3492
|
+
const part = world.part(host2.objectId, ev.part);
|
|
3004
3493
|
const hole = part?.holes[part.holes.length - 1];
|
|
3005
3494
|
fluids.puncture(e.containerId, [local.x, local.y, local.z], [n.x, n.y, n.z], hole?.diameter ?? 9e-3);
|
|
3006
3495
|
};
|
|
@@ -3206,6 +3695,7 @@ function createTestRoom(options) {
|
|
|
3206
3695
|
};
|
|
3207
3696
|
const clear = () => {
|
|
3208
3697
|
clearEverything();
|
|
3698
|
+
restingTops.length = 0;
|
|
3209
3699
|
pending = [];
|
|
3210
3700
|
action("clear", t("act.clear"));
|
|
3211
3701
|
};
|
|
@@ -3425,6 +3915,7 @@ function createTestRoom(options) {
|
|
|
3425
3915
|
}
|
|
3426
3916
|
});
|
|
3427
3917
|
player.kick(recoil[0], recoil[1]);
|
|
3918
|
+
emitHost({ type: "shot", weapon: toolId, shots: shots.length, recoil, rounds: shots.filter((s) => s.pellet === 0).length, mode: tool.mode });
|
|
3428
3919
|
const f = first;
|
|
3429
3920
|
const d = f ? num(f.distance, 1) : "";
|
|
3430
3921
|
let text;
|
|
@@ -3606,6 +4097,7 @@ function createTestRoom(options) {
|
|
|
3606
4097
|
const detonate = (p, at, impact) => {
|
|
3607
4098
|
const w = toolName(p.weapon);
|
|
3608
4099
|
const results = world.explode(at, p.energy, p.radius, "player");
|
|
4100
|
+
renderer.explode(at, p.energy, p.radius);
|
|
3609
4101
|
action("explode", t("act.explode", { weapon: w, energy: num(p.energy, 0), radius: num(p.radius, 1), n: results.length }), { data: { weapon: p.weapon, energyJ: p.energy, radius: p.radius, point: at } });
|
|
3610
4102
|
if (p.fire) {
|
|
3611
4103
|
fluids.splash("fuel", [at[0], Math.max(at[1], groundAt(at[0], at[2])), at[2]], 0.5);
|
|
@@ -3664,7 +4156,8 @@ function createTestRoom(options) {
|
|
|
3664
4156
|
if (cat === "hand") return;
|
|
3665
4157
|
const res = tool.trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], dt, held, "player");
|
|
3666
4158
|
if (res.status === "empty") {
|
|
3667
|
-
|
|
4159
|
+
emitHost({ type: "empty", weapon: toolId });
|
|
4160
|
+
if (!tool.reloading && tool.reserve > 0) startReload();
|
|
3668
4161
|
say(t(tool.reloading ? "hud.reloading" : "hud.empty"));
|
|
3669
4162
|
return;
|
|
3670
4163
|
}
|
|
@@ -3716,6 +4209,7 @@ function createTestRoom(options) {
|
|
|
3716
4209
|
progress = null;
|
|
3717
4210
|
action("tool", t("act.tool", { weapon: toolName(id) }), { data: { weapon: id } });
|
|
3718
4211
|
fire("tool", id);
|
|
4212
|
+
emitHost({ type: "tool", weapon: id });
|
|
3719
4213
|
};
|
|
3720
4214
|
const cycleTool = (dir) => {
|
|
3721
4215
|
if (!loadout.length) return;
|
|
@@ -3943,13 +4437,16 @@ function createTestRoom(options) {
|
|
|
3943
4437
|
case "KeyR":
|
|
3944
4438
|
if (heldPiece) heldYaw += Math.PI / 8;
|
|
3945
4439
|
else {
|
|
3946
|
-
|
|
4440
|
+
startReload();
|
|
3947
4441
|
say(t("hud.reloading"));
|
|
3948
4442
|
}
|
|
3949
4443
|
break;
|
|
3950
|
-
case "KeyQ":
|
|
3951
|
-
tool.
|
|
4444
|
+
case "KeyQ": {
|
|
4445
|
+
const before = tool.mode;
|
|
4446
|
+
const mode = tool.cycleMode();
|
|
4447
|
+
if (mode !== before) emitHost({ type: "mode", weapon: toolId, mode });
|
|
3952
4448
|
break;
|
|
4449
|
+
}
|
|
3953
4450
|
case "KeyZ":
|
|
3954
4451
|
tiltKey = 1;
|
|
3955
4452
|
break;
|
|
@@ -4037,14 +4534,18 @@ function createTestRoom(options) {
|
|
|
4037
4534
|
let timeScale = 1;
|
|
4038
4535
|
let fastForward = false;
|
|
4039
4536
|
let hudAcc = 1;
|
|
4537
|
+
let metricsAcc = 0;
|
|
4538
|
+
let ownsLoop = false;
|
|
4539
|
+
const roomMetricIds = options.metrics === false ? [] : options.metrics ?? ROOM_METRICS;
|
|
4540
|
+
const roomMetrics = createRoomMetrics({ gl, destruction: renderer, fluids: fluidView, ids: roomMetricIds });
|
|
4040
4541
|
let qualityName = quality;
|
|
4041
4542
|
const syncContainers = (sdt) => {
|
|
4042
4543
|
for (const e of entries.values()) {
|
|
4043
4544
|
if (!e.containerId) continue;
|
|
4044
|
-
const
|
|
4045
|
-
|
|
4046
|
-
const q =
|
|
4047
|
-
const p = tmpV.set(e.fluidOffset[0], e.fluidOffset[1], e.fluidOffset[2]).applyMatrix4(
|
|
4545
|
+
const host2 = hostOf(e);
|
|
4546
|
+
host2.root.updateMatrixWorld(true);
|
|
4547
|
+
const q = host2.root.getWorldQuaternion(tmpQ);
|
|
4548
|
+
const p = tmpV.set(e.fluidOffset[0], e.fluidOffset[1], e.fluidOffset[2]).applyMatrix4(host2.root.matrixWorld);
|
|
4048
4549
|
const vel = sdt > 0 ? [(p.x - e.last[0]) / sdt, (p.y - e.last[1]) / sdt, (p.z - e.last[2]) / sdt] : void 0;
|
|
4049
4550
|
e.last = [p.x, p.y, p.z];
|
|
4050
4551
|
fluids.setTransform(e.containerId, [p.x, p.y, p.z], [q.x, q.y, q.z, q.w], vel);
|
|
@@ -4055,9 +4556,9 @@ function createTestRoom(options) {
|
|
|
4055
4556
|
if (!e.containerId || !e.fluidPart) continue;
|
|
4056
4557
|
const c = fluids.container(e.containerId);
|
|
4057
4558
|
if (!c) continue;
|
|
4058
|
-
const
|
|
4059
|
-
const part = world.part(
|
|
4060
|
-
if (part && part.liters !== void 0 && Math.abs(part.liters - c.liters) > 1e-4) world.setLiters(
|
|
4559
|
+
const host2 = hostOf(e);
|
|
4560
|
+
const part = world.part(host2.objectId, e.fluidPart);
|
|
4561
|
+
if (part && part.liters !== void 0 && Math.abs(part.liters - c.liters) > 1e-4) world.setLiters(host2.objectId, e.fluidPart, c.liters);
|
|
4061
4562
|
}
|
|
4062
4563
|
};
|
|
4063
4564
|
let paused = false;
|
|
@@ -4088,6 +4589,9 @@ function createTestRoom(options) {
|
|
|
4088
4589
|
updateImpacts();
|
|
4089
4590
|
fluidView.update(dt);
|
|
4090
4591
|
for (const s of tools.values()) s.update(dt);
|
|
4592
|
+
if (reloadingSeen && !tool.reloading) emitHost({ type: "reload", weapon: toolId, phase: "end" });
|
|
4593
|
+
reloadingSeen = tool.reloading;
|
|
4594
|
+
for (const update2 of frameListeners) update2(dt);
|
|
4091
4595
|
for (const tr of tracers) {
|
|
4092
4596
|
if (tr.life > 0) {
|
|
4093
4597
|
tr.life -= dt;
|
|
@@ -4097,6 +4601,7 @@ function createTestRoom(options) {
|
|
|
4097
4601
|
flush();
|
|
4098
4602
|
};
|
|
4099
4603
|
const update = (dt) => {
|
|
4604
|
+
if (!ownsLoop) roomMetrics.frame(dt);
|
|
4100
4605
|
player.update(dt);
|
|
4101
4606
|
if (tiltKey || heldTilt > 0) {
|
|
4102
4607
|
heldTilt = Math.max(0, Math.min(2.4, heldTilt + (tiltKey ? 2.2 : -3) * dt));
|
|
@@ -4115,6 +4620,11 @@ function createTestRoom(options) {
|
|
|
4115
4620
|
hudAcc = 0;
|
|
4116
4621
|
ui.hud(hudState());
|
|
4117
4622
|
}
|
|
4623
|
+
metricsAcc += dt;
|
|
4624
|
+
if (metricsAcc >= METRICS_INTERVAL_S && ui) {
|
|
4625
|
+
metricsAcc = 0;
|
|
4626
|
+
if (ui.benchVisible) ui.metrics(metricRows());
|
|
4627
|
+
}
|
|
4118
4628
|
};
|
|
4119
4629
|
const hudState = () => {
|
|
4120
4630
|
const page = Math.floor(toolIndex / 10);
|
|
@@ -4229,6 +4739,103 @@ function createTestRoom(options) {
|
|
|
4229
4739
|
const moduleCheck = (ok, text, data, itemId) => {
|
|
4230
4740
|
log.add({ channel: "check", type: ok ? "pass" : "fail", text: `${ok ? "\u2713" : "\u2717"} ${text}`, itemId, data: { ok, ...data } });
|
|
4231
4741
|
};
|
|
4742
|
+
const modules = /* @__PURE__ */ new Map();
|
|
4743
|
+
const detachers = /* @__PURE__ */ new Map();
|
|
4744
|
+
const benches = /* @__PURE__ */ new Map();
|
|
4745
|
+
let activeBench = null;
|
|
4746
|
+
const lastBench = () => [...benches.keys()].pop() ?? null;
|
|
4747
|
+
const benchView = () => {
|
|
4748
|
+
const store = activeBench ? benches.get(activeBench) : void 0;
|
|
4749
|
+
if (!store) return null;
|
|
4750
|
+
return { title: modules.get(store.moduleId)?.name ?? store.moduleId, controls: store.controls(), layers: store.layers() };
|
|
4751
|
+
};
|
|
4752
|
+
const METRIC_UNAVAILABLE = "n/a";
|
|
4753
|
+
const metricText = (value, digits, unit) => typeof value === "number" ? `${num(value, digits)}${unit ? ` ${unit}` : ""}` : value ?? METRIC_UNAVAILABLE;
|
|
4754
|
+
const reportedFailures = /* @__PURE__ */ new Set();
|
|
4755
|
+
const reportOnce = (key, text) => {
|
|
4756
|
+
if (reportedFailures.has(key)) return;
|
|
4757
|
+
reportedFailures.add(key);
|
|
4758
|
+
host.log(text);
|
|
4759
|
+
};
|
|
4760
|
+
const failureText = (error) => error instanceof Error ? error.message : String(error);
|
|
4761
|
+
const moduleMetrics = () => {
|
|
4762
|
+
const store = activeBench ? benches.get(activeBench) : void 0;
|
|
4763
|
+
const out = {};
|
|
4764
|
+
for (const metric of store?.metrics ?? []) {
|
|
4765
|
+
try {
|
|
4766
|
+
out[metric.id] = metric.read();
|
|
4767
|
+
} catch (error) {
|
|
4768
|
+
out[metric.id] = METRIC_UNAVAILABLE;
|
|
4769
|
+
reportOnce(`${store?.moduleId}:metric:${metric.id}`, t("mod.metric_failed", { name: modules.get(store?.moduleId ?? "")?.name ?? "", metric: metric.id, error: failureText(error) }));
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
return out;
|
|
4773
|
+
};
|
|
4774
|
+
const metricRows = () => {
|
|
4775
|
+
const sampled = roomMetrics.sample();
|
|
4776
|
+
const room2 = roomMetricIds.flatMap((id) => {
|
|
4777
|
+
const value = sampled[id];
|
|
4778
|
+
return value === void 0 ? [] : [{ id, name: t(`metric.${id}`), text: num(value, id === "frame_ms" ? 1 : 0) }];
|
|
4779
|
+
});
|
|
4780
|
+
const values = moduleMetrics();
|
|
4781
|
+
const declared = (activeBench ? benches.get(activeBench)?.metrics : void 0) ?? [];
|
|
4782
|
+
const module = declared.map((metric) => {
|
|
4783
|
+
const value = values[metric.id];
|
|
4784
|
+
const ratio = metric.max && typeof value === "number" ? value / metric.max : void 0;
|
|
4785
|
+
return { id: metric.id, name: metric.name, text: metricText(value, metric.digits ?? 0, metric.unit), ...ratio === void 0 ? {} : { ratio } };
|
|
4786
|
+
});
|
|
4787
|
+
return { room: room2, module };
|
|
4788
|
+
};
|
|
4789
|
+
const showBench = () => {
|
|
4790
|
+
if (!ui) return;
|
|
4791
|
+
ui.bench(benchView());
|
|
4792
|
+
ui.metrics(metricRows());
|
|
4793
|
+
};
|
|
4794
|
+
const hostListeners = /* @__PURE__ */ new Set();
|
|
4795
|
+
const frameListeners = /* @__PURE__ */ new Set();
|
|
4796
|
+
let reloadingSeen = false;
|
|
4797
|
+
function emitHost(event) {
|
|
4798
|
+
for (const listener of hostListeners) listener(event);
|
|
4799
|
+
}
|
|
4800
|
+
function startReload() {
|
|
4801
|
+
if (tool.reload()) emitHost({ type: "reload", weapon: toolId, phase: "start" });
|
|
4802
|
+
}
|
|
4803
|
+
const host = {
|
|
4804
|
+
THREE,
|
|
4805
|
+
scene,
|
|
4806
|
+
camera,
|
|
4807
|
+
locale,
|
|
4808
|
+
groundAt,
|
|
4809
|
+
tool: () => ({ weapon: toolId, category: tool.def.category, ammo: tool.ammo, capacity: tool.capacity, reserve: tool.reserve, reloading: tool.reloading, mode: tool.mode }),
|
|
4810
|
+
on(listener) {
|
|
4811
|
+
hostListeners.add(listener);
|
|
4812
|
+
return () => hostListeners.delete(listener);
|
|
4813
|
+
},
|
|
4814
|
+
onFrame(update2) {
|
|
4815
|
+
frameListeners.add(update2);
|
|
4816
|
+
return () => frameListeners.delete(update2);
|
|
4817
|
+
},
|
|
4818
|
+
log(text, data) {
|
|
4819
|
+
action("module_note", text, { data });
|
|
4820
|
+
}
|
|
4821
|
+
};
|
|
4822
|
+
const lendBench = (store) => ({
|
|
4823
|
+
value: store.value,
|
|
4824
|
+
layer: store.layer,
|
|
4825
|
+
onChange: store.onChange,
|
|
4826
|
+
patch: store.patch,
|
|
4827
|
+
look: (eye, target) => room.look(eye, target)
|
|
4828
|
+
});
|
|
4829
|
+
const checkModuleBench = (mod, fail) => {
|
|
4830
|
+
const store = createBenchStore(mod, locale, (error) => reportOnce(`${mod.id}:listener`, t("mod.listener_failed", { name: mod.name, error: failureText(error) })));
|
|
4831
|
+
const { controls, layers, metrics } = store.declared;
|
|
4832
|
+
if (controls.length + layers.length + metrics.length === 0) return null;
|
|
4833
|
+
const r = checkBench(store.declared);
|
|
4834
|
+
const text = r.ok ? t("mod.bench_ok", { name: mod.name, controls: controls.length, layers: layers.length, metrics: metrics.length }) : t("mod.bench_fail", { name: mod.name, problems: r.problems.join("; ") });
|
|
4835
|
+
moduleCheck(r.ok, text, { module: mod.id, check: "bench", problems: r.problems });
|
|
4836
|
+
for (const p of r.problems) fail(`bench: ${p}`);
|
|
4837
|
+
return r.ok ? store : null;
|
|
4838
|
+
};
|
|
4232
4839
|
const loadModule = (mod) => {
|
|
4233
4840
|
const report = { module: mod.id, items: [], scenarios: [], ok: true, problems: [] };
|
|
4234
4841
|
const fail = (problem) => {
|
|
@@ -4293,6 +4900,29 @@ function createTestRoom(options) {
|
|
|
4293
4900
|
report.scenarios.push(sc.id);
|
|
4294
4901
|
}
|
|
4295
4902
|
action("module", t("act.module_loaded", { name: mod.name, items: report.items.length, scenarios: report.scenarios.length, result: t(report.ok ? "mod.all_ok" : "mod.some_failed") }), { data: { module: mod.id, items: report.items, scenarios: report.scenarios, ok: report.ok } });
|
|
4903
|
+
const bench = checkModuleBench(mod, fail);
|
|
4904
|
+
detachers.get(mod.id)?.();
|
|
4905
|
+
detachers.delete(mod.id);
|
|
4906
|
+
const previous = benches.get(mod.id);
|
|
4907
|
+
const kept = previous?.encode();
|
|
4908
|
+
previous?.dispose();
|
|
4909
|
+
benches.delete(mod.id);
|
|
4910
|
+
for (const key of [...reportedFailures]) if (key.startsWith(`${mod.id}:`)) reportedFailures.delete(key);
|
|
4911
|
+
modules.set(mod.id, mod);
|
|
4912
|
+
if (bench) {
|
|
4913
|
+
const seed = options.bench?.state?.[mod.id];
|
|
4914
|
+
const restored = kept ?? (typeof seed === "string" ? seed : void 0);
|
|
4915
|
+
if (restored !== void 0) bench.restore(restored);
|
|
4916
|
+
benches.set(mod.id, bench);
|
|
4917
|
+
bench.watch(() => {
|
|
4918
|
+
options.bench?.onChange?.(mod.id, bench.encode());
|
|
4919
|
+
if (activeBench === mod.id) ui?.bench(benchView());
|
|
4920
|
+
});
|
|
4921
|
+
if (!activeBench || !benches.has(activeBench)) activeBench = mod.id;
|
|
4922
|
+
} else if (activeBench === mod.id) activeBench = lastBench();
|
|
4923
|
+
if (mod.attach) detachers.set(mod.id, mod.attach(bench ? { ...host, bench: lendBench(bench) } : host));
|
|
4924
|
+
bench?.sendInitial();
|
|
4925
|
+
showBench();
|
|
4296
4926
|
ui?.refresh();
|
|
4297
4927
|
return report;
|
|
4298
4928
|
};
|
|
@@ -4348,6 +4978,39 @@ function createTestRoom(options) {
|
|
|
4348
4978
|
}
|
|
4349
4979
|
return report;
|
|
4350
4980
|
};
|
|
4981
|
+
const layArea = (layout) => {
|
|
4982
|
+
const holders = layout.placements.filter((p) => p.surface);
|
|
4983
|
+
const rest = layout.placements.filter((p) => !p.surface);
|
|
4984
|
+
const placed = [...holders, ...rest].map((p) => {
|
|
4985
|
+
const so = spawn(p.item, p.position, p.rotationY ?? 0);
|
|
4986
|
+
if (p.surface) {
|
|
4987
|
+
const [hx, hz] = p.surface.halfSize;
|
|
4988
|
+
restingTops.push({ objectId: so.objectId, minX: p.position[0] - hx, maxX: p.position[0] + hx, minZ: p.position[2] - hz, maxZ: p.position[2] + hz, top: p.surface.height });
|
|
4989
|
+
}
|
|
4990
|
+
return { so, asleep: p.asleep === true };
|
|
4991
|
+
});
|
|
4992
|
+
for (const { so, asleep } of placed) {
|
|
4993
|
+
const piece = asleep ? rendererPieces.get(so.objectId) : void 0;
|
|
4994
|
+
if (piece) piece.sleeping = true;
|
|
4995
|
+
}
|
|
4996
|
+
if (layout.view) room.look(layout.view.eye, layout.view.target);
|
|
4997
|
+
return placed.map(({ so }) => so);
|
|
4998
|
+
};
|
|
4999
|
+
const openArea = (area) => {
|
|
5000
|
+
clear();
|
|
5001
|
+
const mod = [...modules.values()].find((m) => m.id === area || m.group === area);
|
|
5002
|
+
const label = mod?.group ?? S.groups[area] ?? area;
|
|
5003
|
+
ui?.showGroup(label);
|
|
5004
|
+
activeBench = mod && benches.has(mod.id) ? mod.id : lastBench();
|
|
5005
|
+
showBench();
|
|
5006
|
+
const layout = mod?.area?.(locale);
|
|
5007
|
+
if (layout) return layArea(layout);
|
|
5008
|
+
const items = catalog.filter((item) => item.group === label);
|
|
5009
|
+
const front = items.filter((item) => item.movable !== false);
|
|
5010
|
+
const back = items.filter((item) => item.movable === false);
|
|
5011
|
+
const row = (list, gap, z) => list.map((item, index) => spawn(item.id, [(index - (list.length - 1) / 2) * gap, 0, z], 0));
|
|
5012
|
+
return [...row(front, 1.6, 1.5), ...row(back, 4.5, -4)];
|
|
5013
|
+
};
|
|
4351
5014
|
const room = {
|
|
4352
5015
|
world,
|
|
4353
5016
|
renderer,
|
|
@@ -4427,10 +5090,22 @@ function createTestRoom(options) {
|
|
|
4427
5090
|
fastForward = on;
|
|
4428
5091
|
},
|
|
4429
5092
|
equip,
|
|
5093
|
+
openArea,
|
|
4430
5094
|
loadModule,
|
|
4431
5095
|
animations: animationsOf,
|
|
4432
5096
|
playAnimation,
|
|
5097
|
+
bench(moduleId) {
|
|
5098
|
+
const id = moduleId ?? activeBench;
|
|
5099
|
+
return (id ? benches.get(id) : void 0) ?? null;
|
|
5100
|
+
},
|
|
5101
|
+
metrics() {
|
|
5102
|
+
return { room: roomMetrics.sample(), module: moduleMetrics() };
|
|
5103
|
+
},
|
|
4433
5104
|
dispose() {
|
|
5105
|
+
for (const detach of detachers.values()) detach();
|
|
5106
|
+
detachers.clear();
|
|
5107
|
+
for (const store of benches.values()) store.dispose();
|
|
5108
|
+
benches.clear();
|
|
4434
5109
|
cancelAnimationFrame?.(raf);
|
|
4435
5110
|
window.removeEventListener("resize", onResize);
|
|
4436
5111
|
player.dispose();
|
|
@@ -4501,25 +5176,40 @@ function createTestRoom(options) {
|
|
|
4501
5176
|
onPlay: (objectId, clipId) => {
|
|
4502
5177
|
playAnimation(clipId, objectId);
|
|
4503
5178
|
},
|
|
4504
|
-
onResize: resize
|
|
5179
|
+
onResize: resize,
|
|
5180
|
+
onControl: (id, value) => {
|
|
5181
|
+
if (activeBench) benches.get(activeBench)?.set(id, value);
|
|
5182
|
+
},
|
|
5183
|
+
onLayer: (id, on) => {
|
|
5184
|
+
if (activeBench) benches.get(activeBench)?.setLayer(id, on);
|
|
5185
|
+
},
|
|
5186
|
+
onAction: (id) => {
|
|
5187
|
+
if (activeBench) benches.get(activeBench)?.act(id);
|
|
5188
|
+
}
|
|
4505
5189
|
});
|
|
4506
5190
|
ui.hud(hudState());
|
|
5191
|
+
showBench();
|
|
4507
5192
|
}
|
|
4508
5193
|
let raf = 0;
|
|
4509
5194
|
if (ownRenderer && typeof requestAnimationFrame === "function") {
|
|
4510
5195
|
let last = performance.now();
|
|
5196
|
+
ownsLoop = true;
|
|
4511
5197
|
const loop = (now) => {
|
|
4512
5198
|
raf = requestAnimationFrame(loop);
|
|
4513
|
-
const
|
|
5199
|
+
const realDelta = (now - last) / 1e3;
|
|
5200
|
+
roomMetrics.frame(realDelta);
|
|
5201
|
+
const dt = Math.min(0.05, realDelta);
|
|
4514
5202
|
last = now;
|
|
4515
5203
|
if (!scenarioRunning) update(dt);
|
|
4516
5204
|
gl.render(scene, camera);
|
|
5205
|
+
roomMetrics.rendered();
|
|
4517
5206
|
};
|
|
4518
5207
|
raf = requestAnimationFrame(loop);
|
|
4519
5208
|
}
|
|
4520
5209
|
if (options.initial) {
|
|
4521
5210
|
for (const [i, id] of options.initial.entries()) spawn(id, [(i - (options.initial.length - 1) / 2) * 1.3, 0, 0.5]);
|
|
4522
5211
|
}
|
|
5212
|
+
if (options.area) openArea(options.area);
|
|
4523
5213
|
return room;
|
|
4524
5214
|
}
|
|
4525
5215
|
function structuredCloneSpec(spec) {
|
|
@@ -4534,7 +5224,9 @@ export {
|
|
|
4534
5224
|
buildFromSpec,
|
|
4535
5225
|
createLog,
|
|
4536
5226
|
createTestRoom,
|
|
5227
|
+
decodeBenchState,
|
|
4537
5228
|
demoItems,
|
|
5229
|
+
encodeBenchState,
|
|
4538
5230
|
locales,
|
|
4539
5231
|
makeScenarios,
|
|
4540
5232
|
resolveStrings,
|