@adatechnology/game-test-room 0.28.2 → 0.29.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 +2 -1
- package/dist/index.d.ts +18 -2
- package/dist/index.global.js +2455 -305
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +397 -83
- package/dist/index.js.map +1 -1
- package/package.json +14 -14
package/dist/index.js
CHANGED
|
@@ -14,6 +14,9 @@ var DEFAULT_TOOL_BLOWS = {
|
|
|
14
14
|
kick: "kick",
|
|
15
15
|
shove: "shove",
|
|
16
16
|
knife: "blade",
|
|
17
|
+
kabar: "blade",
|
|
18
|
+
katana: "blade",
|
|
19
|
+
brass_knuckles: "punch",
|
|
17
20
|
machete: "blade",
|
|
18
21
|
utility_knife: "blade",
|
|
19
22
|
axe: "chop",
|
|
@@ -80,7 +83,7 @@ function createStrideCounter(stride = STRIDE_METRES) {
|
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
// src/room.ts
|
|
83
|
-
import { createArsenal, DEFAULT_LOADOUT, realLoadout, weaponMenu } from "@adatechnology/game-weapons";
|
|
86
|
+
import { canDualWield, createArsenal, DEFAULT_LOADOUT, realLoadout, weaponMenu } from "@adatechnology/game-weapons";
|
|
84
87
|
import { createFilmLcg, createFluids, createItemUse, ITEM_USES, resolveStrings as resolveFluidStrings } from "@adatechnology/game-fluids";
|
|
85
88
|
import { createFilmBridge, createFilmManager, createFluidRenderer } from "@adatechnology/game-fluids-three";
|
|
86
89
|
import { SPRITE_KINDS, createVfx, patchSpriteKinds } from "@adatechnology/game-vfx";
|
|
@@ -627,6 +630,11 @@ var segmentTouchesBox = (from, to, min, max) => {
|
|
|
627
630
|
};
|
|
628
631
|
|
|
629
632
|
// src/explosives.ts
|
|
633
|
+
var SMOKE_PUFFS_PER_SECOND = 36;
|
|
634
|
+
var SMOKE_PUFF_LIFE = 3.2;
|
|
635
|
+
var SMOKE_PUFF_SPEED = 1.7;
|
|
636
|
+
var SMOKE_PUFF_SIZE = 5;
|
|
637
|
+
var SMOKE_PUFF_ALPHA = 1.5;
|
|
630
638
|
var CONE_BLAST_RADIUS = 0.5;
|
|
631
639
|
var summarizeBlast = (results) => {
|
|
632
640
|
const reports = results.flatMap((r) => r.blast ? [r.blast] : []);
|
|
@@ -642,6 +650,8 @@ var toArray = (vector) => [vector.x, vector.y, vector.z];
|
|
|
642
650
|
function createExplosives(context) {
|
|
643
651
|
const { THREE, scene, camera, world, renderer, arsenal, translate, format, action } = context;
|
|
644
652
|
const projectiles = [];
|
|
653
|
+
const smokeSources = [];
|
|
654
|
+
const random = context.random ?? Math.random;
|
|
645
655
|
const meshes = [];
|
|
646
656
|
const up = new THREE.Vector3(0, 1, 0);
|
|
647
657
|
const drop = (objects) => {
|
|
@@ -649,6 +659,7 @@ function createExplosives(context) {
|
|
|
649
659
|
releaseObject3D(objects, { keep: scene });
|
|
650
660
|
};
|
|
651
661
|
const clear = () => {
|
|
662
|
+
smokeSources.length = 0;
|
|
652
663
|
projectiles.length = 0;
|
|
653
664
|
drop(meshes);
|
|
654
665
|
meshes.length = 0;
|
|
@@ -815,6 +826,12 @@ function createExplosives(context) {
|
|
|
815
826
|
const detonate = (projectile, detonation) => {
|
|
816
827
|
const weaponName = context.toolName(projectile.weapon);
|
|
817
828
|
const reach = Math.max(detonation.injuryRadius, detonation.lethalRadius);
|
|
829
|
+
if (detonation.smoke) {
|
|
830
|
+
const [shortest, longest] = detonation.smoke.seconds;
|
|
831
|
+
smokeSources.push({ point: detonation.point, left: shortest + (longest - shortest) * random(), owed: 0, color: detonation.smoke.color });
|
|
832
|
+
action("smoke", translate("act.smoke", { weapon: weaponName, seconds: format(smokeSources[smokeSources.length - 1].left, 0) }), { data: { weapon: projectile.weapon, explosive: detonation.explosive, point: detonation.point } });
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
818
835
|
const results = detonation.energy <= 0 || detonation.cone ? [] : world.explode(detonation.point, detonation.energy, reach, "player", context.blast());
|
|
819
836
|
const reached = detonation.energy <= 0 ? 0 : detonation.cone ? coneBlast(detonation) : results.length;
|
|
820
837
|
if (!detonation.cone) told(results);
|
|
@@ -936,7 +953,22 @@ function createExplosives(context) {
|
|
|
936
953
|
action("tripwire", translate("act.tripwire", { weapon: context.toolName(projectile.weapon), who: crosser }), { data: { weapon: projectile.weapon, body: crosser } });
|
|
937
954
|
detonate(projectile, event.detonation);
|
|
938
955
|
};
|
|
956
|
+
const burnSmoke = (dt) => {
|
|
957
|
+
for (let index = smokeSources.length - 1; index >= 0; index--) {
|
|
958
|
+
const source = smokeSources[index];
|
|
959
|
+
source.left -= dt;
|
|
960
|
+
source.owed += SMOKE_PUFFS_PER_SECOND * dt;
|
|
961
|
+
for (; source.owed >= 1; source.owed -= 1) {
|
|
962
|
+
const angle = random() * Math.PI * 2;
|
|
963
|
+
const speed = SMOKE_PUFF_SPEED * (0.4 + random());
|
|
964
|
+
const velocity = [Math.cos(angle) * speed, 0.5 + random() * 0.8, Math.sin(angle) * speed];
|
|
965
|
+
context.effects.spawn("smoke", [source.point[0], source.point[1] + 0.08, source.point[2]], velocity, { color: source.color, size: SMOKE_PUFF_SIZE, life: SMOKE_PUFF_LIFE, alpha: SMOKE_PUFF_ALPHA });
|
|
966
|
+
}
|
|
967
|
+
if (source.left <= 0) smokeSources.splice(index, 1);
|
|
968
|
+
}
|
|
969
|
+
};
|
|
939
970
|
const update = (dt) => {
|
|
971
|
+
burnSmoke(dt);
|
|
940
972
|
for (let index = projectiles.length - 1; index >= 0; index--) {
|
|
941
973
|
const projectile = projectiles[index];
|
|
942
974
|
if (!projectile) continue;
|
|
@@ -957,7 +989,7 @@ function createExplosives(context) {
|
|
|
957
989
|
}
|
|
958
990
|
}
|
|
959
991
|
};
|
|
960
|
-
return { throwUse, placeUse, backblastUse, detonateCharges, blastAt, update, clear };
|
|
992
|
+
return { throwUse, placeUse, backblastUse, detonateCharges, blastAt, update, clear, smokeLeft: () => smokeSources.map((source) => source.left) };
|
|
961
993
|
}
|
|
962
994
|
|
|
963
995
|
// src/test-table.ts
|
|
@@ -1171,6 +1203,65 @@ var toHostEvent = (weapon, event, options = {}) => {
|
|
|
1171
1203
|
}
|
|
1172
1204
|
};
|
|
1173
1205
|
|
|
1206
|
+
// src/dual-reload.ts
|
|
1207
|
+
var otherHand = (hand) => hand === "R" ? "L" : "R";
|
|
1208
|
+
function createDualReload(host, stowSeconds) {
|
|
1209
|
+
let queue = [];
|
|
1210
|
+
let current = null;
|
|
1211
|
+
const next = () => {
|
|
1212
|
+
let hand = queue.shift();
|
|
1213
|
+
while (hand && !host.needs(hand)) hand = queue.shift();
|
|
1214
|
+
if (!hand) {
|
|
1215
|
+
current = null;
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const seconds = stowSeconds();
|
|
1219
|
+
current = { hand, phase: "stow", left: seconds };
|
|
1220
|
+
host.stow(otherHand(hand), true, seconds);
|
|
1221
|
+
};
|
|
1222
|
+
const draw = (hand) => {
|
|
1223
|
+
const seconds = stowSeconds();
|
|
1224
|
+
current = { hand, phase: "draw", left: seconds };
|
|
1225
|
+
host.stow(otherHand(hand), false, seconds);
|
|
1226
|
+
};
|
|
1227
|
+
return {
|
|
1228
|
+
start(order) {
|
|
1229
|
+
if (current) return;
|
|
1230
|
+
queue = [...order];
|
|
1231
|
+
next();
|
|
1232
|
+
},
|
|
1233
|
+
step(dt) {
|
|
1234
|
+
if (!current) return;
|
|
1235
|
+
if (current.phase === "reload") {
|
|
1236
|
+
if (!host.reloading(current.hand)) draw(current.hand);
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
current.left -= dt;
|
|
1240
|
+
if (current.left > 0) return;
|
|
1241
|
+
if (current.phase === "draw") {
|
|
1242
|
+
next();
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
current.phase = "reload";
|
|
1246
|
+
if (!host.startReload(current.hand)) draw(current.hand);
|
|
1247
|
+
},
|
|
1248
|
+
get busy() {
|
|
1249
|
+
return current !== null;
|
|
1250
|
+
},
|
|
1251
|
+
get current() {
|
|
1252
|
+
return current ? { hand: current.hand, phase: current.phase } : null;
|
|
1253
|
+
},
|
|
1254
|
+
stowed(hand) {
|
|
1255
|
+
return current !== null && current.phase !== "draw" && otherHand(current.hand) === hand;
|
|
1256
|
+
},
|
|
1257
|
+
cancel() {
|
|
1258
|
+
if (current && current.phase !== "draw") host.stow(otherHand(current.hand), false, 0);
|
|
1259
|
+
current = null;
|
|
1260
|
+
queue = [];
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1174
1265
|
// src/loading-view.ts
|
|
1175
1266
|
var LOADING_CLASS = "tr-loading";
|
|
1176
1267
|
var LOADING_CSS = [
|
|
@@ -2344,6 +2435,10 @@ function createPlayer(THREE, o) {
|
|
|
2344
2435
|
o.hooks.primaryDown();
|
|
2345
2436
|
};
|
|
2346
2437
|
const onMouseUp = (e) => {
|
|
2438
|
+
if (e.button === 2) {
|
|
2439
|
+
o.hooks.secondaryUp?.();
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2347
2442
|
if (e.button !== 0) return;
|
|
2348
2443
|
if (holdTimer) clearTimeout(holdTimer);
|
|
2349
2444
|
if (dragging) {
|
|
@@ -3361,7 +3456,7 @@ import { createFootprints, createTerrain, createTerrainImpl, DEFAULT_PROFILE, wa
|
|
|
3361
3456
|
import { createTerrainRenderer, terrainProfileName } from "@adatechnology/game-terrain-three";
|
|
3362
3457
|
|
|
3363
3458
|
// src/avatar.ts
|
|
3364
|
-
var DEFAULT_STRIKE_CLIPS = { hand: "punch_right", fists: "punch_right", shove: "shove_two_hands", kick: "front_kick" };
|
|
3459
|
+
var DEFAULT_STRIKE_CLIPS = { hand: "punch_right", fists: "punch_right", brass_knuckles: "punch_right", shove: "shove_two_hands", kick: "front_kick" };
|
|
3365
3460
|
var DEFAULT_HAND_STANCES = { fists: "fists" };
|
|
3366
3461
|
var THIRD_PERSON_DEFAULTS = {
|
|
3367
3462
|
back: 3.8,
|
|
@@ -3534,6 +3629,12 @@ var en = {
|
|
|
3534
3629
|
"act.clear": "Cleared the room",
|
|
3535
3630
|
"act.reset": "Repaired every object",
|
|
3536
3631
|
"act.tool": "Equipped {weapon}",
|
|
3632
|
+
"act.tool_left": "{weapon} in the left hand: left button fires the right one, right button the left one",
|
|
3633
|
+
"act.tool_left_off": "Left hand free: one weapon again",
|
|
3634
|
+
"act.stow": "{weapon} tucked against the body: the {hand} hand is free for the other magazine",
|
|
3635
|
+
"act.unstow": "{weapon} back in the {hand} hand",
|
|
3636
|
+
"hud.no_dual": "{weapon} does not go one in each hand",
|
|
3637
|
+
"hud.left": "L",
|
|
3537
3638
|
"act.shoot.hit": "{weapon}: shot, {energy} J, hit {where} at {dist} m",
|
|
3538
3639
|
"act.shoot.miss": "{weapon}: shot, {energy} J, hit nothing",
|
|
3539
3640
|
"act.shoot.pellets": "{weapon}: shot, {n} projectiles of up to {energy} J, {hits} hit {where} at {dist} m",
|
|
@@ -3563,6 +3664,7 @@ var en = {
|
|
|
3563
3664
|
"hud.no_charges": "No charge set down waits for the remote detonator",
|
|
3564
3665
|
"blast.zone.lethal": "lethal",
|
|
3565
3666
|
"blast.zone.injury": "injury",
|
|
3667
|
+
"act.smoke": "{weapon}: smoke, burning for {seconds} s",
|
|
3566
3668
|
"act.explode": "{weapon}: exploded, {energy} J within {radius} m, {n} objects in range",
|
|
3567
3669
|
"act.molotov_fire": "{weapon}: fire on the ground, radius {radius} m for {seconds} s",
|
|
3568
3670
|
"act.tool.none": "{weapon}: nothing in reach",
|
|
@@ -3701,6 +3803,7 @@ var en = {
|
|
|
3701
3803
|
"ui.clear_log": "Clear",
|
|
3702
3804
|
"ui.catalog": "Catalogue",
|
|
3703
3805
|
"ui.tool_menu": "Weapons",
|
|
3806
|
+
"ui.tool_menu.dual": "Shift+click: one in each hand",
|
|
3704
3807
|
"ui.tool_menu.search": "Search weapons and tools",
|
|
3705
3808
|
"ui.tool_menu.close": "Close",
|
|
3706
3809
|
"ui.tool_menu.results": "{n} found",
|
|
@@ -3904,6 +4007,12 @@ var ptBR = {
|
|
|
3904
4007
|
"act.clear": "Limpou a sala",
|
|
3905
4008
|
"act.reset": "Consertou todos os objetos",
|
|
3906
4009
|
"act.tool": "Equipou {weapon}",
|
|
4010
|
+
"act.tool_left": "{weapon} na m\xE3o esquerda: bot\xE3o esquerdo dispara a da direita, bot\xE3o direito a da esquerda",
|
|
4011
|
+
"act.tool_left_off": "M\xE3o esquerda livre: uma arma de novo",
|
|
4012
|
+
"act.stow": "{weapon} encostada no corpo: a m\xE3o {hand} fica livre para o outro carregador",
|
|
4013
|
+
"act.unstow": "{weapon} de volta na m\xE3o {hand}",
|
|
4014
|
+
"hud.no_dual": "{weapon} n\xE3o vai uma em cada m\xE3o",
|
|
4015
|
+
"hud.left": "E",
|
|
3907
4016
|
"act.shoot.hit": "{weapon}: disparo, {energy} J, acertou {where} a {dist} m",
|
|
3908
4017
|
"act.shoot.miss": "{weapon}: disparo, {energy} J, n\xE3o acertou nada",
|
|
3909
4018
|
"act.shoot.pellets": "{weapon}: disparo, {n} proj\xE9teis de at\xE9 {energy} J, {hits} acertaram {where} a {dist} m",
|
|
@@ -3933,6 +4042,7 @@ var ptBR = {
|
|
|
3933
4042
|
"hud.no_charges": "Nenhuma carga posta espera o detonador",
|
|
3934
4043
|
"blast.zone.lethal": "letal",
|
|
3935
4044
|
"blast.zone.injury": "de ferimento",
|
|
4045
|
+
"act.smoke": "{weapon}: fuma\xE7a, queima por {seconds} s",
|
|
3936
4046
|
"act.explode": "{weapon}: explodiu, {energy} J em {radius} m, {n} objetos ao alcance",
|
|
3937
4047
|
"act.molotov_fire": "{weapon}: fogo no ch\xE3o, raio de {radius} m por {seconds} s",
|
|
3938
4048
|
"act.tool.none": "{weapon}: nada ao alcance",
|
|
@@ -4071,6 +4181,7 @@ var ptBR = {
|
|
|
4071
4181
|
"ui.clear_log": "Limpar",
|
|
4072
4182
|
"ui.catalog": "Cat\xE1logo",
|
|
4073
4183
|
"ui.tool_menu": "Armas",
|
|
4184
|
+
"ui.tool_menu.dual": "Shift+clique: uma em cada m\xE3o",
|
|
4074
4185
|
"ui.tool_menu.search": "Buscar armas e ferramentas",
|
|
4075
4186
|
"ui.tool_menu.close": "Fechar",
|
|
4076
4187
|
"ui.tool_menu.results": "{n} encontradas",
|
|
@@ -4279,6 +4390,27 @@ function createBenchPanel(o) {
|
|
|
4279
4390
|
return wrap;
|
|
4280
4391
|
};
|
|
4281
4392
|
let playhead = null;
|
|
4393
|
+
const sheetImage = (image) => {
|
|
4394
|
+
const figure = mk("figure", "tr-sheet-image");
|
|
4395
|
+
const picture = mk("img");
|
|
4396
|
+
picture.src = image.url;
|
|
4397
|
+
picture.alt = image.alt;
|
|
4398
|
+
picture.loading = "lazy";
|
|
4399
|
+
picture.referrerPolicy = "no-referrer";
|
|
4400
|
+
figure.appendChild(picture);
|
|
4401
|
+
if (image.credit || image.page) {
|
|
4402
|
+
const caption = mk("figcaption", "sub", image.credit ?? "");
|
|
4403
|
+
if (image.page) {
|
|
4404
|
+
const link = mk("a", void 0, image.credit ? " \u2197" : image.page);
|
|
4405
|
+
link.href = image.page;
|
|
4406
|
+
link.target = "_blank";
|
|
4407
|
+
link.rel = "noopener noreferrer";
|
|
4408
|
+
caption.appendChild(link);
|
|
4409
|
+
}
|
|
4410
|
+
figure.appendChild(caption);
|
|
4411
|
+
}
|
|
4412
|
+
return figure;
|
|
4413
|
+
};
|
|
4282
4414
|
const sheetCard = (sheet) => {
|
|
4283
4415
|
const card = mk("div", "tr-sheet");
|
|
4284
4416
|
if (sheet.kicker) card.appendChild(mk("div", "sub", sheet.kicker));
|
|
@@ -4330,6 +4462,7 @@ function createBenchPanel(o) {
|
|
|
4330
4462
|
playhead = { node: head, metric, end };
|
|
4331
4463
|
card.appendChild(strip);
|
|
4332
4464
|
}
|
|
4465
|
+
if (sheet.image) card.appendChild(sheetImage(sheet.image));
|
|
4333
4466
|
if (sheet.note) card.appendChild(mk("p", "sub", sheet.note));
|
|
4334
4467
|
return card;
|
|
4335
4468
|
};
|
|
@@ -4563,7 +4696,7 @@ function createToolMenu(o) {
|
|
|
4563
4696
|
const close = mk3("button", "tr-btn", "\xD7");
|
|
4564
4697
|
close.type = "button";
|
|
4565
4698
|
close.title = o.t("ui.tool_menu.close");
|
|
4566
|
-
header.append(mk3("h3", void 0, o.t("ui.tool_menu")), search, close);
|
|
4699
|
+
header.append(mk3("h3", void 0, o.t("ui.tool_menu")), mk3("small", "tr-hint", o.t("ui.tool_menu.dual")), search, close);
|
|
4567
4700
|
const tabs = mk3("div", "tr-tabs");
|
|
4568
4701
|
const body = mk3("div", "tr-wmenu-body");
|
|
4569
4702
|
element.append(header, tabs, body);
|
|
@@ -4571,15 +4704,17 @@ function createToolMenu(o) {
|
|
|
4571
4704
|
let activeGroup = "";
|
|
4572
4705
|
let groups = [];
|
|
4573
4706
|
const toolButton = (tool, group) => {
|
|
4574
|
-
const
|
|
4707
|
+
const left = o.currentLeft?.() === tool.id;
|
|
4708
|
+
const button = mk3("button", tool.id === o.current() || left ? "tr-btn on" : "tr-btn");
|
|
4575
4709
|
button.type = "button";
|
|
4576
4710
|
button.title = tool.name;
|
|
4711
|
+
if (left) button.dataset.left = "";
|
|
4577
4712
|
button.dataset.tool = tool.id;
|
|
4578
4713
|
if (tool.thumbnail) button.appendChild(o.thumb(tool.thumbnail, tool.thumbnailCredit));
|
|
4579
4714
|
button.appendChild(mk3("span", void 0, tool.name));
|
|
4580
4715
|
if (group) button.appendChild(mk3("small", void 0, group));
|
|
4581
|
-
button.onclick = () => {
|
|
4582
|
-
o.onPick(tool.id);
|
|
4716
|
+
button.onclick = (event) => {
|
|
4717
|
+
o.onPick(tool.id, { second: event.shiftKey });
|
|
4583
4718
|
toggle(false);
|
|
4584
4719
|
};
|
|
4585
4720
|
return button;
|
|
@@ -4675,6 +4810,8 @@ var CSS = `
|
|
|
4675
4810
|
.tr-prog b{display:block;font-weight:500;margin-bottom:3px}
|
|
4676
4811
|
.tr-sheet{margin-top:8px;padding:8px;border:1px solid #2a2e36;border-radius:6px;font-size:12px;line-height:1.4}
|
|
4677
4812
|
.tr-sheet h4{margin:2px 0 4px;font-size:13px}.tr-sheet p{margin:4px 0}
|
|
4813
|
+
/* the picture of a sheet floats over the scene, top centre, next to what it shows: the bench panel is too short to hold it */
|
|
4814
|
+
.tr-sheet-image{position:fixed;top:52px;left:50%;transform:translateX(-50%);width:min(400px,42vw);z-index:5;margin:0;padding:6px;background:rgba(18,20,24,.92);border:1px solid #2a2e36;border-radius:6px;pointer-events:auto}.tr-sheet-image img{display:block;width:100%;max-height:200px;object-fit:contain;background:#f4f4f2;border-radius:4px}.tr-sheet-image figcaption{margin-top:3px;font-size:11px}.tr-sheet-image a{color:#8ab4f8}
|
|
4678
4815
|
.tr-facts{display:grid;grid-template-columns:auto 1fr;gap:2px 8px;margin:4px 0}.tr-facts dt{opacity:.7}.tr-facts dd{margin:0}
|
|
4679
4816
|
.tr-steps{margin:4px 0;padding-left:16px}.tr-swatches{display:flex;flex-wrap:wrap;gap:6px}.tr-swatch i{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:4px}
|
|
4680
4817
|
.tr-timeline{position:relative;height:22px;margin:8px 0;background:#1b1e24;border-radius:4px;overflow:hidden}
|
|
@@ -4802,7 +4939,7 @@ function createUi(o) {
|
|
|
4802
4939
|
bottom.append(held, hotbar);
|
|
4803
4940
|
let currentTool = "";
|
|
4804
4941
|
const menuOptions = o.toolMenu;
|
|
4805
|
-
const toolMenu = menuOptions ? createToolMenu({ t, groups: menuOptions.groups, current: () => currentTool, onPick: menuOptions.onPick, ...menuOptions.onOpen ? { onOpen: menuOptions.onOpen } : {}, thumb }) : null;
|
|
4942
|
+
const toolMenu = menuOptions ? createToolMenu({ t, groups: menuOptions.groups, current: () => currentTool, onPick: menuOptions.onPick, ...menuOptions.onOpen ? { onOpen: menuOptions.onOpen } : {}, ...menuOptions.currentLeft ? { currentLeft: menuOptions.currentLeft } : {}, thumb }) : null;
|
|
4806
4943
|
if (toolMenu && menuOptions) {
|
|
4807
4944
|
const menuButton = mk4("button", "tr-slot tr-menu-btn");
|
|
4808
4945
|
menuButton.type = "button";
|
|
@@ -5450,6 +5587,8 @@ function createModuleViews(THREE, gl) {
|
|
|
5450
5587
|
|
|
5451
5588
|
// src/room.ts
|
|
5452
5589
|
var FIXED_DT = 1 / 60;
|
|
5590
|
+
var LEFT_HAND_KEY = "#L";
|
|
5591
|
+
var OFF_HAND_SEED = 11;
|
|
5453
5592
|
var TOOL_MENU_KEY_LABELS = { Backquote: "'" };
|
|
5454
5593
|
var EXPLOSIVE_EFFECT_IDS = ["m67", "molotov", "pipe_bomb", "m79_40mm", "m72_66mm", "at4_84mm", "dynamite", "c4", "claymore"];
|
|
5455
5594
|
var DEFAULT_PARTICLE_LIMIT = 900;
|
|
@@ -5745,6 +5884,11 @@ function createTestRoom(options) {
|
|
|
5745
5884
|
return s;
|
|
5746
5885
|
};
|
|
5747
5886
|
let tool = toolState(toolId);
|
|
5887
|
+
let offToolId = null;
|
|
5888
|
+
let offTool = null;
|
|
5889
|
+
let offHeld = false;
|
|
5890
|
+
let offFirst = false;
|
|
5891
|
+
let firedHand = "R";
|
|
5748
5892
|
const catalogMap = /* @__PURE__ */ new Map();
|
|
5749
5893
|
for (const i of options.catalog) catalogMap.set(i.id, i);
|
|
5750
5894
|
const catalog = [...options.catalog];
|
|
@@ -5954,9 +6098,19 @@ function createTestRoom(options) {
|
|
|
5954
6098
|
releaseTrigger();
|
|
5955
6099
|
},
|
|
5956
6100
|
secondary: () => {
|
|
6101
|
+
if (offTool && !heldEntry) {
|
|
6102
|
+
offHeld = true;
|
|
6103
|
+
offFirst = true;
|
|
6104
|
+
return;
|
|
6105
|
+
}
|
|
5957
6106
|
if (heldEntry) throwHeld();
|
|
5958
6107
|
else grab();
|
|
5959
6108
|
},
|
|
6109
|
+
secondaryUp: () => {
|
|
6110
|
+
if (!offHeld) return;
|
|
6111
|
+
offHeld = false;
|
|
6112
|
+
releaseTrigger("L");
|
|
6113
|
+
},
|
|
5960
6114
|
key: (code, down, e) => onKey(code, down, e),
|
|
5961
6115
|
wheel: (dir) => cycleTool(dir)
|
|
5962
6116
|
}
|
|
@@ -6495,6 +6649,10 @@ function createTestRoom(options) {
|
|
|
6495
6649
|
for (const listener of [...resetListeners]) listener();
|
|
6496
6650
|
tools.clear();
|
|
6497
6651
|
tool = toolState(toolId);
|
|
6652
|
+
dualReload.cancel();
|
|
6653
|
+
offTool = null;
|
|
6654
|
+
offToolId = null;
|
|
6655
|
+
firedHand = "R";
|
|
6498
6656
|
if (pristineWorld) {
|
|
6499
6657
|
for (const id of world.ids()) world.remove(id);
|
|
6500
6658
|
world.load(pristineWorld);
|
|
@@ -6760,12 +6918,13 @@ function createTestRoom(options) {
|
|
|
6760
6918
|
const pocketAt = new THREE.Vector3();
|
|
6761
6919
|
const aimTurn = new THREE.Quaternion();
|
|
6762
6920
|
const aimEuler = new THREE.Euler(0, 0, 0, "YXZ");
|
|
6763
|
-
const bodyHoldFrame = () => {
|
|
6764
|
-
const shoulder = avatar?.shoulder?.(
|
|
6921
|
+
const bodyHoldFrame = (hand = "R") => {
|
|
6922
|
+
const shoulder = avatar?.shoulder?.(hand);
|
|
6765
6923
|
if (!shoulder) return null;
|
|
6766
6924
|
shoulder.getWorldPosition(pocketAt);
|
|
6767
|
-
|
|
6768
|
-
pocketAt.
|
|
6925
|
+
const inward = hand === "L" ? -SHOULDER_POCKET_IN : SHOULDER_POCKET_IN;
|
|
6926
|
+
pocketAt.x -= Math.cos(player.yaw) * inward;
|
|
6927
|
+
pocketAt.z += Math.sin(player.yaw) * inward;
|
|
6769
6928
|
aimTurn.setFromEuler(aimEuler.set(player.pitch, player.yaw, 0, "YXZ"));
|
|
6770
6929
|
return { position: [pocketAt.x, pocketAt.y, pocketAt.z], quaternion: [aimTurn.x, aimTurn.y, aimTurn.z, aimTurn.w] };
|
|
6771
6930
|
};
|
|
@@ -6875,17 +7034,18 @@ function createTestRoom(options) {
|
|
|
6875
7034
|
rng = rng * 1664525 + 1013904223 >>> 0;
|
|
6876
7035
|
return rng / 4294967296;
|
|
6877
7036
|
};
|
|
6878
|
-
const muzzlePose = () => {
|
|
6879
|
-
const lent = muzzleSource?.();
|
|
6880
|
-
return lent ? { position: [...lent.position], drawsFlash: lent.drawsFlash } : { position: muzzle(), drawsFlash: false };
|
|
7037
|
+
const muzzlePose = (hand = "R") => {
|
|
7038
|
+
const lent = muzzleSource?.(hand);
|
|
7039
|
+
return lent ? { position: [...lent.position], drawsFlash: lent.drawsFlash } : { position: muzzle(hand), drawsFlash: false };
|
|
6881
7040
|
};
|
|
6882
|
-
const muzzle = () => {
|
|
7041
|
+
const muzzle = (hand = "R") => {
|
|
6883
7042
|
aim();
|
|
6884
7043
|
const e = camera.matrixWorld.elements;
|
|
7044
|
+
const side = hand === "L" ? -0.16 : 0.16;
|
|
6885
7045
|
return [
|
|
6886
|
-
aimO[0] + aimD[0] * 0.45 + e[0] *
|
|
6887
|
-
aimO[1] + aimD[1] * 0.45 + e[1] *
|
|
6888
|
-
aimO[2] + aimD[2] * 0.45 + e[2] *
|
|
7046
|
+
aimO[0] + aimD[0] * 0.45 + e[0] * side - e[4] * 0.1,
|
|
7047
|
+
aimO[1] + aimD[1] * 0.45 + e[1] * side - e[5] * 0.1,
|
|
7048
|
+
aimO[2] + aimD[2] * 0.45 + e[2] * side - e[6] * 0.1
|
|
6889
7049
|
];
|
|
6890
7050
|
};
|
|
6891
7051
|
const tracers = [];
|
|
@@ -7065,10 +7225,10 @@ function createTestRoom(options) {
|
|
|
7065
7225
|
renderer.impulse(objectId, push2.impulse, push2.point);
|
|
7066
7226
|
}
|
|
7067
7227
|
};
|
|
7068
|
-
const fireShots = (shots, recoil) => {
|
|
7228
|
+
const fireShots = (shots, recoil, id = toolId, hand = "R") => {
|
|
7069
7229
|
if (shots.length === 0) return;
|
|
7070
|
-
const w = toolName();
|
|
7071
|
-
const { position: m, drawsFlash } = muzzlePose();
|
|
7230
|
+
const w = toolName(id);
|
|
7231
|
+
const { position: m, drawsFlash } = muzzlePose(hand);
|
|
7072
7232
|
if (!drawsFlash) {
|
|
7073
7233
|
renderer.emit("flash", m, [aimD[0], aimD[1], aimD[2]], 1, 0);
|
|
7074
7234
|
renderer.emit("sparks", m, [aimD[0], aimD[1], aimD[2]], 4, 6);
|
|
@@ -7107,7 +7267,7 @@ function createTestRoom(options) {
|
|
|
7107
7267
|
objectId: f?.object,
|
|
7108
7268
|
itemId: f ? entryOf(f.object)?.so.itemId : void 0,
|
|
7109
7269
|
part: f?.part,
|
|
7110
|
-
data: { weapon:
|
|
7270
|
+
data: { weapon: id, shots: shots.length, hits, energyJ: Math.round(energy), caliberMm: shots[0].caliber, distanceM: f ? Math.round(f.distance * 100) / 100 : null, ...hand === "L" ? { hand } : {} }
|
|
7111
7271
|
});
|
|
7112
7272
|
};
|
|
7113
7273
|
const fireRays = (u) => {
|
|
@@ -7211,7 +7371,7 @@ function createTestRoom(options) {
|
|
|
7211
7371
|
const kickLeg = options.kickLeg ?? "alternate";
|
|
7212
7372
|
let nextLeg = "right";
|
|
7213
7373
|
const punchHand = options.punchHand ?? "alternate";
|
|
7214
|
-
const punchTools = new Set(options.punchTools ?? ["hand", "fists"]);
|
|
7374
|
+
const punchTools = new Set(options.punchTools ?? ["hand", "fists", "brass_knuckles"]);
|
|
7215
7375
|
let nextHand = "right";
|
|
7216
7376
|
const combo = options.strikeCombo;
|
|
7217
7377
|
let comboOn = !!combo && options.strikeComboOn === true;
|
|
@@ -7225,16 +7385,16 @@ function createTestRoom(options) {
|
|
|
7225
7385
|
action("combo", t(on ? "act.combo.on" : "act.combo.off", { name }), { data: { combo: combo.id, on } });
|
|
7226
7386
|
say(t(on ? "act.combo.on" : "act.combo.off", { name }));
|
|
7227
7387
|
};
|
|
7228
|
-
const strikeUse = (u, id = toolId) => {
|
|
7388
|
+
const strikeUse = (u, id = toolId, side) => {
|
|
7229
7389
|
const stepIndex = comboOn && combo?.tools.includes(id) && combo.steps.length > 0 ? simTime - comboAt > combo.reset ? 0 : comboNext : null;
|
|
7230
7390
|
const step2 = stepIndex === null ? void 0 : combo.steps[stepIndex];
|
|
7231
7391
|
const clip = step2 ? step2.clip : strikeClips[id];
|
|
7232
7392
|
const leg = id === "kick" ? kickLeg === "alternate" ? nextLeg : kickLeg : void 0;
|
|
7233
|
-
const hand = step2 ? step2.hand : punchTools.has(id) ? punchHand === "alternate" ? nextHand : punchHand : void 0;
|
|
7234
|
-
const
|
|
7235
|
-
const seconds = clip === void 0 ? null : avatar?.strike?.(clip,
|
|
7393
|
+
const hand = side ? side.hand : step2 ? step2.hand : punchTools.has(id) ? punchHand === "alternate" ? nextHand : punchHand : void 0;
|
|
7394
|
+
const limb = leg ? { leg } : hand ? { hand } : void 0;
|
|
7395
|
+
const seconds = clip === void 0 ? null : avatar?.strike?.(clip, limb) ?? null;
|
|
7236
7396
|
if (leg && seconds !== null && kickLeg === "alternate") nextLeg = leg === "right" ? "left" : "right";
|
|
7237
|
-
if (!step2 && hand && seconds !== null && punchHand === "alternate") nextHand = hand === "right" ? "left" : "right";
|
|
7397
|
+
if (!step2 && !side && hand && seconds !== null && punchHand === "alternate") nextHand = hand === "right" ? "left" : "right";
|
|
7238
7398
|
if (step2 && stepIndex !== null) {
|
|
7239
7399
|
comboNext = (stepIndex + 1) % combo.steps.length;
|
|
7240
7400
|
comboAt = simTime;
|
|
@@ -7255,7 +7415,7 @@ function createTestRoom(options) {
|
|
|
7255
7415
|
data: { weapon: id, clip, impactIn: seconds, ...leg ? { leg } : {}, ...hand ? { hand } : {}, ...step2 ? { combo: combo.id, step: stepIndex } : {} }
|
|
7256
7416
|
});
|
|
7257
7417
|
strikes.push({
|
|
7258
|
-
key: id,
|
|
7418
|
+
key: side?.key ?? id,
|
|
7259
7419
|
remaining: seconds,
|
|
7260
7420
|
...swing && seconds > lead ? { swing, lead } : {},
|
|
7261
7421
|
land: () => {
|
|
@@ -7385,20 +7545,50 @@ function createTestRoom(options) {
|
|
|
7385
7545
|
say,
|
|
7386
7546
|
projectileModel: options.projectileModel
|
|
7387
7547
|
});
|
|
7388
|
-
const
|
|
7548
|
+
const toolOf = (hand) => hand === "L" ? offTool && offToolId ? { id: offToolId, state: offTool } : null : { id: toolId, state: tool };
|
|
7549
|
+
const dualReload = createDualReload(
|
|
7550
|
+
{
|
|
7551
|
+
needs: (hand) => {
|
|
7552
|
+
const state = toolOf(hand)?.state;
|
|
7553
|
+
return state !== void 0 && (state.def.action.kind === "firearm" || state.def.action.kind === "launcher") && state.ammo < state.capacity && state.reserve > 0;
|
|
7554
|
+
},
|
|
7555
|
+
startReload: (hand) => startReload(hand),
|
|
7556
|
+
reloading: (hand) => toolOf(hand)?.state.reloading ?? false,
|
|
7557
|
+
stow: (hand, on, seconds) => {
|
|
7558
|
+
const slot2 = toolOf(hand);
|
|
7559
|
+
if (!slot2) return;
|
|
7560
|
+
if (on) slot2.state.release();
|
|
7561
|
+
emitHost({ type: "stow", weapon: slot2.id, on, duration: seconds, ...hand === "L" ? { hand } : {} });
|
|
7562
|
+
action("stow", t(on ? "act.stow" : "act.unstow", { weapon: toolName(slot2.id), hand: t(hand === "L" ? "hand.left" : "hand.right") }), { data: { weapon: slot2.id, hand, on, seconds } });
|
|
7563
|
+
}
|
|
7564
|
+
},
|
|
7565
|
+
() => arsenal.rules.dualWield.stowSeconds
|
|
7566
|
+
);
|
|
7567
|
+
const strikeKeyOf = (id, hand) => hand === "L" ? `${id}${LEFT_HAND_KEY}` : id;
|
|
7568
|
+
const useTool = (dt, held, hand = "R") => {
|
|
7389
7569
|
aim();
|
|
7390
|
-
const
|
|
7570
|
+
const slot2 = toolOf(hand);
|
|
7571
|
+
if (!slot2) return "cooldown";
|
|
7572
|
+
const { id, state } = slot2;
|
|
7573
|
+
if (offTool && dualReload.busy) {
|
|
7574
|
+
if (!held) say(t("hud.reloading"));
|
|
7575
|
+
return "reloading";
|
|
7576
|
+
}
|
|
7577
|
+
const cat = state.def.category;
|
|
7391
7578
|
if (cat === "hand") {
|
|
7392
7579
|
if (strikes.some((strike) => strike.key === "hand" || strike.key === "fists")) return "cooldown";
|
|
7393
7580
|
strikeUse({ kind: "melee", damage: "blunt", energy: 25, radius: 0.1, reach: 0.8, origin: [aimO[0], aimO[1], aimO[2]], direction: [aimD[0], aimD[1], aimD[2]] }, "hand");
|
|
7394
7581
|
return "ok";
|
|
7395
7582
|
}
|
|
7396
|
-
if (strikes.some((strike) => strike.key ===
|
|
7397
|
-
const res =
|
|
7398
|
-
flushEvents(
|
|
7583
|
+
if (strikes.some((strike) => strike.key === strikeKeyOf(id, hand))) return "cooldown";
|
|
7584
|
+
const res = state.trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], dt, held, "player");
|
|
7585
|
+
flushEvents(id, state, res.recoil, hand);
|
|
7399
7586
|
if (res.status === "empty") {
|
|
7400
|
-
if (!
|
|
7401
|
-
|
|
7587
|
+
if (!state.reloading && state.reserve > 0) {
|
|
7588
|
+
if (offTool) dualReload.start([hand, hand === "R" ? "L" : "R"]);
|
|
7589
|
+
else startReload(hand);
|
|
7590
|
+
}
|
|
7591
|
+
say(t(state.reloading ? "hud.reloading" : "hud.empty"));
|
|
7402
7592
|
return res.status;
|
|
7403
7593
|
}
|
|
7404
7594
|
if (res.status !== "ok") {
|
|
@@ -7408,15 +7598,18 @@ function createTestRoom(options) {
|
|
|
7408
7598
|
else if (res.status === "safe") say(t("hud.safe"));
|
|
7409
7599
|
return res.status;
|
|
7410
7600
|
}
|
|
7411
|
-
|
|
7601
|
+
firedHand = hand;
|
|
7602
|
+
applyUses(res, dt, hand);
|
|
7412
7603
|
return res.status;
|
|
7413
7604
|
};
|
|
7414
|
-
const applyUses = (res, dt) => {
|
|
7415
|
-
|
|
7605
|
+
const applyUses = (res, dt, hand = "R") => {
|
|
7606
|
+
const id = toolOf(hand)?.id ?? toolId;
|
|
7607
|
+
if (res.shots.length) fireShots(res.shots, res.recoil, id, hand);
|
|
7416
7608
|
for (const u of res.uses) {
|
|
7417
7609
|
switch (u.kind) {
|
|
7418
7610
|
case "melee":
|
|
7419
|
-
strikeUse(u);
|
|
7611
|
+
if (hand === "L") strikeUse(u, id, { hand: "left", key: strikeKeyOf(id, hand) });
|
|
7612
|
+
else strikeUse(u);
|
|
7420
7613
|
break;
|
|
7421
7614
|
case "flame":
|
|
7422
7615
|
heatRays(u, "fire");
|
|
@@ -7444,15 +7637,17 @@ function createTestRoom(options) {
|
|
|
7444
7637
|
}
|
|
7445
7638
|
}
|
|
7446
7639
|
};
|
|
7447
|
-
const releaseTrigger = () => {
|
|
7448
|
-
|
|
7449
|
-
|
|
7640
|
+
const releaseTrigger = (hand = "R") => {
|
|
7641
|
+
const slot2 = toolOf(hand);
|
|
7642
|
+
if (!slot2) return;
|
|
7643
|
+
if (!slot2.state.cooking) {
|
|
7644
|
+
slot2.state.release();
|
|
7450
7645
|
return;
|
|
7451
7646
|
}
|
|
7452
7647
|
aim();
|
|
7453
|
-
const res =
|
|
7454
|
-
flushEvents(
|
|
7455
|
-
if (res) applyUses(res, 0);
|
|
7648
|
+
const res = slot2.state.release([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]]);
|
|
7649
|
+
flushEvents(slot2.id, slot2.state, void 0, hand);
|
|
7650
|
+
if (res) applyUses(res, 0, hand);
|
|
7456
7651
|
};
|
|
7457
7652
|
const kick = () => {
|
|
7458
7653
|
if (!arsenal.weapons.has("kick") || strikes.some((strike) => strike.key === "kick")) return false;
|
|
@@ -7481,8 +7676,43 @@ function createTestRoom(options) {
|
|
|
7481
7676
|
}));
|
|
7482
7677
|
};
|
|
7483
7678
|
const toggleToolMenu = (open) => ui?.toolMenu(open) ?? false;
|
|
7679
|
+
const dropOffTool = () => {
|
|
7680
|
+
if (!offTool) return;
|
|
7681
|
+
releaseTrigger("L");
|
|
7682
|
+
offTool = null;
|
|
7683
|
+
offToolId = null;
|
|
7684
|
+
offHeld = false;
|
|
7685
|
+
offFirst = false;
|
|
7686
|
+
dualReload.cancel();
|
|
7687
|
+
firedHand = "R";
|
|
7688
|
+
tool.setStance({ dualWield: false });
|
|
7689
|
+
action("tool_left", t("act.tool_left_off"), { data: { hand: "L" } });
|
|
7690
|
+
emitHost({ type: "tool", weapon: "", hand: "L" });
|
|
7691
|
+
};
|
|
7692
|
+
const setOffTool = (id) => {
|
|
7693
|
+
if (id === null) {
|
|
7694
|
+
dropOffTool();
|
|
7695
|
+
return true;
|
|
7696
|
+
}
|
|
7697
|
+
if (!arsenal.weapons.has(id)) throw new Error(`game-test-room: unknown weapon "${id}"`);
|
|
7698
|
+
if (!canDualWield(arsenal, id) || !canDualWield(arsenal, toolId)) return false;
|
|
7699
|
+
if (offTool) releaseTrigger("L");
|
|
7700
|
+
const state = arsenal.create(id, { seed: OFF_HAND_SEED, reserve: 999, ...toolSettings });
|
|
7701
|
+
state.setFuseLength(fuseLength);
|
|
7702
|
+
state.refill();
|
|
7703
|
+
state.setStance({ dualWield: true });
|
|
7704
|
+
tool.setStance({ dualWield: true });
|
|
7705
|
+
offTool = state;
|
|
7706
|
+
offToolId = id;
|
|
7707
|
+
dualReload.cancel();
|
|
7708
|
+
action("tool_left", t("act.tool_left", { weapon: toolName(id) }), { data: { weapon: id, hand: "L" } });
|
|
7709
|
+
state.draw();
|
|
7710
|
+
flushEvents(id, state, void 0, "L");
|
|
7711
|
+
return true;
|
|
7712
|
+
};
|
|
7484
7713
|
const setTool = (id) => {
|
|
7485
7714
|
if (!arsenal.weapons.has(id)) throw new Error(`game-test-room: unknown weapon "${id}"`);
|
|
7715
|
+
dropOffTool();
|
|
7486
7716
|
releaseTrigger();
|
|
7487
7717
|
toolId = id;
|
|
7488
7718
|
const i = loadout.indexOf(id);
|
|
@@ -7975,15 +8205,13 @@ function createTestRoom(options) {
|
|
|
7975
8205
|
case "KeyR":
|
|
7976
8206
|
if (heldPiece) heldYaw += Math.PI / 8;
|
|
7977
8207
|
else {
|
|
7978
|
-
|
|
8208
|
+
reloadHands();
|
|
7979
8209
|
say(t("hud.reloading"));
|
|
7980
8210
|
}
|
|
7981
8211
|
break;
|
|
7982
|
-
case "KeyQ":
|
|
7983
|
-
|
|
7984
|
-
flushEvents(toolId, tool);
|
|
8212
|
+
case "KeyQ":
|
|
8213
|
+
cycleToolMode();
|
|
7985
8214
|
break;
|
|
7986
|
-
}
|
|
7987
8215
|
case "KeyX":
|
|
7988
8216
|
clearToolFault();
|
|
7989
8217
|
break;
|
|
@@ -8040,6 +8268,20 @@ function createTestRoom(options) {
|
|
|
8040
8268
|
if (!loadout.includes(weaponId)) loadout.push(weaponId);
|
|
8041
8269
|
setTool(weaponId);
|
|
8042
8270
|
};
|
|
8271
|
+
const pickTool = (weaponId, pick = { second: false }) => {
|
|
8272
|
+
if (!arsenal.weapons.has(weaponId)) return;
|
|
8273
|
+
if (!pick.second) {
|
|
8274
|
+
equip(weaponId);
|
|
8275
|
+
return;
|
|
8276
|
+
}
|
|
8277
|
+
if (!canDualWield(arsenal, weaponId)) {
|
|
8278
|
+
equip(weaponId);
|
|
8279
|
+
say(t("hud.no_dual", { weapon: toolName(weaponId) }));
|
|
8280
|
+
return;
|
|
8281
|
+
}
|
|
8282
|
+
if (!offTool || !canDualWield(arsenal, toolId)) equip(weaponId);
|
|
8283
|
+
setOffTool(weaponId);
|
|
8284
|
+
};
|
|
8043
8285
|
let hudAimHit;
|
|
8044
8286
|
const inspectTarget = () => {
|
|
8045
8287
|
if (hudAimHit !== void 0) return hudAimHit;
|
|
@@ -8243,6 +8485,11 @@ function createTestRoom(options) {
|
|
|
8243
8485
|
s.update(dt);
|
|
8244
8486
|
flushEvents(id, s);
|
|
8245
8487
|
}
|
|
8488
|
+
if (offTool && offToolId) {
|
|
8489
|
+
offTool.update(dt);
|
|
8490
|
+
flushEvents(offToolId, offTool, void 0, "L");
|
|
8491
|
+
dualReload.step(dt);
|
|
8492
|
+
}
|
|
8246
8493
|
if (audio) {
|
|
8247
8494
|
const look = camera.getWorldDirection(listenerDirection);
|
|
8248
8495
|
audio.setListener({ position: [camera.position.x, camera.position.y, camera.position.z], yaw: Math.atan2(-look.x, -look.z) });
|
|
@@ -8288,6 +8535,10 @@ function createTestRoom(options) {
|
|
|
8288
8535
|
} else if (useHeld) useTool(dt, !useFirst);
|
|
8289
8536
|
useFirst = false;
|
|
8290
8537
|
}
|
|
8538
|
+
if ((offHeld || offFirst) && offTool && !isPaused && !push.active) {
|
|
8539
|
+
useTool(dt, !offFirst, "L");
|
|
8540
|
+
offFirst = false;
|
|
8541
|
+
}
|
|
8291
8542
|
if (!isPaused) followHeld(dt);
|
|
8292
8543
|
};
|
|
8293
8544
|
const updatePresent = (dt) => {
|
|
@@ -8314,12 +8565,18 @@ function createTestRoom(options) {
|
|
|
8314
8565
|
hudAimHit = void 0;
|
|
8315
8566
|
}
|
|
8316
8567
|
};
|
|
8568
|
+
const ammoText = (state) => {
|
|
8569
|
+
const kind = state.def.action.kind;
|
|
8570
|
+
if (kind === "firearm") return `${state.ammo}/${state.capacity}${state.reserve ? ` \xB7 ${state.reserve}` : ""}`;
|
|
8571
|
+
return kind === "hand" || kind === "melee" || kind === "tool" ? "" : `${num(state.ammo, 0)}`;
|
|
8572
|
+
};
|
|
8317
8573
|
const readHudState = () => {
|
|
8318
8574
|
const page = Math.floor(toolIndex / 10);
|
|
8319
8575
|
const slots = loadout.slice(page * 10, page * 10 + 10).map((id) => arsenal.name(id));
|
|
8320
|
-
const
|
|
8576
|
+
const left = offTool ? ammoText(offTool) : "";
|
|
8577
|
+
const ammo = `${ammoText(tool)}${left ? ` | ${t("hud.left")} ${left}` : ""}`;
|
|
8321
8578
|
return {
|
|
8322
|
-
toolName: toolName(),
|
|
8579
|
+
toolName: offToolId ? `${toolName()} + ${toolName(offToolId)}` : toolName(),
|
|
8323
8580
|
toolId,
|
|
8324
8581
|
ammo,
|
|
8325
8582
|
slots,
|
|
@@ -8509,26 +8766,53 @@ function createTestRoom(options) {
|
|
|
8509
8766
|
if (event.type === "explosion") knockPlayer(event);
|
|
8510
8767
|
for (const listener of hostListeners) listener(event);
|
|
8511
8768
|
}
|
|
8512
|
-
function flushEvents(id, state, recoil) {
|
|
8769
|
+
function flushEvents(id, state, recoil, hand = "R") {
|
|
8513
8770
|
const phase = state.phase;
|
|
8514
8771
|
for (const event of state.drain()) {
|
|
8515
8772
|
const hostEvent = toHostEvent(id, event, { ...recoil ? { recoil } : {}, phase });
|
|
8516
|
-
if (hostEvent) emitHost(hostEvent);
|
|
8773
|
+
if (hostEvent) emitHost(hand === "L" ? { ...hostEvent, hand } : hostEvent);
|
|
8517
8774
|
}
|
|
8518
8775
|
}
|
|
8519
|
-
function startReload() {
|
|
8520
|
-
|
|
8521
|
-
|
|
8776
|
+
function startReload(hand = "R") {
|
|
8777
|
+
const slot2 = toolOf(hand);
|
|
8778
|
+
if (!slot2) return false;
|
|
8779
|
+
const started = slot2.state.reload();
|
|
8780
|
+
flushEvents(slot2.id, slot2.state, void 0, hand);
|
|
8781
|
+
return started;
|
|
8782
|
+
}
|
|
8783
|
+
function reloadHands() {
|
|
8784
|
+
if (!offTool) {
|
|
8785
|
+
startReload();
|
|
8786
|
+
return;
|
|
8787
|
+
}
|
|
8788
|
+
dualReload.start(["R", "L"]);
|
|
8522
8789
|
}
|
|
8790
|
+
const commandHand = () => offTool ? firedHand : "R";
|
|
8523
8791
|
function clearToolFault() {
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8792
|
+
const order = offTool ? [firedHand, firedHand === "R" ? "L" : "R"] : ["R"];
|
|
8793
|
+
for (const hand of order) {
|
|
8794
|
+
const slot2 = toolOf(hand);
|
|
8795
|
+
if (!slot2?.state.fault || !slot2.state.clearFault()) continue;
|
|
8796
|
+
flushEvents(slot2.id, slot2.state, void 0, hand);
|
|
8797
|
+
say(t("hud.clearing"));
|
|
8798
|
+
return;
|
|
8799
|
+
}
|
|
8527
8800
|
}
|
|
8528
8801
|
function toggleSafety() {
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8802
|
+
const hand = commandHand();
|
|
8803
|
+
const slot2 = toolOf(hand);
|
|
8804
|
+
if (!slot2) return;
|
|
8805
|
+
slot2.state.setSafety(!slot2.state.safety);
|
|
8806
|
+
flushEvents(slot2.id, slot2.state, void 0, hand);
|
|
8807
|
+
say(`${t(slot2.state.safety ? "hud.safety_on" : "hud.safety_off")}${offTool ? ` (${toolName(slot2.id)}, ${t(hand === "L" ? "hand.left" : "hand.right")})` : ""}`);
|
|
8808
|
+
}
|
|
8809
|
+
function cycleToolMode() {
|
|
8810
|
+
const hand = commandHand();
|
|
8811
|
+
const slot2 = toolOf(hand);
|
|
8812
|
+
if (!slot2) return;
|
|
8813
|
+
slot2.state.cycleMode();
|
|
8814
|
+
flushEvents(slot2.id, slot2.state, void 0, hand);
|
|
8815
|
+
if (offTool) say(`${toolName(slot2.id)} (${t(hand === "L" ? "hand.left" : "hand.right")}): ${slot2.state.mode}`);
|
|
8532
8816
|
}
|
|
8533
8817
|
function runToolCommand(request) {
|
|
8534
8818
|
const work = (done) => done.ok ? { ok: true, seconds: done.seconds } : { ok: false, reason: done.reason };
|
|
@@ -8550,6 +8834,24 @@ function createTestRoom(options) {
|
|
|
8550
8834
|
return { ok: false, reason: "unknown" };
|
|
8551
8835
|
}
|
|
8552
8836
|
}
|
|
8837
|
+
const hostToolOf = (id, state) => ({
|
|
8838
|
+
weapon: id,
|
|
8839
|
+
category: state.def.category,
|
|
8840
|
+
ammo: state.ammo,
|
|
8841
|
+
capacity: state.capacity,
|
|
8842
|
+
reserve: state.reserve,
|
|
8843
|
+
reloading: state.reloading,
|
|
8844
|
+
mode: state.mode,
|
|
8845
|
+
...state.fault ? { fault: state.fault } : {},
|
|
8846
|
+
safety: state.safety,
|
|
8847
|
+
heat: state.heatLevel,
|
|
8848
|
+
attachments: state.attachments.map((part) => ({ id: part.id, mount: part.mount, ...part.integral ? { integral: true } : {}, ...part.on === void 0 ? {} : { on: part.on } })),
|
|
8849
|
+
stock: state.stock,
|
|
8850
|
+
suppressed: state.suppressed,
|
|
8851
|
+
underbarrelActive: state.underbarrelActive,
|
|
8852
|
+
mass: state.mass,
|
|
8853
|
+
aimSeconds: state.aimSeconds
|
|
8854
|
+
});
|
|
8553
8855
|
const host = {
|
|
8554
8856
|
THREE,
|
|
8555
8857
|
scene,
|
|
@@ -8576,22 +8878,9 @@ function createTestRoom(options) {
|
|
|
8576
8878
|
...options.blastShove ? { blastShove: options.blastShove } : {},
|
|
8577
8879
|
blastReach: (point, target, ignore) => world.blastReach(point, target, ignore ? { ignore } : {}),
|
|
8578
8880
|
tool: () => ({
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
|
|
8582
|
-
capacity: tool.capacity,
|
|
8583
|
-
reserve: tool.reserve,
|
|
8584
|
-
reloading: tool.reloading,
|
|
8585
|
-
mode: tool.mode,
|
|
8586
|
-
...tool.fault ? { fault: tool.fault } : {},
|
|
8587
|
-
safety: tool.safety,
|
|
8588
|
-
heat: tool.heatLevel,
|
|
8589
|
-
attachments: tool.attachments.map((part) => ({ id: part.id, mount: part.mount, ...part.integral ? { integral: true } : {}, ...part.on === void 0 ? {} : { on: part.on } })),
|
|
8590
|
-
stock: tool.stock,
|
|
8591
|
-
suppressed: tool.suppressed,
|
|
8592
|
-
underbarrelActive: tool.underbarrelActive,
|
|
8593
|
-
mass: tool.mass,
|
|
8594
|
-
aimSeconds: tool.aimSeconds
|
|
8881
|
+
...hostToolOf(toolId, tool),
|
|
8882
|
+
...dualReload.stowed("R") ? { stowed: true } : {},
|
|
8883
|
+
...offTool && offToolId ? { offHand: { ...hostToolOf(offToolId, offTool), ...dualReload.stowed("L") ? { stowed: true } : {} } } : {}
|
|
8595
8884
|
}),
|
|
8596
8885
|
command(request) {
|
|
8597
8886
|
const result = runToolCommand(request);
|
|
@@ -8628,7 +8917,7 @@ function createTestRoom(options) {
|
|
|
8628
8917
|
onFrame(update, frameOptions) {
|
|
8629
8918
|
return scheduler.add((ctx) => update(ctx.dt), { phase: frameOptions?.phase ?? "lateSimulate", order: frameOptions?.order ?? 0, id: `module.frame#${moduleFrameCount++}` });
|
|
8630
8919
|
},
|
|
8631
|
-
holdFrame: () => bodyHoldFrame(),
|
|
8920
|
+
holdFrame: (hand) => bodyHoldFrame(hand),
|
|
8632
8921
|
reachFor(reach) {
|
|
8633
8922
|
if (!avatar) {
|
|
8634
8923
|
reach.onGrip?.();
|
|
@@ -9004,6 +9293,7 @@ function createTestRoom(options) {
|
|
|
9004
9293
|
fluids,
|
|
9005
9294
|
film: filmBridge,
|
|
9006
9295
|
vfx: vfx.world,
|
|
9296
|
+
effects: vfx,
|
|
9007
9297
|
fluidRenderer: fluidView,
|
|
9008
9298
|
audio,
|
|
9009
9299
|
environment,
|
|
@@ -9019,6 +9309,29 @@ function createTestRoom(options) {
|
|
|
9019
9309
|
clear,
|
|
9020
9310
|
resetAll,
|
|
9021
9311
|
setTool,
|
|
9312
|
+
setOffTool,
|
|
9313
|
+
get offTool() {
|
|
9314
|
+
return offTool;
|
|
9315
|
+
},
|
|
9316
|
+
useOff(hold = 0) {
|
|
9317
|
+
if (!offTool) return "cooldown";
|
|
9318
|
+
const status = useTool(FIXED_DT, false, "L");
|
|
9319
|
+
flush();
|
|
9320
|
+
let t0 = 0;
|
|
9321
|
+
while (t0 < hold) {
|
|
9322
|
+
useTool(FIXED_DT, true, "L");
|
|
9323
|
+
directStep = true;
|
|
9324
|
+
try {
|
|
9325
|
+
runHeadless(() => scheduler.advance(FIXED_DT));
|
|
9326
|
+
} finally {
|
|
9327
|
+
directStep = false;
|
|
9328
|
+
}
|
|
9329
|
+
t0 += FIXED_DT;
|
|
9330
|
+
}
|
|
9331
|
+
releaseTrigger("L");
|
|
9332
|
+
return status;
|
|
9333
|
+
},
|
|
9334
|
+
reload: () => reloadHands(),
|
|
9022
9335
|
useItem,
|
|
9023
9336
|
use(hold = 0) {
|
|
9024
9337
|
if (tool.def.category === "hand") {
|
|
@@ -9316,7 +9629,8 @@ function createTestRoom(options) {
|
|
|
9316
9629
|
...options.toolMenu !== false ? {
|
|
9317
9630
|
toolMenu: {
|
|
9318
9631
|
groups: toolMenuGroups,
|
|
9319
|
-
onPick:
|
|
9632
|
+
onPick: pickTool,
|
|
9633
|
+
currentLeft: () => offToolId,
|
|
9320
9634
|
// the list takes clicks: the mouse leaves the view while it is open
|
|
9321
9635
|
onOpen: (open) => {
|
|
9322
9636
|
if (open && document.pointerLockElement) document.exitPointerLock?.();
|