@jgengine/shell 0.9.0 → 0.10.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/CHANGELOG.md +21 -1
- package/dist/GamePlayerShell.d.ts +6 -21
- package/dist/GamePlayerShell.js +419 -234
- package/dist/audio/audioEngine.d.ts +8 -0
- package/dist/audio/audioEngine.js +33 -0
- package/dist/audio/musicDirector.d.ts +31 -0
- package/dist/audio/musicDirector.js +125 -0
- package/dist/audio/musicVoices.d.ts +10 -0
- package/dist/audio/musicVoices.js +338 -0
- package/dist/audio/synthEngine.d.ts +9 -0
- package/dist/audio/synthEngine.js +57 -0
- package/dist/camera/GameFirstPersonCamera.d.ts +3 -0
- package/dist/camera/GameFirstPersonCamera.js +21 -4
- package/dist/camera/GameInspectionCamera.js +12 -1
- package/dist/camera/GameOrbitCamera.js +35 -1
- package/dist/camera/orbitCameraMath.d.ts +18 -0
- package/dist/camera/orbitCameraMath.js +6 -1
- package/dist/cartridge.js +1 -0
- package/dist/commandSink.d.ts +20 -0
- package/dist/commandSink.js +27 -0
- package/dist/defineGame.js +3 -1
- package/dist/devtools/CollisionDebugWorld.js +1 -0
- package/dist/devtools/collisionDebugMath.d.ts +1 -0
- package/dist/devtools/collisionDebugMath.js +2 -1
- package/dist/environment/Daylight.d.ts +7 -1
- package/dist/environment/Daylight.js +52 -5
- package/dist/environment/EnvironmentScene.js +14 -5
- package/dist/environment/RoadRibbons.d.ts +7 -0
- package/dist/environment/RoadRibbons.js +57 -0
- package/dist/inputSink.d.ts +18 -0
- package/dist/inputSink.js +35 -0
- package/dist/multiplayer.js +7 -3
- package/dist/postfx/PostProcessing.d.ts +10 -0
- package/dist/postfx/PostProcessing.js +82 -0
- package/dist/postfx/gradeShader.d.ts +4 -0
- package/dist/postfx/gradeShader.js +64 -0
- package/dist/terrain/CarvedTerrain.d.ts +4 -1
- package/dist/terrain/CarvedTerrain.js +4 -3
- package/dist/terrain/terrainDetailMaterial.d.ts +14 -0
- package/dist/terrain/terrainDetailMaterial.js +90 -0
- package/dist/touch/TouchControlsOverlay.js +11 -1
- package/dist/world/WorldHud.d.ts +3 -1
- package/dist/world/WorldHud.js +7 -7
- package/dist/world/worldBarSamples.d.ts +1 -1
- package/dist/world/worldBarSamples.js +7 -1
- package/dist/worldSync.d.ts +10 -0
- package/dist/worldSync.js +19 -0
- package/llms.txt +140 -22
- package/package.json +4 -4
package/dist/GamePlayerShell.js
CHANGED
|
@@ -1,37 +1,45 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Canvas, useFrame, useLoader } from "@react-three/fiber";
|
|
3
|
-
import { Component, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
3
|
+
import { Component, Suspense, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
6
|
+
import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
|
|
6
7
|
import { actionRepeatMs, createActionStateTracker, hotbarSlotActionIndex, resolveActionCommand, shouldDispatchAction, toActionStateBindingMap, } from "@jgengine/core/input/actionBindings";
|
|
7
8
|
import { deriveTouchScheme, withTouchCodes } from "@jgengine/core/input/touchScheme";
|
|
8
9
|
import { buildContextMenu, contextVerbInput, } from "@jgengine/core/interaction/contextMenu";
|
|
9
10
|
import { resolveActivePrompt } from "@jgengine/core/interaction/proximityPrompt";
|
|
10
11
|
import { aimToPoint } from "@jgengine/core/input/pointer";
|
|
12
|
+
import { eyeHeightFromColliders } from "@jgengine/core/combat/shotOrigin";
|
|
11
13
|
import { normalizePointerToAxis } from "@jgengine/core/input/pointerAxis";
|
|
12
14
|
import { createSelectionSet, isMarquee, screenRect, selectWithinRect, } from "@jgengine/core/scene/selection";
|
|
13
|
-
import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, resolveMovementIntent, resolveObstacleStep, snapPositionToGrid, } from "@jgengine/core/movement/movementModel";
|
|
14
15
|
import { steerYaw } from "@jgengine/core/movement/steering";
|
|
15
|
-
import {
|
|
16
|
+
import { stepPlayerMovement, resolvePlayerMovementTuning } from "@jgengine/core/movement/playerMovement";
|
|
16
17
|
import { createGameContext } from "@jgengine/core/runtime/gameContext";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
18
|
+
import { isServerAuthoritative } from "@jgengine/core/runtime/adapter";
|
|
19
|
+
import { attachWorldSync } from "./worldSync.js";
|
|
20
|
+
import { localCommandSink, resolveCommandSink } from "./commandSink.js";
|
|
21
|
+
import { inputFramesEqual, resolveInputSink } from "./inputSink.js";
|
|
19
22
|
import { objectVisualScale } from "@jgengine/core/scene/objectStore";
|
|
20
23
|
import { DEFAULT_PICKUP_RADIUS, WORLD_ITEM_ENTITY_NAME } from "@jgengine/core/game/worldItem";
|
|
21
24
|
import { useGameContext } from "@jgengine/react/provider";
|
|
22
25
|
import { useDisplayProfile } from "@jgengine/react/display";
|
|
23
26
|
import { HudViewportProvider } from "@jgengine/react/hudViewport";
|
|
27
|
+
import { GameViewportProvider } from "@jgengine/react/gameViewport";
|
|
28
|
+
import { RotateDeviceScreen } from "@jgengine/react/rotateDevice";
|
|
24
29
|
import { useSceneEntities, useSceneObjects, usePlayer, useTarget } from "@jgengine/react/hooks";
|
|
25
30
|
import { GameProvider } from "@jgengine/react/provider";
|
|
26
31
|
import { CAMERA_FRUSTUM_DEFAULTS } from "@jgengine/core/game/playableGame";
|
|
27
32
|
import { createSettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
28
33
|
import { applyBindingOverrides, clearBindingOverride, loadBindingOverrides, saveBindingOverride, } from "@jgengine/core/input/bindingOverrides";
|
|
29
34
|
import { playControlsActive } from "@jgengine/core/game/controlGate";
|
|
35
|
+
import { orientationGateActive, orientationHintActive, resolveOrientationRequirement, } from "@jgengine/core/ui/orientation";
|
|
36
|
+
import { resolveOneShotClip } from "@jgengine/core/game/modelAnimation";
|
|
30
37
|
import { sky as resolveSkyDescriptor } from "@jgengine/core/world/features";
|
|
31
38
|
import { devtools } from "@jgengine/core/devtools/devtools";
|
|
32
39
|
import { VERSION } from "@jgengine/core/meta/changelog";
|
|
33
40
|
import { AudioListener, EntityAudioEmitters, ObjectAudioEmitters } from "./audio/AudioComponents.js";
|
|
34
41
|
import { createAudioEngine } from "./audio/audioEngine.js";
|
|
42
|
+
import { PostProcessing } from "./postfx/PostProcessing.js";
|
|
35
43
|
import { CollisionDebugWorld } from "./devtools/CollisionDebugWorld.js";
|
|
36
44
|
import { collisionDebug } from "./devtools/collisionDebug.js";
|
|
37
45
|
import { DevtoolsOverlay, DevtoolsRendererProbe, withDevtoolsLatency } from "./devtools/DevtoolsOverlay.js";
|
|
@@ -90,6 +98,10 @@ const RESERVED_INPUT_ACTIONS = new Set([
|
|
|
90
98
|
"useAbility",
|
|
91
99
|
"interact",
|
|
92
100
|
]);
|
|
101
|
+
/** No action names are reserved when no camera rig is active (hud/none presentation): games may bind `turnLeft`/`interact`/etc. as their own. */
|
|
102
|
+
const EMPTY_RESERVED = new Set();
|
|
103
|
+
/** Empty action list — published while the orientation gate is up to suppress all held input without touching the tracker. */
|
|
104
|
+
const NO_ACTIONS = [];
|
|
93
105
|
const SHELL_MOVEMENT_ACTIONS = ["moveForward", "moveBack", "moveLeft", "moveRight", "jump"];
|
|
94
106
|
function shellDrivesPlayerPose(input) {
|
|
95
107
|
const bound = input ?? {};
|
|
@@ -122,9 +134,12 @@ function pointerAimFor(ctx, service) {
|
|
|
122
134
|
const hit = service.worldHit();
|
|
123
135
|
if (hit === null)
|
|
124
136
|
return undefined;
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
137
|
+
const shooter = ctx.scene.entity.get(ctx.player.possession.active(ctx.player.userId)) ??
|
|
138
|
+
ctx.scene.entity.get(ctx.player.userId);
|
|
139
|
+
if (shooter === null)
|
|
140
|
+
return undefined;
|
|
141
|
+
const eye = eyeHeightFromColliders(ctx.scene.entity.collidersOf(shooter.id));
|
|
142
|
+
return aimToPoint([shooter.position[0], shooter.position[1] + eye, shooter.position[2]], hit.point);
|
|
128
143
|
}
|
|
129
144
|
function pointerContextMenu(ctx, playable, hit) {
|
|
130
145
|
if (hit.entity !== null) {
|
|
@@ -153,74 +168,35 @@ export function shouldFireBoundAction(tracker, action, input, repeatFiredAt, now
|
|
|
153
168
|
});
|
|
154
169
|
}
|
|
155
170
|
/** Resolves and runs the command bound to `action` via the shell's action→command convention (shared by `FrameDriver` and `HudOnlyDriver`). */
|
|
156
|
-
export function dispatchBoundAction(ctx, action, yaw, pitch, aim) {
|
|
157
|
-
const command = resolveActionCommand(action, (name) => ctx.game.commands.has(name),
|
|
171
|
+
export function dispatchBoundAction(ctx, action, yaw, pitch, aim, reserved = RESERVED_INPUT_ACTIONS, sink = localCommandSink(ctx)) {
|
|
172
|
+
const command = resolveActionCommand(action, (name) => ctx.game.commands.has(name), reserved);
|
|
158
173
|
if (command !== null)
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
const OBSTACLE_GATHER_RADIUS = 3;
|
|
162
|
-
/** Placed scene objects within `radius` of `center`, as `CollisionObstacle`s for `resolveObstacleStep` (#162.1). */
|
|
163
|
-
export function nearbyObstacles(objects, center, radius = OBSTACLE_GATHER_RADIUS) {
|
|
164
|
-
const radiusSq = radius * radius;
|
|
165
|
-
const result = [];
|
|
166
|
-
for (const object of objects) {
|
|
167
|
-
const dx = object.position[0] - center[0];
|
|
168
|
-
const dz = object.position[2] - center[2];
|
|
169
|
-
if (dx * dx + dz * dz <= radiusSq)
|
|
170
|
-
result.push({ position: object.position });
|
|
171
|
-
}
|
|
172
|
-
return result;
|
|
173
|
-
}
|
|
174
|
-
/** Applies a pending `MotionIntentBatch` to a vertical velocity: impulses add, then `verticalVelocity` replaces the result outright (#162.4). */
|
|
175
|
-
export function applyMotionImpulses(currentVelocity, batch) {
|
|
176
|
-
if (batch === null)
|
|
177
|
-
return currentVelocity;
|
|
178
|
-
let velocity = currentVelocity;
|
|
179
|
-
for (const impulse of batch.impulses)
|
|
180
|
-
velocity += impulse;
|
|
181
|
-
return batch.verticalVelocity ?? velocity;
|
|
174
|
+
sink.run(command, { yaw, pitch, aim });
|
|
182
175
|
}
|
|
176
|
+
export { applyMotionImpulses } from "@jgengine/core/runtime/motionIntents";
|
|
177
|
+
export { nearbyObstacles } from "@jgengine/core/movement/movementModel";
|
|
178
|
+
export { resolvePhysicsTuning } from "@jgengine/core/movement/playerMovement";
|
|
179
|
+
export { hasEnvironmentTerrain } from "@jgengine/core/world/terrain";
|
|
183
180
|
/** The world's declared sky, when its world feature is an environment with one (#196.1). */
|
|
184
181
|
export function resolveWorldSky(world) {
|
|
185
182
|
return world?.kind === "environment" ? world.sky : undefined;
|
|
186
183
|
}
|
|
187
|
-
/**
|
|
188
|
-
* Maps the game's declared `physics` onto the movement controllers' tuning
|
|
189
|
-
* overrides. `PhysicsConfig.gravity` is a signed world acceleration (negative
|
|
190
|
-
* points down, matching every game's config and the Y-up convention), but the
|
|
191
|
-
* controllers integrate `velocityY -= gravityAcceleration * dt` and so expect a
|
|
192
|
-
* positive downward magnitude. Negating here is what keeps a down-pointing
|
|
193
|
-
* gravity pulling the player *down*; passing the signed value straight through
|
|
194
|
-
* flipped the sign and launched airborne players upward instead.
|
|
195
|
-
*/
|
|
196
|
-
export function resolvePhysicsTuning(physics) {
|
|
197
|
-
if (physics === undefined)
|
|
198
|
-
return undefined;
|
|
199
|
-
return {
|
|
200
|
-
get gravityAcceleration() {
|
|
201
|
-
return physics.gravity === undefined ? undefined : -physics.gravity;
|
|
202
|
-
},
|
|
203
|
-
get jumpVelocity() {
|
|
204
|
-
return physics.jumpVelocity;
|
|
205
|
-
},
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
/** True when the world is an environment feature with terrain, so the voxel controller should sample its height. */
|
|
209
|
-
export function hasEnvironmentTerrain(world) {
|
|
210
|
-
return world?.kind === "environment" && (world.terrain !== undefined || (world.islands?.length ?? 0) > 0);
|
|
211
|
-
}
|
|
212
184
|
function colorFromId(id) {
|
|
213
185
|
let hash = 0;
|
|
214
186
|
for (let index = 0; index < id.length; index += 1)
|
|
215
187
|
hash = (hash * 31 + id.charCodeAt(index)) >>> 0;
|
|
216
188
|
return `hsl(${hash % 360}, 65%, 55%)`;
|
|
217
189
|
}
|
|
190
|
+
function DirectionalShadowLight({ entry }) {
|
|
191
|
+
const size = entry.shadowCameraSize ?? 40;
|
|
192
|
+
return (_jsx("directionalLight", { position: [entry.position[0], entry.position[1], entry.position[2]], intensity: entry.intensity ?? 1.3, color: entry.color, castShadow: entry.castShadow ?? false, "shadow-mapSize-width": entry.shadowMapSize ?? 1024, "shadow-mapSize-height": entry.shadowMapSize ?? 1024, "shadow-camera-left": -size, "shadow-camera-right": size, "shadow-camera-top": size, "shadow-camera-bottom": -size, "shadow-camera-near": 0.5, "shadow-camera-far": Math.max(200, size * 6), "shadow-bias": entry.shadowBias ?? -0.0004, "shadow-normalBias": entry.shadowNormalBias ?? 0.02 }));
|
|
193
|
+
}
|
|
218
194
|
function ConfiguredLighting({ lighting }) {
|
|
219
195
|
return (_jsxs(_Fragment, { children: [lighting.ambient !== undefined ? (_jsx("ambientLight", { color: lighting.ambient.color, intensity: lighting.ambient.intensity ?? 0.55 })) : null, lighting.hemisphere !== undefined ? (_jsx("hemisphereLight", { args: [
|
|
220
196
|
lighting.hemisphere.skyColor ?? "#bfe3ff",
|
|
221
197
|
lighting.hemisphere.groundColor ?? "#4c6b34",
|
|
222
198
|
lighting.hemisphere.intensity ?? 0.55,
|
|
223
|
-
] })) : null, (lighting.directional ?? []).map((entry, index) => (_jsx(
|
|
199
|
+
] })) : null, (lighting.directional ?? []).map((entry, index) => (_jsx(DirectionalShadowLight, { entry: entry }, index)))] }));
|
|
224
200
|
}
|
|
225
201
|
function BackdropFog({ fog }) {
|
|
226
202
|
if (fog === undefined)
|
|
@@ -236,35 +212,193 @@ function EntitySprite({ sprite }) {
|
|
|
236
212
|
}, [texture]);
|
|
237
213
|
return (_jsx("sprite", { "position-y": sprite.y, scale: [sprite.width, sprite.height, 1], children: _jsx("spriteMaterial", { map: texture, transparent: true, alphaTest: 0.08, depthWrite: false }) }));
|
|
238
214
|
}
|
|
215
|
+
class ModelFallbackBoundary extends Component {
|
|
216
|
+
state = { failed: false };
|
|
217
|
+
static getDerivedStateFromError() {
|
|
218
|
+
return { failed: true };
|
|
219
|
+
}
|
|
220
|
+
render() {
|
|
221
|
+
return this.state.failed ? this.props.fallback : this.props.children;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function IsolatedEntityModel({ model, instanceId, fallback, }) {
|
|
225
|
+
return (_jsx(ModelFallbackBoundary, { fallback: fallback ?? null, children: _jsx(Suspense, { fallback: null, children: _jsx(EntityModel, { model: model, instanceId: instanceId }) }) }));
|
|
226
|
+
}
|
|
227
|
+
function BoneAttachment({ rig, model, slot, position, rotation, scale, }) {
|
|
228
|
+
const gltf = useLoader(GLTFLoader, model.url, (loader) => {
|
|
229
|
+
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
230
|
+
});
|
|
231
|
+
const weaponScene = useMemo(() => cloneModelScene(gltf.scene), [gltf]);
|
|
232
|
+
const px = position?.[0] ?? 0;
|
|
233
|
+
const py = position?.[1] ?? 0;
|
|
234
|
+
const pz = position?.[2] ?? 0;
|
|
235
|
+
const rx = rotation?.[0] ?? 0;
|
|
236
|
+
const ry = rotation?.[1] ?? 0;
|
|
237
|
+
const rz = rotation?.[2] ?? 0;
|
|
238
|
+
const s = scale ?? 1;
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
const bone = rig.getObjectByName(slot);
|
|
241
|
+
if (bone === undefined) {
|
|
242
|
+
if (typeof console !== "undefined") {
|
|
243
|
+
console.warn(`[jgengine] entityModels attachment: bone/slot "${slot}" not found on the rig`);
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
weaponScene.position.set(px, py, pz);
|
|
248
|
+
weaponScene.rotation.set(rx, ry, rz);
|
|
249
|
+
weaponScene.scale.setScalar(s);
|
|
250
|
+
bone.add(weaponScene);
|
|
251
|
+
return () => {
|
|
252
|
+
bone.remove(weaponScene);
|
|
253
|
+
};
|
|
254
|
+
}, [rig, weaponScene, slot, px, py, pz, rx, ry, rz, s]);
|
|
255
|
+
useEffect(() => () => disposeClonedMaterials(weaponScene), [weaponScene]);
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
/** Resolves an entity model plus any bone attachments' models through the asset catalog, so `EntityModel` receives fully-resolved `ModelConfig`s. */
|
|
259
|
+
function resolveEntityModel(value, assets, key) {
|
|
260
|
+
const model = resolveModel(value, assets, { seam: "entityModels", key });
|
|
261
|
+
if (model?.attachments === undefined)
|
|
262
|
+
return model;
|
|
263
|
+
return {
|
|
264
|
+
...model,
|
|
265
|
+
attachments: model.attachments.map((attachment) => ({
|
|
266
|
+
...attachment,
|
|
267
|
+
model: resolveModel(attachment.model, assets) ?? attachment.model,
|
|
268
|
+
})),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
239
271
|
function EntityModel({ model, instanceId }) {
|
|
240
|
-
const gltf = useLoader(GLTFLoader, model.url)
|
|
272
|
+
const gltf = useLoader(GLTFLoader, model.url, (loader) => {
|
|
273
|
+
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
274
|
+
});
|
|
241
275
|
const ctx = useGameContext();
|
|
242
276
|
const material = model.material;
|
|
243
|
-
const scale = model.scale ?? 1;
|
|
244
277
|
const baseY = model.y ?? 0;
|
|
245
278
|
const dims = model.dims;
|
|
246
|
-
const centered = (model.anchor ?? "center") === "center" && dims !== undefined;
|
|
247
|
-
const position = centered
|
|
248
|
-
? [-scale * dims.center.x, baseY - scale * dims.minY, -scale * dims.center.z]
|
|
249
|
-
: [0, baseY, 0];
|
|
250
279
|
const scene = useMemo(() => {
|
|
251
280
|
const cloned = cloneModelScene(gltf.scene);
|
|
252
281
|
if (material !== undefined)
|
|
253
282
|
applyMaterialOverride(cloned, material, { clone: false });
|
|
254
283
|
return cloned;
|
|
255
284
|
}, [gltf, material]);
|
|
285
|
+
const measured = useMemo(() => {
|
|
286
|
+
if (model.targetHeight === undefined)
|
|
287
|
+
return null;
|
|
288
|
+
const box = new THREE.Box3().setFromObject(scene);
|
|
289
|
+
const height = box.max.y - box.min.y;
|
|
290
|
+
if (!Number.isFinite(height) || height <= 0)
|
|
291
|
+
return null;
|
|
292
|
+
return {
|
|
293
|
+
normalize: model.targetHeight / height,
|
|
294
|
+
minY: box.min.y,
|
|
295
|
+
centerX: (box.min.x + box.max.x) / 2,
|
|
296
|
+
centerZ: (box.min.z + box.max.z) / 2,
|
|
297
|
+
};
|
|
298
|
+
}, [scene, model.targetHeight]);
|
|
299
|
+
const scale = (model.scale ?? 1) * (measured?.normalize ?? 1);
|
|
300
|
+
const centered = (model.anchor ?? "center") === "center" && dims !== undefined;
|
|
301
|
+
const position = measured !== null
|
|
302
|
+
? [-scale * measured.centerX, baseY - scale * measured.minY, -scale * measured.centerZ]
|
|
303
|
+
: centered
|
|
304
|
+
? [-scale * dims.center.x, baseY - scale * dims.minY, -scale * dims.center.z]
|
|
305
|
+
: [0, baseY, 0];
|
|
256
306
|
useEffect(() => () => {
|
|
257
307
|
disposeClonedMaterials(scene);
|
|
258
308
|
}, [scene]);
|
|
259
309
|
const animation = model.animation;
|
|
260
310
|
const mixerRef = useRef(null);
|
|
261
311
|
const animationPausedRef = useRef(false);
|
|
312
|
+
const stateActionsRef = useRef(null);
|
|
313
|
+
const states = animation?.states;
|
|
314
|
+
const oneShots = animation?.oneShots;
|
|
315
|
+
const oneShotPlayRef = useRef(null);
|
|
316
|
+
const activeOneShotRef = useRef(null);
|
|
262
317
|
useEffect(() => {
|
|
263
318
|
if (animation === undefined || gltf.animations.length === 0) {
|
|
264
319
|
mixerRef.current = null;
|
|
320
|
+
stateActionsRef.current = null;
|
|
265
321
|
return;
|
|
266
322
|
}
|
|
267
323
|
const mixer = new THREE.AnimationMixer(scene);
|
|
324
|
+
if (states !== undefined) {
|
|
325
|
+
const clipFor = (name) => THREE.AnimationClip.findByName(gltf.animations, name) ?? gltf.animations[0];
|
|
326
|
+
const actions = {
|
|
327
|
+
idle: mixer.clipAction(clipFor(states.idle)),
|
|
328
|
+
walk: mixer.clipAction(clipFor(states.walk)),
|
|
329
|
+
...(states.run === undefined ? {} : { run: mixer.clipAction(clipFor(states.run)) }),
|
|
330
|
+
};
|
|
331
|
+
for (const action of Object.values(actions)) {
|
|
332
|
+
action.setLoop(THREE.LoopRepeat, Infinity);
|
|
333
|
+
action.timeScale = animation.timeScale ?? 1;
|
|
334
|
+
action.enabled = true;
|
|
335
|
+
}
|
|
336
|
+
actions.idle.play();
|
|
337
|
+
mixer.update(0);
|
|
338
|
+
mixerRef.current = mixer;
|
|
339
|
+
stateActionsRef.current = { actions, active: "idle", lastPos: null, smoothedSpeed: 0 };
|
|
340
|
+
animationPausedRef.current = false;
|
|
341
|
+
let onOneShotFinished = null;
|
|
342
|
+
if (oneShots !== undefined) {
|
|
343
|
+
const clipNames = new Set();
|
|
344
|
+
for (const spec of Object.values(oneShots)) {
|
|
345
|
+
if (typeof spec === "string")
|
|
346
|
+
clipNames.add(spec);
|
|
347
|
+
else
|
|
348
|
+
for (const name of spec)
|
|
349
|
+
clipNames.add(name);
|
|
350
|
+
}
|
|
351
|
+
const oneShotActions = new Map();
|
|
352
|
+
for (const name of clipNames) {
|
|
353
|
+
const found = THREE.AnimationClip.findByName(gltf.animations, name);
|
|
354
|
+
if (found === null)
|
|
355
|
+
continue;
|
|
356
|
+
const oneShotAction = mixer.clipAction(found);
|
|
357
|
+
oneShotAction.setLoop(THREE.LoopOnce, 1);
|
|
358
|
+
oneShotAction.enabled = true;
|
|
359
|
+
oneShotActions.set(name, oneShotAction);
|
|
360
|
+
}
|
|
361
|
+
onOneShotFinished = ({ action }) => {
|
|
362
|
+
const active = activeOneShotRef.current;
|
|
363
|
+
if (active === null || action !== active.action || active.isDeath)
|
|
364
|
+
return;
|
|
365
|
+
const machine = stateActionsRef.current;
|
|
366
|
+
const back = machine?.actions[machine.active];
|
|
367
|
+
action.fadeOut(0.15);
|
|
368
|
+
if (back !== undefined)
|
|
369
|
+
back.reset().fadeIn(0.15).play();
|
|
370
|
+
activeOneShotRef.current = null;
|
|
371
|
+
};
|
|
372
|
+
mixer.addEventListener("finished", onOneShotFinished);
|
|
373
|
+
oneShotPlayRef.current = (event) => {
|
|
374
|
+
const active = activeOneShotRef.current;
|
|
375
|
+
if (active !== null && active.isDeath)
|
|
376
|
+
return;
|
|
377
|
+
const clipName = resolveOneShotClip(oneShots, event, Math.random());
|
|
378
|
+
if (clipName === null)
|
|
379
|
+
return;
|
|
380
|
+
const oneShotAction = oneShotActions.get(clipName);
|
|
381
|
+
if (oneShotAction === undefined)
|
|
382
|
+
return;
|
|
383
|
+
const machine = stateActionsRef.current;
|
|
384
|
+
machine?.actions[machine.active]?.fadeOut(0.1);
|
|
385
|
+
if (active !== null && active.action !== oneShotAction)
|
|
386
|
+
active.action.stop();
|
|
387
|
+
oneShotAction.clampWhenFinished = event === "death";
|
|
388
|
+
oneShotAction.reset().fadeIn(0.1).play();
|
|
389
|
+
activeOneShotRef.current = { action: oneShotAction, isDeath: event === "death" };
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
return () => {
|
|
393
|
+
if (onOneShotFinished !== null)
|
|
394
|
+
mixer.removeEventListener("finished", onOneShotFinished);
|
|
395
|
+
oneShotPlayRef.current = null;
|
|
396
|
+
activeOneShotRef.current = null;
|
|
397
|
+
mixer.stopAllAction();
|
|
398
|
+
mixerRef.current = null;
|
|
399
|
+
stateActionsRef.current = null;
|
|
400
|
+
};
|
|
401
|
+
}
|
|
268
402
|
const clip = (animation.clip !== undefined ? THREE.AnimationClip.findByName(gltf.animations, animation.clip) : undefined) ??
|
|
269
403
|
gltf.animations[0];
|
|
270
404
|
const action = mixer.clipAction(clip);
|
|
@@ -283,7 +417,39 @@ function EntityModel({ model, instanceId }) {
|
|
|
283
417
|
mixer.stopAllAction();
|
|
284
418
|
mixerRef.current = null;
|
|
285
419
|
};
|
|
286
|
-
}, [
|
|
420
|
+
}, [
|
|
421
|
+
scene,
|
|
422
|
+
gltf,
|
|
423
|
+
animation?.clip,
|
|
424
|
+
animation?.loop,
|
|
425
|
+
animation?.timeScale,
|
|
426
|
+
animation?.paused,
|
|
427
|
+
animation?.time,
|
|
428
|
+
states,
|
|
429
|
+
oneShots,
|
|
430
|
+
]);
|
|
431
|
+
useEffect(() => {
|
|
432
|
+
if (instanceId === undefined || oneShots === undefined)
|
|
433
|
+
return;
|
|
434
|
+
const fire = (event) => oneShotPlayRef.current?.(event);
|
|
435
|
+
const offAnimation = ctx.game.events.on("entity.animation", (event) => {
|
|
436
|
+
if (event.instanceId === instanceId)
|
|
437
|
+
fire(event.event);
|
|
438
|
+
});
|
|
439
|
+
const offHit = ctx.game.events.on("combat.hitReaction", (event) => {
|
|
440
|
+
if (event.instanceId === instanceId)
|
|
441
|
+
fire("hit");
|
|
442
|
+
});
|
|
443
|
+
const offDied = ctx.game.events.on("entity.died", (event) => {
|
|
444
|
+
if (event.instanceId === instanceId)
|
|
445
|
+
fire("death");
|
|
446
|
+
});
|
|
447
|
+
return () => {
|
|
448
|
+
offAnimation();
|
|
449
|
+
offHit();
|
|
450
|
+
offDied();
|
|
451
|
+
};
|
|
452
|
+
}, [ctx, instanceId, oneShots]);
|
|
287
453
|
const paintCanvasRef = useRef(null);
|
|
288
454
|
const paintDrawnCountRef = useRef(0);
|
|
289
455
|
const paintVersionRef = useRef(-1);
|
|
@@ -295,6 +461,38 @@ function EntityModel({ model, instanceId }) {
|
|
|
295
461
|
materialCacheRef.current = null;
|
|
296
462
|
}, [scene]);
|
|
297
463
|
useFrame((_state, delta) => {
|
|
464
|
+
const stateMachine = stateActionsRef.current;
|
|
465
|
+
if (stateMachine !== null && states !== undefined && instanceId !== undefined && delta > 0) {
|
|
466
|
+
const entity = ctx.scene.entity.get(instanceId);
|
|
467
|
+
if (entity !== null) {
|
|
468
|
+
const [x, , z] = entity.position;
|
|
469
|
+
if (stateMachine.lastPos !== null) {
|
|
470
|
+
const instantSpeed = Math.hypot(x - stateMachine.lastPos[0], z - stateMachine.lastPos[2]) / delta;
|
|
471
|
+
stateMachine.smoothedSpeed +=
|
|
472
|
+
(instantSpeed - stateMachine.smoothedSpeed) * Math.min(1, delta * 12);
|
|
473
|
+
}
|
|
474
|
+
stateMachine.lastPos = [x, entity.position[1], z];
|
|
475
|
+
const walkSpeed = states.walkSpeed ?? 0.5;
|
|
476
|
+
const runSpeed = states.runSpeed ?? 6;
|
|
477
|
+
const next = stateMachine.smoothedSpeed < walkSpeed
|
|
478
|
+
? "idle"
|
|
479
|
+
: stateMachine.actions.run !== undefined && stateMachine.smoothedSpeed >= runSpeed
|
|
480
|
+
? "run"
|
|
481
|
+
: "walk";
|
|
482
|
+
if (next !== stateMachine.active) {
|
|
483
|
+
if (activeOneShotRef.current === null) {
|
|
484
|
+
const fade = states.fadeSec ?? 0.2;
|
|
485
|
+
const from = stateMachine.actions[stateMachine.active];
|
|
486
|
+
const to = stateMachine.actions[next];
|
|
487
|
+
if (from !== undefined && to !== undefined) {
|
|
488
|
+
to.reset().fadeIn(fade).play();
|
|
489
|
+
from.fadeOut(fade);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
stateMachine.active = next;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
298
496
|
if (mixerRef.current !== null && !animationPausedRef.current)
|
|
299
497
|
mixerRef.current.update(delta);
|
|
300
498
|
if (instanceId === undefined)
|
|
@@ -319,7 +517,7 @@ function EntityModel({ model, instanceId }) {
|
|
|
319
517
|
}
|
|
320
518
|
paintDrawnCountRef.current = syncPaintCanvas(paintCanvasRef.current, cache.seedColor, strokes, paintDrawnCountRef.current);
|
|
321
519
|
});
|
|
322
|
-
return _jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] });
|
|
520
|
+
return (_jsxs(_Fragment, { children: [_jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] }), (model.attachments ?? []).map((attachment, index) => typeof attachment.model === "string" ? null : (_jsx(BoneAttachment, { rig: scene, model: attachment.model, slot: attachment.slot, position: attachment.position, rotation: attachment.rotation, scale: attachment.scale }, `${attachment.slot}-${index}`)))] }));
|
|
323
521
|
}
|
|
324
522
|
function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, selected, onSelect, }) {
|
|
325
523
|
const groupRef = useRef(null);
|
|
@@ -346,7 +544,7 @@ function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, select
|
|
|
346
544
|
const live = ctx.scene.entity.get(entityId);
|
|
347
545
|
if (live !== null)
|
|
348
546
|
onSelect(live);
|
|
349
|
-
}, children: [selected ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.02, children: [_jsx("ringGeometry", { args: [0.8, 0.95, 32] }), _jsx("meshBasicMaterial", { color: "#34d399", transparent: true, opacity: 0.9 })] })) : null, custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(
|
|
547
|
+
}, children: [selected ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.02, children: [_jsx("ringGeometry", { args: [0.8, 0.95, 32] }), _jsx("meshBasicMaterial", { color: "#34d399", transparent: true, opacity: 0.9 })] })) : null, custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(IsolatedEntityModel, { model: model, instanceId: entityId, fallback: sprite !== undefined ? _jsx(EntitySprite, { sprite: sprite }) : undefined })) : sprite !== undefined ? (_jsx(EntitySprite, { sprite: sprite })) : role === "prop" ? (_jsxs("mesh", { "position-y": 0.5, children: [_jsx("sphereGeometry", { args: [0.45, 16, 16] }), _jsx("meshStandardMaterial", { color: color })] })) : (_jsxs(_Fragment, { children: [_jsxs("mesh", { "position-y": 0.95, children: [_jsx("capsuleGeometry", { args: [0.35, 1.1, 6, 14] }), _jsx("meshStandardMaterial", { color: color })] }), _jsxs("mesh", { position: [0, 1.35, 0.32], children: [_jsx("boxGeometry", { args: [0.16, 0.16, 0.16] }), _jsx("meshStandardMaterial", { color: "#f8fafc" })] })] })), targeted ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.03, children: [_jsx("ringGeometry", { args: [0.6, 0.75, 28] }), _jsx("meshBasicMaterial", { color: "#f87171" })] })) : null] }));
|
|
350
548
|
}
|
|
351
549
|
function ObjectMarker({ object, custom, model, style, }) {
|
|
352
550
|
const groupRef = useRef(null);
|
|
@@ -366,7 +564,7 @@ function ObjectMarker({ object, custom, model, style, }) {
|
|
|
366
564
|
writeEntityPose(group, live);
|
|
367
565
|
group.visible = visibleRef.current(instanceId);
|
|
368
566
|
});
|
|
369
|
-
return (_jsx("group", { ref: groupRef, userData: { [POINTER_OBJECT_KEY]: instanceId }, children: custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(
|
|
567
|
+
return (_jsx("group", { ref: groupRef, userData: { [POINTER_OBJECT_KEY]: instanceId }, children: custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(IsolatedEntityModel, { model: model, instanceId: instanceId })) : style?.hidden === true ? null : (_jsxs("mesh", { "position-y": 0.5 * scaleY, scale: [scaleX, scaleY, scaleZ], children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshStandardMaterial", { color: color, transparent: opacity < 1, opacity: opacity })] })) }));
|
|
370
568
|
}
|
|
371
569
|
function GroundPlane() {
|
|
372
570
|
const geometry = useMemo(() => {
|
|
@@ -404,7 +602,7 @@ function WorldEnvironment({ environment: Environment }) {
|
|
|
404
602
|
return _jsx(Environment, {});
|
|
405
603
|
return (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx("gridHelper", { args: [160, 80, "#3a3f4a", "#2b2f38"], "position-y": 0.01 }), _jsx(RockField, {})] }));
|
|
406
604
|
}
|
|
407
|
-
function WorldActors({ entitySprites, entityModels, objectModels, objectStyles, assets, renderEntity, renderObject, selectedIds, }) {
|
|
605
|
+
function WorldActors({ entitySprites, entityModels, objectModels, objectStyles, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }) {
|
|
408
606
|
const ctx = useGameContext();
|
|
409
607
|
const entities = useSceneEntities();
|
|
410
608
|
const objects = useSceneObjects();
|
|
@@ -417,10 +615,8 @@ function WorldActors({ entitySprites, entityModels, objectModels, objectStyles,
|
|
|
417
615
|
};
|
|
418
616
|
return (_jsxs(_Fragment, { children: [entities
|
|
419
617
|
.filter((entity) => entity.name !== WORLD_ITEM_ENTITY_NAME)
|
|
420
|
-
.
|
|
421
|
-
|
|
422
|
-
key: entity.name,
|
|
423
|
-
}), sprite: entitySprites?.[entity.name], isLocal: entity.id === controlledId, targeted: entity.id === targetId, selected: selectedIds.has(entity.id), onSelect: handleSelect }, entity.id))), objects.map((object) => {
|
|
618
|
+
.filter((entity) => !(hideLocalActor && entity.id === controlledId))
|
|
619
|
+
.map((entity) => (_jsx(EntityMarker, { entity: entity, custom: renderEntity?.(entity), model: resolveEntityModel(entityModels?.[entity.name], assets, entity.name), sprite: entitySprites?.[entity.name], isLocal: entity.id === controlledId, targeted: entity.id === targetId, selected: selectedIds.has(entity.id), onSelect: handleSelect }, entity.id))), objects.map((object) => {
|
|
424
620
|
const model = resolveModel(objectModels?.[object.catalogId], assets, {
|
|
425
621
|
seam: "objectModels",
|
|
426
622
|
key: object.catalogId,
|
|
@@ -428,36 +624,25 @@ function WorldActors({ entitySprites, entityModels, objectModels, objectStyles,
|
|
|
428
624
|
return (_jsx(ObjectMarker, { object: object, custom: renderObject?.(object), model: model, style: objectStyles?.[object.catalogId] }, object.instanceId));
|
|
429
625
|
})] }));
|
|
430
626
|
}
|
|
431
|
-
function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, }) {
|
|
432
|
-
return (_jsxs(_Fragment, { children: [_jsx(WorldEnvironment, { environment: environment }), _jsx(WorldActors, { entitySprites: entitySprites, entityModels: entityModels, objectModels: objectModels, objectStyles: objectStyles, assets: assets, renderEntity: renderEntity, renderObject: renderObject, selectedIds: selectedIds })] }));
|
|
627
|
+
function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }) {
|
|
628
|
+
return (_jsxs(_Fragment, { children: [_jsx(WorldEnvironment, { environment: environment }), _jsx(WorldActors, { entitySprites: entitySprites, entityModels: entityModels, objectModels: objectModels, objectStyles: objectStyles, assets: assets, renderEntity: renderEntity, renderObject: renderObject, selectedIds: selectedIds, hideLocalActor: hideLocalActor })] }));
|
|
433
629
|
}
|
|
434
630
|
function RemotePlayers({ rows }) {
|
|
435
631
|
return (_jsx(_Fragment, { children: rows.map((row) => (_jsxs("group", { position: [row.position.x, row.position.y, row.position.z], "rotation-y": row.rotationY, children: [_jsxs("mesh", { "position-y": 0.95, children: [_jsx("capsuleGeometry", { args: [0.35, 1.1, 6, 14] }), _jsx("meshStandardMaterial", { color: colorFromId(row.userId) })] }), _jsxs("mesh", { position: [0, 1.35, 0.32], children: [_jsx("boxGeometry", { args: [0.16, 0.16, 0.16] }), _jsx("meshStandardMaterial", { color: "#f8fafc" })] })] }, row.userId))) }));
|
|
436
632
|
}
|
|
437
|
-
function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }) {
|
|
633
|
+
function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, gateRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }) {
|
|
438
634
|
const posterElapsedRef = useRef(0);
|
|
439
635
|
const posterDoneRef = useRef(false);
|
|
440
|
-
const motionRef = useRef(createPlayerMotionState());
|
|
441
|
-
const voxelBodyRef = useRef(null);
|
|
442
|
-
const solidCacheRef = useRef({ count: -1, set: new Set() });
|
|
443
636
|
const hasReportedTickError = useRef(false);
|
|
444
637
|
const repeatFiredAtRef = useRef(new Map());
|
|
445
638
|
const slotActions = useMemo(() => findHotbarSlotActions(playable.game.input), [playable]);
|
|
446
639
|
const hotbarId = useMemo(() => hotbarIdFor(playable), [playable]);
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
get height() {
|
|
454
|
-
return collision?.height ?? 1.8;
|
|
455
|
-
},
|
|
456
|
-
get stepHeight() {
|
|
457
|
-
return collision?.stepHeight ?? 0.6;
|
|
458
|
-
},
|
|
459
|
-
}), [collision]);
|
|
460
|
-
const movementTuning = useMemo(() => resolvePhysicsTuning(playable.game.physics), [playable]);
|
|
640
|
+
const movementTuning = useMemo(() => resolvePlayerMovementTuning({
|
|
641
|
+
collision: playable.collision,
|
|
642
|
+
movement: playable.movement,
|
|
643
|
+
physics: playable.game.physics,
|
|
644
|
+
world: playable.game.world,
|
|
645
|
+
}), [playable]);
|
|
461
646
|
const autoPickupRadius = useMemo(() => {
|
|
462
647
|
const cfg = playable.worldItem?.autoPickup;
|
|
463
648
|
if (cfg === undefined || cfg === false)
|
|
@@ -465,10 +650,24 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
465
650
|
const fallback = playable.worldItem?.pickupRadius ?? DEFAULT_PICKUP_RADIUS;
|
|
466
651
|
return cfg === true ? fallback : cfg.radius ?? fallback;
|
|
467
652
|
}, [playable]);
|
|
468
|
-
const ground = useMemo(() => groundFieldFor(playable.game.world), [playable]);
|
|
469
653
|
const drivesPose = useMemo(() => shellDrivesPlayerPose(playable.game.input), [playable]);
|
|
654
|
+
const serverAuthoritative = useMemo(() => isServerAuthoritative(playable.game.multiplayer) && multiplayer !== null, [playable, multiplayer]);
|
|
655
|
+
const commandSink = useMemo(() => ({
|
|
656
|
+
run: (name, input) => resolveCommandSink(ctx, {
|
|
657
|
+
serverAuthoritative,
|
|
658
|
+
backend: multiplayer?.backend ?? null,
|
|
659
|
+
serverId: serverIdRef.current,
|
|
660
|
+
}).run(name, input),
|
|
661
|
+
}), [ctx, multiplayer, serverAuthoritative, serverIdRef]);
|
|
662
|
+
const inputSink = useMemo(() => ({
|
|
663
|
+
send: (frame) => resolveInputSink({
|
|
664
|
+
serverAuthoritative,
|
|
665
|
+
backend: multiplayer?.backend ?? null,
|
|
666
|
+
serverId: serverIdRef.current,
|
|
667
|
+
}).send(frame),
|
|
668
|
+
}), [multiplayer, serverAuthoritative, serverIdRef]);
|
|
669
|
+
const lastSentInputRef = useRef(null);
|
|
470
670
|
const inputActions = useMemo(() => Object.keys(playable.game.input ?? {}), [playable]);
|
|
471
|
-
const hasTerrain = useMemo(() => hasEnvironmentTerrain(playable.game.world), [playable]);
|
|
472
671
|
useFrame((_state, rawDt) => {
|
|
473
672
|
if (poster) {
|
|
474
673
|
if (posterDoneRef.current)
|
|
@@ -480,6 +679,21 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
480
679
|
return;
|
|
481
680
|
}
|
|
482
681
|
}
|
|
682
|
+
const sendInput = () => {
|
|
683
|
+
if (!serverAuthoritative)
|
|
684
|
+
return;
|
|
685
|
+
const frame = { held: ctx.input.held(), pointer: ctx.input.pointer() };
|
|
686
|
+
const last = lastSentInputRef.current;
|
|
687
|
+
if (last !== null && inputFramesEqual(last, frame))
|
|
688
|
+
return;
|
|
689
|
+
lastSentInputRef.current = frame;
|
|
690
|
+
inputSink.send(frame);
|
|
691
|
+
};
|
|
692
|
+
if (gateRef.current) {
|
|
693
|
+
ctx.input.publish(heldActionsFor(tracker, NO_ACTIONS));
|
|
694
|
+
sendInput();
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
483
697
|
const simStart = performance.now();
|
|
484
698
|
try {
|
|
485
699
|
let endPhase = devtools.profile.begin("time+input");
|
|
@@ -487,108 +701,19 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
487
701
|
const gameDt = ctx.time.advance(dt);
|
|
488
702
|
ctx.input.publish(heldActionsFor(tracker, inputActions));
|
|
489
703
|
ctx.input.publishPointer(pointerAxisRef.current);
|
|
704
|
+
sendInput();
|
|
490
705
|
const turnInput = (tracker.isDown("turnRight") ? 1 : 0) - (tracker.isDown("turnLeft") ? 1 : 0);
|
|
491
706
|
if (turnInput !== 0)
|
|
492
707
|
yawRef.current = steerYaw(yawRef.current, turnInput, TURN_SPEED, dt);
|
|
493
708
|
endPhase();
|
|
494
709
|
const playerId = ctx.player.possession.active(ctx.player.userId);
|
|
495
710
|
const player = ctx.scene.entity.get(playerId);
|
|
496
|
-
|
|
497
|
-
const forwardZ = Math.cos(yawRef.current);
|
|
498
|
-
if (player !== null && drivesPose) {
|
|
711
|
+
if (player !== null && drivesPose && !serverAuthoritative) {
|
|
499
712
|
endPhase = devtools.profile.begin("pose");
|
|
500
|
-
|
|
501
|
-
keys.w = tracker.isDown("moveForward");
|
|
502
|
-
keys.s = tracker.isDown("moveBack");
|
|
503
|
-
keys.a = tracker.isDown("moveLeft");
|
|
504
|
-
keys.d = tracker.isDown("moveRight");
|
|
505
|
-
keys.shift = tracker.isDown("sprint") && (movement?.canSprint?.(ctx) ?? true);
|
|
506
|
-
keys.space = tracker.isDown("jump");
|
|
507
|
-
const intent = resolveMovementIntent(keys, true);
|
|
508
|
-
const motionBatch = ctx.player.motion.takePending();
|
|
509
|
-
if (collision?.voxel) {
|
|
510
|
-
let body = voxelBodyRef.current;
|
|
511
|
-
if (body === null) {
|
|
512
|
-
body = createVoxelPlayerBody(player.position[0], player.position[1], player.position[2]);
|
|
513
|
-
voxelBodyRef.current = body;
|
|
514
|
-
}
|
|
515
|
-
const objects = ctx.scene.object.list();
|
|
516
|
-
const cache = solidCacheRef.current;
|
|
517
|
-
if (cache.count !== objects.length) {
|
|
518
|
-
cache.set = new Set(objects.map((o) => `${o.position[0]},${o.position[1]},${o.position[2]}`));
|
|
519
|
-
cache.count = objects.length;
|
|
520
|
-
}
|
|
521
|
-
const solids = cache.set;
|
|
522
|
-
const isSolid = (x, y, z) => solids.has(`${x},${y},${z}`);
|
|
523
|
-
body.velocityY = applyMotionImpulses(body.velocityY, motionBatch);
|
|
524
|
-
[body.velocityX, body.velocityZ] = applyHorizontalImpulses(body.velocityX, body.velocityZ, motionBatch);
|
|
525
|
-
advanceVoxelPlayer(body, intent, forwardX, forwardZ, player.movement.walkSpeed ?? 2, rawDt, isSolid, voxelDims, movementTuning, hasTerrain ? (x, z) => ground.sampleHeight(x, z) : undefined);
|
|
526
|
-
if (motionBatch !== null && motionBatch.y !== null)
|
|
527
|
-
body.y = motionBatch.y;
|
|
528
|
-
ctx.scene.entity.setPose(playerId, {
|
|
529
|
-
position: [body.x, body.y, body.z],
|
|
530
|
-
rotationY: intent.moving
|
|
531
|
-
? Math.atan2(body.velocityX, body.velocityZ)
|
|
532
|
-
: player.rotationY,
|
|
533
|
-
dt: rawDt,
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
else {
|
|
537
|
-
const motion = motionRef.current;
|
|
538
|
-
motion.verticalVelocity = applyMotionImpulses(motion.verticalVelocity, motionBatch);
|
|
539
|
-
[motion.horizontalVelocityX, motion.horizontalVelocityZ] = applyHorizontalImpulses(motion.horizontalVelocityX, motion.horizontalVelocityZ, motionBatch);
|
|
540
|
-
const step = advancePlayerMotion(motion, intent, forwardX, forwardZ, player.movement.walkSpeed ?? 2, rawDt, movementTuning);
|
|
541
|
-
let stepX = step.stepX;
|
|
542
|
-
let stepZ = step.stepZ;
|
|
543
|
-
if (movement?.mode === "axis") {
|
|
544
|
-
const constrained = constrainStepToAxis(stepX, stepZ, movement.axis ?? "x");
|
|
545
|
-
stepX = constrained.stepX;
|
|
546
|
-
stepZ = constrained.stepZ;
|
|
547
|
-
}
|
|
548
|
-
if (movement?.collideObjects === true) {
|
|
549
|
-
const obstacles = nearbyObstacles(ctx.scene.object.list(), player.position);
|
|
550
|
-
const resolved = resolveObstacleStep(player.position, stepX, stepZ, obstacles);
|
|
551
|
-
stepX = resolved.stepX;
|
|
552
|
-
stepZ = resolved.stepZ;
|
|
553
|
-
}
|
|
554
|
-
let nextX = player.position[0] + stepX;
|
|
555
|
-
let nextZ = player.position[2] + stepZ;
|
|
556
|
-
if (movement?.mode === "grid") {
|
|
557
|
-
const snapped = snapPositionToGrid(nextX, nextZ, movement.cellSize ?? 1);
|
|
558
|
-
nextX = snapped[0];
|
|
559
|
-
nextZ = snapped[1];
|
|
560
|
-
}
|
|
561
|
-
let nextY = ground.sampleHeight(nextX, nextZ) + motion.jumpOffset;
|
|
562
|
-
if (motionBatch !== null && motionBatch.y !== null) {
|
|
563
|
-
nextY = motionBatch.y;
|
|
564
|
-
motion.jumpOffset = motionBatch.y - ground.sampleHeight(nextX, nextZ);
|
|
565
|
-
}
|
|
566
|
-
if (movement?.beforeCommit !== undefined) {
|
|
567
|
-
const frame = {
|
|
568
|
-
entityId: playerId,
|
|
569
|
-
current: player.position,
|
|
570
|
-
next: [nextX, nextY, nextZ],
|
|
571
|
-
dt: rawDt,
|
|
572
|
-
ctx,
|
|
573
|
-
};
|
|
574
|
-
const replacement = movement.beforeCommit(frame);
|
|
575
|
-
if (replacement !== undefined) {
|
|
576
|
-
nextX = replacement[0];
|
|
577
|
-
nextY = replacement[1];
|
|
578
|
-
nextZ = replacement[2];
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
ctx.scene.entity.setPose(playerId, {
|
|
582
|
-
position: [nextX, nextY, nextZ],
|
|
583
|
-
rotationY: intent.moving
|
|
584
|
-
? Math.atan2(motion.horizontalVelocityX, motion.horizontalVelocityZ)
|
|
585
|
-
: player.rotationY,
|
|
586
|
-
dt: rawDt,
|
|
587
|
-
});
|
|
588
|
-
}
|
|
713
|
+
stepPlayerMovement(ctx, ctx.player.userId, { held: ctx.input.held(), pointer: ctx.input.pointer() }, rawDt, movementTuning, yawRef.current);
|
|
589
714
|
endPhase();
|
|
590
715
|
}
|
|
591
|
-
if (autoPickupRadius !== null) {
|
|
716
|
+
if (autoPickupRadius !== null && !serverAuthoritative) {
|
|
592
717
|
endPhase = devtools.profile.begin("pickup");
|
|
593
718
|
const self = ctx.scene.entity.get(playerId);
|
|
594
719
|
if (self !== null) {
|
|
@@ -598,9 +723,11 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
598
723
|
}
|
|
599
724
|
endPhase();
|
|
600
725
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
726
|
+
if (!serverAuthoritative) {
|
|
727
|
+
devtools.profile.measure("onTick", () => {
|
|
728
|
+
playable.loop.onTick(ctx, gameDt);
|
|
729
|
+
});
|
|
730
|
+
}
|
|
604
731
|
endPhase = devtools.profile.begin("actions");
|
|
605
732
|
if (tracker.wasPressed("tabTarget")) {
|
|
606
733
|
if (ctx.game.commands.has("target.cycle"))
|
|
@@ -617,7 +744,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
617
744
|
if (pingCommand !== undefined && tracker.wasPressed("ping")) {
|
|
618
745
|
const hit = pointerService.worldHit();
|
|
619
746
|
if (hit !== null && ctx.game.commands.has(pingCommand)) {
|
|
620
|
-
|
|
747
|
+
commandSink.run(pingCommand, {
|
|
621
748
|
point: hit.point,
|
|
622
749
|
entity: hit.entity,
|
|
623
750
|
object: hit.object,
|
|
@@ -632,7 +759,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
632
759
|
collisionDebug.setAimProbe({
|
|
633
760
|
from: aimFrom,
|
|
634
761
|
aim: commandAim,
|
|
635
|
-
originPolicy: { kind: "
|
|
762
|
+
originPolicy: { kind: "eye" },
|
|
636
763
|
maxDistance: 100,
|
|
637
764
|
});
|
|
638
765
|
}
|
|
@@ -652,7 +779,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
652
779
|
if (prompts !== undefined && focus !== null) {
|
|
653
780
|
const active = resolveActivePrompt({ x: focus.position[0], z: focus.position[2] }, prompts);
|
|
654
781
|
if (active !== null && active.prompt.invoke !== null) {
|
|
655
|
-
|
|
782
|
+
commandSink.run(active.prompt.invoke.name, active.prompt.invoke.input);
|
|
656
783
|
}
|
|
657
784
|
}
|
|
658
785
|
continue;
|
|
@@ -660,7 +787,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
660
787
|
if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
|
|
661
788
|
continue;
|
|
662
789
|
repeatFiredAtRef.current.set(action, nowMs);
|
|
663
|
-
dispatchBoundAction(ctx, action, yawRef.current, pitchRef.current, commandAim);
|
|
790
|
+
dispatchBoundAction(ctx, action, yawRef.current, pitchRef.current, commandAim, RESERVED_INPUT_ACTIONS, commandSink);
|
|
664
791
|
}
|
|
665
792
|
if (hotbarId !== null) {
|
|
666
793
|
for (const { action, slot } of slotActions) {
|
|
@@ -713,7 +840,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
713
840
|
}, GAME_SIM_FRAME_PRIORITY);
|
|
714
841
|
return null;
|
|
715
842
|
}
|
|
716
|
-
function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, onRuntimeError, }) {
|
|
843
|
+
function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, gateRef, onRuntimeError, }) {
|
|
717
844
|
const hasReportedTickError = useRef(false);
|
|
718
845
|
const repeatFiredAtRef = useRef(new Map());
|
|
719
846
|
const lastFrameRef = useRef(null);
|
|
@@ -726,6 +853,10 @@ function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, onRuntimeError,
|
|
|
726
853
|
lastFrameRef.current = now;
|
|
727
854
|
if (last === null)
|
|
728
855
|
return;
|
|
856
|
+
if (gateRef.current) {
|
|
857
|
+
ctx.input.publish(heldActionsFor(tracker, NO_ACTIONS));
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
729
860
|
const rawDt = (now - last) / 1000;
|
|
730
861
|
const simStart = performance.now();
|
|
731
862
|
try {
|
|
@@ -744,7 +875,7 @@ function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, onRuntimeError,
|
|
|
744
875
|
if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
|
|
745
876
|
continue;
|
|
746
877
|
repeatFiredAtRef.current.set(action, nowMs);
|
|
747
|
-
dispatchBoundAction(ctx, action, 0, 0, { yaw: 0, pitch: 0 });
|
|
878
|
+
dispatchBoundAction(ctx, action, 0, 0, { yaw: 0, pitch: 0 }, EMPTY_RESERVED);
|
|
748
879
|
}
|
|
749
880
|
tracker.endFrame();
|
|
750
881
|
endPhase();
|
|
@@ -759,7 +890,7 @@ function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, onRuntimeError,
|
|
|
759
890
|
};
|
|
760
891
|
frameId = requestAnimationFrame(tick);
|
|
761
892
|
return () => cancelAnimationFrame(frameId);
|
|
762
|
-
}, [ctx, playable, tracker, pointerAxisRef, onRuntimeError, inputActions]);
|
|
893
|
+
}, [ctx, playable, tracker, pointerAxisRef, gateRef, onRuntimeError, inputActions]);
|
|
763
894
|
return null;
|
|
764
895
|
}
|
|
765
896
|
class GameUiErrorBoundary extends Component {
|
|
@@ -864,21 +995,67 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
864
995
|
pointerAxisRef.current = { ...state, active: false };
|
|
865
996
|
};
|
|
866
997
|
const touchScheme = useMemo(() => deriveTouchScheme(playable.game.input, {
|
|
867
|
-
reserved:
|
|
998
|
+
reserved: resolveRigKind(playable.camera) === "none" || playable.presentation === "hud"
|
|
999
|
+
? EMPTY_RESERVED
|
|
1000
|
+
: RESERVED_INPUT_ACTIONS,
|
|
868
1001
|
firstPerson: resolveRigKind(playable.camera) === "first",
|
|
869
1002
|
config: playable.touch,
|
|
870
1003
|
}), [playable]);
|
|
871
1004
|
const { coarsePointer, portrait, compact } = useDisplayProfile();
|
|
872
1005
|
const touchSink = useMemo(() => ({ onCodeDown: (code) => tracker.handleDown(code), onCodeUp: (code) => tracker.handleUp(code) }), [tracker]);
|
|
873
|
-
const
|
|
1006
|
+
const gateRef = useRef(false);
|
|
1007
|
+
const orientationPlatform = coarsePointer ? "mobile" : "desktop";
|
|
1008
|
+
const orientationRequirement = useMemo(() => resolveOrientationRequirement(playable.orientation, orientationPlatform), [playable, orientationPlatform]);
|
|
1009
|
+
const liveOrientation = portrait ? "portrait" : "landscape";
|
|
1010
|
+
const orientationGate = !poster && coarsePointer && orientationGateActive(orientationRequirement, liveOrientation);
|
|
1011
|
+
const orientationHint = !poster && coarsePointer && orientationHintActive(orientationRequirement, liveOrientation);
|
|
1012
|
+
gateRef.current = orientationGate;
|
|
1013
|
+
const orientationGateEl = orientationGate ? (_jsx(RotateDeviceScreen, { requiredOrientation: orientationRequirement.required ?? "landscape", title: orientationRequirement.required === "portrait" ? "Turn your phone upright" : "Turn your phone sideways", description: `${playable.game.name} is built for ${orientationRequirement.required ?? "landscape"} play.` })) : orientationHint && orientationRequirement.preferred !== null ? (_jsx(OrientationHint, { wanted: orientationRequirement.preferred })) : null;
|
|
1014
|
+
const audioEngine = useMemo(() => createAudioEngine({
|
|
1015
|
+
sounds: playable.audio?.sounds,
|
|
1016
|
+
buses: playable.audio?.buses,
|
|
1017
|
+
music: playable.audio?.music,
|
|
1018
|
+
musicBus: playable.audio?.musicBus,
|
|
1019
|
+
}), [playable]);
|
|
874
1020
|
useEffect(() => () => audioEngine.dispose(), [audioEngine]);
|
|
875
1021
|
useEffect(() => {
|
|
876
1022
|
if (ctx === null)
|
|
877
1023
|
return;
|
|
878
|
-
|
|
1024
|
+
const offPlay = ctx.game.events.on("audio.play", ({ sound, at }) => {
|
|
879
1025
|
audioEngine.playOneShot(sound, at === undefined ? undefined : { x: at[0], y: at[1], z: at[2] });
|
|
880
1026
|
});
|
|
1027
|
+
const offMusic = ctx.game.events.on("audio.music", ({ theme, transpose }) => {
|
|
1028
|
+
audioEngine.playMusic(theme, transpose === undefined ? undefined : { transpose });
|
|
1029
|
+
});
|
|
1030
|
+
const offResume = ctx.game.events.on("audio.resume", () => audioEngine.resume());
|
|
1031
|
+
return () => {
|
|
1032
|
+
offPlay();
|
|
1033
|
+
offMusic();
|
|
1034
|
+
offResume();
|
|
1035
|
+
};
|
|
881
1036
|
}, [ctx, audioEngine]);
|
|
1037
|
+
useEffect(() => {
|
|
1038
|
+
if (typeof document === "undefined")
|
|
1039
|
+
return;
|
|
1040
|
+
document.documentElement.dataset.jgPresentation = playable.presentation ?? "3d";
|
|
1041
|
+
return () => {
|
|
1042
|
+
delete document.documentElement.dataset.jgPresentation;
|
|
1043
|
+
};
|
|
1044
|
+
}, [playable]);
|
|
1045
|
+
useEffect(() => {
|
|
1046
|
+
if (ctx === null || typeof document === "undefined")
|
|
1047
|
+
return;
|
|
1048
|
+
const hud = resolveRigKind(playable.camera) === "none" || playable.presentation === "hud";
|
|
1049
|
+
if (!hud)
|
|
1050
|
+
return;
|
|
1051
|
+
const frame = requestAnimationFrame(() => {
|
|
1052
|
+
document.documentElement.dataset.jgFrameReady = "1";
|
|
1053
|
+
});
|
|
1054
|
+
return () => {
|
|
1055
|
+
cancelAnimationFrame(frame);
|
|
1056
|
+
delete document.documentElement.dataset.jgFrameReady;
|
|
1057
|
+
};
|
|
1058
|
+
}, [ctx, playable]);
|
|
882
1059
|
const userId = multiplayer?.userId ?? DEV_USER_ID;
|
|
883
1060
|
const reportRuntimeError = (error, phase, componentStack) => {
|
|
884
1061
|
const diagnostic = logRuntimeError(error, phase, componentStack);
|
|
@@ -920,6 +1097,9 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
920
1097
|
return;
|
|
921
1098
|
}
|
|
922
1099
|
serverIdRef.current = joined.serverId;
|
|
1100
|
+
if (isServerAuthoritative(playable.game.multiplayer) && multiplayer.backend.feeds !== undefined) {
|
|
1101
|
+
cleanups.push(attachWorldSync(multiplayer.backend.feeds, joined.serverId, ctx));
|
|
1102
|
+
}
|
|
923
1103
|
cleanups.push(multiplayer.backend.presenceSync.subscribe(joined.serverId, (rows) => {
|
|
924
1104
|
setRemotePlayers(rows.filter((row) => row.userId !== multiplayer.userId));
|
|
925
1105
|
}));
|
|
@@ -955,8 +1135,9 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
955
1135
|
cleanups.push(remoteUnsub);
|
|
956
1136
|
}
|
|
957
1137
|
const chatSync = multiplayer.backend.chatSyncFor?.(joined.serverId);
|
|
958
|
-
if (chatSync !== undefined) {
|
|
959
|
-
const
|
|
1138
|
+
if (chatSync !== undefined && ctx.game.chat !== undefined) {
|
|
1139
|
+
const chat = ctx.game.chat;
|
|
1140
|
+
const globalChannelIds = new Set(chat
|
|
960
1141
|
.channels()
|
|
961
1142
|
.filter((channel) => channel.kind === "global")
|
|
962
1143
|
.map((channel) => channel.id));
|
|
@@ -976,7 +1157,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
976
1157
|
if (seenRemoteChat.has(message.id))
|
|
977
1158
|
continue;
|
|
978
1159
|
seenRemoteChat.add(message.id);
|
|
979
|
-
|
|
1160
|
+
chat.send(message.fromUserId, message.channelId, message.body);
|
|
980
1161
|
}
|
|
981
1162
|
}));
|
|
982
1163
|
}
|
|
@@ -994,7 +1175,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
994
1175
|
void multiplayer.backend.transport.leaveServer({ serverId }).catch(() => undefined);
|
|
995
1176
|
}
|
|
996
1177
|
};
|
|
997
|
-
}, [ctx, multiplayer]);
|
|
1178
|
+
}, [ctx, multiplayer, playable]);
|
|
998
1179
|
useEffect(() => {
|
|
999
1180
|
wrapperRef.current?.focus();
|
|
1000
1181
|
}, [ctx]);
|
|
@@ -1010,7 +1191,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1010
1191
|
const rigKind = resolveRigKind(cameraConfig);
|
|
1011
1192
|
if (rigKind === "none" || playable.presentation === "hud") {
|
|
1012
1193
|
const GameUI = playable.GameUI;
|
|
1013
|
-
return (
|
|
1194
|
+
return (_jsx("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
|
|
1014
1195
|
if (event.code === "F2" && devtoolsEnabled) {
|
|
1015
1196
|
event.preventDefault();
|
|
1016
1197
|
f2HeldRef.current = true;
|
|
@@ -1035,7 +1216,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1035
1216
|
}, onBlur: () => {
|
|
1036
1217
|
f2HeldRef.current = false;
|
|
1037
1218
|
tracker.reset();
|
|
1038
|
-
}, onPointerMove: trackPointerAxis, onPointerLeave: deactivatePointerAxis, onPointerCancel: deactivatePointerAxis, children: [_jsx(HudOnlyDriver, { ctx: ctx, playable: playable, tracker: tracker, pointerAxisRef: pointerAxisRef, onRuntimeError: reportRuntimeError }), _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(HudViewportProvider, { platforms: playable.platforms, config: playable.hudFit, userScale: graphics.uiScale, children: _jsx(GameUI, {}) }) }) }), devtoolsEnabled ? (_jsx(DevtoolsOverlay, { open: devtoolsOpen, ctx: ctx, playable: playable, multiplayer: multiplayer })) : null, _jsx(DiagnosticOverlay, { diagnostics: diagnostics, gameName: playable.game.name })] }));
|
|
1219
|
+
}, onPointerDown: () => audioEngine.resume(), onPointerMove: trackPointerAxis, onPointerLeave: deactivatePointerAxis, onPointerCancel: deactivatePointerAxis, children: _jsxs(GameViewportProvider, { platforms: playable.platforms, children: [_jsx(HudOnlyDriver, { ctx: ctx, playable: playable, tracker: tracker, pointerAxisRef: pointerAxisRef, gateRef: gateRef, onRuntimeError: reportRuntimeError }), !orientationGate && coarsePointer && touchScheme !== null && touchScheme.gestures !== null && playControlsActive(ctx) ? (_jsx(TouchPlaySurface, { scheme: touchScheme, sink: touchSink, yawRef: yawRef, pitchRef: pitchRef, maxPitch: 0, onPrimaryTap: () => undefined })) : null, _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(HudViewportProvider, { platforms: playable.platforms, config: playable.hudFit, userScale: graphics.uiScale, children: orientationGate ? null : _jsx(GameUI, {}) }) }) }), orientationGateEl, devtoolsEnabled ? (_jsx(DevtoolsOverlay, { open: devtoolsOpen, ctx: ctx, playable: playable, multiplayer: multiplayer })) : null, _jsx(DiagnosticOverlay, { diagnostics: diagnostics, gameName: playable.game.name })] }) }));
|
|
1039
1220
|
}
|
|
1040
1221
|
const firstPerson = rigKind === "first";
|
|
1041
1222
|
const showReticle = (firstPerson && playable.camera?.firstPerson?.reticle !== false) || rigKind === "shoulder";
|
|
@@ -1047,6 +1228,9 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1047
1228
|
? "health"
|
|
1048
1229
|
: worldBars.statId ?? "health";
|
|
1049
1230
|
const barsRoles = worldBars === undefined || worldBars === true || worldBars === false ? undefined : worldBars.roles;
|
|
1231
|
+
const barsMaxDistance = worldBars === undefined || worldBars === true || worldBars === false
|
|
1232
|
+
? undefined
|
|
1233
|
+
: worldBars.maxDistance;
|
|
1050
1234
|
const resolveEntityRole = (entity) => playable.content.entityById?.(entity.name)?.role;
|
|
1051
1235
|
const pointer = playable.pointer;
|
|
1052
1236
|
const pointerUsesLeft = pointer !== undefined && (pointer.select === true || pointer.moveCommand !== undefined);
|
|
@@ -1086,13 +1270,15 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1086
1270
|
}
|
|
1087
1271
|
commitSelection(selectWithinRect(candidates, rect));
|
|
1088
1272
|
};
|
|
1273
|
+
/** DOM UI (menus, HUD buttons) sits in the same wrapper as the canvas — only canvas-targeted clicks are world input. */
|
|
1274
|
+
const isWorldPointerTarget = (event) => event.target instanceof HTMLCanvasElement;
|
|
1089
1275
|
const handlePointerDown = (event) => {
|
|
1090
1276
|
wrapperRef.current?.focus();
|
|
1091
1277
|
trackPointerAxis(event);
|
|
1092
1278
|
audioEngine.resume();
|
|
1093
1279
|
if (contextMenu !== null)
|
|
1094
1280
|
setContextMenu(null);
|
|
1095
|
-
if (event.button === 0) {
|
|
1281
|
+
if (event.button === 0 && isWorldPointerTarget(event)) {
|
|
1096
1282
|
const point = localXY(event);
|
|
1097
1283
|
pointerDownRef.current = point;
|
|
1098
1284
|
if (pointer?.select === true)
|
|
@@ -1166,6 +1352,8 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1166
1352
|
const handleContextMenu = (event) => {
|
|
1167
1353
|
if (pointer === undefined)
|
|
1168
1354
|
return;
|
|
1355
|
+
if (!isWorldPointerTarget(event))
|
|
1356
|
+
return;
|
|
1169
1357
|
event.preventDefault();
|
|
1170
1358
|
const hit = pointerService.worldHit();
|
|
1171
1359
|
if (hit === null)
|
|
@@ -1218,15 +1406,12 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1218
1406
|
}));
|
|
1219
1407
|
const touchScale = compact ? 0.88 : 1;
|
|
1220
1408
|
const dockMounted = !poster &&
|
|
1409
|
+
!orientationGate &&
|
|
1221
1410
|
coarsePointer &&
|
|
1222
1411
|
controlsActive &&
|
|
1223
1412
|
touchScheme !== null &&
|
|
1224
1413
|
(touchScheme.joystick !== null || touchScheme.buttons.length > 0);
|
|
1225
|
-
|
|
1226
|
-
coarsePointer &&
|
|
1227
|
-
playable.orientation !== undefined &&
|
|
1228
|
-
(playable.orientation === "landscape") === portrait;
|
|
1229
|
-
return (_jsx(SettingsProvider, { store: settingsStore, children: _jsxs(PlayerFovProvider, { config: playable.camera, orthographic: orthographic, children: [_jsx(AudioSettingsBridge, { store: settingsStore, engine: audioEngine, buses: playable.audio?.buses }), _jsx(SettingsRuntime, { variant: settingsVariant, surface: settingsSurface, actions: settingsActions, input: playable.game.input ?? {}, buses: playable.audio?.buses, extra: settingsConfig.extra ?? [], categories: settingsConfig.categories ?? [], hide: hideCategories, fovEnabled: fovControlEnabled, hideBindings: settingsConfig.hideBindings ?? [], overrides: bindingOverrides, rebind: rebindAction, resetBinding: resetActionBinding, children: _jsxs("div", { ref: wrapperRef, tabIndex: 0, ...(poster && posterFrozen ? { "data-poster-ready": "" } : {}), className: "relative h-full w-full bg-neutral-950 outline-none", style: {
|
|
1414
|
+
return (_jsx(SettingsProvider, { store: settingsStore, children: _jsxs(PlayerFovProvider, { config: playable.camera, orthographic: orthographic, children: [_jsx(AudioSettingsBridge, { store: settingsStore, engine: audioEngine, buses: playable.audio?.buses }), _jsx(SettingsRuntime, { variant: settingsVariant, surface: settingsSurface, actions: settingsActions, input: playable.game.input ?? {}, buses: playable.audio?.buses, extra: settingsConfig.extra ?? [], categories: settingsConfig.categories ?? [], hide: hideCategories, fovEnabled: fovControlEnabled, hideBindings: settingsConfig.hideBindings ?? [], overrides: bindingOverrides, rebind: rebindAction, resetBinding: resetActionBinding, children: _jsx("div", { ref: wrapperRef, tabIndex: 0, ...(poster && posterFrozen ? { "data-poster-ready": "" } : {}), className: "relative h-full w-full bg-neutral-950 outline-none", style: {
|
|
1230
1415
|
"--jg-hud-dock-clearance": `${dockMounted ? touchDockClearance(touchScheme, touchScale) : 0}px`,
|
|
1231
1416
|
}, onKeyDown: (event) => {
|
|
1232
1417
|
if (event.code === "F2" && devtoolsEnabled) {
|
|
@@ -1265,24 +1450,24 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1265
1450
|
else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
|
|
1266
1451
|
ctx.game.commands.run("ui.hotbarScrollPrev", {});
|
|
1267
1452
|
}
|
|
1268
|
-
}, children: [_jsxs(Canvas, { frameloop: poster && posterFrozen ? "demand" : "always", orthographic: orthographic, camera: orthographic
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1453
|
+
}, children: _jsxs(GameViewportProvider, { platforms: playable.platforms, children: [_jsxs(Canvas, { frameloop: poster && posterFrozen ? "demand" : "always", orthographic: orthographic, camera: orthographic
|
|
1454
|
+
? {
|
|
1455
|
+
zoom: playable.camera?.frustum?.zoom ?? CAMERA_FRUSTUM_DEFAULTS.zoom,
|
|
1456
|
+
near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
|
|
1457
|
+
far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
|
|
1458
|
+
}
|
|
1459
|
+
: {
|
|
1460
|
+
fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
|
|
1461
|
+
near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
|
|
1462
|
+
far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
|
|
1463
|
+
}, shadows: graphics.shadows, dpr: graphics.dpr, gl: { preserveDrawingBuffer: true }, style: { touchAction: "none" }, children: [backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, lighting !== undefined ? (_jsx(ConfiguredLighting, { lighting: lighting })) : effectiveSky === undefined ? (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 })] })) : null, effectiveSky !== undefined ? (effectiveSky.timeOfDay ? (_jsx(TimeOfDayDaylight, { sky: effectiveSky, clock: ctx.time, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) })) : (_jsx(SkyDaylight, { sky: effectiveSky, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) }))) : null, _jsx(BackdropFog, { fog: backdrop?.fog }), _jsxs(GameProvider, { context: ctx, children: [_jsx(CullingProvider, { config: playable.visibility, children: _jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, objectStyles: playable.objectStyles, environment: AutoEnvironment, assets: playable.game.assets, renderEntity: playable.renderEntity, renderObject: playable.renderObject, selectedIds: selectedIds, hideLocalActor: firstPerson }) }), WorldOverlay !== undefined ? _jsx(WorldOverlay, {}) : null, barsStatId !== null ? (_jsx(WorldEntityBars, { statId: barsStatId, roles: barsRoles, resolveRole: resolveEntityRole, ...(barsMaxDistance === undefined ? {} : { maxDistance: barsMaxDistance }) })) : null, _jsx(WorldItems, { config: playable.worldItem }), _jsx(WorldTelegraphs, {}), _jsx(WorldFloatText, {}), _jsx(ProjectileTracers, {}), devtoolsEnabled ? _jsx(CollisionDebugWorld, {}) : null, _jsx(CombatCameraShake, {}), _jsx(AudioListener, { engine: audioEngine }), _jsx(EntityAudioEmitters, { engine: audioEngine, entitySounds: playable.entitySounds }), _jsx(ObjectAudioEmitters, { engine: audioEngine, objectSounds: playable.objectSounds }), _jsx(GameCameraRig, { yawRef: yawRef, pitchRef: pitchRef, config: cameraConfig, pointerControls: pointerUsesLeft, panKeysEnabled: rtsPanKeysEnabled, director: ctx.camera, onDragChange: (dragging) => {
|
|
1464
|
+
cameraDraggingRef.current = dragging;
|
|
1465
|
+
} }), _jsx(PointerProbe, { service: pointerService })] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, pointerAxisRef: pointerAxisRef, gateRef: gateRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand, poster: poster, onPosterSettled: () => {
|
|
1466
|
+
if (posterSettledRef.current)
|
|
1467
|
+
return;
|
|
1468
|
+
posterSettledRef.current = true;
|
|
1469
|
+
setPosterFrozen(true);
|
|
1470
|
+
} }), _jsx(DevtoolsRendererProbe, {}), playable.postProcessing !== undefined && playable.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: playable.postProcessing })) : null] }), !poster && !orientationGate && coarsePointer && controlsActive && touchScheme !== null && (touchScheme.gestures !== null || touchScheme.look) ? (_jsx(TouchPlaySurface, { scheme: touchScheme, sink: touchSink, yawRef: yawRef, pitchRef: pitchRef, maxPitch: playable.camera?.firstPerson?.maxPitch ?? 1.45, onPrimaryTap: () => {
|
|
1471
|
+
primaryClickRef.current = true;
|
|
1472
|
+
} })) : null, _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(HudViewportProvider, { platforms: playable.platforms, config: playable.hudFit, userScale: graphics.uiScale, children: orientationGate ? null : _jsx(GameUI, {}) }) }) }), !poster && !orientationGate && showReticle ? _jsx(Reticle, {}) : null, dockMounted && touchScheme !== null ? (_jsx(TouchControlsDock, { scheme: touchScheme, sink: touchSink, scale: touchScale })) : null, orientationGateEl, marquee !== null ? _jsx(MarqueeBox, { rect: marquee }) : null, contextMenu !== null ? (_jsx(ContextMenuView, { menu: contextMenu.menu, x: contextMenu.x, y: contextMenu.y, onPick: handleVerbPick, onClose: () => setContextMenu(null) })) : null, devtoolsEnabled ? (_jsx(DevtoolsOverlay, { open: devtoolsOpen, ctx: ctx, playable: playable, multiplayer: multiplayer })) : null, poster ? null : _jsx(DiagnosticOverlay, { diagnostics: diagnostics, gameName: playable.game.name }), poster || orthographic || settingsHostsFov ? null : _jsx(PlayerFovSlider, {}), poster ? null : _jsx(SettingsChrome, {})] }) }) })] }) }));
|
|
1288
1473
|
}
|