@adatechnology/game-test-room 0.16.0 → 0.17.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 +5 -2
- package/dist/index.d.ts +88 -5
- package/dist/index.global.js +3362 -276
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +843 -94
- package/dist/index.js.map +1 -1
- package/package.json +13 -10
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// src/room.ts
|
|
2
2
|
import { createDestruction, JOINT_DEFAULTS, resolveStrings as resolveDestructionStrings } from "@adatechnology/game-destruction";
|
|
3
3
|
import { createAudioEngine, createAudioSink } from "@adatechnology/game-audio";
|
|
4
|
-
import { createDestructionRenderer, createDestructionSound, specFromObject3D } from "@adatechnology/game-destruction-three";
|
|
4
|
+
import { createDestructionRenderer, createDestructionSound, dragSoundKey, impactKey, specFromObject3D } from "@adatechnology/game-destruction-three";
|
|
5
5
|
import { createArsenal, DEFAULT_LOADOUT } from "@adatechnology/game-weapons";
|
|
6
6
|
import { createFilmLcg, createFluids, createItemUse, ITEM_USES, resolveStrings as resolveFluidStrings } from "@adatechnology/game-fluids";
|
|
7
7
|
import { createFilmBridge, createFilmManager, createFluidRenderer } from "@adatechnology/game-fluids-three";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { createVfx } from "@adatechnology/game-vfx";
|
|
9
|
+
import { createVfxRenderer } from "@adatechnology/game-vfx-three";
|
|
10
10
|
import { checkAnimation, checkBench, checkDestructible, checkSpec, createEnvironmentBus, shotExtras, useToHit } from "@adatechnology/game-contracts";
|
|
11
11
|
|
|
12
12
|
// src/arena.ts
|
|
@@ -130,14 +130,6 @@ function createArena(THREE, size, scene, scenery, tapOption) {
|
|
|
130
130
|
sc.far = 40;
|
|
131
131
|
sun.shadow.bias = -6e-4;
|
|
132
132
|
group.add(sun);
|
|
133
|
-
const floor = new THREE.Mesh(new THREE.PlaneGeometry(size * 2, size * 2), new THREE.MeshStandardMaterial({ color: 9342602, roughness: 0.95 }));
|
|
134
|
-
floor.rotation.x = -Math.PI / 2;
|
|
135
|
-
floor.receiveShadow = true;
|
|
136
|
-
floor.name = "floor";
|
|
137
|
-
group.add(floor);
|
|
138
|
-
const grid = new THREE.GridHelper(size * 2, size * 2, 5000274, 7829371);
|
|
139
|
-
grid.position.y = 4e-3;
|
|
140
|
-
group.add(grid);
|
|
141
133
|
const sceneryObjects = resolveScenery(scenery);
|
|
142
134
|
const wallObject = sceneryObjects.find((o) => o.id === WALL_ID);
|
|
143
135
|
const wallZ = wallObject ? wallObject.position[2] : DEFAULT_WALL_Z;
|
|
@@ -176,16 +168,23 @@ function createArena(THREE, size, scene, scenery, tapOption) {
|
|
|
176
168
|
const surfaces = [{ id: "floor", point: [0, 0, 0], normal: [0, 1, 0], halfSize: [size, size], porosity: 0.05 }];
|
|
177
169
|
for (const o of sceneryObjects) {
|
|
178
170
|
if (!o.top) continue;
|
|
179
|
-
const
|
|
180
|
-
|
|
171
|
+
const boards = (o.top.parts ?? []).flatMap((part) => {
|
|
172
|
+
const f = specBounds(THREE, o.spec, o.position, o.rotationY, { onlyPart: part });
|
|
173
|
+
return f ? [{ part, minX: f.minX, maxX: f.maxX, minZ: f.minZ, maxZ: f.maxZ }] : [];
|
|
174
|
+
});
|
|
175
|
+
const whole = specBounds(THREE, o.spec, o.position, o.rotationY, { onlyPart: o.top.part });
|
|
176
|
+
if (!whole) continue;
|
|
177
|
+
const b = boards.length === 0 ? whole : { minX: Math.min(...boards.map((f) => f.minX)), maxX: Math.max(...boards.map((f) => f.maxX)), minZ: Math.min(...boards.map((f) => f.minZ)), maxZ: Math.max(...boards.map((f) => f.maxZ)) };
|
|
181
178
|
const top = o.position[1] + o.top.y;
|
|
182
|
-
tops.push({ id: o.id, topPart: o.top.part, minX: b.minX, maxX: b.maxX, minZ: b.minZ, maxZ: b.maxZ, top });
|
|
179
|
+
tops.push({ id: o.id, topPart: o.top.part, minX: b.minX, maxX: b.maxX, minZ: b.minZ, maxZ: b.maxZ, top, ...boards.length > 0 ? { boards } : {} });
|
|
183
180
|
surfaces.push({ id: o.id, point: [(b.minX + b.maxX) / 2, top, (b.minZ + b.maxZ) / 2], normal: [0, 1, 0], halfSize: [(b.maxX - b.minX) / 2, (b.maxZ - b.minZ) / 2], porosity: 0.3 });
|
|
184
181
|
}
|
|
185
182
|
const colliders = [];
|
|
183
|
+
const colliderIds = [];
|
|
186
184
|
for (const o of sceneryObjects) {
|
|
187
185
|
const b = specBounds(THREE, o.spec, o.position, o.rotationY, { minHeight: 0.1 });
|
|
188
186
|
if (!b) continue;
|
|
187
|
+
colliderIds.push(o.id);
|
|
189
188
|
colliders.push(o.id === WALL_ID ? [-size, b.minZ - 1, size, b.maxZ + 0.1] : [b.minX, b.minZ, b.maxX, b.maxZ]);
|
|
190
189
|
}
|
|
191
190
|
let tapSpout = null;
|
|
@@ -226,6 +225,7 @@ function createArena(THREE, size, scene, scenery, tapOption) {
|
|
|
226
225
|
}
|
|
227
226
|
tapSpout = [cx + 0.3, y + 0.5, cz];
|
|
228
227
|
colliders.push([postX - 0.05, cz - 0.1, postX + 0.05, cz + 0.1]);
|
|
228
|
+
colliderIds.push(null);
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
return {
|
|
@@ -233,6 +233,7 @@ function createArena(THREE, size, scene, scenery, tapOption) {
|
|
|
233
233
|
objects: sceneryObjects,
|
|
234
234
|
surfaces,
|
|
235
235
|
colliders,
|
|
236
|
+
colliderIds,
|
|
236
237
|
playerStart,
|
|
237
238
|
wallZ,
|
|
238
239
|
tops,
|
|
@@ -1881,13 +1882,19 @@ function createNarrator(c) {
|
|
|
1881
1882
|
// src/player.ts
|
|
1882
1883
|
var EYE = 1.7;
|
|
1883
1884
|
var PLAYER_RADIUS = 0.3;
|
|
1885
|
+
var FOOT = 0.5;
|
|
1884
1886
|
var WALK = 3.4;
|
|
1885
1887
|
var CROUCH_DROP = 0.55;
|
|
1886
1888
|
var CROUCH_WALK = 1.7;
|
|
1887
1889
|
var CROUCH_RATE = 9;
|
|
1890
|
+
var BEND_RATE = 7;
|
|
1891
|
+
var CLEAR_SHARES = [1, 0.75, 0.5, 0.25];
|
|
1892
|
+
var CROUCH_TAP = 0.25;
|
|
1888
1893
|
var RUN = 6.2;
|
|
1889
1894
|
var JUMP = 4.6;
|
|
1890
1895
|
var GRAVITY = 13;
|
|
1896
|
+
var FLOOR_CLIMB_SPEED = 3;
|
|
1897
|
+
var FLOOR_FALL_SPEED = 6;
|
|
1891
1898
|
var LOOK = 22e-4;
|
|
1892
1899
|
function createPlayer(THREE, o) {
|
|
1893
1900
|
const cam = o.camera;
|
|
@@ -1896,6 +1903,8 @@ function createPlayer(THREE, o) {
|
|
|
1896
1903
|
let y = 0;
|
|
1897
1904
|
let eye = EYE;
|
|
1898
1905
|
let crouch = 0;
|
|
1906
|
+
let bend = 0;
|
|
1907
|
+
let bendTarget = 0;
|
|
1899
1908
|
let vy = 0;
|
|
1900
1909
|
let walked = 0;
|
|
1901
1910
|
let running = false;
|
|
@@ -1904,33 +1913,79 @@ function createPlayer(THREE, o) {
|
|
|
1904
1913
|
const keys = /* @__PURE__ */ new Set();
|
|
1905
1914
|
const crouchKeys = o.crouchKeys ?? ["KeyC"];
|
|
1906
1915
|
const isCrouchHeld = () => crouchKeys.some((code) => keys.has(code));
|
|
1916
|
+
let crouchLatched = false;
|
|
1917
|
+
let crouchPress = null;
|
|
1918
|
+
const isCrouched = () => crouchLatched || isCrouchHeld();
|
|
1919
|
+
const pressCrouch = (code) => {
|
|
1920
|
+
if (crouchKeys.includes(code) && !isCrouchHeld()) crouchPress = { held: 0, latched: crouchLatched };
|
|
1921
|
+
};
|
|
1922
|
+
const releaseCrouch = (code) => {
|
|
1923
|
+
if (!crouchKeys.includes(code) || !crouchPress || isCrouchHeld()) return;
|
|
1924
|
+
crouchLatched = crouchPress.held < CROUCH_TAP ? !crouchPress.latched : false;
|
|
1925
|
+
crouchPress = null;
|
|
1926
|
+
};
|
|
1907
1927
|
const euler = new THREE.Euler(0, 0, 0, "YXZ");
|
|
1928
|
+
const floorHere = () => o.floorAt?.(px.x, px.z) ?? 0;
|
|
1929
|
+
let floorLevel = 0;
|
|
1930
|
+
const followFloor = (dt) => {
|
|
1931
|
+
const gap = floorHere() - floorLevel;
|
|
1932
|
+
const reach = (gap > 0 ? FLOOR_CLIMB_SPEED : FLOOR_FALL_SPEED) * dt;
|
|
1933
|
+
floorLevel += Math.max(-reach, Math.min(reach, gap));
|
|
1934
|
+
};
|
|
1908
1935
|
const apply = () => {
|
|
1909
1936
|
euler.set(pitch, yaw, 0, "YXZ");
|
|
1910
1937
|
cam.quaternion.setFromEuler(euler);
|
|
1911
|
-
cam.position.set(px.x, eye + y - CROUCH_DROP * crouch, px.z);
|
|
1938
|
+
cam.position.set(px.x, eye + floorLevel + y - CROUCH_DROP * crouch - bend, px.z);
|
|
1912
1939
|
cam.updateMatrixWorld(true);
|
|
1913
1940
|
};
|
|
1914
1941
|
apply();
|
|
1915
|
-
const
|
|
1916
|
-
|
|
1917
|
-
|
|
1942
|
+
const outside = (x, z) => Math.abs(x) > o.limit - PLAYER_RADIUS || Math.abs(z) > o.limit - PLAYER_RADIUS;
|
|
1943
|
+
const blocked = (x, z, radius = PLAYER_RADIUS) => {
|
|
1944
|
+
if (outside(x, z)) return true;
|
|
1945
|
+
for (const c of o.colliders) if (x > c[0] - radius && x < c[2] + radius && z > c[1] - radius && z < c[3] + radius) return true;
|
|
1918
1946
|
if (o.blockers) {
|
|
1919
|
-
for (const
|
|
1947
|
+
for (const isBlocked of o.blockers) if (isBlocked(x, z, radius)) return true;
|
|
1920
1948
|
}
|
|
1921
|
-
return false;
|
|
1949
|
+
return o.footing ? o.footing(x, z, radius, y).blocked : false;
|
|
1922
1950
|
};
|
|
1951
|
+
const clearRadius = () => {
|
|
1952
|
+
for (const share of CLEAR_SHARES) if (!blocked(px.x, px.z, PLAYER_RADIUS * share)) return PLAYER_RADIUS * share;
|
|
1953
|
+
return null;
|
|
1954
|
+
};
|
|
1955
|
+
const wish = { x: 0, z: 0 };
|
|
1923
1956
|
const move = (dx, dz) => {
|
|
1924
|
-
|
|
1925
|
-
|
|
1957
|
+
wish.x += dx;
|
|
1958
|
+
wish.z += dz;
|
|
1959
|
+
const radius = clearRadius();
|
|
1960
|
+
const free = (x, z) => radius === null ? !outside(x, z) : !blocked(x, z, radius);
|
|
1961
|
+
const steps = Math.max(1, Math.ceil(Math.hypot(dx, dz) / (PLAYER_RADIUS / 3)));
|
|
1962
|
+
const sx = dx / steps;
|
|
1963
|
+
const sz = dz / steps;
|
|
1964
|
+
for (let i = 0; i < steps; i++) {
|
|
1965
|
+
if (free(px.x + sx, px.z)) px.x += sx;
|
|
1966
|
+
if (free(px.x, px.z + sz)) px.z += sz;
|
|
1967
|
+
}
|
|
1968
|
+
};
|
|
1969
|
+
const walkKeys = () => {
|
|
1970
|
+
const f = (keys.has("KeyW") || keys.has("ArrowUp") ? 1 : 0) - (keys.has("KeyS") || keys.has("ArrowDown") ? 1 : 0);
|
|
1971
|
+
const r = (keys.has("KeyD") || keys.has("ArrowRight") ? 1 : 0) - (keys.has("KeyA") || keys.has("ArrowLeft") ? 1 : 0);
|
|
1972
|
+
if (!f && !r) return [0, 0];
|
|
1973
|
+
const len = Math.hypot(f, r);
|
|
1974
|
+
return [(-Math.sin(yaw) * f + Math.cos(yaw) * r) / len, (-Math.cos(yaw) * f - Math.sin(yaw) * r) / len];
|
|
1926
1975
|
};
|
|
1927
1976
|
let dragging = false;
|
|
1928
1977
|
let moved = 0;
|
|
1929
1978
|
let downAt = 0;
|
|
1930
1979
|
let useStarted = false;
|
|
1931
1980
|
let holdTimer = null;
|
|
1981
|
+
let walkHeld = false;
|
|
1982
|
+
let lookLimit = null;
|
|
1932
1983
|
const look = (dx, dy) => {
|
|
1933
1984
|
yaw -= dx * LOOK;
|
|
1985
|
+
if (lookLimit) {
|
|
1986
|
+
const off = Math.atan2(Math.sin(yaw - lookLimit.yaw), Math.cos(yaw - lookLimit.yaw));
|
|
1987
|
+
yaw = lookLimit.yaw + Math.max(-lookLimit.range, Math.min(lookLimit.range, off));
|
|
1988
|
+
}
|
|
1934
1989
|
pitch = Math.max(-1.5, Math.min(1.5, pitch - dy * LOOK));
|
|
1935
1990
|
};
|
|
1936
1991
|
const onLockChange = () => {
|
|
@@ -2022,16 +2077,23 @@ function createPlayer(THREE, o) {
|
|
|
2022
2077
|
if (isControlHeld || e.altKey && locked) {
|
|
2023
2078
|
if (!/^F\d+$/.test(e.code)) e.preventDefault();
|
|
2024
2079
|
}
|
|
2025
|
-
if (!e.repeat)
|
|
2080
|
+
if (!e.repeat) {
|
|
2081
|
+
pressCrouch(e.code);
|
|
2082
|
+
keys.add(e.code);
|
|
2083
|
+
}
|
|
2026
2084
|
o.hooks.key(e.code, true, e);
|
|
2027
2085
|
};
|
|
2028
2086
|
const onKeyUp = (e) => {
|
|
2029
2087
|
keys.delete(e.code);
|
|
2088
|
+
releaseCrouch(e.code);
|
|
2030
2089
|
if (typing(e)) return;
|
|
2031
2090
|
o.hooks.key(e.code, false, e);
|
|
2032
2091
|
};
|
|
2033
2092
|
const onContext = (e) => e.preventDefault();
|
|
2034
|
-
const onBlur = () =>
|
|
2093
|
+
const onBlur = () => {
|
|
2094
|
+
keys.clear();
|
|
2095
|
+
crouchPress = null;
|
|
2096
|
+
};
|
|
2035
2097
|
const onBeforeUnload = (e) => {
|
|
2036
2098
|
if (!locked && !keys.has("ControlLeft") && !keys.has("ControlRight")) return;
|
|
2037
2099
|
e.preventDefault();
|
|
@@ -2048,6 +2110,21 @@ function createPlayer(THREE, o) {
|
|
|
2048
2110
|
window.addEventListener("blur", onBlur);
|
|
2049
2111
|
window.addEventListener("beforeunload", onBeforeUnload);
|
|
2050
2112
|
return {
|
|
2113
|
+
get walkIntent() {
|
|
2114
|
+
return walkKeys();
|
|
2115
|
+
},
|
|
2116
|
+
canStand(x, z) {
|
|
2117
|
+
return !blocked(x, z);
|
|
2118
|
+
},
|
|
2119
|
+
setWalkHeld(held) {
|
|
2120
|
+
walkHeld = held;
|
|
2121
|
+
},
|
|
2122
|
+
setBend(metres) {
|
|
2123
|
+
bendTarget = metres;
|
|
2124
|
+
},
|
|
2125
|
+
setLookLimit(limit) {
|
|
2126
|
+
lookLimit = limit;
|
|
2127
|
+
},
|
|
2051
2128
|
get yaw() {
|
|
2052
2129
|
return yaw;
|
|
2053
2130
|
},
|
|
@@ -2064,10 +2141,10 @@ function createPlayer(THREE, o) {
|
|
|
2064
2141
|
return px.z;
|
|
2065
2142
|
},
|
|
2066
2143
|
get feet() {
|
|
2067
|
-
return y;
|
|
2144
|
+
return floorLevel + y;
|
|
2068
2145
|
},
|
|
2069
2146
|
get position() {
|
|
2070
|
-
return [px.x, y, px.z];
|
|
2147
|
+
return [px.x, floorLevel + y, px.z];
|
|
2071
2148
|
},
|
|
2072
2149
|
get speed() {
|
|
2073
2150
|
return walked;
|
|
@@ -2076,49 +2153,43 @@ function createPlayer(THREE, o) {
|
|
|
2076
2153
|
return running;
|
|
2077
2154
|
},
|
|
2078
2155
|
get crouching() {
|
|
2079
|
-
return
|
|
2156
|
+
return isCrouched();
|
|
2080
2157
|
},
|
|
2081
2158
|
keys,
|
|
2082
2159
|
update(dt) {
|
|
2083
|
-
|
|
2160
|
+
if (crouchPress) crouchPress.held += dt;
|
|
2161
|
+
const crouched = isCrouched();
|
|
2084
2162
|
crouch += ((crouched ? 1 : 0) - crouch) * Math.min(1, dt * CROUCH_RATE);
|
|
2085
2163
|
if (!crouched && crouch < 1e-3) crouch = 0;
|
|
2086
|
-
|
|
2164
|
+
bend += (bendTarget - bend) * Math.min(1, dt * BEND_RATE);
|
|
2165
|
+
const run = !crouched && !walkHeld && (keys.has("ShiftLeft") || keys.has("ShiftRight"));
|
|
2087
2166
|
const fromX = px.x;
|
|
2088
2167
|
const fromZ = px.z;
|
|
2089
2168
|
const speed = crouched ? CROUCH_WALK : run ? RUN : WALK;
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
if (
|
|
2093
|
-
if (keys.has("KeyS") || keys.has("ArrowDown")) f -= 1;
|
|
2094
|
-
if (keys.has("KeyD") || keys.has("ArrowRight")) r += 1;
|
|
2095
|
-
if (keys.has("KeyA") || keys.has("ArrowLeft")) r -= 1;
|
|
2096
|
-
if ((f || r || keys.has("Space")) && eye !== EYE) eye = EYE;
|
|
2097
|
-
if (f || r) {
|
|
2098
|
-
const len = Math.hypot(f, r);
|
|
2099
|
-
const fx = -Math.sin(yaw);
|
|
2100
|
-
const fz = -Math.cos(yaw);
|
|
2101
|
-
const rx = Math.cos(yaw);
|
|
2102
|
-
const rz = -Math.sin(yaw);
|
|
2103
|
-
move((fx * f + rx * r) / len * speed * dt, (fz * f + rz * r) / len * speed * dt);
|
|
2104
|
-
}
|
|
2169
|
+
const [walkX, walkZ] = walkKeys();
|
|
2170
|
+
if ((walkX || walkZ || keys.has("Space")) && eye !== EYE) eye = EYE;
|
|
2171
|
+
if ((walkX || walkZ) && !walkHeld) move(walkX * speed * dt, walkZ * speed * dt);
|
|
2105
2172
|
walked = dt > 0 ? Math.hypot(px.x - fromX, px.z - fromZ) / dt : 0;
|
|
2106
2173
|
running = run && walked > 0;
|
|
2107
|
-
|
|
2108
|
-
if (y
|
|
2174
|
+
const floor = Math.max(floorHere(), o.footing?.(px.x, px.z, PLAYER_RADIUS * FOOT, y).floor ?? -Infinity);
|
|
2175
|
+
if (y < floor && vy <= 0) y = floor;
|
|
2176
|
+
if (y <= floor && keys.has("Space") && !walkHeld) vy = JUMP;
|
|
2177
|
+
if (y > floor || vy > 0) {
|
|
2109
2178
|
vy -= GRAVITY * dt;
|
|
2110
2179
|
y += vy * dt;
|
|
2111
|
-
if (y <=
|
|
2112
|
-
y =
|
|
2180
|
+
if (y <= floor) {
|
|
2181
|
+
y = floor;
|
|
2113
2182
|
vy = 0;
|
|
2114
2183
|
}
|
|
2115
2184
|
}
|
|
2185
|
+
followFloor(dt);
|
|
2116
2186
|
apply();
|
|
2117
2187
|
},
|
|
2118
2188
|
setPose(position, yw, pt) {
|
|
2119
2189
|
px.x = position[0];
|
|
2120
2190
|
px.z = position[2];
|
|
2121
|
-
|
|
2191
|
+
floorLevel = floorHere();
|
|
2192
|
+
eye = position[1] - floorLevel;
|
|
2122
2193
|
y = 0;
|
|
2123
2194
|
vy = 0;
|
|
2124
2195
|
yaw = yw;
|
|
@@ -2128,7 +2199,8 @@ function createPlayer(THREE, o) {
|
|
|
2128
2199
|
look(position, target) {
|
|
2129
2200
|
px.x = position[0];
|
|
2130
2201
|
px.z = position[2];
|
|
2131
|
-
|
|
2202
|
+
floorLevel = floorHere();
|
|
2203
|
+
eye = position[1] - floorLevel;
|
|
2132
2204
|
y = 0;
|
|
2133
2205
|
vy = 0;
|
|
2134
2206
|
const dx = target[0] - position[0];
|
|
@@ -2147,6 +2219,12 @@ function createPlayer(THREE, o) {
|
|
|
2147
2219
|
move(dx, dz);
|
|
2148
2220
|
apply();
|
|
2149
2221
|
},
|
|
2222
|
+
takeWish() {
|
|
2223
|
+
const out = [wish.x, wish.z];
|
|
2224
|
+
wish.x = 0;
|
|
2225
|
+
wish.z = 0;
|
|
2226
|
+
return out;
|
|
2227
|
+
},
|
|
2150
2228
|
walk(forward, right) {
|
|
2151
2229
|
move(-Math.sin(yaw) * forward + Math.cos(yaw) * right, -Math.cos(yaw) * forward - Math.sin(yaw) * right);
|
|
2152
2230
|
apply();
|
|
@@ -2168,6 +2246,204 @@ function createPlayer(THREE, o) {
|
|
|
2168
2246
|
};
|
|
2169
2247
|
}
|
|
2170
2248
|
|
|
2249
|
+
// src/furniture-push.ts
|
|
2250
|
+
import { createFurniturePush } from "@adatechnology/game-player";
|
|
2251
|
+
var PUSH_REACH = 1.1;
|
|
2252
|
+
var STANDOFF = 0.58;
|
|
2253
|
+
var LOW_TOP = 0.95;
|
|
2254
|
+
var LOW_STANDOFF = 0.42;
|
|
2255
|
+
var LOW_HAND_ON_TOP = 0.07;
|
|
2256
|
+
var LOW_EYE_OVER_TOP = 0.62;
|
|
2257
|
+
var MAX_BEND = 0.8;
|
|
2258
|
+
var STANDING_EYE = 1.7;
|
|
2259
|
+
var APPROACH_SPEED = 1.2;
|
|
2260
|
+
var MAX_BASE_HEIGHT = 0.12;
|
|
2261
|
+
var FLOOR_CLEARANCE = 0.06;
|
|
2262
|
+
var SWEEP_SKIN = 0.015;
|
|
2263
|
+
var HAND_SPREAD = 0.2;
|
|
2264
|
+
var HAND_TOP = 1.35;
|
|
2265
|
+
var HAND_UNDER_TOP = 0.05;
|
|
2266
|
+
var LOOK_RANGE = 0.9;
|
|
2267
|
+
var DRAG_GRAIN_METRES = 0.3;
|
|
2268
|
+
var RING_DELAY_S = 0.1;
|
|
2269
|
+
function contactOf(box2, player) {
|
|
2270
|
+
const x = Math.max(box2.min[0] ?? 0, Math.min(box2.max[0] ?? 0, player.x));
|
|
2271
|
+
const z = Math.max(box2.min[2] ?? 0, Math.min(box2.max[2] ?? 0, player.z));
|
|
2272
|
+
const dx = x - player.x;
|
|
2273
|
+
const dz = z - player.z;
|
|
2274
|
+
const distance = Math.hypot(dx, dz);
|
|
2275
|
+
if (distance < 1e-3) return { contact: [x, z], into: [-Math.sin(player.yaw), -Math.cos(player.yaw)], distance: 0 };
|
|
2276
|
+
return { contact: [x, z], into: [dx / distance, dz / distance], distance };
|
|
2277
|
+
}
|
|
2278
|
+
function createRoomPush(options) {
|
|
2279
|
+
const { THREE, player, world, arena, blockers } = options;
|
|
2280
|
+
const control = createFurniturePush(options.rules);
|
|
2281
|
+
const box3 = new THREE.Box3();
|
|
2282
|
+
let leaning = null;
|
|
2283
|
+
let scriptedWalk = null;
|
|
2284
|
+
let dragged = 0;
|
|
2285
|
+
const event = (type, lean) => options.onEvent({ type, objectId: lean.target.objectId, name: lean.target.name, massKg: lean.massKg });
|
|
2286
|
+
const standsClear = (lean, dx, dz) => {
|
|
2287
|
+
const minX = lean.min[0] + dx;
|
|
2288
|
+
const maxX = lean.max[0] + dx;
|
|
2289
|
+
const minZ = lean.min[2] + dz;
|
|
2290
|
+
const maxZ = lean.max[2] + dz;
|
|
2291
|
+
if (Math.max(Math.abs(minX), Math.abs(maxX)) > arena.limit || Math.max(Math.abs(minZ), Math.abs(maxZ)) > arena.limit) return false;
|
|
2292
|
+
for (const c of arena.colliders) {
|
|
2293
|
+
if (lean.max[0] > c[0] && lean.min[0] < c[2] && lean.max[2] > c[1] && lean.min[2] < c[3]) continue;
|
|
2294
|
+
if (maxX > c[0] && minX < c[2] && maxZ > c[1] && minZ < c[3]) return false;
|
|
2295
|
+
}
|
|
2296
|
+
if (blockers.size === 0) return true;
|
|
2297
|
+
const midX = (minX + maxX) / 2;
|
|
2298
|
+
const midZ = (minZ + maxZ) / 2;
|
|
2299
|
+
for (const [x, z] of [[minX, minZ], [maxX, minZ], [minX, maxZ], [maxX, maxZ], [midX, minZ], [midX, maxZ], [minX, midZ], [maxX, midZ]]) {
|
|
2300
|
+
for (const isBlocked of blockers) if (isBlocked(x, z, 0)) return false;
|
|
2301
|
+
}
|
|
2302
|
+
return true;
|
|
2303
|
+
};
|
|
2304
|
+
const isFree = (lean, dx, dz) => {
|
|
2305
|
+
if (!standsClear(lean, dx, dz) || !player.canStand(player.x + dx, player.z + dz)) return false;
|
|
2306
|
+
const distance = Math.hypot(dx, dz);
|
|
2307
|
+
const box2 = { min: [lean.min[0], lean.min[1] + FLOOR_CLEARANCE, lean.min[2]], max: [lean.max[0], lean.max[1], lean.max[2]] };
|
|
2308
|
+
return world.sweepBox(box2, [dx / distance, 0, dz / distance], distance + SWEEP_SKIN, { ignore: lean.target.objectId }) === null;
|
|
2309
|
+
};
|
|
2310
|
+
const release = () => {
|
|
2311
|
+
const lean = leaning;
|
|
2312
|
+
if (!lean) return;
|
|
2313
|
+
leaning = null;
|
|
2314
|
+
if (lean.piece) {
|
|
2315
|
+
lean.piece.held = false;
|
|
2316
|
+
lean.piece.sleeping = false;
|
|
2317
|
+
lean.piece.velocity = [0, 0, 0];
|
|
2318
|
+
}
|
|
2319
|
+
control.release();
|
|
2320
|
+
player.setWalkHeld(false);
|
|
2321
|
+
player.setBend(0);
|
|
2322
|
+
player.setLookLimit(null);
|
|
2323
|
+
options.ring(null);
|
|
2324
|
+
event("release", lean);
|
|
2325
|
+
};
|
|
2326
|
+
const approach = (lean, dt) => {
|
|
2327
|
+
const off = Math.hypot(lean.contact[0] - player.x, lean.contact[1] - player.z) - (lean.isLow ? LOW_STANDOFF : STANDOFF);
|
|
2328
|
+
const move = Math.sign(off) * Math.min(Math.abs(off), APPROACH_SPEED * dt);
|
|
2329
|
+
if (move !== 0) player.moveBy(lean.into[0] * move, lean.into[1] * move);
|
|
2330
|
+
};
|
|
2331
|
+
const moveWith = (lean, dx, dz) => {
|
|
2332
|
+
lean.target.root.position.x += dx;
|
|
2333
|
+
lean.target.root.position.z += dz;
|
|
2334
|
+
lean.target.root.updateMatrixWorld(true);
|
|
2335
|
+
player.moveBy(dx, dz);
|
|
2336
|
+
lean.min[0] += dx;
|
|
2337
|
+
lean.max[0] += dx;
|
|
2338
|
+
lean.min[2] += dz;
|
|
2339
|
+
lean.max[2] += dz;
|
|
2340
|
+
lean.contact[0] += dx;
|
|
2341
|
+
lean.contact[1] += dz;
|
|
2342
|
+
dragged += Math.hypot(dx, dz);
|
|
2343
|
+
if (dragged < DRAG_GRAIN_METRES) return;
|
|
2344
|
+
dragged = 0;
|
|
2345
|
+
options.dragSound(lean.target.objectId, [lean.contact[0], lean.min[1], lean.contact[1]], 0.3 + 0.5 * Math.min(1, lean.massKg / 100));
|
|
2346
|
+
};
|
|
2347
|
+
return {
|
|
2348
|
+
get active() {
|
|
2349
|
+
return leaning !== null;
|
|
2350
|
+
},
|
|
2351
|
+
get state() {
|
|
2352
|
+
return leaning ? { objectId: leaning.target.objectId, phase: control.phase, progress: control.progress, block: control.block, massKg: leaning.massKg } : null;
|
|
2353
|
+
},
|
|
2354
|
+
get isBent() {
|
|
2355
|
+
return leaning?.isLow ?? false;
|
|
2356
|
+
},
|
|
2357
|
+
get avatarState() {
|
|
2358
|
+
return leaning && control.phase !== "idle" ? { phase: control.phase, progress: control.progress } : null;
|
|
2359
|
+
},
|
|
2360
|
+
begin() {
|
|
2361
|
+
if (leaning) return true;
|
|
2362
|
+
const target = options.pick();
|
|
2363
|
+
if (!target || !world.has(target.objectId)) return false;
|
|
2364
|
+
box3.setFromObject(target.root);
|
|
2365
|
+
if (box3.min.y > player.feet + MAX_BASE_HEIGHT) return false;
|
|
2366
|
+
const min = [box3.min.x, box3.min.y, box3.min.z];
|
|
2367
|
+
const max = [box3.max.x, box3.max.y, box3.max.z];
|
|
2368
|
+
const { contact, into, distance } = contactOf({ min, max }, player);
|
|
2369
|
+
if (distance > PUSH_REACH) return false;
|
|
2370
|
+
const massKg = options.massOf(target.objectId);
|
|
2371
|
+
const material = options.materialOf(target.objectId);
|
|
2372
|
+
const isLow = max[1] - player.feet < LOW_TOP;
|
|
2373
|
+
const side = new THREE.Vector3(-into[1], 0, into[0]);
|
|
2374
|
+
const way = new THREE.Vector3(into[0], 0, into[1]);
|
|
2375
|
+
const up = new THREE.Vector3(0, 1, 0);
|
|
2376
|
+
const basis = isLow ? new THREE.Matrix4().makeBasis(way, up, side) : new THREE.Matrix4().makeBasis(up, way.clone().negate(), side);
|
|
2377
|
+
const palm = new THREE.Quaternion().setFromRotationMatrix(basis);
|
|
2378
|
+
leaning = { target, massKg, piece: target.piece, min, max, contact, into, isLow, palm: [palm.x, palm.y, palm.z, palm.w] };
|
|
2379
|
+
if (target.piece) target.piece.held = true;
|
|
2380
|
+
control.begin(material === void 0 ? { massKg } : { massKg, material });
|
|
2381
|
+
dragged = 0;
|
|
2382
|
+
player.setWalkHeld(true);
|
|
2383
|
+
if (isLow) player.setBend(Math.min(MAX_BEND, Math.max(0, STANDING_EYE - (max[1] - player.feet + LOW_EYE_OVER_TOP))));
|
|
2384
|
+
player.setLookLimit({ yaw: Math.atan2(-into[0], -into[1]), range: LOOK_RANGE });
|
|
2385
|
+
event("grip", leaning);
|
|
2386
|
+
return true;
|
|
2387
|
+
},
|
|
2388
|
+
end() {
|
|
2389
|
+
if (!leaning) return false;
|
|
2390
|
+
const tap = control.isTap;
|
|
2391
|
+
release();
|
|
2392
|
+
return tap;
|
|
2393
|
+
},
|
|
2394
|
+
setWalk(walk) {
|
|
2395
|
+
scriptedWalk = walk;
|
|
2396
|
+
},
|
|
2397
|
+
step(dt) {
|
|
2398
|
+
const lean = leaning;
|
|
2399
|
+
if (!lean) return;
|
|
2400
|
+
if (!world.has(lean.target.objectId)) {
|
|
2401
|
+
release();
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
const gripping = control.phase === "grip";
|
|
2405
|
+
if (gripping) approach(lean, dt);
|
|
2406
|
+
const result = control.update(dt, { walk: scriptedWalk ?? player.walkIntent, isFree: (dx, dz) => isFree(lean, dx, dz) });
|
|
2407
|
+
if (result.engaged) {
|
|
2408
|
+
if (control.block !== "too_heavy") {
|
|
2409
|
+
lean.piece ?? (lean.piece = options.makePiece(lean.target.objectId));
|
|
2410
|
+
lean.piece.held = true;
|
|
2411
|
+
}
|
|
2412
|
+
event(control.block === "too_heavy" ? "too_heavy" : "engaged", lean);
|
|
2413
|
+
}
|
|
2414
|
+
if (result.hitObstacle) {
|
|
2415
|
+
options.hitSound(lean.target.objectId, [lean.contact[0], lean.min[1] + 0.3, lean.contact[1]]);
|
|
2416
|
+
event("obstacle", lean);
|
|
2417
|
+
}
|
|
2418
|
+
if (result.dx !== 0 || result.dz !== 0) moveWith(lean, result.dx, result.dz);
|
|
2419
|
+
options.ring(control.phase === "grip" && control.heldSeconds >= RING_DELAY_S ? control.progress : null);
|
|
2420
|
+
},
|
|
2421
|
+
handTargets() {
|
|
2422
|
+
const lean = leaning;
|
|
2423
|
+
if (!lean) return null;
|
|
2424
|
+
const strain = control.phase === "strain";
|
|
2425
|
+
const y = lean.isLow ? lean.max[1] + 0.01 : Math.min(HAND_TOP, lean.max[1] - HAND_UNDER_TOP);
|
|
2426
|
+
const shake = strain ? Math.sin(performance.now() * 0.045) * 6e-3 : 0;
|
|
2427
|
+
const inset = lean.isLow ? -LOW_HAND_ON_TOP + shake : (strain ? 0.05 : 0.02) + shake;
|
|
2428
|
+
const px = lean.contact[0] - lean.into[0] * inset;
|
|
2429
|
+
const pz = lean.contact[1] - lean.into[1] * inset;
|
|
2430
|
+
const sideX = -lean.into[1] * HAND_SPREAD;
|
|
2431
|
+
const sideZ = lean.into[0] * HAND_SPREAD;
|
|
2432
|
+
const grip = 0.05 + 0.2 * control.progress;
|
|
2433
|
+
return {
|
|
2434
|
+
R: { position: [px + sideX, y, pz + sideZ], quaternion: lean.palm },
|
|
2435
|
+
L: { position: [px - sideX, y, pz - sideZ], quaternion: lean.palm },
|
|
2436
|
+
grip,
|
|
2437
|
+
gripL: grip
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
// src/room.ts
|
|
2444
|
+
import { createTerrain, createTerrainImpl, DEFAULT_PROFILE } from "@adatechnology/game-terrain";
|
|
2445
|
+
import { createTerrainRenderer } from "@adatechnology/game-terrain-three";
|
|
2446
|
+
|
|
2171
2447
|
// src/avatar.ts
|
|
2172
2448
|
var DEFAULT_STRIKE_CLIPS = { hand: "punch_right", fists: "punch_right", shove: "shove_two_hands", kick: "front_kick" };
|
|
2173
2449
|
var DEFAULT_HAND_STANCES = { fists: "fists" };
|
|
@@ -2349,6 +2625,8 @@ var en = {
|
|
|
2349
2625
|
"act.strike.hand": "{weapon}: {clip} with the {hand} hand begins, impact in {seconds} s",
|
|
2350
2626
|
"hand.left": "left",
|
|
2351
2627
|
"hand.right": "right",
|
|
2628
|
+
"act.combo.on": "{name}: on (each blow follows the sequence)",
|
|
2629
|
+
"act.combo.off": "{name}: off",
|
|
2352
2630
|
"act.flame.hit": "{weapon}: flame at {temp} \xB0C on {where}",
|
|
2353
2631
|
"act.flame.air": "{weapon}: flame, nothing in range",
|
|
2354
2632
|
"act.heat.hit": "{weapon}: heating {where} at {temp} \xB0C",
|
|
@@ -2372,8 +2650,10 @@ var en = {
|
|
|
2372
2650
|
"act.extinguish.hit": "{weapon}: foam on {what}",
|
|
2373
2651
|
"act.extinguish.air": "{weapon}: foam, nothing in range",
|
|
2374
2652
|
"act.grab": "Grabbed {name} ({kg} kg)",
|
|
2653
|
+
"act.bump": "Pushed {name} ({kg} kg) walking into it",
|
|
2375
2654
|
"act.drop": "Dropped {name}",
|
|
2376
2655
|
"act.throw": "Threw {name} at {speed} m/s",
|
|
2656
|
+
"ground.name": "Ground",
|
|
2377
2657
|
"act.landing": "{name} landed at {speed} m/s: {energy} J",
|
|
2378
2658
|
"act.bench_failed": "{name} failed: {error}",
|
|
2379
2659
|
"loading.working": "Working\u2026",
|
|
@@ -2407,6 +2687,14 @@ var en = {
|
|
|
2407
2687
|
"hud.nothing_to_grab": "Nothing to grab here",
|
|
2408
2688
|
"hud.cannot_grab": "{name} cannot be picked up",
|
|
2409
2689
|
"hud.too_heavy": "{name} is too heavy ({kg} kg)",
|
|
2690
|
+
"hud.push_on": "Pushing {name}: W pushes, S pulls, let go of E to stop",
|
|
2691
|
+
"hud.push_too_heavy": "{name} does not budge ({kg} kg)",
|
|
2692
|
+
"hud.push_blocked": "{name} is stuck: there is something in the way",
|
|
2693
|
+
"act.push_grip": "leans on {name} ({kg} kg)",
|
|
2694
|
+
"act.push_engaged": "starts pushing {name} ({kg} kg)",
|
|
2695
|
+
"act.push_too_heavy": "strains against {name}: {kg} kg is too much",
|
|
2696
|
+
"act.push_obstacle": "{name} hits something and locks",
|
|
2697
|
+
"act.push_release": "lets go of {name}",
|
|
2410
2698
|
"hud.empty_container": "{name} is empty",
|
|
2411
2699
|
"hud.refused": "Wrong tool for the {kind}",
|
|
2412
2700
|
"hud.drop": "drop",
|
|
@@ -2414,7 +2702,7 @@ var en = {
|
|
|
2414
2702
|
"hud.throw": "throw",
|
|
2415
2703
|
"hud.tilt": "tilt (hold)",
|
|
2416
2704
|
"hud.equip": "equip {weapon}",
|
|
2417
|
-
"ui.help": "Click: look \xB7 WASD: walk \xB7 Shift: run \xB7 C: crouch \xB7 Space: jump \xB7 1-0 / wheel: tool \xB7 LMB: use (bare hands: punch) \xB7 E: grab/drop \xB7 G or RMB: throw \xB7 R: reload \xB7 Q: fire mode \xB7 X: clear a jam \xB7 B: safety \xB7 Y: detonate charges set down \xB7 Tab: panels \xB7 Esc: free the mouse",
|
|
2705
|
+
"ui.help": "Click: look \xB7 WASD: walk \xB7 Shift: run \xB7 C: crouch (tap stays, hold while pressed) \xB7 Space: jump \xB7 1-0 / wheel: tool \xB7 LMB: use (bare hands: punch) \xB7 E: grab/drop (hold against furniture: push) \xB7 G or RMB: throw \xB7 R: reload \xB7 Q: fire mode \xB7 X: clear a jam \xB7 B: safety \xB7 Y: detonate charges set down \xB7 K: kick \xB7 N: strike combo on/off \xB7 Tab: panels \xB7 Esc: free the mouse",
|
|
2418
2706
|
"ui.help.camera": "V: 1st/3rd person (Shift+V: kick)",
|
|
2419
2707
|
"camera.first": "1st person",
|
|
2420
2708
|
"camera.third": "3rd person",
|
|
@@ -2461,6 +2749,7 @@ var en = {
|
|
|
2461
2749
|
"ui.slow": "Slow motion 0.25\xD7",
|
|
2462
2750
|
"ui.fast": "Time \xD710",
|
|
2463
2751
|
"ui.tap": "Tap",
|
|
2752
|
+
"ui.kick": "Kick",
|
|
2464
2753
|
"ui.fullscreen": "Full screen",
|
|
2465
2754
|
"ui.fullscreen.hint": "Full screen opened from here keeps the whole keyboard in the game, Ctrl+W included (F11 does not)",
|
|
2466
2755
|
"ui.blast.shelter": "Shelter",
|
|
@@ -2660,6 +2949,8 @@ var ptBR = {
|
|
|
2660
2949
|
"act.strike.hand": "{weapon}: {clip} com a m\xE3o {hand} come\xE7a, impacto em {seconds} s",
|
|
2661
2950
|
"hand.left": "esquerda",
|
|
2662
2951
|
"hand.right": "direita",
|
|
2952
|
+
"act.combo.on": "{name}: ligada (cada golpe segue a sequ\xEAncia)",
|
|
2953
|
+
"act.combo.off": "{name}: desligada",
|
|
2663
2954
|
"act.flame.hit": "{weapon}: chama a {temp} \xB0C em {where}",
|
|
2664
2955
|
"act.flame.air": "{weapon}: chama, nada ao alcance",
|
|
2665
2956
|
"act.heat.hit": "{weapon}: esquentando {where} a {temp} \xB0C",
|
|
@@ -2683,8 +2974,10 @@ var ptBR = {
|
|
|
2683
2974
|
"act.extinguish.hit": "{weapon}: espuma em {what}",
|
|
2684
2975
|
"act.extinguish.air": "{weapon}: espuma, nada ao alcance",
|
|
2685
2976
|
"act.grab": "Pegou {name} ({kg} kg)",
|
|
2977
|
+
"act.bump": "Empurrou {name} ({kg} kg) ao esbarrar",
|
|
2686
2978
|
"act.drop": "Soltou {name}",
|
|
2687
2979
|
"act.throw": "Arremessou {name} a {speed} m/s",
|
|
2980
|
+
"ground.name": "Ch\xE3o",
|
|
2688
2981
|
"act.landing": "{name} caiu a {speed} m/s: {energy} J",
|
|
2689
2982
|
"act.bench_failed": "{name} falhou: {error}",
|
|
2690
2983
|
"loading.working": "Trabalhando\u2026",
|
|
@@ -2718,6 +3011,14 @@ var ptBR = {
|
|
|
2718
3011
|
"hud.nothing_to_grab": "Nada para pegar aqui",
|
|
2719
3012
|
"hud.cannot_grab": "{name} n\xE3o d\xE1 para pegar",
|
|
2720
3013
|
"hud.too_heavy": "{name} \xE9 pesado demais ({kg} kg)",
|
|
3014
|
+
"hud.push_on": "Empurrando {name}: W empurra, S puxa, solte o E para parar",
|
|
3015
|
+
"hud.push_too_heavy": "{name} n\xE3o se mexe ({kg} kg)",
|
|
3016
|
+
"hud.push_blocked": "{name} travou: tem algo no caminho",
|
|
3017
|
+
"act.push_grip": "encosta em {name} ({kg} kg)",
|
|
3018
|
+
"act.push_engaged": "come\xE7a a empurrar {name} ({kg} kg)",
|
|
3019
|
+
"act.push_too_heavy": "faz for\xE7a contra {name}: {kg} kg \xE9 demais",
|
|
3020
|
+
"act.push_obstacle": "{name} bate em algo e trava",
|
|
3021
|
+
"act.push_release": "solta {name}",
|
|
2721
3022
|
"hud.empty_container": "{name} est\xE1 vazio",
|
|
2722
3023
|
"hud.refused": "Ferramenta errada para {kind}",
|
|
2723
3024
|
"hud.drop": "soltar",
|
|
@@ -2725,7 +3026,7 @@ var ptBR = {
|
|
|
2725
3026
|
"hud.throw": "arremessar",
|
|
2726
3027
|
"hud.tilt": "inclinar (segurar)",
|
|
2727
3028
|
"hud.equip": "equipar {weapon}",
|
|
2728
|
-
"ui.help": "Clique: olhar \xB7 WASD: andar \xB7 Shift: correr \xB7 C: agachar \xB7 Espa\xE7o: pular \xB7 1-0 / roda: ferramenta \xB7 bot\xE3o esquerdo: usar (m\xE3o livre: soco) \xB7 E: pegar/soltar \xB7 G ou bot\xE3o direito: arremessar \xB7 R: recarregar \xB7 Q: modo de tiro \xB7 X: destravar \xB7 B: trava \xB7 Y: detonar cargas postas \xB7 Tab: pain\xE9is \xB7 Esc: soltar o mouse",
|
|
3029
|
+
"ui.help": "Clique: olhar \xB7 WASD: andar \xB7 Shift: correr \xB7 C: agachar (toque fixa, segurar s\xF3 enquanto aperta) \xB7 Espa\xE7o: pular \xB7 1-0 / roda: ferramenta \xB7 bot\xE3o esquerdo: usar (m\xE3o livre: soco) \xB7 E: pegar/soltar (segurar encostado no m\xF3vel: empurrar) \xB7 G ou bot\xE3o direito: arremessar \xB7 R: recarregar \xB7 Q: modo de tiro \xB7 X: destravar \xB7 B: trava \xB7 Y: detonar cargas postas \xB7 K: chute \xB7 N: liga/desliga a sequ\xEAncia de golpes \xB7 Tab: pain\xE9is \xB7 Esc: soltar o mouse",
|
|
2729
3030
|
"ui.help.camera": "V: 1\xAA/3\xAA pessoa (Shift+V: chute)",
|
|
2730
3031
|
"camera.first": "1\xAA pessoa",
|
|
2731
3032
|
"camera.third": "3\xAA pessoa",
|
|
@@ -2772,6 +3073,7 @@ var ptBR = {
|
|
|
2772
3073
|
"ui.slow": "C\xE2mera lenta 0,25\xD7",
|
|
2773
3074
|
"ui.fast": "Tempo \xD710",
|
|
2774
3075
|
"ui.tap": "Torneira",
|
|
3076
|
+
"ui.kick": "Chute",
|
|
2775
3077
|
"ui.fullscreen": "Tela cheia",
|
|
2776
3078
|
"ui.fullscreen.hint": "A tela cheia aberta por aqui segura o teclado todo no jogo, inclusive Ctrl+W (o F11 n\xE3o)",
|
|
2777
3079
|
"ui.blast.shelter": "Abrigo",
|
|
@@ -3238,6 +3540,8 @@ var CSS = `
|
|
|
3238
3540
|
.tr-cross{position:absolute;left:50%;top:50%;width:18px;height:18px;margin:-9px 0 0 -9px}
|
|
3239
3541
|
.tr-cross:before,.tr-cross:after{content:"";position:absolute;background:#fff;opacity:.85;box-shadow:0 0 2px #000}
|
|
3240
3542
|
.tr-cross:before{left:8px;top:0;width:2px;height:18px}.tr-cross:after{top:8px;left:0;height:2px;width:18px}
|
|
3543
|
+
.tr-ring{position:absolute;left:50%;top:50%;width:64px;height:64px;margin:-32px 0 0 -32px;transform:rotate(-90deg);display:none;pointer-events:none;filter:drop-shadow(0 0 2px #000)}
|
|
3544
|
+
.tr-ring circle{fill:none;stroke-width:5}.tr-ring .tr-ring-bg{stroke:rgba(255,255,255,.25)}.tr-ring .tr-ring-fill{stroke:#ffd166;stroke-linecap:round}
|
|
3241
3545
|
.tr-cross i{position:absolute;left:6px;top:6px;width:6px;height:6px;background:#0b0c0e;border-radius:50%;z-index:1}
|
|
3242
3546
|
.tr-top{position:absolute;top:8px;left:50%;transform:translateX(-50%);text-align:center;max-width:46vw}
|
|
3243
3547
|
.tr-tool{display:inline-block;background:rgba(10,12,16,.72);padding:4px 12px;border-radius:6px;font-size:14px;font-weight:600}
|
|
@@ -3341,6 +3645,22 @@ function createUi(o) {
|
|
|
3341
3645
|
o.container.appendChild(root);
|
|
3342
3646
|
const cross = mk3("div", "tr-cross");
|
|
3343
3647
|
cross.appendChild(mk3("i"));
|
|
3648
|
+
const ring = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
3649
|
+
ring.setAttribute("class", "tr-ring");
|
|
3650
|
+
ring.setAttribute("viewBox", "0 0 64 64");
|
|
3651
|
+
const ringCircle = (className) => {
|
|
3652
|
+
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
3653
|
+
circle.setAttribute("class", className);
|
|
3654
|
+
circle.setAttribute("cx", "32");
|
|
3655
|
+
circle.setAttribute("cy", "32");
|
|
3656
|
+
circle.setAttribute("r", "27");
|
|
3657
|
+
ring.appendChild(circle);
|
|
3658
|
+
return circle;
|
|
3659
|
+
};
|
|
3660
|
+
ringCircle("tr-ring-bg");
|
|
3661
|
+
const ringFill = ringCircle("tr-ring-fill");
|
|
3662
|
+
const RING_LENGTH = 2 * Math.PI * 27;
|
|
3663
|
+
ringFill.style.strokeDasharray = String(RING_LENGTH);
|
|
3344
3664
|
const top = mk3("div", "tr-top");
|
|
3345
3665
|
const toolBox = mk3("div", "tr-tool");
|
|
3346
3666
|
const status = mk3("div", "tr-status");
|
|
@@ -3542,6 +3862,8 @@ function createUi(o) {
|
|
|
3542
3862
|
o.onFast(fast);
|
|
3543
3863
|
});
|
|
3544
3864
|
const tapBtn = ctl(t("ui.tap"), () => o.onTap());
|
|
3865
|
+
const comboBtn = o.combo ? ctl(`${o.combo.label} (N)`, () => o.combo.onToggle()) : null;
|
|
3866
|
+
if (o.kick) ctl(o.kick.label, () => o.kick.onKick());
|
|
3545
3867
|
const fullscreenBtn = ctl(t("ui.fullscreen"), () => void toggleFullscreen());
|
|
3546
3868
|
fullscreenBtn.title = t("ui.fullscreen.hint");
|
|
3547
3869
|
const blastBtns = ["shelter", "fragmentHits", "confinement"].map((key) => ({ key, b: ctl(t(`ui.blast.${key}`), () => o.onBlast(key)) }));
|
|
@@ -3624,7 +3946,7 @@ function createUi(o) {
|
|
|
3624
3946
|
o.onResize();
|
|
3625
3947
|
};
|
|
3626
3948
|
toggle.onclick = togglePanels;
|
|
3627
|
-
root.append(cross, top, bottom, help, left, right, toggle);
|
|
3949
|
+
root.append(cross, ring, top, bottom, help, left, right, toggle);
|
|
3628
3950
|
const bar = (value, max, cls) => {
|
|
3629
3951
|
const b = mk3("div", `tr-bar ${cls}`);
|
|
3630
3952
|
const i = mk3("i");
|
|
@@ -3705,6 +4027,10 @@ function createUi(o) {
|
|
|
3705
4027
|
let lastInspect = "";
|
|
3706
4028
|
let hudPage = 0;
|
|
3707
4029
|
const api = {
|
|
4030
|
+
holdRing(value) {
|
|
4031
|
+
ring.style.display = value === null ? "none" : "block";
|
|
4032
|
+
if (value !== null) ringFill.style.strokeDashoffset = String(RING_LENGTH * (1 - value));
|
|
4033
|
+
},
|
|
3708
4034
|
hud(s) {
|
|
3709
4035
|
toolBox.textContent = "";
|
|
3710
4036
|
toolBox.append(document.createTextNode(s.toolName));
|
|
@@ -3753,6 +4079,7 @@ function createUi(o) {
|
|
|
3753
4079
|
}
|
|
3754
4080
|
slotsEl.forEach((b, i) => b.classList.toggle("on", i === s.slot));
|
|
3755
4081
|
tapBtn.classList.toggle("on", s.tap);
|
|
4082
|
+
comboBtn?.classList.toggle("on", !!s.combo);
|
|
3756
4083
|
for (const { key: key2, b } of blastBtns) b.classList.toggle("on", s.blast[key2]);
|
|
3757
4084
|
count.textContent = t("ui.objects", { n: s.objects });
|
|
3758
4085
|
const ak = JSON.stringify(s.animations);
|
|
@@ -3802,6 +4129,18 @@ function createUi(o) {
|
|
|
3802
4129
|
|
|
3803
4130
|
// src/room.ts
|
|
3804
4131
|
var FIXED_DT = 1 / 60;
|
|
4132
|
+
var SOLID_SPAN = 3;
|
|
4133
|
+
var STEP_UP = 0.45;
|
|
4134
|
+
var BODY_HEIGHT = 1.75;
|
|
4135
|
+
var KICK_MASS = 6;
|
|
4136
|
+
var KICK_HEIGHT = 0.6;
|
|
4137
|
+
var GROUND_ID = "room_ground";
|
|
4138
|
+
var TERRAIN_CELL = 0.1;
|
|
4139
|
+
var TERRAIN_MAX_CELLS = 1e3;
|
|
4140
|
+
var GROUND_BURST_MAX = 28;
|
|
4141
|
+
var GROUND_BURST_PER_METRE = 30;
|
|
4142
|
+
var GROUND_DUST_RADIUS = 0.2;
|
|
4143
|
+
var GROUND_FIRE_EVERY = 0.3;
|
|
3805
4144
|
var SHOVE_SPEED = 0.04;
|
|
3806
4145
|
var METRICS_INTERVAL_S = 0.25;
|
|
3807
4146
|
var CARRY_NEAR = 0.62;
|
|
@@ -3862,10 +4201,12 @@ function createTestRoom(options) {
|
|
|
3862
4201
|
const ownWorld = !options.world;
|
|
3863
4202
|
const world = options.world ?? createDestruction({ seed: 1, ...options.worldOptions, materials: [...options.worldOptions?.materials ?? []] });
|
|
3864
4203
|
const pristineWorld = ownWorld ? world.serialize() : null;
|
|
3865
|
-
const
|
|
3866
|
-
const p = world.part(
|
|
3867
|
-
return !!p && p.status !== "broken" && p.status !== "melted" && p.status !== "ashes";
|
|
4204
|
+
const partStands = (object, part) => {
|
|
4205
|
+
const p = world.part(object, part);
|
|
4206
|
+
return !!p && p.status !== "broken" && p.status !== "melted" && p.status !== "ashes" && !renderer.piece(object);
|
|
3868
4207
|
};
|
|
4208
|
+
const topOk = (t2) => t2.boards ? t2.boards.some((b) => partStands(t2.id, b.part)) : partStands(t2.id, t2.topPart);
|
|
4209
|
+
const topHolds = (t2, x, z) => t2.boards ? t2.boards.some((b) => x > b.minX && x < b.maxX && z > b.minZ && z < b.maxZ && partStands(t2.id, b.part)) : topOk(t2);
|
|
3869
4210
|
const restingTops = [];
|
|
3870
4211
|
const restingTopAt = (x, z) => {
|
|
3871
4212
|
let top = 0;
|
|
@@ -3874,10 +4215,18 @@ function createTestRoom(options) {
|
|
|
3874
4215
|
};
|
|
3875
4216
|
const sceneryTopAt = (x, z) => {
|
|
3876
4217
|
let top = 0;
|
|
3877
|
-
for (const t2 of arena.tops) if (t2.top > top && x > t2.minX && x < t2.maxX && z > t2.minZ && z < t2.maxZ &&
|
|
4218
|
+
for (const t2 of arena.tops) if (t2.top > top && x > t2.minX && x < t2.maxX && z > t2.minZ && z < t2.maxZ && topHolds(t2, x, z)) top = t2.top;
|
|
3878
4219
|
return top;
|
|
3879
4220
|
};
|
|
3880
|
-
const
|
|
4221
|
+
const defaultGround = options.ground ?? { surface: DEFAULT_PROFILE };
|
|
4222
|
+
const terrain = createTerrain({ bounds: [-arenaSize, -arenaSize, arenaSize, arenaSize], cellSize: Math.max(TERRAIN_CELL, 2 * arenaSize / TERRAIN_MAX_CELLS), layout: defaultGround, ...options.terrainBudget ? { budget: options.terrainBudget } : {} });
|
|
4223
|
+
const terrainView = createTerrainRenderer(THREE, { terrain });
|
|
4224
|
+
arena.group.add(terrainView.group);
|
|
4225
|
+
const groundAt = (x, z) => {
|
|
4226
|
+
const top = Math.max(sceneryTopAt(x, z), restingTopAt(x, z));
|
|
4227
|
+
const floor = terrain.heightAt(x, z);
|
|
4228
|
+
return top > 0 ? Math.max(top, floor) : floor;
|
|
4229
|
+
};
|
|
3881
4230
|
const ownFluids = !options.fluids;
|
|
3882
4231
|
const fluids = options.fluids ?? createFluids({ seed: 1, locale, liquids: options.liquids, groundAt });
|
|
3883
4232
|
const pristineFluids = ownFluids ? fluids.serialize() : null;
|
|
@@ -3898,13 +4247,6 @@ function createTestRoom(options) {
|
|
|
3898
4247
|
destructionSound?.onSound(key, point, volume);
|
|
3899
4248
|
options.onSound?.(key, point, volume);
|
|
3900
4249
|
};
|
|
3901
|
-
const craterField = options.craters ? createGroundField() : void 0;
|
|
3902
|
-
const craterPatch = craterField ? createGroundPatch(THREE, { field: craterField, material: new THREE.MeshStandardMaterial({ color: 9342602, roughness: 0.95 }) }) : void 0;
|
|
3903
|
-
if (craterPatch) {
|
|
3904
|
-
const floor = arena.group.getObjectByName("floor");
|
|
3905
|
-
if (floor) floor.visible = false;
|
|
3906
|
-
scene.add(craterPatch.mesh);
|
|
3907
|
-
}
|
|
3908
4250
|
const vfx = createVfxRenderer(THREE, {
|
|
3909
4251
|
scene,
|
|
3910
4252
|
camera,
|
|
@@ -3913,8 +4255,7 @@ function createTestRoom(options) {
|
|
|
3913
4255
|
pointScale,
|
|
3914
4256
|
vfx: createVfx({
|
|
3915
4257
|
profile: quality,
|
|
3916
|
-
groundAt
|
|
3917
|
-
...craterField ? { ground: craterField } : {}
|
|
4258
|
+
groundAt
|
|
3918
4259
|
})
|
|
3919
4260
|
});
|
|
3920
4261
|
const renderer = createDestructionRenderer(THREE, {
|
|
@@ -3935,6 +4276,7 @@ function createTestRoom(options) {
|
|
|
3935
4276
|
const e = entries.get(objectId);
|
|
3936
4277
|
if (!e) return;
|
|
3937
4278
|
e.piece = piece;
|
|
4279
|
+
leaveFootprint(objectId);
|
|
3938
4280
|
rendererPieces.set(objectId, piece);
|
|
3939
4281
|
action("loosened", t("act.loosened", { name: e.name }), { objectId, itemId: e.so.itemId });
|
|
3940
4282
|
},
|
|
@@ -3959,6 +4301,31 @@ function createTestRoom(options) {
|
|
|
3959
4301
|
if (best) splitOf.set(piece.objectId, best);
|
|
3960
4302
|
}
|
|
3961
4303
|
});
|
|
4304
|
+
const showGroundBlow = (result) => {
|
|
4305
|
+
const material = terrain.material(result.removed[0]?.material ?? result.exposed);
|
|
4306
|
+
const def = world.materials.get(material.id);
|
|
4307
|
+
const size = Math.min(GROUND_BURST_MAX, Math.ceil(result.radius * GROUND_BURST_PER_METRE));
|
|
4308
|
+
renderer.emit(material.debris === "clods" ? "dust" : material.debris, result.point, [0, 1, 0], Math.max(2, size), Math.min(4, 1 + result.radius * 4));
|
|
4309
|
+
if (result.radius > GROUND_DUST_RADIUS) renderer.emit("dust", result.point, [0, 1, 0], size, 1.4);
|
|
4310
|
+
playSound(impactKey(def), result.point, Math.min(1, 0.25 + result.radius));
|
|
4311
|
+
};
|
|
4312
|
+
terrain.onDeform((region) => renderer.wakeRegion(region.x, region.z, region.radius));
|
|
4313
|
+
const groundImpl = createTerrainImpl(terrain, { id: GROUND_ID, name: t("ground.name"), onBlow: showGroundBlow });
|
|
4314
|
+
const registerGround = () => {
|
|
4315
|
+
if (!world.has(GROUND_ID)) world.register(groundImpl, { id: GROUND_ID });
|
|
4316
|
+
};
|
|
4317
|
+
registerGround();
|
|
4318
|
+
let groundFireClock = 0;
|
|
4319
|
+
const showGroundFire = (dt) => {
|
|
4320
|
+
groundFireClock += dt;
|
|
4321
|
+
if (groundFireClock < GROUND_FIRE_EVERY) return;
|
|
4322
|
+
groundFireClock = 0;
|
|
4323
|
+
for (const [x, z] of terrain.burningPoints()) {
|
|
4324
|
+
const y = terrain.heightAt(x, z);
|
|
4325
|
+
renderer.emit("fire", [x, y + 0.05, z], [0, 1, 0], 3, 1.6);
|
|
4326
|
+
if (rnd() < 0.25) renderer.emit("smoke", [x, y + 0.2, z], [0, 1, 0], 1, 1);
|
|
4327
|
+
}
|
|
4328
|
+
};
|
|
3962
4329
|
const fluidView = createFluidRenderer(THREE, { scene, fluids, quality, pointScale, vfx: vfx.world });
|
|
3963
4330
|
const filmRandom = createFilmLcg(1);
|
|
3964
4331
|
const film = createFilmManager(THREE, {
|
|
@@ -4093,12 +4460,79 @@ function createTestRoom(options) {
|
|
|
4093
4460
|
let useHeld = false;
|
|
4094
4461
|
let useFirst = false;
|
|
4095
4462
|
const blockers = /* @__PURE__ */ new Set();
|
|
4463
|
+
const drawnScenery = (i) => {
|
|
4464
|
+
const c = arena.colliders[i];
|
|
4465
|
+
return arena.colliderIds[i] !== null && c[2] - c[0] <= SOLID_SPAN && c[3] - c[1] <= SOLID_SPAN;
|
|
4466
|
+
};
|
|
4467
|
+
const standingColliders = [];
|
|
4468
|
+
const standingIds = [];
|
|
4469
|
+
const footprintsWithBody = /* @__PURE__ */ new Set();
|
|
4470
|
+
const standFootprints = () => {
|
|
4471
|
+
standingColliders.length = 0;
|
|
4472
|
+
standingIds.length = 0;
|
|
4473
|
+
footprintsWithBody.clear();
|
|
4474
|
+
arena.colliders.forEach((collider, i) => {
|
|
4475
|
+
if (drawnScenery(i)) return;
|
|
4476
|
+
standingColliders.push(collider);
|
|
4477
|
+
standingIds.push(arena.colliderIds[i] ?? null);
|
|
4478
|
+
});
|
|
4479
|
+
};
|
|
4480
|
+
standFootprints();
|
|
4481
|
+
const leaveFootprint = (objectId) => {
|
|
4482
|
+
const i = standingIds.indexOf(objectId);
|
|
4483
|
+
if (i < 0) return;
|
|
4484
|
+
standingIds.splice(i, 1);
|
|
4485
|
+
standingColliders.splice(i, 1);
|
|
4486
|
+
footprintsWithBody.add(objectId);
|
|
4487
|
+
};
|
|
4488
|
+
const footing = (x, z, radius, feet) => {
|
|
4489
|
+
let floor = -Infinity;
|
|
4490
|
+
let blocked = false;
|
|
4491
|
+
for (const s of renderer.solidsNear(x, z, radius)) {
|
|
4492
|
+
if ((s.piece?.mass ?? s.loose?.mass ?? Infinity) < KICK_MASS) continue;
|
|
4493
|
+
const dx = Math.max(s.min[0] - x, 0, x - s.max[0]);
|
|
4494
|
+
const dz = Math.max(s.min[2] - z, 0, z - s.max[2]);
|
|
4495
|
+
if (dx * dx + dz * dz > radius * radius) continue;
|
|
4496
|
+
if (s.max[1] <= feet + STEP_UP) floor = Math.max(floor, s.max[1]);
|
|
4497
|
+
else if (s.min[1] < feet + BODY_HEIGHT) blocked = true;
|
|
4498
|
+
}
|
|
4499
|
+
return { floor, blocked };
|
|
4500
|
+
};
|
|
4501
|
+
const kickFrom = { x: 0, z: 0 };
|
|
4502
|
+
const kickLight = (dt) => {
|
|
4503
|
+
const x = player.x;
|
|
4504
|
+
const z = player.z;
|
|
4505
|
+
const speed = dt > 0 ? Math.hypot(x - kickFrom.x, z - kickFrom.z) / dt : 0;
|
|
4506
|
+
kickFrom.x = x;
|
|
4507
|
+
kickFrom.z = z;
|
|
4508
|
+
if (speed < 0.2 || speed > 20) return;
|
|
4509
|
+
const kicked = /* @__PURE__ */ new Set();
|
|
4510
|
+
for (const s of renderer.solidsNear(x, z, PLAYER_RADIUS)) {
|
|
4511
|
+
const pc = s.piece;
|
|
4512
|
+
const body = pc ?? s.loose;
|
|
4513
|
+
if (!body || body.mass >= KICK_MASS || kicked.has(body) || s.min[1] > player.feet + KICK_HEIGHT) continue;
|
|
4514
|
+
kicked.add(body);
|
|
4515
|
+
const cx = (s.min[0] + s.max[0]) / 2 - x;
|
|
4516
|
+
const cz = (s.min[2] + s.max[2]) / 2 - z;
|
|
4517
|
+
const d = Math.hypot(cx, cz) || 1;
|
|
4518
|
+
const v = speed * 1.2;
|
|
4519
|
+
if (!pc) {
|
|
4520
|
+
s.loose?.push(cx / d * v, cz / d * v);
|
|
4521
|
+
continue;
|
|
4522
|
+
}
|
|
4523
|
+
if (pc.velocity[0] * cx + pc.velocity[2] * cz >= v * d) continue;
|
|
4524
|
+
pc.velocity = [cx / d * v, Math.max(pc.velocity[1], 0.6), cz / d * v];
|
|
4525
|
+
pc.sleeping = false;
|
|
4526
|
+
}
|
|
4527
|
+
};
|
|
4096
4528
|
const player = createPlayer(THREE, {
|
|
4097
4529
|
camera,
|
|
4098
4530
|
dom,
|
|
4099
|
-
colliders:
|
|
4531
|
+
colliders: standingColliders,
|
|
4532
|
+
footing: (x, z, radius, feet) => footing(x, z, radius, feet),
|
|
4100
4533
|
blockers,
|
|
4101
4534
|
limit: arenaSize,
|
|
4535
|
+
floorAt: terrain.heightAt,
|
|
4102
4536
|
start: arena.playerStart,
|
|
4103
4537
|
crouchKeys: options.crouch?.keys,
|
|
4104
4538
|
hooks: {
|
|
@@ -4122,6 +4556,68 @@ function createTestRoom(options) {
|
|
|
4122
4556
|
let counter = 0;
|
|
4123
4557
|
let slot = 0;
|
|
4124
4558
|
const maxCarry = options.maxCarryKg ?? 60;
|
|
4559
|
+
const PUSH_AHEAD = 0.6;
|
|
4560
|
+
const heaviestMaterial = (id) => {
|
|
4561
|
+
let best;
|
|
4562
|
+
for (const p of world.state(id)?.parts ?? []) if (p.status !== "broken" && (!best || p.mass > best.mass)) best = { mass: p.mass, material: p.material };
|
|
4563
|
+
return best?.material;
|
|
4564
|
+
};
|
|
4565
|
+
const leanedOn = () => {
|
|
4566
|
+
const fx = -Math.sin(player.yaw);
|
|
4567
|
+
const fz = -Math.cos(player.yaw);
|
|
4568
|
+
let best = null;
|
|
4569
|
+
let bestDistance = PUSH_REACH;
|
|
4570
|
+
for (const e of entries.values()) {
|
|
4571
|
+
if (e.arena || !e.piece && !e.free) continue;
|
|
4572
|
+
const b = bodyBox(e);
|
|
4573
|
+
if (!b) continue;
|
|
4574
|
+
const dx = Math.max(b.minX, Math.min(b.maxX, player.x)) - player.x;
|
|
4575
|
+
const dz = Math.max(b.minZ, Math.min(b.maxZ, player.z)) - player.z;
|
|
4576
|
+
const distance = Math.hypot(dx, dz);
|
|
4577
|
+
if (distance >= bestDistance || distance > 1e-3 && (dx * fx + dz * fz) / distance < PUSH_AHEAD) continue;
|
|
4578
|
+
best = e;
|
|
4579
|
+
bestDistance = distance;
|
|
4580
|
+
}
|
|
4581
|
+
return best ? { objectId: best.so.objectId, root: best.so.root, name: nameOf(best.so.objectId), piece: best.piece ?? null } : null;
|
|
4582
|
+
};
|
|
4583
|
+
const push = createRoomPush({
|
|
4584
|
+
THREE,
|
|
4585
|
+
player,
|
|
4586
|
+
world,
|
|
4587
|
+
arena: { limit: arenaSize, colliders: standingColliders },
|
|
4588
|
+
blockers,
|
|
4589
|
+
...options.push ? { rules: options.push } : {},
|
|
4590
|
+
pick: () => {
|
|
4591
|
+
if (heldPiece || tool.def.category !== "hand" && toolId !== "fists") return null;
|
|
4592
|
+
aim();
|
|
4593
|
+
const r = rayWorld([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], 3.2);
|
|
4594
|
+
const aimed = r ? rendererPieces.get(r.object)?.root ?? (entries.get(r.object)?.free ? entries.get(r.object)?.so.root : void 0) : void 0;
|
|
4595
|
+
if (r && aimed) return { objectId: r.object, root: aimed, name: nameOf(r.object), piece: rendererPieces.get(r.object) ?? null };
|
|
4596
|
+
return leanedOn();
|
|
4597
|
+
},
|
|
4598
|
+
massOf: (objectId) => massOfObject(objectId),
|
|
4599
|
+
materialOf: heaviestMaterial,
|
|
4600
|
+
makePiece: (objectId) => {
|
|
4601
|
+
const piece = renderer.makePiece(objectId, { mass: Math.max(0.2, massOfObject(objectId)) });
|
|
4602
|
+
const e = entries.get(objectId);
|
|
4603
|
+
if (e) e.piece = piece;
|
|
4604
|
+
rendererPieces.set(objectId, piece);
|
|
4605
|
+
leaveFootprint(objectId);
|
|
4606
|
+
return piece;
|
|
4607
|
+
},
|
|
4608
|
+
dragSound: (objectId, point, volume) => playSound(dragSoundKey(world.materials.get(heaviestMaterial(objectId) ?? "")), point, volume),
|
|
4609
|
+
hitSound: (objectId, point) => playSound(impactKey(world.materials.get(heaviestMaterial(objectId) ?? "")), point, 0.5),
|
|
4610
|
+
onEvent: (ev) => {
|
|
4611
|
+
const vars = { name: ev.name, kg: num(ev.massKg, 0) };
|
|
4612
|
+
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 } });
|
|
4613
|
+
fire("push", ev);
|
|
4614
|
+
if (ev.type === "engaged") say(t("hud.push_on", vars));
|
|
4615
|
+
else if (ev.type === "too_heavy") say(t("hud.push_too_heavy", vars));
|
|
4616
|
+
else if (ev.type === "obstacle") say(t("hud.push_blocked", vars));
|
|
4617
|
+
},
|
|
4618
|
+
ring: (value) => ui?.holdRing(value)
|
|
4619
|
+
});
|
|
4620
|
+
let pushByKey = false;
|
|
4125
4621
|
const massOfObject = (id) => {
|
|
4126
4622
|
const st = world.state(id);
|
|
4127
4623
|
let m = 0;
|
|
@@ -4189,6 +4685,45 @@ function createTestRoom(options) {
|
|
|
4189
4685
|
Object.defineProperty(holder, "visible", { get: () => built.visible, set: (v) => void (built.visible = v), configurable: true });
|
|
4190
4686
|
return holder;
|
|
4191
4687
|
};
|
|
4688
|
+
const sceneryObjects = /* @__PURE__ */ new Map();
|
|
4689
|
+
const placeScenery = (objectId, item, position, rotationY) => {
|
|
4690
|
+
const root = buildRoot(item);
|
|
4691
|
+
root.position.set(position[0], position[1], position[2]);
|
|
4692
|
+
root.rotation.y = rotationY;
|
|
4693
|
+
root.name = item.id;
|
|
4694
|
+
scene.add(root);
|
|
4695
|
+
root.updateMatrixWorld(true);
|
|
4696
|
+
const own = item.impl?.(root, objectId);
|
|
4697
|
+
const { spec, parts } = own ? { spec: structuredCloneSpec(own.describe()), parts: void 0 } : resolveSpec(item, root);
|
|
4698
|
+
root.updateMatrixWorld(true);
|
|
4699
|
+
const q = root.getWorldQuaternion(tmpQ);
|
|
4700
|
+
if (own) world.register(own, { id: objectId, position, rotation: [q.x, q.y, q.z, q.w] });
|
|
4701
|
+
else world.add(spec, { id: objectId, position, rotation: [q.x, q.y, q.z, q.w] });
|
|
4702
|
+
const e = registerEntry(objectId, item.id, root, spec, item.name, parts, item, true, false);
|
|
4703
|
+
fire("spawn", e.so);
|
|
4704
|
+
return e;
|
|
4705
|
+
};
|
|
4706
|
+
const restoreScenery = () => {
|
|
4707
|
+
for (const [objectId, o] of sceneryObjects) if (!entries.has(objectId)) placeScenery(objectId, o.item, o.position, o.rotationY);
|
|
4708
|
+
};
|
|
4709
|
+
const addScenery = (itemId, position, rotationY = 0) => {
|
|
4710
|
+
const item = catalogMap.get(itemId);
|
|
4711
|
+
if (!item) return null;
|
|
4712
|
+
const objectId = `${item.id}_scenery_${sceneryObjects.size + 1}`;
|
|
4713
|
+
sceneryObjects.set(objectId, { item, position: [position[0], position[1], position[2]], rotationY });
|
|
4714
|
+
const e = placeScenery(objectId, item, position, rotationY);
|
|
4715
|
+
action("spawn", t("act.spawn", { name: item.name, x: num(position[0], 1), z: num(position[2], 1) }), { objectId, itemId: item.id, data: { position, movable: false, scenery: true } });
|
|
4716
|
+
return {
|
|
4717
|
+
objectId,
|
|
4718
|
+
get root() {
|
|
4719
|
+
return entries.get(objectId)?.so.root ?? e.so.root;
|
|
4720
|
+
},
|
|
4721
|
+
remove() {
|
|
4722
|
+
sceneryObjects.delete(objectId);
|
|
4723
|
+
removeEntry(objectId);
|
|
4724
|
+
}
|
|
4725
|
+
};
|
|
4726
|
+
};
|
|
4192
4727
|
const spawn = (itemId, position, rotationY) => {
|
|
4193
4728
|
const item = catalogMap.get(itemId);
|
|
4194
4729
|
if (!item) throw new Error(`game-test-room: unknown item "${itemId}"`);
|
|
@@ -4240,6 +4775,77 @@ function createTestRoom(options) {
|
|
|
4240
4775
|
fire("spawn", e.so);
|
|
4241
4776
|
return e.so;
|
|
4242
4777
|
};
|
|
4778
|
+
const STEP_OVER = STEP_UP;
|
|
4779
|
+
const HEADROOM = 1.9;
|
|
4780
|
+
const PUSH_WALK_N = 250;
|
|
4781
|
+
const PUSH_RUN_N = 450;
|
|
4782
|
+
const FLOOR_FRICTION = 0.5;
|
|
4783
|
+
const bodyBoxes = /* @__PURE__ */ new Map();
|
|
4784
|
+
const bodyBounds = new THREE.Box3();
|
|
4785
|
+
let frameStamp = 0;
|
|
4786
|
+
const bodyBox = (e) => {
|
|
4787
|
+
const id = e.so.objectId;
|
|
4788
|
+
const known = bodyBoxes.get(id);
|
|
4789
|
+
if (known && (known.stamp === frameStamp || !e.piece || e.piece.sleeping)) return known;
|
|
4790
|
+
if (!e.so.root.parent || world.state(id)?.destroyed) {
|
|
4791
|
+
bodyBoxes.delete(id);
|
|
4792
|
+
return null;
|
|
4793
|
+
}
|
|
4794
|
+
bodyBounds.setFromObject(e.so.root);
|
|
4795
|
+
if (bodyBounds.isEmpty()) return null;
|
|
4796
|
+
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 };
|
|
4797
|
+
bodyBoxes.set(id, box2);
|
|
4798
|
+
return box2;
|
|
4799
|
+
};
|
|
4800
|
+
const footPos = new THREE.Vector3();
|
|
4801
|
+
const inTheWay = (e, x, z, r) => {
|
|
4802
|
+
if (e.arena && !footprintsWithBody.has(e.so.objectId) || e.piece && e.piece.held) return null;
|
|
4803
|
+
const ud = e.so.root.userData;
|
|
4804
|
+
if ("collider" in ud && !ud.collider?.kind) return null;
|
|
4805
|
+
const b = bodyBox(e);
|
|
4806
|
+
if (!b || b.base > player.feet + HEADROOM) return null;
|
|
4807
|
+
const c = ud.collider;
|
|
4808
|
+
if (c?.kind === "cylinder" && c.radius != null) {
|
|
4809
|
+
e.so.root.getWorldPosition(footPos);
|
|
4810
|
+
const reach = c.radius + r;
|
|
4811
|
+
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;
|
|
4812
|
+
}
|
|
4813
|
+
return x > b.minX - r && x < b.maxX + r && z > b.minZ - r && z < b.maxZ + r ? b : null;
|
|
4814
|
+
};
|
|
4815
|
+
blockers.add((x, z, r) => {
|
|
4816
|
+
for (const e of entries.values()) {
|
|
4817
|
+
const b = inTheWay(e, x, z, r);
|
|
4818
|
+
if (b && b.top - b.base >= STEP_OVER) return true;
|
|
4819
|
+
}
|
|
4820
|
+
return false;
|
|
4821
|
+
});
|
|
4822
|
+
const pushByPlayer = (dt) => {
|
|
4823
|
+
const [wx, wz] = player.takeWish();
|
|
4824
|
+
const len = Math.hypot(wx, wz);
|
|
4825
|
+
if (dt <= 0 || len < 1e-6) return;
|
|
4826
|
+
const dx = wx / len;
|
|
4827
|
+
const dz = wz / len;
|
|
4828
|
+
const speed = len / dt;
|
|
4829
|
+
const force = player.running ? PUSH_RUN_N : PUSH_WALK_N;
|
|
4830
|
+
const ax = player.x + dx * (Math.min(len, 0.5) + 0.03);
|
|
4831
|
+
const az = player.z + dz * (Math.min(len, 0.5) + 0.03);
|
|
4832
|
+
for (const e of entries.values()) {
|
|
4833
|
+
const piece = e.piece;
|
|
4834
|
+
if (!piece) continue;
|
|
4835
|
+
const b = inTheWay(e, ax, az, PLAYER_RADIUS);
|
|
4836
|
+
if (!b) continue;
|
|
4837
|
+
const grip = FLOOR_FRICTION * piece.mass * 9.81;
|
|
4838
|
+
if (force <= grip) continue;
|
|
4839
|
+
const along = piece.velocity[0] * dx + piece.velocity[2] * dz;
|
|
4840
|
+
const impulse = Math.min(piece.mass * Math.max(0, speed - along), (force - grip) * dt);
|
|
4841
|
+
if (impulse <= 1e-4) continue;
|
|
4842
|
+
const y = (b.base + Math.max(b.base, Math.min(b.top, player.feet + 1.4))) / 2;
|
|
4843
|
+
const point = [Math.max(b.minX, Math.min(b.maxX, player.x)), y, Math.max(b.minZ, Math.min(b.maxZ, player.z))];
|
|
4844
|
+
piece.sleeping = false;
|
|
4845
|
+
renderer.impulse(e.so.objectId, [dx * impulse, 0, dz * impulse], point);
|
|
4846
|
+
log.coalesce(`bump:${e.so.objectId}`, { channel: "action", type: "bump", text: t("act.bump", { name: e.name, kg: num(piece.mass, 1) }), objectId: e.so.objectId, itemId: e.so.itemId, data: { massKg: Math.round(piece.mass * 10) / 10, pushN: force } }, 1);
|
|
4847
|
+
}
|
|
4848
|
+
};
|
|
4243
4849
|
const massGuess = (spec) => {
|
|
4244
4850
|
let m = 0;
|
|
4245
4851
|
for (const p of spec.parts) {
|
|
@@ -4292,6 +4898,7 @@ function createTestRoom(options) {
|
|
|
4292
4898
|
explosives.clear();
|
|
4293
4899
|
fireZones.length = 0;
|
|
4294
4900
|
burningStains.length = 0;
|
|
4901
|
+
terrain.setLayout(defaultGround);
|
|
4295
4902
|
for (const id of [...entries.keys()]) if (!entries.get(id)?.arena) removeEntry(id);
|
|
4296
4903
|
clearLoose();
|
|
4297
4904
|
for (const e of entries.values()) {
|
|
@@ -4329,6 +4936,8 @@ function createTestRoom(options) {
|
|
|
4329
4936
|
action("reset", t("act.reset"));
|
|
4330
4937
|
};
|
|
4331
4938
|
const hardReset = () => {
|
|
4939
|
+
pushByKey = false;
|
|
4940
|
+
push.end();
|
|
4332
4941
|
clearEverything();
|
|
4333
4942
|
for (const id of [...entries.keys()]) removeEntry(id);
|
|
4334
4943
|
tools.clear();
|
|
@@ -4336,6 +4945,7 @@ function createTestRoom(options) {
|
|
|
4336
4945
|
if (pristineWorld) {
|
|
4337
4946
|
for (const id of world.ids()) world.remove(id);
|
|
4338
4947
|
world.load(pristineWorld);
|
|
4948
|
+
registerGround();
|
|
4339
4949
|
}
|
|
4340
4950
|
if (pristineFluids) {
|
|
4341
4951
|
fluids.load(pristineFluids);
|
|
@@ -4349,6 +4959,7 @@ function createTestRoom(options) {
|
|
|
4349
4959
|
prevVy.clear();
|
|
4350
4960
|
lastImpact.clear();
|
|
4351
4961
|
addArena();
|
|
4962
|
+
standFootprints();
|
|
4352
4963
|
timeScale = 1;
|
|
4353
4964
|
fastForward = false;
|
|
4354
4965
|
walkPlan = null;
|
|
@@ -4413,10 +5024,11 @@ function createTestRoom(options) {
|
|
|
4413
5024
|
}
|
|
4414
5025
|
feetPrev = [x, z];
|
|
4415
5026
|
avatarState.running = !player.crouching && (player.keys.has("ShiftLeft") || player.keys.has("ShiftRight"));
|
|
4416
|
-
avatarState.crouch = crouchForced || player.crouching;
|
|
5027
|
+
avatarState.crouch = crouchForced || player.crouching || push.isBent;
|
|
4417
5028
|
avatarState.armed = armedForced ?? tool.def.action.kind === "firearm";
|
|
4418
5029
|
avatarState.pitch = player.pitch;
|
|
4419
5030
|
avatarState.holding = heldPiece !== null;
|
|
5031
|
+
avatarState.push = push.avatarState;
|
|
4420
5032
|
avatarState.itemUse = useItemPlayer ? { id: useItemName, ...useItemPlayer.pose() } : heldPiece && heldTilt > 0.01 ? { id: "tilt", position: [0.2, -0.1, -0.38], rotation: [-heldTilt, 0, 0] } : null;
|
|
4421
5033
|
avatar.root.position.set(x, player.feet, z);
|
|
4422
5034
|
avatar.root.rotation.set(0, player.yaw + Math.PI, 0);
|
|
@@ -4432,11 +5044,11 @@ function createTestRoom(options) {
|
|
|
4432
5044
|
const stepFirstPerson = (dt) => {
|
|
4433
5045
|
const arms = avatar?.firstPerson;
|
|
4434
5046
|
if (!arms) return;
|
|
4435
|
-
const targets = handSource?.() ?? carriedHand;
|
|
5047
|
+
const targets = push.handTargets() ?? handSource?.() ?? carriedHand;
|
|
4436
5048
|
const state = {
|
|
4437
5049
|
camera,
|
|
4438
5050
|
targets,
|
|
4439
|
-
stance: handStances[toolId] ?? (targets ? "hold" : "relaxed"),
|
|
5051
|
+
stance: push.active ? "hold" : handStances[toolId] ?? (targets ? "hold" : "relaxed"),
|
|
4440
5052
|
speed: avatarState.speed,
|
|
4441
5053
|
running: avatarState.running,
|
|
4442
5054
|
crouch: avatarState.crouch,
|
|
@@ -4457,6 +5069,19 @@ function createTestRoom(options) {
|
|
|
4457
5069
|
camera.updateMatrixWorld(true);
|
|
4458
5070
|
};
|
|
4459
5071
|
};
|
|
5072
|
+
const KICK_HIP = 0.9;
|
|
5073
|
+
const KICK_DROP = 0.35;
|
|
5074
|
+
const legAim = () => {
|
|
5075
|
+
aim();
|
|
5076
|
+
const low = aimO[1] < player.feet + KICK_HIP;
|
|
5077
|
+
const pitch = Math.max(-0.9, Math.min(0.1, player.pitch - (low ? 0 : KICK_DROP)));
|
|
5078
|
+
aimO[0] = player.x;
|
|
5079
|
+
aimO[1] = Math.min(player.feet + KICK_HIP, aimO[1]);
|
|
5080
|
+
aimO[2] = player.z;
|
|
5081
|
+
aimD[0] = -Math.sin(player.yaw) * Math.cos(pitch);
|
|
5082
|
+
aimD[1] = Math.sin(pitch);
|
|
5083
|
+
aimD[2] = -Math.cos(player.yaw) * Math.cos(pitch);
|
|
5084
|
+
};
|
|
4460
5085
|
const aim = () => {
|
|
4461
5086
|
camera.updateMatrixWorld(true);
|
|
4462
5087
|
camera.getWorldPosition(tmpV);
|
|
@@ -4623,17 +5248,51 @@ function createTestRoom(options) {
|
|
|
4623
5248
|
if (!rendererPieces.has(objectId)) return;
|
|
4624
5249
|
renderer.impulse(objectId, [dir[0] * momentum, dir[1] * momentum, dir[2] * momentum], point);
|
|
4625
5250
|
};
|
|
5251
|
+
const shotRay = new THREE.Raycaster();
|
|
5252
|
+
const LIQUID_SLOWING = 0.25;
|
|
5253
|
+
const SHOT_GRIP = 0.3;
|
|
5254
|
+
const shotPush = (s, objectId, results, fallback) => {
|
|
5255
|
+
const e = entries.get(objectId);
|
|
5256
|
+
const speed = Math.max(80, s.speed);
|
|
5257
|
+
const onIt = results.filter((r) => r.object === objectId);
|
|
5258
|
+
const last = onIt[onIt.length - 1];
|
|
5259
|
+
let kept = last && last.penetrated ? Math.sqrt(Math.max(0, Math.min(1, last.exitEnergy / Math.max(s.energy, 1e-6)))) : 0;
|
|
5260
|
+
let point = fallback;
|
|
5261
|
+
let inward = [s.direction[0], s.direction[1], s.direction[2]];
|
|
5262
|
+
if (e) {
|
|
5263
|
+
shotRay.set(new THREE.Vector3(...s.origin), new THREE.Vector3(...s.direction).normalize());
|
|
5264
|
+
shotRay.far = 60;
|
|
5265
|
+
shotRay.camera = camera;
|
|
5266
|
+
const hit = shotRay.intersectObject(e.so.root, true).find((h) => h.object.isMesh && !h.object.userData.destructionDebris && h.face);
|
|
5267
|
+
if (hit?.face) {
|
|
5268
|
+
point = [hit.point.x, hit.point.y, hit.point.z];
|
|
5269
|
+
const n = hit.face.normal.clone().transformDirection(hit.object.matrixWorld);
|
|
5270
|
+
if (n.x * s.direction[0] + n.y * s.direction[1] + n.z * s.direction[2] > 0) n.negate();
|
|
5271
|
+
inward = [-n.x, -n.y, -n.z];
|
|
5272
|
+
}
|
|
5273
|
+
const c = kept > 0 && e.containerId ? fluids.container(e.containerId) : null;
|
|
5274
|
+
if (c && c.liters > 1e-4) {
|
|
5275
|
+
const size = new THREE.Box3().setFromObject(e.so.root).getSize(new THREE.Vector3());
|
|
5276
|
+
kept *= Math.exp(-(Math.min(size.x, size.z) * 0.9) / LIQUID_SLOWING);
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5279
|
+
const momentum = 2 * s.energy / speed * (1 - kept);
|
|
5280
|
+
const along = s.direction[0] * inward[0] + s.direction[1] * inward[1] + s.direction[2] * inward[2];
|
|
5281
|
+
const impulse = [0, 1, 2].map((k) => momentum * (along * inward[k] + SHOT_GRIP * (s.direction[k] - along * inward[k])));
|
|
5282
|
+
return { impulse, point };
|
|
5283
|
+
};
|
|
4626
5284
|
const shoveFree = (pushes) => {
|
|
4627
|
-
for (const [objectId,
|
|
5285
|
+
for (const [objectId, push2] of pushes) {
|
|
4628
5286
|
const e = entries.get(objectId);
|
|
4629
5287
|
if (!e || e.piece || !e.free) continue;
|
|
4630
5288
|
const mass = Math.max(0.2, massOfObject(objectId));
|
|
4631
|
-
if (Math.hypot(...
|
|
5289
|
+
if (Math.hypot(...push2.impulse) / mass < SHOVE_SPEED) continue;
|
|
4632
5290
|
const piece = renderer.makePiece(objectId, { mass });
|
|
4633
5291
|
e.piece = piece;
|
|
4634
5292
|
rendererPieces.set(objectId, piece);
|
|
5293
|
+
leaveFootprint(objectId);
|
|
4635
5294
|
action("loosened", t("act.shoved", { name: e.name }), { objectId, itemId: e.so.itemId });
|
|
4636
|
-
renderer.impulse(objectId,
|
|
5295
|
+
renderer.impulse(objectId, push2.impulse, push2.point);
|
|
4637
5296
|
}
|
|
4638
5297
|
};
|
|
4639
5298
|
const fireShots = (shots, recoil) => {
|
|
@@ -4656,12 +5315,11 @@ function createTestRoom(options) {
|
|
|
4656
5315
|
hits++;
|
|
4657
5316
|
end = r0.point;
|
|
4658
5317
|
if (!first) first = { object: r0.object, part: r0.part, point: r0.point, normal: [0, 1, 0], distance: dist3(s.origin, r0.point) };
|
|
4659
|
-
const
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
pushes.set(r0.object, push);
|
|
5318
|
+
const blow = shotPush(s, r0.object, results, r0.point);
|
|
5319
|
+
if (rendererPieces.has(r0.object)) renderer.impulse(r0.object, blow.impulse, blow.point);
|
|
5320
|
+
const push2 = pushes.get(r0.object) ?? { impulse: [0, 0, 0], point: blow.point };
|
|
5321
|
+
for (let k = 0; k < 3; k++) push2.impulse[k] += blow.impulse[k];
|
|
5322
|
+
pushes.set(r0.object, push2);
|
|
4665
5323
|
}
|
|
4666
5324
|
if (shots.length <= 3 || rnd() < 0.34) tracer(m, end);
|
|
4667
5325
|
}
|
|
@@ -4739,8 +5397,22 @@ function createTestRoom(options) {
|
|
|
4739
5397
|
1
|
|
4740
5398
|
);
|
|
4741
5399
|
};
|
|
4742
|
-
const
|
|
5400
|
+
const kickHeights = options.kickHeights ?? [0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85];
|
|
5401
|
+
const footRay = (u) => {
|
|
5402
|
+
const flat = Math.hypot(u.direction[0], u.direction[2]);
|
|
5403
|
+
const direction = flat > 1e-6 ? [u.direction[0] / flat, 0, u.direction[2] / flat] : [-Math.sin(player.yaw), 0, -Math.cos(player.yaw)];
|
|
5404
|
+
const feet = player.position;
|
|
5405
|
+
let best = null;
|
|
5406
|
+
for (const height2 of kickHeights) {
|
|
5407
|
+
const origin = [feet[0], feet[1] + height2, feet[2]];
|
|
5408
|
+
const hit = rayWorld(origin, direction, u.reach);
|
|
5409
|
+
if (hit && (!best || hit.distance < best.distance)) best = { origin, distance: hit.distance };
|
|
5410
|
+
}
|
|
5411
|
+
return best ? { ...u, origin: best.origin, direction } : u;
|
|
5412
|
+
};
|
|
5413
|
+
const meleeUse = (use, id = toolId) => {
|
|
4743
5414
|
const w = toolName(id);
|
|
5415
|
+
const u = id === "kick" ? footRay(use) : use;
|
|
4744
5416
|
const r = rayWorld(u.origin, u.direction, u.reach);
|
|
4745
5417
|
if (!r) {
|
|
4746
5418
|
action("melee", t("act.melee.miss", { weapon: w }), { data: { weapon: id } });
|
|
@@ -4761,27 +5433,49 @@ function createTestRoom(options) {
|
|
|
4761
5433
|
const punchHand = options.punchHand ?? "alternate";
|
|
4762
5434
|
const punchTools = new Set(options.punchTools ?? ["hand", "fists"]);
|
|
4763
5435
|
let nextHand = "right";
|
|
5436
|
+
const combo = options.strikeCombo;
|
|
5437
|
+
let comboOn = !!combo && options.strikeComboOn === true;
|
|
5438
|
+
let comboNext = 0;
|
|
5439
|
+
let comboAt = -Infinity;
|
|
5440
|
+
const setStrikeCombo = (on) => {
|
|
5441
|
+
if (!combo || on === comboOn) return;
|
|
5442
|
+
comboOn = on;
|
|
5443
|
+
comboNext = 0;
|
|
5444
|
+
const name = combo.name?.[locale] ?? combo.id;
|
|
5445
|
+
action("combo", t(on ? "act.combo.on" : "act.combo.off", { name }), { data: { combo: combo.id, on } });
|
|
5446
|
+
say(t(on ? "act.combo.on" : "act.combo.off", { name }));
|
|
5447
|
+
};
|
|
4764
5448
|
const strikeUse = (u, id = toolId) => {
|
|
4765
|
-
const
|
|
5449
|
+
const stepIndex = comboOn && combo?.tools.includes(id) && combo.steps.length > 0 ? simTime - comboAt > combo.reset ? 0 : comboNext : null;
|
|
5450
|
+
const step2 = stepIndex === null ? void 0 : combo.steps[stepIndex];
|
|
5451
|
+
const clip = step2 ? step2.clip : strikeClips[id];
|
|
4766
5452
|
const leg = id === "kick" ? kickLeg === "alternate" ? nextLeg : kickLeg : void 0;
|
|
4767
|
-
const hand = punchTools.has(id) ? punchHand === "alternate" ? nextHand : punchHand : void 0;
|
|
5453
|
+
const hand = step2 ? step2.hand : punchTools.has(id) ? punchHand === "alternate" ? nextHand : punchHand : void 0;
|
|
4768
5454
|
const side = leg ? { leg } : hand ? { hand } : void 0;
|
|
4769
5455
|
const seconds = clip === void 0 ? null : avatar?.strike?.(clip, side) ?? null;
|
|
4770
5456
|
if (leg && seconds !== null && kickLeg === "alternate") nextLeg = leg === "right" ? "left" : "right";
|
|
4771
|
-
if (hand && seconds !== null && punchHand === "alternate") nextHand = hand === "right" ? "left" : "right";
|
|
5457
|
+
if (!step2 && hand && seconds !== null && punchHand === "alternate") nextHand = hand === "right" ? "left" : "right";
|
|
5458
|
+
if (step2 && stepIndex !== null) {
|
|
5459
|
+
comboNext = (stepIndex + 1) % combo.steps.length;
|
|
5460
|
+
comboAt = simTime;
|
|
5461
|
+
}
|
|
4772
5462
|
if (clip === void 0 || seconds === null) {
|
|
4773
|
-
|
|
5463
|
+
if (leg) {
|
|
5464
|
+
legAim();
|
|
5465
|
+
meleeUse({ ...u, origin: [aimO[0], aimO[1], aimO[2]], direction: [aimD[0], aimD[1], aimD[2]] }, id);
|
|
5466
|
+
} else meleeUse(u, id);
|
|
4774
5467
|
return;
|
|
4775
5468
|
}
|
|
4776
5469
|
const text = leg ? "act.strike.leg" : hand ? "act.strike.hand" : "act.strike";
|
|
4777
5470
|
action("strike", t(text, { weapon: toolName(id), clip, seconds: num(seconds, 2), leg: leg ? t(`leg.${leg}`) : "", hand: hand ? t(`hand.${hand}`) : "" }), {
|
|
4778
|
-
data: { weapon: id, clip, impactIn: seconds, ...leg ? { leg } : {}, ...hand ? { hand } : {} }
|
|
5471
|
+
data: { weapon: id, clip, impactIn: seconds, ...leg ? { leg } : {}, ...hand ? { hand } : {}, ...step2 ? { combo: combo.id, step: stepIndex } : {} }
|
|
4779
5472
|
});
|
|
4780
5473
|
strikes.push({
|
|
4781
5474
|
key: id,
|
|
4782
5475
|
remaining: seconds,
|
|
4783
5476
|
land: () => {
|
|
4784
|
-
|
|
5477
|
+
if (leg) legAim();
|
|
5478
|
+
else aim();
|
|
4785
5479
|
meleeUse({ ...u, origin: [aimO[0], aimO[1], aimO[2]], direction: [aimD[0], aimD[1], aimD[2]] }, id);
|
|
4786
5480
|
}
|
|
4787
5481
|
});
|
|
@@ -4967,12 +5661,14 @@ function createTestRoom(options) {
|
|
|
4967
5661
|
if (res) applyUses(res, 0);
|
|
4968
5662
|
};
|
|
4969
5663
|
const kick = () => {
|
|
4970
|
-
if (!arsenal.weapons.has("kick") || strikes.some((strike) => strike.key === "kick")) return;
|
|
5664
|
+
if (!arsenal.weapons.has("kick") || strikes.some((strike) => strike.key === "kick")) return false;
|
|
4971
5665
|
aim();
|
|
4972
5666
|
const res = toolState("kick").trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], 0, false, "player");
|
|
4973
|
-
if (res.status !== "ok") return;
|
|
5667
|
+
if (res.status !== "ok") return false;
|
|
4974
5668
|
for (const u of res.uses) if (u.kind === "melee") strikeUse(u, "kick");
|
|
5669
|
+
return true;
|
|
4975
5670
|
};
|
|
5671
|
+
const kickKeys = options.kick?.keys ?? ["KeyK"];
|
|
4976
5672
|
const setTool = (id) => {
|
|
4977
5673
|
if (!arsenal.weapons.has(id)) throw new Error(`game-test-room: unknown weapon "${id}"`);
|
|
4978
5674
|
releaseTrigger();
|
|
@@ -5020,6 +5716,7 @@ function createTestRoom(options) {
|
|
|
5020
5716
|
else useTool(FIXED_DT, false);
|
|
5021
5717
|
};
|
|
5022
5718
|
const grab = () => {
|
|
5719
|
+
if (push.active) return false;
|
|
5023
5720
|
if (heldPiece) {
|
|
5024
5721
|
const name = heldName;
|
|
5025
5722
|
releaseHeld();
|
|
@@ -5235,7 +5932,7 @@ function createTestRoom(options) {
|
|
|
5235
5932
|
const reach = Math.max(size.x, size.y, size.z) * 0.5;
|
|
5236
5933
|
const origin = [centre.x + dir[0] * (reach + 0.02), centre.y + dir[1] * (reach + 0.02), centre.z + dir[2] * (reach + 0.02)];
|
|
5237
5934
|
const r = withoutHeld(() => world.raycast(origin, dir, speed * FIXED_DT * 2 + 0.06));
|
|
5238
|
-
if (!r || r.object === piece.objectId) return;
|
|
5935
|
+
if (!r || r.object === piece.objectId || r.object === GROUND_ID) return;
|
|
5239
5936
|
const energy = 0.5 * piece.mass * speed * speed;
|
|
5240
5937
|
lastImpact.set(piece.objectId, simTime);
|
|
5241
5938
|
world.hitPart(r.object, r.part, { type: "blunt", point: r.point, direction: dir, energy, radius: 0.08, source: "thrown" });
|
|
@@ -5248,12 +5945,21 @@ function createTestRoom(options) {
|
|
|
5248
5945
|
data: { energyJ: Math.round(energy), speedMs: Math.round(speed * 10) / 10, target: r.object, targetPart: r.part }
|
|
5249
5946
|
});
|
|
5250
5947
|
};
|
|
5948
|
+
const strikeGroundWith = (piece, energy) => {
|
|
5949
|
+
const box2 = tmpBox.setFromObject(piece.root);
|
|
5950
|
+
const centre = box2.getCenter(tmpV);
|
|
5951
|
+
const size = box2.getSize(tmpV2);
|
|
5952
|
+
if (sceneryTopAt(centre.x, centre.z) > 0 || restingTopAt(centre.x, centre.z) > 0) return;
|
|
5953
|
+
const result = terrain.strike("blunt", { x: centre.x, z: centre.z, energy, contactRadius: Math.max(size.x, size.z) / 2 });
|
|
5954
|
+
if (result.radius > 0) showGroundBlow(result);
|
|
5955
|
+
};
|
|
5251
5956
|
const land = (piece) => {
|
|
5252
5957
|
const vy = piece.velocity[1];
|
|
5253
5958
|
const prev = prevVy.get(piece.objectId) ?? 0;
|
|
5254
5959
|
prevVy.set(piece.objectId, vy);
|
|
5255
5960
|
if (piece.held || prev > -MIN_LANDING_LOG || vy < prev + 2 || !world.has(piece.objectId)) return;
|
|
5256
5961
|
const energy = 0.5 * piece.mass * prev * prev;
|
|
5962
|
+
strikeGroundWith(piece, energy);
|
|
5257
5963
|
action("landing", t("act.landing", { name: nameOf(piece.objectId), speed: num(-prev, 1), energy: num(energy, energy < 100 ? 1 : 0) }), {
|
|
5258
5964
|
objectId: piece.objectId,
|
|
5259
5965
|
itemId: entryOf(piece.objectId)?.so.itemId,
|
|
@@ -5274,6 +5980,10 @@ function createTestRoom(options) {
|
|
|
5274
5980
|
const onKey = (code, down, e) => {
|
|
5275
5981
|
if (!down) {
|
|
5276
5982
|
if (code === "KeyZ") tiltKey = 0;
|
|
5983
|
+
if (code === "KeyE" && pushByKey) {
|
|
5984
|
+
pushByKey = false;
|
|
5985
|
+
if (push.end()) grab();
|
|
5986
|
+
}
|
|
5277
5987
|
return;
|
|
5278
5988
|
}
|
|
5279
5989
|
const digit = /^Digit(\d)$/.exec(code);
|
|
@@ -5284,9 +5994,15 @@ function createTestRoom(options) {
|
|
|
5284
5994
|
if (idx < loadout.length) setTool(loadout[idx]);
|
|
5285
5995
|
return;
|
|
5286
5996
|
}
|
|
5997
|
+
if (kickKeys.includes(code) && !e.ctrlKey && !e.metaKey) {
|
|
5998
|
+
kick();
|
|
5999
|
+
return;
|
|
6000
|
+
}
|
|
5287
6001
|
switch (code) {
|
|
5288
6002
|
case "KeyE":
|
|
5289
|
-
|
|
6003
|
+
if (e.repeat) break;
|
|
6004
|
+
pushByKey = !heldPiece && push.begin();
|
|
6005
|
+
if (!pushByKey) grab();
|
|
5290
6006
|
break;
|
|
5291
6007
|
case "KeyG":
|
|
5292
6008
|
throwHeld();
|
|
@@ -5329,6 +6045,9 @@ function createTestRoom(options) {
|
|
|
5329
6045
|
case "KeyY":
|
|
5330
6046
|
explosives.detonateCharges();
|
|
5331
6047
|
break;
|
|
6048
|
+
case "KeyN":
|
|
6049
|
+
setStrikeCombo(!comboOn);
|
|
6050
|
+
break;
|
|
5332
6051
|
case "KeyH": {
|
|
5333
6052
|
const w = (heldEntry ?? entryOf(inspectTarget()?.object ?? ""))?.item?.weapon;
|
|
5334
6053
|
if (w) equip(w);
|
|
@@ -5457,8 +6176,9 @@ function createTestRoom(options) {
|
|
|
5457
6176
|
}
|
|
5458
6177
|
syncLiters();
|
|
5459
6178
|
renderer.update(dt);
|
|
6179
|
+
terrainView.sync(camera.position);
|
|
6180
|
+
showGroundFire(dt);
|
|
5460
6181
|
vfx.update(dt);
|
|
5461
|
-
craterPatch?.sync();
|
|
5462
6182
|
updateImpacts();
|
|
5463
6183
|
fluidView.update(dt);
|
|
5464
6184
|
filmBridge.update(dt);
|
|
@@ -5482,12 +6202,21 @@ function createTestRoom(options) {
|
|
|
5482
6202
|
};
|
|
5483
6203
|
const update = (dt) => {
|
|
5484
6204
|
if (!ownsLoop) roomMetrics.frame(dt);
|
|
6205
|
+
frameStamp++;
|
|
5485
6206
|
player.update(dt);
|
|
6207
|
+
if (pushByKey && !player.keys.has("KeyE")) {
|
|
6208
|
+
pushByKey = false;
|
|
6209
|
+
push.end();
|
|
6210
|
+
}
|
|
6211
|
+
push.step(paused ? 0 : dt);
|
|
6212
|
+
if (!paused) kickLight(dt);
|
|
6213
|
+
if (!paused) pushByPlayer(dt);
|
|
6214
|
+
else player.takeWish();
|
|
5486
6215
|
stepAvatar(paused ? 0 : dt);
|
|
5487
6216
|
if (tiltKey || heldTilt > 0) {
|
|
5488
6217
|
heldTilt = Math.max(0, Math.min(2.4, heldTilt + (tiltKey ? 2.2 : -3) * dt));
|
|
5489
6218
|
}
|
|
5490
|
-
if ((useHeld || useFirst) && !paused) {
|
|
6219
|
+
if ((useHeld || useFirst) && !paused && !push.active) {
|
|
5491
6220
|
if (tool.def.category === "hand") {
|
|
5492
6221
|
if (useFirst) handUse();
|
|
5493
6222
|
} else if (useHeld) useTool(dt, !useFirst);
|
|
@@ -5529,6 +6258,7 @@ function createTestRoom(options) {
|
|
|
5529
6258
|
timeScale,
|
|
5530
6259
|
fast: fastForward,
|
|
5531
6260
|
tap: isTapOn(),
|
|
6261
|
+
combo: combo ? comboOn : null,
|
|
5532
6262
|
blast: { shelter: blastOn("shelter"), fragmentHits: blastOn("fragmentHits"), confinement: blastOn("confinement") },
|
|
5533
6263
|
quality: qualityName,
|
|
5534
6264
|
camera: avatar ? cameraMode : null,
|
|
@@ -5804,6 +6534,9 @@ function createTestRoom(options) {
|
|
|
5804
6534
|
onClear(listener) {
|
|
5805
6535
|
clearListeners.add(listener);
|
|
5806
6536
|
return () => clearListeners.delete(listener);
|
|
6537
|
+
},
|
|
6538
|
+
addObject(itemId, place) {
|
|
6539
|
+
return addScenery(itemId, place.position, place.rotationY);
|
|
5807
6540
|
}
|
|
5808
6541
|
};
|
|
5809
6542
|
let sendingInitial = false;
|
|
@@ -5958,6 +6691,7 @@ function createTestRoom(options) {
|
|
|
5958
6691
|
const n = Math.max(1, Math.round(seconds / dt));
|
|
5959
6692
|
for (let i = 0; i < n; i++) {
|
|
5960
6693
|
player.update(0);
|
|
6694
|
+
push.step(dt);
|
|
5961
6695
|
followHeld(dt);
|
|
5962
6696
|
stepSim(dt);
|
|
5963
6697
|
}
|
|
@@ -6000,6 +6734,7 @@ function createTestRoom(options) {
|
|
|
6000
6734
|
} finally {
|
|
6001
6735
|
scenarioRunning = false;
|
|
6002
6736
|
paused = wasPaused;
|
|
6737
|
+
restoreScenery();
|
|
6003
6738
|
}
|
|
6004
6739
|
return report;
|
|
6005
6740
|
};
|
|
@@ -6033,6 +6768,7 @@ function createTestRoom(options) {
|
|
|
6033
6768
|
const openArea = (area) => {
|
|
6034
6769
|
clear();
|
|
6035
6770
|
const mod = [...modules.values()].find((m) => m.id === area || m.group === area);
|
|
6771
|
+
if (mod?.ground) terrain.setLayout(mod.ground);
|
|
6036
6772
|
const label = mod?.group ?? S.groups[area] ?? area;
|
|
6037
6773
|
ui?.showGroup(label);
|
|
6038
6774
|
activeBench = mod && benches.has(mod.id) ? mod.id : lastBench();
|
|
@@ -6199,7 +6935,13 @@ function createTestRoom(options) {
|
|
|
6199
6935
|
setFastForward(on) {
|
|
6200
6936
|
fastForward = on;
|
|
6201
6937
|
},
|
|
6938
|
+
setStrikeCombo,
|
|
6939
|
+
kick,
|
|
6940
|
+
get strikeCombo() {
|
|
6941
|
+
return combo ? comboOn : null;
|
|
6942
|
+
},
|
|
6202
6943
|
equip,
|
|
6944
|
+
terrain,
|
|
6203
6945
|
openArea,
|
|
6204
6946
|
modules: () => [...modules.values()].map((m) => ({ id: m.id, name: m.name, group: m.group, hasArea: typeof m.area === "function" })),
|
|
6205
6947
|
openAreaAsync: (area) => loading.busy(t("loading.open_area", { area: areaLabel(area) }), () => openArea(area)),
|
|
@@ -6251,6 +6993,12 @@ function createTestRoom(options) {
|
|
|
6251
6993
|
debugView.updateProjectionMatrix();
|
|
6252
6994
|
debugView.updateMatrixWorld(true);
|
|
6253
6995
|
},
|
|
6996
|
+
get playerPosition() {
|
|
6997
|
+
return player.position;
|
|
6998
|
+
},
|
|
6999
|
+
push: { begin: () => push.begin(), end: () => push.end(), setWalk: (walk) => push.setWalk(walk), get state() {
|
|
7000
|
+
return push.state;
|
|
7001
|
+
} },
|
|
6254
7002
|
setCameraMode,
|
|
6255
7003
|
get cameraMode() {
|
|
6256
7004
|
return cameraMode;
|
|
@@ -6272,7 +7020,6 @@ function createTestRoom(options) {
|
|
|
6272
7020
|
loading.dispose();
|
|
6273
7021
|
renderer.dispose();
|
|
6274
7022
|
vfx.dispose();
|
|
6275
|
-
craterPatch?.dispose();
|
|
6276
7023
|
fluidView.dispose();
|
|
6277
7024
|
filmBridge.dispose();
|
|
6278
7025
|
film.dispose();
|
|
@@ -6350,6 +7097,8 @@ function createTestRoom(options) {
|
|
|
6350
7097
|
setTool(loadout[index]);
|
|
6351
7098
|
},
|
|
6352
7099
|
onTap: () => toggleTap(),
|
|
7100
|
+
...combo ? { combo: { label: combo.name?.[locale] ?? combo.id, onToggle: () => setStrikeCombo(!comboOn) } } : {},
|
|
7101
|
+
...arsenal.weapons.has("kick") ? { kick: { label: `${t("ui.kick")} (${kickKeys.map((key) => key.replace(/^Key/, "")).join("/")})`, onKick: () => void kick() } } : {},
|
|
6353
7102
|
onBlast: (key) => toggleBlast(key),
|
|
6354
7103
|
onPlay: (objectId, clipId) => {
|
|
6355
7104
|
playAnimation(clipId, objectId);
|