@adatechnology/game-test-room 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/index.d.ts +109 -5
- package/dist/index.global.js +3540 -295
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +880 -89
- 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
|
|
@@ -110,7 +110,7 @@ function resolveScenery(scenery) {
|
|
|
110
110
|
if (!position) throw new Error(`game-test-room: scenery "${id}" is new, so it needs a position`);
|
|
111
111
|
merged.set(id, { ...obj, position });
|
|
112
112
|
}
|
|
113
|
-
return [...merged.entries()].map(([id, o]) => ({ id, spec: o.spec, position: o.position, rotationY: o.rotationY ?? 0, colors: o.colors, build: o.build, label: o.label, top: o.top, spout: o.spout }));
|
|
113
|
+
return [...merged.entries()].map(([id, o]) => ({ id, spec: o.spec, position: o.position, rotationY: o.rotationY ?? 0, colors: o.colors, build: o.build, follow: o.follow, label: o.label, top: o.top, spout: o.spout, free: o.free }));
|
|
114
114
|
}
|
|
115
115
|
function createArena(THREE, size, scene, scenery, tapOption) {
|
|
116
116
|
const group = markKitOrigin(new THREE.Group(), { owner: "game-test-room", kind: "room" });
|
|
@@ -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;
|
|
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;
|
|
1922
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" };
|
|
@@ -2346,6 +2622,11 @@ var en = {
|
|
|
2346
2622
|
"act.strike.leg": "{weapon}: {clip} with the {leg} leg begins, impact in {seconds} s",
|
|
2347
2623
|
"leg.left": "left",
|
|
2348
2624
|
"leg.right": "right",
|
|
2625
|
+
"act.strike.hand": "{weapon}: {clip} with the {hand} hand begins, impact in {seconds} s",
|
|
2626
|
+
"hand.left": "left",
|
|
2627
|
+
"hand.right": "right",
|
|
2628
|
+
"act.combo.on": "{name}: on (each blow follows the sequence)",
|
|
2629
|
+
"act.combo.off": "{name}: off",
|
|
2349
2630
|
"act.flame.hit": "{weapon}: flame at {temp} \xB0C on {where}",
|
|
2350
2631
|
"act.flame.air": "{weapon}: flame, nothing in range",
|
|
2351
2632
|
"act.heat.hit": "{weapon}: heating {where} at {temp} \xB0C",
|
|
@@ -2369,14 +2650,17 @@ var en = {
|
|
|
2369
2650
|
"act.extinguish.hit": "{weapon}: foam on {what}",
|
|
2370
2651
|
"act.extinguish.air": "{weapon}: foam, nothing in range",
|
|
2371
2652
|
"act.grab": "Grabbed {name} ({kg} kg)",
|
|
2653
|
+
"act.bump": "Pushed {name} ({kg} kg) walking into it",
|
|
2372
2654
|
"act.drop": "Dropped {name}",
|
|
2373
2655
|
"act.throw": "Threw {name} at {speed} m/s",
|
|
2656
|
+
"ground.name": "Ground",
|
|
2374
2657
|
"act.landing": "{name} landed at {speed} m/s: {energy} J",
|
|
2375
2658
|
"act.bench_failed": "{name} failed: {error}",
|
|
2376
2659
|
"loading.working": "Working\u2026",
|
|
2377
2660
|
"loading.open_area": "Opening {area}\u2026",
|
|
2378
2661
|
"loading.spawn": "Building {name}\u2026",
|
|
2379
2662
|
"act.loosened": "{name} was heavy but free: the blast moved it",
|
|
2663
|
+
"act.shoved": "{name} stands free: the shots moved it",
|
|
2380
2664
|
"act.impact": "{name} hit {where} at {speed} m/s: {energy} J of blunt damage",
|
|
2381
2665
|
"act.use_item": "{name}: {use}",
|
|
2382
2666
|
"act.tap.on": "Tap opened",
|
|
@@ -2403,6 +2687,14 @@ var en = {
|
|
|
2403
2687
|
"hud.nothing_to_grab": "Nothing to grab here",
|
|
2404
2688
|
"hud.cannot_grab": "{name} cannot be picked up",
|
|
2405
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}",
|
|
2406
2698
|
"hud.empty_container": "{name} is empty",
|
|
2407
2699
|
"hud.refused": "Wrong tool for the {kind}",
|
|
2408
2700
|
"hud.drop": "drop",
|
|
@@ -2410,7 +2702,7 @@ var en = {
|
|
|
2410
2702
|
"hud.throw": "throw",
|
|
2411
2703
|
"hud.tilt": "tilt (hold)",
|
|
2412
2704
|
"hud.equip": "equip {weapon}",
|
|
2413
|
-
"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",
|
|
2414
2706
|
"ui.help.camera": "V: 1st/3rd person (Shift+V: kick)",
|
|
2415
2707
|
"camera.first": "1st person",
|
|
2416
2708
|
"camera.third": "3rd person",
|
|
@@ -2457,6 +2749,7 @@ var en = {
|
|
|
2457
2749
|
"ui.slow": "Slow motion 0.25\xD7",
|
|
2458
2750
|
"ui.fast": "Time \xD710",
|
|
2459
2751
|
"ui.tap": "Tap",
|
|
2752
|
+
"ui.kick": "Kick",
|
|
2460
2753
|
"ui.fullscreen": "Full screen",
|
|
2461
2754
|
"ui.fullscreen.hint": "Full screen opened from here keeps the whole keyboard in the game, Ctrl+W included (F11 does not)",
|
|
2462
2755
|
"ui.blast.shelter": "Shelter",
|
|
@@ -2653,6 +2946,11 @@ var ptBR = {
|
|
|
2653
2946
|
"act.strike.leg": "{weapon}: {clip} com a perna {leg} come\xE7a, impacto em {seconds} s",
|
|
2654
2947
|
"leg.left": "esquerda",
|
|
2655
2948
|
"leg.right": "direita",
|
|
2949
|
+
"act.strike.hand": "{weapon}: {clip} com a m\xE3o {hand} come\xE7a, impacto em {seconds} s",
|
|
2950
|
+
"hand.left": "esquerda",
|
|
2951
|
+
"hand.right": "direita",
|
|
2952
|
+
"act.combo.on": "{name}: ligada (cada golpe segue a sequ\xEAncia)",
|
|
2953
|
+
"act.combo.off": "{name}: desligada",
|
|
2656
2954
|
"act.flame.hit": "{weapon}: chama a {temp} \xB0C em {where}",
|
|
2657
2955
|
"act.flame.air": "{weapon}: chama, nada ao alcance",
|
|
2658
2956
|
"act.heat.hit": "{weapon}: esquentando {where} a {temp} \xB0C",
|
|
@@ -2676,14 +2974,17 @@ var ptBR = {
|
|
|
2676
2974
|
"act.extinguish.hit": "{weapon}: espuma em {what}",
|
|
2677
2975
|
"act.extinguish.air": "{weapon}: espuma, nada ao alcance",
|
|
2678
2976
|
"act.grab": "Pegou {name} ({kg} kg)",
|
|
2977
|
+
"act.bump": "Empurrou {name} ({kg} kg) ao esbarrar",
|
|
2679
2978
|
"act.drop": "Soltou {name}",
|
|
2680
2979
|
"act.throw": "Arremessou {name} a {speed} m/s",
|
|
2980
|
+
"ground.name": "Ch\xE3o",
|
|
2681
2981
|
"act.landing": "{name} caiu a {speed} m/s: {energy} J",
|
|
2682
2982
|
"act.bench_failed": "{name} falhou: {error}",
|
|
2683
2983
|
"loading.working": "Trabalhando\u2026",
|
|
2684
2984
|
"loading.open_area": "Abrindo {area}\u2026",
|
|
2685
2985
|
"loading.spawn": "Montando {name}\u2026",
|
|
2686
2986
|
"act.loosened": "{name} \xE9 pesado mas estava solto: a explos\xE3o o moveu",
|
|
2987
|
+
"act.shoved": "{name} estava solto no ch\xE3o: os tiros o moveram",
|
|
2687
2988
|
"act.impact": "{name} bateu em {where} a {speed} m/s: {energy} J de dano de impacto",
|
|
2688
2989
|
"act.use_item": "{name}: {use}",
|
|
2689
2990
|
"act.tap.on": "Abriu a torneira",
|
|
@@ -2710,6 +3011,14 @@ var ptBR = {
|
|
|
2710
3011
|
"hud.nothing_to_grab": "Nada para pegar aqui",
|
|
2711
3012
|
"hud.cannot_grab": "{name} n\xE3o d\xE1 para pegar",
|
|
2712
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}",
|
|
2713
3022
|
"hud.empty_container": "{name} est\xE1 vazio",
|
|
2714
3023
|
"hud.refused": "Ferramenta errada para {kind}",
|
|
2715
3024
|
"hud.drop": "soltar",
|
|
@@ -2717,7 +3026,7 @@ var ptBR = {
|
|
|
2717
3026
|
"hud.throw": "arremessar",
|
|
2718
3027
|
"hud.tilt": "inclinar (segurar)",
|
|
2719
3028
|
"hud.equip": "equipar {weapon}",
|
|
2720
|
-
"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",
|
|
2721
3030
|
"ui.help.camera": "V: 1\xAA/3\xAA pessoa (Shift+V: chute)",
|
|
2722
3031
|
"camera.first": "1\xAA pessoa",
|
|
2723
3032
|
"camera.third": "3\xAA pessoa",
|
|
@@ -2764,6 +3073,7 @@ var ptBR = {
|
|
|
2764
3073
|
"ui.slow": "C\xE2mera lenta 0,25\xD7",
|
|
2765
3074
|
"ui.fast": "Tempo \xD710",
|
|
2766
3075
|
"ui.tap": "Torneira",
|
|
3076
|
+
"ui.kick": "Chute",
|
|
2767
3077
|
"ui.fullscreen": "Tela cheia",
|
|
2768
3078
|
"ui.fullscreen.hint": "A tela cheia aberta por aqui segura o teclado todo no jogo, inclusive Ctrl+W (o F11 n\xE3o)",
|
|
2769
3079
|
"ui.blast.shelter": "Abrigo",
|
|
@@ -3147,7 +3457,7 @@ function createTestTablePanel(table, t) {
|
|
|
3147
3457
|
relay.type = "button";
|
|
3148
3458
|
relay.onclick = () => table.relay();
|
|
3149
3459
|
trials.appendChild(relay);
|
|
3150
|
-
element.append(head, search, found, sub,
|
|
3460
|
+
element.append(head, search, found, sub, trials, list);
|
|
3151
3461
|
const groups = () => {
|
|
3152
3462
|
const keep = select.value;
|
|
3153
3463
|
select.textContent = "";
|
|
@@ -3230,6 +3540,8 @@ var CSS = `
|
|
|
3230
3540
|
.tr-cross{position:absolute;left:50%;top:50%;width:18px;height:18px;margin:-9px 0 0 -9px}
|
|
3231
3541
|
.tr-cross:before,.tr-cross:after{content:"";position:absolute;background:#fff;opacity:.85;box-shadow:0 0 2px #000}
|
|
3232
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}
|
|
3233
3545
|
.tr-cross i{position:absolute;left:6px;top:6px;width:6px;height:6px;background:#0b0c0e;border-radius:50%;z-index:1}
|
|
3234
3546
|
.tr-top{position:absolute;top:8px;left:50%;transform:translateX(-50%);text-align:center;max-width:46vw}
|
|
3235
3547
|
.tr-tool{display:inline-block;background:rgba(10,12,16,.72);padding:4px 12px;border-radius:6px;font-size:14px;font-weight:600}
|
|
@@ -3333,6 +3645,22 @@ function createUi(o) {
|
|
|
3333
3645
|
o.container.appendChild(root);
|
|
3334
3646
|
const cross = mk3("div", "tr-cross");
|
|
3335
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);
|
|
3336
3664
|
const top = mk3("div", "tr-top");
|
|
3337
3665
|
const toolBox = mk3("div", "tr-tool");
|
|
3338
3666
|
const status = mk3("div", "tr-status");
|
|
@@ -3534,6 +3862,8 @@ function createUi(o) {
|
|
|
3534
3862
|
o.onFast(fast);
|
|
3535
3863
|
});
|
|
3536
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());
|
|
3537
3867
|
const fullscreenBtn = ctl(t("ui.fullscreen"), () => void toggleFullscreen());
|
|
3538
3868
|
fullscreenBtn.title = t("ui.fullscreen.hint");
|
|
3539
3869
|
const blastBtns = ["shelter", "fragmentHits", "confinement"].map((key) => ({ key, b: ctl(t(`ui.blast.${key}`), () => o.onBlast(key)) }));
|
|
@@ -3616,7 +3946,7 @@ function createUi(o) {
|
|
|
3616
3946
|
o.onResize();
|
|
3617
3947
|
};
|
|
3618
3948
|
toggle.onclick = togglePanels;
|
|
3619
|
-
root.append(cross, top, bottom, help, left, right, toggle);
|
|
3949
|
+
root.append(cross, ring, top, bottom, help, left, right, toggle);
|
|
3620
3950
|
const bar = (value, max, cls) => {
|
|
3621
3951
|
const b = mk3("div", `tr-bar ${cls}`);
|
|
3622
3952
|
const i = mk3("i");
|
|
@@ -3697,6 +4027,10 @@ function createUi(o) {
|
|
|
3697
4027
|
let lastInspect = "";
|
|
3698
4028
|
let hudPage = 0;
|
|
3699
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
|
+
},
|
|
3700
4034
|
hud(s) {
|
|
3701
4035
|
toolBox.textContent = "";
|
|
3702
4036
|
toolBox.append(document.createTextNode(s.toolName));
|
|
@@ -3745,6 +4079,7 @@ function createUi(o) {
|
|
|
3745
4079
|
}
|
|
3746
4080
|
slotsEl.forEach((b, i) => b.classList.toggle("on", i === s.slot));
|
|
3747
4081
|
tapBtn.classList.toggle("on", s.tap);
|
|
4082
|
+
comboBtn?.classList.toggle("on", !!s.combo);
|
|
3748
4083
|
for (const { key: key2, b } of blastBtns) b.classList.toggle("on", s.blast[key2]);
|
|
3749
4084
|
count.textContent = t("ui.objects", { n: s.objects });
|
|
3750
4085
|
const ak = JSON.stringify(s.animations);
|
|
@@ -3794,6 +4129,19 @@ function createUi(o) {
|
|
|
3794
4129
|
|
|
3795
4130
|
// src/room.ts
|
|
3796
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;
|
|
4144
|
+
var SHOVE_SPEED = 0.04;
|
|
3797
4145
|
var METRICS_INTERVAL_S = 0.25;
|
|
3798
4146
|
var CARRY_NEAR = 0.62;
|
|
3799
4147
|
var CARRY_PALM = 0.012;
|
|
@@ -3853,10 +4201,12 @@ function createTestRoom(options) {
|
|
|
3853
4201
|
const ownWorld = !options.world;
|
|
3854
4202
|
const world = options.world ?? createDestruction({ seed: 1, ...options.worldOptions, materials: [...options.worldOptions?.materials ?? []] });
|
|
3855
4203
|
const pristineWorld = ownWorld ? world.serialize() : null;
|
|
3856
|
-
const
|
|
3857
|
-
const p = world.part(
|
|
3858
|
-
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);
|
|
3859
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);
|
|
3860
4210
|
const restingTops = [];
|
|
3861
4211
|
const restingTopAt = (x, z) => {
|
|
3862
4212
|
let top = 0;
|
|
@@ -3865,10 +4215,18 @@ function createTestRoom(options) {
|
|
|
3865
4215
|
};
|
|
3866
4216
|
const sceneryTopAt = (x, z) => {
|
|
3867
4217
|
let top = 0;
|
|
3868
|
-
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;
|
|
3869
4219
|
return top;
|
|
3870
4220
|
};
|
|
3871
|
-
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
|
+
};
|
|
3872
4230
|
const ownFluids = !options.fluids;
|
|
3873
4231
|
const fluids = options.fluids ?? createFluids({ seed: 1, locale, liquids: options.liquids, groundAt });
|
|
3874
4232
|
const pristineFluids = ownFluids ? fluids.serialize() : null;
|
|
@@ -3889,13 +4247,6 @@ function createTestRoom(options) {
|
|
|
3889
4247
|
destructionSound?.onSound(key, point, volume);
|
|
3890
4248
|
options.onSound?.(key, point, volume);
|
|
3891
4249
|
};
|
|
3892
|
-
const craterField = options.craters ? createGroundField() : void 0;
|
|
3893
|
-
const craterPatch = craterField ? createGroundPatch(THREE, { field: craterField, material: new THREE.MeshStandardMaterial({ color: 9342602, roughness: 0.95 }) }) : void 0;
|
|
3894
|
-
if (craterPatch) {
|
|
3895
|
-
const floor = arena.group.getObjectByName("floor");
|
|
3896
|
-
if (floor) floor.visible = false;
|
|
3897
|
-
scene.add(craterPatch.mesh);
|
|
3898
|
-
}
|
|
3899
4250
|
const vfx = createVfxRenderer(THREE, {
|
|
3900
4251
|
scene,
|
|
3901
4252
|
camera,
|
|
@@ -3904,8 +4255,7 @@ function createTestRoom(options) {
|
|
|
3904
4255
|
pointScale,
|
|
3905
4256
|
vfx: createVfx({
|
|
3906
4257
|
profile: quality,
|
|
3907
|
-
groundAt
|
|
3908
|
-
...craterField ? { ground: craterField } : {}
|
|
4258
|
+
groundAt
|
|
3909
4259
|
})
|
|
3910
4260
|
});
|
|
3911
4261
|
const renderer = createDestructionRenderer(THREE, {
|
|
@@ -3926,6 +4276,7 @@ function createTestRoom(options) {
|
|
|
3926
4276
|
const e = entries.get(objectId);
|
|
3927
4277
|
if (!e) return;
|
|
3928
4278
|
e.piece = piece;
|
|
4279
|
+
leaveFootprint(objectId);
|
|
3929
4280
|
rendererPieces.set(objectId, piece);
|
|
3930
4281
|
action("loosened", t("act.loosened", { name: e.name }), { objectId, itemId: e.so.itemId });
|
|
3931
4282
|
},
|
|
@@ -3950,6 +4301,31 @@ function createTestRoom(options) {
|
|
|
3950
4301
|
if (best) splitOf.set(piece.objectId, best);
|
|
3951
4302
|
}
|
|
3952
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
|
+
};
|
|
3953
4329
|
const fluidView = createFluidRenderer(THREE, { scene, fluids, quality, pointScale, vfx: vfx.world });
|
|
3954
4330
|
const filmRandom = createFilmLcg(1);
|
|
3955
4331
|
const film = createFilmManager(THREE, {
|
|
@@ -4084,12 +4460,79 @@ function createTestRoom(options) {
|
|
|
4084
4460
|
let useHeld = false;
|
|
4085
4461
|
let useFirst = false;
|
|
4086
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
|
+
};
|
|
4087
4528
|
const player = createPlayer(THREE, {
|
|
4088
4529
|
camera,
|
|
4089
4530
|
dom,
|
|
4090
|
-
colliders:
|
|
4531
|
+
colliders: standingColliders,
|
|
4532
|
+
footing: (x, z, radius, feet) => footing(x, z, radius, feet),
|
|
4091
4533
|
blockers,
|
|
4092
4534
|
limit: arenaSize,
|
|
4535
|
+
floorAt: terrain.heightAt,
|
|
4093
4536
|
start: arena.playerStart,
|
|
4094
4537
|
crouchKeys: options.crouch?.keys,
|
|
4095
4538
|
hooks: {
|
|
@@ -4113,6 +4556,68 @@ function createTestRoom(options) {
|
|
|
4113
4556
|
let counter = 0;
|
|
4114
4557
|
let slot = 0;
|
|
4115
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;
|
|
4116
4621
|
const massOfObject = (id) => {
|
|
4117
4622
|
const st = world.state(id);
|
|
4118
4623
|
let m = 0;
|
|
@@ -4156,7 +4661,9 @@ function createTestRoom(options) {
|
|
|
4156
4661
|
scene.add(root);
|
|
4157
4662
|
root.updateMatrixWorld(true);
|
|
4158
4663
|
world.add(structuredCloneSpec(o.spec), { id: o.id, position: o.position, ...o.rotationY ? { rotation: [0, Math.sin(o.rotationY / 2), 0, Math.cos(o.rotationY / 2)] } : {} });
|
|
4159
|
-
registerEntry(o.id, o.id, root, o.spec, o.label ?? S.text[`arena.${o.id}`] ?? o.id, void 0, void 0, true, false);
|
|
4664
|
+
const e = registerEntry(o.id, o.id, root, o.spec, o.label ?? S.text[`arena.${o.id}`] ?? o.id, void 0, void 0, true, false);
|
|
4665
|
+
if (o.free) e.free = true;
|
|
4666
|
+
e.follower = o.follow?.(root, world, o.id) || null;
|
|
4160
4667
|
}
|
|
4161
4668
|
};
|
|
4162
4669
|
addArena();
|
|
@@ -4178,6 +4685,45 @@ function createTestRoom(options) {
|
|
|
4178
4685
|
Object.defineProperty(holder, "visible", { get: () => built.visible, set: (v) => void (built.visible = v), configurable: true });
|
|
4179
4686
|
return holder;
|
|
4180
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
|
+
};
|
|
4181
4727
|
const spawn = (itemId, position, rotationY) => {
|
|
4182
4728
|
const item = catalogMap.get(itemId);
|
|
4183
4729
|
if (!item) throw new Error(`game-test-room: unknown item "${itemId}"`);
|
|
@@ -4229,6 +4775,77 @@ function createTestRoom(options) {
|
|
|
4229
4775
|
fire("spawn", e.so);
|
|
4230
4776
|
return e.so;
|
|
4231
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
|
+
};
|
|
4232
4849
|
const massGuess = (spec) => {
|
|
4233
4850
|
let m = 0;
|
|
4234
4851
|
for (const p of spec.parts) {
|
|
@@ -4251,6 +4868,7 @@ function createTestRoom(options) {
|
|
|
4251
4868
|
renderer.unbind(id);
|
|
4252
4869
|
} catch {
|
|
4253
4870
|
}
|
|
4871
|
+
e.follower?.dispose?.();
|
|
4254
4872
|
if (world.has(id)) world.remove(id);
|
|
4255
4873
|
e.so.root.removeFromParent();
|
|
4256
4874
|
letGo(e.so.root);
|
|
@@ -4280,11 +4898,13 @@ function createTestRoom(options) {
|
|
|
4280
4898
|
explosives.clear();
|
|
4281
4899
|
fireZones.length = 0;
|
|
4282
4900
|
burningStains.length = 0;
|
|
4901
|
+
terrain.setLayout(defaultGround);
|
|
4283
4902
|
for (const id of [...entries.keys()]) if (!entries.get(id)?.arena) removeEntry(id);
|
|
4284
4903
|
clearLoose();
|
|
4285
4904
|
for (const e of entries.values()) {
|
|
4286
4905
|
world.reset(e.so.objectId);
|
|
4287
4906
|
renderer.restore(e.so.objectId);
|
|
4907
|
+
e.follower?.restore?.();
|
|
4288
4908
|
}
|
|
4289
4909
|
renderer.clear();
|
|
4290
4910
|
vfx.world.clear();
|
|
@@ -4306,6 +4926,7 @@ function createTestRoom(options) {
|
|
|
4306
4926
|
if (heldEntry === e) continue;
|
|
4307
4927
|
world.reset(e.so.objectId);
|
|
4308
4928
|
renderer.restore(e.so.objectId);
|
|
4929
|
+
e.follower?.restore?.();
|
|
4309
4930
|
if (e.containerId) {
|
|
4310
4931
|
const c = fluids.container(e.containerId);
|
|
4311
4932
|
const f = e.item?.fluid?.container;
|
|
@@ -4315,6 +4936,8 @@ function createTestRoom(options) {
|
|
|
4315
4936
|
action("reset", t("act.reset"));
|
|
4316
4937
|
};
|
|
4317
4938
|
const hardReset = () => {
|
|
4939
|
+
pushByKey = false;
|
|
4940
|
+
push.end();
|
|
4318
4941
|
clearEverything();
|
|
4319
4942
|
for (const id of [...entries.keys()]) removeEntry(id);
|
|
4320
4943
|
tools.clear();
|
|
@@ -4322,6 +4945,7 @@ function createTestRoom(options) {
|
|
|
4322
4945
|
if (pristineWorld) {
|
|
4323
4946
|
for (const id of world.ids()) world.remove(id);
|
|
4324
4947
|
world.load(pristineWorld);
|
|
4948
|
+
registerGround();
|
|
4325
4949
|
}
|
|
4326
4950
|
if (pristineFluids) {
|
|
4327
4951
|
fluids.load(pristineFluids);
|
|
@@ -4335,6 +4959,7 @@ function createTestRoom(options) {
|
|
|
4335
4959
|
prevVy.clear();
|
|
4336
4960
|
lastImpact.clear();
|
|
4337
4961
|
addArena();
|
|
4962
|
+
standFootprints();
|
|
4338
4963
|
timeScale = 1;
|
|
4339
4964
|
fastForward = false;
|
|
4340
4965
|
walkPlan = null;
|
|
@@ -4399,10 +5024,11 @@ function createTestRoom(options) {
|
|
|
4399
5024
|
}
|
|
4400
5025
|
feetPrev = [x, z];
|
|
4401
5026
|
avatarState.running = !player.crouching && (player.keys.has("ShiftLeft") || player.keys.has("ShiftRight"));
|
|
4402
|
-
avatarState.crouch = crouchForced || player.crouching;
|
|
5027
|
+
avatarState.crouch = crouchForced || player.crouching || push.isBent;
|
|
4403
5028
|
avatarState.armed = armedForced ?? tool.def.action.kind === "firearm";
|
|
4404
5029
|
avatarState.pitch = player.pitch;
|
|
4405
5030
|
avatarState.holding = heldPiece !== null;
|
|
5031
|
+
avatarState.push = push.avatarState;
|
|
4406
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;
|
|
4407
5033
|
avatar.root.position.set(x, player.feet, z);
|
|
4408
5034
|
avatar.root.rotation.set(0, player.yaw + Math.PI, 0);
|
|
@@ -4418,11 +5044,11 @@ function createTestRoom(options) {
|
|
|
4418
5044
|
const stepFirstPerson = (dt) => {
|
|
4419
5045
|
const arms = avatar?.firstPerson;
|
|
4420
5046
|
if (!arms) return;
|
|
4421
|
-
const targets = handSource?.() ?? carriedHand;
|
|
5047
|
+
const targets = push.handTargets() ?? handSource?.() ?? carriedHand;
|
|
4422
5048
|
const state = {
|
|
4423
5049
|
camera,
|
|
4424
5050
|
targets,
|
|
4425
|
-
stance: handStances[toolId] ?? (targets ? "hold" : "relaxed"),
|
|
5051
|
+
stance: push.active ? "hold" : handStances[toolId] ?? (targets ? "hold" : "relaxed"),
|
|
4426
5052
|
speed: avatarState.speed,
|
|
4427
5053
|
running: avatarState.running,
|
|
4428
5054
|
crouch: avatarState.crouch,
|
|
@@ -4443,6 +5069,19 @@ function createTestRoom(options) {
|
|
|
4443
5069
|
camera.updateMatrixWorld(true);
|
|
4444
5070
|
};
|
|
4445
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
|
+
};
|
|
4446
5085
|
const aim = () => {
|
|
4447
5086
|
camera.updateMatrixWorld(true);
|
|
4448
5087
|
camera.getWorldPosition(tmpV);
|
|
@@ -4609,6 +5248,53 @@ function createTestRoom(options) {
|
|
|
4609
5248
|
if (!rendererPieces.has(objectId)) return;
|
|
4610
5249
|
renderer.impulse(objectId, [dir[0] * momentum, dir[1] * momentum, dir[2] * momentum], point);
|
|
4611
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
|
+
};
|
|
5284
|
+
const shoveFree = (pushes) => {
|
|
5285
|
+
for (const [objectId, push2] of pushes) {
|
|
5286
|
+
const e = entries.get(objectId);
|
|
5287
|
+
if (!e || e.piece || !e.free) continue;
|
|
5288
|
+
const mass = Math.max(0.2, massOfObject(objectId));
|
|
5289
|
+
if (Math.hypot(...push2.impulse) / mass < SHOVE_SPEED) continue;
|
|
5290
|
+
const piece = renderer.makePiece(objectId, { mass });
|
|
5291
|
+
e.piece = piece;
|
|
5292
|
+
rendererPieces.set(objectId, piece);
|
|
5293
|
+
leaveFootprint(objectId);
|
|
5294
|
+
action("loosened", t("act.shoved", { name: e.name }), { objectId, itemId: e.so.itemId });
|
|
5295
|
+
renderer.impulse(objectId, push2.impulse, push2.point);
|
|
5296
|
+
}
|
|
5297
|
+
};
|
|
4612
5298
|
const fireShots = (shots, recoil) => {
|
|
4613
5299
|
if (shots.length === 0) return;
|
|
4614
5300
|
const w = toolName();
|
|
@@ -4618,6 +5304,7 @@ function createTestRoom(options) {
|
|
|
4618
5304
|
let hits = 0;
|
|
4619
5305
|
let first = null;
|
|
4620
5306
|
let energy = 0;
|
|
5307
|
+
const pushes = /* @__PURE__ */ new Map();
|
|
4621
5308
|
withoutHeld(() => {
|
|
4622
5309
|
for (const s of shots) {
|
|
4623
5310
|
energy = Math.max(energy, s.energy);
|
|
@@ -4628,12 +5315,16 @@ function createTestRoom(options) {
|
|
|
4628
5315
|
hits++;
|
|
4629
5316
|
end = r0.point;
|
|
4630
5317
|
if (!first) first = { object: r0.object, part: r0.part, point: r0.point, normal: [0, 1, 0], distance: dist3(s.origin, r0.point) };
|
|
4631
|
-
const
|
|
4632
|
-
|
|
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);
|
|
4633
5323
|
}
|
|
4634
5324
|
if (shots.length <= 3 || rnd() < 0.34) tracer(m, end);
|
|
4635
5325
|
}
|
|
4636
5326
|
});
|
|
5327
|
+
shoveFree(pushes);
|
|
4637
5328
|
player.kick(recoil[0], recoil[1]);
|
|
4638
5329
|
const f = first;
|
|
4639
5330
|
const d = f ? num(f.distance, 1) : "";
|
|
@@ -4706,8 +5397,22 @@ function createTestRoom(options) {
|
|
|
4706
5397
|
1
|
|
4707
5398
|
);
|
|
4708
5399
|
};
|
|
4709
|
-
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) => {
|
|
4710
5414
|
const w = toolName(id);
|
|
5415
|
+
const u = id === "kick" ? footRay(use) : use;
|
|
4711
5416
|
const r = rayWorld(u.origin, u.direction, u.reach);
|
|
4712
5417
|
if (!r) {
|
|
4713
5418
|
action("melee", t("act.melee.miss", { weapon: w }), { data: { weapon: id } });
|
|
@@ -4725,21 +5430,52 @@ function createTestRoom(options) {
|
|
|
4725
5430
|
let strikes = [];
|
|
4726
5431
|
const kickLeg = options.kickLeg ?? "alternate";
|
|
4727
5432
|
let nextLeg = "right";
|
|
5433
|
+
const punchHand = options.punchHand ?? "alternate";
|
|
5434
|
+
const punchTools = new Set(options.punchTools ?? ["hand", "fists"]);
|
|
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
|
+
};
|
|
4728
5448
|
const strikeUse = (u, id = toolId) => {
|
|
4729
|
-
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];
|
|
4730
5452
|
const leg = id === "kick" ? kickLeg === "alternate" ? nextLeg : kickLeg : void 0;
|
|
4731
|
-
const
|
|
5453
|
+
const hand = step2 ? step2.hand : punchTools.has(id) ? punchHand === "alternate" ? nextHand : punchHand : void 0;
|
|
5454
|
+
const side = leg ? { leg } : hand ? { hand } : void 0;
|
|
5455
|
+
const seconds = clip === void 0 ? null : avatar?.strike?.(clip, side) ?? null;
|
|
4732
5456
|
if (leg && seconds !== null && kickLeg === "alternate") nextLeg = leg === "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
|
+
}
|
|
4733
5462
|
if (clip === void 0 || seconds === null) {
|
|
4734
|
-
|
|
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);
|
|
4735
5467
|
return;
|
|
4736
5468
|
}
|
|
4737
|
-
|
|
5469
|
+
const text = leg ? "act.strike.leg" : hand ? "act.strike.hand" : "act.strike";
|
|
5470
|
+
action("strike", t(text, { weapon: toolName(id), clip, seconds: num(seconds, 2), leg: leg ? t(`leg.${leg}`) : "", hand: hand ? t(`hand.${hand}`) : "" }), {
|
|
5471
|
+
data: { weapon: id, clip, impactIn: seconds, ...leg ? { leg } : {}, ...hand ? { hand } : {}, ...step2 ? { combo: combo.id, step: stepIndex } : {} }
|
|
5472
|
+
});
|
|
4738
5473
|
strikes.push({
|
|
4739
5474
|
key: id,
|
|
4740
5475
|
remaining: seconds,
|
|
4741
5476
|
land: () => {
|
|
4742
|
-
|
|
5477
|
+
if (leg) legAim();
|
|
5478
|
+
else aim();
|
|
4743
5479
|
meleeUse({ ...u, origin: [aimO[0], aimO[1], aimO[2]], direction: [aimD[0], aimD[1], aimD[2]] }, id);
|
|
4744
5480
|
}
|
|
4745
5481
|
});
|
|
@@ -4925,12 +5661,14 @@ function createTestRoom(options) {
|
|
|
4925
5661
|
if (res) applyUses(res, 0);
|
|
4926
5662
|
};
|
|
4927
5663
|
const kick = () => {
|
|
4928
|
-
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;
|
|
4929
5665
|
aim();
|
|
4930
5666
|
const res = toolState("kick").trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], 0, false, "player");
|
|
4931
|
-
if (res.status !== "ok") return;
|
|
5667
|
+
if (res.status !== "ok") return false;
|
|
4932
5668
|
for (const u of res.uses) if (u.kind === "melee") strikeUse(u, "kick");
|
|
5669
|
+
return true;
|
|
4933
5670
|
};
|
|
5671
|
+
const kickKeys = options.kick?.keys ?? ["KeyK"];
|
|
4934
5672
|
const setTool = (id) => {
|
|
4935
5673
|
if (!arsenal.weapons.has(id)) throw new Error(`game-test-room: unknown weapon "${id}"`);
|
|
4936
5674
|
releaseTrigger();
|
|
@@ -4978,6 +5716,7 @@ function createTestRoom(options) {
|
|
|
4978
5716
|
else useTool(FIXED_DT, false);
|
|
4979
5717
|
};
|
|
4980
5718
|
const grab = () => {
|
|
5719
|
+
if (push.active) return false;
|
|
4981
5720
|
if (heldPiece) {
|
|
4982
5721
|
const name = heldName;
|
|
4983
5722
|
releaseHeld();
|
|
@@ -5193,7 +5932,7 @@ function createTestRoom(options) {
|
|
|
5193
5932
|
const reach = Math.max(size.x, size.y, size.z) * 0.5;
|
|
5194
5933
|
const origin = [centre.x + dir[0] * (reach + 0.02), centre.y + dir[1] * (reach + 0.02), centre.z + dir[2] * (reach + 0.02)];
|
|
5195
5934
|
const r = withoutHeld(() => world.raycast(origin, dir, speed * FIXED_DT * 2 + 0.06));
|
|
5196
|
-
if (!r || r.object === piece.objectId) return;
|
|
5935
|
+
if (!r || r.object === piece.objectId || r.object === GROUND_ID) return;
|
|
5197
5936
|
const energy = 0.5 * piece.mass * speed * speed;
|
|
5198
5937
|
lastImpact.set(piece.objectId, simTime);
|
|
5199
5938
|
world.hitPart(r.object, r.part, { type: "blunt", point: r.point, direction: dir, energy, radius: 0.08, source: "thrown" });
|
|
@@ -5206,12 +5945,21 @@ function createTestRoom(options) {
|
|
|
5206
5945
|
data: { energyJ: Math.round(energy), speedMs: Math.round(speed * 10) / 10, target: r.object, targetPart: r.part }
|
|
5207
5946
|
});
|
|
5208
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
|
+
};
|
|
5209
5956
|
const land = (piece) => {
|
|
5210
5957
|
const vy = piece.velocity[1];
|
|
5211
5958
|
const prev = prevVy.get(piece.objectId) ?? 0;
|
|
5212
5959
|
prevVy.set(piece.objectId, vy);
|
|
5213
5960
|
if (piece.held || prev > -MIN_LANDING_LOG || vy < prev + 2 || !world.has(piece.objectId)) return;
|
|
5214
5961
|
const energy = 0.5 * piece.mass * prev * prev;
|
|
5962
|
+
strikeGroundWith(piece, energy);
|
|
5215
5963
|
action("landing", t("act.landing", { name: nameOf(piece.objectId), speed: num(-prev, 1), energy: num(energy, energy < 100 ? 1 : 0) }), {
|
|
5216
5964
|
objectId: piece.objectId,
|
|
5217
5965
|
itemId: entryOf(piece.objectId)?.so.itemId,
|
|
@@ -5232,6 +5980,10 @@ function createTestRoom(options) {
|
|
|
5232
5980
|
const onKey = (code, down, e) => {
|
|
5233
5981
|
if (!down) {
|
|
5234
5982
|
if (code === "KeyZ") tiltKey = 0;
|
|
5983
|
+
if (code === "KeyE" && pushByKey) {
|
|
5984
|
+
pushByKey = false;
|
|
5985
|
+
if (push.end()) grab();
|
|
5986
|
+
}
|
|
5235
5987
|
return;
|
|
5236
5988
|
}
|
|
5237
5989
|
const digit = /^Digit(\d)$/.exec(code);
|
|
@@ -5242,9 +5994,15 @@ function createTestRoom(options) {
|
|
|
5242
5994
|
if (idx < loadout.length) setTool(loadout[idx]);
|
|
5243
5995
|
return;
|
|
5244
5996
|
}
|
|
5997
|
+
if (kickKeys.includes(code) && !e.ctrlKey && !e.metaKey) {
|
|
5998
|
+
kick();
|
|
5999
|
+
return;
|
|
6000
|
+
}
|
|
5245
6001
|
switch (code) {
|
|
5246
6002
|
case "KeyE":
|
|
5247
|
-
|
|
6003
|
+
if (e.repeat) break;
|
|
6004
|
+
pushByKey = !heldPiece && push.begin();
|
|
6005
|
+
if (!pushByKey) grab();
|
|
5248
6006
|
break;
|
|
5249
6007
|
case "KeyG":
|
|
5250
6008
|
throwHeld();
|
|
@@ -5287,6 +6045,9 @@ function createTestRoom(options) {
|
|
|
5287
6045
|
case "KeyY":
|
|
5288
6046
|
explosives.detonateCharges();
|
|
5289
6047
|
break;
|
|
6048
|
+
case "KeyN":
|
|
6049
|
+
setStrikeCombo(!comboOn);
|
|
6050
|
+
break;
|
|
5290
6051
|
case "KeyH": {
|
|
5291
6052
|
const w = (heldEntry ?? entryOf(inspectTarget()?.object ?? ""))?.item?.weapon;
|
|
5292
6053
|
if (w) equip(w);
|
|
@@ -5415,8 +6176,9 @@ function createTestRoom(options) {
|
|
|
5415
6176
|
}
|
|
5416
6177
|
syncLiters();
|
|
5417
6178
|
renderer.update(dt);
|
|
6179
|
+
terrainView.sync(camera.position);
|
|
6180
|
+
showGroundFire(dt);
|
|
5418
6181
|
vfx.update(dt);
|
|
5419
|
-
craterPatch?.sync();
|
|
5420
6182
|
updateImpacts();
|
|
5421
6183
|
fluidView.update(dt);
|
|
5422
6184
|
filmBridge.update(dt);
|
|
@@ -5440,12 +6202,21 @@ function createTestRoom(options) {
|
|
|
5440
6202
|
};
|
|
5441
6203
|
const update = (dt) => {
|
|
5442
6204
|
if (!ownsLoop) roomMetrics.frame(dt);
|
|
6205
|
+
frameStamp++;
|
|
5443
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();
|
|
5444
6215
|
stepAvatar(paused ? 0 : dt);
|
|
5445
6216
|
if (tiltKey || heldTilt > 0) {
|
|
5446
6217
|
heldTilt = Math.max(0, Math.min(2.4, heldTilt + (tiltKey ? 2.2 : -3) * dt));
|
|
5447
6218
|
}
|
|
5448
|
-
if ((useHeld || useFirst) && !paused) {
|
|
6219
|
+
if ((useHeld || useFirst) && !paused && !push.active) {
|
|
5449
6220
|
if (tool.def.category === "hand") {
|
|
5450
6221
|
if (useFirst) handUse();
|
|
5451
6222
|
} else if (useHeld) useTool(dt, !useFirst);
|
|
@@ -5487,6 +6258,7 @@ function createTestRoom(options) {
|
|
|
5487
6258
|
timeScale,
|
|
5488
6259
|
fast: fastForward,
|
|
5489
6260
|
tap: isTapOn(),
|
|
6261
|
+
combo: combo ? comboOn : null,
|
|
5490
6262
|
blast: { shelter: blastOn("shelter"), fragmentHits: blastOn("fragmentHits"), confinement: blastOn("confinement") },
|
|
5491
6263
|
quality: qualityName,
|
|
5492
6264
|
camera: avatar ? cameraMode : null,
|
|
@@ -5762,6 +6534,9 @@ function createTestRoom(options) {
|
|
|
5762
6534
|
onClear(listener) {
|
|
5763
6535
|
clearListeners.add(listener);
|
|
5764
6536
|
return () => clearListeners.delete(listener);
|
|
6537
|
+
},
|
|
6538
|
+
addObject(itemId, place) {
|
|
6539
|
+
return addScenery(itemId, place.position, place.rotationY);
|
|
5765
6540
|
}
|
|
5766
6541
|
};
|
|
5767
6542
|
let sendingInitial = false;
|
|
@@ -5916,6 +6691,7 @@ function createTestRoom(options) {
|
|
|
5916
6691
|
const n = Math.max(1, Math.round(seconds / dt));
|
|
5917
6692
|
for (let i = 0; i < n; i++) {
|
|
5918
6693
|
player.update(0);
|
|
6694
|
+
push.step(dt);
|
|
5919
6695
|
followHeld(dt);
|
|
5920
6696
|
stepSim(dt);
|
|
5921
6697
|
}
|
|
@@ -5958,6 +6734,7 @@ function createTestRoom(options) {
|
|
|
5958
6734
|
} finally {
|
|
5959
6735
|
scenarioRunning = false;
|
|
5960
6736
|
paused = wasPaused;
|
|
6737
|
+
restoreScenery();
|
|
5961
6738
|
}
|
|
5962
6739
|
return report;
|
|
5963
6740
|
};
|
|
@@ -5991,6 +6768,7 @@ function createTestRoom(options) {
|
|
|
5991
6768
|
const openArea = (area) => {
|
|
5992
6769
|
clear();
|
|
5993
6770
|
const mod = [...modules.values()].find((m) => m.id === area || m.group === area);
|
|
6771
|
+
if (mod?.ground) terrain.setLayout(mod.ground);
|
|
5994
6772
|
const label = mod?.group ?? S.groups[area] ?? area;
|
|
5995
6773
|
ui?.showGroup(label);
|
|
5996
6774
|
activeBench = mod && benches.has(mod.id) ? mod.id : lastBench();
|
|
@@ -6157,7 +6935,13 @@ function createTestRoom(options) {
|
|
|
6157
6935
|
setFastForward(on) {
|
|
6158
6936
|
fastForward = on;
|
|
6159
6937
|
},
|
|
6938
|
+
setStrikeCombo,
|
|
6939
|
+
kick,
|
|
6940
|
+
get strikeCombo() {
|
|
6941
|
+
return combo ? comboOn : null;
|
|
6942
|
+
},
|
|
6160
6943
|
equip,
|
|
6944
|
+
terrain,
|
|
6161
6945
|
openArea,
|
|
6162
6946
|
modules: () => [...modules.values()].map((m) => ({ id: m.id, name: m.name, group: m.group, hasArea: typeof m.area === "function" })),
|
|
6163
6947
|
openAreaAsync: (area) => loading.busy(t("loading.open_area", { area: areaLabel(area) }), () => openArea(area)),
|
|
@@ -6209,6 +6993,12 @@ function createTestRoom(options) {
|
|
|
6209
6993
|
debugView.updateProjectionMatrix();
|
|
6210
6994
|
debugView.updateMatrixWorld(true);
|
|
6211
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
|
+
} },
|
|
6212
7002
|
setCameraMode,
|
|
6213
7003
|
get cameraMode() {
|
|
6214
7004
|
return cameraMode;
|
|
@@ -6230,7 +7020,6 @@ function createTestRoom(options) {
|
|
|
6230
7020
|
loading.dispose();
|
|
6231
7021
|
renderer.dispose();
|
|
6232
7022
|
vfx.dispose();
|
|
6233
|
-
craterPatch?.dispose();
|
|
6234
7023
|
fluidView.dispose();
|
|
6235
7024
|
filmBridge.dispose();
|
|
6236
7025
|
film.dispose();
|
|
@@ -6308,6 +7097,8 @@ function createTestRoom(options) {
|
|
|
6308
7097
|
setTool(loadout[index]);
|
|
6309
7098
|
},
|
|
6310
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() } } : {},
|
|
6311
7102
|
onBlast: (key) => toggleBlast(key),
|
|
6312
7103
|
onPlay: (objectId, clipId) => {
|
|
6313
7104
|
playAnimation(clipId, objectId);
|