@adatechnology/game-test-room 0.21.0 → 0.23.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/dist/index.d.ts +14 -7
- package/dist/index.global.js +3362 -547
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +420 -27
- package/dist/index.js.map +1 -1
- package/package.json +14 -14
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// src/room.ts
|
|
2
2
|
import { createDestruction, JOINT_DEFAULTS, resolveStrings as resolveDestructionStrings } from "@adatechnology/game-destruction";
|
|
3
|
-
import { createAudioEngine, createAudioSink } from "@adatechnology/game-audio";
|
|
4
|
-
import {
|
|
3
|
+
import { createAudioEngine, createAudioSink, loadKitSamples } from "@adatechnology/game-audio";
|
|
4
|
+
import { createDestructionRenderer, createDestructionSound, dragSoundKey, impactKey, impactSoundKeys, specFromObject3D } from "@adatechnology/game-destruction-three";
|
|
5
|
+
import { bodyMass, KNOCK_CHEST, knockOf, surfaceOfMaterial } from "@adatechnology/game-contracts";
|
|
5
6
|
|
|
6
7
|
// src/body-sounds.ts
|
|
7
8
|
import { BODY_FOLEY_BANK } from "@adatechnology/game-audio";
|
|
@@ -31,13 +32,14 @@ function blowOfTool(id, category, table = DEFAULT_TOOL_BLOWS) {
|
|
|
31
32
|
}
|
|
32
33
|
var levelOf = (energyJ) => Math.max(0.5, Math.min(1, 0.5 + energyJ / 800));
|
|
33
34
|
var STRIDE_METRES = { walk: 0.72, run: 1.05 };
|
|
35
|
+
var SLAM_HURT_SPEED = 3.5;
|
|
34
36
|
function createBodySounds(audio) {
|
|
35
37
|
audio?.register(BODY_FOLEY_BANK);
|
|
36
38
|
const played = [];
|
|
37
39
|
const play = (id, position, volume, delay = 0) => {
|
|
38
40
|
played.push(id);
|
|
39
41
|
if (played.length > 64) played.shift();
|
|
40
|
-
audio?.play(id, { position, volume, ...delay > 0 ? { delay } : {} });
|
|
42
|
+
audio?.play(id, { position, volume, urgent: true, ...delay > 0 ? { delay } : {} });
|
|
41
43
|
};
|
|
42
44
|
return {
|
|
43
45
|
swing(blow, position, delay = 0) {
|
|
@@ -49,6 +51,10 @@ function createBodySounds(audio) {
|
|
|
49
51
|
},
|
|
50
52
|
jump: (position) => play("jump", position, 0.6),
|
|
51
53
|
land: (surface, speed, position) => play(landSoundId(surface, speed), position, Math.min(1, 0.45 + speed / 8)),
|
|
54
|
+
slam(speed, position) {
|
|
55
|
+
play("body_fall", position, Math.min(1, 0.4 + speed / 8));
|
|
56
|
+
if (speed >= SLAM_HURT_SPEED) play("player_hurt", position, Math.min(1, 0.5 + speed / 12));
|
|
57
|
+
},
|
|
52
58
|
played
|
|
53
59
|
};
|
|
54
60
|
}
|
|
@@ -73,7 +79,6 @@ function createStrideCounter(stride = STRIDE_METRES) {
|
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
// src/room.ts
|
|
76
|
-
import { createDestructionRenderer, createDestructionSound, dragSoundKey, impactKey, specFromObject3D } from "@adatechnology/game-destruction-three";
|
|
77
82
|
import { createArsenal, DEFAULT_LOADOUT } from "@adatechnology/game-weapons";
|
|
78
83
|
import { createFilmLcg, createFluids, createItemUse, ITEM_USES, resolveStrings as resolveFluidStrings } from "@adatechnology/game-fluids";
|
|
79
84
|
import { createFilmBridge, createFilmManager, createFluidRenderer } from "@adatechnology/game-fluids-three";
|
|
@@ -316,6 +321,38 @@ function createArena(THREE, size, scene, scenery, tapOption) {
|
|
|
316
321
|
};
|
|
317
322
|
}
|
|
318
323
|
|
|
324
|
+
// src/hand-sounds.ts
|
|
325
|
+
import { LEAF_FOLEY_SETS } from "@adatechnology/game-audio";
|
|
326
|
+
function pickupClassOf(def) {
|
|
327
|
+
if (!def) return "plastic";
|
|
328
|
+
const tags = def.tags ?? [];
|
|
329
|
+
if (def.id === "paper") return "paper";
|
|
330
|
+
if (tags.includes("metal")) return "metal";
|
|
331
|
+
if (tags.includes("glass") || tags.includes("ceramic")) return "glass";
|
|
332
|
+
if (tags.includes("plastic") || tags.includes("rubber")) return "plastic";
|
|
333
|
+
if (tags.includes("wood")) return "wood";
|
|
334
|
+
if (tags.includes("fabric")) return "fabric";
|
|
335
|
+
if (tags.includes("plant")) return "food";
|
|
336
|
+
return "plastic";
|
|
337
|
+
}
|
|
338
|
+
var pickupSoundOf = (sounds, def) => `item_pickup_${sounds?.handling ?? pickupClassOf(def)}`;
|
|
339
|
+
function leafFoleyOf(sounds, part, impactKey2, slides2) {
|
|
340
|
+
const named = sounds?.leaves?.[part] ?? sounds?.leaves?.["*"];
|
|
341
|
+
const set = named ? LEAF_FOLEY_SETS[named] : void 0;
|
|
342
|
+
if (set) return set;
|
|
343
|
+
if (slides2) return LEAF_FOLEY_SETS.drawer;
|
|
344
|
+
return /metal|glass/.test(impactKey2) ? LEAF_FOLEY_SETS.metal_door : LEAF_FOLEY_SETS.wood_door;
|
|
345
|
+
}
|
|
346
|
+
var openSoundOf = (sounds, def) => sounds?.open ?? (pickupClassOf(def) === "metal" ? "can_open" : "bottle_open");
|
|
347
|
+
var USE_SOUNDS = {
|
|
348
|
+
drink: "drink_gulp",
|
|
349
|
+
sip: "drink_gulp",
|
|
350
|
+
chug: "drink_gulp",
|
|
351
|
+
eat: "eat_chew"
|
|
352
|
+
};
|
|
353
|
+
var USE_SOUND_EVERY = 0.55;
|
|
354
|
+
var useSoundOf = (sounds, useId) => sounds?.uses?.[useId] ?? USE_SOUNDS[useId];
|
|
355
|
+
|
|
319
356
|
// src/bench.ts
|
|
320
357
|
var snap = (control, value) => {
|
|
321
358
|
const clamped = Math.min(control.max, Math.max(control.min, value));
|
|
@@ -765,7 +802,7 @@ function createExplosives(context) {
|
|
|
765
802
|
if (!detonation.cone) told(results);
|
|
766
803
|
if (detonation.energy > 0) {
|
|
767
804
|
const isPlayed = context.effects.explosion(detonation.explosive, detonation.point, { direction: detonation.direction }) !== null;
|
|
768
|
-
renderer.explode(detonation.point, detonation.energy, reach, { visual: !isPlayed, report: results });
|
|
805
|
+
renderer.explode(detonation.point, detonation.energy, reach, { visual: !isPlayed, report: results, effect: detonation.explosive });
|
|
769
806
|
}
|
|
770
807
|
if (detonation.stunSeconds !== void 0) renderer.emit("flash", detonation.point, [0, 1, 0], 1, 1);
|
|
771
808
|
const onPlayer = blastEffect(detonation, toArray(camera.position));
|
|
@@ -2088,6 +2125,8 @@ function createNarrator(c) {
|
|
|
2088
2125
|
}
|
|
2089
2126
|
|
|
2090
2127
|
// src/player.ts
|
|
2128
|
+
import { stepKnock } from "@adatechnology/game-contracts";
|
|
2129
|
+
var SLAM_MIN_SPEED = 1.5;
|
|
2091
2130
|
var LANDING_MIN_SPEED = 1;
|
|
2092
2131
|
var EYE = 1.7;
|
|
2093
2132
|
var PLAYER_RADIUS = 0.3;
|
|
@@ -2105,6 +2144,10 @@ var GRAVITY = 13;
|
|
|
2105
2144
|
var FLOOR_CLIMB_SPEED = 3;
|
|
2106
2145
|
var FLOOR_FALL_SPEED = 6;
|
|
2107
2146
|
var LOOK = 22e-4;
|
|
2147
|
+
var LYING_EYE = 0.3;
|
|
2148
|
+
var LYING_PITCH = 1.05;
|
|
2149
|
+
var LYING_ROLL = 0.35;
|
|
2150
|
+
var LYING_PITCH_FACE_DOWN = -1.05;
|
|
2108
2151
|
function createPlayer(THREE, o) {
|
|
2109
2152
|
const cam = o.camera;
|
|
2110
2153
|
let yaw = 0;
|
|
@@ -2142,10 +2185,16 @@ function createPlayer(THREE, o) {
|
|
|
2142
2185
|
const reach = (gap > 0 ? FLOOR_CLIMB_SPEED : FLOOR_FALL_SPEED) * dt;
|
|
2143
2186
|
floorLevel += Math.max(-reach, Math.min(reach, gap));
|
|
2144
2187
|
};
|
|
2188
|
+
let knocked = null;
|
|
2189
|
+
let knockedForward = false;
|
|
2190
|
+
let rollSide = 1;
|
|
2145
2191
|
const apply = () => {
|
|
2146
|
-
|
|
2192
|
+
const lying = knocked?.lying ?? 0;
|
|
2193
|
+
const fallen = lying * lying * (3 - 2 * lying);
|
|
2194
|
+
euler.set(pitch + ((knockedForward ? LYING_PITCH_FACE_DOWN : LYING_PITCH) - pitch) * fallen, yaw, LYING_ROLL * rollSide * fallen, "YXZ");
|
|
2147
2195
|
cam.quaternion.setFromEuler(euler);
|
|
2148
|
-
|
|
2196
|
+
const standing = eye - CROUCH_DROP * crouch - bend;
|
|
2197
|
+
cam.position.set(px.x, floorLevel + y + standing + (LYING_EYE - standing) * fallen, px.z);
|
|
2149
2198
|
cam.updateMatrixWorld(true);
|
|
2150
2199
|
};
|
|
2151
2200
|
apply();
|
|
@@ -2320,6 +2369,41 @@ function createPlayer(THREE, o) {
|
|
|
2320
2369
|
window.addEventListener("keyup", onKeyUp);
|
|
2321
2370
|
window.addEventListener("blur", onBlur);
|
|
2322
2371
|
window.addEventListener("beforeunload", onBeforeUnload);
|
|
2372
|
+
const stepKnocked = (dt) => {
|
|
2373
|
+
const k = knocked;
|
|
2374
|
+
if (!k) return;
|
|
2375
|
+
const floor = Math.max(floorHere(), o.footing?.(px.x, px.z, PLAYER_RADIUS * FOOT, y).floor ?? -Infinity);
|
|
2376
|
+
floorUnder = floor;
|
|
2377
|
+
const at = { x: px.x, y, z: px.z };
|
|
2378
|
+
const fromY = y;
|
|
2379
|
+
const fallSpeed = Math.max(0, -k.velocity[1]);
|
|
2380
|
+
const done = stepKnock(k, at, dt, () => floor, true);
|
|
2381
|
+
const wantX = at.x - px.x;
|
|
2382
|
+
const wantZ = at.z - px.z;
|
|
2383
|
+
const fromX = px.x;
|
|
2384
|
+
const fromZ = px.z;
|
|
2385
|
+
const vx = k.velocity[0];
|
|
2386
|
+
const vz = k.velocity[2];
|
|
2387
|
+
move(wantX, wantZ);
|
|
2388
|
+
const stoppedX = Math.abs(wantX) > 1e-6 && Math.abs(px.x - fromX) < Math.abs(wantX) * 0.5;
|
|
2389
|
+
const stoppedZ = Math.abs(wantZ) > 1e-6 && Math.abs(px.z - fromZ) < Math.abs(wantZ) * 0.5;
|
|
2390
|
+
if (stoppedX) k.velocity[0] = 0;
|
|
2391
|
+
if (stoppedZ) k.velocity[2] = 0;
|
|
2392
|
+
const into = Math.hypot(stoppedX ? vx : 0, stoppedZ ? vz : 0);
|
|
2393
|
+
if (into >= SLAM_MIN_SPEED) o.onSlam?.({ against: "obstacle", speed: into, direction: [stoppedX ? vx / into : 0, 0, stoppedZ ? vz / into : 0] });
|
|
2394
|
+
y = at.y;
|
|
2395
|
+
if (fromY > floor && y <= floor) {
|
|
2396
|
+
const speed = Math.max(fallSpeed, (fromY - y) / Math.max(dt, 1e-6));
|
|
2397
|
+
if (k.down && speed >= SLAM_MIN_SPEED * 0.5) o.onSlam?.({ against: "ground", speed: Math.hypot(speed, Math.hypot(vx, vz) * 0.5), direction: [0, -1, 0] });
|
|
2398
|
+
else if (speed >= LANDING_MIN_SPEED) o.onFeet?.({ type: "land", speed });
|
|
2399
|
+
}
|
|
2400
|
+
vy = 0;
|
|
2401
|
+
walked = dt > 0 ? Math.hypot(px.x - fromX, px.z - fromZ) / dt : 0;
|
|
2402
|
+
running = false;
|
|
2403
|
+
if (done) knocked = null;
|
|
2404
|
+
followFloor(dt);
|
|
2405
|
+
apply();
|
|
2406
|
+
};
|
|
2323
2407
|
return {
|
|
2324
2408
|
get walkIntent() {
|
|
2325
2409
|
return walkKeys();
|
|
@@ -2375,7 +2459,32 @@ function createPlayer(THREE, o) {
|
|
|
2375
2459
|
return isCrouched();
|
|
2376
2460
|
},
|
|
2377
2461
|
keys,
|
|
2462
|
+
knock(next) {
|
|
2463
|
+
if (seated) return;
|
|
2464
|
+
if (knocked) {
|
|
2465
|
+
knocked.velocity = [knocked.velocity[0] + next.velocity[0], knocked.velocity[1] + next.velocity[1], knocked.velocity[2] + next.velocity[2]];
|
|
2466
|
+
knocked.airborne || (knocked.airborne = next.airborne);
|
|
2467
|
+
knocked.down || (knocked.down = next.down);
|
|
2468
|
+
} else knocked = { ...next, velocity: [...next.velocity] };
|
|
2469
|
+
if (next.down) {
|
|
2470
|
+
const right = Math.cos(yaw) * next.velocity[0] - Math.sin(yaw) * next.velocity[2];
|
|
2471
|
+
rollSide = right >= 0 ? -1 : 1;
|
|
2472
|
+
knockedForward = -Math.sin(yaw) * next.velocity[0] - Math.cos(yaw) * next.velocity[2] > 0;
|
|
2473
|
+
crouchLatched = false;
|
|
2474
|
+
}
|
|
2475
|
+
if (knocked.airborne) vy = Math.max(vy, knocked.velocity[1]);
|
|
2476
|
+
},
|
|
2477
|
+
get knocked() {
|
|
2478
|
+
return knocked;
|
|
2479
|
+
},
|
|
2480
|
+
get knockedForward() {
|
|
2481
|
+
return knockedForward;
|
|
2482
|
+
},
|
|
2378
2483
|
update(dt) {
|
|
2484
|
+
if (knocked) {
|
|
2485
|
+
stepKnocked(dt);
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2379
2488
|
if (crouchPress) crouchPress.held += dt;
|
|
2380
2489
|
const crouched = isCrouched();
|
|
2381
2490
|
crouch += ((crouched ? 1 : 0) - crouch) * Math.min(1, dt * CROUCH_RATE);
|
|
@@ -2415,6 +2524,7 @@ function createPlayer(THREE, o) {
|
|
|
2415
2524
|
apply();
|
|
2416
2525
|
},
|
|
2417
2526
|
setPose(position, yw, pt) {
|
|
2527
|
+
knocked = null;
|
|
2418
2528
|
px.x = position[0];
|
|
2419
2529
|
px.z = position[2];
|
|
2420
2530
|
floorLevel = floorHere();
|
|
@@ -2426,6 +2536,7 @@ function createPlayer(THREE, o) {
|
|
|
2426
2536
|
apply();
|
|
2427
2537
|
},
|
|
2428
2538
|
look(position, target) {
|
|
2539
|
+
knocked = null;
|
|
2429
2540
|
px.x = position[0];
|
|
2430
2541
|
px.z = position[2];
|
|
2431
2542
|
floorLevel = floorHere();
|
|
@@ -2782,6 +2893,8 @@ var TURN_RATE = 8;
|
|
|
2782
2893
|
var MAX_WALK_SECONDS = 1.5;
|
|
2783
2894
|
var APPROACH_SPEED2 = 3;
|
|
2784
2895
|
var FLOOR_GAP = 0.03;
|
|
2896
|
+
var LEAF_SLAM_VOLUME = 0.85;
|
|
2897
|
+
var shutVolume = (massKg) => Math.min(1, 0.3 + massKg * 0.03);
|
|
2785
2898
|
var key = (objectId, part) => `${objectId}\0${part}`;
|
|
2786
2899
|
var slides = (clip, part) => {
|
|
2787
2900
|
const last = clip.keys[clip.keys.length - 1]?.parts?.[part];
|
|
@@ -3027,7 +3140,7 @@ function createRoomDoors(options) {
|
|
|
3027
3140
|
using = { objectId, leaf, obj, grip, pull, phase: control.phase, progress: 0, walked: 0, there: goal === null };
|
|
3028
3141
|
const at = obj.localToWorld(grip.clone());
|
|
3029
3142
|
if (goal === null) {
|
|
3030
|
-
options.sound("leaf_rattle", [at.x, at.y, at.z], 0.6);
|
|
3143
|
+
options.sound(options.leafFoley?.(objectId, leaf.part, slides(leaf.clip, leaf.part))?.locked ?? "leaf_rattle", [at.x, at.y, at.z], 0.6);
|
|
3031
3144
|
event("locked", using);
|
|
3032
3145
|
} else event("reach", using);
|
|
3033
3146
|
return true;
|
|
@@ -3112,17 +3225,27 @@ function createRoomDoors(options) {
|
|
|
3112
3225
|
const at = u.obj.localToWorld(tmp.copy(u.grip));
|
|
3113
3226
|
const point = [at.x, at.y, at.z];
|
|
3114
3227
|
const slide = slides(u.leaf.clip, u.leaf.part);
|
|
3115
|
-
const
|
|
3228
|
+
const foley = options.leafFoley?.(u.objectId, u.leaf.part, slide) ?? null;
|
|
3229
|
+
const own = (moment) => options.leafSound?.(u.objectId, u.leaf.part, moment);
|
|
3230
|
+
const shut = own("shut") ?? options.impactKey(u.objectId, u.leaf.part).replace("impact_", "leaf_shut_");
|
|
3116
3231
|
if (wasPhase === "reach" && s.phase !== "reach") {
|
|
3117
|
-
|
|
3118
|
-
|
|
3232
|
+
const opening = foley ? void 0 : own("open");
|
|
3233
|
+
if (foley) {
|
|
3234
|
+
if (was < 0.02) options.sound(foley.open, point, 0.5);
|
|
3235
|
+
if (foley.turn) options.sound(foley.turn, point, 0.35);
|
|
3236
|
+
} else if (slide) options.sound("leaf_slide", point, 0.5);
|
|
3237
|
+
else if (opening) {
|
|
3238
|
+
if (was < 0.02) options.sound(opening, point, 0.5);
|
|
3239
|
+
} else {
|
|
3119
3240
|
if (was < 0.02) options.sound("leaf_latch", point, 0.5);
|
|
3120
3241
|
options.sound(/metal|glass/.test(shut) ? "leaf_creak_metal" : "leaf_creak_wood", point, 0.35);
|
|
3121
3242
|
}
|
|
3122
3243
|
}
|
|
3123
3244
|
if (s.arrived) {
|
|
3124
3245
|
if (s.opening <= 0) {
|
|
3125
|
-
|
|
3246
|
+
const volume = shutVolume(massOf(u.objectId, u.leaf.part));
|
|
3247
|
+
if (foley) options.sound(foley.slam && volume >= LEAF_SLAM_VOLUME ? foley.slam : foley.close, point, volume);
|
|
3248
|
+
else options.sound(slide ? "leaf_slide" : shut, point, volume);
|
|
3126
3249
|
event("closed", u);
|
|
3127
3250
|
} else event("opened", u);
|
|
3128
3251
|
}
|
|
@@ -3393,6 +3516,10 @@ var en = {
|
|
|
3393
3516
|
"loading.open_area": "Opening {area}\u2026",
|
|
3394
3517
|
"loading.spawn": "Building {name}\u2026",
|
|
3395
3518
|
"act.loosened": "{name} was heavy but free: the blast moved it",
|
|
3519
|
+
"act.knocked_down": "The blast threw the player off their feet at {speed} m/s ({reach}% got through the cover)",
|
|
3520
|
+
"act.player_slammed": "The player was thrown into {what} at {speed} m/s",
|
|
3521
|
+
"act.player_fell": "The player hit the floor at {speed} m/s",
|
|
3522
|
+
"act.player_shoved": "The blast pushed the player at {speed} m/s ({reach}% got through the cover)",
|
|
3396
3523
|
"act.came_apart": "{name} came apart: what stayed is loose and falls",
|
|
3397
3524
|
"act.shoved": "{name} stands free: the shots moved it",
|
|
3398
3525
|
"act.impact": "{name} hit {where} at {speed} m/s: {energy} J of blunt damage",
|
|
@@ -3751,6 +3878,10 @@ var ptBR = {
|
|
|
3751
3878
|
"loading.open_area": "Abrindo {area}\u2026",
|
|
3752
3879
|
"loading.spawn": "Montando {name}\u2026",
|
|
3753
3880
|
"act.loosened": "{name} \xE9 pesado mas estava solto: a explos\xE3o o moveu",
|
|
3881
|
+
"act.knocked_down": "A explos\xE3o derrubou o jogador a {speed} m/s ({reach}% passou pela cobertura)",
|
|
3882
|
+
"act.player_slammed": "O jogador foi arremessado contra {what} a {speed} m/s",
|
|
3883
|
+
"act.player_fell": "O jogador bateu no ch\xE3o a {speed} m/s",
|
|
3884
|
+
"act.player_shoved": "A explos\xE3o empurrou o jogador a {speed} m/s ({reach}% passou pela cobertura)",
|
|
3754
3885
|
"act.came_apart": "{name} se desmontou: o que ficou est\xE1 solto e cai",
|
|
3755
3886
|
"act.shoved": "{name} estava solto no ch\xE3o: os tiros o moveram",
|
|
3756
3887
|
"act.impact": "{name} bateu em {where} a {speed} m/s: {energy} J de dano de impacto",
|
|
@@ -4576,9 +4707,16 @@ function createUi(o) {
|
|
|
4576
4707
|
nodes.delete(oldest.value);
|
|
4577
4708
|
logBody.firstElementChild?.remove();
|
|
4578
4709
|
}
|
|
4579
|
-
if (autoscroll)
|
|
4710
|
+
if (autoscroll) isScrollDue = true;
|
|
4580
4711
|
}
|
|
4712
|
+
let isScrollDue = false;
|
|
4713
|
+
const scrollToEnd = () => {
|
|
4714
|
+
if (!isScrollDue) return;
|
|
4715
|
+
isScrollDue = false;
|
|
4716
|
+
logBody.scrollTop = logBody.scrollHeight;
|
|
4717
|
+
};
|
|
4581
4718
|
const flushLog = () => {
|
|
4719
|
+
scrollToEnd();
|
|
4582
4720
|
if (pendingAdds.length === 0 && pendingUpdates.size === 0) return;
|
|
4583
4721
|
if (!isLogVisible()) {
|
|
4584
4722
|
isLogStale = true;
|
|
@@ -4971,6 +5109,134 @@ function createUi(o) {
|
|
|
4971
5109
|
return api;
|
|
4972
5110
|
}
|
|
4973
5111
|
|
|
5112
|
+
// src/module-views.ts
|
|
5113
|
+
var FRAME_PX = 3;
|
|
5114
|
+
function createModuleViews(THREE, gl) {
|
|
5115
|
+
const entries = [];
|
|
5116
|
+
let mainCamera = null;
|
|
5117
|
+
const overlay = new THREE.Scene();
|
|
5118
|
+
const ortho = new THREE.OrthographicCamera(-0.5, 0.5, 0.5, -0.5, 0, 2);
|
|
5119
|
+
ortho.position.z = 1;
|
|
5120
|
+
const plane = new THREE.PlaneGeometry(1, 1);
|
|
5121
|
+
const roundedMask = (rounded, aspect) => {
|
|
5122
|
+
if (!rounded || typeof document === "undefined") return null;
|
|
5123
|
+
const canvas = document.createElement("canvas");
|
|
5124
|
+
canvas.width = 256;
|
|
5125
|
+
canvas.height = Math.max(16, Math.round(256 / aspect));
|
|
5126
|
+
const g = canvas.getContext("2d");
|
|
5127
|
+
if (!g) return null;
|
|
5128
|
+
const r = Math.min(canvas.width, canvas.height) * Math.min(0.5, rounded);
|
|
5129
|
+
g.fillStyle = "#000";
|
|
5130
|
+
g.fillRect(0, 0, canvas.width, canvas.height);
|
|
5131
|
+
g.fillStyle = "#fff";
|
|
5132
|
+
g.beginPath();
|
|
5133
|
+
g.roundRect(0, 0, canvas.width, canvas.height, r);
|
|
5134
|
+
g.fill();
|
|
5135
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
5136
|
+
texture.colorSpace = THREE.NoColorSpace;
|
|
5137
|
+
return texture;
|
|
5138
|
+
};
|
|
5139
|
+
const size = () => {
|
|
5140
|
+
if (typeof gl.getSize !== "function") return { w: 960, h: 600 };
|
|
5141
|
+
const v = new THREE.Vector2();
|
|
5142
|
+
gl.getSize(v);
|
|
5143
|
+
return { w: v.x, h: v.y };
|
|
5144
|
+
};
|
|
5145
|
+
const take = (entry) => {
|
|
5146
|
+
const i = entries.indexOf(entry);
|
|
5147
|
+
if (i < 0) return;
|
|
5148
|
+
entries.splice(i, 1);
|
|
5149
|
+
overlay.remove(entry.quad, entry.frame);
|
|
5150
|
+
entry.target.dispose();
|
|
5151
|
+
entry.quad.material.dispose();
|
|
5152
|
+
entry.frame.material.dispose();
|
|
5153
|
+
entry.alpha?.dispose();
|
|
5154
|
+
};
|
|
5155
|
+
return {
|
|
5156
|
+
add(view) {
|
|
5157
|
+
const { w, h } = size();
|
|
5158
|
+
const scale = view.resolution ?? 0.5;
|
|
5159
|
+
const pr = typeof gl.getPixelRatio === "function" ? gl.getPixelRatio() : 1;
|
|
5160
|
+
const target = new THREE.WebGLRenderTarget(Math.max(16, Math.round(w * view.rect.width * pr * scale)), Math.max(16, Math.round(h * view.rect.height * pr * scale)), { samples: 2 });
|
|
5161
|
+
target.texture.colorSpace = THREE.LinearSRGBColorSpace;
|
|
5162
|
+
const alpha = roundedMask(view.rounded ?? 0, w * view.rect.width / Math.max(1, h * view.rect.height));
|
|
5163
|
+
const quad = new THREE.Mesh(plane, new THREE.MeshBasicMaterial({ map: target.texture, side: THREE.DoubleSide, depthTest: false, depthWrite: false, ...alpha ? { alphaMap: alpha, transparent: true } : {} }));
|
|
5164
|
+
const frame = new THREE.Mesh(plane, new THREE.MeshBasicMaterial({ color: 723982, side: THREE.DoubleSide, depthTest: false, depthWrite: false, ...alpha ? { alphaMap: alpha, transparent: true } : {} }));
|
|
5165
|
+
quad.renderOrder = 1;
|
|
5166
|
+
frame.visible = quad.visible = false;
|
|
5167
|
+
overlay.add(frame, quad);
|
|
5168
|
+
const entry = { view, target, quad, frame, alpha };
|
|
5169
|
+
entries.push(entry);
|
|
5170
|
+
return () => take(entry);
|
|
5171
|
+
},
|
|
5172
|
+
setMain(camera) {
|
|
5173
|
+
mainCamera = camera;
|
|
5174
|
+
},
|
|
5175
|
+
main(fallback) {
|
|
5176
|
+
if (!mainCamera) return fallback;
|
|
5177
|
+
const persp = mainCamera;
|
|
5178
|
+
const back = fallback;
|
|
5179
|
+
if (persp.isPerspectiveCamera && back.isPerspectiveCamera && persp.aspect !== back.aspect) {
|
|
5180
|
+
persp.aspect = back.aspect;
|
|
5181
|
+
persp.updateProjectionMatrix();
|
|
5182
|
+
}
|
|
5183
|
+
return mainCamera;
|
|
5184
|
+
},
|
|
5185
|
+
render(scene) {
|
|
5186
|
+
if (!entries.length) return;
|
|
5187
|
+
const { w, h } = size();
|
|
5188
|
+
const autoClear = gl.autoClear;
|
|
5189
|
+
const shadows = gl.shadowMap.autoUpdate;
|
|
5190
|
+
gl.shadowMap.autoUpdate = false;
|
|
5191
|
+
for (const entry of entries) {
|
|
5192
|
+
const cam = entry.view.camera;
|
|
5193
|
+
const aspect = w * entry.view.rect.width / Math.max(1, h * entry.view.rect.height);
|
|
5194
|
+
if (cam.isPerspectiveCamera && Math.abs(cam.aspect - aspect) > 1e-3) {
|
|
5195
|
+
cam.aspect = aspect;
|
|
5196
|
+
cam.updateProjectionMatrix();
|
|
5197
|
+
}
|
|
5198
|
+
gl.setRenderTarget(entry.target);
|
|
5199
|
+
gl.clear();
|
|
5200
|
+
gl.render(scene, cam);
|
|
5201
|
+
}
|
|
5202
|
+
gl.setRenderTarget(null);
|
|
5203
|
+
gl.autoClear = false;
|
|
5204
|
+
gl.setScissorTest(true);
|
|
5205
|
+
for (const entry of entries) {
|
|
5206
|
+
const r = entry.view.rect;
|
|
5207
|
+
const x = r.x * w;
|
|
5208
|
+
const y = (1 - r.y - r.height) * h;
|
|
5209
|
+
const vw = r.width * w;
|
|
5210
|
+
const vh = r.height * h;
|
|
5211
|
+
for (const mesh of [entry.frame, entry.quad]) mesh.visible = false;
|
|
5212
|
+
entry.frame.visible = true;
|
|
5213
|
+
gl.setViewport(x - FRAME_PX, y - FRAME_PX, vw + 2 * FRAME_PX, vh + 2 * FRAME_PX);
|
|
5214
|
+
gl.setScissor(x - FRAME_PX, y - FRAME_PX, vw + 2 * FRAME_PX, vh + 2 * FRAME_PX);
|
|
5215
|
+
gl.render(overlay, ortho);
|
|
5216
|
+
entry.frame.visible = false;
|
|
5217
|
+
entry.quad.visible = true;
|
|
5218
|
+
entry.quad.scale.x = entry.view.mirror ? -1 : 1;
|
|
5219
|
+
gl.setViewport(x, y, vw, vh);
|
|
5220
|
+
gl.setScissor(x, y, vw, vh);
|
|
5221
|
+
gl.render(overlay, ortho);
|
|
5222
|
+
entry.quad.visible = false;
|
|
5223
|
+
}
|
|
5224
|
+
gl.setScissorTest(false);
|
|
5225
|
+
gl.setViewport(0, 0, w, h);
|
|
5226
|
+
gl.autoClear = autoClear;
|
|
5227
|
+
gl.shadowMap.autoUpdate = shadows;
|
|
5228
|
+
},
|
|
5229
|
+
get count() {
|
|
5230
|
+
return entries.length;
|
|
5231
|
+
},
|
|
5232
|
+
dispose() {
|
|
5233
|
+
for (const entry of [...entries]) take(entry);
|
|
5234
|
+
plane.dispose();
|
|
5235
|
+
mainCamera = null;
|
|
5236
|
+
}
|
|
5237
|
+
};
|
|
5238
|
+
}
|
|
5239
|
+
|
|
4974
5240
|
// src/room.ts
|
|
4975
5241
|
var FIXED_DT = 1 / 60;
|
|
4976
5242
|
var EXPLOSIVE_EFFECT_IDS = ["m67", "molotov", "pipe_bomb", "m79_40mm", "m72_66mm", "at4_84mm", "dynamite", "c4", "claymore"];
|
|
@@ -4999,6 +5265,12 @@ var CAP_RISE = 0.03;
|
|
|
4999
5265
|
var MAX_STRIDE_SPEED = 10;
|
|
5000
5266
|
var v3 = (v) => [v.x, v.y, v.z];
|
|
5001
5267
|
var dist3 = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
5268
|
+
var vehicleLeafKey = (spec, part, moment) => {
|
|
5269
|
+
const tags = spec.parts.find((p) => p.id === part)?.tags ?? [];
|
|
5270
|
+
const leaf = tags.includes("hood") ? "hood" : tags.includes("lid") || tags.includes("trunk") ? "trunk" : "door";
|
|
5271
|
+
return `vehicle_${leaf}_${moment}`;
|
|
5272
|
+
};
|
|
5273
|
+
var MARK_EVENTS = /* @__PURE__ */ new Set(["dent", "hole", "crack", "chip", "stage", "fracture"]);
|
|
5002
5274
|
function createTestRoom(options) {
|
|
5003
5275
|
const THREE = options.THREE;
|
|
5004
5276
|
const locale = options.locale ?? "en";
|
|
@@ -5045,6 +5317,7 @@ function createTestRoom(options) {
|
|
|
5045
5317
|
};
|
|
5046
5318
|
const onResize = () => resize();
|
|
5047
5319
|
window.addEventListener("resize", onResize);
|
|
5320
|
+
const moduleViews = createModuleViews(THREE, gl);
|
|
5048
5321
|
const arenaSize = options.arenaSize ?? 14;
|
|
5049
5322
|
const arena = createArena(THREE, arenaSize, scene, options.scenery, options.tap);
|
|
5050
5323
|
scene.add(arena.group);
|
|
@@ -5104,10 +5377,23 @@ function createTestRoom(options) {
|
|
|
5104
5377
|
const ownAudio = options.audio === void 0;
|
|
5105
5378
|
const audio = options.audio === false ? null : options.audio ?? createAudioEngine({ deferredStart: {} });
|
|
5106
5379
|
const destructionSound = audio ? createDestructionSound(audio) : null;
|
|
5380
|
+
if (audio && options.samplesBaseUrl !== null) {
|
|
5381
|
+
void loadKitSamples(audio, { baseUrl: options.samplesBaseUrl ?? "./sfx/" }).then((bank) => {
|
|
5382
|
+
if (!bank || typeof window === "undefined") return;
|
|
5383
|
+
const preload = () => {
|
|
5384
|
+
window.removeEventListener("keydown", preload, true);
|
|
5385
|
+
window.removeEventListener("pointerdown", preload, true);
|
|
5386
|
+
void audio.loadSamples(["latch", "creak_wood", "shut_wood", "rattle", "grab_hard", "grab_soft"]).catch(() => void 0);
|
|
5387
|
+
};
|
|
5388
|
+
window.addEventListener("keydown", preload, true);
|
|
5389
|
+
window.addEventListener("pointerdown", preload, true);
|
|
5390
|
+
});
|
|
5391
|
+
}
|
|
5107
5392
|
const bodySounds = createBodySounds(audio);
|
|
5108
|
-
const playSound = (key2, point, volume) => {
|
|
5109
|
-
destructionSound?.onSound(key2, point, volume);
|
|
5110
|
-
options.onSound?.(key2, point, volume);
|
|
5393
|
+
const playSound = (key2, point, volume, detail) => {
|
|
5394
|
+
destructionSound?.onSound(key2, point, volume, detail);
|
|
5395
|
+
if (detail) options.onSound?.(key2, point, volume, detail);
|
|
5396
|
+
else options.onSound?.(key2, point, volume);
|
|
5111
5397
|
};
|
|
5112
5398
|
const lights = createLightBudget(THREE, scene, { ...ROOM_LIGHT_SIZES, ...options.lightSizes });
|
|
5113
5399
|
const warmup = createProgramWarmup(THREE);
|
|
@@ -5177,7 +5463,7 @@ function createTestRoom(options) {
|
|
|
5177
5463
|
const size = Math.min(GROUND_BURST_MAX, Math.ceil(result.radius * GROUND_BURST_PER_METRE));
|
|
5178
5464
|
renderer.emit(material.debris === "clods" ? "dust" : material.debris, result.point, [0, 1, 0], Math.max(2, size), Math.min(4, 1 + result.radius * 4));
|
|
5179
5465
|
if (result.radius > GROUND_DUST_RADIUS) renderer.emit("dust", result.point, [0, 1, 0], size, 1.4);
|
|
5180
|
-
playSound(impactKey(def), result.point, Math.min(1, 0.25 + result.radius));
|
|
5466
|
+
playSound(impactKey(def), result.point, Math.min(1, 0.25 + result.radius), impactSoundKeys(material.id, def));
|
|
5181
5467
|
};
|
|
5182
5468
|
terrain.onDeform((region) => renderer.wakeRegion(region.x, region.z, region.radius));
|
|
5183
5469
|
const groundImpl = createTerrainImpl(terrain, { id: GROUND_ID, name: t("ground.name"), onBlow: showGroundBlow });
|
|
@@ -5405,6 +5691,7 @@ function createTestRoom(options) {
|
|
|
5405
5691
|
floorAt: terrain.heightAt,
|
|
5406
5692
|
start: arena.playerStart,
|
|
5407
5693
|
crouchKeys: options.crouch?.keys,
|
|
5694
|
+
onSlam: (event) => slamPlayer(event),
|
|
5408
5695
|
onFeet: (event) => {
|
|
5409
5696
|
const at = player.position;
|
|
5410
5697
|
if (event.type === "jump") bodySounds.jump(at);
|
|
@@ -5484,7 +5771,11 @@ function createTestRoom(options) {
|
|
|
5484
5771
|
return piece;
|
|
5485
5772
|
},
|
|
5486
5773
|
dragSound: (objectId, point, volume) => playSound(dragSoundKey(world.materials.get(heaviestMaterial(objectId) ?? "")), point, volume),
|
|
5487
|
-
hitSound: (objectId, point) =>
|
|
5774
|
+
hitSound: (objectId, point) => {
|
|
5775
|
+
const material = heaviestMaterial(objectId) ?? "";
|
|
5776
|
+
const def = world.materials.get(material);
|
|
5777
|
+
playSound(impactKey(def), point, 0.5, impactSoundKeys(material, def, "blunt"));
|
|
5778
|
+
},
|
|
5488
5779
|
onEvent: (ev) => {
|
|
5489
5780
|
const vars = { name: ev.name, kg: num(ev.massKg, 0) };
|
|
5490
5781
|
action(`push_${ev.type}`, t(`act.push_${ev.type}`, vars), { objectId: ev.objectId, itemId: entryOf(ev.objectId)?.so.itemId, data: { massKg: Math.round(ev.massKg * 10) / 10 } });
|
|
@@ -5574,7 +5865,13 @@ function createTestRoom(options) {
|
|
|
5574
5865
|
isLocked: (objectId, part) => lockedLeaves.has(`${objectId}\0*`) || lockedLeaves.has(`${objectId}\0${part}`),
|
|
5575
5866
|
nameOf,
|
|
5576
5867
|
impactKey: (objectId, part) => impactKey(world.materials.get(world.part(objectId, part)?.material ?? heaviestMaterial(objectId) ?? "")),
|
|
5868
|
+
leafSound: (objectId, part, moment) => {
|
|
5869
|
+
const spec = specs.get(objectId);
|
|
5870
|
+
return spec?.tags?.includes("vehicle") ? vehicleLeafKey(spec, part, moment) : void 0;
|
|
5871
|
+
},
|
|
5577
5872
|
sound: playSound,
|
|
5873
|
+
// a vehicle's doors, hood and lid have their own sounds in the vehicle bank (game-vehicles-three), not those of the house
|
|
5874
|
+
leafFoley: (objectId, part, slides2) => specs.get(objectId)?.tags?.includes("vehicle") ? null : leafFoleyOf(entryOf(objectId)?.item?.sounds, part, impactKey(world.materials.get(world.part(objectId, part)?.material ?? heaviestMaterial(objectId) ?? "")), slides2),
|
|
5578
5875
|
onEvent: (ev) => {
|
|
5579
5876
|
const vars = { name: ev.name, percent: Math.round(ev.opening * 100) };
|
|
5580
5877
|
action(`door_${ev.type}`, t(`act.door_${ev.type}`, vars), { objectId: ev.objectId, itemId: entryOf(ev.objectId)?.so.itemId, part: ev.part, data: { opening: Math.round(ev.opening * 1e3) / 1e3, ...ev.by ? { by: ev.by } : {} } });
|
|
@@ -5903,6 +6200,7 @@ function createTestRoom(options) {
|
|
|
5903
6200
|
explosives.clear();
|
|
5904
6201
|
fireZones.length = 0;
|
|
5905
6202
|
burningStains.length = 0;
|
|
6203
|
+
destructionSound?.reset();
|
|
5906
6204
|
terrain.setLayout(defaultGround);
|
|
5907
6205
|
for (const id of [...entries.keys()]) if (!entries.get(id)?.arena) removeEntry(id);
|
|
5908
6206
|
clearLoose();
|
|
@@ -5983,6 +6281,37 @@ function createTestRoom(options) {
|
|
|
5983
6281
|
let avatar = null;
|
|
5984
6282
|
let cameraMode = options.cameraMode ?? "first";
|
|
5985
6283
|
let crouchForced = false;
|
|
6284
|
+
let downForced = false;
|
|
6285
|
+
const playerMass = options.playerMass ?? bodyMass(1.75);
|
|
6286
|
+
const SLAM_REACH = PLAYER_RADIUS + 0.35;
|
|
6287
|
+
const SLAM_SHOULDER = 1.3;
|
|
6288
|
+
const slamPlayer = (event) => {
|
|
6289
|
+
const [x, feet, z] = player.position;
|
|
6290
|
+
const energy = 0.5 * playerMass * event.speed * event.speed;
|
|
6291
|
+
let hit = null;
|
|
6292
|
+
if (event.against === "obstacle") {
|
|
6293
|
+
const from = [x, feet + ((player.knocked?.lying ?? 0) > 0.5 ? 0.3 : SLAM_SHOULDER), z];
|
|
6294
|
+
hit = rayWorld(from, event.direction, SLAM_REACH);
|
|
6295
|
+
if (hit) world.hitPart(hit.object, hit.part, { type: "blunt", point: hit.point, direction: event.direction, energy, radius: 0.2, ...hit.normal ? { normal: hit.normal } : {} });
|
|
6296
|
+
}
|
|
6297
|
+
bodySounds.slam(event.speed, [x, feet + 0.5, z]);
|
|
6298
|
+
action("player_slammed", t(event.against === "obstacle" ? "act.player_slammed" : "act.player_fell", { speed: num(event.speed, 1), what: hit ? nameOf(hit.object) : "" }), {
|
|
6299
|
+
...hit ? { objectId: hit.object, part: hit.part } : {},
|
|
6300
|
+
data: { against: event.against, speed: event.speed, energy, ...hit ? { object: hit.object, part: hit.part } : {} }
|
|
6301
|
+
});
|
|
6302
|
+
};
|
|
6303
|
+
const knockPlayer = (event) => {
|
|
6304
|
+
if (seating.seat) return;
|
|
6305
|
+
const feet = [player.x, player.feet, player.z];
|
|
6306
|
+
const reach = world.blastReach(event.point, [feet[0], feet[1] + KNOCK_CHEST, feet[2]], event.source ? { ignore: [event.source] } : {});
|
|
6307
|
+
const knock = knockOf({ point: event.point, energy: event.energy, radius: event.radius, reach }, feet, playerMass, options.blastShove);
|
|
6308
|
+
if (!knock) return;
|
|
6309
|
+
player.knock(knock);
|
|
6310
|
+
const speed = Math.hypot(knock.velocity[0], knock.velocity[1], knock.velocity[2]);
|
|
6311
|
+
action(knock.down ? "player_knocked_down" : "player_shoved", t(knock.down ? "act.knocked_down" : "act.player_shoved", { speed: num(speed, 1), reach: num(reach * 100, 0) }), {
|
|
6312
|
+
data: { speed, reach, down: knock.down, source: event.source }
|
|
6313
|
+
});
|
|
6314
|
+
};
|
|
5986
6315
|
const avatarState = { speed: 0, running: false, crouch: false, down: false, armed: false, pitch: 0 };
|
|
5987
6316
|
let feetPrev = null;
|
|
5988
6317
|
let armedForced = null;
|
|
@@ -6038,6 +6367,8 @@ function createTestRoom(options) {
|
|
|
6038
6367
|
feetPrev = [x, z];
|
|
6039
6368
|
avatarState.running = !player.crouching && (player.keys.has("ShiftLeft") || player.keys.has("ShiftRight"));
|
|
6040
6369
|
avatarState.crouch = crouchForced || player.crouching || push.isBent;
|
|
6370
|
+
avatarState.down = downForced || Boolean(player.knocked?.down || (player.knocked?.lying ?? 0) > 0.5);
|
|
6371
|
+
avatarState.fallForward = downForced ? void 0 : player.knockedForward;
|
|
6041
6372
|
avatarState.armed = armedForced ?? tool.def.action.kind === "firearm";
|
|
6042
6373
|
avatarState.pitch = player.pitch;
|
|
6043
6374
|
avatarState.holding = heldPiece !== null;
|
|
@@ -6110,14 +6441,18 @@ function createTestRoom(options) {
|
|
|
6110
6441
|
if (!avatar?.reach) return;
|
|
6111
6442
|
avatar.reach(targets, { camera, keepOutside: { contact: (origin, direction, maxDistance) => rayWorld(origin, direction, maxDistance)?.distance ?? null } });
|
|
6112
6443
|
};
|
|
6444
|
+
const thirdEuler = new THREE.Euler(0, 0, 0, "YXZ");
|
|
6113
6445
|
const applyThirdView = () => {
|
|
6114
6446
|
if (!thirdActive()) return () => void 0;
|
|
6115
6447
|
const home = camera.position.clone();
|
|
6448
|
+
const turn = camera.quaternion.clone();
|
|
6116
6449
|
const pose = thirdPose(0);
|
|
6117
6450
|
camera.position.set(pose.position[0], pose.position[1], pose.position[2]);
|
|
6451
|
+
if (player.knocked) camera.quaternion.setFromEuler(thirdEuler.set(player.pitch, player.yaw, 0, "YXZ"));
|
|
6118
6452
|
camera.updateMatrixWorld(true);
|
|
6119
6453
|
return () => {
|
|
6120
6454
|
camera.position.copy(home);
|
|
6455
|
+
camera.quaternion.copy(turn);
|
|
6121
6456
|
camera.updateMatrixWorld(true);
|
|
6122
6457
|
};
|
|
6123
6458
|
};
|
|
@@ -6233,6 +6568,15 @@ function createTestRoom(options) {
|
|
|
6233
6568
|
};
|
|
6234
6569
|
const fireZones = [];
|
|
6235
6570
|
const burningStains = [];
|
|
6571
|
+
const poolFire = (point, burning) => playSound(burning ? "burn_loop" : "burn_end", point, 0.5, { fuel: "liquid" });
|
|
6572
|
+
const dropZone = (i) => {
|
|
6573
|
+
const [zone] = fireZones.splice(i, 1);
|
|
6574
|
+
if (zone) poolFire(zone.point, false);
|
|
6575
|
+
};
|
|
6576
|
+
const dropStain = (i) => {
|
|
6577
|
+
const [stain] = burningStains.splice(i, 1);
|
|
6578
|
+
if (stain) poolFire(stain.center, false);
|
|
6579
|
+
};
|
|
6236
6580
|
const stainRadius = (s) => {
|
|
6237
6581
|
let sum = 0;
|
|
6238
6582
|
for (const r of s.front) sum += r;
|
|
@@ -6249,6 +6593,7 @@ function createTestRoom(options) {
|
|
|
6249
6593
|
if (stainBurning(s)) continue;
|
|
6250
6594
|
const seconds = Math.max(3, Math.min(20, 4 + s.liters * 30 + stainRadius(s) * 6));
|
|
6251
6595
|
burningStains.push({ center: [s.center[0], s.center[1], s.center[2]], remaining: seconds, acc: 0 });
|
|
6596
|
+
poolFire([s.center[0], s.center[1], s.center[2]], true);
|
|
6252
6597
|
lit = true;
|
|
6253
6598
|
log.add({
|
|
6254
6599
|
channel: "event",
|
|
@@ -6279,7 +6624,7 @@ function createTestRoom(options) {
|
|
|
6279
6624
|
const a = rnd() * Math.PI * 2;
|
|
6280
6625
|
const r = Math.sqrt(rnd()) * z.radius;
|
|
6281
6626
|
renderer.emit("fire", [z.point[0] + Math.cos(a) * r, z.point[1] + 0.05, z.point[2] + Math.sin(a) * r], [0, 1, 0], 2, 1.5);
|
|
6282
|
-
if (z.remaining <= 0)
|
|
6627
|
+
if (z.remaining <= 0) dropZone(i);
|
|
6283
6628
|
}
|
|
6284
6629
|
if (burningStains.length) {
|
|
6285
6630
|
const stains = fluids.stains();
|
|
@@ -6300,7 +6645,7 @@ function createTestRoom(options) {
|
|
|
6300
6645
|
if (b.remaining <= 0 || !st) {
|
|
6301
6646
|
fluids.clearStains(b.center, r + 0.1);
|
|
6302
6647
|
log.add({ channel: "event", type: "stain_burned", text: t("ev.stain_burned"), data: { point: b.center } });
|
|
6303
|
-
|
|
6648
|
+
dropStain(i);
|
|
6304
6649
|
}
|
|
6305
6650
|
}
|
|
6306
6651
|
}
|
|
@@ -6499,7 +6844,13 @@ function createTestRoom(options) {
|
|
|
6499
6844
|
action("melee", t("act.melee.miss", { weapon: w }), { data: { weapon: id } });
|
|
6500
6845
|
return;
|
|
6501
6846
|
}
|
|
6502
|
-
|
|
6847
|
+
const hit = useToHit(u, r.point, r.normal);
|
|
6848
|
+
const result = world.hitPart(r.object, r.part, { ...hit, source: "player" });
|
|
6849
|
+
if (!result.events.some((e) => MARK_EVENTS.has(e.type))) {
|
|
6850
|
+
const material = world.part(r.object, r.part)?.material ?? heaviestMaterial(r.object) ?? "";
|
|
6851
|
+
const def = world.materials.get(material);
|
|
6852
|
+
playSound(impactKey(def), r.point, Math.min(0.8, 0.25 + u.energy / 400), impactSoundKeys(material, def, hit.type));
|
|
6853
|
+
}
|
|
6503
6854
|
const blow = blowOfTool(id, toolDef(id)?.category ?? "");
|
|
6504
6855
|
if (blow) bodySounds.hit(blow, blowTarget(r), r.point, u.energy);
|
|
6505
6856
|
pushPiece(r.object, u.direction, 2 * u.energy / 6, r.point);
|
|
@@ -6631,8 +6982,8 @@ function createTestRoom(options) {
|
|
|
6631
6982
|
hit.add(r.object);
|
|
6632
6983
|
world.extinguish(r.object, void 0, u.coolC * Math.max(u.duration, 0.05) * 6);
|
|
6633
6984
|
fluids.clearStains(r.point, 0.5);
|
|
6634
|
-
for (let i = burningStains.length - 1; i >= 0; i--) if (Math.hypot(burningStains[i].center[0] - r.point[0], burningStains[i].center[2] - r.point[2]) < 1.2)
|
|
6635
|
-
for (let i = fireZones.length - 1; i >= 0; i--) if (Math.hypot(fireZones[i].point[0] - r.point[0], fireZones[i].point[2] - r.point[2]) < 1.5)
|
|
6985
|
+
for (let i = burningStains.length - 1; i >= 0; i--) if (Math.hypot(burningStains[i].center[0] - r.point[0], burningStains[i].center[2] - r.point[2]) < 1.2) dropStain(i);
|
|
6986
|
+
for (let i = fireZones.length - 1; i >= 0; i--) if (Math.hypot(fireZones[i].point[0] - r.point[0], fireZones[i].point[2] - r.point[2]) < 1.5) dropZone(i);
|
|
6636
6987
|
}
|
|
6637
6988
|
});
|
|
6638
6989
|
log.coalesce(
|
|
@@ -6643,7 +6994,9 @@ function createTestRoom(options) {
|
|
|
6643
6994
|
};
|
|
6644
6995
|
const startFire = (weapon, at, fire2, impact) => {
|
|
6645
6996
|
fluids.splash("fuel", [at[0], Math.max(at[1], groundAt(at[0], at[2])), at[2]], 0.5);
|
|
6646
|
-
|
|
6997
|
+
const zone = { point: [at[0], Math.max(0.02, groundAt(at[0], at[2])), at[2]], radius: fire2.radius, temperature: fire2.temperature, remaining: fire2.seconds, acc: 0 };
|
|
6998
|
+
fireZones.push(zone);
|
|
6999
|
+
poolFire(zone.point, true);
|
|
6647
7000
|
renderer.emit("fire", at, [0, 1, 0], 24, 3);
|
|
6648
7001
|
action("molotov_fire", t("act.molotov_fire", { weapon: toolName(weapon), seconds: num(fire2.seconds, 0), radius: num(fire2.radius, 1) }), { data: { weapon, impact } });
|
|
6649
7002
|
};
|
|
@@ -6857,7 +7210,9 @@ function createTestRoom(options) {
|
|
|
6857
7210
|
heldYaw = 0;
|
|
6858
7211
|
heldTilt = 0;
|
|
6859
7212
|
heldVel[0] = heldVel[1] = heldVel[2] = 0;
|
|
7213
|
+
playSound(pickupSoundOf(heldEntry?.item?.sounds, world.materials.get(heaviestMaterial(r.object) ?? "")), r.point, Math.min(1, 0.45 + kg * 0.1));
|
|
6860
7214
|
action("grab", t("act.grab", { name: heldName, kg: num(kg, 1) }), { objectId: r.object, itemId: entryOf(r.object)?.so.itemId, data: { massKg: Math.round(kg * 10) / 10 } });
|
|
7215
|
+
playSound(impactKey(world.materials.get(heaviestMaterial(r.object) ?? "")).replace("impact_", "grab_"), v3(piece.root.getWorldPosition(tmpV)), 0.8);
|
|
6861
7216
|
fire("grab", { held: true, objectId: r.object });
|
|
6862
7217
|
return true;
|
|
6863
7218
|
};
|
|
@@ -6870,6 +7225,7 @@ function createTestRoom(options) {
|
|
|
6870
7225
|
releaseHeld();
|
|
6871
7226
|
piece.velocity = [0, 0, 0];
|
|
6872
7227
|
renderer.impulse(id, [aimD[0] * speed * piece.mass, (aimD[1] * speed + 1.2) * piece.mass, aimD[2] * speed * piece.mass]);
|
|
7228
|
+
playSound("item_throw", [piece.root.position.x, piece.root.position.y, piece.root.position.z], Math.min(1, 0.3 + speed * 0.04));
|
|
6873
7229
|
action("throw", t("act.throw", { name, speed: num(speed, 1) }), { objectId: id, itemId: entryOf(id)?.so.itemId, data: { speedMs: speed, massKg: piece.mass } });
|
|
6874
7230
|
fire("throw", { objectId: id, speed });
|
|
6875
7231
|
return true;
|
|
@@ -6981,6 +7337,7 @@ function createTestRoom(options) {
|
|
|
6981
7337
|
e.opened = true;
|
|
6982
7338
|
if (e.containerId) fluids.open(e.containerId);
|
|
6983
7339
|
seal.open?.(wrapped.get(e.so.root) ?? e.so.root);
|
|
7340
|
+
playSound(openSoundOf(e.item?.sounds, world.materials.get(heaviestMaterial(e.so.objectId) ?? "")), v3(e.so.root.getWorldPosition(tmpV)), 0.7);
|
|
6984
7341
|
const tool2 = opener.tool ? catalogMap.get(opener.tool)?.name ?? (arsenal.weapons.has(opener.tool) ? toolName(opener.tool) : opener.tool) : void 0;
|
|
6985
7342
|
action("open_seal", tool2 ? t("act.open_with", { name: e.name, tool: tool2 }) : t("act.open_by_hand", { name: e.name }), { objectId: e.so.objectId, itemId: e.so.itemId, data: { tool: opener.tool ?? "hand" } });
|
|
6986
7343
|
return true;
|
|
@@ -7029,17 +7386,28 @@ function createTestRoom(options) {
|
|
|
7029
7386
|
if (consuming && c.closed) fluids.open(e.containerId);
|
|
7030
7387
|
useItemPlayer = createItemUse(fluids, e.containerId, def);
|
|
7031
7388
|
useItemName = def.id;
|
|
7389
|
+
useSoundIn = 0;
|
|
7032
7390
|
if (eating) useLiters = c.liters;
|
|
7033
7391
|
action("use_item", t("act.use_item", { name: e.name, use: S.text[`use.${def.id}`] ?? def.id }), { objectId: e.so.objectId, itemId: e.so.itemId, data: { use: def.id } });
|
|
7034
7392
|
return true;
|
|
7035
7393
|
};
|
|
7036
7394
|
let useLiters = null;
|
|
7395
|
+
let useSoundIn = 0;
|
|
7037
7396
|
const updateItemUse = (dt) => {
|
|
7038
7397
|
if (!useItemPlayer) return;
|
|
7039
7398
|
const e = heldEntry;
|
|
7040
7399
|
const c = e?.containerId ? fluids.container(e.containerId) : void 0;
|
|
7041
7400
|
const before = c?.liters;
|
|
7042
7401
|
useItemPlayer.update(dt);
|
|
7402
|
+
const useSound = e && useLiters !== null ? useSoundOf(e.item?.sounds, useItemName) : void 0;
|
|
7403
|
+
if (useSound && e) {
|
|
7404
|
+
useSoundIn -= dt;
|
|
7405
|
+
const now = e.containerId ? fluids.container(e.containerId)?.liters : void 0;
|
|
7406
|
+
if (useSoundIn <= 0 && before !== void 0 && now !== void 0 && now < before) {
|
|
7407
|
+
playSound(useSound, v3(e.so.root.getWorldPosition(tmpV)), 0.6);
|
|
7408
|
+
useSoundIn = USE_SOUND_EVERY;
|
|
7409
|
+
}
|
|
7410
|
+
}
|
|
7043
7411
|
const seal = e?.item?.seal;
|
|
7044
7412
|
twisting = null;
|
|
7045
7413
|
if (e && seal?.openUse === useItemName && !e.opened) {
|
|
@@ -7249,7 +7617,9 @@ function createTestRoom(options) {
|
|
|
7249
7617
|
if (!loadout.includes(weaponId)) loadout.push(weaponId);
|
|
7250
7618
|
setTool(weaponId);
|
|
7251
7619
|
};
|
|
7620
|
+
let hudAimHit;
|
|
7252
7621
|
const inspectTarget = () => {
|
|
7622
|
+
if (hudAimHit !== void 0) return hudAimHit;
|
|
7253
7623
|
aim();
|
|
7254
7624
|
return rayWorld(aimO, aimD, 14);
|
|
7255
7625
|
};
|
|
@@ -7434,6 +7804,7 @@ function createTestRoom(options) {
|
|
|
7434
7804
|
}
|
|
7435
7805
|
syncLiters();
|
|
7436
7806
|
renderer.update(dt);
|
|
7807
|
+
destructionSound?.update(dt);
|
|
7437
7808
|
for (const e of entries.values()) e.follower?.update?.();
|
|
7438
7809
|
terrainView.sync(camera.position);
|
|
7439
7810
|
showGroundFire(dt);
|
|
@@ -7490,12 +7861,12 @@ function createTestRoom(options) {
|
|
|
7490
7861
|
const updatePresent = (dt) => {
|
|
7491
7862
|
reachAvatar();
|
|
7492
7863
|
if (progress && simTime - progress.at > 0.25) progress = null;
|
|
7864
|
+
ui?.flushLog();
|
|
7493
7865
|
hudAcc += dt;
|
|
7494
7866
|
if (hudAcc >= 0.1 && ui) {
|
|
7495
7867
|
hudAcc = 0;
|
|
7496
7868
|
ui.hud(hudState());
|
|
7497
7869
|
}
|
|
7498
|
-
ui?.flushLog();
|
|
7499
7870
|
metricsAcc += dt;
|
|
7500
7871
|
if (metricsAcc >= METRICS_INTERVAL_S && ui) {
|
|
7501
7872
|
metricsAcc = 0;
|
|
@@ -7503,6 +7874,14 @@ function createTestRoom(options) {
|
|
|
7503
7874
|
}
|
|
7504
7875
|
};
|
|
7505
7876
|
const hudState = () => {
|
|
7877
|
+
hudAimHit = inspectTarget();
|
|
7878
|
+
try {
|
|
7879
|
+
return readHudState();
|
|
7880
|
+
} finally {
|
|
7881
|
+
hudAimHit = void 0;
|
|
7882
|
+
}
|
|
7883
|
+
};
|
|
7884
|
+
const readHudState = () => {
|
|
7506
7885
|
const page = Math.floor(toolIndex / 10);
|
|
7507
7886
|
const slots = loadout.slice(page * 10, page * 10 + 10).map((id) => arsenal.name(id));
|
|
7508
7887
|
const ammo = tool.def.action.kind === "firearm" ? `${tool.ammo}/${tool.capacity}${tool.reserve ? ` \xB7 ${tool.reserve}` : ""}` : tool.def.action.kind === "hand" ? "" : tool.def.action.kind === "melee" || tool.def.action.kind === "tool" ? "" : `${num(tool.ammo, 0)}`;
|
|
@@ -7693,6 +8072,7 @@ function createTestRoom(options) {
|
|
|
7693
8072
|
const environment = options.environment ?? createEnvironmentBus();
|
|
7694
8073
|
const audioSink = audio ? createAudioSink(audio) : void 0;
|
|
7695
8074
|
function emitHost(event) {
|
|
8075
|
+
if (event.type === "explosion") knockPlayer(event);
|
|
7696
8076
|
for (const listener of hostListeners) listener(event);
|
|
7697
8077
|
}
|
|
7698
8078
|
function flushEvents(id, state, recoil) {
|
|
@@ -7752,7 +8132,11 @@ function createTestRoom(options) {
|
|
|
7752
8132
|
lights,
|
|
7753
8133
|
pools,
|
|
7754
8134
|
loading,
|
|
8135
|
+
addView: (view) => moduleViews.add(view),
|
|
8136
|
+
setViewCamera: (view) => moduleViews.setMain(view ?? null),
|
|
8137
|
+
holdPlayer: (held) => player.setWalkHeld(held),
|
|
7755
8138
|
...options.blastShove ? { blastShove: options.blastShove } : {},
|
|
8139
|
+
blastReach: (point, target, ignore) => world.blastReach(point, target, ignore ? { ignore } : {}),
|
|
7756
8140
|
tool: () => ({
|
|
7757
8141
|
weapon: toolId,
|
|
7758
8142
|
category: tool.def.category,
|
|
@@ -7927,6 +8311,7 @@ function createTestRoom(options) {
|
|
|
7927
8311
|
impl: ti.impl,
|
|
7928
8312
|
animations: ti.animations,
|
|
7929
8313
|
seats: ti.seats,
|
|
8314
|
+
sounds: ti.sounds,
|
|
7930
8315
|
thumbnail: ti.thumbnail,
|
|
7931
8316
|
thumbnailCredit: ti.thumbnailCredit
|
|
7932
8317
|
};
|
|
@@ -8313,7 +8698,10 @@ function createTestRoom(options) {
|
|
|
8313
8698
|
crouchForced = state.crouch;
|
|
8314
8699
|
avatarState.crouch = state.crouch || player.crouching;
|
|
8315
8700
|
}
|
|
8316
|
-
if (state.down !== void 0)
|
|
8701
|
+
if (state.down !== void 0) {
|
|
8702
|
+
downForced = state.down;
|
|
8703
|
+
avatarState.down = state.down || Boolean(player.knocked?.down);
|
|
8704
|
+
}
|
|
8317
8705
|
if (state.armed !== void 0) armedForced = state.armed;
|
|
8318
8706
|
},
|
|
8319
8707
|
avatarState: () => ({ ...avatarState }),
|
|
@@ -8334,6 +8722,9 @@ function createTestRoom(options) {
|
|
|
8334
8722
|
get playerPosition() {
|
|
8335
8723
|
return player.position;
|
|
8336
8724
|
},
|
|
8725
|
+
get playerKnock() {
|
|
8726
|
+
return player.knocked;
|
|
8727
|
+
},
|
|
8337
8728
|
sit: (objectId, seatId) => seating.sit(objectId, seatId),
|
|
8338
8729
|
stand: () => seating.stand(),
|
|
8339
8730
|
get seat() {
|
|
@@ -8357,6 +8748,7 @@ function createTestRoom(options) {
|
|
|
8357
8748
|
if (ownAudio) audio?.dispose();
|
|
8358
8749
|
for (const detach of detachers.values()) detach();
|
|
8359
8750
|
detachers.clear();
|
|
8751
|
+
moduleViews.dispose();
|
|
8360
8752
|
for (const store of benches.values()) store.dispose();
|
|
8361
8753
|
benches.clear();
|
|
8362
8754
|
stopLoop?.();
|
|
@@ -8519,7 +8911,8 @@ function createTestRoom(options) {
|
|
|
8519
8911
|
for (const batcher of batchers.values()) batcher.update();
|
|
8520
8912
|
const restoreCamera = room.applyCamera();
|
|
8521
8913
|
graphics?.beforeRender(camera);
|
|
8522
|
-
gl.render(scene, debugView ?? camera);
|
|
8914
|
+
gl.render(scene, debugView ?? moduleViews.main(camera));
|
|
8915
|
+
moduleViews.render(scene);
|
|
8523
8916
|
graphics?.afterRender();
|
|
8524
8917
|
restoreCamera();
|
|
8525
8918
|
pacer.submitted();
|