@adatechnology/game-test-room 0.3.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 +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.global.js +6409 -541
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +534 -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,346 @@ 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 };
|
|
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
|
+
|
|
1303
1679
|
// src/log.ts
|
|
1304
1680
|
var MAX_ENTRIES = 5e3;
|
|
1305
1681
|
var pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
@@ -1991,7 +2367,7 @@ function createNarrator(c) {
|
|
|
1991
2367
|
|
|
1992
2368
|
// src/player.ts
|
|
1993
2369
|
var EYE = 1.7;
|
|
1994
|
-
var
|
|
2370
|
+
var PLAYER_RADIUS = 0.3;
|
|
1995
2371
|
var WALK = 3.4;
|
|
1996
2372
|
var RUN = 6.2;
|
|
1997
2373
|
var JUMP = 4.6;
|
|
@@ -2016,8 +2392,8 @@ function createPlayer(THREE, o) {
|
|
|
2016
2392
|
};
|
|
2017
2393
|
apply();
|
|
2018
2394
|
const blocked = (x, z) => {
|
|
2019
|
-
if (Math.abs(x) > o.limit -
|
|
2020
|
-
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;
|
|
2021
2397
|
return false;
|
|
2022
2398
|
};
|
|
2023
2399
|
const move = (dx, dz) => {
|
|
@@ -2239,6 +2615,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2239
2615
|
sc("steel_through_hole", ({ room, step, expect }) => {
|
|
2240
2616
|
const id = at(room, "steel_sheet", 0, -4);
|
|
2241
2617
|
room.setTool("pistol_9mm");
|
|
2618
|
+
room.waitUntilReady();
|
|
2242
2619
|
look(room, [0, 1.1, -1.5], [0, 1.1, -4]);
|
|
2243
2620
|
room.use();
|
|
2244
2621
|
step(0.2);
|
|
@@ -2253,6 +2630,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2253
2630
|
sc("concrete_stops_bullet", ({ room, step, expect }) => {
|
|
2254
2631
|
const id = at(room, "concrete_block", 0, -4);
|
|
2255
2632
|
room.setTool("pistol_9mm");
|
|
2633
|
+
room.waitUntilReady();
|
|
2256
2634
|
look(room, [0, 0.5, -1.5], [0, 0.2, -3.8]);
|
|
2257
2635
|
room.use();
|
|
2258
2636
|
step(0.2);
|
|
@@ -2268,6 +2646,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2268
2646
|
sc("hammer_dents_drum", ({ room, step, expect }) => {
|
|
2269
2647
|
const id = at(room, "steel_drum", 0, -4);
|
|
2270
2648
|
room.setTool("hammer");
|
|
2649
|
+
room.waitUntilReady();
|
|
2271
2650
|
look(room, [0, 0.6, -3.35], [0, 0.5, -4]);
|
|
2272
2651
|
let swings = 0;
|
|
2273
2652
|
for (; swings < 8 && (part(room, id, "drum_body")?.dents.length ?? 0) === 0; swings++) {
|
|
@@ -2284,6 +2663,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2284
2663
|
sc("glass_shatters", ({ room, step, expect }) => {
|
|
2285
2664
|
const id = at(room, "glass_pane", 0, -4);
|
|
2286
2665
|
room.setTool("pistol_9mm");
|
|
2666
|
+
room.waitUntilReady();
|
|
2287
2667
|
look(room, [0, 1.3, -1.5], [0, 1.05, -4]);
|
|
2288
2668
|
let shots = 0;
|
|
2289
2669
|
for (; shots < 10 && part(room, id, "pane")?.status !== "broken"; shots++) {
|
|
@@ -2298,6 +2678,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2298
2678
|
sc("shotgun_splinters_crate", ({ room, step, expect }) => {
|
|
2299
2679
|
const id = at(room, "wooden_crate", 0, -4);
|
|
2300
2680
|
room.setTool("shotgun_pump_12");
|
|
2681
|
+
room.waitUntilReady();
|
|
2301
2682
|
look(room, [0, 0.6, -1.8], [0, 0.25, -3.77]);
|
|
2302
2683
|
let shots = 0;
|
|
2303
2684
|
const splinters = () => room.log.entries({ type: "fracture", objectId: id }).filter((e) => e.data?.style === "splinter").length;
|
|
@@ -2331,6 +2712,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2331
2712
|
const id = at(room, "chair", 0, -4);
|
|
2332
2713
|
const before = room.renderer.pieces().length;
|
|
2333
2714
|
room.setTool("kick");
|
|
2715
|
+
room.waitUntilReady();
|
|
2334
2716
|
let kicks = 0;
|
|
2335
2717
|
for (; kicks < 8 && room.renderer.pieces().length <= before && part(room, id, "leg_1"); kicks++) {
|
|
2336
2718
|
const st = room.world.state(id);
|
|
@@ -2347,6 +2729,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2347
2729
|
sc("screwdriver_shelf_board", ({ room, step, expect }) => {
|
|
2348
2730
|
const id = at(room, "shelf", 0, -4);
|
|
2349
2731
|
room.setTool("screwdriver");
|
|
2732
|
+
room.waitUntilReady();
|
|
2350
2733
|
look(room, [0, 0.8, -3.5], [0, 0.75, -3.86]);
|
|
2351
2734
|
const t0 = room.log.entries().length ? room.log.entries().slice(-1)[0].t : 0;
|
|
2352
2735
|
room.use(5);
|
|
@@ -2360,6 +2743,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2360
2743
|
sc("wrong_tool_refused", ({ room, expect }) => {
|
|
2361
2744
|
const id = at(room, "steel_sheet", 0, -4);
|
|
2362
2745
|
room.setTool("screwdriver");
|
|
2746
|
+
room.waitUntilReady();
|
|
2363
2747
|
look(room, [0.2, 1.05, -3.6], [0.34, 1, -4]);
|
|
2364
2748
|
room.use(0.5);
|
|
2365
2749
|
const refused = count(room, "tool_refused");
|
|
@@ -2369,6 +2753,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2369
2753
|
sc("lighter_burns_crate", ({ room, step, expect }) => {
|
|
2370
2754
|
const id = at(room, "wooden_crate", 0, -4);
|
|
2371
2755
|
room.setTool("lighter");
|
|
2756
|
+
room.waitUntilReady();
|
|
2372
2757
|
look(room, [0, 0.3, -3.65], [0, 0.25, -3.76]);
|
|
2373
2758
|
room.use(5);
|
|
2374
2759
|
const lit = anyBurning(room, id);
|
|
@@ -2391,6 +2776,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2391
2776
|
const melt0 = part(room, drum2, "drum_body")?.melt ?? 0;
|
|
2392
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 });
|
|
2393
2778
|
room.setTool("cutting_torch");
|
|
2779
|
+
room.waitUntilReady();
|
|
2394
2780
|
look(room, [-0.15, 0.55, -3.55], [-0.15, 0.5, -3.72]);
|
|
2395
2781
|
room.use(20);
|
|
2396
2782
|
const p = part(room, drum2, "drum_body");
|
|
@@ -2399,6 +2785,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2399
2785
|
sc("bucket_melts_drips", ({ room, step, expect }) => {
|
|
2400
2786
|
const id = at(room, "plastic_bucket", 0, -4);
|
|
2401
2787
|
room.setTool("flamethrower");
|
|
2788
|
+
room.waitUntilReady();
|
|
2402
2789
|
look(room, [0, 0.5, -2.4], [0, 0.15, -4]);
|
|
2403
2790
|
room.use(2.5);
|
|
2404
2791
|
step(10);
|
|
@@ -2473,6 +2860,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2473
2860
|
sc("jerrycan_leaks_fuel", ({ room, step, expect }) => {
|
|
2474
2861
|
const id = at(room, "jerrycan", 0, -3.5);
|
|
2475
2862
|
room.setTool("pistol_9mm");
|
|
2863
|
+
room.waitUntilReady();
|
|
2476
2864
|
look(room, [0, 0.4, -1.5], [0, 0.12, -3.43]);
|
|
2477
2865
|
room.use();
|
|
2478
2866
|
step(1.5);
|
|
@@ -2487,6 +2875,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2487
2875
|
const id = at(room, "jerrycan", 0, -3.5);
|
|
2488
2876
|
const crate2 = at(room, "wooden_crate", 0.9, -3.2);
|
|
2489
2877
|
room.setTool("pistol_9mm");
|
|
2878
|
+
room.waitUntilReady();
|
|
2490
2879
|
look(room, [0, 0.4, -1.5], [0, 0.12, -3.43]);
|
|
2491
2880
|
room.use();
|
|
2492
2881
|
step(6);
|
|
@@ -2494,6 +2883,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2494
2883
|
expect(!!stain, t("sc.fuel_stain_burns.stain", { stain: yn(!!stain) }));
|
|
2495
2884
|
if (!stain) return;
|
|
2496
2885
|
room.setTool("lighter");
|
|
2886
|
+
room.waitUntilReady();
|
|
2497
2887
|
look(room, [stain.center[0], 0.1, stain.center[2] + 0.06], [stain.center[0], 0, stain.center[2]]);
|
|
2498
2888
|
room.use(1);
|
|
2499
2889
|
step(4);
|
|
@@ -2529,6 +2919,7 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2529
2919
|
const before = anyBurning(room, id);
|
|
2530
2920
|
expect(before, t("sc.extinguisher_puts_out_fire.burning", { burning: yn(before) }));
|
|
2531
2921
|
room.setTool("extinguisher");
|
|
2922
|
+
room.waitUntilReady();
|
|
2532
2923
|
look(room, [0, 0.9, -2.2], [0, 0.25, -4]);
|
|
2533
2924
|
room.use(2.5);
|
|
2534
2925
|
step(1);
|
|
@@ -3397,10 +3788,14 @@ function createTestRoom(options) {
|
|
|
3397
3788
|
const tools = /* @__PURE__ */ new Map();
|
|
3398
3789
|
let toolIndex = Math.max(0, loadout.indexOf("hand"));
|
|
3399
3790
|
let toolId = loadout[toolIndex] ?? "hand";
|
|
3791
|
+
const toolSettings = {};
|
|
3792
|
+
let fuseLength;
|
|
3793
|
+
const SETTING_IDS = { condition: "condition", ammoQuality: "ammo_quality", fuseLength: "fuse_length" };
|
|
3400
3794
|
const toolState = (id) => {
|
|
3401
3795
|
let s = tools.get(id);
|
|
3402
3796
|
if (!s) {
|
|
3403
|
-
s = arsenal.create(id, { seed: 7, reserve: 999 });
|
|
3797
|
+
s = arsenal.create(id, { seed: 7, reserve: 999, ...toolSettings });
|
|
3798
|
+
s.setFuseLength(fuseLength);
|
|
3404
3799
|
s.refill();
|
|
3405
3800
|
tools.set(id, s);
|
|
3406
3801
|
}
|
|
@@ -3501,6 +3896,7 @@ function createTestRoom(options) {
|
|
|
3501
3896
|
for (const ev of batch) {
|
|
3502
3897
|
if (ev.type === "leak") puncture(ev);
|
|
3503
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" });
|
|
3504
3900
|
fire("event", ev);
|
|
3505
3901
|
options.onEvent?.(ev);
|
|
3506
3902
|
}
|
|
@@ -3521,7 +3917,7 @@ function createTestRoom(options) {
|
|
|
3521
3917
|
},
|
|
3522
3918
|
primaryUp: () => {
|
|
3523
3919
|
useHeld = false;
|
|
3524
|
-
|
|
3920
|
+
releaseTrigger();
|
|
3525
3921
|
},
|
|
3526
3922
|
secondary: () => {
|
|
3527
3923
|
if (heldEntry) throwHeld();
|
|
@@ -3678,8 +4074,7 @@ function createTestRoom(options) {
|
|
|
3678
4074
|
if (heldEntry || heldPiece) releaseHeld();
|
|
3679
4075
|
useItemPlayer = null;
|
|
3680
4076
|
playing.length = 0;
|
|
3681
|
-
|
|
3682
|
-
clearProjectileMeshes();
|
|
4077
|
+
explosives.clear();
|
|
3683
4078
|
fireZones.length = 0;
|
|
3684
4079
|
burningStains.length = 0;
|
|
3685
4080
|
for (const id of [...entries.keys()]) if (!entries.get(id)?.arena) removeEntry(id);
|
|
@@ -3915,7 +4310,6 @@ function createTestRoom(options) {
|
|
|
3915
4310
|
}
|
|
3916
4311
|
});
|
|
3917
4312
|
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
4313
|
const f = first;
|
|
3920
4314
|
const d = f ? num(f.distance, 1) : "";
|
|
3921
4315
|
let text;
|
|
@@ -4063,100 +4457,46 @@ function createTestRoom(options) {
|
|
|
4063
4457
|
1
|
|
4064
4458
|
);
|
|
4065
4459
|
};
|
|
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
|
-
};
|
|
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
|
+
});
|
|
4153
4493
|
const useTool = (dt, held) => {
|
|
4154
4494
|
aim();
|
|
4155
4495
|
const cat = tool.def.category;
|
|
4156
4496
|
if (cat === "hand") return;
|
|
4157
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);
|
|
4158
4499
|
if (res.status === "empty") {
|
|
4159
|
-
emitHost({ type: "empty", weapon: toolId });
|
|
4160
4500
|
if (!tool.reloading && tool.reserve > 0) startReload();
|
|
4161
4501
|
say(t(tool.reloading ? "hud.reloading" : "hud.empty"));
|
|
4162
4502
|
return;
|
|
@@ -4164,8 +4504,13 @@ function createTestRoom(options) {
|
|
|
4164
4504
|
if (res.status !== "ok") {
|
|
4165
4505
|
if (res.status === "reloading") say(t("hud.reloading"));
|
|
4166
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"));
|
|
4167
4509
|
return;
|
|
4168
4510
|
}
|
|
4511
|
+
applyUses(res, dt);
|
|
4512
|
+
};
|
|
4513
|
+
const applyUses = (res, dt) => {
|
|
4169
4514
|
if (res.shots.length) fireShots(res.shots, res.recoil);
|
|
4170
4515
|
for (const u of res.uses) {
|
|
4171
4516
|
switch (u.kind) {
|
|
@@ -4179,7 +4524,13 @@ function createTestRoom(options) {
|
|
|
4179
4524
|
heatRays(u, "heat");
|
|
4180
4525
|
break;
|
|
4181
4526
|
case "throw":
|
|
4182
|
-
throwUse(u);
|
|
4527
|
+
explosives.throwUse(u);
|
|
4528
|
+
break;
|
|
4529
|
+
case "place":
|
|
4530
|
+
explosives.placeUse(u);
|
|
4531
|
+
break;
|
|
4532
|
+
case "backblast":
|
|
4533
|
+
explosives.backblastUse(u);
|
|
4183
4534
|
break;
|
|
4184
4535
|
case "tool":
|
|
4185
4536
|
toolWork(u, dt);
|
|
@@ -4192,6 +4543,16 @@ function createTestRoom(options) {
|
|
|
4192
4543
|
}
|
|
4193
4544
|
}
|
|
4194
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
|
+
};
|
|
4195
4556
|
const kick = () => {
|
|
4196
4557
|
if (!arsenal.weapons.has("kick")) return;
|
|
4197
4558
|
aim();
|
|
@@ -4201,7 +4562,7 @@ function createTestRoom(options) {
|
|
|
4201
4562
|
};
|
|
4202
4563
|
const setTool = (id) => {
|
|
4203
4564
|
if (!arsenal.weapons.has(id)) throw new Error(`game-test-room: unknown weapon "${id}"`);
|
|
4204
|
-
|
|
4565
|
+
releaseTrigger();
|
|
4205
4566
|
toolId = id;
|
|
4206
4567
|
const i = loadout.indexOf(id);
|
|
4207
4568
|
if (i >= 0) toolIndex = i;
|
|
@@ -4209,7 +4570,8 @@ function createTestRoom(options) {
|
|
|
4209
4570
|
progress = null;
|
|
4210
4571
|
action("tool", t("act.tool", { weapon: toolName(id) }), { data: { weapon: id } });
|
|
4211
4572
|
fire("tool", id);
|
|
4212
|
-
|
|
4573
|
+
tool.draw();
|
|
4574
|
+
flushEvents(id, tool);
|
|
4213
4575
|
};
|
|
4214
4576
|
const cycleTool = (dir) => {
|
|
4215
4577
|
if (!loadout.length) return;
|
|
@@ -4442,11 +4804,16 @@ function createTestRoom(options) {
|
|
|
4442
4804
|
}
|
|
4443
4805
|
break;
|
|
4444
4806
|
case "KeyQ": {
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
if (mode !== before) emitHost({ type: "mode", weapon: toolId, mode });
|
|
4807
|
+
tool.cycleMode();
|
|
4808
|
+
flushEvents(toolId, tool);
|
|
4448
4809
|
break;
|
|
4449
4810
|
}
|
|
4811
|
+
case "KeyX":
|
|
4812
|
+
clearToolFault();
|
|
4813
|
+
break;
|
|
4814
|
+
case "KeyB":
|
|
4815
|
+
toggleSafety();
|
|
4816
|
+
break;
|
|
4450
4817
|
case "KeyZ":
|
|
4451
4818
|
tiltKey = 1;
|
|
4452
4819
|
break;
|
|
@@ -4463,6 +4830,9 @@ function createTestRoom(options) {
|
|
|
4463
4830
|
case "KeyT":
|
|
4464
4831
|
toggleTap();
|
|
4465
4832
|
break;
|
|
4833
|
+
case "KeyY":
|
|
4834
|
+
explosives.detonateCharges();
|
|
4835
|
+
break;
|
|
4466
4836
|
case "KeyH": {
|
|
4467
4837
|
const w = (heldEntry ?? entryOf(inspectTarget()?.object ?? ""))?.item?.weapon;
|
|
4468
4838
|
if (w) equip(w);
|
|
@@ -4577,7 +4947,7 @@ function createTestRoom(options) {
|
|
|
4577
4947
|
const reps = fastForward ? 10 : 1;
|
|
4578
4948
|
updateAnimations(dt);
|
|
4579
4949
|
updateItemUse(dt);
|
|
4580
|
-
|
|
4950
|
+
explosives.update(dt);
|
|
4581
4951
|
syncContainers(dt);
|
|
4582
4952
|
for (let i = 0; i < reps; i++) {
|
|
4583
4953
|
world.update(dt);
|
|
@@ -4588,9 +4958,10 @@ function createTestRoom(options) {
|
|
|
4588
4958
|
renderer.update(dt);
|
|
4589
4959
|
updateImpacts();
|
|
4590
4960
|
fluidView.update(dt);
|
|
4591
|
-
for (const s of tools
|
|
4592
|
-
|
|
4593
|
-
|
|
4961
|
+
for (const [id, s] of tools) {
|
|
4962
|
+
s.update(dt);
|
|
4963
|
+
flushEvents(id, s);
|
|
4964
|
+
}
|
|
4594
4965
|
for (const update2 of frameListeners) update2(dt);
|
|
4595
4966
|
for (const tr of tracers) {
|
|
4596
4967
|
if (tr.life > 0) {
|
|
@@ -4793,12 +5164,29 @@ function createTestRoom(options) {
|
|
|
4793
5164
|
};
|
|
4794
5165
|
const hostListeners = /* @__PURE__ */ new Set();
|
|
4795
5166
|
const frameListeners = /* @__PURE__ */ new Set();
|
|
4796
|
-
let reloadingSeen = false;
|
|
4797
5167
|
function emitHost(event) {
|
|
4798
5168
|
for (const listener of hostListeners) listener(event);
|
|
4799
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
|
+
}
|
|
4800
5177
|
function startReload() {
|
|
4801
|
-
|
|
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"));
|
|
4802
5190
|
}
|
|
4803
5191
|
const host = {
|
|
4804
5192
|
THREE,
|
|
@@ -4806,7 +5194,31 @@ function createTestRoom(options) {
|
|
|
4806
5194
|
camera,
|
|
4807
5195
|
locale,
|
|
4808
5196
|
groundAt,
|
|
4809
|
-
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
|
+
},
|
|
4810
5222
|
on(listener) {
|
|
4811
5223
|
hostListeners.add(listener);
|
|
4812
5224
|
return () => hostListeners.delete(listener);
|
|
@@ -5044,7 +5456,7 @@ function createTestRoom(options) {
|
|
|
5044
5456
|
stepSim(FIXED_DT);
|
|
5045
5457
|
t0 += FIXED_DT;
|
|
5046
5458
|
}
|
|
5047
|
-
|
|
5459
|
+
releaseTrigger();
|
|
5048
5460
|
},
|
|
5049
5461
|
grab,
|
|
5050
5462
|
throwHeld,
|
|
@@ -5061,6 +5473,15 @@ function createTestRoom(options) {
|
|
|
5061
5473
|
return [...entries.values()].filter((e) => !e.arena).map((e) => e.so);
|
|
5062
5474
|
},
|
|
5063
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
|
+
},
|
|
5064
5485
|
on(type, fn) {
|
|
5065
5486
|
listeners[type].add(fn);
|
|
5066
5487
|
return () => listeners[type].delete(fn);
|
|
@@ -5113,7 +5534,7 @@ function createTestRoom(options) {
|
|
|
5113
5534
|
renderer.dispose();
|
|
5114
5535
|
fluidView.dispose();
|
|
5115
5536
|
for (const tr of tracers) tr.line.removeFromParent();
|
|
5116
|
-
|
|
5537
|
+
explosives.clear();
|
|
5117
5538
|
if (ownRenderer) {
|
|
5118
5539
|
gl.dispose();
|
|
5119
5540
|
dom.remove();
|