@adatechnology/game-test-room 0.28.2 → 0.29.1
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 +2692 -183
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +449 -88
- package/dist/index.js.map +1 -1
- package/package.json +15 -15
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,13 @@ 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;
|
|
638
|
+
var SMOKE_HISS = { loop: "smoke_grenade_hiss", volume: 0.7, halfAt: 6, max: 1 };
|
|
639
|
+
var GRENADE_PIN_SOUND = "grenade_pin_pull";
|
|
630
640
|
var CONE_BLAST_RADIUS = 0.5;
|
|
631
641
|
var summarizeBlast = (results) => {
|
|
632
642
|
const reports = results.flatMap((r) => r.blast ? [r.blast] : []);
|
|
@@ -642,13 +652,35 @@ var toArray = (vector) => [vector.x, vector.y, vector.z];
|
|
|
642
652
|
function createExplosives(context) {
|
|
643
653
|
const { THREE, scene, camera, world, renderer, arsenal, translate, format, action } = context;
|
|
644
654
|
const projectiles = [];
|
|
655
|
+
const smokeSources = [];
|
|
656
|
+
const random = context.random ?? Math.random;
|
|
645
657
|
const meshes = [];
|
|
646
658
|
const up = new THREE.Vector3(0, 1, 0);
|
|
659
|
+
let smokeHiss = null;
|
|
660
|
+
const hissSmoke = () => {
|
|
661
|
+
if (!context.audio) return;
|
|
662
|
+
if (smokeSources.length === 0) {
|
|
663
|
+
smokeHiss?.stop();
|
|
664
|
+
smokeHiss = null;
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
const at = camera.position;
|
|
668
|
+
let level = 0;
|
|
669
|
+
for (const source of smokeSources) {
|
|
670
|
+
const distance = Math.hypot(source.point[0] - at.x, source.point[1] - at.y, source.point[2] - at.z);
|
|
671
|
+
level += SMOKE_HISS.volume / (1 + (distance / SMOKE_HISS.halfAt) ** 2);
|
|
672
|
+
}
|
|
673
|
+
smokeHiss ?? (smokeHiss = context.audio.loop(SMOKE_HISS.loop));
|
|
674
|
+
smokeHiss.set({ volume: Math.min(SMOKE_HISS.max, level) });
|
|
675
|
+
};
|
|
647
676
|
const drop = (objects) => {
|
|
648
677
|
for (const object of objects) object.removeFromParent();
|
|
649
678
|
releaseObject3D(objects, { keep: scene });
|
|
650
679
|
};
|
|
651
680
|
const clear = () => {
|
|
681
|
+
smokeSources.length = 0;
|
|
682
|
+
smokeHiss?.stop();
|
|
683
|
+
smokeHiss = null;
|
|
652
684
|
projectiles.length = 0;
|
|
653
685
|
drop(meshes);
|
|
654
686
|
meshes.length = 0;
|
|
@@ -702,6 +734,7 @@ function createExplosives(context) {
|
|
|
702
734
|
age: 0,
|
|
703
735
|
...use.explosive !== void 0 ? { body: createExplosiveBody(use, arsenal.explosives, arsenal.rules.explosives), aligned: isRocket } : {}
|
|
704
736
|
});
|
|
737
|
+
if (!isRocket && !isMolotov && use.explosive !== void 0) context.audio?.play(GRENADE_PIN_SOUND, { position: use.origin });
|
|
705
738
|
const key2 = isRocket ? "act.launch" : isMolotov ? "act.throw.molotov" : "act.throw.grenade";
|
|
706
739
|
action(isRocket ? "launch_projectile" : "throw_projectile", translate(key2, { weapon: context.toolName(), fuse: format(use.fuse, 1), arm: format(use.armDistance ?? 0, 0) }), {
|
|
707
740
|
data: { weapon: context.toolId(), fuse: use.fuse, explosive: use.explosive }
|
|
@@ -815,6 +848,12 @@ function createExplosives(context) {
|
|
|
815
848
|
const detonate = (projectile, detonation) => {
|
|
816
849
|
const weaponName = context.toolName(projectile.weapon);
|
|
817
850
|
const reach = Math.max(detonation.injuryRadius, detonation.lethalRadius);
|
|
851
|
+
if (detonation.smoke) {
|
|
852
|
+
const [shortest, longest] = detonation.smoke.seconds;
|
|
853
|
+
smokeSources.push({ point: detonation.point, left: shortest + (longest - shortest) * random(), owed: 0, color: detonation.smoke.color });
|
|
854
|
+
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 } });
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
818
857
|
const results = detonation.energy <= 0 || detonation.cone ? [] : world.explode(detonation.point, detonation.energy, reach, "player", context.blast());
|
|
819
858
|
const reached = detonation.energy <= 0 ? 0 : detonation.cone ? coneBlast(detonation) : results.length;
|
|
820
859
|
if (!detonation.cone) told(results);
|
|
@@ -936,7 +975,23 @@ function createExplosives(context) {
|
|
|
936
975
|
action("tripwire", translate("act.tripwire", { weapon: context.toolName(projectile.weapon), who: crosser }), { data: { weapon: projectile.weapon, body: crosser } });
|
|
937
976
|
detonate(projectile, event.detonation);
|
|
938
977
|
};
|
|
978
|
+
const burnSmoke = (dt) => {
|
|
979
|
+
for (let index = smokeSources.length - 1; index >= 0; index--) {
|
|
980
|
+
const source = smokeSources[index];
|
|
981
|
+
source.left -= dt;
|
|
982
|
+
source.owed += SMOKE_PUFFS_PER_SECOND * dt;
|
|
983
|
+
for (; source.owed >= 1; source.owed -= 1) {
|
|
984
|
+
const angle = random() * Math.PI * 2;
|
|
985
|
+
const speed = SMOKE_PUFF_SPEED * (0.4 + random());
|
|
986
|
+
const velocity = [Math.cos(angle) * speed, 0.5 + random() * 0.8, Math.sin(angle) * speed];
|
|
987
|
+
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 });
|
|
988
|
+
}
|
|
989
|
+
if (source.left <= 0) smokeSources.splice(index, 1);
|
|
990
|
+
}
|
|
991
|
+
hissSmoke();
|
|
992
|
+
};
|
|
939
993
|
const update = (dt) => {
|
|
994
|
+
burnSmoke(dt);
|
|
940
995
|
for (let index = projectiles.length - 1; index >= 0; index--) {
|
|
941
996
|
const projectile = projectiles[index];
|
|
942
997
|
if (!projectile) continue;
|
|
@@ -957,7 +1012,7 @@ function createExplosives(context) {
|
|
|
957
1012
|
}
|
|
958
1013
|
}
|
|
959
1014
|
};
|
|
960
|
-
return { throwUse, placeUse, backblastUse, detonateCharges, blastAt, update, clear };
|
|
1015
|
+
return { throwUse, placeUse, backblastUse, detonateCharges, blastAt, update, clear, smokeLeft: () => smokeSources.map((source) => source.left) };
|
|
961
1016
|
}
|
|
962
1017
|
|
|
963
1018
|
// src/test-table.ts
|
|
@@ -1171,6 +1226,65 @@ var toHostEvent = (weapon, event, options = {}) => {
|
|
|
1171
1226
|
}
|
|
1172
1227
|
};
|
|
1173
1228
|
|
|
1229
|
+
// src/dual-reload.ts
|
|
1230
|
+
var otherHand = (hand) => hand === "R" ? "L" : "R";
|
|
1231
|
+
function createDualReload(host, stowSeconds) {
|
|
1232
|
+
let queue = [];
|
|
1233
|
+
let current = null;
|
|
1234
|
+
const next = () => {
|
|
1235
|
+
let hand = queue.shift();
|
|
1236
|
+
while (hand && !host.needs(hand)) hand = queue.shift();
|
|
1237
|
+
if (!hand) {
|
|
1238
|
+
current = null;
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
const seconds = stowSeconds();
|
|
1242
|
+
current = { hand, phase: "stow", left: seconds };
|
|
1243
|
+
host.stow(otherHand(hand), true, seconds);
|
|
1244
|
+
};
|
|
1245
|
+
const draw = (hand) => {
|
|
1246
|
+
const seconds = stowSeconds();
|
|
1247
|
+
current = { hand, phase: "draw", left: seconds };
|
|
1248
|
+
host.stow(otherHand(hand), false, seconds);
|
|
1249
|
+
};
|
|
1250
|
+
return {
|
|
1251
|
+
start(order) {
|
|
1252
|
+
if (current) return;
|
|
1253
|
+
queue = [...order];
|
|
1254
|
+
next();
|
|
1255
|
+
},
|
|
1256
|
+
step(dt) {
|
|
1257
|
+
if (!current) return;
|
|
1258
|
+
if (current.phase === "reload") {
|
|
1259
|
+
if (!host.reloading(current.hand)) draw(current.hand);
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
current.left -= dt;
|
|
1263
|
+
if (current.left > 0) return;
|
|
1264
|
+
if (current.phase === "draw") {
|
|
1265
|
+
next();
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
current.phase = "reload";
|
|
1269
|
+
if (!host.startReload(current.hand)) draw(current.hand);
|
|
1270
|
+
},
|
|
1271
|
+
get busy() {
|
|
1272
|
+
return current !== null;
|
|
1273
|
+
},
|
|
1274
|
+
get current() {
|
|
1275
|
+
return current ? { hand: current.hand, phase: current.phase } : null;
|
|
1276
|
+
},
|
|
1277
|
+
stowed(hand) {
|
|
1278
|
+
return current !== null && current.phase !== "draw" && otherHand(current.hand) === hand;
|
|
1279
|
+
},
|
|
1280
|
+
cancel() {
|
|
1281
|
+
if (current && current.phase !== "draw") host.stow(otherHand(current.hand), false, 0);
|
|
1282
|
+
current = null;
|
|
1283
|
+
queue = [];
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1174
1288
|
// src/loading-view.ts
|
|
1175
1289
|
var LOADING_CLASS = "tr-loading";
|
|
1176
1290
|
var LOADING_CSS = [
|
|
@@ -2344,6 +2458,10 @@ function createPlayer(THREE, o) {
|
|
|
2344
2458
|
o.hooks.primaryDown();
|
|
2345
2459
|
};
|
|
2346
2460
|
const onMouseUp = (e) => {
|
|
2461
|
+
if (e.button === 2) {
|
|
2462
|
+
o.hooks.secondaryUp?.();
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2347
2465
|
if (e.button !== 0) return;
|
|
2348
2466
|
if (holdTimer) clearTimeout(holdTimer);
|
|
2349
2467
|
if (dragging) {
|
|
@@ -3361,7 +3479,7 @@ import { createFootprints, createTerrain, createTerrainImpl, DEFAULT_PROFILE, wa
|
|
|
3361
3479
|
import { createTerrainRenderer, terrainProfileName } from "@adatechnology/game-terrain-three";
|
|
3362
3480
|
|
|
3363
3481
|
// src/avatar.ts
|
|
3364
|
-
var DEFAULT_STRIKE_CLIPS = { hand: "punch_right", fists: "punch_right", shove: "shove_two_hands", kick: "front_kick" };
|
|
3482
|
+
var DEFAULT_STRIKE_CLIPS = { hand: "punch_right", fists: "punch_right", brass_knuckles: "punch_right", shove: "shove_two_hands", kick: "front_kick" };
|
|
3365
3483
|
var DEFAULT_HAND_STANCES = { fists: "fists" };
|
|
3366
3484
|
var THIRD_PERSON_DEFAULTS = {
|
|
3367
3485
|
back: 3.8,
|
|
@@ -3534,6 +3652,12 @@ var en = {
|
|
|
3534
3652
|
"act.clear": "Cleared the room",
|
|
3535
3653
|
"act.reset": "Repaired every object",
|
|
3536
3654
|
"act.tool": "Equipped {weapon}",
|
|
3655
|
+
"act.tool_left": "{weapon} in the left hand: left button fires the right one, right button the left one",
|
|
3656
|
+
"act.tool_left_off": "Left hand free: one weapon again",
|
|
3657
|
+
"act.stow": "{weapon} tucked against the body: the {hand} hand is free for the other magazine",
|
|
3658
|
+
"act.unstow": "{weapon} back in the {hand} hand",
|
|
3659
|
+
"hud.no_dual": "{weapon} does not go one in each hand",
|
|
3660
|
+
"hud.left": "L",
|
|
3537
3661
|
"act.shoot.hit": "{weapon}: shot, {energy} J, hit {where} at {dist} m",
|
|
3538
3662
|
"act.shoot.miss": "{weapon}: shot, {energy} J, hit nothing",
|
|
3539
3663
|
"act.shoot.pellets": "{weapon}: shot, {n} projectiles of up to {energy} J, {hits} hit {where} at {dist} m",
|
|
@@ -3563,6 +3687,7 @@ var en = {
|
|
|
3563
3687
|
"hud.no_charges": "No charge set down waits for the remote detonator",
|
|
3564
3688
|
"blast.zone.lethal": "lethal",
|
|
3565
3689
|
"blast.zone.injury": "injury",
|
|
3690
|
+
"act.smoke": "{weapon}: smoke, burning for {seconds} s",
|
|
3566
3691
|
"act.explode": "{weapon}: exploded, {energy} J within {radius} m, {n} objects in range",
|
|
3567
3692
|
"act.molotov_fire": "{weapon}: fire on the ground, radius {radius} m for {seconds} s",
|
|
3568
3693
|
"act.tool.none": "{weapon}: nothing in reach",
|
|
@@ -3593,6 +3718,7 @@ var en = {
|
|
|
3593
3718
|
"act.player_thrown_wounded": "The blast threw the player at {speed} m/s, {distance} m from it: badly wounded, stunned for {stun} s ({reach}% got through the cover)",
|
|
3594
3719
|
"act.player_stunned": "The blast stunned the player for {stun} s, {distance} m from it: dizzy, ears ringing",
|
|
3595
3720
|
"act.came_apart": "{name} came apart: what stayed is loose and falls",
|
|
3721
|
+
"check.broken_floating": "ERROR: what was left of {name} stood on nothing after the break; it was let go to fall",
|
|
3596
3722
|
"act.shoved": "{name} stands free: the shots moved it",
|
|
3597
3723
|
"act.impact": "{name} hit {where} at {speed} m/s: {energy} J of blunt damage",
|
|
3598
3724
|
"act.use_item": "{name}: {use}",
|
|
@@ -3701,6 +3827,7 @@ var en = {
|
|
|
3701
3827
|
"ui.clear_log": "Clear",
|
|
3702
3828
|
"ui.catalog": "Catalogue",
|
|
3703
3829
|
"ui.tool_menu": "Weapons",
|
|
3830
|
+
"ui.tool_menu.dual": "Shift+click: one in each hand",
|
|
3704
3831
|
"ui.tool_menu.search": "Search weapons and tools",
|
|
3705
3832
|
"ui.tool_menu.close": "Close",
|
|
3706
3833
|
"ui.tool_menu.results": "{n} found",
|
|
@@ -3904,6 +4031,12 @@ var ptBR = {
|
|
|
3904
4031
|
"act.clear": "Limpou a sala",
|
|
3905
4032
|
"act.reset": "Consertou todos os objetos",
|
|
3906
4033
|
"act.tool": "Equipou {weapon}",
|
|
4034
|
+
"act.tool_left": "{weapon} na m\xE3o esquerda: bot\xE3o esquerdo dispara a da direita, bot\xE3o direito a da esquerda",
|
|
4035
|
+
"act.tool_left_off": "M\xE3o esquerda livre: uma arma de novo",
|
|
4036
|
+
"act.stow": "{weapon} encostada no corpo: a m\xE3o {hand} fica livre para o outro carregador",
|
|
4037
|
+
"act.unstow": "{weapon} de volta na m\xE3o {hand}",
|
|
4038
|
+
"hud.no_dual": "{weapon} n\xE3o vai uma em cada m\xE3o",
|
|
4039
|
+
"hud.left": "E",
|
|
3907
4040
|
"act.shoot.hit": "{weapon}: disparo, {energy} J, acertou {where} a {dist} m",
|
|
3908
4041
|
"act.shoot.miss": "{weapon}: disparo, {energy} J, n\xE3o acertou nada",
|
|
3909
4042
|
"act.shoot.pellets": "{weapon}: disparo, {n} proj\xE9teis de at\xE9 {energy} J, {hits} acertaram {where} a {dist} m",
|
|
@@ -3933,6 +4066,7 @@ var ptBR = {
|
|
|
3933
4066
|
"hud.no_charges": "Nenhuma carga posta espera o detonador",
|
|
3934
4067
|
"blast.zone.lethal": "letal",
|
|
3935
4068
|
"blast.zone.injury": "de ferimento",
|
|
4069
|
+
"act.smoke": "{weapon}: fuma\xE7a, queima por {seconds} s",
|
|
3936
4070
|
"act.explode": "{weapon}: explodiu, {energy} J em {radius} m, {n} objetos ao alcance",
|
|
3937
4071
|
"act.molotov_fire": "{weapon}: fogo no ch\xE3o, raio de {radius} m por {seconds} s",
|
|
3938
4072
|
"act.tool.none": "{weapon}: nada ao alcance",
|
|
@@ -3963,6 +4097,7 @@ var ptBR = {
|
|
|
3963
4097
|
"act.player_thrown_wounded": "A explos\xE3o arremessou o jogador a {speed} m/s, a {distance} m dela: ferido gravemente, atordoado por {stun} s ({reach}% passou pela cobertura)",
|
|
3964
4098
|
"act.player_stunned": "A explos\xE3o atordoou o jogador por {stun} s, a {distance} m dela: tonto, com os ouvidos zumbindo",
|
|
3965
4099
|
"act.came_apart": "{name} se desmontou: o que ficou est\xE1 solto e cai",
|
|
4100
|
+
"check.broken_floating": "ERRO: o que sobrou de {name} ficou sem apoio depois de quebrar; foi solto para cair",
|
|
3966
4101
|
"act.shoved": "{name} estava solto no ch\xE3o: os tiros o moveram",
|
|
3967
4102
|
"act.impact": "{name} bateu em {where} a {speed} m/s: {energy} J de dano de impacto",
|
|
3968
4103
|
"act.use_item": "{name}: {use}",
|
|
@@ -4071,6 +4206,7 @@ var ptBR = {
|
|
|
4071
4206
|
"ui.clear_log": "Limpar",
|
|
4072
4207
|
"ui.catalog": "Cat\xE1logo",
|
|
4073
4208
|
"ui.tool_menu": "Armas",
|
|
4209
|
+
"ui.tool_menu.dual": "Shift+clique: uma em cada m\xE3o",
|
|
4074
4210
|
"ui.tool_menu.search": "Buscar armas e ferramentas",
|
|
4075
4211
|
"ui.tool_menu.close": "Fechar",
|
|
4076
4212
|
"ui.tool_menu.results": "{n} encontradas",
|
|
@@ -4279,6 +4415,27 @@ function createBenchPanel(o) {
|
|
|
4279
4415
|
return wrap;
|
|
4280
4416
|
};
|
|
4281
4417
|
let playhead = null;
|
|
4418
|
+
const sheetImage = (image) => {
|
|
4419
|
+
const figure = mk("figure", "tr-sheet-image");
|
|
4420
|
+
const picture = mk("img");
|
|
4421
|
+
picture.src = image.url;
|
|
4422
|
+
picture.alt = image.alt;
|
|
4423
|
+
picture.loading = "lazy";
|
|
4424
|
+
picture.referrerPolicy = "no-referrer";
|
|
4425
|
+
figure.appendChild(picture);
|
|
4426
|
+
if (image.credit || image.page) {
|
|
4427
|
+
const caption = mk("figcaption", "sub", image.credit ?? "");
|
|
4428
|
+
if (image.page) {
|
|
4429
|
+
const link = mk("a", void 0, image.credit ? " \u2197" : image.page);
|
|
4430
|
+
link.href = image.page;
|
|
4431
|
+
link.target = "_blank";
|
|
4432
|
+
link.rel = "noopener noreferrer";
|
|
4433
|
+
caption.appendChild(link);
|
|
4434
|
+
}
|
|
4435
|
+
figure.appendChild(caption);
|
|
4436
|
+
}
|
|
4437
|
+
return figure;
|
|
4438
|
+
};
|
|
4282
4439
|
const sheetCard = (sheet) => {
|
|
4283
4440
|
const card = mk("div", "tr-sheet");
|
|
4284
4441
|
if (sheet.kicker) card.appendChild(mk("div", "sub", sheet.kicker));
|
|
@@ -4330,6 +4487,7 @@ function createBenchPanel(o) {
|
|
|
4330
4487
|
playhead = { node: head, metric, end };
|
|
4331
4488
|
card.appendChild(strip);
|
|
4332
4489
|
}
|
|
4490
|
+
if (sheet.image) card.appendChild(sheetImage(sheet.image));
|
|
4333
4491
|
if (sheet.note) card.appendChild(mk("p", "sub", sheet.note));
|
|
4334
4492
|
return card;
|
|
4335
4493
|
};
|
|
@@ -4563,7 +4721,7 @@ function createToolMenu(o) {
|
|
|
4563
4721
|
const close = mk3("button", "tr-btn", "\xD7");
|
|
4564
4722
|
close.type = "button";
|
|
4565
4723
|
close.title = o.t("ui.tool_menu.close");
|
|
4566
|
-
header.append(mk3("h3", void 0, o.t("ui.tool_menu")), search, close);
|
|
4724
|
+
header.append(mk3("h3", void 0, o.t("ui.tool_menu")), mk3("small", "tr-hint", o.t("ui.tool_menu.dual")), search, close);
|
|
4567
4725
|
const tabs = mk3("div", "tr-tabs");
|
|
4568
4726
|
const body = mk3("div", "tr-wmenu-body");
|
|
4569
4727
|
element.append(header, tabs, body);
|
|
@@ -4571,15 +4729,17 @@ function createToolMenu(o) {
|
|
|
4571
4729
|
let activeGroup = "";
|
|
4572
4730
|
let groups = [];
|
|
4573
4731
|
const toolButton = (tool, group) => {
|
|
4574
|
-
const
|
|
4732
|
+
const left = o.currentLeft?.() === tool.id;
|
|
4733
|
+
const button = mk3("button", tool.id === o.current() || left ? "tr-btn on" : "tr-btn");
|
|
4575
4734
|
button.type = "button";
|
|
4576
4735
|
button.title = tool.name;
|
|
4736
|
+
if (left) button.dataset.left = "";
|
|
4577
4737
|
button.dataset.tool = tool.id;
|
|
4578
4738
|
if (tool.thumbnail) button.appendChild(o.thumb(tool.thumbnail, tool.thumbnailCredit));
|
|
4579
4739
|
button.appendChild(mk3("span", void 0, tool.name));
|
|
4580
4740
|
if (group) button.appendChild(mk3("small", void 0, group));
|
|
4581
|
-
button.onclick = () => {
|
|
4582
|
-
o.onPick(tool.id);
|
|
4741
|
+
button.onclick = (event) => {
|
|
4742
|
+
o.onPick(tool.id, { second: event.shiftKey });
|
|
4583
4743
|
toggle(false);
|
|
4584
4744
|
};
|
|
4585
4745
|
return button;
|
|
@@ -4675,6 +4835,8 @@ var CSS = `
|
|
|
4675
4835
|
.tr-prog b{display:block;font-weight:500;margin-bottom:3px}
|
|
4676
4836
|
.tr-sheet{margin-top:8px;padding:8px;border:1px solid #2a2e36;border-radius:6px;font-size:12px;line-height:1.4}
|
|
4677
4837
|
.tr-sheet h4{margin:2px 0 4px;font-size:13px}.tr-sheet p{margin:4px 0}
|
|
4838
|
+
/* 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 */
|
|
4839
|
+
.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
4840
|
.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
4841
|
.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
4842
|
.tr-timeline{position:relative;height:22px;margin:8px 0;background:#1b1e24;border-radius:4px;overflow:hidden}
|
|
@@ -4802,7 +4964,7 @@ function createUi(o) {
|
|
|
4802
4964
|
bottom.append(held, hotbar);
|
|
4803
4965
|
let currentTool = "";
|
|
4804
4966
|
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;
|
|
4967
|
+
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
4968
|
if (toolMenu && menuOptions) {
|
|
4807
4969
|
const menuButton = mk4("button", "tr-slot tr-menu-btn");
|
|
4808
4970
|
menuButton.type = "button";
|
|
@@ -5450,6 +5612,8 @@ function createModuleViews(THREE, gl) {
|
|
|
5450
5612
|
|
|
5451
5613
|
// src/room.ts
|
|
5452
5614
|
var FIXED_DT = 1 / 60;
|
|
5615
|
+
var LEFT_HAND_KEY = "#L";
|
|
5616
|
+
var OFF_HAND_SEED = 11;
|
|
5453
5617
|
var TOOL_MENU_KEY_LABELS = { Backquote: "'" };
|
|
5454
5618
|
var EXPLOSIVE_EFFECT_IDS = ["m67", "molotov", "pipe_bomb", "m79_40mm", "m72_66mm", "at4_84mm", "dynamite", "c4", "claymore"];
|
|
5455
5619
|
var DEFAULT_PARTICLE_LIMIT = 900;
|
|
@@ -5664,6 +5828,14 @@ function createTestRoom(options) {
|
|
|
5664
5828
|
rendererPieces.set(objectId, piece);
|
|
5665
5829
|
action("loosened", t(cause === "blast" ? "act.loosened" : "act.came_apart", { name: e.name }), { objectId, itemId: e.so.itemId });
|
|
5666
5830
|
},
|
|
5831
|
+
// after a break the collision of the object is measured again from what is still drawn; what stood on nothing was let go, which is a bug the room says aloud
|
|
5832
|
+
onBrokenChecked: (objectId, released) => {
|
|
5833
|
+
bodyBoxes.delete(objectId);
|
|
5834
|
+
if (standingIds.includes(objectId)) leaveFootprint(objectId);
|
|
5835
|
+
if (!released) return;
|
|
5836
|
+
const e = entries.get(objectId);
|
|
5837
|
+
log.add({ channel: "check", type: "fail", text: t("check.broken_floating", { name: e?.name ?? objectId }), objectId, ...e ? { itemId: e.so.itemId } : {}, data: { invariant: "broken_floating" } });
|
|
5838
|
+
},
|
|
5667
5839
|
onPiece: (piece) => {
|
|
5668
5840
|
rendererPieces.set(piece.objectId, piece);
|
|
5669
5841
|
loose.push(piece);
|
|
@@ -5745,6 +5917,11 @@ function createTestRoom(options) {
|
|
|
5745
5917
|
return s;
|
|
5746
5918
|
};
|
|
5747
5919
|
let tool = toolState(toolId);
|
|
5920
|
+
let offToolId = null;
|
|
5921
|
+
let offTool = null;
|
|
5922
|
+
let offHeld = false;
|
|
5923
|
+
let offFirst = false;
|
|
5924
|
+
let firedHand = "R";
|
|
5748
5925
|
const catalogMap = /* @__PURE__ */ new Map();
|
|
5749
5926
|
for (const i of options.catalog) catalogMap.set(i.id, i);
|
|
5750
5927
|
const catalog = [...options.catalog];
|
|
@@ -5954,9 +6131,19 @@ function createTestRoom(options) {
|
|
|
5954
6131
|
releaseTrigger();
|
|
5955
6132
|
},
|
|
5956
6133
|
secondary: () => {
|
|
6134
|
+
if (offTool && !heldEntry) {
|
|
6135
|
+
offHeld = true;
|
|
6136
|
+
offFirst = true;
|
|
6137
|
+
return;
|
|
6138
|
+
}
|
|
5957
6139
|
if (heldEntry) throwHeld();
|
|
5958
6140
|
else grab();
|
|
5959
6141
|
},
|
|
6142
|
+
secondaryUp: () => {
|
|
6143
|
+
if (!offHeld) return;
|
|
6144
|
+
offHeld = false;
|
|
6145
|
+
releaseTrigger("L");
|
|
6146
|
+
},
|
|
5960
6147
|
key: (code, down, e) => onKey(code, down, e),
|
|
5961
6148
|
wheel: (dir) => cycleTool(dir)
|
|
5962
6149
|
}
|
|
@@ -6312,9 +6499,14 @@ function createTestRoom(options) {
|
|
|
6312
6499
|
bodyBoxes.delete(id);
|
|
6313
6500
|
return null;
|
|
6314
6501
|
}
|
|
6315
|
-
|
|
6316
|
-
if (
|
|
6317
|
-
|
|
6502
|
+
const parts = renderer.shownBoxes(id);
|
|
6503
|
+
if (parts.length === 0) {
|
|
6504
|
+
bodyBoxes.delete(id);
|
|
6505
|
+
return null;
|
|
6506
|
+
}
|
|
6507
|
+
bodyBounds.makeEmpty();
|
|
6508
|
+
for (const p of parts) bodyBounds.expandByPoint(tmpV.set(p[0], p[1], p[2])).expandByPoint(tmpV.set(p[3], p[4], p[5]));
|
|
6509
|
+
const box2 = { minX: bodyBounds.min.x, maxX: bodyBounds.max.x, minZ: bodyBounds.min.z, maxZ: bodyBounds.max.z, base: bodyBounds.min.y, top: bodyBounds.max.y, stamp: frameStamp, parts };
|
|
6318
6510
|
bodyBoxes.set(id, box2);
|
|
6319
6511
|
return box2;
|
|
6320
6512
|
};
|
|
@@ -6331,7 +6523,15 @@ function createTestRoom(options) {
|
|
|
6331
6523
|
const reach = c.radius + r;
|
|
6332
6524
|
return (x - footPos.x) ** 2 + (z - footPos.z) ** 2 < reach * reach ? { ...b, top: Math.min(b.top, footPos.y + (c.height ?? b.top - footPos.y)) } : null;
|
|
6333
6525
|
}
|
|
6334
|
-
|
|
6526
|
+
if (!(x > b.minX - r && x < b.maxX + r && z > b.minZ - r && z < b.maxZ + r)) return null;
|
|
6527
|
+
let base = Infinity;
|
|
6528
|
+
let top = -Infinity;
|
|
6529
|
+
for (const p of b.parts) {
|
|
6530
|
+
if (!(x > p[0] - r && x < p[3] + r && z > p[2] - r && z < p[5] + r)) continue;
|
|
6531
|
+
base = Math.min(base, p[1]);
|
|
6532
|
+
top = Math.max(top, p[4]);
|
|
6533
|
+
}
|
|
6534
|
+
return top > base ? { ...b, base, top } : null;
|
|
6335
6535
|
};
|
|
6336
6536
|
blockers.add((x, z, r) => {
|
|
6337
6537
|
for (const e of entries.values()) {
|
|
@@ -6495,6 +6695,10 @@ function createTestRoom(options) {
|
|
|
6495
6695
|
for (const listener of [...resetListeners]) listener();
|
|
6496
6696
|
tools.clear();
|
|
6497
6697
|
tool = toolState(toolId);
|
|
6698
|
+
dualReload.cancel();
|
|
6699
|
+
offTool = null;
|
|
6700
|
+
offToolId = null;
|
|
6701
|
+
firedHand = "R";
|
|
6498
6702
|
if (pristineWorld) {
|
|
6499
6703
|
for (const id of world.ids()) world.remove(id);
|
|
6500
6704
|
world.load(pristineWorld);
|
|
@@ -6760,12 +6964,13 @@ function createTestRoom(options) {
|
|
|
6760
6964
|
const pocketAt = new THREE.Vector3();
|
|
6761
6965
|
const aimTurn = new THREE.Quaternion();
|
|
6762
6966
|
const aimEuler = new THREE.Euler(0, 0, 0, "YXZ");
|
|
6763
|
-
const bodyHoldFrame = () => {
|
|
6764
|
-
const shoulder = avatar?.shoulder?.(
|
|
6967
|
+
const bodyHoldFrame = (hand = "R") => {
|
|
6968
|
+
const shoulder = avatar?.shoulder?.(hand);
|
|
6765
6969
|
if (!shoulder) return null;
|
|
6766
6970
|
shoulder.getWorldPosition(pocketAt);
|
|
6767
|
-
|
|
6768
|
-
pocketAt.
|
|
6971
|
+
const inward = hand === "L" ? -SHOULDER_POCKET_IN : SHOULDER_POCKET_IN;
|
|
6972
|
+
pocketAt.x -= Math.cos(player.yaw) * inward;
|
|
6973
|
+
pocketAt.z += Math.sin(player.yaw) * inward;
|
|
6769
6974
|
aimTurn.setFromEuler(aimEuler.set(player.pitch, player.yaw, 0, "YXZ"));
|
|
6770
6975
|
return { position: [pocketAt.x, pocketAt.y, pocketAt.z], quaternion: [aimTurn.x, aimTurn.y, aimTurn.z, aimTurn.w] };
|
|
6771
6976
|
};
|
|
@@ -6875,17 +7080,18 @@ function createTestRoom(options) {
|
|
|
6875
7080
|
rng = rng * 1664525 + 1013904223 >>> 0;
|
|
6876
7081
|
return rng / 4294967296;
|
|
6877
7082
|
};
|
|
6878
|
-
const muzzlePose = () => {
|
|
6879
|
-
const lent = muzzleSource?.();
|
|
6880
|
-
return lent ? { position: [...lent.position], drawsFlash: lent.drawsFlash } : { position: muzzle(), drawsFlash: false };
|
|
7083
|
+
const muzzlePose = (hand = "R") => {
|
|
7084
|
+
const lent = muzzleSource?.(hand);
|
|
7085
|
+
return lent ? { position: [...lent.position], drawsFlash: lent.drawsFlash } : { position: muzzle(hand), drawsFlash: false };
|
|
6881
7086
|
};
|
|
6882
|
-
const muzzle = () => {
|
|
7087
|
+
const muzzle = (hand = "R") => {
|
|
6883
7088
|
aim();
|
|
6884
7089
|
const e = camera.matrixWorld.elements;
|
|
7090
|
+
const side = hand === "L" ? -0.16 : 0.16;
|
|
6885
7091
|
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] *
|
|
7092
|
+
aimO[0] + aimD[0] * 0.45 + e[0] * side - e[4] * 0.1,
|
|
7093
|
+
aimO[1] + aimD[1] * 0.45 + e[1] * side - e[5] * 0.1,
|
|
7094
|
+
aimO[2] + aimD[2] * 0.45 + e[2] * side - e[6] * 0.1
|
|
6889
7095
|
];
|
|
6890
7096
|
};
|
|
6891
7097
|
const tracers = [];
|
|
@@ -7065,10 +7271,10 @@ function createTestRoom(options) {
|
|
|
7065
7271
|
renderer.impulse(objectId, push2.impulse, push2.point);
|
|
7066
7272
|
}
|
|
7067
7273
|
};
|
|
7068
|
-
const fireShots = (shots, recoil) => {
|
|
7274
|
+
const fireShots = (shots, recoil, id = toolId, hand = "R") => {
|
|
7069
7275
|
if (shots.length === 0) return;
|
|
7070
|
-
const w = toolName();
|
|
7071
|
-
const { position: m, drawsFlash } = muzzlePose();
|
|
7276
|
+
const w = toolName(id);
|
|
7277
|
+
const { position: m, drawsFlash } = muzzlePose(hand);
|
|
7072
7278
|
if (!drawsFlash) {
|
|
7073
7279
|
renderer.emit("flash", m, [aimD[0], aimD[1], aimD[2]], 1, 0);
|
|
7074
7280
|
renderer.emit("sparks", m, [aimD[0], aimD[1], aimD[2]], 4, 6);
|
|
@@ -7107,7 +7313,7 @@ function createTestRoom(options) {
|
|
|
7107
7313
|
objectId: f?.object,
|
|
7108
7314
|
itemId: f ? entryOf(f.object)?.so.itemId : void 0,
|
|
7109
7315
|
part: f?.part,
|
|
7110
|
-
data: { weapon:
|
|
7316
|
+
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
7317
|
});
|
|
7112
7318
|
};
|
|
7113
7319
|
const fireRays = (u) => {
|
|
@@ -7211,7 +7417,7 @@ function createTestRoom(options) {
|
|
|
7211
7417
|
const kickLeg = options.kickLeg ?? "alternate";
|
|
7212
7418
|
let nextLeg = "right";
|
|
7213
7419
|
const punchHand = options.punchHand ?? "alternate";
|
|
7214
|
-
const punchTools = new Set(options.punchTools ?? ["hand", "fists"]);
|
|
7420
|
+
const punchTools = new Set(options.punchTools ?? ["hand", "fists", "brass_knuckles"]);
|
|
7215
7421
|
let nextHand = "right";
|
|
7216
7422
|
const combo = options.strikeCombo;
|
|
7217
7423
|
let comboOn = !!combo && options.strikeComboOn === true;
|
|
@@ -7225,16 +7431,16 @@ function createTestRoom(options) {
|
|
|
7225
7431
|
action("combo", t(on ? "act.combo.on" : "act.combo.off", { name }), { data: { combo: combo.id, on } });
|
|
7226
7432
|
say(t(on ? "act.combo.on" : "act.combo.off", { name }));
|
|
7227
7433
|
};
|
|
7228
|
-
const strikeUse = (u, id = toolId) => {
|
|
7434
|
+
const strikeUse = (u, id = toolId, side) => {
|
|
7229
7435
|
const stepIndex = comboOn && combo?.tools.includes(id) && combo.steps.length > 0 ? simTime - comboAt > combo.reset ? 0 : comboNext : null;
|
|
7230
7436
|
const step2 = stepIndex === null ? void 0 : combo.steps[stepIndex];
|
|
7231
7437
|
const clip = step2 ? step2.clip : strikeClips[id];
|
|
7232
7438
|
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,
|
|
7439
|
+
const hand = side ? side.hand : step2 ? step2.hand : punchTools.has(id) ? punchHand === "alternate" ? nextHand : punchHand : void 0;
|
|
7440
|
+
const limb = leg ? { leg } : hand ? { hand } : void 0;
|
|
7441
|
+
const seconds = clip === void 0 ? null : avatar?.strike?.(clip, limb) ?? null;
|
|
7236
7442
|
if (leg && seconds !== null && kickLeg === "alternate") nextLeg = leg === "right" ? "left" : "right";
|
|
7237
|
-
if (!step2 && hand && seconds !== null && punchHand === "alternate") nextHand = hand === "right" ? "left" : "right";
|
|
7443
|
+
if (!step2 && !side && hand && seconds !== null && punchHand === "alternate") nextHand = hand === "right" ? "left" : "right";
|
|
7238
7444
|
if (step2 && stepIndex !== null) {
|
|
7239
7445
|
comboNext = (stepIndex + 1) % combo.steps.length;
|
|
7240
7446
|
comboAt = simTime;
|
|
@@ -7255,7 +7461,7 @@ function createTestRoom(options) {
|
|
|
7255
7461
|
data: { weapon: id, clip, impactIn: seconds, ...leg ? { leg } : {}, ...hand ? { hand } : {}, ...step2 ? { combo: combo.id, step: stepIndex } : {} }
|
|
7256
7462
|
});
|
|
7257
7463
|
strikes.push({
|
|
7258
|
-
key: id,
|
|
7464
|
+
key: side?.key ?? id,
|
|
7259
7465
|
remaining: seconds,
|
|
7260
7466
|
...swing && seconds > lead ? { swing, lead } : {},
|
|
7261
7467
|
land: () => {
|
|
@@ -7383,22 +7589,53 @@ function createTestRoom(options) {
|
|
|
7383
7589
|
emitHost,
|
|
7384
7590
|
startFire,
|
|
7385
7591
|
say,
|
|
7386
|
-
projectileModel: options.projectileModel
|
|
7592
|
+
projectileModel: options.projectileModel,
|
|
7593
|
+
audio
|
|
7387
7594
|
});
|
|
7388
|
-
const
|
|
7595
|
+
const toolOf = (hand) => hand === "L" ? offTool && offToolId ? { id: offToolId, state: offTool } : null : { id: toolId, state: tool };
|
|
7596
|
+
const dualReload = createDualReload(
|
|
7597
|
+
{
|
|
7598
|
+
needs: (hand) => {
|
|
7599
|
+
const state = toolOf(hand)?.state;
|
|
7600
|
+
return state !== void 0 && (state.def.action.kind === "firearm" || state.def.action.kind === "launcher") && state.ammo < state.capacity && state.reserve > 0;
|
|
7601
|
+
},
|
|
7602
|
+
startReload: (hand) => startReload(hand),
|
|
7603
|
+
reloading: (hand) => toolOf(hand)?.state.reloading ?? false,
|
|
7604
|
+
stow: (hand, on, seconds) => {
|
|
7605
|
+
const slot2 = toolOf(hand);
|
|
7606
|
+
if (!slot2) return;
|
|
7607
|
+
if (on) slot2.state.release();
|
|
7608
|
+
emitHost({ type: "stow", weapon: slot2.id, on, duration: seconds, ...hand === "L" ? { hand } : {} });
|
|
7609
|
+
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 } });
|
|
7610
|
+
}
|
|
7611
|
+
},
|
|
7612
|
+
() => arsenal.rules.dualWield.stowSeconds
|
|
7613
|
+
);
|
|
7614
|
+
const strikeKeyOf = (id, hand) => hand === "L" ? `${id}${LEFT_HAND_KEY}` : id;
|
|
7615
|
+
const useTool = (dt, held, hand = "R") => {
|
|
7389
7616
|
aim();
|
|
7390
|
-
const
|
|
7617
|
+
const slot2 = toolOf(hand);
|
|
7618
|
+
if (!slot2) return "cooldown";
|
|
7619
|
+
const { id, state } = slot2;
|
|
7620
|
+
if (offTool && dualReload.busy) {
|
|
7621
|
+
if (!held) say(t("hud.reloading"));
|
|
7622
|
+
return "reloading";
|
|
7623
|
+
}
|
|
7624
|
+
const cat = state.def.category;
|
|
7391
7625
|
if (cat === "hand") {
|
|
7392
7626
|
if (strikes.some((strike) => strike.key === "hand" || strike.key === "fists")) return "cooldown";
|
|
7393
7627
|
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
7628
|
return "ok";
|
|
7395
7629
|
}
|
|
7396
|
-
if (strikes.some((strike) => strike.key ===
|
|
7397
|
-
const res =
|
|
7398
|
-
flushEvents(
|
|
7630
|
+
if (strikes.some((strike) => strike.key === strikeKeyOf(id, hand))) return "cooldown";
|
|
7631
|
+
const res = state.trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], dt, held, "player");
|
|
7632
|
+
flushEvents(id, state, res.recoil, hand);
|
|
7399
7633
|
if (res.status === "empty") {
|
|
7400
|
-
if (!
|
|
7401
|
-
|
|
7634
|
+
if (!state.reloading && state.reserve > 0) {
|
|
7635
|
+
if (offTool) dualReload.start([hand, hand === "R" ? "L" : "R"]);
|
|
7636
|
+
else startReload(hand);
|
|
7637
|
+
}
|
|
7638
|
+
say(t(state.reloading ? "hud.reloading" : "hud.empty"));
|
|
7402
7639
|
return res.status;
|
|
7403
7640
|
}
|
|
7404
7641
|
if (res.status !== "ok") {
|
|
@@ -7408,15 +7645,18 @@ function createTestRoom(options) {
|
|
|
7408
7645
|
else if (res.status === "safe") say(t("hud.safe"));
|
|
7409
7646
|
return res.status;
|
|
7410
7647
|
}
|
|
7411
|
-
|
|
7648
|
+
firedHand = hand;
|
|
7649
|
+
applyUses(res, dt, hand);
|
|
7412
7650
|
return res.status;
|
|
7413
7651
|
};
|
|
7414
|
-
const applyUses = (res, dt) => {
|
|
7415
|
-
|
|
7652
|
+
const applyUses = (res, dt, hand = "R") => {
|
|
7653
|
+
const id = toolOf(hand)?.id ?? toolId;
|
|
7654
|
+
if (res.shots.length) fireShots(res.shots, res.recoil, id, hand);
|
|
7416
7655
|
for (const u of res.uses) {
|
|
7417
7656
|
switch (u.kind) {
|
|
7418
7657
|
case "melee":
|
|
7419
|
-
strikeUse(u);
|
|
7658
|
+
if (hand === "L") strikeUse(u, id, { hand: "left", key: strikeKeyOf(id, hand) });
|
|
7659
|
+
else strikeUse(u);
|
|
7420
7660
|
break;
|
|
7421
7661
|
case "flame":
|
|
7422
7662
|
heatRays(u, "fire");
|
|
@@ -7444,15 +7684,17 @@ function createTestRoom(options) {
|
|
|
7444
7684
|
}
|
|
7445
7685
|
}
|
|
7446
7686
|
};
|
|
7447
|
-
const releaseTrigger = () => {
|
|
7448
|
-
|
|
7449
|
-
|
|
7687
|
+
const releaseTrigger = (hand = "R") => {
|
|
7688
|
+
const slot2 = toolOf(hand);
|
|
7689
|
+
if (!slot2) return;
|
|
7690
|
+
if (!slot2.state.cooking) {
|
|
7691
|
+
slot2.state.release();
|
|
7450
7692
|
return;
|
|
7451
7693
|
}
|
|
7452
7694
|
aim();
|
|
7453
|
-
const res =
|
|
7454
|
-
flushEvents(
|
|
7455
|
-
if (res) applyUses(res, 0);
|
|
7695
|
+
const res = slot2.state.release([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]]);
|
|
7696
|
+
flushEvents(slot2.id, slot2.state, void 0, hand);
|
|
7697
|
+
if (res) applyUses(res, 0, hand);
|
|
7456
7698
|
};
|
|
7457
7699
|
const kick = () => {
|
|
7458
7700
|
if (!arsenal.weapons.has("kick") || strikes.some((strike) => strike.key === "kick")) return false;
|
|
@@ -7481,8 +7723,43 @@ function createTestRoom(options) {
|
|
|
7481
7723
|
}));
|
|
7482
7724
|
};
|
|
7483
7725
|
const toggleToolMenu = (open) => ui?.toolMenu(open) ?? false;
|
|
7726
|
+
const dropOffTool = () => {
|
|
7727
|
+
if (!offTool) return;
|
|
7728
|
+
releaseTrigger("L");
|
|
7729
|
+
offTool = null;
|
|
7730
|
+
offToolId = null;
|
|
7731
|
+
offHeld = false;
|
|
7732
|
+
offFirst = false;
|
|
7733
|
+
dualReload.cancel();
|
|
7734
|
+
firedHand = "R";
|
|
7735
|
+
tool.setStance({ dualWield: false });
|
|
7736
|
+
action("tool_left", t("act.tool_left_off"), { data: { hand: "L" } });
|
|
7737
|
+
emitHost({ type: "tool", weapon: "", hand: "L" });
|
|
7738
|
+
};
|
|
7739
|
+
const setOffTool = (id) => {
|
|
7740
|
+
if (id === null) {
|
|
7741
|
+
dropOffTool();
|
|
7742
|
+
return true;
|
|
7743
|
+
}
|
|
7744
|
+
if (!arsenal.weapons.has(id)) throw new Error(`game-test-room: unknown weapon "${id}"`);
|
|
7745
|
+
if (!canDualWield(arsenal, id) || !canDualWield(arsenal, toolId)) return false;
|
|
7746
|
+
if (offTool) releaseTrigger("L");
|
|
7747
|
+
const state = arsenal.create(id, { seed: OFF_HAND_SEED, reserve: 999, ...toolSettings });
|
|
7748
|
+
state.setFuseLength(fuseLength);
|
|
7749
|
+
state.refill();
|
|
7750
|
+
state.setStance({ dualWield: true });
|
|
7751
|
+
tool.setStance({ dualWield: true });
|
|
7752
|
+
offTool = state;
|
|
7753
|
+
offToolId = id;
|
|
7754
|
+
dualReload.cancel();
|
|
7755
|
+
action("tool_left", t("act.tool_left", { weapon: toolName(id) }), { data: { weapon: id, hand: "L" } });
|
|
7756
|
+
state.draw();
|
|
7757
|
+
flushEvents(id, state, void 0, "L");
|
|
7758
|
+
return true;
|
|
7759
|
+
};
|
|
7484
7760
|
const setTool = (id) => {
|
|
7485
7761
|
if (!arsenal.weapons.has(id)) throw new Error(`game-test-room: unknown weapon "${id}"`);
|
|
7762
|
+
dropOffTool();
|
|
7486
7763
|
releaseTrigger();
|
|
7487
7764
|
toolId = id;
|
|
7488
7765
|
const i = loadout.indexOf(id);
|
|
@@ -7975,15 +8252,13 @@ function createTestRoom(options) {
|
|
|
7975
8252
|
case "KeyR":
|
|
7976
8253
|
if (heldPiece) heldYaw += Math.PI / 8;
|
|
7977
8254
|
else {
|
|
7978
|
-
|
|
8255
|
+
reloadHands();
|
|
7979
8256
|
say(t("hud.reloading"));
|
|
7980
8257
|
}
|
|
7981
8258
|
break;
|
|
7982
|
-
case "KeyQ":
|
|
7983
|
-
|
|
7984
|
-
flushEvents(toolId, tool);
|
|
8259
|
+
case "KeyQ":
|
|
8260
|
+
cycleToolMode();
|
|
7985
8261
|
break;
|
|
7986
|
-
}
|
|
7987
8262
|
case "KeyX":
|
|
7988
8263
|
clearToolFault();
|
|
7989
8264
|
break;
|
|
@@ -8040,6 +8315,20 @@ function createTestRoom(options) {
|
|
|
8040
8315
|
if (!loadout.includes(weaponId)) loadout.push(weaponId);
|
|
8041
8316
|
setTool(weaponId);
|
|
8042
8317
|
};
|
|
8318
|
+
const pickTool = (weaponId, pick = { second: false }) => {
|
|
8319
|
+
if (!arsenal.weapons.has(weaponId)) return;
|
|
8320
|
+
if (!pick.second) {
|
|
8321
|
+
equip(weaponId);
|
|
8322
|
+
return;
|
|
8323
|
+
}
|
|
8324
|
+
if (!canDualWield(arsenal, weaponId)) {
|
|
8325
|
+
equip(weaponId);
|
|
8326
|
+
say(t("hud.no_dual", { weapon: toolName(weaponId) }));
|
|
8327
|
+
return;
|
|
8328
|
+
}
|
|
8329
|
+
if (!offTool || !canDualWield(arsenal, toolId)) equip(weaponId);
|
|
8330
|
+
setOffTool(weaponId);
|
|
8331
|
+
};
|
|
8043
8332
|
let hudAimHit;
|
|
8044
8333
|
const inspectTarget = () => {
|
|
8045
8334
|
if (hudAimHit !== void 0) return hudAimHit;
|
|
@@ -8243,6 +8532,11 @@ function createTestRoom(options) {
|
|
|
8243
8532
|
s.update(dt);
|
|
8244
8533
|
flushEvents(id, s);
|
|
8245
8534
|
}
|
|
8535
|
+
if (offTool && offToolId) {
|
|
8536
|
+
offTool.update(dt);
|
|
8537
|
+
flushEvents(offToolId, offTool, void 0, "L");
|
|
8538
|
+
dualReload.step(dt);
|
|
8539
|
+
}
|
|
8246
8540
|
if (audio) {
|
|
8247
8541
|
const look = camera.getWorldDirection(listenerDirection);
|
|
8248
8542
|
audio.setListener({ position: [camera.position.x, camera.position.y, camera.position.z], yaw: Math.atan2(-look.x, -look.z) });
|
|
@@ -8288,6 +8582,10 @@ function createTestRoom(options) {
|
|
|
8288
8582
|
} else if (useHeld) useTool(dt, !useFirst);
|
|
8289
8583
|
useFirst = false;
|
|
8290
8584
|
}
|
|
8585
|
+
if ((offHeld || offFirst) && offTool && !isPaused && !push.active) {
|
|
8586
|
+
useTool(dt, !offFirst, "L");
|
|
8587
|
+
offFirst = false;
|
|
8588
|
+
}
|
|
8291
8589
|
if (!isPaused) followHeld(dt);
|
|
8292
8590
|
};
|
|
8293
8591
|
const updatePresent = (dt) => {
|
|
@@ -8314,12 +8612,18 @@ function createTestRoom(options) {
|
|
|
8314
8612
|
hudAimHit = void 0;
|
|
8315
8613
|
}
|
|
8316
8614
|
};
|
|
8615
|
+
const ammoText = (state) => {
|
|
8616
|
+
const kind = state.def.action.kind;
|
|
8617
|
+
if (kind === "firearm") return `${state.ammo}/${state.capacity}${state.reserve ? ` \xB7 ${state.reserve}` : ""}`;
|
|
8618
|
+
return kind === "hand" || kind === "melee" || kind === "tool" ? "" : `${num(state.ammo, 0)}`;
|
|
8619
|
+
};
|
|
8317
8620
|
const readHudState = () => {
|
|
8318
8621
|
const page = Math.floor(toolIndex / 10);
|
|
8319
8622
|
const slots = loadout.slice(page * 10, page * 10 + 10).map((id) => arsenal.name(id));
|
|
8320
|
-
const
|
|
8623
|
+
const left = offTool ? ammoText(offTool) : "";
|
|
8624
|
+
const ammo = `${ammoText(tool)}${left ? ` | ${t("hud.left")} ${left}` : ""}`;
|
|
8321
8625
|
return {
|
|
8322
|
-
toolName: toolName(),
|
|
8626
|
+
toolName: offToolId ? `${toolName()} + ${toolName(offToolId)}` : toolName(),
|
|
8323
8627
|
toolId,
|
|
8324
8628
|
ammo,
|
|
8325
8629
|
slots,
|
|
@@ -8509,26 +8813,53 @@ function createTestRoom(options) {
|
|
|
8509
8813
|
if (event.type === "explosion") knockPlayer(event);
|
|
8510
8814
|
for (const listener of hostListeners) listener(event);
|
|
8511
8815
|
}
|
|
8512
|
-
function flushEvents(id, state, recoil) {
|
|
8816
|
+
function flushEvents(id, state, recoil, hand = "R") {
|
|
8513
8817
|
const phase = state.phase;
|
|
8514
8818
|
for (const event of state.drain()) {
|
|
8515
8819
|
const hostEvent = toHostEvent(id, event, { ...recoil ? { recoil } : {}, phase });
|
|
8516
|
-
if (hostEvent) emitHost(hostEvent);
|
|
8820
|
+
if (hostEvent) emitHost(hand === "L" ? { ...hostEvent, hand } : hostEvent);
|
|
8517
8821
|
}
|
|
8518
8822
|
}
|
|
8519
|
-
function startReload() {
|
|
8520
|
-
|
|
8521
|
-
|
|
8823
|
+
function startReload(hand = "R") {
|
|
8824
|
+
const slot2 = toolOf(hand);
|
|
8825
|
+
if (!slot2) return false;
|
|
8826
|
+
const started = slot2.state.reload();
|
|
8827
|
+
flushEvents(slot2.id, slot2.state, void 0, hand);
|
|
8828
|
+
return started;
|
|
8522
8829
|
}
|
|
8830
|
+
function reloadHands() {
|
|
8831
|
+
if (!offTool) {
|
|
8832
|
+
startReload();
|
|
8833
|
+
return;
|
|
8834
|
+
}
|
|
8835
|
+
dualReload.start(["R", "L"]);
|
|
8836
|
+
}
|
|
8837
|
+
const commandHand = () => offTool ? firedHand : "R";
|
|
8523
8838
|
function clearToolFault() {
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8839
|
+
const order = offTool ? [firedHand, firedHand === "R" ? "L" : "R"] : ["R"];
|
|
8840
|
+
for (const hand of order) {
|
|
8841
|
+
const slot2 = toolOf(hand);
|
|
8842
|
+
if (!slot2?.state.fault || !slot2.state.clearFault()) continue;
|
|
8843
|
+
flushEvents(slot2.id, slot2.state, void 0, hand);
|
|
8844
|
+
say(t("hud.clearing"));
|
|
8845
|
+
return;
|
|
8846
|
+
}
|
|
8527
8847
|
}
|
|
8528
8848
|
function toggleSafety() {
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8849
|
+
const hand = commandHand();
|
|
8850
|
+
const slot2 = toolOf(hand);
|
|
8851
|
+
if (!slot2) return;
|
|
8852
|
+
slot2.state.setSafety(!slot2.state.safety);
|
|
8853
|
+
flushEvents(slot2.id, slot2.state, void 0, hand);
|
|
8854
|
+
say(`${t(slot2.state.safety ? "hud.safety_on" : "hud.safety_off")}${offTool ? ` (${toolName(slot2.id)}, ${t(hand === "L" ? "hand.left" : "hand.right")})` : ""}`);
|
|
8855
|
+
}
|
|
8856
|
+
function cycleToolMode() {
|
|
8857
|
+
const hand = commandHand();
|
|
8858
|
+
const slot2 = toolOf(hand);
|
|
8859
|
+
if (!slot2) return;
|
|
8860
|
+
slot2.state.cycleMode();
|
|
8861
|
+
flushEvents(slot2.id, slot2.state, void 0, hand);
|
|
8862
|
+
if (offTool) say(`${toolName(slot2.id)} (${t(hand === "L" ? "hand.left" : "hand.right")}): ${slot2.state.mode}`);
|
|
8532
8863
|
}
|
|
8533
8864
|
function runToolCommand(request) {
|
|
8534
8865
|
const work = (done) => done.ok ? { ok: true, seconds: done.seconds } : { ok: false, reason: done.reason };
|
|
@@ -8550,6 +8881,24 @@ function createTestRoom(options) {
|
|
|
8550
8881
|
return { ok: false, reason: "unknown" };
|
|
8551
8882
|
}
|
|
8552
8883
|
}
|
|
8884
|
+
const hostToolOf = (id, state) => ({
|
|
8885
|
+
weapon: id,
|
|
8886
|
+
category: state.def.category,
|
|
8887
|
+
ammo: state.ammo,
|
|
8888
|
+
capacity: state.capacity,
|
|
8889
|
+
reserve: state.reserve,
|
|
8890
|
+
reloading: state.reloading,
|
|
8891
|
+
mode: state.mode,
|
|
8892
|
+
...state.fault ? { fault: state.fault } : {},
|
|
8893
|
+
safety: state.safety,
|
|
8894
|
+
heat: state.heatLevel,
|
|
8895
|
+
attachments: state.attachments.map((part) => ({ id: part.id, mount: part.mount, ...part.integral ? { integral: true } : {}, ...part.on === void 0 ? {} : { on: part.on } })),
|
|
8896
|
+
stock: state.stock,
|
|
8897
|
+
suppressed: state.suppressed,
|
|
8898
|
+
underbarrelActive: state.underbarrelActive,
|
|
8899
|
+
mass: state.mass,
|
|
8900
|
+
aimSeconds: state.aimSeconds
|
|
8901
|
+
});
|
|
8553
8902
|
const host = {
|
|
8554
8903
|
THREE,
|
|
8555
8904
|
scene,
|
|
@@ -8576,22 +8925,9 @@ function createTestRoom(options) {
|
|
|
8576
8925
|
...options.blastShove ? { blastShove: options.blastShove } : {},
|
|
8577
8926
|
blastReach: (point, target, ignore) => world.blastReach(point, target, ignore ? { ignore } : {}),
|
|
8578
8927
|
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
|
|
8928
|
+
...hostToolOf(toolId, tool),
|
|
8929
|
+
...dualReload.stowed("R") ? { stowed: true } : {},
|
|
8930
|
+
...offTool && offToolId ? { offHand: { ...hostToolOf(offToolId, offTool), ...dualReload.stowed("L") ? { stowed: true } : {} } } : {}
|
|
8595
8931
|
}),
|
|
8596
8932
|
command(request) {
|
|
8597
8933
|
const result = runToolCommand(request);
|
|
@@ -8628,7 +8964,7 @@ function createTestRoom(options) {
|
|
|
8628
8964
|
onFrame(update, frameOptions) {
|
|
8629
8965
|
return scheduler.add((ctx) => update(ctx.dt), { phase: frameOptions?.phase ?? "lateSimulate", order: frameOptions?.order ?? 0, id: `module.frame#${moduleFrameCount++}` });
|
|
8630
8966
|
},
|
|
8631
|
-
holdFrame: () => bodyHoldFrame(),
|
|
8967
|
+
holdFrame: (hand) => bodyHoldFrame(hand),
|
|
8632
8968
|
reachFor(reach) {
|
|
8633
8969
|
if (!avatar) {
|
|
8634
8970
|
reach.onGrip?.();
|
|
@@ -9004,6 +9340,7 @@ function createTestRoom(options) {
|
|
|
9004
9340
|
fluids,
|
|
9005
9341
|
film: filmBridge,
|
|
9006
9342
|
vfx: vfx.world,
|
|
9343
|
+
effects: vfx,
|
|
9007
9344
|
fluidRenderer: fluidView,
|
|
9008
9345
|
audio,
|
|
9009
9346
|
environment,
|
|
@@ -9019,6 +9356,29 @@ function createTestRoom(options) {
|
|
|
9019
9356
|
clear,
|
|
9020
9357
|
resetAll,
|
|
9021
9358
|
setTool,
|
|
9359
|
+
setOffTool,
|
|
9360
|
+
get offTool() {
|
|
9361
|
+
return offTool;
|
|
9362
|
+
},
|
|
9363
|
+
useOff(hold = 0) {
|
|
9364
|
+
if (!offTool) return "cooldown";
|
|
9365
|
+
const status = useTool(FIXED_DT, false, "L");
|
|
9366
|
+
flush();
|
|
9367
|
+
let t0 = 0;
|
|
9368
|
+
while (t0 < hold) {
|
|
9369
|
+
useTool(FIXED_DT, true, "L");
|
|
9370
|
+
directStep = true;
|
|
9371
|
+
try {
|
|
9372
|
+
runHeadless(() => scheduler.advance(FIXED_DT));
|
|
9373
|
+
} finally {
|
|
9374
|
+
directStep = false;
|
|
9375
|
+
}
|
|
9376
|
+
t0 += FIXED_DT;
|
|
9377
|
+
}
|
|
9378
|
+
releaseTrigger("L");
|
|
9379
|
+
return status;
|
|
9380
|
+
},
|
|
9381
|
+
reload: () => reloadHands(),
|
|
9022
9382
|
useItem,
|
|
9023
9383
|
use(hold = 0) {
|
|
9024
9384
|
if (tool.def.category === "hand") {
|
|
@@ -9316,7 +9676,8 @@ function createTestRoom(options) {
|
|
|
9316
9676
|
...options.toolMenu !== false ? {
|
|
9317
9677
|
toolMenu: {
|
|
9318
9678
|
groups: toolMenuGroups,
|
|
9319
|
-
onPick:
|
|
9679
|
+
onPick: pickTool,
|
|
9680
|
+
currentLeft: () => offToolId,
|
|
9320
9681
|
// the list takes clicks: the mouse leaves the view while it is open
|
|
9321
9682
|
onOpen: (open) => {
|
|
9322
9683
|
if (open && document.pointerLockElement) document.exitPointerLock?.();
|