@adatechnology/game-test-room 0.2.0 → 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 +48 -2
- package/dist/index.global.js +734 -113
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +662 -87
- 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",
|
|
@@ -511,6 +526,21 @@ var ptBR = {
|
|
|
511
526
|
"ui.animations": "Anima\xE7\xF5es",
|
|
512
527
|
"ui.animations_aim": "Mire num item ou segure-o para ver as anima\xE7\xF5es.",
|
|
513
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",
|
|
514
544
|
"act.spawn": "Criou {name} em x {x} \xB7 z {z}",
|
|
515
545
|
"act.clear": "Limpou a sala",
|
|
516
546
|
"act.reset": "Consertou todos os objetos",
|
|
@@ -1122,6 +1152,154 @@ function createArena(THREE, size, scene) {
|
|
|
1122
1152
|
};
|
|
1123
1153
|
}
|
|
1124
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
|
+
|
|
1125
1303
|
// src/log.ts
|
|
1126
1304
|
var MAX_ENTRIES = 5e3;
|
|
1127
1305
|
var pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
@@ -1250,6 +1428,56 @@ function createLog(options = {}) {
|
|
|
1250
1428
|
return log;
|
|
1251
1429
|
}
|
|
1252
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
|
+
|
|
1253
1481
|
// src/narrate.ts
|
|
1254
1482
|
var SKIP_DAMAGE = /* @__PURE__ */ new Set(["fire", "heat"]);
|
|
1255
1483
|
function createNarrator(c) {
|
|
@@ -2320,6 +2548,209 @@ function scenariosFor(locale = "en") {
|
|
|
2320
2548
|
}
|
|
2321
2549
|
var SCENARIOS = scenariosFor("en");
|
|
2322
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
|
+
|
|
2323
2754
|
// src/ui.ts
|
|
2324
2755
|
var CSS = `
|
|
2325
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}
|
|
@@ -2372,6 +2803,13 @@ var CSS = `
|
|
|
2372
2803
|
.tr-cat{flex:0 1 auto;max-height:40%}
|
|
2373
2804
|
.tr-anim{flex:0 0 auto;max-height:18%}
|
|
2374
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}
|
|
2375
2813
|
.tr-tests{flex:1 1 auto;min-height:100px}
|
|
2376
2814
|
.tr-tests .list{display:flex;flex-direction:column;gap:3px;margin-top:6px}.tr-tests .list .tr-btn{text-align:left}
|
|
2377
2815
|
.tr-row{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;align-items:center}
|
|
@@ -2380,7 +2818,7 @@ var CSS = `
|
|
|
2380
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}}
|
|
2381
2819
|
`;
|
|
2382
2820
|
var fold = (text) => text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
|
2383
|
-
var
|
|
2821
|
+
var mk2 = (tag, cls, text) => {
|
|
2384
2822
|
const e = document.createElement(tag);
|
|
2385
2823
|
if (cls) e.className = cls;
|
|
2386
2824
|
if (text !== void 0) e.textContent = text;
|
|
@@ -2388,42 +2826,42 @@ var mk = (tag, cls, text) => {
|
|
|
2388
2826
|
};
|
|
2389
2827
|
function createUi(o) {
|
|
2390
2828
|
const { t } = o;
|
|
2391
|
-
const style =
|
|
2829
|
+
const style = mk2("style");
|
|
2392
2830
|
style.textContent = CSS;
|
|
2393
|
-
const root =
|
|
2831
|
+
const root = mk2("div", "tr-root");
|
|
2394
2832
|
const pos = getComputedStyle?.(o.container)?.position;
|
|
2395
2833
|
if (!pos || pos === "static") o.container.style.position = "relative";
|
|
2396
2834
|
root.appendChild(style);
|
|
2397
2835
|
o.container.appendChild(root);
|
|
2398
|
-
const cross =
|
|
2399
|
-
cross.appendChild(
|
|
2400
|
-
const top =
|
|
2401
|
-
const toolBox =
|
|
2402
|
-
const status =
|
|
2403
|
-
const prog =
|
|
2404
|
-
const progLabel =
|
|
2405
|
-
const progBar =
|
|
2406
|
-
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");
|
|
2407
2845
|
progBar.appendChild(progFill);
|
|
2408
2846
|
prog.append(progLabel, progBar);
|
|
2409
2847
|
top.append(toolBox, status, prog);
|
|
2410
|
-
const bottom =
|
|
2411
|
-
const held =
|
|
2412
|
-
const hotbar =
|
|
2848
|
+
const bottom = mk2("div", "tr-bottom");
|
|
2849
|
+
const held = mk2("div", "tr-held");
|
|
2850
|
+
const hotbar = mk2("div", "tr-hotbar");
|
|
2413
2851
|
bottom.append(held, hotbar);
|
|
2414
|
-
const help =
|
|
2852
|
+
const help = mk2("div", "tr-help");
|
|
2415
2853
|
help.textContent = t("ui.help");
|
|
2416
|
-
const left =
|
|
2417
|
-
const insp =
|
|
2418
|
-
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");
|
|
2419
2857
|
left.append(insp, logPanel);
|
|
2420
|
-
const logTitle =
|
|
2421
|
-
const logBar =
|
|
2422
|
-
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");
|
|
2423
2861
|
const filters = { action: true, event: true, check: true };
|
|
2424
2862
|
const filterBtns = [];
|
|
2425
2863
|
for (const ch of ["action", "event", "check"]) {
|
|
2426
|
-
const b =
|
|
2864
|
+
const b = mk2("button", "tr-btn on", t(`ui.filter.${ch}`));
|
|
2427
2865
|
b.type = "button";
|
|
2428
2866
|
b.onclick = () => {
|
|
2429
2867
|
filters[ch] = !filters[ch];
|
|
@@ -2434,8 +2872,8 @@ function createUi(o) {
|
|
|
2434
2872
|
logBar.appendChild(b);
|
|
2435
2873
|
}
|
|
2436
2874
|
let autoscroll = true;
|
|
2437
|
-
const asLabel =
|
|
2438
|
-
const asBox =
|
|
2875
|
+
const asLabel = mk2("label");
|
|
2876
|
+
const asBox = mk2("input");
|
|
2439
2877
|
asBox.type = "checkbox";
|
|
2440
2878
|
asBox.checked = true;
|
|
2441
2879
|
asBox.onchange = () => {
|
|
@@ -2444,7 +2882,7 @@ function createUi(o) {
|
|
|
2444
2882
|
asLabel.append(asBox, document.createTextNode(" " + t("ui.autoscroll")));
|
|
2445
2883
|
logBar.appendChild(asLabel);
|
|
2446
2884
|
const logBtn = (label, fn) => {
|
|
2447
|
-
const b =
|
|
2885
|
+
const b = mk2("button", "tr-btn", label);
|
|
2448
2886
|
b.type = "button";
|
|
2449
2887
|
b.onclick = fn;
|
|
2450
2888
|
logBar.appendChild(b);
|
|
@@ -2452,7 +2890,7 @@ function createUi(o) {
|
|
|
2452
2890
|
logBtn(t("ui.copy_json"), () => {
|
|
2453
2891
|
const text = o.log.toJSON();
|
|
2454
2892
|
const select = () => {
|
|
2455
|
-
const ta =
|
|
2893
|
+
const ta = mk2("textarea");
|
|
2456
2894
|
ta.value = text;
|
|
2457
2895
|
ta.setAttribute("readonly", "");
|
|
2458
2896
|
ta.style.cssText = "position:fixed;left:8px;bottom:8px;width:min(480px,80vw);height:120px;z-index:99999";
|
|
@@ -2486,12 +2924,12 @@ function createUi(o) {
|
|
|
2486
2924
|
const fill = (n, e) => {
|
|
2487
2925
|
n.className = lineClass(e);
|
|
2488
2926
|
n.textContent = "";
|
|
2489
|
-
const tm =
|
|
2927
|
+
const tm = mk2("time", void 0, fmtTime(e.t));
|
|
2490
2928
|
n.append(tm, document.createTextNode(e.text));
|
|
2491
2929
|
};
|
|
2492
2930
|
const addNode = (e) => {
|
|
2493
2931
|
if (!filters[e.channel]) return;
|
|
2494
|
-
const n =
|
|
2932
|
+
const n = mk2("div");
|
|
2495
2933
|
fill(n, e);
|
|
2496
2934
|
nodes.set(e.seq, n);
|
|
2497
2935
|
logBody.appendChild(n);
|
|
@@ -2513,17 +2951,18 @@ function createUi(o) {
|
|
|
2513
2951
|
if (n) fill(n, e);
|
|
2514
2952
|
} else addNode(e);
|
|
2515
2953
|
});
|
|
2516
|
-
const right =
|
|
2517
|
-
const cat =
|
|
2518
|
-
const anim =
|
|
2519
|
-
const tests =
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
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");
|
|
2523
2962
|
itemSearch.type = "search";
|
|
2524
2963
|
itemSearch.placeholder = t("ui.search_items");
|
|
2525
|
-
const tabs =
|
|
2526
|
-
const items =
|
|
2964
|
+
const tabs = mk2("div", "tr-tabs");
|
|
2965
|
+
const items = mk2("div", "tr-items");
|
|
2527
2966
|
cat.append(itemSearch, tabs, items);
|
|
2528
2967
|
let activeGroup = "";
|
|
2529
2968
|
const visibleItems = () => {
|
|
@@ -2535,17 +2974,17 @@ function createUi(o) {
|
|
|
2535
2974
|
items.textContent = "";
|
|
2536
2975
|
const searching = itemSearch.value.trim() !== "";
|
|
2537
2976
|
const found = visibleItems();
|
|
2538
|
-
if (!found.length) items.appendChild(
|
|
2977
|
+
if (!found.length) items.appendChild(mk2("div", "tr-empty", t("ui.no_results")));
|
|
2539
2978
|
for (const { item: it, group } of found) {
|
|
2540
|
-
const wrap =
|
|
2541
|
-
const b =
|
|
2542
|
-
if (searching) b.appendChild(
|
|
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));
|
|
2543
2982
|
b.type = "button";
|
|
2544
2983
|
b.title = it.note ?? it.name;
|
|
2545
2984
|
b.onclick = () => o.onSpawn(it.id);
|
|
2546
2985
|
wrap.appendChild(b);
|
|
2547
2986
|
if (it.weapon) {
|
|
2548
|
-
const w =
|
|
2987
|
+
const w = mk2("button", "tr-btn", "\u2694");
|
|
2549
2988
|
w.type = "button";
|
|
2550
2989
|
w.title = t("ui.equip");
|
|
2551
2990
|
w.onclick = () => o.onEquip(it.id);
|
|
@@ -2559,7 +2998,7 @@ function createUi(o) {
|
|
|
2559
2998
|
const groupNames = [...o.groups().keys()];
|
|
2560
2999
|
if (!groupNames.includes(activeGroup)) activeGroup = groupNames[0] ?? "";
|
|
2561
3000
|
for (const g of groupNames) {
|
|
2562
|
-
const b =
|
|
3001
|
+
const b = mk2("button", g === activeGroup ? "tr-btn on" : "tr-btn", g);
|
|
2563
3002
|
b.type = "button";
|
|
2564
3003
|
b.onclick = () => {
|
|
2565
3004
|
activeGroup = g;
|
|
@@ -2572,9 +3011,9 @@ function createUi(o) {
|
|
|
2572
3011
|
renderTabs();
|
|
2573
3012
|
itemSearch.oninput = renderItems;
|
|
2574
3013
|
for (const box2 of [itemSearch]) box2.addEventListener("keydown", (e) => e.stopPropagation());
|
|
2575
|
-
const controls =
|
|
3014
|
+
const controls = mk2("div", "tr-row");
|
|
2576
3015
|
const ctl = (label, fn) => {
|
|
2577
|
-
const b =
|
|
3016
|
+
const b = mk2("button", "tr-btn", label);
|
|
2578
3017
|
b.type = "button";
|
|
2579
3018
|
b.onclick = () => fn(b);
|
|
2580
3019
|
controls.appendChild(b);
|
|
@@ -2595,29 +3034,29 @@ function createUi(o) {
|
|
|
2595
3034
|
o.onFast(fast);
|
|
2596
3035
|
});
|
|
2597
3036
|
const tapBtn = ctl(t("ui.tap"), () => o.onTap());
|
|
2598
|
-
const qSel =
|
|
3037
|
+
const qSel = mk2("select");
|
|
2599
3038
|
for (const q of ["low", "medium", "high"]) {
|
|
2600
|
-
const op =
|
|
3039
|
+
const op = mk2("option", void 0, t(`ui.quality.${q}`));
|
|
2601
3040
|
op.value = q;
|
|
2602
3041
|
qSel.appendChild(op);
|
|
2603
3042
|
}
|
|
2604
3043
|
qSel.value = o.quality;
|
|
2605
3044
|
qSel.onchange = () => o.onQuality(qSel.value);
|
|
2606
|
-
const qLabel =
|
|
3045
|
+
const qLabel = mk2("label", void 0, t("ui.quality") + " ");
|
|
2607
3046
|
qLabel.appendChild(qSel);
|
|
2608
3047
|
controls.appendChild(qLabel);
|
|
2609
|
-
const count =
|
|
3048
|
+
const count = mk2("span");
|
|
2610
3049
|
controls.appendChild(count);
|
|
2611
3050
|
cat.appendChild(controls);
|
|
2612
|
-
tests.appendChild(
|
|
2613
|
-
const runAll =
|
|
3051
|
+
tests.appendChild(mk2("h3", void 0, t("ui.tests")));
|
|
3052
|
+
const runAll = mk2("button", "tr-btn", t("ui.run_all"));
|
|
2614
3053
|
runAll.type = "button";
|
|
2615
|
-
const summary =
|
|
2616
|
-
const testSearch =
|
|
3054
|
+
const summary = mk2("div", "tr-summary");
|
|
3055
|
+
const testSearch = mk2("input", "tr-search");
|
|
2617
3056
|
testSearch.type = "search";
|
|
2618
3057
|
testSearch.placeholder = t("ui.search_tests");
|
|
2619
3058
|
testSearch.addEventListener("keydown", (e) => e.stopPropagation());
|
|
2620
|
-
const list =
|
|
3059
|
+
const list = mk2("div", "list");
|
|
2621
3060
|
tests.append(runAll, summary, testSearch, list);
|
|
2622
3061
|
testSearch.oninput = () => {
|
|
2623
3062
|
const term = fold(testSearch.value.trim());
|
|
@@ -2640,7 +3079,7 @@ function createUi(o) {
|
|
|
2640
3079
|
list.textContent = "";
|
|
2641
3080
|
scBtns.clear();
|
|
2642
3081
|
for (const s of o.scenarios()) {
|
|
2643
|
-
const b =
|
|
3082
|
+
const b = mk2("button", "tr-btn", s.name);
|
|
2644
3083
|
b.type = "button";
|
|
2645
3084
|
b.onclick = async () => {
|
|
2646
3085
|
if (busy) return;
|
|
@@ -2666,7 +3105,7 @@ function createUi(o) {
|
|
|
2666
3105
|
busy = false;
|
|
2667
3106
|
}
|
|
2668
3107
|
};
|
|
2669
|
-
const toggle =
|
|
3108
|
+
const toggle = mk2("button", "tr-btn tr-toggle", t("ui.panel"));
|
|
2670
3109
|
toggle.type = "button";
|
|
2671
3110
|
const togglePanels = () => {
|
|
2672
3111
|
root.classList.toggle("open");
|
|
@@ -2675,15 +3114,15 @@ function createUi(o) {
|
|
|
2675
3114
|
toggle.onclick = togglePanels;
|
|
2676
3115
|
root.append(cross, top, bottom, help, left, right, toggle);
|
|
2677
3116
|
const bar = (value, max, cls) => {
|
|
2678
|
-
const b =
|
|
2679
|
-
const i =
|
|
3117
|
+
const b = mk2("div", `tr-bar ${cls}`);
|
|
3118
|
+
const i = mk2("i");
|
|
2680
3119
|
i.style.width = `${Math.max(0, Math.min(100, value / Math.max(1e-9, max) * 100))}%`;
|
|
2681
3120
|
b.appendChild(i);
|
|
2682
3121
|
return b;
|
|
2683
3122
|
};
|
|
2684
3123
|
const row = (k, v) => {
|
|
2685
|
-
const r =
|
|
2686
|
-
r.append(
|
|
3124
|
+
const r = mk2("div", "r");
|
|
3125
|
+
r.append(mk2("span", void 0, k), mk2("span", void 0, v));
|
|
2687
3126
|
return r;
|
|
2688
3127
|
};
|
|
2689
3128
|
const num = (n, d = 1) => {
|
|
@@ -2692,12 +3131,12 @@ function createUi(o) {
|
|
|
2692
3131
|
};
|
|
2693
3132
|
const renderInspector = (i) => {
|
|
2694
3133
|
insp.textContent = "";
|
|
2695
|
-
insp.appendChild(
|
|
3134
|
+
insp.appendChild(mk2("h3", void 0, t("ui.inspector")));
|
|
2696
3135
|
if (!i) {
|
|
2697
|
-
insp.appendChild(
|
|
3136
|
+
insp.appendChild(mk2("div", void 0, t("ui.aim_at_something")));
|
|
2698
3137
|
return;
|
|
2699
3138
|
}
|
|
2700
|
-
insp.appendChild(
|
|
3139
|
+
insp.appendChild(mk2("div", "t", `${i.object} \u203A ${i.part}`));
|
|
2701
3140
|
insp.appendChild(row(t("ins.material"), i.material));
|
|
2702
3141
|
insp.appendChild(row(t("ins.status"), i.status + (i.burning ? ` \xB7 ${t("ins.burning")}` : "")));
|
|
2703
3142
|
insp.appendChild(row(t("ins.hp"), `${num(i.hp, 0)} / ${num(i.maxHp, 0)}`));
|
|
@@ -2716,30 +3155,30 @@ function createUi(o) {
|
|
|
2716
3155
|
insp.appendChild(bar(Math.min(i.stress, 1.2), 1.2, i.stress > 1 ? "bad" : i.stress > 0.7 ? "warn" : "hp"));
|
|
2717
3156
|
}
|
|
2718
3157
|
if (i.joints.length) {
|
|
2719
|
-
insp.appendChild(
|
|
3158
|
+
insp.appendChild(mk2("div", void 0, t("ins.joints") + ":"));
|
|
2720
3159
|
for (const j of i.joints) {
|
|
2721
3160
|
const tools = j.tools.length ? j.tools.join(", ") : t("tool.none");
|
|
2722
|
-
insp.appendChild(
|
|
3161
|
+
insp.appendChild(mk2("div", void 0, `\u2022 ${j.kind} (${j.id})${j.open ? ` \xB7 ${t("ins.open")}` : ` \u2192 ${tools}`}`));
|
|
2723
3162
|
}
|
|
2724
3163
|
}
|
|
2725
|
-
if (i.note) insp.appendChild(
|
|
3164
|
+
if (i.note) insp.appendChild(mk2("div", void 0, i.note));
|
|
2726
3165
|
};
|
|
2727
3166
|
let lastAnim = "";
|
|
2728
3167
|
const renderAnimations = (a) => {
|
|
2729
3168
|
anim.textContent = "";
|
|
2730
|
-
anim.appendChild(
|
|
3169
|
+
anim.appendChild(mk2("h3", void 0, t("ui.animations")));
|
|
2731
3170
|
if (!a) {
|
|
2732
|
-
anim.appendChild(
|
|
3171
|
+
anim.appendChild(mk2("div", "sub", t("ui.animations_aim")));
|
|
2733
3172
|
return;
|
|
2734
3173
|
}
|
|
2735
|
-
anim.appendChild(
|
|
3174
|
+
anim.appendChild(mk2("div", "sub", a.name));
|
|
2736
3175
|
if (a.clips.length === 0) {
|
|
2737
|
-
anim.appendChild(
|
|
3176
|
+
anim.appendChild(mk2("div", "sub", t("ui.animations_none")));
|
|
2738
3177
|
return;
|
|
2739
3178
|
}
|
|
2740
|
-
const l =
|
|
3179
|
+
const l = mk2("div", "list");
|
|
2741
3180
|
for (const c of a.clips) {
|
|
2742
|
-
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)`);
|
|
2743
3182
|
b.type = "button";
|
|
2744
3183
|
b.onclick = () => o.onPlay(a.objectId, c.id);
|
|
2745
3184
|
l.appendChild(b);
|
|
@@ -2756,7 +3195,7 @@ function createUi(o) {
|
|
|
2756
3195
|
hud(s) {
|
|
2757
3196
|
toolBox.textContent = "";
|
|
2758
3197
|
toolBox.append(document.createTextNode(s.toolName));
|
|
2759
|
-
if (s.ammo) toolBox.append(
|
|
3198
|
+
if (s.ammo) toolBox.append(mk2("small", void 0, s.ammo));
|
|
2760
3199
|
if (s.status) status.textContent = s.status;
|
|
2761
3200
|
else if (!toastTimer) status.textContent = "";
|
|
2762
3201
|
if (s.progress) {
|
|
@@ -2777,7 +3216,7 @@ function createUi(o) {
|
|
|
2777
3216
|
];
|
|
2778
3217
|
if (s.held.weapon) keys.push(["H", t("hud.equip", { weapon: s.held.weapon })]);
|
|
2779
3218
|
for (const [k, label] of keys) {
|
|
2780
|
-
held.append(
|
|
3219
|
+
held.append(mk2("kbd", void 0, k), document.createTextNode(` ${label} `));
|
|
2781
3220
|
}
|
|
2782
3221
|
} else held.style.display = "none";
|
|
2783
3222
|
const key = s.slots.join("|") + s.page;
|
|
@@ -2787,9 +3226,9 @@ function createUi(o) {
|
|
|
2787
3226
|
slotsEl.length = 0;
|
|
2788
3227
|
hudPage = s.page - 1;
|
|
2789
3228
|
s.slots.forEach((name, i) => {
|
|
2790
|
-
const b =
|
|
3229
|
+
const b = mk2("button", "tr-slot");
|
|
2791
3230
|
b.type = "button";
|
|
2792
|
-
b.append(
|
|
3231
|
+
b.append(mk2("b", void 0, String((i + 1) % 10)), document.createTextNode(name));
|
|
2793
3232
|
b.title = name;
|
|
2794
3233
|
b.onclick = () => o.onTool(hudPage * 10 + i);
|
|
2795
3234
|
hotbar.appendChild(b);
|
|
@@ -2827,6 +3266,11 @@ function createUi(o) {
|
|
|
2827
3266
|
renderTabs();
|
|
2828
3267
|
},
|
|
2829
3268
|
togglePanels,
|
|
3269
|
+
bench: benchPanel.show,
|
|
3270
|
+
metrics: benchPanel.metrics,
|
|
3271
|
+
get benchVisible() {
|
|
3272
|
+
return benchPanel.visible;
|
|
3273
|
+
},
|
|
2830
3274
|
dispose() {
|
|
2831
3275
|
offLog();
|
|
2832
3276
|
if (toastTimer) clearTimeout(toastTimer);
|
|
@@ -2839,6 +3283,7 @@ function createUi(o) {
|
|
|
2839
3283
|
|
|
2840
3284
|
// src/room.ts
|
|
2841
3285
|
var FIXED_DT = 1 / 60;
|
|
3286
|
+
var METRICS_INTERVAL_S = 0.25;
|
|
2842
3287
|
var v3 = (v) => [v.x, v.y, v.z];
|
|
2843
3288
|
var dist3 = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
2844
3289
|
function createTestRoom(options) {
|
|
@@ -3470,7 +3915,7 @@ function createTestRoom(options) {
|
|
|
3470
3915
|
}
|
|
3471
3916
|
});
|
|
3472
3917
|
player.kick(recoil[0], recoil[1]);
|
|
3473
|
-
emitHost({ type: "shot", weapon: toolId, shots: shots.length, recoil });
|
|
3918
|
+
emitHost({ type: "shot", weapon: toolId, shots: shots.length, recoil, rounds: shots.filter((s) => s.pellet === 0).length, mode: tool.mode });
|
|
3474
3919
|
const f = first;
|
|
3475
3920
|
const d = f ? num(f.distance, 1) : "";
|
|
3476
3921
|
let text;
|
|
@@ -3652,6 +4097,7 @@ function createTestRoom(options) {
|
|
|
3652
4097
|
const detonate = (p, at, impact) => {
|
|
3653
4098
|
const w = toolName(p.weapon);
|
|
3654
4099
|
const results = world.explode(at, p.energy, p.radius, "player");
|
|
4100
|
+
renderer.explode(at, p.energy, p.radius);
|
|
3655
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 } });
|
|
3656
4102
|
if (p.fire) {
|
|
3657
4103
|
fluids.splash("fuel", [at[0], Math.max(at[1], groundAt(at[0], at[2])), at[2]], 0.5);
|
|
@@ -3995,9 +4441,12 @@ function createTestRoom(options) {
|
|
|
3995
4441
|
say(t("hud.reloading"));
|
|
3996
4442
|
}
|
|
3997
4443
|
break;
|
|
3998
|
-
case "KeyQ":
|
|
3999
|
-
tool.
|
|
4444
|
+
case "KeyQ": {
|
|
4445
|
+
const before = tool.mode;
|
|
4446
|
+
const mode = tool.cycleMode();
|
|
4447
|
+
if (mode !== before) emitHost({ type: "mode", weapon: toolId, mode });
|
|
4000
4448
|
break;
|
|
4449
|
+
}
|
|
4001
4450
|
case "KeyZ":
|
|
4002
4451
|
tiltKey = 1;
|
|
4003
4452
|
break;
|
|
@@ -4085,6 +4534,10 @@ function createTestRoom(options) {
|
|
|
4085
4534
|
let timeScale = 1;
|
|
4086
4535
|
let fastForward = false;
|
|
4087
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 });
|
|
4088
4541
|
let qualityName = quality;
|
|
4089
4542
|
const syncContainers = (sdt) => {
|
|
4090
4543
|
for (const e of entries.values()) {
|
|
@@ -4148,6 +4601,7 @@ function createTestRoom(options) {
|
|
|
4148
4601
|
flush();
|
|
4149
4602
|
};
|
|
4150
4603
|
const update = (dt) => {
|
|
4604
|
+
if (!ownsLoop) roomMetrics.frame(dt);
|
|
4151
4605
|
player.update(dt);
|
|
4152
4606
|
if (tiltKey || heldTilt > 0) {
|
|
4153
4607
|
heldTilt = Math.max(0, Math.min(2.4, heldTilt + (tiltKey ? 2.2 : -3) * dt));
|
|
@@ -4166,6 +4620,11 @@ function createTestRoom(options) {
|
|
|
4166
4620
|
hudAcc = 0;
|
|
4167
4621
|
ui.hud(hudState());
|
|
4168
4622
|
}
|
|
4623
|
+
metricsAcc += dt;
|
|
4624
|
+
if (metricsAcc >= METRICS_INTERVAL_S && ui) {
|
|
4625
|
+
metricsAcc = 0;
|
|
4626
|
+
if (ui.benchVisible) ui.metrics(metricRows());
|
|
4627
|
+
}
|
|
4169
4628
|
};
|
|
4170
4629
|
const hudState = () => {
|
|
4171
4630
|
const page = Math.floor(toolIndex / 10);
|
|
@@ -4281,7 +4740,57 @@ function createTestRoom(options) {
|
|
|
4281
4740
|
log.add({ channel: "check", type: ok ? "pass" : "fail", text: `${ok ? "\u2713" : "\u2717"} ${text}`, itemId, data: { ok, ...data } });
|
|
4282
4741
|
};
|
|
4283
4742
|
const modules = /* @__PURE__ */ new Map();
|
|
4284
|
-
const detachers =
|
|
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
|
+
};
|
|
4285
4794
|
const hostListeners = /* @__PURE__ */ new Set();
|
|
4286
4795
|
const frameListeners = /* @__PURE__ */ new Set();
|
|
4287
4796
|
let reloadingSeen = false;
|
|
@@ -4297,7 +4806,7 @@ function createTestRoom(options) {
|
|
|
4297
4806
|
camera,
|
|
4298
4807
|
locale,
|
|
4299
4808
|
groundAt,
|
|
4300
|
-
tool: () => ({ weapon: toolId, category: tool.def.category, ammo: tool.ammo, capacity: tool.capacity, reserve: tool.reserve, reloading: tool.reloading }),
|
|
4809
|
+
tool: () => ({ weapon: toolId, category: tool.def.category, ammo: tool.ammo, capacity: tool.capacity, reserve: tool.reserve, reloading: tool.reloading, mode: tool.mode }),
|
|
4301
4810
|
on(listener) {
|
|
4302
4811
|
hostListeners.add(listener);
|
|
4303
4812
|
return () => hostListeners.delete(listener);
|
|
@@ -4310,6 +4819,23 @@ function createTestRoom(options) {
|
|
|
4310
4819
|
action("module_note", text, { data });
|
|
4311
4820
|
}
|
|
4312
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
|
+
};
|
|
4313
4839
|
const loadModule = (mod) => {
|
|
4314
4840
|
const report = { module: mod.id, items: [], scenarios: [], ok: true, problems: [] };
|
|
4315
4841
|
const fail = (problem) => {
|
|
@@ -4374,8 +4900,29 @@ function createTestRoom(options) {
|
|
|
4374
4900
|
report.scenarios.push(sc.id);
|
|
4375
4901
|
}
|
|
4376
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);
|
|
4377
4911
|
modules.set(mod.id, mod);
|
|
4378
|
-
if (
|
|
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();
|
|
4379
4926
|
ui?.refresh();
|
|
4380
4927
|
return report;
|
|
4381
4928
|
};
|
|
@@ -4454,6 +5001,8 @@ function createTestRoom(options) {
|
|
|
4454
5001
|
const mod = [...modules.values()].find((m) => m.id === area || m.group === area);
|
|
4455
5002
|
const label = mod?.group ?? S.groups[area] ?? area;
|
|
4456
5003
|
ui?.showGroup(label);
|
|
5004
|
+
activeBench = mod && benches.has(mod.id) ? mod.id : lastBench();
|
|
5005
|
+
showBench();
|
|
4457
5006
|
const layout = mod?.area?.(locale);
|
|
4458
5007
|
if (layout) return layArea(layout);
|
|
4459
5008
|
const items = catalog.filter((item) => item.group === label);
|
|
@@ -4545,8 +5094,18 @@ function createTestRoom(options) {
|
|
|
4545
5094
|
loadModule,
|
|
4546
5095
|
animations: animationsOf,
|
|
4547
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
|
+
},
|
|
4548
5104
|
dispose() {
|
|
4549
|
-
for (const detach of detachers.
|
|
5105
|
+
for (const detach of detachers.values()) detach();
|
|
5106
|
+
detachers.clear();
|
|
5107
|
+
for (const store of benches.values()) store.dispose();
|
|
5108
|
+
benches.clear();
|
|
4550
5109
|
cancelAnimationFrame?.(raf);
|
|
4551
5110
|
window.removeEventListener("resize", onResize);
|
|
4552
5111
|
player.dispose();
|
|
@@ -4617,19 +5176,33 @@ function createTestRoom(options) {
|
|
|
4617
5176
|
onPlay: (objectId, clipId) => {
|
|
4618
5177
|
playAnimation(clipId, objectId);
|
|
4619
5178
|
},
|
|
4620
|
-
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
|
+
}
|
|
4621
5189
|
});
|
|
4622
5190
|
ui.hud(hudState());
|
|
5191
|
+
showBench();
|
|
4623
5192
|
}
|
|
4624
5193
|
let raf = 0;
|
|
4625
5194
|
if (ownRenderer && typeof requestAnimationFrame === "function") {
|
|
4626
5195
|
let last = performance.now();
|
|
5196
|
+
ownsLoop = true;
|
|
4627
5197
|
const loop = (now) => {
|
|
4628
5198
|
raf = requestAnimationFrame(loop);
|
|
4629
|
-
const
|
|
5199
|
+
const realDelta = (now - last) / 1e3;
|
|
5200
|
+
roomMetrics.frame(realDelta);
|
|
5201
|
+
const dt = Math.min(0.05, realDelta);
|
|
4630
5202
|
last = now;
|
|
4631
5203
|
if (!scenarioRunning) update(dt);
|
|
4632
5204
|
gl.render(scene, camera);
|
|
5205
|
+
roomMetrics.rendered();
|
|
4633
5206
|
};
|
|
4634
5207
|
raf = requestAnimationFrame(loop);
|
|
4635
5208
|
}
|
|
@@ -4651,7 +5224,9 @@ export {
|
|
|
4651
5224
|
buildFromSpec,
|
|
4652
5225
|
createLog,
|
|
4653
5226
|
createTestRoom,
|
|
5227
|
+
decodeBenchState,
|
|
4654
5228
|
demoItems,
|
|
5229
|
+
encodeBenchState,
|
|
4655
5230
|
locales,
|
|
4656
5231
|
makeScenarios,
|
|
4657
5232
|
resolveStrings,
|