@adatechnology/game-test-room 0.2.0 → 0.4.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 +29 -0
- package/dist/index.d.ts +50 -2
- package/dist/index.global.js +7124 -635
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1189 -193
- 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",
|
|
@@ -234,6 +249,15 @@ var en = {
|
|
|
234
249
|
"act.heat.air": "{weapon}: nothing in range",
|
|
235
250
|
"act.throw.grenade": "{weapon}: thrown, fuse {fuse} s",
|
|
236
251
|
"act.throw.molotov": "{weapon}: thrown, breaks on impact",
|
|
252
|
+
"act.launch": "{weapon}: launched, arms after {arm} m",
|
|
253
|
+
"act.place": "{weapon}: charge set down, waits for {triggers}",
|
|
254
|
+
"act.tripwire": "{weapon}: {who} crossed the trip wire",
|
|
255
|
+
"act.dud": "{weapon}: hit before arming ({distance} m), did not explode",
|
|
256
|
+
"act.backblast": "{weapon}: blast behind the tube, {radius} m, {n} objects in the cone, {player} on the shooter",
|
|
257
|
+
"act.blast_player": "{weapon}: you are in the {zone} zone, {distance} m away (stun {stun} s)",
|
|
258
|
+
"hud.no_charges": "No charge set down waits for the remote detonator",
|
|
259
|
+
"blast.zone.lethal": "lethal",
|
|
260
|
+
"blast.zone.injury": "injury",
|
|
237
261
|
"act.explode": "{weapon}: exploded, {energy} J within {radius} m, {n} objects in range",
|
|
238
262
|
"act.molotov_fire": "{weapon}: fire on the ground, radius {radius} m for {seconds} s",
|
|
239
263
|
"act.tool.none": "{weapon}: nothing in reach",
|
|
@@ -259,6 +283,15 @@ var en = {
|
|
|
259
283
|
"hud.reloading": "Reloading...",
|
|
260
284
|
"hud.empty": "Empty",
|
|
261
285
|
"hud.no_fuel": "Out of fuel",
|
|
286
|
+
"hud.fault": "Jammed ({fault}): press X to clear",
|
|
287
|
+
"hud.safe": "Safety on: press B",
|
|
288
|
+
"hud.clearing": "Clearing the jam...",
|
|
289
|
+
"hud.safety_on": "Safety on",
|
|
290
|
+
"hud.safety_off": "Safety off",
|
|
291
|
+
"fault.misfire": "misfire",
|
|
292
|
+
"fault.stovepipe": "stovepipe",
|
|
293
|
+
"fault.double_feed": "double feed",
|
|
294
|
+
"fault.failure_to_extract": "failure to extract",
|
|
262
295
|
"hud.nothing_to_grab": "Nothing to grab here",
|
|
263
296
|
"hud.cannot_grab": "{name} cannot be picked up",
|
|
264
297
|
"hud.too_heavy": "{name} is too heavy ({kg} kg)",
|
|
@@ -269,7 +302,7 @@ var en = {
|
|
|
269
302
|
"hud.throw": "throw",
|
|
270
303
|
"hud.tilt": "tilt (hold)",
|
|
271
304
|
"hud.equip": "equip {weapon}",
|
|
272
|
-
"ui.help": "Click: look \xB7 WASD: walk \xB7 Shift: run \xB7 Space: jump \xB7 1-0 / wheel: tool \xB7 LMB: use \xB7 E: grab/drop \xB7 G or RMB: throw \xB7 Tab: panels \xB7 Esc: free the mouse",
|
|
305
|
+
"ui.help": "Click: look \xB7 WASD: walk \xB7 Shift: run \xB7 Space: jump \xB7 1-0 / wheel: tool \xB7 LMB: use \xB7 E: grab/drop \xB7 G or RMB: throw \xB7 R: reload \xB7 Q: fire mode \xB7 X: clear a jam \xB7 B: safety \xB7 Y: detonate charges set down \xB7 Tab: panels \xB7 Esc: free the mouse",
|
|
273
306
|
"ui.log": "Behaviour log",
|
|
274
307
|
"ui.filter.action": "actions",
|
|
275
308
|
"ui.filter.event": "events",
|
|
@@ -511,6 +544,21 @@ var ptBR = {
|
|
|
511
544
|
"ui.animations": "Anima\xE7\xF5es",
|
|
512
545
|
"ui.animations_aim": "Mire num item ou segure-o para ver as anima\xE7\xF5es.",
|
|
513
546
|
"ui.animations_none": "Este item n\xE3o traz anima\xE7\xF5es.",
|
|
547
|
+
"mod.bench_ok": "{name}: bancada ok ({controls} controles, {layers} camadas, {metrics} contadores)",
|
|
548
|
+
"mod.bench_fail": "{name}: bancada quebrada: {problems}",
|
|
549
|
+
"mod.metric_failed": '{name}: o contador "{metric}" lan\xE7ou erro e mostra n/a: {error}',
|
|
550
|
+
"mod.listener_failed": "{name}: um ouvinte da bancada lan\xE7ou erro: {error}",
|
|
551
|
+
"ui.bench": "Bancada",
|
|
552
|
+
"ui.layers": "Camadas",
|
|
553
|
+
"ui.metrics": "Contadores",
|
|
554
|
+
"metric.fps": "FPS",
|
|
555
|
+
"metric.frame_ms": "ms/quadro",
|
|
556
|
+
"metric.draw_calls": "Chamadas de desenho",
|
|
557
|
+
"metric.triangles": "Tri\xE2ngulos",
|
|
558
|
+
"metric.objects": "Objetos",
|
|
559
|
+
"metric.fragments": "Fragmentos",
|
|
560
|
+
"metric.particles": "Part\xEDculas",
|
|
561
|
+
"metric.decals": "Decalques",
|
|
514
562
|
"act.spawn": "Criou {name} em x {x} \xB7 z {z}",
|
|
515
563
|
"act.clear": "Limpou a sala",
|
|
516
564
|
"act.reset": "Consertou todos os objetos",
|
|
@@ -526,6 +574,15 @@ var ptBR = {
|
|
|
526
574
|
"act.heat.air": "{weapon}: nada ao alcance",
|
|
527
575
|
"act.throw.grenade": "{weapon}: arremessada, pavio de {fuse} s",
|
|
528
576
|
"act.throw.molotov": "{weapon}: arremessado, quebra no impacto",
|
|
577
|
+
"act.launch": "{weapon}: disparado, arma depois de {arm} m",
|
|
578
|
+
"act.place": "{weapon}: carga posta, espera {triggers}",
|
|
579
|
+
"act.tripwire": "{weapon}: {who} cruzou o fio",
|
|
580
|
+
"act.dud": "{weapon}: bateu antes de armar ({distance} m), n\xE3o explodiu",
|
|
581
|
+
"act.backblast": "{weapon}: sopro atr\xE1s do tubo, {radius} m, {n} objetos no cone, {player} no atirador",
|
|
582
|
+
"act.blast_player": "{weapon}: voc\xEA est\xE1 na zona {zone}, a {distance} m (atordoado {stun} s)",
|
|
583
|
+
"hud.no_charges": "Nenhuma carga posta espera o detonador",
|
|
584
|
+
"blast.zone.lethal": "letal",
|
|
585
|
+
"blast.zone.injury": "de ferimento",
|
|
529
586
|
"act.explode": "{weapon}: explodiu, {energy} J em {radius} m, {n} objetos ao alcance",
|
|
530
587
|
"act.molotov_fire": "{weapon}: fogo no ch\xE3o, raio de {radius} m por {seconds} s",
|
|
531
588
|
"act.tool.none": "{weapon}: nada ao alcance",
|
|
@@ -551,6 +608,15 @@ var ptBR = {
|
|
|
551
608
|
"hud.reloading": "Recarregando...",
|
|
552
609
|
"hud.empty": "Sem muni\xE7\xE3o",
|
|
553
610
|
"hud.no_fuel": "Sem combust\xEDvel",
|
|
611
|
+
"hud.fault": "Travou ({fault}): aperte X para destravar",
|
|
612
|
+
"hud.safe": "Trava ligada: aperte B",
|
|
613
|
+
"hud.clearing": "Destravando...",
|
|
614
|
+
"hud.safety_on": "Trava ligada",
|
|
615
|
+
"hud.safety_off": "Trava desligada",
|
|
616
|
+
"fault.misfire": "nega",
|
|
617
|
+
"fault.stovepipe": "chamin\xE9",
|
|
618
|
+
"fault.double_feed": "dupla alimenta\xE7\xE3o",
|
|
619
|
+
"fault.failure_to_extract": "falha de extra\xE7\xE3o",
|
|
554
620
|
"hud.nothing_to_grab": "Nada para pegar aqui",
|
|
555
621
|
"hud.cannot_grab": "{name} n\xE3o d\xE1 para pegar",
|
|
556
622
|
"hud.too_heavy": "{name} \xE9 pesado demais ({kg} kg)",
|
|
@@ -561,7 +627,7 @@ var ptBR = {
|
|
|
561
627
|
"hud.throw": "arremessar",
|
|
562
628
|
"hud.tilt": "inclinar (segurar)",
|
|
563
629
|
"hud.equip": "equipar {weapon}",
|
|
564
|
-
"ui.help": "Clique: olhar \xB7 WASD: andar \xB7 Shift: correr \xB7 Espa\xE7o: pular \xB7 1-0 / roda: ferramenta \xB7 bot\xE3o esquerdo: usar \xB7 E: pegar/soltar \xB7 G ou bot\xE3o direito: arremessar \xB7 Tab: pain\xE9is \xB7 Esc: soltar o mouse",
|
|
630
|
+
"ui.help": "Clique: olhar \xB7 WASD: andar \xB7 Shift: correr \xB7 Espa\xE7o: pular \xB7 1-0 / roda: ferramenta \xB7 bot\xE3o esquerdo: usar \xB7 E: pegar/soltar \xB7 G ou bot\xE3o direito: arremessar \xB7 R: recarregar \xB7 Q: modo de tiro \xB7 X: destravar \xB7 B: trava \xB7 Y: detonar cargas postas \xB7 Tab: pain\xE9is \xB7 Esc: soltar o mouse",
|
|
565
631
|
"ui.log": "Registro de comportamento",
|
|
566
632
|
"ui.filter.action": "a\xE7\xF5es",
|
|
567
633
|
"ui.filter.event": "eventos",
|
|
@@ -1122,6 +1188,494 @@ function createArena(THREE, size, scene) {
|
|
|
1122
1188
|
};
|
|
1123
1189
|
}
|
|
1124
1190
|
|
|
1191
|
+
// src/bench.ts
|
|
1192
|
+
var snap = (control, value) => {
|
|
1193
|
+
const clamped = Math.min(control.max, Math.max(control.min, value));
|
|
1194
|
+
if (!control.step) return clamped;
|
|
1195
|
+
const stepped = control.min + Math.round((clamped - control.min) / control.step) * control.step;
|
|
1196
|
+
return Number(Math.min(control.max, stepped).toFixed(10));
|
|
1197
|
+
};
|
|
1198
|
+
function fitValue(control, value) {
|
|
1199
|
+
if (control.kind === "number") return typeof value === "number" && Number.isFinite(value) ? snap(control, value) : void 0;
|
|
1200
|
+
if (control.kind === "boolean") return typeof value === "boolean" ? value : void 0;
|
|
1201
|
+
if (control.kind === "choice") return typeof value === "string" && control.options.some((option) => option.id === value) ? value : void 0;
|
|
1202
|
+
return void 0;
|
|
1203
|
+
}
|
|
1204
|
+
var fromText = (control, text) => {
|
|
1205
|
+
if (control.kind === "number") return text.trim() === "" ? Number.NaN : Number(text);
|
|
1206
|
+
if (control.kind === "boolean") return text === "1" ? true : text === "0" ? false : void 0;
|
|
1207
|
+
return text;
|
|
1208
|
+
};
|
|
1209
|
+
var toText = (value) => typeof value === "boolean" ? value ? "1" : "0" : String(value);
|
|
1210
|
+
function encodeBenchState(controls, layers) {
|
|
1211
|
+
const pairs = [];
|
|
1212
|
+
for (const control of controls) if (control.value !== void 0) pairs.push(`${control.id}=${encodeURIComponent(toText(control.value))}`);
|
|
1213
|
+
for (const layer of layers) pairs.push(`${layer.id}=${layer.on ? "1" : "0"}`);
|
|
1214
|
+
return pairs.join(";");
|
|
1215
|
+
}
|
|
1216
|
+
function decodeBenchState(encoded, controls, layers) {
|
|
1217
|
+
const out = /* @__PURE__ */ new Map();
|
|
1218
|
+
for (const pair of encoded.split(";")) {
|
|
1219
|
+
const at = pair.indexOf("=");
|
|
1220
|
+
if (at <= 0) continue;
|
|
1221
|
+
const id = pair.slice(0, at);
|
|
1222
|
+
let text;
|
|
1223
|
+
try {
|
|
1224
|
+
text = decodeURIComponent(pair.slice(at + 1));
|
|
1225
|
+
} catch {
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
const control = controls.find((c) => c.id === id);
|
|
1229
|
+
if (control) {
|
|
1230
|
+
const value = fitValue(control, fromText(control, text));
|
|
1231
|
+
if (value !== void 0) out.set(id, value);
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
if (layers.some((layer) => layer.id === id) && (text === "1" || text === "0")) out.set(id, text === "1");
|
|
1235
|
+
}
|
|
1236
|
+
return out;
|
|
1237
|
+
}
|
|
1238
|
+
function createBenchStore(module, locale, onListenerError) {
|
|
1239
|
+
const declared = { controls: module.controls?.(locale) ?? [], layers: module.layers?.(locale) ?? [], metrics: module.metrics?.(locale) ?? [] };
|
|
1240
|
+
const controls = declared.controls.map((control) => ({ ...control, disabled: false }));
|
|
1241
|
+
const values = /* @__PURE__ */ new Map();
|
|
1242
|
+
for (const control of controls) if (control.kind !== "action") values.set(control.id, control.default);
|
|
1243
|
+
const layers = new Map(declared.layers.map((layer) => [layer.id, layer.default !== false]));
|
|
1244
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1245
|
+
const watchers = /* @__PURE__ */ new Set();
|
|
1246
|
+
const emit = (change) => {
|
|
1247
|
+
for (const listener of listeners) {
|
|
1248
|
+
try {
|
|
1249
|
+
listener(change);
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
onListenerError?.(error);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
const changed = () => {
|
|
1256
|
+
for (const watcher of watchers) watcher();
|
|
1257
|
+
};
|
|
1258
|
+
const controlOf = (id) => controls.find((control) => control.id === id);
|
|
1259
|
+
const store = {
|
|
1260
|
+
moduleId: module.id,
|
|
1261
|
+
metrics: declared.metrics,
|
|
1262
|
+
declared,
|
|
1263
|
+
controls: () => controls.map((control) => ({ ...control, value: values.get(control.id) })),
|
|
1264
|
+
layers: () => declared.layers.map((layer) => ({ ...layer, on: layers.get(layer.id) === true })),
|
|
1265
|
+
value: (id) => values.get(id),
|
|
1266
|
+
layer: (id) => layers.get(id) === true,
|
|
1267
|
+
set(id, value) {
|
|
1268
|
+
const control = controlOf(id);
|
|
1269
|
+
if (!control || control.disabled) return false;
|
|
1270
|
+
const fitted = fitValue(control, value);
|
|
1271
|
+
if (fitted === void 0) return false;
|
|
1272
|
+
values.set(id, fitted);
|
|
1273
|
+
emit({ kind: "control", id, value: fitted });
|
|
1274
|
+
changed();
|
|
1275
|
+
return true;
|
|
1276
|
+
},
|
|
1277
|
+
setLayer(id, on) {
|
|
1278
|
+
if (!layers.has(id)) return false;
|
|
1279
|
+
layers.set(id, on);
|
|
1280
|
+
emit({ kind: "layer", id, on });
|
|
1281
|
+
changed();
|
|
1282
|
+
return true;
|
|
1283
|
+
},
|
|
1284
|
+
act(id) {
|
|
1285
|
+
const control = controlOf(id);
|
|
1286
|
+
if (!control || control.kind !== "action" || control.disabled) return false;
|
|
1287
|
+
emit({ kind: "action", id });
|
|
1288
|
+
return true;
|
|
1289
|
+
},
|
|
1290
|
+
encode: () => encodeBenchState(store.controls(), store.layers()),
|
|
1291
|
+
onChange(listener) {
|
|
1292
|
+
listeners.add(listener);
|
|
1293
|
+
return () => listeners.delete(listener);
|
|
1294
|
+
},
|
|
1295
|
+
watch(listener) {
|
|
1296
|
+
watchers.add(listener);
|
|
1297
|
+
return () => watchers.delete(listener);
|
|
1298
|
+
},
|
|
1299
|
+
patch(id, patch) {
|
|
1300
|
+
const index = controls.findIndex((control2) => control2.id === id);
|
|
1301
|
+
if (index < 0) return;
|
|
1302
|
+
let control = controls[index];
|
|
1303
|
+
if (patch.hidden !== void 0) control = { ...control, hidden: patch.hidden };
|
|
1304
|
+
if (patch.disabled !== void 0) control = { ...control, disabled: patch.disabled };
|
|
1305
|
+
if (patch.options && control.kind === "choice" && patch.options.length > 0) {
|
|
1306
|
+
control = { ...control, options: [...patch.options] };
|
|
1307
|
+
const offered = control.options;
|
|
1308
|
+
if (!offered.some((option) => option.id === values.get(id))) values.set(id, offered[0].id);
|
|
1309
|
+
}
|
|
1310
|
+
controls[index] = control;
|
|
1311
|
+
if (patch.value !== void 0) {
|
|
1312
|
+
const fitted = fitValue(control, patch.value);
|
|
1313
|
+
if (fitted !== void 0) values.set(id, fitted);
|
|
1314
|
+
}
|
|
1315
|
+
changed();
|
|
1316
|
+
},
|
|
1317
|
+
sendInitial() {
|
|
1318
|
+
for (const control of controls) {
|
|
1319
|
+
const value = values.get(control.id);
|
|
1320
|
+
if (value !== void 0) emit({ kind: "control", id: control.id, value });
|
|
1321
|
+
}
|
|
1322
|
+
for (const layer of declared.layers) emit({ kind: "layer", id: layer.id, on: layers.get(layer.id) === true });
|
|
1323
|
+
changed();
|
|
1324
|
+
},
|
|
1325
|
+
restore(encoded) {
|
|
1326
|
+
for (const [id, value] of decodeBenchState(encoded, controls, declared.layers)) {
|
|
1327
|
+
if (typeof value === "boolean" && layers.has(id)) layers.set(id, value);
|
|
1328
|
+
else values.set(id, value);
|
|
1329
|
+
}
|
|
1330
|
+
},
|
|
1331
|
+
dispose() {
|
|
1332
|
+
listeners.clear();
|
|
1333
|
+
watchers.clear();
|
|
1334
|
+
}
|
|
1335
|
+
};
|
|
1336
|
+
return store;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// src/explosives.ts
|
|
1340
|
+
import { backblastEffect, blastEffect, createExplosiveBody, tripwireSegment } from "@adatechnology/game-weapons";
|
|
1341
|
+
|
|
1342
|
+
// src/segment-box.ts
|
|
1343
|
+
var segmentTouchesBox = (from, to, min, max) => {
|
|
1344
|
+
let near = 0;
|
|
1345
|
+
let far = 1;
|
|
1346
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
1347
|
+
const start = from[axis];
|
|
1348
|
+
const delta = to[axis] - start;
|
|
1349
|
+
const low = min[axis];
|
|
1350
|
+
const high = max[axis];
|
|
1351
|
+
if (Math.abs(delta) < 1e-12) {
|
|
1352
|
+
if (start < low || start > high) return false;
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
const entering = (low - start) / delta;
|
|
1356
|
+
const leaving = (high - start) / delta;
|
|
1357
|
+
near = Math.max(near, Math.min(entering, leaving));
|
|
1358
|
+
far = Math.min(far, Math.max(entering, leaving));
|
|
1359
|
+
if (near > far) return false;
|
|
1360
|
+
}
|
|
1361
|
+
return true;
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
// src/explosives.ts
|
|
1365
|
+
var CONE_BLAST_RADIUS = 0.5;
|
|
1366
|
+
var toArray = (vector) => [vector.x, vector.y, vector.z];
|
|
1367
|
+
function createExplosives(context) {
|
|
1368
|
+
const { THREE, scene, camera, world, renderer, arsenal, translate, format, action } = context;
|
|
1369
|
+
const projectiles = [];
|
|
1370
|
+
const meshes = [];
|
|
1371
|
+
const up = new THREE.Vector3(0, 1, 0);
|
|
1372
|
+
const clear = () => {
|
|
1373
|
+
projectiles.length = 0;
|
|
1374
|
+
for (const mesh of meshes) mesh.removeFromParent();
|
|
1375
|
+
meshes.length = 0;
|
|
1376
|
+
};
|
|
1377
|
+
const addMesh = (geometry, color, metalness, at) => {
|
|
1378
|
+
const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color, roughness: 0.6, metalness }));
|
|
1379
|
+
mesh.castShadow = true;
|
|
1380
|
+
mesh.position.set(at[0], at[1], at[2]);
|
|
1381
|
+
scene.add(mesh);
|
|
1382
|
+
meshes.push(mesh);
|
|
1383
|
+
return mesh;
|
|
1384
|
+
};
|
|
1385
|
+
const remove = (projectile) => {
|
|
1386
|
+
for (const extra of projectile.attached ?? []) {
|
|
1387
|
+
extra.removeFromParent();
|
|
1388
|
+
const extraAt = meshes.indexOf(extra);
|
|
1389
|
+
if (extraAt >= 0) meshes.splice(extraAt, 1);
|
|
1390
|
+
}
|
|
1391
|
+
projectile.mesh.removeFromParent();
|
|
1392
|
+
const meshAt = meshes.indexOf(projectile.mesh);
|
|
1393
|
+
if (meshAt >= 0) meshes.splice(meshAt, 1);
|
|
1394
|
+
const at = projectiles.indexOf(projectile);
|
|
1395
|
+
if (at >= 0) projectiles.splice(at, 1);
|
|
1396
|
+
};
|
|
1397
|
+
const throwUse = (use) => {
|
|
1398
|
+
const isMolotov = !!use.fire;
|
|
1399
|
+
const isRocket = use.trigger === "impact" && (use.armDistance ?? 0) > 0;
|
|
1400
|
+
const geometry = isRocket ? new THREE.CylinderGeometry(0.04, 0.04, 0.5, 8) : isMolotov ? new THREE.CylinderGeometry(0.035, 0.04, 0.2, 8) : new THREE.SphereGeometry(0.045, 8, 6);
|
|
1401
|
+
const mesh = addMesh(geometry, isMolotov ? 3897915 : isRocket ? 7041116 : 4936480, isMolotov ? 0 : 0.4, use.origin);
|
|
1402
|
+
projectiles.push({
|
|
1403
|
+
mesh,
|
|
1404
|
+
pos: new THREE.Vector3(use.origin[0], use.origin[1], use.origin[2]),
|
|
1405
|
+
vel: new THREE.Vector3(use.velocity[0], use.velocity[1], use.velocity[2]),
|
|
1406
|
+
weapon: context.toolId(),
|
|
1407
|
+
fuse: use.fuse,
|
|
1408
|
+
burstOnImpact: isMolotov && use.fuse <= 0,
|
|
1409
|
+
energy: use.energy,
|
|
1410
|
+
radius: use.radius,
|
|
1411
|
+
fire: use.fire,
|
|
1412
|
+
age: 0,
|
|
1413
|
+
...use.explosive !== void 0 ? { body: createExplosiveBody(use, arsenal.explosives, arsenal.rules.explosives), aligned: isRocket } : {}
|
|
1414
|
+
});
|
|
1415
|
+
const key = isRocket ? "act.launch" : isMolotov ? "act.throw.molotov" : "act.throw.grenade";
|
|
1416
|
+
action(isRocket ? "launch_projectile" : "throw_projectile", translate(key, { weapon: context.toolName(), fuse: format(use.fuse, 1), arm: format(use.armDistance ?? 0, 0) }), {
|
|
1417
|
+
data: { weapon: context.toolId(), fuse: use.fuse, explosive: use.explosive }
|
|
1418
|
+
});
|
|
1419
|
+
};
|
|
1420
|
+
const placementPoint = (use) => {
|
|
1421
|
+
const hit = context.withoutHeld(() => world.raycast(use.origin, use.direction, 3));
|
|
1422
|
+
if (hit) return [hit.point[0] + hit.normal[0] * 0.03, hit.point[1] + hit.normal[1] * 0.03, hit.point[2] + hit.normal[2] * 0.03];
|
|
1423
|
+
const x = use.origin[0] + use.direction[0] * 2;
|
|
1424
|
+
const z = use.origin[2] + use.direction[2] * 2;
|
|
1425
|
+
return [x, context.groundAt(x, z) + 0.03, z];
|
|
1426
|
+
};
|
|
1427
|
+
const markerAt = (at) => {
|
|
1428
|
+
const marker = new THREE.Group();
|
|
1429
|
+
marker.name = "charge_marker";
|
|
1430
|
+
const stake = new THREE.Mesh(new THREE.CylinderGeometry(0.01, 0.01, 0.4, 6), new THREE.MeshStandardMaterial({ color: 9136957, roughness: 0.8 }));
|
|
1431
|
+
stake.position.y = 0.2;
|
|
1432
|
+
const flag = new THREE.Mesh(new THREE.ConeGeometry(0.06, 0.12, 4), new THREE.MeshStandardMaterial({ color: 16738816, emissive: 16724736, emissiveIntensity: 0.4 }));
|
|
1433
|
+
flag.position.y = 0.46;
|
|
1434
|
+
marker.add(stake, flag);
|
|
1435
|
+
marker.position.set(at[0] + 0.12, at[1] - 0.03, at[2]);
|
|
1436
|
+
scene.add(marker);
|
|
1437
|
+
meshes.push(marker);
|
|
1438
|
+
return marker;
|
|
1439
|
+
};
|
|
1440
|
+
const wireLine = (from, to) => {
|
|
1441
|
+
const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(...from), new THREE.Vector3(...to)]);
|
|
1442
|
+
const line = new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 15262920 }));
|
|
1443
|
+
line.name = "charge_wire";
|
|
1444
|
+
scene.add(line);
|
|
1445
|
+
meshes.push(line);
|
|
1446
|
+
return line;
|
|
1447
|
+
};
|
|
1448
|
+
const touching = (wire) => context.bodies().filter((body) => segmentTouchesBox(wire.from, wire.to, toArray(body.box.min), toArray(body.box.max))).map((body) => body.id);
|
|
1449
|
+
const placeUse = (use) => {
|
|
1450
|
+
const at = placementPoint(use);
|
|
1451
|
+
const placed = { ...use, origin: at };
|
|
1452
|
+
const body = createExplosiveBody(placed, arsenal.explosives, arsenal.rules.explosives);
|
|
1453
|
+
if (!body) return;
|
|
1454
|
+
const mesh = addMesh(new THREE.BoxGeometry(0.18, 0.06, 0.1), use.explosive === "claymore" ? 5597999 : 12757112, 0.1, at);
|
|
1455
|
+
mesh.rotation.y = Math.atan2(use.direction[0], use.direction[2]);
|
|
1456
|
+
const attached = [markerAt(at)];
|
|
1457
|
+
const segment = tripwireSegment(placed);
|
|
1458
|
+
const projectile = { mesh, pos: new THREE.Vector3(at[0], at[1], at[2]), vel: new THREE.Vector3(), weapon: context.toolId(), fuse: 0, burstOnImpact: false, energy: use.energy, radius: use.radius, age: 0, body, placed: true, attached };
|
|
1459
|
+
if (segment) {
|
|
1460
|
+
const wire = { from: segment.from, to: segment.to, letThrough: /* @__PURE__ */ new Set() };
|
|
1461
|
+
for (const id of touching(wire)) wire.letThrough.add(id);
|
|
1462
|
+
attached.push(wireLine(segment.from, segment.to));
|
|
1463
|
+
projectile.wire = wire;
|
|
1464
|
+
}
|
|
1465
|
+
projectiles.push(projectile);
|
|
1466
|
+
action("place_charge", translate("act.place", { weapon: context.toolName(), triggers: use.triggers.join("/") || "-" }), { data: { weapon: context.toolId(), explosive: use.explosive, triggers: [...use.triggers], point: at, ...segment ? { wire: segment } : {} } });
|
|
1467
|
+
};
|
|
1468
|
+
const backblastUse = (use) => {
|
|
1469
|
+
renderer.emit("smoke", use.origin, use.direction, 14, use.radius);
|
|
1470
|
+
let reached = 0;
|
|
1471
|
+
for (const object of context.objects()) if (backblastEffect(use, toArray(object.position)) > 0) reached++;
|
|
1472
|
+
const onPlayer = backblastEffect(use, toArray(camera.position));
|
|
1473
|
+
action("backblast", translate("act.backblast", { weapon: context.toolName(), radius: format(use.radius, 0), n: reached, player: format(onPlayer, 2) }), {
|
|
1474
|
+
data: { weapon: context.toolId(), radius: use.radius, halfAngleDeg: use.halfAngleDeg, objects: reached, onPlayer }
|
|
1475
|
+
});
|
|
1476
|
+
};
|
|
1477
|
+
const surfaceProbe = (from, to) => {
|
|
1478
|
+
const dx = to[0] - from[0];
|
|
1479
|
+
const dy = to[1] - from[1];
|
|
1480
|
+
const dz = to[2] - from[2];
|
|
1481
|
+
const length = Math.hypot(dx, dy, dz);
|
|
1482
|
+
if (length < 1e-6) return null;
|
|
1483
|
+
const direction = [dx / length, dy / length, dz / length];
|
|
1484
|
+
const ray = context.withoutHeld(() => world.raycast(from, direction, length + 0.03));
|
|
1485
|
+
let best = ray && ray.distance <= length + 0.03 ? { point: ray.point, normal: ray.normal, ground: ray.normal[1] > 0.7, at: ray.distance } : null;
|
|
1486
|
+
const floor = context.groundAt(to[0], to[2]) + 0.045;
|
|
1487
|
+
if (to[1] < floor) {
|
|
1488
|
+
const point = [to[0], floor, to[2]];
|
|
1489
|
+
const at = Math.hypot(point[0] - from[0], point[1] - from[1], point[2] - from[2]);
|
|
1490
|
+
if (!best || at < best.at) best = { point, normal: [0, 1, 0], ground: true, at };
|
|
1491
|
+
}
|
|
1492
|
+
return best ? { point: best.point, normal: best.normal, ground: best.ground } : null;
|
|
1493
|
+
};
|
|
1494
|
+
const mostExposedPoint = (box2, detonation) => {
|
|
1495
|
+
const { min, max } = box2;
|
|
1496
|
+
const candidates = [toArray(box2.getCenter(new THREE.Vector3())), toArray(box2.clampPoint(new THREE.Vector3(...detonation.point), new THREE.Vector3()))];
|
|
1497
|
+
for (const x of [min.x, max.x]) for (const y of [min.y, max.y]) for (const z of [min.z, max.z]) candidates.push([x, y, z]);
|
|
1498
|
+
let best;
|
|
1499
|
+
for (const point of candidates) {
|
|
1500
|
+
const effect = blastEffect(detonation, point);
|
|
1501
|
+
if (effect.zone !== "none" && (!best || effect.falloff > best.falloff)) best = { point, falloff: effect.falloff };
|
|
1502
|
+
}
|
|
1503
|
+
return best?.point;
|
|
1504
|
+
};
|
|
1505
|
+
const coneBlast = (detonation) => {
|
|
1506
|
+
let reached = 0;
|
|
1507
|
+
for (const object of context.objects()) {
|
|
1508
|
+
const box2 = new THREE.Box3().setFromObject(object);
|
|
1509
|
+
if (box2.isEmpty()) box2.expandByPoint(object.position);
|
|
1510
|
+
const at = mostExposedPoint(box2, detonation);
|
|
1511
|
+
if (!at) continue;
|
|
1512
|
+
reached += world.explode(at, detonation.energy * blastEffect(detonation, at).falloff, CONE_BLAST_RADIUS, "player").length;
|
|
1513
|
+
}
|
|
1514
|
+
return reached;
|
|
1515
|
+
};
|
|
1516
|
+
const detonate = (projectile, detonation) => {
|
|
1517
|
+
const weaponName = context.toolName(projectile.weapon);
|
|
1518
|
+
const reach = Math.max(detonation.injuryRadius, detonation.lethalRadius);
|
|
1519
|
+
const reached = detonation.energy <= 0 ? 0 : detonation.cone ? coneBlast(detonation) : world.explode(detonation.point, detonation.energy, reach, "player").length;
|
|
1520
|
+
if (detonation.energy > 0) renderer.explode(detonation.point, detonation.energy, reach);
|
|
1521
|
+
if (detonation.stunSeconds !== void 0) renderer.emit("flash", detonation.point, [0, 1, 0], 1, 1);
|
|
1522
|
+
const onPlayer = blastEffect(detonation, toArray(camera.position));
|
|
1523
|
+
action("explode", translate("act.explode", { weapon: weaponName, energy: format(detonation.energy, 0), radius: format(reach, 1), n: reached }), {
|
|
1524
|
+
data: { weapon: projectile.weapon, explosive: detonation.explosive, cause: detonation.cause, energyJ: detonation.energy, radius: reach, point: detonation.point, player: onPlayer.zone, stunSeconds: onPlayer.stunSeconds }
|
|
1525
|
+
});
|
|
1526
|
+
if (onPlayer.zone !== "none") {
|
|
1527
|
+
action("blast_player", translate("act.blast_player", { weapon: weaponName, zone: translate(`blast.zone.${onPlayer.zone}`), distance: format(onPlayer.distance, 1), stun: format(onPlayer.stunSeconds, 1) }), { data: { ...onPlayer } });
|
|
1528
|
+
}
|
|
1529
|
+
context.emitHost({ type: "explosion", source: projectile.weapon, point: detonation.point, energy: detonation.energy, radius: reach, cause: detonation.cause });
|
|
1530
|
+
if (detonation.fire) context.startFire(projectile.weapon, detonation.point, detonation.fire, detonation.cause === "impact");
|
|
1531
|
+
};
|
|
1532
|
+
const detonateBare = (projectile, isImpact) => {
|
|
1533
|
+
detonate(projectile, {
|
|
1534
|
+
explosive: projectile.weapon,
|
|
1535
|
+
cause: isImpact ? "impact" : "timer",
|
|
1536
|
+
point: toArray(projectile.pos),
|
|
1537
|
+
direction: [0, 1, 0],
|
|
1538
|
+
at: projectile.age,
|
|
1539
|
+
energy: projectile.energy,
|
|
1540
|
+
lethalRadius: 0,
|
|
1541
|
+
injuryRadius: projectile.radius,
|
|
1542
|
+
noise: 0,
|
|
1543
|
+
destructionLevel: 0,
|
|
1544
|
+
...projectile.fire ? { fire: projectile.fire } : {}
|
|
1545
|
+
});
|
|
1546
|
+
};
|
|
1547
|
+
const detonateCharges = () => {
|
|
1548
|
+
let count = 0;
|
|
1549
|
+
for (const projectile of [...projectiles]) {
|
|
1550
|
+
if (!projectile.placed || !projectile.body) continue;
|
|
1551
|
+
const event = projectile.body.trigger("remote");
|
|
1552
|
+
if (event?.type !== "detonate") continue;
|
|
1553
|
+
detonate(projectile, event.detonation);
|
|
1554
|
+
remove(projectile);
|
|
1555
|
+
count++;
|
|
1556
|
+
}
|
|
1557
|
+
if (count === 0) context.say(translate("hud.no_charges"));
|
|
1558
|
+
return count;
|
|
1559
|
+
};
|
|
1560
|
+
const stepBody = (projectile, body, dt) => {
|
|
1561
|
+
const events = body.step(dt, surfaceProbe);
|
|
1562
|
+
projectile.pos.set(body.position[0], body.position[1], body.position[2]);
|
|
1563
|
+
projectile.mesh.position.copy(projectile.pos);
|
|
1564
|
+
if (projectile.aligned && body.velocity[0] ** 2 + body.velocity[1] ** 2 + body.velocity[2] ** 2 > 1e-6) {
|
|
1565
|
+
projectile.mesh.quaternion.setFromUnitVectors(up, new THREE.Vector3(body.velocity[0], body.velocity[1], body.velocity[2]).normalize());
|
|
1566
|
+
} else if (!projectile.placed) projectile.mesh.rotation.x += dt * 8;
|
|
1567
|
+
for (const event of events) {
|
|
1568
|
+
if (event.type === "dud") action("dud", translate("act.dud", { weapon: context.toolName(projectile.weapon), distance: format(body.travelled, 1) }), { data: { weapon: projectile.weapon, point: event.point } });
|
|
1569
|
+
else if (event.type === "detonate") detonate(projectile, event.detonation);
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
const flyBare = (projectile, dt) => {
|
|
1573
|
+
const { gravity } = arsenal.rules.explosives;
|
|
1574
|
+
let isBurst = false;
|
|
1575
|
+
projectile.vel.y -= gravity * dt;
|
|
1576
|
+
const speed = projectile.vel.length();
|
|
1577
|
+
const step = speed * dt;
|
|
1578
|
+
if (step > 1e-6) {
|
|
1579
|
+
const direction = [projectile.vel.x / speed, projectile.vel.y / speed, projectile.vel.z / speed];
|
|
1580
|
+
const hit = context.withoutHeld(() => world.raycast(toArray(projectile.pos), direction, step + 0.05));
|
|
1581
|
+
if (hit && hit.distance <= step + 0.04) {
|
|
1582
|
+
projectile.pos.set(hit.point[0] - direction[0] * 0.03, hit.point[1] - direction[1] * 0.03, hit.point[2] - direction[2] * 0.03);
|
|
1583
|
+
if (projectile.burstOnImpact) isBurst = true;
|
|
1584
|
+
else {
|
|
1585
|
+
const normal = hit.normal;
|
|
1586
|
+
const normalSpeed = projectile.vel.x * normal[0] + projectile.vel.y * normal[1] + projectile.vel.z * normal[2];
|
|
1587
|
+
projectile.vel.x = (projectile.vel.x - 2 * normalSpeed * normal[0]) * 0.35;
|
|
1588
|
+
projectile.vel.y = (projectile.vel.y - 2 * normalSpeed * normal[1]) * 0.35;
|
|
1589
|
+
projectile.vel.z = (projectile.vel.z - 2 * normalSpeed * normal[2]) * 0.35;
|
|
1590
|
+
}
|
|
1591
|
+
} else projectile.pos.addScaledVector(projectile.vel, dt);
|
|
1592
|
+
}
|
|
1593
|
+
const ground = context.groundAt(projectile.pos.x, projectile.pos.z) + 0.045;
|
|
1594
|
+
if (projectile.pos.y < ground) {
|
|
1595
|
+
projectile.pos.y = ground;
|
|
1596
|
+
if (projectile.burstOnImpact) isBurst = true;
|
|
1597
|
+
else {
|
|
1598
|
+
projectile.vel.y = Math.abs(projectile.vel.y) * 0.35;
|
|
1599
|
+
projectile.vel.x *= 0.7;
|
|
1600
|
+
projectile.vel.z *= 0.7;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
return isBurst;
|
|
1604
|
+
};
|
|
1605
|
+
const checkWire = (projectile, body) => {
|
|
1606
|
+
const wire = projectile.wire;
|
|
1607
|
+
if (!wire) return;
|
|
1608
|
+
const crossing = touching(wire);
|
|
1609
|
+
for (const id of [...wire.letThrough]) if (!crossing.includes(id)) wire.letThrough.delete(id);
|
|
1610
|
+
const crosser = crossing.find((id) => !wire.letThrough.has(id));
|
|
1611
|
+
if (crosser === void 0) return;
|
|
1612
|
+
const event = body.trigger("wire");
|
|
1613
|
+
if (event?.type !== "detonate") return;
|
|
1614
|
+
action("tripwire", translate("act.tripwire", { weapon: context.toolName(projectile.weapon), who: crosser }), { data: { weapon: projectile.weapon, body: crosser } });
|
|
1615
|
+
detonate(projectile, event.detonation);
|
|
1616
|
+
};
|
|
1617
|
+
const update = (dt) => {
|
|
1618
|
+
for (let index = projectiles.length - 1; index >= 0; index--) {
|
|
1619
|
+
const projectile = projectiles[index];
|
|
1620
|
+
if (!projectile) continue;
|
|
1621
|
+
projectile.age += dt;
|
|
1622
|
+
if (projectile.body) {
|
|
1623
|
+
if (projectile.wire && !projectile.body.done) checkWire(projectile, projectile.body);
|
|
1624
|
+
stepBody(projectile, projectile.body, dt);
|
|
1625
|
+
if (projectile.body.done || !projectile.placed && projectile.age > 60) remove(projectile);
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
const isBurst = flyBare(projectile, dt);
|
|
1629
|
+
projectile.mesh.position.copy(projectile.pos);
|
|
1630
|
+
projectile.mesh.rotation.x += dt * 8;
|
|
1631
|
+
projectile.fuse -= dt;
|
|
1632
|
+
if (isBurst || !projectile.burstOnImpact && projectile.fuse <= 0 || projectile.age > 30) {
|
|
1633
|
+
detonateBare(projectile, isBurst);
|
|
1634
|
+
remove(projectile);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
return { throwUse, placeUse, backblastUse, detonateCharges, update, clear };
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// src/host-events.ts
|
|
1642
|
+
var NO_RECOIL = [0, 0];
|
|
1643
|
+
var durationOf = (phase, id) => phase?.id === id ? { duration: Math.round((phase.end - phase.start) * 1e6) / 1e6 } : {};
|
|
1644
|
+
var toHostEvent = (weapon, event, options = {}) => {
|
|
1645
|
+
const { recoil = NO_RECOIL, phase } = options;
|
|
1646
|
+
switch (event.type) {
|
|
1647
|
+
case "draw":
|
|
1648
|
+
return event.phase === "start" ? { type: "tool", weapon, ...durationOf(phase, "draw") } : null;
|
|
1649
|
+
// a launcher spends a round and throws one projectile: the weapon counts none because it is a throw, a host counts the one that leaves
|
|
1650
|
+
case "shot":
|
|
1651
|
+
return { type: "shot", weapon, shots: Math.max(1, event.shots), recoil, rounds: event.rounds, mode: event.mode };
|
|
1652
|
+
case "misfire":
|
|
1653
|
+
return { type: "misfire", weapon };
|
|
1654
|
+
case "reload":
|
|
1655
|
+
return { type: "reload", weapon, phase: event.phase, ...event.phase === "start" ? { kind: event.kind, ...durationOf(phase, "reload") } : {} };
|
|
1656
|
+
case "reload_stage":
|
|
1657
|
+
return { type: "stage", weapon, stage: event.stage };
|
|
1658
|
+
case "feed":
|
|
1659
|
+
return { type: "feed", weapon };
|
|
1660
|
+
case "cycle":
|
|
1661
|
+
return { type: "cycle", weapon, phase: event.phase, ...event.phase === "start" ? durationOf(phase, "cycle") : {} };
|
|
1662
|
+
case "empty":
|
|
1663
|
+
return { type: "empty", weapon };
|
|
1664
|
+
case "mode":
|
|
1665
|
+
return { type: "mode", weapon, mode: event.mode };
|
|
1666
|
+
case "fault":
|
|
1667
|
+
return { type: "fault", weapon, fault: event.fault };
|
|
1668
|
+
case "clear":
|
|
1669
|
+
return { type: "clear", weapon, fault: event.fault, phase: event.phase, ...event.phase === "start" ? durationOf(phase, "clear") : {} };
|
|
1670
|
+
case "safety":
|
|
1671
|
+
return { type: "safety", weapon, on: event.on };
|
|
1672
|
+
case "cook":
|
|
1673
|
+
return { type: "cook", weapon, phase: event.phase, fuse: event.fuse };
|
|
1674
|
+
default:
|
|
1675
|
+
return null;
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
|
|
1125
1679
|
// src/log.ts
|
|
1126
1680
|
var MAX_ENTRIES = 5e3;
|
|
1127
1681
|
var pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
@@ -1250,6 +1804,56 @@ function createLog(options = {}) {
|
|
|
1250
1804
|
return log;
|
|
1251
1805
|
}
|
|
1252
1806
|
|
|
1807
|
+
// src/metrics.ts
|
|
1808
|
+
var ROOM_METRICS = ["fps", "frame_ms", "draw_calls", "triangles", "objects", "fragments", "particles", "decals"];
|
|
1809
|
+
var WINDOW_S = 0.5;
|
|
1810
|
+
function createRoomMetrics(sources) {
|
|
1811
|
+
const wanted = new Set(sources.ids);
|
|
1812
|
+
const gl = sources.gl;
|
|
1813
|
+
let windowS = 0;
|
|
1814
|
+
let frames = 0;
|
|
1815
|
+
let fps;
|
|
1816
|
+
let frameMs;
|
|
1817
|
+
let calls;
|
|
1818
|
+
let triangles;
|
|
1819
|
+
let ownsRender = false;
|
|
1820
|
+
return {
|
|
1821
|
+
frame(dt) {
|
|
1822
|
+
frames++;
|
|
1823
|
+
windowS += dt;
|
|
1824
|
+
if (windowS < WINDOW_S) return;
|
|
1825
|
+
fps = frames / windowS;
|
|
1826
|
+
frameMs = windowS / frames * 1e3;
|
|
1827
|
+
frames = 0;
|
|
1828
|
+
windowS = 0;
|
|
1829
|
+
},
|
|
1830
|
+
rendered() {
|
|
1831
|
+
ownsRender = true;
|
|
1832
|
+
calls = gl.info?.render?.calls;
|
|
1833
|
+
triangles = gl.info?.render?.triangles;
|
|
1834
|
+
},
|
|
1835
|
+
sample() {
|
|
1836
|
+
const out = {};
|
|
1837
|
+
const put = (id, value) => {
|
|
1838
|
+
if (wanted.has(id) && value !== void 0) out[id] = value;
|
|
1839
|
+
};
|
|
1840
|
+
put("fps", fps);
|
|
1841
|
+
put("frame_ms", frameMs);
|
|
1842
|
+
const render = ownsRender ? { calls, triangles } : gl.info?.render;
|
|
1843
|
+
put("draw_calls", render?.calls);
|
|
1844
|
+
put("triangles", render?.triangles);
|
|
1845
|
+
if (!["objects", "fragments", "particles", "decals"].some((id) => wanted.has(id))) return out;
|
|
1846
|
+
const destruction = sources.destruction.stats();
|
|
1847
|
+
const fluids = sources.fluids.stats();
|
|
1848
|
+
put("objects", destruction.pieces);
|
|
1849
|
+
put("fragments", destruction.fragments);
|
|
1850
|
+
put("particles", destruction.particles + fluids.particles);
|
|
1851
|
+
put("decals", destruction.decals + fluids.stains);
|
|
1852
|
+
return out;
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1253
1857
|
// src/narrate.ts
|
|
1254
1858
|
var SKIP_DAMAGE = /* @__PURE__ */ new Set(["fire", "heat"]);
|
|
1255
1859
|
function createNarrator(c) {
|
|
@@ -1763,7 +2367,7 @@ function createNarrator(c) {
|
|
|
1763
2367
|
|
|
1764
2368
|
// src/player.ts
|
|
1765
2369
|
var EYE = 1.7;
|
|
1766
|
-
var
|
|
2370
|
+
var PLAYER_RADIUS = 0.3;
|
|
1767
2371
|
var WALK = 3.4;
|
|
1768
2372
|
var RUN = 6.2;
|
|
1769
2373
|
var JUMP = 4.6;
|
|
@@ -1788,8 +2392,8 @@ function createPlayer(THREE, o) {
|
|
|
1788
2392
|
};
|
|
1789
2393
|
apply();
|
|
1790
2394
|
const blocked = (x, z) => {
|
|
1791
|
-
if (Math.abs(x) > o.limit -
|
|
1792
|
-
for (const c of o.colliders) if (x > c[0] -
|
|
2395
|
+
if (Math.abs(x) > o.limit - PLAYER_RADIUS || Math.abs(z) > o.limit - PLAYER_RADIUS) return true;
|
|
2396
|
+
for (const c of o.colliders) if (x > c[0] - PLAYER_RADIUS && x < c[2] + PLAYER_RADIUS && z > c[1] - PLAYER_RADIUS && z < c[3] + PLAYER_RADIUS) return true;
|
|
1793
2397
|
return false;
|
|
1794
2398
|
};
|
|
1795
2399
|
const move = (dx, dz) => {
|
|
@@ -2011,6 +2615,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2011
2615
|
sc("steel_through_hole", ({ room, step, expect }) => {
|
|
2012
2616
|
const id = at(room, "steel_sheet", 0, -4);
|
|
2013
2617
|
room.setTool("pistol_9mm");
|
|
2618
|
+
room.waitUntilReady();
|
|
2014
2619
|
look(room, [0, 1.1, -1.5], [0, 1.1, -4]);
|
|
2015
2620
|
room.use();
|
|
2016
2621
|
step(0.2);
|
|
@@ -2025,6 +2630,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2025
2630
|
sc("concrete_stops_bullet", ({ room, step, expect }) => {
|
|
2026
2631
|
const id = at(room, "concrete_block", 0, -4);
|
|
2027
2632
|
room.setTool("pistol_9mm");
|
|
2633
|
+
room.waitUntilReady();
|
|
2028
2634
|
look(room, [0, 0.5, -1.5], [0, 0.2, -3.8]);
|
|
2029
2635
|
room.use();
|
|
2030
2636
|
step(0.2);
|
|
@@ -2040,6 +2646,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2040
2646
|
sc("hammer_dents_drum", ({ room, step, expect }) => {
|
|
2041
2647
|
const id = at(room, "steel_drum", 0, -4);
|
|
2042
2648
|
room.setTool("hammer");
|
|
2649
|
+
room.waitUntilReady();
|
|
2043
2650
|
look(room, [0, 0.6, -3.35], [0, 0.5, -4]);
|
|
2044
2651
|
let swings = 0;
|
|
2045
2652
|
for (; swings < 8 && (part(room, id, "drum_body")?.dents.length ?? 0) === 0; swings++) {
|
|
@@ -2056,6 +2663,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2056
2663
|
sc("glass_shatters", ({ room, step, expect }) => {
|
|
2057
2664
|
const id = at(room, "glass_pane", 0, -4);
|
|
2058
2665
|
room.setTool("pistol_9mm");
|
|
2666
|
+
room.waitUntilReady();
|
|
2059
2667
|
look(room, [0, 1.3, -1.5], [0, 1.05, -4]);
|
|
2060
2668
|
let shots = 0;
|
|
2061
2669
|
for (; shots < 10 && part(room, id, "pane")?.status !== "broken"; shots++) {
|
|
@@ -2070,6 +2678,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2070
2678
|
sc("shotgun_splinters_crate", ({ room, step, expect }) => {
|
|
2071
2679
|
const id = at(room, "wooden_crate", 0, -4);
|
|
2072
2680
|
room.setTool("shotgun_pump_12");
|
|
2681
|
+
room.waitUntilReady();
|
|
2073
2682
|
look(room, [0, 0.6, -1.8], [0, 0.25, -3.77]);
|
|
2074
2683
|
let shots = 0;
|
|
2075
2684
|
const splinters = () => room.log.entries({ type: "fracture", objectId: id }).filter((e) => e.data?.style === "splinter").length;
|
|
@@ -2103,6 +2712,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2103
2712
|
const id = at(room, "chair", 0, -4);
|
|
2104
2713
|
const before = room.renderer.pieces().length;
|
|
2105
2714
|
room.setTool("kick");
|
|
2715
|
+
room.waitUntilReady();
|
|
2106
2716
|
let kicks = 0;
|
|
2107
2717
|
for (; kicks < 8 && room.renderer.pieces().length <= before && part(room, id, "leg_1"); kicks++) {
|
|
2108
2718
|
const st = room.world.state(id);
|
|
@@ -2119,6 +2729,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2119
2729
|
sc("screwdriver_shelf_board", ({ room, step, expect }) => {
|
|
2120
2730
|
const id = at(room, "shelf", 0, -4);
|
|
2121
2731
|
room.setTool("screwdriver");
|
|
2732
|
+
room.waitUntilReady();
|
|
2122
2733
|
look(room, [0, 0.8, -3.5], [0, 0.75, -3.86]);
|
|
2123
2734
|
const t0 = room.log.entries().length ? room.log.entries().slice(-1)[0].t : 0;
|
|
2124
2735
|
room.use(5);
|
|
@@ -2132,6 +2743,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2132
2743
|
sc("wrong_tool_refused", ({ room, expect }) => {
|
|
2133
2744
|
const id = at(room, "steel_sheet", 0, -4);
|
|
2134
2745
|
room.setTool("screwdriver");
|
|
2746
|
+
room.waitUntilReady();
|
|
2135
2747
|
look(room, [0.2, 1.05, -3.6], [0.34, 1, -4]);
|
|
2136
2748
|
room.use(0.5);
|
|
2137
2749
|
const refused = count(room, "tool_refused");
|
|
@@ -2141,6 +2753,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2141
2753
|
sc("lighter_burns_crate", ({ room, step, expect }) => {
|
|
2142
2754
|
const id = at(room, "wooden_crate", 0, -4);
|
|
2143
2755
|
room.setTool("lighter");
|
|
2756
|
+
room.waitUntilReady();
|
|
2144
2757
|
look(room, [0, 0.3, -3.65], [0, 0.25, -3.76]);
|
|
2145
2758
|
room.use(5);
|
|
2146
2759
|
const lit = anyBurning(room, id);
|
|
@@ -2163,6 +2776,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2163
2776
|
const melt0 = part(room, drum2, "drum_body")?.melt ?? 0;
|
|
2164
2777
|
expect(melt0 === 0 && peak < 1450, t("sc.torch_melts_steel.wood", { peak: num(peak, 0), melt: num(melt0 * 100, 0), crate: yn(anyBurning(room, crate2)) }), { peak });
|
|
2165
2778
|
room.setTool("cutting_torch");
|
|
2779
|
+
room.waitUntilReady();
|
|
2166
2780
|
look(room, [-0.15, 0.55, -3.55], [-0.15, 0.5, -3.72]);
|
|
2167
2781
|
room.use(20);
|
|
2168
2782
|
const p = part(room, drum2, "drum_body");
|
|
@@ -2171,6 +2785,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2171
2785
|
sc("bucket_melts_drips", ({ room, step, expect }) => {
|
|
2172
2786
|
const id = at(room, "plastic_bucket", 0, -4);
|
|
2173
2787
|
room.setTool("flamethrower");
|
|
2788
|
+
room.waitUntilReady();
|
|
2174
2789
|
look(room, [0, 0.5, -2.4], [0, 0.15, -4]);
|
|
2175
2790
|
room.use(2.5);
|
|
2176
2791
|
step(10);
|
|
@@ -2245,6 +2860,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2245
2860
|
sc("jerrycan_leaks_fuel", ({ room, step, expect }) => {
|
|
2246
2861
|
const id = at(room, "jerrycan", 0, -3.5);
|
|
2247
2862
|
room.setTool("pistol_9mm");
|
|
2863
|
+
room.waitUntilReady();
|
|
2248
2864
|
look(room, [0, 0.4, -1.5], [0, 0.12, -3.43]);
|
|
2249
2865
|
room.use();
|
|
2250
2866
|
step(1.5);
|
|
@@ -2259,6 +2875,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2259
2875
|
const id = at(room, "jerrycan", 0, -3.5);
|
|
2260
2876
|
const crate2 = at(room, "wooden_crate", 0.9, -3.2);
|
|
2261
2877
|
room.setTool("pistol_9mm");
|
|
2878
|
+
room.waitUntilReady();
|
|
2262
2879
|
look(room, [0, 0.4, -1.5], [0, 0.12, -3.43]);
|
|
2263
2880
|
room.use();
|
|
2264
2881
|
step(6);
|
|
@@ -2266,6 +2883,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2266
2883
|
expect(!!stain, t("sc.fuel_stain_burns.stain", { stain: yn(!!stain) }));
|
|
2267
2884
|
if (!stain) return;
|
|
2268
2885
|
room.setTool("lighter");
|
|
2886
|
+
room.waitUntilReady();
|
|
2269
2887
|
look(room, [stain.center[0], 0.1, stain.center[2] + 0.06], [stain.center[0], 0, stain.center[2]]);
|
|
2270
2888
|
room.use(1);
|
|
2271
2889
|
step(4);
|
|
@@ -2301,6 +2919,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2301
2919
|
const before = anyBurning(room, id);
|
|
2302
2920
|
expect(before, t("sc.extinguisher_puts_out_fire.burning", { burning: yn(before) }));
|
|
2303
2921
|
room.setTool("extinguisher");
|
|
2922
|
+
room.waitUntilReady();
|
|
2304
2923
|
look(room, [0, 0.9, -2.2], [0, 0.25, -4]);
|
|
2305
2924
|
room.use(2.5);
|
|
2306
2925
|
step(1);
|
|
@@ -2320,6 +2939,209 @@ function scenariosFor(locale = "en") {
|
|
|
2320
2939
|
}
|
|
2321
2940
|
var SCENARIOS = scenariosFor("en");
|
|
2322
2941
|
|
|
2942
|
+
// src/bench-panel.ts
|
|
2943
|
+
var SEGMENTED_MAX = 4;
|
|
2944
|
+
var mk = (tag, cls, text) => {
|
|
2945
|
+
const e = document.createElement(tag);
|
|
2946
|
+
if (cls) e.className = cls;
|
|
2947
|
+
if (text !== void 0) e.textContent = text;
|
|
2948
|
+
return e;
|
|
2949
|
+
};
|
|
2950
|
+
var shapeOf = (view) => JSON.stringify([view.title, view.controls.map((c) => ({ ...c, value: void 0 })), view.layers.map((l) => [l.id, l.name])]);
|
|
2951
|
+
function createBenchPanel(o) {
|
|
2952
|
+
const { t } = o;
|
|
2953
|
+
const element = mk("div", "tr-panel tr-bench");
|
|
2954
|
+
element.style.display = "none";
|
|
2955
|
+
element.addEventListener("keydown", (e) => e.stopPropagation());
|
|
2956
|
+
const roomLine = mk("div", "sub tr-bench-room");
|
|
2957
|
+
const body = mk("div");
|
|
2958
|
+
const metricTitle = mk("div", "sub", t("ui.metrics"));
|
|
2959
|
+
metricTitle.style.display = "none";
|
|
2960
|
+
const metricList = mk("dl", "tr-metrics");
|
|
2961
|
+
element.append(mk("h3", void 0, t("ui.bench")), roomLine, body, metricTitle, metricList);
|
|
2962
|
+
let shape = "";
|
|
2963
|
+
let hasRoom = false;
|
|
2964
|
+
let hasBench = false;
|
|
2965
|
+
const refreshers = [];
|
|
2966
|
+
const metricNodes = /* @__PURE__ */ new Map();
|
|
2967
|
+
let metricKey = "";
|
|
2968
|
+
const syncDisplay = () => {
|
|
2969
|
+
element.style.display = hasRoom || hasBench ? "" : "none";
|
|
2970
|
+
};
|
|
2971
|
+
const numberControl = (control) => {
|
|
2972
|
+
const wrap = mk("label", "tr-ctl");
|
|
2973
|
+
const input = mk("input");
|
|
2974
|
+
input.type = "range";
|
|
2975
|
+
input.min = String(control.min);
|
|
2976
|
+
input.max = String(control.max);
|
|
2977
|
+
input.step = control.step ? String(control.step) : "any";
|
|
2978
|
+
input.disabled = control.disabled;
|
|
2979
|
+
const reading = mk("output");
|
|
2980
|
+
input.oninput = () => o.onControl(control.id, Number(input.value));
|
|
2981
|
+
wrap.append(mk("span", void 0, control.name), input, reading);
|
|
2982
|
+
refreshers.push((view) => {
|
|
2983
|
+
const value = view.controls.find((c) => c.id === control.id)?.value;
|
|
2984
|
+
if (typeof value !== "number") return;
|
|
2985
|
+
if (Number(input.value) !== value) input.value = String(value);
|
|
2986
|
+
reading.textContent = `${o.num(value, control.step && control.step >= 1 ? 0 : 2)}${control.unit ? ` ${control.unit}` : ""}`;
|
|
2987
|
+
});
|
|
2988
|
+
return wrap;
|
|
2989
|
+
};
|
|
2990
|
+
const booleanControl = (control) => {
|
|
2991
|
+
const wrap = mk("label", "tr-ctl");
|
|
2992
|
+
const box2 = mk("input");
|
|
2993
|
+
box2.type = "checkbox";
|
|
2994
|
+
box2.disabled = control.disabled;
|
|
2995
|
+
box2.onchange = () => o.onControl(control.id, box2.checked);
|
|
2996
|
+
wrap.append(box2, document.createTextNode(` ${control.name}`));
|
|
2997
|
+
refreshers.push((view) => {
|
|
2998
|
+
box2.checked = view.controls.find((c) => c.id === control.id)?.value === true;
|
|
2999
|
+
});
|
|
3000
|
+
return wrap;
|
|
3001
|
+
};
|
|
3002
|
+
const choiceControl = (control) => {
|
|
3003
|
+
const wrap = mk("div", "tr-ctl");
|
|
3004
|
+
wrap.appendChild(mk("span", void 0, control.name));
|
|
3005
|
+
if (control.options.length <= SEGMENTED_MAX) {
|
|
3006
|
+
const group = mk("div", "tr-seg");
|
|
3007
|
+
group.setAttribute("role", "group");
|
|
3008
|
+
const buttons = control.options.map((option) => {
|
|
3009
|
+
const b = mk("button", "tr-btn", option.name);
|
|
3010
|
+
b.type = "button";
|
|
3011
|
+
b.disabled = control.disabled;
|
|
3012
|
+
b.onclick = () => o.onControl(control.id, option.id);
|
|
3013
|
+
group.appendChild(b);
|
|
3014
|
+
return { b, id: option.id };
|
|
3015
|
+
});
|
|
3016
|
+
wrap.appendChild(group);
|
|
3017
|
+
refreshers.push((view) => {
|
|
3018
|
+
const value = view.controls.find((c) => c.id === control.id)?.value;
|
|
3019
|
+
for (const { b, id } of buttons) {
|
|
3020
|
+
b.setAttribute("aria-pressed", String(id === value));
|
|
3021
|
+
b.classList.toggle("on", id === value);
|
|
3022
|
+
}
|
|
3023
|
+
});
|
|
3024
|
+
return wrap;
|
|
3025
|
+
}
|
|
3026
|
+
const select = mk("select");
|
|
3027
|
+
select.disabled = control.disabled;
|
|
3028
|
+
for (const option of control.options) {
|
|
3029
|
+
const op = mk("option", void 0, option.name);
|
|
3030
|
+
op.value = option.id;
|
|
3031
|
+
select.appendChild(op);
|
|
3032
|
+
}
|
|
3033
|
+
select.onchange = () => o.onControl(control.id, select.value);
|
|
3034
|
+
wrap.appendChild(select);
|
|
3035
|
+
refreshers.push((view) => {
|
|
3036
|
+
const value = view.controls.find((c) => c.id === control.id)?.value;
|
|
3037
|
+
if (typeof value === "string") select.value = value;
|
|
3038
|
+
});
|
|
3039
|
+
return wrap;
|
|
3040
|
+
};
|
|
3041
|
+
const actionControl = (control) => {
|
|
3042
|
+
const b = mk("button", "tr-btn", control.name);
|
|
3043
|
+
b.type = "button";
|
|
3044
|
+
b.disabled = control.disabled;
|
|
3045
|
+
b.onclick = () => o.onAction(control.id);
|
|
3046
|
+
return b;
|
|
3047
|
+
};
|
|
3048
|
+
const controlElement = (control) => {
|
|
3049
|
+
const el = control.kind === "number" ? numberControl(control) : control.kind === "boolean" ? booleanControl(control) : control.kind === "choice" ? choiceControl(control) : actionControl(control);
|
|
3050
|
+
if (control.hint) el.title = control.hint;
|
|
3051
|
+
return el;
|
|
3052
|
+
};
|
|
3053
|
+
const layerList = (layers) => {
|
|
3054
|
+
const wrap = mk("div", "tr-layers");
|
|
3055
|
+
wrap.appendChild(mk("div", "sub", t("ui.layers")));
|
|
3056
|
+
for (const layer of layers) {
|
|
3057
|
+
const label = mk("label", "tr-ctl");
|
|
3058
|
+
const box2 = mk("input");
|
|
3059
|
+
box2.type = "checkbox";
|
|
3060
|
+
box2.onchange = () => o.onLayer(layer.id, box2.checked);
|
|
3061
|
+
label.append(box2, document.createTextNode(` ${layer.name}`));
|
|
3062
|
+
wrap.appendChild(label);
|
|
3063
|
+
refreshers.push((view) => {
|
|
3064
|
+
box2.checked = view.layers.find((l) => l.id === layer.id)?.on === true;
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
3067
|
+
return wrap;
|
|
3068
|
+
};
|
|
3069
|
+
const rebuild = (view) => {
|
|
3070
|
+
body.textContent = "";
|
|
3071
|
+
refreshers.length = 0;
|
|
3072
|
+
body.appendChild(mk("div", "sub", view.title));
|
|
3073
|
+
const list = mk("div", "tr-ctls");
|
|
3074
|
+
for (const control of view.controls) if (!control.hidden) list.appendChild(controlElement(control));
|
|
3075
|
+
body.appendChild(list);
|
|
3076
|
+
if (view.layers.length) body.appendChild(layerList(view.layers));
|
|
3077
|
+
};
|
|
3078
|
+
const renderMetrics = (rows) => {
|
|
3079
|
+
metricList.textContent = "";
|
|
3080
|
+
metricNodes.clear();
|
|
3081
|
+
metricTitle.style.display = rows.length ? "" : "none";
|
|
3082
|
+
for (const row of rows) {
|
|
3083
|
+
const dd = mk("dd");
|
|
3084
|
+
const text = mk("span", void 0, row.text);
|
|
3085
|
+
dd.appendChild(text);
|
|
3086
|
+
let bar = null;
|
|
3087
|
+
if (row.ratio !== void 0) {
|
|
3088
|
+
bar = mk("div", "tr-bar");
|
|
3089
|
+
bar.appendChild(mk("i"));
|
|
3090
|
+
dd.appendChild(bar);
|
|
3091
|
+
}
|
|
3092
|
+
metricList.append(mk("dt", void 0, row.name), dd);
|
|
3093
|
+
metricNodes.set(row.id, { text, bar });
|
|
3094
|
+
}
|
|
3095
|
+
};
|
|
3096
|
+
const fillBar = (bar, ratio) => {
|
|
3097
|
+
bar.className = `tr-bar${ratio > 1 ? " bad" : ratio >= 0.8 ? " warn" : ""}`;
|
|
3098
|
+
const fill = bar.firstElementChild;
|
|
3099
|
+
fill.style.width = `${Math.round(Math.max(0, Math.min(1, ratio)) * 100)}%`;
|
|
3100
|
+
};
|
|
3101
|
+
return {
|
|
3102
|
+
element,
|
|
3103
|
+
show(view) {
|
|
3104
|
+
hasBench = view !== null;
|
|
3105
|
+
syncDisplay();
|
|
3106
|
+
if (!view) {
|
|
3107
|
+
shape = "";
|
|
3108
|
+
body.textContent = "";
|
|
3109
|
+
refreshers.length = 0;
|
|
3110
|
+
renderMetrics([]);
|
|
3111
|
+
metricKey = "";
|
|
3112
|
+
return;
|
|
3113
|
+
}
|
|
3114
|
+
const next = shapeOf(view);
|
|
3115
|
+
if (next !== shape) {
|
|
3116
|
+
shape = next;
|
|
3117
|
+
rebuild(view);
|
|
3118
|
+
}
|
|
3119
|
+
for (const refresh of refreshers) refresh(view);
|
|
3120
|
+
},
|
|
3121
|
+
metrics(rows) {
|
|
3122
|
+
hasRoom = rows.room.length > 0;
|
|
3123
|
+
syncDisplay();
|
|
3124
|
+
roomLine.textContent = rows.room.map((row) => `${row.name} ${row.text}`).join(" \xB7 ");
|
|
3125
|
+
const key = rows.module.map((row) => `${row.id}:${row.ratio !== void 0}`).join("|");
|
|
3126
|
+
if (key !== metricKey) {
|
|
3127
|
+
metricKey = key;
|
|
3128
|
+
renderMetrics(rows.module);
|
|
3129
|
+
}
|
|
3130
|
+
for (const row of rows.module) {
|
|
3131
|
+
const node = metricNodes.get(row.id);
|
|
3132
|
+
if (!node) continue;
|
|
3133
|
+
node.text.textContent = row.text;
|
|
3134
|
+
if (node.bar && row.ratio !== void 0) fillBar(node.bar, row.ratio);
|
|
3135
|
+
}
|
|
3136
|
+
},
|
|
3137
|
+
get visible() {
|
|
3138
|
+
if (element.style.display === "none") return false;
|
|
3139
|
+
const parent = element.parentElement;
|
|
3140
|
+
return !parent || getComputedStyle(parent).display !== "none";
|
|
3141
|
+
}
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3144
|
+
|
|
2323
3145
|
// src/ui.ts
|
|
2324
3146
|
var CSS = `
|
|
2325
3147
|
.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 +3194,13 @@ var CSS = `
|
|
|
2372
3194
|
.tr-cat{flex:0 1 auto;max-height:40%}
|
|
2373
3195
|
.tr-anim{flex:0 0 auto;max-height:18%}
|
|
2374
3196
|
.tr-anim .list{display:flex;flex-wrap:wrap;gap:4px}.tr-anim .sub{color:#9aa3b2;margin-bottom:4px}
|
|
3197
|
+
.tr-bench{flex:0 1 auto;max-height:34%}
|
|
3198
|
+
.tr-bench .sub{color:#9aa3b2;margin-bottom:4px}
|
|
3199
|
+
.tr-ctls{display:flex;flex-direction:column;gap:5px}
|
|
3200
|
+
.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}
|
|
3201
|
+
.tr-seg{display:inline-flex;flex-wrap:wrap;gap:2px}
|
|
3202
|
+
.tr-layers{margin-top:6px;display:flex;flex-direction:column;gap:2px}
|
|
3203
|
+
.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
3204
|
.tr-tests{flex:1 1 auto;min-height:100px}
|
|
2376
3205
|
.tr-tests .list{display:flex;flex-direction:column;gap:3px;margin-top:6px}.tr-tests .list .tr-btn{text-align:left}
|
|
2377
3206
|
.tr-row{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;align-items:center}
|
|
@@ -2380,7 +3209,7 @@ var CSS = `
|
|
|
2380
3209
|
@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
3210
|
`;
|
|
2382
3211
|
var fold = (text) => text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
|
2383
|
-
var
|
|
3212
|
+
var mk2 = (tag, cls, text) => {
|
|
2384
3213
|
const e = document.createElement(tag);
|
|
2385
3214
|
if (cls) e.className = cls;
|
|
2386
3215
|
if (text !== void 0) e.textContent = text;
|
|
@@ -2388,42 +3217,42 @@ var mk = (tag, cls, text) => {
|
|
|
2388
3217
|
};
|
|
2389
3218
|
function createUi(o) {
|
|
2390
3219
|
const { t } = o;
|
|
2391
|
-
const style =
|
|
3220
|
+
const style = mk2("style");
|
|
2392
3221
|
style.textContent = CSS;
|
|
2393
|
-
const root =
|
|
3222
|
+
const root = mk2("div", "tr-root");
|
|
2394
3223
|
const pos = getComputedStyle?.(o.container)?.position;
|
|
2395
3224
|
if (!pos || pos === "static") o.container.style.position = "relative";
|
|
2396
3225
|
root.appendChild(style);
|
|
2397
3226
|
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 =
|
|
3227
|
+
const cross = mk2("div", "tr-cross");
|
|
3228
|
+
cross.appendChild(mk2("i"));
|
|
3229
|
+
const top = mk2("div", "tr-top");
|
|
3230
|
+
const toolBox = mk2("div", "tr-tool");
|
|
3231
|
+
const status = mk2("div", "tr-status");
|
|
3232
|
+
const prog = mk2("div", "tr-prog");
|
|
3233
|
+
const progLabel = mk2("b");
|
|
3234
|
+
const progBar = mk2("div", "tr-bar");
|
|
3235
|
+
const progFill = mk2("i");
|
|
2407
3236
|
progBar.appendChild(progFill);
|
|
2408
3237
|
prog.append(progLabel, progBar);
|
|
2409
3238
|
top.append(toolBox, status, prog);
|
|
2410
|
-
const bottom =
|
|
2411
|
-
const held =
|
|
2412
|
-
const hotbar =
|
|
3239
|
+
const bottom = mk2("div", "tr-bottom");
|
|
3240
|
+
const held = mk2("div", "tr-held");
|
|
3241
|
+
const hotbar = mk2("div", "tr-hotbar");
|
|
2413
3242
|
bottom.append(held, hotbar);
|
|
2414
|
-
const help =
|
|
3243
|
+
const help = mk2("div", "tr-help");
|
|
2415
3244
|
help.textContent = t("ui.help");
|
|
2416
|
-
const left =
|
|
2417
|
-
const insp =
|
|
2418
|
-
const logPanel =
|
|
3245
|
+
const left = mk2("div", "tr-left");
|
|
3246
|
+
const insp = mk2("div", "tr-panel tr-insp");
|
|
3247
|
+
const logPanel = mk2("div", "tr-panel tr-log");
|
|
2419
3248
|
left.append(insp, logPanel);
|
|
2420
|
-
const logTitle =
|
|
2421
|
-
const logBar =
|
|
2422
|
-
const logBody =
|
|
3249
|
+
const logTitle = mk2("h3", void 0, t("ui.log"));
|
|
3250
|
+
const logBar = mk2("div", "tr-logbar");
|
|
3251
|
+
const logBody = mk2("div", "tr-logbody");
|
|
2423
3252
|
const filters = { action: true, event: true, check: true };
|
|
2424
3253
|
const filterBtns = [];
|
|
2425
3254
|
for (const ch of ["action", "event", "check"]) {
|
|
2426
|
-
const b =
|
|
3255
|
+
const b = mk2("button", "tr-btn on", t(`ui.filter.${ch}`));
|
|
2427
3256
|
b.type = "button";
|
|
2428
3257
|
b.onclick = () => {
|
|
2429
3258
|
filters[ch] = !filters[ch];
|
|
@@ -2434,8 +3263,8 @@ function createUi(o) {
|
|
|
2434
3263
|
logBar.appendChild(b);
|
|
2435
3264
|
}
|
|
2436
3265
|
let autoscroll = true;
|
|
2437
|
-
const asLabel =
|
|
2438
|
-
const asBox =
|
|
3266
|
+
const asLabel = mk2("label");
|
|
3267
|
+
const asBox = mk2("input");
|
|
2439
3268
|
asBox.type = "checkbox";
|
|
2440
3269
|
asBox.checked = true;
|
|
2441
3270
|
asBox.onchange = () => {
|
|
@@ -2444,7 +3273,7 @@ function createUi(o) {
|
|
|
2444
3273
|
asLabel.append(asBox, document.createTextNode(" " + t("ui.autoscroll")));
|
|
2445
3274
|
logBar.appendChild(asLabel);
|
|
2446
3275
|
const logBtn = (label, fn) => {
|
|
2447
|
-
const b =
|
|
3276
|
+
const b = mk2("button", "tr-btn", label);
|
|
2448
3277
|
b.type = "button";
|
|
2449
3278
|
b.onclick = fn;
|
|
2450
3279
|
logBar.appendChild(b);
|
|
@@ -2452,7 +3281,7 @@ function createUi(o) {
|
|
|
2452
3281
|
logBtn(t("ui.copy_json"), () => {
|
|
2453
3282
|
const text = o.log.toJSON();
|
|
2454
3283
|
const select = () => {
|
|
2455
|
-
const ta =
|
|
3284
|
+
const ta = mk2("textarea");
|
|
2456
3285
|
ta.value = text;
|
|
2457
3286
|
ta.setAttribute("readonly", "");
|
|
2458
3287
|
ta.style.cssText = "position:fixed;left:8px;bottom:8px;width:min(480px,80vw);height:120px;z-index:99999";
|
|
@@ -2486,12 +3315,12 @@ function createUi(o) {
|
|
|
2486
3315
|
const fill = (n, e) => {
|
|
2487
3316
|
n.className = lineClass(e);
|
|
2488
3317
|
n.textContent = "";
|
|
2489
|
-
const tm =
|
|
3318
|
+
const tm = mk2("time", void 0, fmtTime(e.t));
|
|
2490
3319
|
n.append(tm, document.createTextNode(e.text));
|
|
2491
3320
|
};
|
|
2492
3321
|
const addNode = (e) => {
|
|
2493
3322
|
if (!filters[e.channel]) return;
|
|
2494
|
-
const n =
|
|
3323
|
+
const n = mk2("div");
|
|
2495
3324
|
fill(n, e);
|
|
2496
3325
|
nodes.set(e.seq, n);
|
|
2497
3326
|
logBody.appendChild(n);
|
|
@@ -2513,17 +3342,18 @@ function createUi(o) {
|
|
|
2513
3342
|
if (n) fill(n, e);
|
|
2514
3343
|
} else addNode(e);
|
|
2515
3344
|
});
|
|
2516
|
-
const right =
|
|
2517
|
-
const cat =
|
|
2518
|
-
const anim =
|
|
2519
|
-
const tests =
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
3345
|
+
const right = mk2("div", "tr-right");
|
|
3346
|
+
const cat = mk2("div", "tr-panel tr-cat");
|
|
3347
|
+
const anim = mk2("div", "tr-panel tr-anim");
|
|
3348
|
+
const tests = mk2("div", "tr-panel tr-tests");
|
|
3349
|
+
const benchPanel = createBenchPanel({ t, num: (n, d) => num(n, d), onControl: o.onControl, onLayer: o.onLayer, onAction: o.onAction });
|
|
3350
|
+
right.append(cat, anim, benchPanel.element, tests);
|
|
3351
|
+
cat.appendChild(mk2("h3", void 0, t("ui.catalog")));
|
|
3352
|
+
const itemSearch = mk2("input", "tr-search");
|
|
2523
3353
|
itemSearch.type = "search";
|
|
2524
3354
|
itemSearch.placeholder = t("ui.search_items");
|
|
2525
|
-
const tabs =
|
|
2526
|
-
const items =
|
|
3355
|
+
const tabs = mk2("div", "tr-tabs");
|
|
3356
|
+
const items = mk2("div", "tr-items");
|
|
2527
3357
|
cat.append(itemSearch, tabs, items);
|
|
2528
3358
|
let activeGroup = "";
|
|
2529
3359
|
const visibleItems = () => {
|
|
@@ -2535,17 +3365,17 @@ function createUi(o) {
|
|
|
2535
3365
|
items.textContent = "";
|
|
2536
3366
|
const searching = itemSearch.value.trim() !== "";
|
|
2537
3367
|
const found = visibleItems();
|
|
2538
|
-
if (!found.length) items.appendChild(
|
|
3368
|
+
if (!found.length) items.appendChild(mk2("div", "tr-empty", t("ui.no_results")));
|
|
2539
3369
|
for (const { item: it, group } of found) {
|
|
2540
|
-
const wrap =
|
|
2541
|
-
const b =
|
|
2542
|
-
if (searching) b.appendChild(
|
|
3370
|
+
const wrap = mk2("div", "tr-item");
|
|
3371
|
+
const b = mk2("button", "tr-btn", it.name);
|
|
3372
|
+
if (searching) b.appendChild(mk2("small", void 0, group));
|
|
2543
3373
|
b.type = "button";
|
|
2544
3374
|
b.title = it.note ?? it.name;
|
|
2545
3375
|
b.onclick = () => o.onSpawn(it.id);
|
|
2546
3376
|
wrap.appendChild(b);
|
|
2547
3377
|
if (it.weapon) {
|
|
2548
|
-
const w =
|
|
3378
|
+
const w = mk2("button", "tr-btn", "\u2694");
|
|
2549
3379
|
w.type = "button";
|
|
2550
3380
|
w.title = t("ui.equip");
|
|
2551
3381
|
w.onclick = () => o.onEquip(it.id);
|
|
@@ -2559,7 +3389,7 @@ function createUi(o) {
|
|
|
2559
3389
|
const groupNames = [...o.groups().keys()];
|
|
2560
3390
|
if (!groupNames.includes(activeGroup)) activeGroup = groupNames[0] ?? "";
|
|
2561
3391
|
for (const g of groupNames) {
|
|
2562
|
-
const b =
|
|
3392
|
+
const b = mk2("button", g === activeGroup ? "tr-btn on" : "tr-btn", g);
|
|
2563
3393
|
b.type = "button";
|
|
2564
3394
|
b.onclick = () => {
|
|
2565
3395
|
activeGroup = g;
|
|
@@ -2572,9 +3402,9 @@ function createUi(o) {
|
|
|
2572
3402
|
renderTabs();
|
|
2573
3403
|
itemSearch.oninput = renderItems;
|
|
2574
3404
|
for (const box2 of [itemSearch]) box2.addEventListener("keydown", (e) => e.stopPropagation());
|
|
2575
|
-
const controls =
|
|
3405
|
+
const controls = mk2("div", "tr-row");
|
|
2576
3406
|
const ctl = (label, fn) => {
|
|
2577
|
-
const b =
|
|
3407
|
+
const b = mk2("button", "tr-btn", label);
|
|
2578
3408
|
b.type = "button";
|
|
2579
3409
|
b.onclick = () => fn(b);
|
|
2580
3410
|
controls.appendChild(b);
|
|
@@ -2595,29 +3425,29 @@ function createUi(o) {
|
|
|
2595
3425
|
o.onFast(fast);
|
|
2596
3426
|
});
|
|
2597
3427
|
const tapBtn = ctl(t("ui.tap"), () => o.onTap());
|
|
2598
|
-
const qSel =
|
|
3428
|
+
const qSel = mk2("select");
|
|
2599
3429
|
for (const q of ["low", "medium", "high"]) {
|
|
2600
|
-
const op =
|
|
3430
|
+
const op = mk2("option", void 0, t(`ui.quality.${q}`));
|
|
2601
3431
|
op.value = q;
|
|
2602
3432
|
qSel.appendChild(op);
|
|
2603
3433
|
}
|
|
2604
3434
|
qSel.value = o.quality;
|
|
2605
3435
|
qSel.onchange = () => o.onQuality(qSel.value);
|
|
2606
|
-
const qLabel =
|
|
3436
|
+
const qLabel = mk2("label", void 0, t("ui.quality") + " ");
|
|
2607
3437
|
qLabel.appendChild(qSel);
|
|
2608
3438
|
controls.appendChild(qLabel);
|
|
2609
|
-
const count =
|
|
3439
|
+
const count = mk2("span");
|
|
2610
3440
|
controls.appendChild(count);
|
|
2611
3441
|
cat.appendChild(controls);
|
|
2612
|
-
tests.appendChild(
|
|
2613
|
-
const runAll =
|
|
3442
|
+
tests.appendChild(mk2("h3", void 0, t("ui.tests")));
|
|
3443
|
+
const runAll = mk2("button", "tr-btn", t("ui.run_all"));
|
|
2614
3444
|
runAll.type = "button";
|
|
2615
|
-
const summary =
|
|
2616
|
-
const testSearch =
|
|
3445
|
+
const summary = mk2("div", "tr-summary");
|
|
3446
|
+
const testSearch = mk2("input", "tr-search");
|
|
2617
3447
|
testSearch.type = "search";
|
|
2618
3448
|
testSearch.placeholder = t("ui.search_tests");
|
|
2619
3449
|
testSearch.addEventListener("keydown", (e) => e.stopPropagation());
|
|
2620
|
-
const list =
|
|
3450
|
+
const list = mk2("div", "list");
|
|
2621
3451
|
tests.append(runAll, summary, testSearch, list);
|
|
2622
3452
|
testSearch.oninput = () => {
|
|
2623
3453
|
const term = fold(testSearch.value.trim());
|
|
@@ -2640,7 +3470,7 @@ function createUi(o) {
|
|
|
2640
3470
|
list.textContent = "";
|
|
2641
3471
|
scBtns.clear();
|
|
2642
3472
|
for (const s of o.scenarios()) {
|
|
2643
|
-
const b =
|
|
3473
|
+
const b = mk2("button", "tr-btn", s.name);
|
|
2644
3474
|
b.type = "button";
|
|
2645
3475
|
b.onclick = async () => {
|
|
2646
3476
|
if (busy) return;
|
|
@@ -2666,7 +3496,7 @@ function createUi(o) {
|
|
|
2666
3496
|
busy = false;
|
|
2667
3497
|
}
|
|
2668
3498
|
};
|
|
2669
|
-
const toggle =
|
|
3499
|
+
const toggle = mk2("button", "tr-btn tr-toggle", t("ui.panel"));
|
|
2670
3500
|
toggle.type = "button";
|
|
2671
3501
|
const togglePanels = () => {
|
|
2672
3502
|
root.classList.toggle("open");
|
|
@@ -2675,15 +3505,15 @@ function createUi(o) {
|
|
|
2675
3505
|
toggle.onclick = togglePanels;
|
|
2676
3506
|
root.append(cross, top, bottom, help, left, right, toggle);
|
|
2677
3507
|
const bar = (value, max, cls) => {
|
|
2678
|
-
const b =
|
|
2679
|
-
const i =
|
|
3508
|
+
const b = mk2("div", `tr-bar ${cls}`);
|
|
3509
|
+
const i = mk2("i");
|
|
2680
3510
|
i.style.width = `${Math.max(0, Math.min(100, value / Math.max(1e-9, max) * 100))}%`;
|
|
2681
3511
|
b.appendChild(i);
|
|
2682
3512
|
return b;
|
|
2683
3513
|
};
|
|
2684
3514
|
const row = (k, v) => {
|
|
2685
|
-
const r =
|
|
2686
|
-
r.append(
|
|
3515
|
+
const r = mk2("div", "r");
|
|
3516
|
+
r.append(mk2("span", void 0, k), mk2("span", void 0, v));
|
|
2687
3517
|
return r;
|
|
2688
3518
|
};
|
|
2689
3519
|
const num = (n, d = 1) => {
|
|
@@ -2692,12 +3522,12 @@ function createUi(o) {
|
|
|
2692
3522
|
};
|
|
2693
3523
|
const renderInspector = (i) => {
|
|
2694
3524
|
insp.textContent = "";
|
|
2695
|
-
insp.appendChild(
|
|
3525
|
+
insp.appendChild(mk2("h3", void 0, t("ui.inspector")));
|
|
2696
3526
|
if (!i) {
|
|
2697
|
-
insp.appendChild(
|
|
3527
|
+
insp.appendChild(mk2("div", void 0, t("ui.aim_at_something")));
|
|
2698
3528
|
return;
|
|
2699
3529
|
}
|
|
2700
|
-
insp.appendChild(
|
|
3530
|
+
insp.appendChild(mk2("div", "t", `${i.object} \u203A ${i.part}`));
|
|
2701
3531
|
insp.appendChild(row(t("ins.material"), i.material));
|
|
2702
3532
|
insp.appendChild(row(t("ins.status"), i.status + (i.burning ? ` \xB7 ${t("ins.burning")}` : "")));
|
|
2703
3533
|
insp.appendChild(row(t("ins.hp"), `${num(i.hp, 0)} / ${num(i.maxHp, 0)}`));
|
|
@@ -2716,30 +3546,30 @@ function createUi(o) {
|
|
|
2716
3546
|
insp.appendChild(bar(Math.min(i.stress, 1.2), 1.2, i.stress > 1 ? "bad" : i.stress > 0.7 ? "warn" : "hp"));
|
|
2717
3547
|
}
|
|
2718
3548
|
if (i.joints.length) {
|
|
2719
|
-
insp.appendChild(
|
|
3549
|
+
insp.appendChild(mk2("div", void 0, t("ins.joints") + ":"));
|
|
2720
3550
|
for (const j of i.joints) {
|
|
2721
3551
|
const tools = j.tools.length ? j.tools.join(", ") : t("tool.none");
|
|
2722
|
-
insp.appendChild(
|
|
3552
|
+
insp.appendChild(mk2("div", void 0, `\u2022 ${j.kind} (${j.id})${j.open ? ` \xB7 ${t("ins.open")}` : ` \u2192 ${tools}`}`));
|
|
2723
3553
|
}
|
|
2724
3554
|
}
|
|
2725
|
-
if (i.note) insp.appendChild(
|
|
3555
|
+
if (i.note) insp.appendChild(mk2("div", void 0, i.note));
|
|
2726
3556
|
};
|
|
2727
3557
|
let lastAnim = "";
|
|
2728
3558
|
const renderAnimations = (a) => {
|
|
2729
3559
|
anim.textContent = "";
|
|
2730
|
-
anim.appendChild(
|
|
3560
|
+
anim.appendChild(mk2("h3", void 0, t("ui.animations")));
|
|
2731
3561
|
if (!a) {
|
|
2732
|
-
anim.appendChild(
|
|
3562
|
+
anim.appendChild(mk2("div", "sub", t("ui.animations_aim")));
|
|
2733
3563
|
return;
|
|
2734
3564
|
}
|
|
2735
|
-
anim.appendChild(
|
|
3565
|
+
anim.appendChild(mk2("div", "sub", a.name));
|
|
2736
3566
|
if (a.clips.length === 0) {
|
|
2737
|
-
anim.appendChild(
|
|
3567
|
+
anim.appendChild(mk2("div", "sub", t("ui.animations_none")));
|
|
2738
3568
|
return;
|
|
2739
3569
|
}
|
|
2740
|
-
const l =
|
|
3570
|
+
const l = mk2("div", "list");
|
|
2741
3571
|
for (const c of a.clips) {
|
|
2742
|
-
const b =
|
|
3572
|
+
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
3573
|
b.type = "button";
|
|
2744
3574
|
b.onclick = () => o.onPlay(a.objectId, c.id);
|
|
2745
3575
|
l.appendChild(b);
|
|
@@ -2756,7 +3586,7 @@ function createUi(o) {
|
|
|
2756
3586
|
hud(s) {
|
|
2757
3587
|
toolBox.textContent = "";
|
|
2758
3588
|
toolBox.append(document.createTextNode(s.toolName));
|
|
2759
|
-
if (s.ammo) toolBox.append(
|
|
3589
|
+
if (s.ammo) toolBox.append(mk2("small", void 0, s.ammo));
|
|
2760
3590
|
if (s.status) status.textContent = s.status;
|
|
2761
3591
|
else if (!toastTimer) status.textContent = "";
|
|
2762
3592
|
if (s.progress) {
|
|
@@ -2777,7 +3607,7 @@ function createUi(o) {
|
|
|
2777
3607
|
];
|
|
2778
3608
|
if (s.held.weapon) keys.push(["H", t("hud.equip", { weapon: s.held.weapon })]);
|
|
2779
3609
|
for (const [k, label] of keys) {
|
|
2780
|
-
held.append(
|
|
3610
|
+
held.append(mk2("kbd", void 0, k), document.createTextNode(` ${label} `));
|
|
2781
3611
|
}
|
|
2782
3612
|
} else held.style.display = "none";
|
|
2783
3613
|
const key = s.slots.join("|") + s.page;
|
|
@@ -2787,9 +3617,9 @@ function createUi(o) {
|
|
|
2787
3617
|
slotsEl.length = 0;
|
|
2788
3618
|
hudPage = s.page - 1;
|
|
2789
3619
|
s.slots.forEach((name, i) => {
|
|
2790
|
-
const b =
|
|
3620
|
+
const b = mk2("button", "tr-slot");
|
|
2791
3621
|
b.type = "button";
|
|
2792
|
-
b.append(
|
|
3622
|
+
b.append(mk2("b", void 0, String((i + 1) % 10)), document.createTextNode(name));
|
|
2793
3623
|
b.title = name;
|
|
2794
3624
|
b.onclick = () => o.onTool(hudPage * 10 + i);
|
|
2795
3625
|
hotbar.appendChild(b);
|
|
@@ -2827,6 +3657,11 @@ function createUi(o) {
|
|
|
2827
3657
|
renderTabs();
|
|
2828
3658
|
},
|
|
2829
3659
|
togglePanels,
|
|
3660
|
+
bench: benchPanel.show,
|
|
3661
|
+
metrics: benchPanel.metrics,
|
|
3662
|
+
get benchVisible() {
|
|
3663
|
+
return benchPanel.visible;
|
|
3664
|
+
},
|
|
2830
3665
|
dispose() {
|
|
2831
3666
|
offLog();
|
|
2832
3667
|
if (toastTimer) clearTimeout(toastTimer);
|
|
@@ -2839,6 +3674,7 @@ function createUi(o) {
|
|
|
2839
3674
|
|
|
2840
3675
|
// src/room.ts
|
|
2841
3676
|
var FIXED_DT = 1 / 60;
|
|
3677
|
+
var METRICS_INTERVAL_S = 0.25;
|
|
2842
3678
|
var v3 = (v) => [v.x, v.y, v.z];
|
|
2843
3679
|
var dist3 = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
2844
3680
|
function createTestRoom(options) {
|
|
@@ -2952,10 +3788,14 @@ function createTestRoom(options) {
|
|
|
2952
3788
|
const tools = /* @__PURE__ */ new Map();
|
|
2953
3789
|
let toolIndex = Math.max(0, loadout.indexOf("hand"));
|
|
2954
3790
|
let toolId = loadout[toolIndex] ?? "hand";
|
|
3791
|
+
const toolSettings = {};
|
|
3792
|
+
let fuseLength;
|
|
3793
|
+
const SETTING_IDS = { condition: "condition", ammoQuality: "ammo_quality", fuseLength: "fuse_length" };
|
|
2955
3794
|
const toolState = (id) => {
|
|
2956
3795
|
let s = tools.get(id);
|
|
2957
3796
|
if (!s) {
|
|
2958
|
-
s = arsenal.create(id, { seed: 7, reserve: 999 });
|
|
3797
|
+
s = arsenal.create(id, { seed: 7, reserve: 999, ...toolSettings });
|
|
3798
|
+
s.setFuseLength(fuseLength);
|
|
2959
3799
|
s.refill();
|
|
2960
3800
|
tools.set(id, s);
|
|
2961
3801
|
}
|
|
@@ -3056,6 +3896,7 @@ function createTestRoom(options) {
|
|
|
3056
3896
|
for (const ev of batch) {
|
|
3057
3897
|
if (ev.type === "leak") puncture(ev);
|
|
3058
3898
|
else if (ev.type === "ignite") igniteNear(ev.point, 0.9);
|
|
3899
|
+
else if (ev.type === "explode") emitHost({ type: "explosion", source: ev.object, point: ev.point, energy: ev.energy, radius: ev.radius, cause: "part" });
|
|
3059
3900
|
fire("event", ev);
|
|
3060
3901
|
options.onEvent?.(ev);
|
|
3061
3902
|
}
|
|
@@ -3076,7 +3917,7 @@ function createTestRoom(options) {
|
|
|
3076
3917
|
},
|
|
3077
3918
|
primaryUp: () => {
|
|
3078
3919
|
useHeld = false;
|
|
3079
|
-
|
|
3920
|
+
releaseTrigger();
|
|
3080
3921
|
},
|
|
3081
3922
|
secondary: () => {
|
|
3082
3923
|
if (heldEntry) throwHeld();
|
|
@@ -3233,8 +4074,7 @@ function createTestRoom(options) {
|
|
|
3233
4074
|
if (heldEntry || heldPiece) releaseHeld();
|
|
3234
4075
|
useItemPlayer = null;
|
|
3235
4076
|
playing.length = 0;
|
|
3236
|
-
|
|
3237
|
-
clearProjectileMeshes();
|
|
4077
|
+
explosives.clear();
|
|
3238
4078
|
fireZones.length = 0;
|
|
3239
4079
|
burningStains.length = 0;
|
|
3240
4080
|
for (const id of [...entries.keys()]) if (!entries.get(id)?.arena) removeEntry(id);
|
|
@@ -3470,7 +4310,6 @@ function createTestRoom(options) {
|
|
|
3470
4310
|
}
|
|
3471
4311
|
});
|
|
3472
4312
|
player.kick(recoil[0], recoil[1]);
|
|
3473
|
-
emitHost({ type: "shot", weapon: toolId, shots: shots.length, recoil });
|
|
3474
4313
|
const f = first;
|
|
3475
4314
|
const d = f ? num(f.distance, 1) : "";
|
|
3476
4315
|
let text;
|
|
@@ -3618,99 +4457,46 @@ function createTestRoom(options) {
|
|
|
3618
4457
|
1
|
|
3619
4458
|
);
|
|
3620
4459
|
};
|
|
3621
|
-
const
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
};
|
|
3627
|
-
|
|
3628
|
-
const
|
|
3629
|
-
const
|
|
3630
|
-
const
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
scene
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
const results = world.explode(at, p.energy, p.radius, "player");
|
|
3655
|
-
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
|
-
if (p.fire) {
|
|
3657
|
-
fluids.splash("fuel", [at[0], Math.max(at[1], groundAt(at[0], at[2])), at[2]], 0.5);
|
|
3658
|
-
fireZones.push({ point: [at[0], Math.max(0.02, groundAt(at[0], at[2])), at[2]], radius: p.fire.radius, temperature: p.fire.temperature, remaining: p.fire.seconds, acc: 0 });
|
|
3659
|
-
renderer.emit("fire", at, [0, 1, 0], 24, 3);
|
|
3660
|
-
action("molotov_fire", t("act.molotov_fire", { weapon: w, seconds: num(p.fire.seconds, 0), radius: num(p.fire.radius, 1) }), { data: { weapon: p.weapon, impact } });
|
|
3661
|
-
}
|
|
3662
|
-
};
|
|
3663
|
-
const updateProjectiles = (dt) => {
|
|
3664
|
-
for (let i = projectiles.length - 1; i >= 0; i--) {
|
|
3665
|
-
const p = projectiles[i];
|
|
3666
|
-
p.age += dt;
|
|
3667
|
-
p.vel.y -= 9.81 * dt;
|
|
3668
|
-
const speed = p.vel.length();
|
|
3669
|
-
const step2 = speed * dt;
|
|
3670
|
-
let burst = false;
|
|
3671
|
-
if (step2 > 1e-6) {
|
|
3672
|
-
const d = [p.vel.x / speed, p.vel.y / speed, p.vel.z / speed];
|
|
3673
|
-
const r = withoutHeld(() => world.raycast(v3(p.pos), d, step2 + 0.05));
|
|
3674
|
-
if (r && r.distance <= step2 + 0.04) {
|
|
3675
|
-
p.pos.set(r.point[0] - d[0] * 0.03, r.point[1] - d[1] * 0.03, r.point[2] - d[2] * 0.03);
|
|
3676
|
-
if (p.burstOnImpact) burst = true;
|
|
3677
|
-
else {
|
|
3678
|
-
const n = r.normal;
|
|
3679
|
-
const vn = p.vel.x * n[0] + p.vel.y * n[1] + p.vel.z * n[2];
|
|
3680
|
-
p.vel.x = (p.vel.x - 2 * vn * n[0]) * 0.35;
|
|
3681
|
-
p.vel.y = (p.vel.y - 2 * vn * n[1]) * 0.35;
|
|
3682
|
-
p.vel.z = (p.vel.z - 2 * vn * n[2]) * 0.35;
|
|
3683
|
-
}
|
|
3684
|
-
} else p.pos.addScaledVector(p.vel, dt);
|
|
3685
|
-
}
|
|
3686
|
-
const g = groundAt(p.pos.x, p.pos.z) + 0.045;
|
|
3687
|
-
if (p.pos.y < g) {
|
|
3688
|
-
p.pos.y = g;
|
|
3689
|
-
if (p.burstOnImpact) burst = true;
|
|
3690
|
-
else {
|
|
3691
|
-
p.vel.y = Math.abs(p.vel.y) * 0.35;
|
|
3692
|
-
p.vel.x *= 0.7;
|
|
3693
|
-
p.vel.z *= 0.7;
|
|
3694
|
-
}
|
|
3695
|
-
}
|
|
3696
|
-
p.mesh.position.copy(p.pos);
|
|
3697
|
-
p.mesh.rotation.x += dt * 8;
|
|
3698
|
-
p.fuse -= dt;
|
|
3699
|
-
if (burst || !p.burstOnImpact && p.fuse <= 0 || p.age > 30) {
|
|
3700
|
-
detonate(p, v3(p.pos), burst);
|
|
3701
|
-
p.mesh.removeFromParent();
|
|
3702
|
-
projMeshes.splice(projMeshes.indexOf(p.mesh), 1);
|
|
3703
|
-
projectiles.splice(i, 1);
|
|
3704
|
-
}
|
|
3705
|
-
}
|
|
3706
|
-
};
|
|
4460
|
+
const startFire = (weapon, at, fire2, impact) => {
|
|
4461
|
+
fluids.splash("fuel", [at[0], Math.max(at[1], groundAt(at[0], at[2])), at[2]], 0.5);
|
|
4462
|
+
fireZones.push({ point: [at[0], Math.max(0.02, groundAt(at[0], at[2])), at[2]], radius: fire2.radius, temperature: fire2.temperature, remaining: fire2.seconds, acc: 0 });
|
|
4463
|
+
renderer.emit("fire", at, [0, 1, 0], 24, 3);
|
|
4464
|
+
action("molotov_fire", t("act.molotov_fire", { weapon: toolName(weapon), seconds: num(fire2.seconds, 0), radius: num(fire2.radius, 1) }), { data: { weapon, impact } });
|
|
4465
|
+
};
|
|
4466
|
+
function crossingBodies() {
|
|
4467
|
+
const bodies = [...entries.values()].filter((e) => !e.arena).map((e) => ({ id: e.so.objectId, box: new THREE.Box3().setFromObject(e.so.root) }));
|
|
4468
|
+
const { x, y, z } = camera.position;
|
|
4469
|
+
const feet = groundAt(x, z);
|
|
4470
|
+
bodies.push({ id: "player", box: new THREE.Box3(new THREE.Vector3(x - PLAYER_RADIUS, feet, z - PLAYER_RADIUS), new THREE.Vector3(x + PLAYER_RADIUS, y + 0.1, z + PLAYER_RADIUS)) });
|
|
4471
|
+
return bodies;
|
|
4472
|
+
}
|
|
4473
|
+
const explosives = createExplosives({
|
|
4474
|
+
THREE,
|
|
4475
|
+
scene,
|
|
4476
|
+
camera,
|
|
4477
|
+
world,
|
|
4478
|
+
renderer,
|
|
4479
|
+
arsenal,
|
|
4480
|
+
objects: () => [...entries.values()].filter((e) => !e.arena).map((e) => e.so.root),
|
|
4481
|
+
bodies: crossingBodies,
|
|
4482
|
+
toolId: () => toolId,
|
|
4483
|
+
toolName,
|
|
4484
|
+
withoutHeld,
|
|
4485
|
+
groundAt,
|
|
4486
|
+
translate: t,
|
|
4487
|
+
format: num,
|
|
4488
|
+
action,
|
|
4489
|
+
emitHost,
|
|
4490
|
+
startFire,
|
|
4491
|
+
say
|
|
4492
|
+
});
|
|
3707
4493
|
const useTool = (dt, held) => {
|
|
3708
4494
|
aim();
|
|
3709
4495
|
const cat = tool.def.category;
|
|
3710
4496
|
if (cat === "hand") return;
|
|
3711
4497
|
const res = tool.trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], dt, held, "player");
|
|
4498
|
+
flushEvents(toolId, tool, res.recoil);
|
|
3712
4499
|
if (res.status === "empty") {
|
|
3713
|
-
emitHost({ type: "empty", weapon: toolId });
|
|
3714
4500
|
if (!tool.reloading && tool.reserve > 0) startReload();
|
|
3715
4501
|
say(t(tool.reloading ? "hud.reloading" : "hud.empty"));
|
|
3716
4502
|
return;
|
|
@@ -3718,8 +4504,13 @@ function createTestRoom(options) {
|
|
|
3718
4504
|
if (res.status !== "ok") {
|
|
3719
4505
|
if (res.status === "reloading") say(t("hud.reloading"));
|
|
3720
4506
|
else if (res.status === "no_fuel") say(t("hud.no_fuel"));
|
|
4507
|
+
else if (res.status === "fault" && res.fault) say(t("hud.fault", { fault: t(`fault.${res.fault}`) }));
|
|
4508
|
+
else if (res.status === "safe") say(t("hud.safe"));
|
|
3721
4509
|
return;
|
|
3722
4510
|
}
|
|
4511
|
+
applyUses(res, dt);
|
|
4512
|
+
};
|
|
4513
|
+
const applyUses = (res, dt) => {
|
|
3723
4514
|
if (res.shots.length) fireShots(res.shots, res.recoil);
|
|
3724
4515
|
for (const u of res.uses) {
|
|
3725
4516
|
switch (u.kind) {
|
|
@@ -3733,7 +4524,13 @@ function createTestRoom(options) {
|
|
|
3733
4524
|
heatRays(u, "heat");
|
|
3734
4525
|
break;
|
|
3735
4526
|
case "throw":
|
|
3736
|
-
throwUse(u);
|
|
4527
|
+
explosives.throwUse(u);
|
|
4528
|
+
break;
|
|
4529
|
+
case "place":
|
|
4530
|
+
explosives.placeUse(u);
|
|
4531
|
+
break;
|
|
4532
|
+
case "backblast":
|
|
4533
|
+
explosives.backblastUse(u);
|
|
3737
4534
|
break;
|
|
3738
4535
|
case "tool":
|
|
3739
4536
|
toolWork(u, dt);
|
|
@@ -3746,6 +4543,16 @@ function createTestRoom(options) {
|
|
|
3746
4543
|
}
|
|
3747
4544
|
}
|
|
3748
4545
|
};
|
|
4546
|
+
const releaseTrigger = () => {
|
|
4547
|
+
if (!tool.cooking) {
|
|
4548
|
+
tool.release();
|
|
4549
|
+
return;
|
|
4550
|
+
}
|
|
4551
|
+
aim();
|
|
4552
|
+
const res = tool.release([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]]);
|
|
4553
|
+
flushEvents(toolId, tool);
|
|
4554
|
+
if (res) applyUses(res, 0);
|
|
4555
|
+
};
|
|
3749
4556
|
const kick = () => {
|
|
3750
4557
|
if (!arsenal.weapons.has("kick")) return;
|
|
3751
4558
|
aim();
|
|
@@ -3755,7 +4562,7 @@ function createTestRoom(options) {
|
|
|
3755
4562
|
};
|
|
3756
4563
|
const setTool = (id) => {
|
|
3757
4564
|
if (!arsenal.weapons.has(id)) throw new Error(`game-test-room: unknown weapon "${id}"`);
|
|
3758
|
-
|
|
4565
|
+
releaseTrigger();
|
|
3759
4566
|
toolId = id;
|
|
3760
4567
|
const i = loadout.indexOf(id);
|
|
3761
4568
|
if (i >= 0) toolIndex = i;
|
|
@@ -3763,7 +4570,8 @@ function createTestRoom(options) {
|
|
|
3763
4570
|
progress = null;
|
|
3764
4571
|
action("tool", t("act.tool", { weapon: toolName(id) }), { data: { weapon: id } });
|
|
3765
4572
|
fire("tool", id);
|
|
3766
|
-
|
|
4573
|
+
tool.draw();
|
|
4574
|
+
flushEvents(id, tool);
|
|
3767
4575
|
};
|
|
3768
4576
|
const cycleTool = (dir) => {
|
|
3769
4577
|
if (!loadout.length) return;
|
|
@@ -3995,8 +4803,16 @@ function createTestRoom(options) {
|
|
|
3995
4803
|
say(t("hud.reloading"));
|
|
3996
4804
|
}
|
|
3997
4805
|
break;
|
|
3998
|
-
case "KeyQ":
|
|
4806
|
+
case "KeyQ": {
|
|
3999
4807
|
tool.cycleMode();
|
|
4808
|
+
flushEvents(toolId, tool);
|
|
4809
|
+
break;
|
|
4810
|
+
}
|
|
4811
|
+
case "KeyX":
|
|
4812
|
+
clearToolFault();
|
|
4813
|
+
break;
|
|
4814
|
+
case "KeyB":
|
|
4815
|
+
toggleSafety();
|
|
4000
4816
|
break;
|
|
4001
4817
|
case "KeyZ":
|
|
4002
4818
|
tiltKey = 1;
|
|
@@ -4014,6 +4830,9 @@ function createTestRoom(options) {
|
|
|
4014
4830
|
case "KeyT":
|
|
4015
4831
|
toggleTap();
|
|
4016
4832
|
break;
|
|
4833
|
+
case "KeyY":
|
|
4834
|
+
explosives.detonateCharges();
|
|
4835
|
+
break;
|
|
4017
4836
|
case "KeyH": {
|
|
4018
4837
|
const w = (heldEntry ?? entryOf(inspectTarget()?.object ?? ""))?.item?.weapon;
|
|
4019
4838
|
if (w) equip(w);
|
|
@@ -4085,6 +4904,10 @@ function createTestRoom(options) {
|
|
|
4085
4904
|
let timeScale = 1;
|
|
4086
4905
|
let fastForward = false;
|
|
4087
4906
|
let hudAcc = 1;
|
|
4907
|
+
let metricsAcc = 0;
|
|
4908
|
+
let ownsLoop = false;
|
|
4909
|
+
const roomMetricIds = options.metrics === false ? [] : options.metrics ?? ROOM_METRICS;
|
|
4910
|
+
const roomMetrics = createRoomMetrics({ gl, destruction: renderer, fluids: fluidView, ids: roomMetricIds });
|
|
4088
4911
|
let qualityName = quality;
|
|
4089
4912
|
const syncContainers = (sdt) => {
|
|
4090
4913
|
for (const e of entries.values()) {
|
|
@@ -4124,7 +4947,7 @@ function createTestRoom(options) {
|
|
|
4124
4947
|
const reps = fastForward ? 10 : 1;
|
|
4125
4948
|
updateAnimations(dt);
|
|
4126
4949
|
updateItemUse(dt);
|
|
4127
|
-
|
|
4950
|
+
explosives.update(dt);
|
|
4128
4951
|
syncContainers(dt);
|
|
4129
4952
|
for (let i = 0; i < reps; i++) {
|
|
4130
4953
|
world.update(dt);
|
|
@@ -4135,9 +4958,10 @@ function createTestRoom(options) {
|
|
|
4135
4958
|
renderer.update(dt);
|
|
4136
4959
|
updateImpacts();
|
|
4137
4960
|
fluidView.update(dt);
|
|
4138
|
-
for (const s of tools
|
|
4139
|
-
|
|
4140
|
-
|
|
4961
|
+
for (const [id, s] of tools) {
|
|
4962
|
+
s.update(dt);
|
|
4963
|
+
flushEvents(id, s);
|
|
4964
|
+
}
|
|
4141
4965
|
for (const update2 of frameListeners) update2(dt);
|
|
4142
4966
|
for (const tr of tracers) {
|
|
4143
4967
|
if (tr.life > 0) {
|
|
@@ -4148,6 +4972,7 @@ function createTestRoom(options) {
|
|
|
4148
4972
|
flush();
|
|
4149
4973
|
};
|
|
4150
4974
|
const update = (dt) => {
|
|
4975
|
+
if (!ownsLoop) roomMetrics.frame(dt);
|
|
4151
4976
|
player.update(dt);
|
|
4152
4977
|
if (tiltKey || heldTilt > 0) {
|
|
4153
4978
|
heldTilt = Math.max(0, Math.min(2.4, heldTilt + (tiltKey ? 2.2 : -3) * dt));
|
|
@@ -4166,6 +4991,11 @@ function createTestRoom(options) {
|
|
|
4166
4991
|
hudAcc = 0;
|
|
4167
4992
|
ui.hud(hudState());
|
|
4168
4993
|
}
|
|
4994
|
+
metricsAcc += dt;
|
|
4995
|
+
if (metricsAcc >= METRICS_INTERVAL_S && ui) {
|
|
4996
|
+
metricsAcc = 0;
|
|
4997
|
+
if (ui.benchVisible) ui.metrics(metricRows());
|
|
4998
|
+
}
|
|
4169
4999
|
};
|
|
4170
5000
|
const hudState = () => {
|
|
4171
5001
|
const page = Math.floor(toolIndex / 10);
|
|
@@ -4281,15 +5111,82 @@ function createTestRoom(options) {
|
|
|
4281
5111
|
log.add({ channel: "check", type: ok ? "pass" : "fail", text: `${ok ? "\u2713" : "\u2717"} ${text}`, itemId, data: { ok, ...data } });
|
|
4282
5112
|
};
|
|
4283
5113
|
const modules = /* @__PURE__ */ new Map();
|
|
4284
|
-
const detachers =
|
|
5114
|
+
const detachers = /* @__PURE__ */ new Map();
|
|
5115
|
+
const benches = /* @__PURE__ */ new Map();
|
|
5116
|
+
let activeBench = null;
|
|
5117
|
+
const lastBench = () => [...benches.keys()].pop() ?? null;
|
|
5118
|
+
const benchView = () => {
|
|
5119
|
+
const store = activeBench ? benches.get(activeBench) : void 0;
|
|
5120
|
+
if (!store) return null;
|
|
5121
|
+
return { title: modules.get(store.moduleId)?.name ?? store.moduleId, controls: store.controls(), layers: store.layers() };
|
|
5122
|
+
};
|
|
5123
|
+
const METRIC_UNAVAILABLE = "n/a";
|
|
5124
|
+
const metricText = (value, digits, unit) => typeof value === "number" ? `${num(value, digits)}${unit ? ` ${unit}` : ""}` : value ?? METRIC_UNAVAILABLE;
|
|
5125
|
+
const reportedFailures = /* @__PURE__ */ new Set();
|
|
5126
|
+
const reportOnce = (key, text) => {
|
|
5127
|
+
if (reportedFailures.has(key)) return;
|
|
5128
|
+
reportedFailures.add(key);
|
|
5129
|
+
host.log(text);
|
|
5130
|
+
};
|
|
5131
|
+
const failureText = (error) => error instanceof Error ? error.message : String(error);
|
|
5132
|
+
const moduleMetrics = () => {
|
|
5133
|
+
const store = activeBench ? benches.get(activeBench) : void 0;
|
|
5134
|
+
const out = {};
|
|
5135
|
+
for (const metric of store?.metrics ?? []) {
|
|
5136
|
+
try {
|
|
5137
|
+
out[metric.id] = metric.read();
|
|
5138
|
+
} catch (error) {
|
|
5139
|
+
out[metric.id] = METRIC_UNAVAILABLE;
|
|
5140
|
+
reportOnce(`${store?.moduleId}:metric:${metric.id}`, t("mod.metric_failed", { name: modules.get(store?.moduleId ?? "")?.name ?? "", metric: metric.id, error: failureText(error) }));
|
|
5141
|
+
}
|
|
5142
|
+
}
|
|
5143
|
+
return out;
|
|
5144
|
+
};
|
|
5145
|
+
const metricRows = () => {
|
|
5146
|
+
const sampled = roomMetrics.sample();
|
|
5147
|
+
const room2 = roomMetricIds.flatMap((id) => {
|
|
5148
|
+
const value = sampled[id];
|
|
5149
|
+
return value === void 0 ? [] : [{ id, name: t(`metric.${id}`), text: num(value, id === "frame_ms" ? 1 : 0) }];
|
|
5150
|
+
});
|
|
5151
|
+
const values = moduleMetrics();
|
|
5152
|
+
const declared = (activeBench ? benches.get(activeBench)?.metrics : void 0) ?? [];
|
|
5153
|
+
const module = declared.map((metric) => {
|
|
5154
|
+
const value = values[metric.id];
|
|
5155
|
+
const ratio = metric.max && typeof value === "number" ? value / metric.max : void 0;
|
|
5156
|
+
return { id: metric.id, name: metric.name, text: metricText(value, metric.digits ?? 0, metric.unit), ...ratio === void 0 ? {} : { ratio } };
|
|
5157
|
+
});
|
|
5158
|
+
return { room: room2, module };
|
|
5159
|
+
};
|
|
5160
|
+
const showBench = () => {
|
|
5161
|
+
if (!ui) return;
|
|
5162
|
+
ui.bench(benchView());
|
|
5163
|
+
ui.metrics(metricRows());
|
|
5164
|
+
};
|
|
4285
5165
|
const hostListeners = /* @__PURE__ */ new Set();
|
|
4286
5166
|
const frameListeners = /* @__PURE__ */ new Set();
|
|
4287
|
-
let reloadingSeen = false;
|
|
4288
5167
|
function emitHost(event) {
|
|
4289
5168
|
for (const listener of hostListeners) listener(event);
|
|
4290
5169
|
}
|
|
5170
|
+
function flushEvents(id, state, recoil) {
|
|
5171
|
+
const phase = state.phase;
|
|
5172
|
+
for (const event of state.drain()) {
|
|
5173
|
+
const hostEvent = toHostEvent(id, event, { ...recoil ? { recoil } : {}, phase });
|
|
5174
|
+
if (hostEvent) emitHost(hostEvent);
|
|
5175
|
+
}
|
|
5176
|
+
}
|
|
4291
5177
|
function startReload() {
|
|
4292
|
-
|
|
5178
|
+
tool.reload();
|
|
5179
|
+
flushEvents(toolId, tool);
|
|
5180
|
+
}
|
|
5181
|
+
function clearToolFault() {
|
|
5182
|
+
if (!tool.clearFault()) return;
|
|
5183
|
+
flushEvents(toolId, tool);
|
|
5184
|
+
say(t("hud.clearing"));
|
|
5185
|
+
}
|
|
5186
|
+
function toggleSafety() {
|
|
5187
|
+
tool.setSafety(!tool.safety);
|
|
5188
|
+
flushEvents(toolId, tool);
|
|
5189
|
+
say(t(tool.safety ? "hud.safety_on" : "hud.safety_off"));
|
|
4293
5190
|
}
|
|
4294
5191
|
const host = {
|
|
4295
5192
|
THREE,
|
|
@@ -4297,7 +5194,31 @@ function createTestRoom(options) {
|
|
|
4297
5194
|
camera,
|
|
4298
5195
|
locale,
|
|
4299
5196
|
groundAt,
|
|
4300
|
-
tool: () => ({
|
|
5197
|
+
tool: () => ({
|
|
5198
|
+
weapon: toolId,
|
|
5199
|
+
category: tool.def.category,
|
|
5200
|
+
ammo: tool.ammo,
|
|
5201
|
+
capacity: tool.capacity,
|
|
5202
|
+
reserve: tool.reserve,
|
|
5203
|
+
reloading: tool.reloading,
|
|
5204
|
+
mode: tool.mode,
|
|
5205
|
+
...tool.fault ? { fault: tool.fault } : {},
|
|
5206
|
+
safety: tool.safety,
|
|
5207
|
+
heat: tool.heatLevel
|
|
5208
|
+
}),
|
|
5209
|
+
configureTool(settings) {
|
|
5210
|
+
const condition = settings[SETTING_IDS.condition];
|
|
5211
|
+
const ammoQuality = settings[SETTING_IDS.ammoQuality];
|
|
5212
|
+
if (condition !== void 0) toolSettings.condition = condition;
|
|
5213
|
+
if (ammoQuality !== void 0) toolSettings.ammoQuality = ammoQuality;
|
|
5214
|
+
const fuseMetres = settings[SETTING_IDS.fuseLength] === void 0 ? void 0 : Number(settings[SETTING_IDS.fuseLength]);
|
|
5215
|
+
if (fuseMetres !== void 0 && fuseMetres > 0) fuseLength = fuseMetres;
|
|
5216
|
+
for (const state of tools.values()) {
|
|
5217
|
+
if (condition !== void 0) state.setCondition(condition);
|
|
5218
|
+
if (ammoQuality !== void 0) state.setAmmoQuality(ammoQuality);
|
|
5219
|
+
if (fuseMetres !== void 0 && fuseMetres > 0) state.setFuseLength(fuseMetres);
|
|
5220
|
+
}
|
|
5221
|
+
},
|
|
4301
5222
|
on(listener) {
|
|
4302
5223
|
hostListeners.add(listener);
|
|
4303
5224
|
return () => hostListeners.delete(listener);
|
|
@@ -4310,6 +5231,23 @@ function createTestRoom(options) {
|
|
|
4310
5231
|
action("module_note", text, { data });
|
|
4311
5232
|
}
|
|
4312
5233
|
};
|
|
5234
|
+
const lendBench = (store) => ({
|
|
5235
|
+
value: store.value,
|
|
5236
|
+
layer: store.layer,
|
|
5237
|
+
onChange: store.onChange,
|
|
5238
|
+
patch: store.patch,
|
|
5239
|
+
look: (eye, target) => room.look(eye, target)
|
|
5240
|
+
});
|
|
5241
|
+
const checkModuleBench = (mod, fail) => {
|
|
5242
|
+
const store = createBenchStore(mod, locale, (error) => reportOnce(`${mod.id}:listener`, t("mod.listener_failed", { name: mod.name, error: failureText(error) })));
|
|
5243
|
+
const { controls, layers, metrics } = store.declared;
|
|
5244
|
+
if (controls.length + layers.length + metrics.length === 0) return null;
|
|
5245
|
+
const r = checkBench(store.declared);
|
|
5246
|
+
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("; ") });
|
|
5247
|
+
moduleCheck(r.ok, text, { module: mod.id, check: "bench", problems: r.problems });
|
|
5248
|
+
for (const p of r.problems) fail(`bench: ${p}`);
|
|
5249
|
+
return r.ok ? store : null;
|
|
5250
|
+
};
|
|
4313
5251
|
const loadModule = (mod) => {
|
|
4314
5252
|
const report = { module: mod.id, items: [], scenarios: [], ok: true, problems: [] };
|
|
4315
5253
|
const fail = (problem) => {
|
|
@@ -4374,8 +5312,29 @@ function createTestRoom(options) {
|
|
|
4374
5312
|
report.scenarios.push(sc.id);
|
|
4375
5313
|
}
|
|
4376
5314
|
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 } });
|
|
5315
|
+
const bench = checkModuleBench(mod, fail);
|
|
5316
|
+
detachers.get(mod.id)?.();
|
|
5317
|
+
detachers.delete(mod.id);
|
|
5318
|
+
const previous = benches.get(mod.id);
|
|
5319
|
+
const kept = previous?.encode();
|
|
5320
|
+
previous?.dispose();
|
|
5321
|
+
benches.delete(mod.id);
|
|
5322
|
+
for (const key of [...reportedFailures]) if (key.startsWith(`${mod.id}:`)) reportedFailures.delete(key);
|
|
4377
5323
|
modules.set(mod.id, mod);
|
|
4378
|
-
if (
|
|
5324
|
+
if (bench) {
|
|
5325
|
+
const seed = options.bench?.state?.[mod.id];
|
|
5326
|
+
const restored = kept ?? (typeof seed === "string" ? seed : void 0);
|
|
5327
|
+
if (restored !== void 0) bench.restore(restored);
|
|
5328
|
+
benches.set(mod.id, bench);
|
|
5329
|
+
bench.watch(() => {
|
|
5330
|
+
options.bench?.onChange?.(mod.id, bench.encode());
|
|
5331
|
+
if (activeBench === mod.id) ui?.bench(benchView());
|
|
5332
|
+
});
|
|
5333
|
+
if (!activeBench || !benches.has(activeBench)) activeBench = mod.id;
|
|
5334
|
+
} else if (activeBench === mod.id) activeBench = lastBench();
|
|
5335
|
+
if (mod.attach) detachers.set(mod.id, mod.attach(bench ? { ...host, bench: lendBench(bench) } : host));
|
|
5336
|
+
bench?.sendInitial();
|
|
5337
|
+
showBench();
|
|
4379
5338
|
ui?.refresh();
|
|
4380
5339
|
return report;
|
|
4381
5340
|
};
|
|
@@ -4454,6 +5413,8 @@ function createTestRoom(options) {
|
|
|
4454
5413
|
const mod = [...modules.values()].find((m) => m.id === area || m.group === area);
|
|
4455
5414
|
const label = mod?.group ?? S.groups[area] ?? area;
|
|
4456
5415
|
ui?.showGroup(label);
|
|
5416
|
+
activeBench = mod && benches.has(mod.id) ? mod.id : lastBench();
|
|
5417
|
+
showBench();
|
|
4457
5418
|
const layout = mod?.area?.(locale);
|
|
4458
5419
|
if (layout) return layArea(layout);
|
|
4459
5420
|
const items = catalog.filter((item) => item.group === label);
|
|
@@ -4495,7 +5456,7 @@ function createTestRoom(options) {
|
|
|
4495
5456
|
stepSim(FIXED_DT);
|
|
4496
5457
|
t0 += FIXED_DT;
|
|
4497
5458
|
}
|
|
4498
|
-
|
|
5459
|
+
releaseTrigger();
|
|
4499
5460
|
},
|
|
4500
5461
|
grab,
|
|
4501
5462
|
throwHeld,
|
|
@@ -4512,6 +5473,15 @@ function createTestRoom(options) {
|
|
|
4512
5473
|
return [...entries.values()].filter((e) => !e.arena).map((e) => e.so);
|
|
4513
5474
|
},
|
|
4514
5475
|
update,
|
|
5476
|
+
waitUntilReady(maxSeconds = 5) {
|
|
5477
|
+
let waited = 0;
|
|
5478
|
+
while (tool.phase && waited < maxSeconds) {
|
|
5479
|
+
update(FIXED_DT);
|
|
5480
|
+
waited += FIXED_DT;
|
|
5481
|
+
}
|
|
5482
|
+
update(FIXED_DT);
|
|
5483
|
+
return waited + FIXED_DT;
|
|
5484
|
+
},
|
|
4515
5485
|
on(type, fn) {
|
|
4516
5486
|
listeners[type].add(fn);
|
|
4517
5487
|
return () => listeners[type].delete(fn);
|
|
@@ -4545,8 +5515,18 @@ function createTestRoom(options) {
|
|
|
4545
5515
|
loadModule,
|
|
4546
5516
|
animations: animationsOf,
|
|
4547
5517
|
playAnimation,
|
|
5518
|
+
bench(moduleId) {
|
|
5519
|
+
const id = moduleId ?? activeBench;
|
|
5520
|
+
return (id ? benches.get(id) : void 0) ?? null;
|
|
5521
|
+
},
|
|
5522
|
+
metrics() {
|
|
5523
|
+
return { room: roomMetrics.sample(), module: moduleMetrics() };
|
|
5524
|
+
},
|
|
4548
5525
|
dispose() {
|
|
4549
|
-
for (const detach of detachers.
|
|
5526
|
+
for (const detach of detachers.values()) detach();
|
|
5527
|
+
detachers.clear();
|
|
5528
|
+
for (const store of benches.values()) store.dispose();
|
|
5529
|
+
benches.clear();
|
|
4550
5530
|
cancelAnimationFrame?.(raf);
|
|
4551
5531
|
window.removeEventListener("resize", onResize);
|
|
4552
5532
|
player.dispose();
|
|
@@ -4554,7 +5534,7 @@ function createTestRoom(options) {
|
|
|
4554
5534
|
renderer.dispose();
|
|
4555
5535
|
fluidView.dispose();
|
|
4556
5536
|
for (const tr of tracers) tr.line.removeFromParent();
|
|
4557
|
-
|
|
5537
|
+
explosives.clear();
|
|
4558
5538
|
if (ownRenderer) {
|
|
4559
5539
|
gl.dispose();
|
|
4560
5540
|
dom.remove();
|
|
@@ -4617,19 +5597,33 @@ function createTestRoom(options) {
|
|
|
4617
5597
|
onPlay: (objectId, clipId) => {
|
|
4618
5598
|
playAnimation(clipId, objectId);
|
|
4619
5599
|
},
|
|
4620
|
-
onResize: resize
|
|
5600
|
+
onResize: resize,
|
|
5601
|
+
onControl: (id, value) => {
|
|
5602
|
+
if (activeBench) benches.get(activeBench)?.set(id, value);
|
|
5603
|
+
},
|
|
5604
|
+
onLayer: (id, on) => {
|
|
5605
|
+
if (activeBench) benches.get(activeBench)?.setLayer(id, on);
|
|
5606
|
+
},
|
|
5607
|
+
onAction: (id) => {
|
|
5608
|
+
if (activeBench) benches.get(activeBench)?.act(id);
|
|
5609
|
+
}
|
|
4621
5610
|
});
|
|
4622
5611
|
ui.hud(hudState());
|
|
5612
|
+
showBench();
|
|
4623
5613
|
}
|
|
4624
5614
|
let raf = 0;
|
|
4625
5615
|
if (ownRenderer && typeof requestAnimationFrame === "function") {
|
|
4626
5616
|
let last = performance.now();
|
|
5617
|
+
ownsLoop = true;
|
|
4627
5618
|
const loop = (now) => {
|
|
4628
5619
|
raf = requestAnimationFrame(loop);
|
|
4629
|
-
const
|
|
5620
|
+
const realDelta = (now - last) / 1e3;
|
|
5621
|
+
roomMetrics.frame(realDelta);
|
|
5622
|
+
const dt = Math.min(0.05, realDelta);
|
|
4630
5623
|
last = now;
|
|
4631
5624
|
if (!scenarioRunning) update(dt);
|
|
4632
5625
|
gl.render(scene, camera);
|
|
5626
|
+
roomMetrics.rendered();
|
|
4633
5627
|
};
|
|
4634
5628
|
raf = requestAnimationFrame(loop);
|
|
4635
5629
|
}
|
|
@@ -4651,7 +5645,9 @@ export {
|
|
|
4651
5645
|
buildFromSpec,
|
|
4652
5646
|
createLog,
|
|
4653
5647
|
createTestRoom,
|
|
5648
|
+
decodeBenchState,
|
|
4654
5649
|
demoItems,
|
|
5650
|
+
encodeBenchState,
|
|
4655
5651
|
locales,
|
|
4656
5652
|
makeScenarios,
|
|
4657
5653
|
resolveStrings,
|