@adatechnology/game-test-room 0.3.0 → 0.5.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 +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.global.js +12374 -494
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +573 -113
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -249,6 +249,15 @@ var en = {
|
|
|
249
249
|
"act.heat.air": "{weapon}: nothing in range",
|
|
250
250
|
"act.throw.grenade": "{weapon}: thrown, fuse {fuse} s",
|
|
251
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",
|
|
252
261
|
"act.explode": "{weapon}: exploded, {energy} J within {radius} m, {n} objects in range",
|
|
253
262
|
"act.molotov_fire": "{weapon}: fire on the ground, radius {radius} m for {seconds} s",
|
|
254
263
|
"act.tool.none": "{weapon}: nothing in reach",
|
|
@@ -274,6 +283,15 @@ var en = {
|
|
|
274
283
|
"hud.reloading": "Reloading...",
|
|
275
284
|
"hud.empty": "Empty",
|
|
276
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",
|
|
277
295
|
"hud.nothing_to_grab": "Nothing to grab here",
|
|
278
296
|
"hud.cannot_grab": "{name} cannot be picked up",
|
|
279
297
|
"hud.too_heavy": "{name} is too heavy ({kg} kg)",
|
|
@@ -284,7 +302,7 @@ var en = {
|
|
|
284
302
|
"hud.throw": "throw",
|
|
285
303
|
"hud.tilt": "tilt (hold)",
|
|
286
304
|
"hud.equip": "equip {weapon}",
|
|
287
|
-
"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",
|
|
288
306
|
"ui.log": "Behaviour log",
|
|
289
307
|
"ui.filter.action": "actions",
|
|
290
308
|
"ui.filter.event": "events",
|
|
@@ -556,6 +574,15 @@ var ptBR = {
|
|
|
556
574
|
"act.heat.air": "{weapon}: nada ao alcance",
|
|
557
575
|
"act.throw.grenade": "{weapon}: arremessada, pavio de {fuse} s",
|
|
558
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",
|
|
559
586
|
"act.explode": "{weapon}: explodiu, {energy} J em {radius} m, {n} objetos ao alcance",
|
|
560
587
|
"act.molotov_fire": "{weapon}: fogo no ch\xE3o, raio de {radius} m por {seconds} s",
|
|
561
588
|
"act.tool.none": "{weapon}: nada ao alcance",
|
|
@@ -581,6 +608,15 @@ var ptBR = {
|
|
|
581
608
|
"hud.reloading": "Recarregando...",
|
|
582
609
|
"hud.empty": "Sem muni\xE7\xE3o",
|
|
583
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",
|
|
584
620
|
"hud.nothing_to_grab": "Nada para pegar aqui",
|
|
585
621
|
"hud.cannot_grab": "{name} n\xE3o d\xE1 para pegar",
|
|
586
622
|
"hud.too_heavy": "{name} \xE9 pesado demais ({kg} kg)",
|
|
@@ -591,7 +627,7 @@ var ptBR = {
|
|
|
591
627
|
"hud.throw": "arremessar",
|
|
592
628
|
"hud.tilt": "inclinar (segurar)",
|
|
593
629
|
"hud.equip": "equipar {weapon}",
|
|
594
|
-
"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",
|
|
595
631
|
"ui.log": "Registro de comportamento",
|
|
596
632
|
"ui.filter.action": "a\xE7\xF5es",
|
|
597
633
|
"ui.filter.event": "eventos",
|
|
@@ -1300,6 +1336,354 @@ function createBenchStore(module, locale, onListenerError) {
|
|
|
1300
1336
|
return store;
|
|
1301
1337
|
}
|
|
1302
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, ...event.suppressed ? { suppressed: true } : {} };
|
|
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
|
+
case "attachment":
|
|
1675
|
+
return { type: "attachment", weapon, action: event.action, attachment: event.id, mount: event.mount, ...event.action === "break" ? {} : durationOf(phase, "fit") };
|
|
1676
|
+
case "toggle":
|
|
1677
|
+
return { type: "toggle", weapon, attachment: event.id, on: event.on };
|
|
1678
|
+
case "stock":
|
|
1679
|
+
return { type: "stock", weapon, state: event.state, kind: event.kind, ...durationOf(phase, "stock") };
|
|
1680
|
+
case "underbarrel":
|
|
1681
|
+
return { type: "underbarrel", weapon, on: event.on, ...durationOf(phase, "switch") };
|
|
1682
|
+
default:
|
|
1683
|
+
return null;
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
|
|
1303
1687
|
// src/log.ts
|
|
1304
1688
|
var MAX_ENTRIES = 5e3;
|
|
1305
1689
|
var pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
@@ -1991,7 +2375,7 @@ function createNarrator(c) {
|
|
|
1991
2375
|
|
|
1992
2376
|
// src/player.ts
|
|
1993
2377
|
var EYE = 1.7;
|
|
1994
|
-
var
|
|
2378
|
+
var PLAYER_RADIUS = 0.3;
|
|
1995
2379
|
var WALK = 3.4;
|
|
1996
2380
|
var RUN = 6.2;
|
|
1997
2381
|
var JUMP = 4.6;
|
|
@@ -2016,8 +2400,8 @@ function createPlayer(THREE, o) {
|
|
|
2016
2400
|
};
|
|
2017
2401
|
apply();
|
|
2018
2402
|
const blocked = (x, z) => {
|
|
2019
|
-
if (Math.abs(x) > o.limit -
|
|
2020
|
-
for (const c of o.colliders) if (x > c[0] -
|
|
2403
|
+
if (Math.abs(x) > o.limit - PLAYER_RADIUS || Math.abs(z) > o.limit - PLAYER_RADIUS) return true;
|
|
2404
|
+
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;
|
|
2021
2405
|
return false;
|
|
2022
2406
|
};
|
|
2023
2407
|
const move = (dx, dz) => {
|
|
@@ -2239,6 +2623,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2239
2623
|
sc("steel_through_hole", ({ room, step, expect }) => {
|
|
2240
2624
|
const id = at(room, "steel_sheet", 0, -4);
|
|
2241
2625
|
room.setTool("pistol_9mm");
|
|
2626
|
+
room.waitUntilReady();
|
|
2242
2627
|
look(room, [0, 1.1, -1.5], [0, 1.1, -4]);
|
|
2243
2628
|
room.use();
|
|
2244
2629
|
step(0.2);
|
|
@@ -2253,6 +2638,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2253
2638
|
sc("concrete_stops_bullet", ({ room, step, expect }) => {
|
|
2254
2639
|
const id = at(room, "concrete_block", 0, -4);
|
|
2255
2640
|
room.setTool("pistol_9mm");
|
|
2641
|
+
room.waitUntilReady();
|
|
2256
2642
|
look(room, [0, 0.5, -1.5], [0, 0.2, -3.8]);
|
|
2257
2643
|
room.use();
|
|
2258
2644
|
step(0.2);
|
|
@@ -2268,6 +2654,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2268
2654
|
sc("hammer_dents_drum", ({ room, step, expect }) => {
|
|
2269
2655
|
const id = at(room, "steel_drum", 0, -4);
|
|
2270
2656
|
room.setTool("hammer");
|
|
2657
|
+
room.waitUntilReady();
|
|
2271
2658
|
look(room, [0, 0.6, -3.35], [0, 0.5, -4]);
|
|
2272
2659
|
let swings = 0;
|
|
2273
2660
|
for (; swings < 8 && (part(room, id, "drum_body")?.dents.length ?? 0) === 0; swings++) {
|
|
@@ -2284,6 +2671,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2284
2671
|
sc("glass_shatters", ({ room, step, expect }) => {
|
|
2285
2672
|
const id = at(room, "glass_pane", 0, -4);
|
|
2286
2673
|
room.setTool("pistol_9mm");
|
|
2674
|
+
room.waitUntilReady();
|
|
2287
2675
|
look(room, [0, 1.3, -1.5], [0, 1.05, -4]);
|
|
2288
2676
|
let shots = 0;
|
|
2289
2677
|
for (; shots < 10 && part(room, id, "pane")?.status !== "broken"; shots++) {
|
|
@@ -2298,6 +2686,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2298
2686
|
sc("shotgun_splinters_crate", ({ room, step, expect }) => {
|
|
2299
2687
|
const id = at(room, "wooden_crate", 0, -4);
|
|
2300
2688
|
room.setTool("shotgun_pump_12");
|
|
2689
|
+
room.waitUntilReady();
|
|
2301
2690
|
look(room, [0, 0.6, -1.8], [0, 0.25, -3.77]);
|
|
2302
2691
|
let shots = 0;
|
|
2303
2692
|
const splinters = () => room.log.entries({ type: "fracture", objectId: id }).filter((e) => e.data?.style === "splinter").length;
|
|
@@ -2331,6 +2720,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2331
2720
|
const id = at(room, "chair", 0, -4);
|
|
2332
2721
|
const before = room.renderer.pieces().length;
|
|
2333
2722
|
room.setTool("kick");
|
|
2723
|
+
room.waitUntilReady();
|
|
2334
2724
|
let kicks = 0;
|
|
2335
2725
|
for (; kicks < 8 && room.renderer.pieces().length <= before && part(room, id, "leg_1"); kicks++) {
|
|
2336
2726
|
const st = room.world.state(id);
|
|
@@ -2347,6 +2737,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2347
2737
|
sc("screwdriver_shelf_board", ({ room, step, expect }) => {
|
|
2348
2738
|
const id = at(room, "shelf", 0, -4);
|
|
2349
2739
|
room.setTool("screwdriver");
|
|
2740
|
+
room.waitUntilReady();
|
|
2350
2741
|
look(room, [0, 0.8, -3.5], [0, 0.75, -3.86]);
|
|
2351
2742
|
const t0 = room.log.entries().length ? room.log.entries().slice(-1)[0].t : 0;
|
|
2352
2743
|
room.use(5);
|
|
@@ -2360,6 +2751,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2360
2751
|
sc("wrong_tool_refused", ({ room, expect }) => {
|
|
2361
2752
|
const id = at(room, "steel_sheet", 0, -4);
|
|
2362
2753
|
room.setTool("screwdriver");
|
|
2754
|
+
room.waitUntilReady();
|
|
2363
2755
|
look(room, [0.2, 1.05, -3.6], [0.34, 1, -4]);
|
|
2364
2756
|
room.use(0.5);
|
|
2365
2757
|
const refused = count(room, "tool_refused");
|
|
@@ -2369,6 +2761,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2369
2761
|
sc("lighter_burns_crate", ({ room, step, expect }) => {
|
|
2370
2762
|
const id = at(room, "wooden_crate", 0, -4);
|
|
2371
2763
|
room.setTool("lighter");
|
|
2764
|
+
room.waitUntilReady();
|
|
2372
2765
|
look(room, [0, 0.3, -3.65], [0, 0.25, -3.76]);
|
|
2373
2766
|
room.use(5);
|
|
2374
2767
|
const lit = anyBurning(room, id);
|
|
@@ -2391,6 +2784,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2391
2784
|
const melt0 = part(room, drum2, "drum_body")?.melt ?? 0;
|
|
2392
2785
|
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 });
|
|
2393
2786
|
room.setTool("cutting_torch");
|
|
2787
|
+
room.waitUntilReady();
|
|
2394
2788
|
look(room, [-0.15, 0.55, -3.55], [-0.15, 0.5, -3.72]);
|
|
2395
2789
|
room.use(20);
|
|
2396
2790
|
const p = part(room, drum2, "drum_body");
|
|
@@ -2399,6 +2793,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2399
2793
|
sc("bucket_melts_drips", ({ room, step, expect }) => {
|
|
2400
2794
|
const id = at(room, "plastic_bucket", 0, -4);
|
|
2401
2795
|
room.setTool("flamethrower");
|
|
2796
|
+
room.waitUntilReady();
|
|
2402
2797
|
look(room, [0, 0.5, -2.4], [0, 0.15, -4]);
|
|
2403
2798
|
room.use(2.5);
|
|
2404
2799
|
step(10);
|
|
@@ -2473,6 +2868,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2473
2868
|
sc("jerrycan_leaks_fuel", ({ room, step, expect }) => {
|
|
2474
2869
|
const id = at(room, "jerrycan", 0, -3.5);
|
|
2475
2870
|
room.setTool("pistol_9mm");
|
|
2871
|
+
room.waitUntilReady();
|
|
2476
2872
|
look(room, [0, 0.4, -1.5], [0, 0.12, -3.43]);
|
|
2477
2873
|
room.use();
|
|
2478
2874
|
step(1.5);
|
|
@@ -2487,6 +2883,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2487
2883
|
const id = at(room, "jerrycan", 0, -3.5);
|
|
2488
2884
|
const crate2 = at(room, "wooden_crate", 0.9, -3.2);
|
|
2489
2885
|
room.setTool("pistol_9mm");
|
|
2886
|
+
room.waitUntilReady();
|
|
2490
2887
|
look(room, [0, 0.4, -1.5], [0, 0.12, -3.43]);
|
|
2491
2888
|
room.use();
|
|
2492
2889
|
step(6);
|
|
@@ -2494,6 +2891,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2494
2891
|
expect(!!stain, t("sc.fuel_stain_burns.stain", { stain: yn(!!stain) }));
|
|
2495
2892
|
if (!stain) return;
|
|
2496
2893
|
room.setTool("lighter");
|
|
2894
|
+
room.waitUntilReady();
|
|
2497
2895
|
look(room, [stain.center[0], 0.1, stain.center[2] + 0.06], [stain.center[0], 0, stain.center[2]]);
|
|
2498
2896
|
room.use(1);
|
|
2499
2897
|
step(4);
|
|
@@ -2529,6 +2927,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2529
2927
|
const before = anyBurning(room, id);
|
|
2530
2928
|
expect(before, t("sc.extinguisher_puts_out_fire.burning", { burning: yn(before) }));
|
|
2531
2929
|
room.setTool("extinguisher");
|
|
2930
|
+
room.waitUntilReady();
|
|
2532
2931
|
look(room, [0, 0.9, -2.2], [0, 0.25, -4]);
|
|
2533
2932
|
room.use(2.5);
|
|
2534
2933
|
step(1);
|
|
@@ -3397,10 +3796,14 @@ function createTestRoom(options) {
|
|
|
3397
3796
|
const tools = /* @__PURE__ */ new Map();
|
|
3398
3797
|
let toolIndex = Math.max(0, loadout.indexOf("hand"));
|
|
3399
3798
|
let toolId = loadout[toolIndex] ?? "hand";
|
|
3799
|
+
const toolSettings = {};
|
|
3800
|
+
let fuseLength;
|
|
3801
|
+
const SETTING_IDS = { condition: "condition", ammoQuality: "ammo_quality", fuseLength: "fuse_length" };
|
|
3400
3802
|
const toolState = (id) => {
|
|
3401
3803
|
let s = tools.get(id);
|
|
3402
3804
|
if (!s) {
|
|
3403
|
-
s = arsenal.create(id, { seed: 7, reserve: 999 });
|
|
3805
|
+
s = arsenal.create(id, { seed: 7, reserve: 999, ...toolSettings });
|
|
3806
|
+
s.setFuseLength(fuseLength);
|
|
3404
3807
|
s.refill();
|
|
3405
3808
|
tools.set(id, s);
|
|
3406
3809
|
}
|
|
@@ -3501,6 +3904,7 @@ function createTestRoom(options) {
|
|
|
3501
3904
|
for (const ev of batch) {
|
|
3502
3905
|
if (ev.type === "leak") puncture(ev);
|
|
3503
3906
|
else if (ev.type === "ignite") igniteNear(ev.point, 0.9);
|
|
3907
|
+
else if (ev.type === "explode") emitHost({ type: "explosion", source: ev.object, point: ev.point, energy: ev.energy, radius: ev.radius, cause: "part" });
|
|
3504
3908
|
fire("event", ev);
|
|
3505
3909
|
options.onEvent?.(ev);
|
|
3506
3910
|
}
|
|
@@ -3521,7 +3925,7 @@ function createTestRoom(options) {
|
|
|
3521
3925
|
},
|
|
3522
3926
|
primaryUp: () => {
|
|
3523
3927
|
useHeld = false;
|
|
3524
|
-
|
|
3928
|
+
releaseTrigger();
|
|
3525
3929
|
},
|
|
3526
3930
|
secondary: () => {
|
|
3527
3931
|
if (heldEntry) throwHeld();
|
|
@@ -3678,8 +4082,7 @@ function createTestRoom(options) {
|
|
|
3678
4082
|
if (heldEntry || heldPiece) releaseHeld();
|
|
3679
4083
|
useItemPlayer = null;
|
|
3680
4084
|
playing.length = 0;
|
|
3681
|
-
|
|
3682
|
-
clearProjectileMeshes();
|
|
4085
|
+
explosives.clear();
|
|
3683
4086
|
fireZones.length = 0;
|
|
3684
4087
|
burningStains.length = 0;
|
|
3685
4088
|
for (const id of [...entries.keys()]) if (!entries.get(id)?.arena) removeEntry(id);
|
|
@@ -3915,7 +4318,6 @@ function createTestRoom(options) {
|
|
|
3915
4318
|
}
|
|
3916
4319
|
});
|
|
3917
4320
|
player.kick(recoil[0], recoil[1]);
|
|
3918
|
-
emitHost({ type: "shot", weapon: toolId, shots: shots.length, recoil, rounds: shots.filter((s) => s.pellet === 0).length, mode: tool.mode });
|
|
3919
4321
|
const f = first;
|
|
3920
4322
|
const d = f ? num(f.distance, 1) : "";
|
|
3921
4323
|
let text;
|
|
@@ -4063,100 +4465,46 @@ function createTestRoom(options) {
|
|
|
4063
4465
|
1
|
|
4064
4466
|
);
|
|
4065
4467
|
};
|
|
4066
|
-
const
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
};
|
|
4072
|
-
|
|
4073
|
-
const
|
|
4074
|
-
const
|
|
4075
|
-
const
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
scene
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
const results = world.explode(at, p.energy, p.radius, "player");
|
|
4100
|
-
renderer.explode(at, p.energy, p.radius);
|
|
4101
|
-
action("explode", t("act.explode", { weapon: w, energy: num(p.energy, 0), radius: num(p.radius, 1), n: results.length }), { data: { weapon: p.weapon, energyJ: p.energy, radius: p.radius, point: at } });
|
|
4102
|
-
if (p.fire) {
|
|
4103
|
-
fluids.splash("fuel", [at[0], Math.max(at[1], groundAt(at[0], at[2])), at[2]], 0.5);
|
|
4104
|
-
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 });
|
|
4105
|
-
renderer.emit("fire", at, [0, 1, 0], 24, 3);
|
|
4106
|
-
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 } });
|
|
4107
|
-
}
|
|
4108
|
-
};
|
|
4109
|
-
const updateProjectiles = (dt) => {
|
|
4110
|
-
for (let i = projectiles.length - 1; i >= 0; i--) {
|
|
4111
|
-
const p = projectiles[i];
|
|
4112
|
-
p.age += dt;
|
|
4113
|
-
p.vel.y -= 9.81 * dt;
|
|
4114
|
-
const speed = p.vel.length();
|
|
4115
|
-
const step2 = speed * dt;
|
|
4116
|
-
let burst = false;
|
|
4117
|
-
if (step2 > 1e-6) {
|
|
4118
|
-
const d = [p.vel.x / speed, p.vel.y / speed, p.vel.z / speed];
|
|
4119
|
-
const r = withoutHeld(() => world.raycast(v3(p.pos), d, step2 + 0.05));
|
|
4120
|
-
if (r && r.distance <= step2 + 0.04) {
|
|
4121
|
-
p.pos.set(r.point[0] - d[0] * 0.03, r.point[1] - d[1] * 0.03, r.point[2] - d[2] * 0.03);
|
|
4122
|
-
if (p.burstOnImpact) burst = true;
|
|
4123
|
-
else {
|
|
4124
|
-
const n = r.normal;
|
|
4125
|
-
const vn = p.vel.x * n[0] + p.vel.y * n[1] + p.vel.z * n[2];
|
|
4126
|
-
p.vel.x = (p.vel.x - 2 * vn * n[0]) * 0.35;
|
|
4127
|
-
p.vel.y = (p.vel.y - 2 * vn * n[1]) * 0.35;
|
|
4128
|
-
p.vel.z = (p.vel.z - 2 * vn * n[2]) * 0.35;
|
|
4129
|
-
}
|
|
4130
|
-
} else p.pos.addScaledVector(p.vel, dt);
|
|
4131
|
-
}
|
|
4132
|
-
const g = groundAt(p.pos.x, p.pos.z) + 0.045;
|
|
4133
|
-
if (p.pos.y < g) {
|
|
4134
|
-
p.pos.y = g;
|
|
4135
|
-
if (p.burstOnImpact) burst = true;
|
|
4136
|
-
else {
|
|
4137
|
-
p.vel.y = Math.abs(p.vel.y) * 0.35;
|
|
4138
|
-
p.vel.x *= 0.7;
|
|
4139
|
-
p.vel.z *= 0.7;
|
|
4140
|
-
}
|
|
4141
|
-
}
|
|
4142
|
-
p.mesh.position.copy(p.pos);
|
|
4143
|
-
p.mesh.rotation.x += dt * 8;
|
|
4144
|
-
p.fuse -= dt;
|
|
4145
|
-
if (burst || !p.burstOnImpact && p.fuse <= 0 || p.age > 30) {
|
|
4146
|
-
detonate(p, v3(p.pos), burst);
|
|
4147
|
-
p.mesh.removeFromParent();
|
|
4148
|
-
projMeshes.splice(projMeshes.indexOf(p.mesh), 1);
|
|
4149
|
-
projectiles.splice(i, 1);
|
|
4150
|
-
}
|
|
4151
|
-
}
|
|
4152
|
-
};
|
|
4468
|
+
const startFire = (weapon, at, fire2, impact) => {
|
|
4469
|
+
fluids.splash("fuel", [at[0], Math.max(at[1], groundAt(at[0], at[2])), at[2]], 0.5);
|
|
4470
|
+
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 });
|
|
4471
|
+
renderer.emit("fire", at, [0, 1, 0], 24, 3);
|
|
4472
|
+
action("molotov_fire", t("act.molotov_fire", { weapon: toolName(weapon), seconds: num(fire2.seconds, 0), radius: num(fire2.radius, 1) }), { data: { weapon, impact } });
|
|
4473
|
+
};
|
|
4474
|
+
function crossingBodies() {
|
|
4475
|
+
const bodies = [...entries.values()].filter((e) => !e.arena).map((e) => ({ id: e.so.objectId, box: new THREE.Box3().setFromObject(e.so.root) }));
|
|
4476
|
+
const { x, y, z } = camera.position;
|
|
4477
|
+
const feet = groundAt(x, z);
|
|
4478
|
+
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)) });
|
|
4479
|
+
return bodies;
|
|
4480
|
+
}
|
|
4481
|
+
const explosives = createExplosives({
|
|
4482
|
+
THREE,
|
|
4483
|
+
scene,
|
|
4484
|
+
camera,
|
|
4485
|
+
world,
|
|
4486
|
+
renderer,
|
|
4487
|
+
arsenal,
|
|
4488
|
+
objects: () => [...entries.values()].filter((e) => !e.arena).map((e) => e.so.root),
|
|
4489
|
+
bodies: crossingBodies,
|
|
4490
|
+
toolId: () => toolId,
|
|
4491
|
+
toolName,
|
|
4492
|
+
withoutHeld,
|
|
4493
|
+
groundAt,
|
|
4494
|
+
translate: t,
|
|
4495
|
+
format: num,
|
|
4496
|
+
action,
|
|
4497
|
+
emitHost,
|
|
4498
|
+
startFire,
|
|
4499
|
+
say
|
|
4500
|
+
});
|
|
4153
4501
|
const useTool = (dt, held) => {
|
|
4154
4502
|
aim();
|
|
4155
4503
|
const cat = tool.def.category;
|
|
4156
4504
|
if (cat === "hand") return;
|
|
4157
4505
|
const res = tool.trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], dt, held, "player");
|
|
4506
|
+
flushEvents(toolId, tool, res.recoil);
|
|
4158
4507
|
if (res.status === "empty") {
|
|
4159
|
-
emitHost({ type: "empty", weapon: toolId });
|
|
4160
4508
|
if (!tool.reloading && tool.reserve > 0) startReload();
|
|
4161
4509
|
say(t(tool.reloading ? "hud.reloading" : "hud.empty"));
|
|
4162
4510
|
return;
|
|
@@ -4164,8 +4512,13 @@ function createTestRoom(options) {
|
|
|
4164
4512
|
if (res.status !== "ok") {
|
|
4165
4513
|
if (res.status === "reloading") say(t("hud.reloading"));
|
|
4166
4514
|
else if (res.status === "no_fuel") say(t("hud.no_fuel"));
|
|
4515
|
+
else if (res.status === "fault" && res.fault) say(t("hud.fault", { fault: t(`fault.${res.fault}`) }));
|
|
4516
|
+
else if (res.status === "safe") say(t("hud.safe"));
|
|
4167
4517
|
return;
|
|
4168
4518
|
}
|
|
4519
|
+
applyUses(res, dt);
|
|
4520
|
+
};
|
|
4521
|
+
const applyUses = (res, dt) => {
|
|
4169
4522
|
if (res.shots.length) fireShots(res.shots, res.recoil);
|
|
4170
4523
|
for (const u of res.uses) {
|
|
4171
4524
|
switch (u.kind) {
|
|
@@ -4179,7 +4532,13 @@ function createTestRoom(options) {
|
|
|
4179
4532
|
heatRays(u, "heat");
|
|
4180
4533
|
break;
|
|
4181
4534
|
case "throw":
|
|
4182
|
-
throwUse(u);
|
|
4535
|
+
explosives.throwUse(u);
|
|
4536
|
+
break;
|
|
4537
|
+
case "place":
|
|
4538
|
+
explosives.placeUse(u);
|
|
4539
|
+
break;
|
|
4540
|
+
case "backblast":
|
|
4541
|
+
explosives.backblastUse(u);
|
|
4183
4542
|
break;
|
|
4184
4543
|
case "tool":
|
|
4185
4544
|
toolWork(u, dt);
|
|
@@ -4192,6 +4551,16 @@ function createTestRoom(options) {
|
|
|
4192
4551
|
}
|
|
4193
4552
|
}
|
|
4194
4553
|
};
|
|
4554
|
+
const releaseTrigger = () => {
|
|
4555
|
+
if (!tool.cooking) {
|
|
4556
|
+
tool.release();
|
|
4557
|
+
return;
|
|
4558
|
+
}
|
|
4559
|
+
aim();
|
|
4560
|
+
const res = tool.release([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]]);
|
|
4561
|
+
flushEvents(toolId, tool);
|
|
4562
|
+
if (res) applyUses(res, 0);
|
|
4563
|
+
};
|
|
4195
4564
|
const kick = () => {
|
|
4196
4565
|
if (!arsenal.weapons.has("kick")) return;
|
|
4197
4566
|
aim();
|
|
@@ -4201,7 +4570,7 @@ function createTestRoom(options) {
|
|
|
4201
4570
|
};
|
|
4202
4571
|
const setTool = (id) => {
|
|
4203
4572
|
if (!arsenal.weapons.has(id)) throw new Error(`game-test-room: unknown weapon "${id}"`);
|
|
4204
|
-
|
|
4573
|
+
releaseTrigger();
|
|
4205
4574
|
toolId = id;
|
|
4206
4575
|
const i = loadout.indexOf(id);
|
|
4207
4576
|
if (i >= 0) toolIndex = i;
|
|
@@ -4209,7 +4578,8 @@ function createTestRoom(options) {
|
|
|
4209
4578
|
progress = null;
|
|
4210
4579
|
action("tool", t("act.tool", { weapon: toolName(id) }), { data: { weapon: id } });
|
|
4211
4580
|
fire("tool", id);
|
|
4212
|
-
|
|
4581
|
+
tool.draw();
|
|
4582
|
+
flushEvents(id, tool);
|
|
4213
4583
|
};
|
|
4214
4584
|
const cycleTool = (dir) => {
|
|
4215
4585
|
if (!loadout.length) return;
|
|
@@ -4442,11 +4812,16 @@ function createTestRoom(options) {
|
|
|
4442
4812
|
}
|
|
4443
4813
|
break;
|
|
4444
4814
|
case "KeyQ": {
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
if (mode !== before) emitHost({ type: "mode", weapon: toolId, mode });
|
|
4815
|
+
tool.cycleMode();
|
|
4816
|
+
flushEvents(toolId, tool);
|
|
4448
4817
|
break;
|
|
4449
4818
|
}
|
|
4819
|
+
case "KeyX":
|
|
4820
|
+
clearToolFault();
|
|
4821
|
+
break;
|
|
4822
|
+
case "KeyB":
|
|
4823
|
+
toggleSafety();
|
|
4824
|
+
break;
|
|
4450
4825
|
case "KeyZ":
|
|
4451
4826
|
tiltKey = 1;
|
|
4452
4827
|
break;
|
|
@@ -4463,6 +4838,9 @@ function createTestRoom(options) {
|
|
|
4463
4838
|
case "KeyT":
|
|
4464
4839
|
toggleTap();
|
|
4465
4840
|
break;
|
|
4841
|
+
case "KeyY":
|
|
4842
|
+
explosives.detonateCharges();
|
|
4843
|
+
break;
|
|
4466
4844
|
case "KeyH": {
|
|
4467
4845
|
const w = (heldEntry ?? entryOf(inspectTarget()?.object ?? ""))?.item?.weapon;
|
|
4468
4846
|
if (w) equip(w);
|
|
@@ -4577,7 +4955,7 @@ function createTestRoom(options) {
|
|
|
4577
4955
|
const reps = fastForward ? 10 : 1;
|
|
4578
4956
|
updateAnimations(dt);
|
|
4579
4957
|
updateItemUse(dt);
|
|
4580
|
-
|
|
4958
|
+
explosives.update(dt);
|
|
4581
4959
|
syncContainers(dt);
|
|
4582
4960
|
for (let i = 0; i < reps; i++) {
|
|
4583
4961
|
world.update(dt);
|
|
@@ -4588,9 +4966,10 @@ function createTestRoom(options) {
|
|
|
4588
4966
|
renderer.update(dt);
|
|
4589
4967
|
updateImpacts();
|
|
4590
4968
|
fluidView.update(dt);
|
|
4591
|
-
for (const s of tools
|
|
4592
|
-
|
|
4593
|
-
|
|
4969
|
+
for (const [id, s] of tools) {
|
|
4970
|
+
s.update(dt);
|
|
4971
|
+
flushEvents(id, s);
|
|
4972
|
+
}
|
|
4594
4973
|
for (const update2 of frameListeners) update2(dt);
|
|
4595
4974
|
for (const tr of tracers) {
|
|
4596
4975
|
if (tr.life > 0) {
|
|
@@ -4793,12 +5172,49 @@ function createTestRoom(options) {
|
|
|
4793
5172
|
};
|
|
4794
5173
|
const hostListeners = /* @__PURE__ */ new Set();
|
|
4795
5174
|
const frameListeners = /* @__PURE__ */ new Set();
|
|
4796
|
-
let reloadingSeen = false;
|
|
4797
5175
|
function emitHost(event) {
|
|
4798
5176
|
for (const listener of hostListeners) listener(event);
|
|
4799
5177
|
}
|
|
5178
|
+
function flushEvents(id, state, recoil) {
|
|
5179
|
+
const phase = state.phase;
|
|
5180
|
+
for (const event of state.drain()) {
|
|
5181
|
+
const hostEvent = toHostEvent(id, event, { ...recoil ? { recoil } : {}, phase });
|
|
5182
|
+
if (hostEvent) emitHost(hostEvent);
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
4800
5185
|
function startReload() {
|
|
4801
|
-
|
|
5186
|
+
tool.reload();
|
|
5187
|
+
flushEvents(toolId, tool);
|
|
5188
|
+
}
|
|
5189
|
+
function clearToolFault() {
|
|
5190
|
+
if (!tool.clearFault()) return;
|
|
5191
|
+
flushEvents(toolId, tool);
|
|
5192
|
+
say(t("hud.clearing"));
|
|
5193
|
+
}
|
|
5194
|
+
function toggleSafety() {
|
|
5195
|
+
tool.setSafety(!tool.safety);
|
|
5196
|
+
flushEvents(toolId, tool);
|
|
5197
|
+
say(t(tool.safety ? "hud.safety_on" : "hud.safety_off"));
|
|
5198
|
+
}
|
|
5199
|
+
function runToolCommand(request) {
|
|
5200
|
+
const work = (done) => done.ok ? { ok: true, seconds: done.seconds } : { ok: false, reason: done.reason };
|
|
5201
|
+
switch (request.type) {
|
|
5202
|
+
case "fit":
|
|
5203
|
+
return work(tool.fit(request.attachment, { install: "bench", ...request.mount ? { mount: request.mount } : {} }));
|
|
5204
|
+
case "remove":
|
|
5205
|
+
return work(tool.remove(request.attachment, { install: "bench" }));
|
|
5206
|
+
case "toggle":
|
|
5207
|
+
return tool.toggle(request.attachment) === null ? { ok: false, reason: "not_fitted" } : { ok: true };
|
|
5208
|
+
case "stock":
|
|
5209
|
+
return tool.setStock(request.state) ? { ok: true } : { ok: false, reason: "busy" };
|
|
5210
|
+
case "underbarrel":
|
|
5211
|
+
return tool.useUnderbarrel(request.on) ? { ok: true } : { ok: false, reason: "busy" };
|
|
5212
|
+
case "stance":
|
|
5213
|
+
tool.setStance({ ...request.aiming === void 0 ? {} : { aiming: request.aiming }, ...request.supported === void 0 ? {} : { supported: request.supported } });
|
|
5214
|
+
return { ok: true };
|
|
5215
|
+
default:
|
|
5216
|
+
return { ok: false, reason: "unknown" };
|
|
5217
|
+
}
|
|
4802
5218
|
}
|
|
4803
5219
|
const host = {
|
|
4804
5220
|
THREE,
|
|
@@ -4806,7 +5222,42 @@ function createTestRoom(options) {
|
|
|
4806
5222
|
camera,
|
|
4807
5223
|
locale,
|
|
4808
5224
|
groundAt,
|
|
4809
|
-
tool: () => ({
|
|
5225
|
+
tool: () => ({
|
|
5226
|
+
weapon: toolId,
|
|
5227
|
+
category: tool.def.category,
|
|
5228
|
+
ammo: tool.ammo,
|
|
5229
|
+
capacity: tool.capacity,
|
|
5230
|
+
reserve: tool.reserve,
|
|
5231
|
+
reloading: tool.reloading,
|
|
5232
|
+
mode: tool.mode,
|
|
5233
|
+
...tool.fault ? { fault: tool.fault } : {},
|
|
5234
|
+
safety: tool.safety,
|
|
5235
|
+
heat: tool.heatLevel,
|
|
5236
|
+
attachments: tool.attachments.map((part) => ({ id: part.id, mount: part.mount, ...part.integral ? { integral: true } : {}, ...part.on === void 0 ? {} : { on: part.on } })),
|
|
5237
|
+
stock: tool.stock,
|
|
5238
|
+
suppressed: tool.suppressed,
|
|
5239
|
+
underbarrelActive: tool.underbarrelActive,
|
|
5240
|
+
mass: tool.mass,
|
|
5241
|
+
aimSeconds: tool.aimSeconds
|
|
5242
|
+
}),
|
|
5243
|
+
command(request) {
|
|
5244
|
+
const result = runToolCommand(request);
|
|
5245
|
+
flushEvents(toolId, tool);
|
|
5246
|
+
return result;
|
|
5247
|
+
},
|
|
5248
|
+
configureTool(settings) {
|
|
5249
|
+
const condition = settings[SETTING_IDS.condition];
|
|
5250
|
+
const ammoQuality = settings[SETTING_IDS.ammoQuality];
|
|
5251
|
+
if (condition !== void 0) toolSettings.condition = condition;
|
|
5252
|
+
if (ammoQuality !== void 0) toolSettings.ammoQuality = ammoQuality;
|
|
5253
|
+
const fuseMetres = settings[SETTING_IDS.fuseLength] === void 0 ? void 0 : Number(settings[SETTING_IDS.fuseLength]);
|
|
5254
|
+
if (fuseMetres !== void 0 && fuseMetres > 0) fuseLength = fuseMetres;
|
|
5255
|
+
for (const state of tools.values()) {
|
|
5256
|
+
if (condition !== void 0) state.setCondition(condition);
|
|
5257
|
+
if (ammoQuality !== void 0) state.setAmmoQuality(ammoQuality);
|
|
5258
|
+
if (fuseMetres !== void 0 && fuseMetres > 0) state.setFuseLength(fuseMetres);
|
|
5259
|
+
}
|
|
5260
|
+
},
|
|
4810
5261
|
on(listener) {
|
|
4811
5262
|
hostListeners.add(listener);
|
|
4812
5263
|
return () => hostListeners.delete(listener);
|
|
@@ -5044,7 +5495,7 @@ function createTestRoom(options) {
|
|
|
5044
5495
|
stepSim(FIXED_DT);
|
|
5045
5496
|
t0 += FIXED_DT;
|
|
5046
5497
|
}
|
|
5047
|
-
|
|
5498
|
+
releaseTrigger();
|
|
5048
5499
|
},
|
|
5049
5500
|
grab,
|
|
5050
5501
|
throwHeld,
|
|
@@ -5061,6 +5512,15 @@ function createTestRoom(options) {
|
|
|
5061
5512
|
return [...entries.values()].filter((e) => !e.arena).map((e) => e.so);
|
|
5062
5513
|
},
|
|
5063
5514
|
update,
|
|
5515
|
+
waitUntilReady(maxSeconds = 5) {
|
|
5516
|
+
let waited = 0;
|
|
5517
|
+
while (tool.phase && waited < maxSeconds) {
|
|
5518
|
+
update(FIXED_DT);
|
|
5519
|
+
waited += FIXED_DT;
|
|
5520
|
+
}
|
|
5521
|
+
update(FIXED_DT);
|
|
5522
|
+
return waited + FIXED_DT;
|
|
5523
|
+
},
|
|
5064
5524
|
on(type, fn) {
|
|
5065
5525
|
listeners[type].add(fn);
|
|
5066
5526
|
return () => listeners[type].delete(fn);
|
|
@@ -5113,7 +5573,7 @@ function createTestRoom(options) {
|
|
|
5113
5573
|
renderer.dispose();
|
|
5114
5574
|
fluidView.dispose();
|
|
5115
5575
|
for (const tr of tracers) tr.line.removeFromParent();
|
|
5116
|
-
|
|
5576
|
+
explosives.clear();
|
|
5117
5577
|
if (ownRenderer) {
|
|
5118
5578
|
gl.dispose();
|
|
5119
5579
|
dom.remove();
|