@adatechnology/game-test-room 0.28.1 → 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 +2543 -281
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +429 -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",
|
|
@@ -3576,6 +3678,7 @@ var en = {
|
|
|
3576
3678
|
"act.throw": "Threw {name} at {speed} m/s",
|
|
3577
3679
|
"ground.name": "Ground",
|
|
3578
3680
|
"act.landing": "{name} landed at {speed} m/s: {energy} J",
|
|
3681
|
+
"act.ground_gave_way": "the bare bars could not hold the weight on them: the slab gave way",
|
|
3579
3682
|
"act.player_land": "You landed at {speed} m/s",
|
|
3580
3683
|
"act.bench_failed": "{name} failed: {error}",
|
|
3581
3684
|
"loading.working": "Working\u2026",
|
|
@@ -3700,6 +3803,7 @@ var en = {
|
|
|
3700
3803
|
"ui.clear_log": "Clear",
|
|
3701
3804
|
"ui.catalog": "Catalogue",
|
|
3702
3805
|
"ui.tool_menu": "Weapons",
|
|
3806
|
+
"ui.tool_menu.dual": "Shift+click: one in each hand",
|
|
3703
3807
|
"ui.tool_menu.search": "Search weapons and tools",
|
|
3704
3808
|
"ui.tool_menu.close": "Close",
|
|
3705
3809
|
"ui.tool_menu.results": "{n} found",
|
|
@@ -3903,6 +4007,12 @@ var ptBR = {
|
|
|
3903
4007
|
"act.clear": "Limpou a sala",
|
|
3904
4008
|
"act.reset": "Consertou todos os objetos",
|
|
3905
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",
|
|
3906
4016
|
"act.shoot.hit": "{weapon}: disparo, {energy} J, acertou {where} a {dist} m",
|
|
3907
4017
|
"act.shoot.miss": "{weapon}: disparo, {energy} J, n\xE3o acertou nada",
|
|
3908
4018
|
"act.shoot.pellets": "{weapon}: disparo, {n} proj\xE9teis de at\xE9 {energy} J, {hits} acertaram {where} a {dist} m",
|
|
@@ -3932,6 +4042,7 @@ var ptBR = {
|
|
|
3932
4042
|
"hud.no_charges": "Nenhuma carga posta espera o detonador",
|
|
3933
4043
|
"blast.zone.lethal": "letal",
|
|
3934
4044
|
"blast.zone.injury": "de ferimento",
|
|
4045
|
+
"act.smoke": "{weapon}: fuma\xE7a, queima por {seconds} s",
|
|
3935
4046
|
"act.explode": "{weapon}: explodiu, {energy} J em {radius} m, {n} objetos ao alcance",
|
|
3936
4047
|
"act.molotov_fire": "{weapon}: fogo no ch\xE3o, raio de {radius} m por {seconds} s",
|
|
3937
4048
|
"act.tool.none": "{weapon}: nada ao alcance",
|
|
@@ -3945,6 +4056,7 @@ var ptBR = {
|
|
|
3945
4056
|
"act.throw": "Arremessou {name} a {speed} m/s",
|
|
3946
4057
|
"ground.name": "Ch\xE3o",
|
|
3947
4058
|
"act.landing": "{name} caiu a {speed} m/s: {energy} J",
|
|
4059
|
+
"act.ground_gave_way": "os vergalh\xF5es expostos n\xE3o aguentaram o peso: a laje cedeu",
|
|
3948
4060
|
"act.player_land": "Voc\xEA aterrissou a {speed} m/s",
|
|
3949
4061
|
"act.bench_failed": "{name} falhou: {error}",
|
|
3950
4062
|
"loading.working": "Trabalhando\u2026",
|
|
@@ -4069,6 +4181,7 @@ var ptBR = {
|
|
|
4069
4181
|
"ui.clear_log": "Limpar",
|
|
4070
4182
|
"ui.catalog": "Cat\xE1logo",
|
|
4071
4183
|
"ui.tool_menu": "Armas",
|
|
4184
|
+
"ui.tool_menu.dual": "Shift+clique: uma em cada m\xE3o",
|
|
4072
4185
|
"ui.tool_menu.search": "Buscar armas e ferramentas",
|
|
4073
4186
|
"ui.tool_menu.close": "Fechar",
|
|
4074
4187
|
"ui.tool_menu.results": "{n} encontradas",
|
|
@@ -4277,6 +4390,27 @@ function createBenchPanel(o) {
|
|
|
4277
4390
|
return wrap;
|
|
4278
4391
|
};
|
|
4279
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
|
+
};
|
|
4280
4414
|
const sheetCard = (sheet) => {
|
|
4281
4415
|
const card = mk("div", "tr-sheet");
|
|
4282
4416
|
if (sheet.kicker) card.appendChild(mk("div", "sub", sheet.kicker));
|
|
@@ -4328,6 +4462,7 @@ function createBenchPanel(o) {
|
|
|
4328
4462
|
playhead = { node: head, metric, end };
|
|
4329
4463
|
card.appendChild(strip);
|
|
4330
4464
|
}
|
|
4465
|
+
if (sheet.image) card.appendChild(sheetImage(sheet.image));
|
|
4331
4466
|
if (sheet.note) card.appendChild(mk("p", "sub", sheet.note));
|
|
4332
4467
|
return card;
|
|
4333
4468
|
};
|
|
@@ -4561,7 +4696,7 @@ function createToolMenu(o) {
|
|
|
4561
4696
|
const close = mk3("button", "tr-btn", "\xD7");
|
|
4562
4697
|
close.type = "button";
|
|
4563
4698
|
close.title = o.t("ui.tool_menu.close");
|
|
4564
|
-
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);
|
|
4565
4700
|
const tabs = mk3("div", "tr-tabs");
|
|
4566
4701
|
const body = mk3("div", "tr-wmenu-body");
|
|
4567
4702
|
element.append(header, tabs, body);
|
|
@@ -4569,15 +4704,17 @@ function createToolMenu(o) {
|
|
|
4569
4704
|
let activeGroup = "";
|
|
4570
4705
|
let groups = [];
|
|
4571
4706
|
const toolButton = (tool, group) => {
|
|
4572
|
-
const
|
|
4707
|
+
const left = o.currentLeft?.() === tool.id;
|
|
4708
|
+
const button = mk3("button", tool.id === o.current() || left ? "tr-btn on" : "tr-btn");
|
|
4573
4709
|
button.type = "button";
|
|
4574
4710
|
button.title = tool.name;
|
|
4711
|
+
if (left) button.dataset.left = "";
|
|
4575
4712
|
button.dataset.tool = tool.id;
|
|
4576
4713
|
if (tool.thumbnail) button.appendChild(o.thumb(tool.thumbnail, tool.thumbnailCredit));
|
|
4577
4714
|
button.appendChild(mk3("span", void 0, tool.name));
|
|
4578
4715
|
if (group) button.appendChild(mk3("small", void 0, group));
|
|
4579
|
-
button.onclick = () => {
|
|
4580
|
-
o.onPick(tool.id);
|
|
4716
|
+
button.onclick = (event) => {
|
|
4717
|
+
o.onPick(tool.id, { second: event.shiftKey });
|
|
4581
4718
|
toggle(false);
|
|
4582
4719
|
};
|
|
4583
4720
|
return button;
|
|
@@ -4673,6 +4810,8 @@ var CSS = `
|
|
|
4673
4810
|
.tr-prog b{display:block;font-weight:500;margin-bottom:3px}
|
|
4674
4811
|
.tr-sheet{margin-top:8px;padding:8px;border:1px solid #2a2e36;border-radius:6px;font-size:12px;line-height:1.4}
|
|
4675
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}
|
|
4676
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}
|
|
4677
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}
|
|
4678
4817
|
.tr-timeline{position:relative;height:22px;margin:8px 0;background:#1b1e24;border-radius:4px;overflow:hidden}
|
|
@@ -4800,7 +4939,7 @@ function createUi(o) {
|
|
|
4800
4939
|
bottom.append(held, hotbar);
|
|
4801
4940
|
let currentTool = "";
|
|
4802
4941
|
const menuOptions = o.toolMenu;
|
|
4803
|
-
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;
|
|
4804
4943
|
if (toolMenu && menuOptions) {
|
|
4805
4944
|
const menuButton = mk4("button", "tr-slot tr-menu-btn");
|
|
4806
4945
|
menuButton.type = "button";
|
|
@@ -5448,6 +5587,8 @@ function createModuleViews(THREE, gl) {
|
|
|
5448
5587
|
|
|
5449
5588
|
// src/room.ts
|
|
5450
5589
|
var FIXED_DT = 1 / 60;
|
|
5590
|
+
var LEFT_HAND_KEY = "#L";
|
|
5591
|
+
var OFF_HAND_SEED = 11;
|
|
5451
5592
|
var TOOL_MENU_KEY_LABELS = { Backquote: "'" };
|
|
5452
5593
|
var EXPLOSIVE_EFFECT_IDS = ["m67", "molotov", "pipe_bomb", "m79_40mm", "m72_66mm", "at4_84mm", "dynamite", "c4", "claymore"];
|
|
5453
5594
|
var DEFAULT_PARTICLE_LIMIT = 900;
|
|
@@ -5465,6 +5606,11 @@ var GROUND_ID = "room_ground";
|
|
|
5465
5606
|
var TERRAIN_CELL = 0.1;
|
|
5466
5607
|
var GROUND_SPLASH_MAX_LITRES = 3;
|
|
5467
5608
|
var WATER_ENTRY_SPEED = 1.5;
|
|
5609
|
+
var LOAD_EVERY = 0.2;
|
|
5610
|
+
var RESTING_GAP = 0.06;
|
|
5611
|
+
var PLAYER_FOOTPRINT = 0.25;
|
|
5612
|
+
var RESTING_SPEED = 0.3;
|
|
5613
|
+
var GRAVITY_EARTH = 9.81;
|
|
5468
5614
|
var TERRAIN_MAX_CELLS = 1e3;
|
|
5469
5615
|
var GROUND_BURST_MAX = 28;
|
|
5470
5616
|
var GROUND_BURST_PER_METRE = 30;
|
|
@@ -5738,6 +5884,11 @@ function createTestRoom(options) {
|
|
|
5738
5884
|
return s;
|
|
5739
5885
|
};
|
|
5740
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";
|
|
5741
5892
|
const catalogMap = /* @__PURE__ */ new Map();
|
|
5742
5893
|
for (const i of options.catalog) catalogMap.set(i.id, i);
|
|
5743
5894
|
const catalog = [...options.catalog];
|
|
@@ -5947,9 +6098,19 @@ function createTestRoom(options) {
|
|
|
5947
6098
|
releaseTrigger();
|
|
5948
6099
|
},
|
|
5949
6100
|
secondary: () => {
|
|
6101
|
+
if (offTool && !heldEntry) {
|
|
6102
|
+
offHeld = true;
|
|
6103
|
+
offFirst = true;
|
|
6104
|
+
return;
|
|
6105
|
+
}
|
|
5950
6106
|
if (heldEntry) throwHeld();
|
|
5951
6107
|
else grab();
|
|
5952
6108
|
},
|
|
6109
|
+
secondaryUp: () => {
|
|
6110
|
+
if (!offHeld) return;
|
|
6111
|
+
offHeld = false;
|
|
6112
|
+
releaseTrigger("L");
|
|
6113
|
+
},
|
|
5953
6114
|
key: (code, down, e) => onKey(code, down, e),
|
|
5954
6115
|
wheel: (dir) => cycleTool(dir)
|
|
5955
6116
|
}
|
|
@@ -6488,6 +6649,10 @@ function createTestRoom(options) {
|
|
|
6488
6649
|
for (const listener of [...resetListeners]) listener();
|
|
6489
6650
|
tools.clear();
|
|
6490
6651
|
tool = toolState(toolId);
|
|
6652
|
+
dualReload.cancel();
|
|
6653
|
+
offTool = null;
|
|
6654
|
+
offToolId = null;
|
|
6655
|
+
firedHand = "R";
|
|
6491
6656
|
if (pristineWorld) {
|
|
6492
6657
|
for (const id of world.ids()) world.remove(id);
|
|
6493
6658
|
world.load(pristineWorld);
|
|
@@ -6753,12 +6918,13 @@ function createTestRoom(options) {
|
|
|
6753
6918
|
const pocketAt = new THREE.Vector3();
|
|
6754
6919
|
const aimTurn = new THREE.Quaternion();
|
|
6755
6920
|
const aimEuler = new THREE.Euler(0, 0, 0, "YXZ");
|
|
6756
|
-
const bodyHoldFrame = () => {
|
|
6757
|
-
const shoulder = avatar?.shoulder?.(
|
|
6921
|
+
const bodyHoldFrame = (hand = "R") => {
|
|
6922
|
+
const shoulder = avatar?.shoulder?.(hand);
|
|
6758
6923
|
if (!shoulder) return null;
|
|
6759
6924
|
shoulder.getWorldPosition(pocketAt);
|
|
6760
|
-
|
|
6761
|
-
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;
|
|
6762
6928
|
aimTurn.setFromEuler(aimEuler.set(player.pitch, player.yaw, 0, "YXZ"));
|
|
6763
6929
|
return { position: [pocketAt.x, pocketAt.y, pocketAt.z], quaternion: [aimTurn.x, aimTurn.y, aimTurn.z, aimTurn.w] };
|
|
6764
6930
|
};
|
|
@@ -6868,17 +7034,18 @@ function createTestRoom(options) {
|
|
|
6868
7034
|
rng = rng * 1664525 + 1013904223 >>> 0;
|
|
6869
7035
|
return rng / 4294967296;
|
|
6870
7036
|
};
|
|
6871
|
-
const muzzlePose = () => {
|
|
6872
|
-
const lent = muzzleSource?.();
|
|
6873
|
-
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 };
|
|
6874
7040
|
};
|
|
6875
|
-
const muzzle = () => {
|
|
7041
|
+
const muzzle = (hand = "R") => {
|
|
6876
7042
|
aim();
|
|
6877
7043
|
const e = camera.matrixWorld.elements;
|
|
7044
|
+
const side = hand === "L" ? -0.16 : 0.16;
|
|
6878
7045
|
return [
|
|
6879
|
-
aimO[0] + aimD[0] * 0.45 + e[0] *
|
|
6880
|
-
aimO[1] + aimD[1] * 0.45 + e[1] *
|
|
6881
|
-
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
|
|
6882
7049
|
];
|
|
6883
7050
|
};
|
|
6884
7051
|
const tracers = [];
|
|
@@ -7058,10 +7225,10 @@ function createTestRoom(options) {
|
|
|
7058
7225
|
renderer.impulse(objectId, push2.impulse, push2.point);
|
|
7059
7226
|
}
|
|
7060
7227
|
};
|
|
7061
|
-
const fireShots = (shots, recoil) => {
|
|
7228
|
+
const fireShots = (shots, recoil, id = toolId, hand = "R") => {
|
|
7062
7229
|
if (shots.length === 0) return;
|
|
7063
|
-
const w = toolName();
|
|
7064
|
-
const { position: m, drawsFlash } = muzzlePose();
|
|
7230
|
+
const w = toolName(id);
|
|
7231
|
+
const { position: m, drawsFlash } = muzzlePose(hand);
|
|
7065
7232
|
if (!drawsFlash) {
|
|
7066
7233
|
renderer.emit("flash", m, [aimD[0], aimD[1], aimD[2]], 1, 0);
|
|
7067
7234
|
renderer.emit("sparks", m, [aimD[0], aimD[1], aimD[2]], 4, 6);
|
|
@@ -7100,7 +7267,7 @@ function createTestRoom(options) {
|
|
|
7100
7267
|
objectId: f?.object,
|
|
7101
7268
|
itemId: f ? entryOf(f.object)?.so.itemId : void 0,
|
|
7102
7269
|
part: f?.part,
|
|
7103
|
-
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 } : {} }
|
|
7104
7271
|
});
|
|
7105
7272
|
};
|
|
7106
7273
|
const fireRays = (u) => {
|
|
@@ -7204,7 +7371,7 @@ function createTestRoom(options) {
|
|
|
7204
7371
|
const kickLeg = options.kickLeg ?? "alternate";
|
|
7205
7372
|
let nextLeg = "right";
|
|
7206
7373
|
const punchHand = options.punchHand ?? "alternate";
|
|
7207
|
-
const punchTools = new Set(options.punchTools ?? ["hand", "fists"]);
|
|
7374
|
+
const punchTools = new Set(options.punchTools ?? ["hand", "fists", "brass_knuckles"]);
|
|
7208
7375
|
let nextHand = "right";
|
|
7209
7376
|
const combo = options.strikeCombo;
|
|
7210
7377
|
let comboOn = !!combo && options.strikeComboOn === true;
|
|
@@ -7218,16 +7385,16 @@ function createTestRoom(options) {
|
|
|
7218
7385
|
action("combo", t(on ? "act.combo.on" : "act.combo.off", { name }), { data: { combo: combo.id, on } });
|
|
7219
7386
|
say(t(on ? "act.combo.on" : "act.combo.off", { name }));
|
|
7220
7387
|
};
|
|
7221
|
-
const strikeUse = (u, id = toolId) => {
|
|
7388
|
+
const strikeUse = (u, id = toolId, side) => {
|
|
7222
7389
|
const stepIndex = comboOn && combo?.tools.includes(id) && combo.steps.length > 0 ? simTime - comboAt > combo.reset ? 0 : comboNext : null;
|
|
7223
7390
|
const step2 = stepIndex === null ? void 0 : combo.steps[stepIndex];
|
|
7224
7391
|
const clip = step2 ? step2.clip : strikeClips[id];
|
|
7225
7392
|
const leg = id === "kick" ? kickLeg === "alternate" ? nextLeg : kickLeg : void 0;
|
|
7226
|
-
const hand = step2 ? step2.hand : punchTools.has(id) ? punchHand === "alternate" ? nextHand : punchHand : void 0;
|
|
7227
|
-
const
|
|
7228
|
-
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;
|
|
7229
7396
|
if (leg && seconds !== null && kickLeg === "alternate") nextLeg = leg === "right" ? "left" : "right";
|
|
7230
|
-
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";
|
|
7231
7398
|
if (step2 && stepIndex !== null) {
|
|
7232
7399
|
comboNext = (stepIndex + 1) % combo.steps.length;
|
|
7233
7400
|
comboAt = simTime;
|
|
@@ -7248,7 +7415,7 @@ function createTestRoom(options) {
|
|
|
7248
7415
|
data: { weapon: id, clip, impactIn: seconds, ...leg ? { leg } : {}, ...hand ? { hand } : {}, ...step2 ? { combo: combo.id, step: stepIndex } : {} }
|
|
7249
7416
|
});
|
|
7250
7417
|
strikes.push({
|
|
7251
|
-
key: id,
|
|
7418
|
+
key: side?.key ?? id,
|
|
7252
7419
|
remaining: seconds,
|
|
7253
7420
|
...swing && seconds > lead ? { swing, lead } : {},
|
|
7254
7421
|
land: () => {
|
|
@@ -7378,20 +7545,50 @@ function createTestRoom(options) {
|
|
|
7378
7545
|
say,
|
|
7379
7546
|
projectileModel: options.projectileModel
|
|
7380
7547
|
});
|
|
7381
|
-
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") => {
|
|
7382
7569
|
aim();
|
|
7383
|
-
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;
|
|
7384
7578
|
if (cat === "hand") {
|
|
7385
7579
|
if (strikes.some((strike) => strike.key === "hand" || strike.key === "fists")) return "cooldown";
|
|
7386
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");
|
|
7387
7581
|
return "ok";
|
|
7388
7582
|
}
|
|
7389
|
-
if (strikes.some((strike) => strike.key ===
|
|
7390
|
-
const res =
|
|
7391
|
-
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);
|
|
7392
7586
|
if (res.status === "empty") {
|
|
7393
|
-
if (!
|
|
7394
|
-
|
|
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"));
|
|
7395
7592
|
return res.status;
|
|
7396
7593
|
}
|
|
7397
7594
|
if (res.status !== "ok") {
|
|
@@ -7401,15 +7598,18 @@ function createTestRoom(options) {
|
|
|
7401
7598
|
else if (res.status === "safe") say(t("hud.safe"));
|
|
7402
7599
|
return res.status;
|
|
7403
7600
|
}
|
|
7404
|
-
|
|
7601
|
+
firedHand = hand;
|
|
7602
|
+
applyUses(res, dt, hand);
|
|
7405
7603
|
return res.status;
|
|
7406
7604
|
};
|
|
7407
|
-
const applyUses = (res, dt) => {
|
|
7408
|
-
|
|
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);
|
|
7409
7608
|
for (const u of res.uses) {
|
|
7410
7609
|
switch (u.kind) {
|
|
7411
7610
|
case "melee":
|
|
7412
|
-
strikeUse(u);
|
|
7611
|
+
if (hand === "L") strikeUse(u, id, { hand: "left", key: strikeKeyOf(id, hand) });
|
|
7612
|
+
else strikeUse(u);
|
|
7413
7613
|
break;
|
|
7414
7614
|
case "flame":
|
|
7415
7615
|
heatRays(u, "fire");
|
|
@@ -7437,15 +7637,17 @@ function createTestRoom(options) {
|
|
|
7437
7637
|
}
|
|
7438
7638
|
}
|
|
7439
7639
|
};
|
|
7440
|
-
const releaseTrigger = () => {
|
|
7441
|
-
|
|
7442
|
-
|
|
7640
|
+
const releaseTrigger = (hand = "R") => {
|
|
7641
|
+
const slot2 = toolOf(hand);
|
|
7642
|
+
if (!slot2) return;
|
|
7643
|
+
if (!slot2.state.cooking) {
|
|
7644
|
+
slot2.state.release();
|
|
7443
7645
|
return;
|
|
7444
7646
|
}
|
|
7445
7647
|
aim();
|
|
7446
|
-
const res =
|
|
7447
|
-
flushEvents(
|
|
7448
|
-
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);
|
|
7449
7651
|
};
|
|
7450
7652
|
const kick = () => {
|
|
7451
7653
|
if (!arsenal.weapons.has("kick") || strikes.some((strike) => strike.key === "kick")) return false;
|
|
@@ -7474,8 +7676,43 @@ function createTestRoom(options) {
|
|
|
7474
7676
|
}));
|
|
7475
7677
|
};
|
|
7476
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
|
+
};
|
|
7477
7713
|
const setTool = (id) => {
|
|
7478
7714
|
if (!arsenal.weapons.has(id)) throw new Error(`game-test-room: unknown weapon "${id}"`);
|
|
7715
|
+
dropOffTool();
|
|
7479
7716
|
releaseTrigger();
|
|
7480
7717
|
toolId = id;
|
|
7481
7718
|
const i = loadout.indexOf(id);
|
|
@@ -7905,6 +8142,30 @@ function createTestRoom(options) {
|
|
|
7905
8142
|
for (const p of loose) floatPiece(p, dt);
|
|
7906
8143
|
};
|
|
7907
8144
|
const footprints = createFootprints(terrain);
|
|
8145
|
+
let loadClock = 0;
|
|
8146
|
+
const restingLoad = (piece) => {
|
|
8147
|
+
if (piece.held || Math.hypot(piece.velocity[0], piece.velocity[1], piece.velocity[2]) > RESTING_SPEED) return null;
|
|
8148
|
+
const box2 = tmpBox.setFromObject(piece.root);
|
|
8149
|
+
const centre = box2.getCenter(tmpV);
|
|
8150
|
+
return { x: centre.x, z: centre.z, radius: Math.max(piece.halfExtents[0], piece.halfExtents[2]), newtons: piece.mass * GRAVITY_EARTH };
|
|
8151
|
+
};
|
|
8152
|
+
const updateLoads = (dt) => {
|
|
8153
|
+
loadClock += dt;
|
|
8154
|
+
if (loadClock < LOAD_EVERY) return;
|
|
8155
|
+
loadClock = 0;
|
|
8156
|
+
const loads = [];
|
|
8157
|
+
const [px, py, pz] = player.position;
|
|
8158
|
+
if (py - terrain.heightAt(px, pz) <= RESTING_GAP) loads.push({ x: px, z: pz, radius: PLAYER_FOOTPRINT, newtons: playerMass * GRAVITY_EARTH });
|
|
8159
|
+
for (const e of entries.values()) {
|
|
8160
|
+
const load = e.piece ? restingLoad(e.piece) : null;
|
|
8161
|
+
if (load) loads.push(load);
|
|
8162
|
+
}
|
|
8163
|
+
for (const p of loose) {
|
|
8164
|
+
const load = restingLoad(p);
|
|
8165
|
+
if (load) loads.push(load);
|
|
8166
|
+
}
|
|
8167
|
+
if (loads.length > 0 && terrain.bear(loads)) action("ground_gave_way", t("act.ground_gave_way"));
|
|
8168
|
+
};
|
|
7908
8169
|
const prevVy = /* @__PURE__ */ new Map();
|
|
7909
8170
|
const tmpBox = new THREE.Box3();
|
|
7910
8171
|
const onKey = (code, down, e) => {
|
|
@@ -7944,15 +8205,13 @@ function createTestRoom(options) {
|
|
|
7944
8205
|
case "KeyR":
|
|
7945
8206
|
if (heldPiece) heldYaw += Math.PI / 8;
|
|
7946
8207
|
else {
|
|
7947
|
-
|
|
8208
|
+
reloadHands();
|
|
7948
8209
|
say(t("hud.reloading"));
|
|
7949
8210
|
}
|
|
7950
8211
|
break;
|
|
7951
|
-
case "KeyQ":
|
|
7952
|
-
|
|
7953
|
-
flushEvents(toolId, tool);
|
|
8212
|
+
case "KeyQ":
|
|
8213
|
+
cycleToolMode();
|
|
7954
8214
|
break;
|
|
7955
|
-
}
|
|
7956
8215
|
case "KeyX":
|
|
7957
8216
|
clearToolFault();
|
|
7958
8217
|
break;
|
|
@@ -8009,6 +8268,20 @@ function createTestRoom(options) {
|
|
|
8009
8268
|
if (!loadout.includes(weaponId)) loadout.push(weaponId);
|
|
8010
8269
|
setTool(weaponId);
|
|
8011
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
|
+
};
|
|
8012
8285
|
let hudAimHit;
|
|
8013
8286
|
const inspectTarget = () => {
|
|
8014
8287
|
if (hudAimHit !== void 0) return hudAimHit;
|
|
@@ -8203,6 +8476,7 @@ function createTestRoom(options) {
|
|
|
8203
8476
|
vfx.update(dt);
|
|
8204
8477
|
updateImpacts();
|
|
8205
8478
|
updateWater(dt);
|
|
8479
|
+
updateLoads(dt);
|
|
8206
8480
|
if (player.position[1] <= terrain.footAt(player.position[0], player.position[2]) + 0.15) footprints.update(player.position[0], player.position[2]);
|
|
8207
8481
|
fluidView.update(dt);
|
|
8208
8482
|
filmBridge.update(dt);
|
|
@@ -8211,6 +8485,11 @@ function createTestRoom(options) {
|
|
|
8211
8485
|
s.update(dt);
|
|
8212
8486
|
flushEvents(id, s);
|
|
8213
8487
|
}
|
|
8488
|
+
if (offTool && offToolId) {
|
|
8489
|
+
offTool.update(dt);
|
|
8490
|
+
flushEvents(offToolId, offTool, void 0, "L");
|
|
8491
|
+
dualReload.step(dt);
|
|
8492
|
+
}
|
|
8214
8493
|
if (audio) {
|
|
8215
8494
|
const look = camera.getWorldDirection(listenerDirection);
|
|
8216
8495
|
audio.setListener({ position: [camera.position.x, camera.position.y, camera.position.z], yaw: Math.atan2(-look.x, -look.z) });
|
|
@@ -8256,6 +8535,10 @@ function createTestRoom(options) {
|
|
|
8256
8535
|
} else if (useHeld) useTool(dt, !useFirst);
|
|
8257
8536
|
useFirst = false;
|
|
8258
8537
|
}
|
|
8538
|
+
if ((offHeld || offFirst) && offTool && !isPaused && !push.active) {
|
|
8539
|
+
useTool(dt, !offFirst, "L");
|
|
8540
|
+
offFirst = false;
|
|
8541
|
+
}
|
|
8259
8542
|
if (!isPaused) followHeld(dt);
|
|
8260
8543
|
};
|
|
8261
8544
|
const updatePresent = (dt) => {
|
|
@@ -8282,12 +8565,18 @@ function createTestRoom(options) {
|
|
|
8282
8565
|
hudAimHit = void 0;
|
|
8283
8566
|
}
|
|
8284
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
|
+
};
|
|
8285
8573
|
const readHudState = () => {
|
|
8286
8574
|
const page = Math.floor(toolIndex / 10);
|
|
8287
8575
|
const slots = loadout.slice(page * 10, page * 10 + 10).map((id) => arsenal.name(id));
|
|
8288
|
-
const
|
|
8576
|
+
const left = offTool ? ammoText(offTool) : "";
|
|
8577
|
+
const ammo = `${ammoText(tool)}${left ? ` | ${t("hud.left")} ${left}` : ""}`;
|
|
8289
8578
|
return {
|
|
8290
|
-
toolName: toolName(),
|
|
8579
|
+
toolName: offToolId ? `${toolName()} + ${toolName(offToolId)}` : toolName(),
|
|
8291
8580
|
toolId,
|
|
8292
8581
|
ammo,
|
|
8293
8582
|
slots,
|
|
@@ -8477,26 +8766,53 @@ function createTestRoom(options) {
|
|
|
8477
8766
|
if (event.type === "explosion") knockPlayer(event);
|
|
8478
8767
|
for (const listener of hostListeners) listener(event);
|
|
8479
8768
|
}
|
|
8480
|
-
function flushEvents(id, state, recoil) {
|
|
8769
|
+
function flushEvents(id, state, recoil, hand = "R") {
|
|
8481
8770
|
const phase = state.phase;
|
|
8482
8771
|
for (const event of state.drain()) {
|
|
8483
8772
|
const hostEvent = toHostEvent(id, event, { ...recoil ? { recoil } : {}, phase });
|
|
8484
|
-
if (hostEvent) emitHost(hostEvent);
|
|
8773
|
+
if (hostEvent) emitHost(hand === "L" ? { ...hostEvent, hand } : hostEvent);
|
|
8485
8774
|
}
|
|
8486
8775
|
}
|
|
8487
|
-
function startReload() {
|
|
8488
|
-
|
|
8489
|
-
|
|
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;
|
|
8490
8782
|
}
|
|
8783
|
+
function reloadHands() {
|
|
8784
|
+
if (!offTool) {
|
|
8785
|
+
startReload();
|
|
8786
|
+
return;
|
|
8787
|
+
}
|
|
8788
|
+
dualReload.start(["R", "L"]);
|
|
8789
|
+
}
|
|
8790
|
+
const commandHand = () => offTool ? firedHand : "R";
|
|
8491
8791
|
function clearToolFault() {
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
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
|
+
}
|
|
8495
8800
|
}
|
|
8496
8801
|
function toggleSafety() {
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
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}`);
|
|
8500
8816
|
}
|
|
8501
8817
|
function runToolCommand(request) {
|
|
8502
8818
|
const work = (done) => done.ok ? { ok: true, seconds: done.seconds } : { ok: false, reason: done.reason };
|
|
@@ -8518,6 +8834,24 @@ function createTestRoom(options) {
|
|
|
8518
8834
|
return { ok: false, reason: "unknown" };
|
|
8519
8835
|
}
|
|
8520
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
|
+
});
|
|
8521
8855
|
const host = {
|
|
8522
8856
|
THREE,
|
|
8523
8857
|
scene,
|
|
@@ -8544,22 +8878,9 @@ function createTestRoom(options) {
|
|
|
8544
8878
|
...options.blastShove ? { blastShove: options.blastShove } : {},
|
|
8545
8879
|
blastReach: (point, target, ignore) => world.blastReach(point, target, ignore ? { ignore } : {}),
|
|
8546
8880
|
tool: () => ({
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
capacity: tool.capacity,
|
|
8551
|
-
reserve: tool.reserve,
|
|
8552
|
-
reloading: tool.reloading,
|
|
8553
|
-
mode: tool.mode,
|
|
8554
|
-
...tool.fault ? { fault: tool.fault } : {},
|
|
8555
|
-
safety: tool.safety,
|
|
8556
|
-
heat: tool.heatLevel,
|
|
8557
|
-
attachments: tool.attachments.map((part) => ({ id: part.id, mount: part.mount, ...part.integral ? { integral: true } : {}, ...part.on === void 0 ? {} : { on: part.on } })),
|
|
8558
|
-
stock: tool.stock,
|
|
8559
|
-
suppressed: tool.suppressed,
|
|
8560
|
-
underbarrelActive: tool.underbarrelActive,
|
|
8561
|
-
mass: tool.mass,
|
|
8562
|
-
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 } : {} } } : {}
|
|
8563
8884
|
}),
|
|
8564
8885
|
command(request) {
|
|
8565
8886
|
const result = runToolCommand(request);
|
|
@@ -8596,7 +8917,7 @@ function createTestRoom(options) {
|
|
|
8596
8917
|
onFrame(update, frameOptions) {
|
|
8597
8918
|
return scheduler.add((ctx) => update(ctx.dt), { phase: frameOptions?.phase ?? "lateSimulate", order: frameOptions?.order ?? 0, id: `module.frame#${moduleFrameCount++}` });
|
|
8598
8919
|
},
|
|
8599
|
-
holdFrame: () => bodyHoldFrame(),
|
|
8920
|
+
holdFrame: (hand) => bodyHoldFrame(hand),
|
|
8600
8921
|
reachFor(reach) {
|
|
8601
8922
|
if (!avatar) {
|
|
8602
8923
|
reach.onGrip?.();
|
|
@@ -8972,6 +9293,7 @@ function createTestRoom(options) {
|
|
|
8972
9293
|
fluids,
|
|
8973
9294
|
film: filmBridge,
|
|
8974
9295
|
vfx: vfx.world,
|
|
9296
|
+
effects: vfx,
|
|
8975
9297
|
fluidRenderer: fluidView,
|
|
8976
9298
|
audio,
|
|
8977
9299
|
environment,
|
|
@@ -8987,6 +9309,29 @@ function createTestRoom(options) {
|
|
|
8987
9309
|
clear,
|
|
8988
9310
|
resetAll,
|
|
8989
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(),
|
|
8990
9335
|
useItem,
|
|
8991
9336
|
use(hold = 0) {
|
|
8992
9337
|
if (tool.def.category === "hand") {
|
|
@@ -9284,7 +9629,8 @@ function createTestRoom(options) {
|
|
|
9284
9629
|
...options.toolMenu !== false ? {
|
|
9285
9630
|
toolMenu: {
|
|
9286
9631
|
groups: toolMenuGroups,
|
|
9287
|
-
onPick:
|
|
9632
|
+
onPick: pickTool,
|
|
9633
|
+
currentLeft: () => offToolId,
|
|
9288
9634
|
// the list takes clicks: the mouse leaves the view while it is open
|
|
9289
9635
|
onOpen: (open) => {
|
|
9290
9636
|
if (open && document.pointerLockElement) document.exitPointerLock?.();
|