@jgengine/shell 0.8.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 +36 -0
- package/README.md +11 -0
- package/dist/GamePlayerShell.d.ts +10 -22
- package/dist/GamePlayerShell.js +709 -264
- package/dist/GameUiPreview.js +2 -1
- package/dist/audio/AudioComponents.js +25 -8
- package/dist/audio/audioEngine.d.ts +9 -0
- package/dist/audio/audioEngine.js +37 -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/behaviour.d.ts +5 -24
- package/dist/behaviour.js +14 -50
- package/dist/behaviourAttach.d.ts +9 -0
- package/dist/behaviourAttach.js +32 -0
- package/dist/behaviourDriver.d.ts +7 -0
- package/dist/behaviourDriver.js +21 -0
- package/dist/camera/GameCameraRig.js +1 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +3 -0
- package/dist/camera/GameFirstPersonCamera.js +35 -4
- package/dist/camera/GameInspectionCamera.js +12 -1
- package/dist/camera/GameOrbitCamera.js +44 -1
- package/dist/camera/PlayerFov.d.ts +18 -0
- package/dist/camera/PlayerFov.js +48 -0
- package/dist/camera/cameraBlendMath.d.ts +13 -0
- package/dist/camera/cameraBlendMath.js +36 -0
- package/dist/camera/cameraRigs.d.ts +3 -0
- package/dist/camera/cameraRigs.js +36 -33
- package/dist/camera/fovPreference.d.ts +25 -0
- package/dist/camera/fovPreference.js +73 -0
- package/dist/camera/index.d.ts +3 -0
- package/dist/camera/index.js +3 -0
- package/dist/camera/orbitCameraMath.d.ts +18 -0
- package/dist/camera/orbitCameraMath.js +6 -1
- package/dist/camera/rigMath.d.ts +10 -0
- package/dist/camera/rigMath.js +36 -1
- package/dist/camera/shakeChannel.d.ts +3 -17
- package/dist/camera/shakeChannel.js +2 -21
- package/dist/camera/shakeChannelMath.d.ts +8 -0
- package/dist/camera/shakeChannelMath.js +21 -0
- package/dist/cartridge.d.ts +145 -0
- package/dist/cartridge.js +246 -0
- package/dist/commandSink.d.ts +20 -0
- package/dist/commandSink.js +27 -0
- package/dist/defineGame.js +4 -1
- package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
- package/dist/devtools/CollisionDebugWorld.js +181 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +65 -1
- package/dist/devtools/DevtoolsOverlay.js +383 -41
- package/dist/devtools/collisionDebug.d.ts +57 -0
- package/dist/devtools/collisionDebug.js +127 -0
- package/dist/devtools/collisionDebugMath.d.ts +103 -0
- package/dist/devtools/collisionDebugMath.js +129 -0
- package/dist/environment/Daylight.d.ts +20 -8
- package/dist/environment/Daylight.js +63 -15
- package/dist/environment/EnvironmentScene.js +91 -35
- package/dist/environment/RoadRibbons.d.ts +7 -0
- package/dist/environment/RoadRibbons.js +57 -0
- package/dist/environment/groundPadMath.d.ts +2 -2
- package/dist/environment/groundPadMath.js +1 -1
- package/dist/environment/index.d.ts +2 -1
- package/dist/environment/index.js +1 -0
- package/dist/environment/skyLightingPolicy.d.ts +10 -0
- package/dist/environment/skyLightingPolicy.js +6 -0
- package/dist/input/mouseLook.d.ts +27 -0
- package/dist/input/mouseLook.js +35 -0
- package/dist/inputSink.d.ts +18 -0
- package/dist/inputSink.js +35 -0
- package/dist/materialOverride.d.ts +4 -6
- package/dist/materialOverride.js +12 -16
- package/dist/multiplayer.js +7 -3
- package/dist/pointer/PointerProbe.js +6 -2
- package/dist/pointer/pointerService.d.ts +3 -0
- package/dist/pointer/pointerService.js +6 -0
- 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/render/modelRender.d.ts +10 -1
- package/dist/render/modelRender.js +32 -5
- package/dist/render/resolveModel.d.ts +14 -0
- package/dist/render/resolveModel.js +24 -0
- package/dist/settings/QuickControls.d.ts +4 -0
- package/dist/settings/QuickControls.js +42 -0
- package/dist/settings/SettingsChrome.d.ts +1 -0
- package/dist/settings/SettingsChrome.js +10 -0
- package/dist/settings/SettingsMenu.d.ts +6 -0
- package/dist/settings/SettingsMenu.js +148 -0
- package/dist/settings/SettingsRuntime.d.ts +11 -0
- package/dist/settings/SettingsRuntime.js +19 -0
- package/dist/settings/appliedSettings.d.ts +14 -0
- package/dist/settings/appliedSettings.js +27 -0
- package/dist/settings/settingsController.d.ts +20 -0
- package/dist/settings/settingsController.js +165 -0
- package/dist/structures/GeneratedBuilding.d.ts +10 -0
- package/dist/structures/GeneratedBuilding.js +96 -8
- package/dist/structures/index.d.ts +1 -1
- package/dist/structures/index.js +1 -1
- package/dist/terrain/CarvedTerrain.d.ts +5 -1
- package/dist/terrain/CarvedTerrain.js +6 -5
- package/dist/terrain/GrassField.d.ts +3 -1
- package/dist/terrain/GrassField.js +4 -2
- package/dist/terrain/grassBudget.d.ts +3 -0
- package/dist/terrain/grassBudget.js +6 -0
- package/dist/terrain/grassGeometry.js +1 -1
- package/dist/terrain/terrainDetailMaterial.d.ts +14 -0
- package/dist/terrain/terrainDetailMaterial.js +90 -0
- package/dist/terrain/terrainMath.d.ts +8 -0
- package/dist/terrain/terrainMath.js +9 -0
- package/dist/touch/OrientationHint.d.ts +3 -0
- package/dist/touch/OrientationHint.js +13 -0
- package/dist/touch/TouchControlsOverlay.d.ts +17 -1
- package/dist/touch/TouchControlsOverlay.js +125 -9
- package/dist/visibility/CullingProvider.d.ts +21 -0
- package/dist/visibility/CullingProvider.js +134 -0
- package/dist/vision/FrustumSensorHud.d.ts +1 -6
- package/dist/vision/FrustumSensorHud.js +25 -27
- package/dist/vision/frustumSampleEqual.d.ts +2 -0
- package/dist/vision/frustumSampleEqual.js +10 -0
- package/dist/water/Ocean.js +1 -1
- package/dist/water/OceanConfig.d.ts +13 -0
- package/dist/water/OceanConfig.js +25 -17
- package/dist/water/index.d.ts +1 -1
- package/dist/water/index.js +1 -1
- package/dist/weather/FireSpreadLayer.js +7 -2
- package/dist/weather/RainField.d.ts +3 -1
- package/dist/weather/RainField.js +4 -4
- package/dist/weather/SnowField.d.ts +3 -1
- package/dist/weather/SnowField.js +4 -4
- package/dist/weather/fireSpreadPose.d.ts +2 -0
- package/dist/weather/fireSpreadPose.js +4 -0
- package/dist/weather/weatherMath.d.ts +5 -1
- package/dist/weather/weatherMath.js +7 -2
- package/dist/world/DataObjects.d.ts +1 -1
- package/dist/world/DataObjects.js +3 -1
- package/dist/world/SpriteBatch.d.ts +44 -0
- package/dist/world/SpriteBatch.js +112 -0
- package/dist/world/WorldHud.d.ts +6 -1
- package/dist/world/WorldHud.js +95 -48
- package/dist/world/entityPose.d.ts +14 -0
- package/dist/world/entityPose.js +10 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +4 -0
- package/dist/world/worldBarSamples.d.ts +30 -0
- package/dist/world/worldBarSamples.js +57 -0
- package/dist/worldSync.d.ts +10 -0
- package/dist/worldSync.js +19 -0
- package/llms.txt +1522 -1143
- package/package.json +4 -4
package/dist/GamePlayerShell.js
CHANGED
|
@@ -1,42 +1,70 @@
|
|
|
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, 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";
|
|
13
|
+
import { normalizePointerToAxis } from "@jgengine/core/input/pointerAxis";
|
|
11
14
|
import { createSelectionSet, isMarquee, screenRect, selectWithinRect, } from "@jgengine/core/scene/selection";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
15
|
+
import { steerYaw } from "@jgengine/core/movement/steering";
|
|
16
|
+
import { stepPlayerMovement, resolvePlayerMovementTuning } from "@jgengine/core/movement/playerMovement";
|
|
14
17
|
import { createGameContext } from "@jgengine/core/runtime/gameContext";
|
|
15
|
-
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";
|
|
16
22
|
import { objectVisualScale } from "@jgengine/core/scene/objectStore";
|
|
17
23
|
import { DEFAULT_PICKUP_RADIUS, WORLD_ITEM_ENTITY_NAME } from "@jgengine/core/game/worldItem";
|
|
18
24
|
import { useGameContext } from "@jgengine/react/provider";
|
|
19
25
|
import { useDisplayProfile } from "@jgengine/react/display";
|
|
26
|
+
import { HudViewportProvider } from "@jgengine/react/hudViewport";
|
|
27
|
+
import { GameViewportProvider } from "@jgengine/react/gameViewport";
|
|
28
|
+
import { RotateDeviceScreen } from "@jgengine/react/rotateDevice";
|
|
20
29
|
import { useSceneEntities, useSceneObjects, usePlayer, useTarget } from "@jgengine/react/hooks";
|
|
21
30
|
import { GameProvider } from "@jgengine/react/provider";
|
|
22
31
|
import { CAMERA_FRUSTUM_DEFAULTS } from "@jgengine/core/game/playableGame";
|
|
32
|
+
import { createSettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
33
|
+
import { applyBindingOverrides, clearBindingOverride, loadBindingOverrides, saveBindingOverride, } from "@jgengine/core/input/bindingOverrides";
|
|
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";
|
|
23
37
|
import { sky as resolveSkyDescriptor } from "@jgengine/core/world/features";
|
|
24
38
|
import { devtools } from "@jgengine/core/devtools/devtools";
|
|
39
|
+
import { VERSION } from "@jgengine/core/meta/changelog";
|
|
25
40
|
import { AudioListener, EntityAudioEmitters, ObjectAudioEmitters } from "./audio/AudioComponents.js";
|
|
26
41
|
import { createAudioEngine } from "./audio/audioEngine.js";
|
|
42
|
+
import { PostProcessing } from "./postfx/PostProcessing.js";
|
|
43
|
+
import { CollisionDebugWorld } from "./devtools/CollisionDebugWorld.js";
|
|
44
|
+
import { collisionDebug } from "./devtools/collisionDebug.js";
|
|
27
45
|
import { DevtoolsOverlay, DevtoolsRendererProbe, withDevtoolsLatency } from "./devtools/DevtoolsOverlay.js";
|
|
28
|
-
import { GAME_SIM_FRAME_PRIORITY, GameCameraRig, resolveRigKind, rtsPanKeysConflict } from "./camera/index.js";
|
|
46
|
+
import { GAME_SIM_FRAME_PRIORITY, GameCameraRig, PlayerFovProvider, PlayerFovSlider, resolveRigKind, rtsPanKeysConflict, } from "./camera/index.js";
|
|
47
|
+
import { resolveModel, tryResolveCatalogModel } from "./render/resolveModel.js";
|
|
48
|
+
import { CullingProvider, useRenderVisibility } from "./visibility/CullingProvider.js";
|
|
29
49
|
import { SkyDaylight, TimeOfDayDaylight } from "./environment.js";
|
|
50
|
+
import { resolveSkyLightOwnership, skyEmitsLights } from "./environment/skyLightingPolicy.js";
|
|
30
51
|
import { EnvironmentScene } from "./environment/EnvironmentScene.js";
|
|
31
52
|
import { applyMaterialOverride } from "./materialOverride.js";
|
|
32
53
|
import { PointerProbe } from "./pointer/PointerProbe.js";
|
|
33
|
-
import {
|
|
54
|
+
import { applyPaintTextureToMaterials, cacheStandardMaterials, cloneModelScene, createPaintCanvas, disposeClonedMaterials, syncPaintCanvas, } from "./render/modelRender.js";
|
|
55
|
+
import { writeEntityPose } from "./world/entityPose.js";
|
|
34
56
|
import { MarqueeBox, ContextMenuView } from "./pointer/PointerOverlays.js";
|
|
35
57
|
import { createPointerService, POINTER_ENTITY_KEY, POINTER_OBJECT_KEY, } from "./pointer/pointerService.js";
|
|
36
58
|
import { CombatCameraShake, ProjectileTracers, Reticle, WorldEntityBars, WorldFloatText, WorldTelegraphs, } from "./world/WorldHud.js";
|
|
37
59
|
import { GridWorldScene } from "./world/GridWorldScene.js";
|
|
38
60
|
import { WorldItems } from "./world/WorldItems.js";
|
|
39
|
-
import {
|
|
61
|
+
import { OrientationHint } from "./touch/OrientationHint.js";
|
|
62
|
+
import { TouchControlsDock, TouchPlaySurface, touchDockClearance } from "./touch/TouchControlsOverlay.js";
|
|
63
|
+
import { BUILT_IN_SETTING_CATEGORIES } from "@jgengine/core/settings/settingsModel";
|
|
64
|
+
import { SettingsProvider } from "@jgengine/react/settings";
|
|
65
|
+
import { SettingsRuntime } from "./settings/SettingsRuntime.js";
|
|
66
|
+
import { SettingsChrome } from "./settings/SettingsChrome.js";
|
|
67
|
+
import { AudioSettingsBridge, useGraphicsSettings } from "./settings/appliedSettings.js";
|
|
40
68
|
const DEV_USER_ID = "dev-player";
|
|
41
69
|
const TURN_SPEED = 2.4;
|
|
42
70
|
const PRIMARY_CLICK_MOVE_THRESHOLD_PX = 6;
|
|
@@ -44,14 +72,15 @@ const GROUND_SIZE = 160;
|
|
|
44
72
|
const GROUND_SEGMENTS = 80;
|
|
45
73
|
const DEFAULT_BACKGROUND_COLOR = "#14161b";
|
|
46
74
|
const DEFAULT_BACKDROP_FOG_COLOR = "#1a1c22";
|
|
47
|
-
function errorToDiagnostic(error, phase) {
|
|
75
|
+
function errorToDiagnostic(error, phase, componentStack) {
|
|
76
|
+
const capturedAt = new Date().toISOString();
|
|
48
77
|
if (error instanceof Error) {
|
|
49
|
-
return { phase, message: error.message, stack: error.stack };
|
|
78
|
+
return { phase, message: error.message, stack: error.stack, componentStack, capturedAt };
|
|
50
79
|
}
|
|
51
|
-
return { phase, message: typeof error === "string" ? error : JSON.stringify(error) };
|
|
80
|
+
return { phase, message: typeof error === "string" ? error : JSON.stringify(error), componentStack, capturedAt };
|
|
52
81
|
}
|
|
53
|
-
function logRuntimeError(error, phase) {
|
|
54
|
-
const diagnostic = errorToDiagnostic(error, phase);
|
|
82
|
+
function logRuntimeError(error, phase, componentStack) {
|
|
83
|
+
const diagnostic = errorToDiagnostic(error, phase, componentStack);
|
|
55
84
|
console.error(`[jgengine:${phase}] ${diagnostic.message}`, error);
|
|
56
85
|
return diagnostic;
|
|
57
86
|
}
|
|
@@ -69,6 +98,10 @@ const RESERVED_INPUT_ACTIONS = new Set([
|
|
|
69
98
|
"useAbility",
|
|
70
99
|
"interact",
|
|
71
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 = [];
|
|
72
105
|
const SHELL_MOVEMENT_ACTIONS = ["moveForward", "moveBack", "moveLeft", "moveRight", "jump"];
|
|
73
106
|
function shellDrivesPlayerPose(input) {
|
|
74
107
|
const bound = input ?? {};
|
|
@@ -101,9 +134,12 @@ function pointerAimFor(ctx, service) {
|
|
|
101
134
|
const hit = service.worldHit();
|
|
102
135
|
if (hit === null)
|
|
103
136
|
return undefined;
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
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);
|
|
107
143
|
}
|
|
108
144
|
function pointerContextMenu(ctx, playable, hit) {
|
|
109
145
|
if (hit.entity !== null) {
|
|
@@ -132,72 +168,35 @@ export function shouldFireBoundAction(tracker, action, input, repeatFiredAt, now
|
|
|
132
168
|
});
|
|
133
169
|
}
|
|
134
170
|
/** Resolves and runs the command bound to `action` via the shell's action→command convention (shared by `FrameDriver` and `HudOnlyDriver`). */
|
|
135
|
-
export function dispatchBoundAction(ctx, action, yaw, pitch, aim) {
|
|
136
|
-
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);
|
|
137
173
|
if (command !== null)
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
const OBSTACLE_GATHER_RADIUS = 3;
|
|
141
|
-
/** Placed scene objects within `radius` of `center`, as `CollisionObstacle`s for `resolveObstacleStep` (#162.1). */
|
|
142
|
-
export function nearbyObstacles(objects, center, radius = OBSTACLE_GATHER_RADIUS) {
|
|
143
|
-
const radiusSq = radius * radius;
|
|
144
|
-
const result = [];
|
|
145
|
-
for (const object of objects) {
|
|
146
|
-
const dx = object.position[0] - center[0];
|
|
147
|
-
const dz = object.position[2] - center[2];
|
|
148
|
-
if (dx * dx + dz * dz <= radiusSq)
|
|
149
|
-
result.push({ position: object.position });
|
|
150
|
-
}
|
|
151
|
-
return result;
|
|
152
|
-
}
|
|
153
|
-
/** Applies a pending `MotionIntentBatch` to a vertical velocity: impulses add, then `verticalVelocity` replaces the result outright (#162.4). */
|
|
154
|
-
export function applyMotionImpulses(currentVelocity, batch) {
|
|
155
|
-
if (batch === null)
|
|
156
|
-
return currentVelocity;
|
|
157
|
-
let velocity = currentVelocity;
|
|
158
|
-
for (const impulse of batch.impulses)
|
|
159
|
-
velocity += impulse;
|
|
160
|
-
return batch.verticalVelocity ?? velocity;
|
|
174
|
+
sink.run(command, { yaw, pitch, aim });
|
|
161
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";
|
|
162
180
|
/** The world's declared sky, when its world feature is an environment with one (#196.1). */
|
|
163
181
|
export function resolveWorldSky(world) {
|
|
164
182
|
return world?.kind === "environment" ? world.sky : undefined;
|
|
165
183
|
}
|
|
166
|
-
/**
|
|
167
|
-
* Maps the game's declared `physics` onto the movement controllers' tuning
|
|
168
|
-
* overrides. `PhysicsConfig.gravity` is a signed world acceleration (negative
|
|
169
|
-
* points down, matching every game's config and the Y-up convention), but the
|
|
170
|
-
* controllers integrate `velocityY -= gravityAcceleration * dt` and so expect a
|
|
171
|
-
* positive downward magnitude. Negating here is what keeps a down-pointing
|
|
172
|
-
* gravity pulling the player *down*; passing the signed value straight through
|
|
173
|
-
* flipped the sign and launched airborne players upward instead.
|
|
174
|
-
*/
|
|
175
|
-
export function resolvePhysicsTuning(physics) {
|
|
176
|
-
if (physics?.gravity === undefined && physics?.jumpVelocity === undefined)
|
|
177
|
-
return undefined;
|
|
178
|
-
const tuning = {};
|
|
179
|
-
if (physics.gravity !== undefined)
|
|
180
|
-
tuning.gravityAcceleration = -physics.gravity;
|
|
181
|
-
if (physics.jumpVelocity !== undefined)
|
|
182
|
-
tuning.jumpVelocity = physics.jumpVelocity;
|
|
183
|
-
return tuning;
|
|
184
|
-
}
|
|
185
|
-
/** True when the world is an environment feature with terrain, so the voxel controller should sample its height. */
|
|
186
|
-
export function hasEnvironmentTerrain(world) {
|
|
187
|
-
return world?.kind === "environment" && world.terrain !== undefined;
|
|
188
|
-
}
|
|
189
184
|
function colorFromId(id) {
|
|
190
185
|
let hash = 0;
|
|
191
186
|
for (let index = 0; index < id.length; index += 1)
|
|
192
187
|
hash = (hash * 31 + id.charCodeAt(index)) >>> 0;
|
|
193
188
|
return `hsl(${hash % 360}, 65%, 55%)`;
|
|
194
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
|
+
}
|
|
195
194
|
function ConfiguredLighting({ lighting }) {
|
|
196
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: [
|
|
197
196
|
lighting.hemisphere.skyColor ?? "#bfe3ff",
|
|
198
197
|
lighting.hemisphere.groundColor ?? "#4c6b34",
|
|
199
198
|
lighting.hemisphere.intensity ?? 0.55,
|
|
200
|
-
] })) : null, (lighting.directional ?? []).map((entry, index) => (_jsx(
|
|
199
|
+
] })) : null, (lighting.directional ?? []).map((entry, index) => (_jsx(DirectionalShadowLight, { entry: entry }, index)))] }));
|
|
201
200
|
}
|
|
202
201
|
function BackdropFog({ fog }) {
|
|
203
202
|
if (fog === undefined)
|
|
@@ -213,32 +212,193 @@ function EntitySprite({ sprite }) {
|
|
|
213
212
|
}, [texture]);
|
|
214
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 }) }));
|
|
215
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
|
+
}
|
|
216
271
|
function EntityModel({ model, instanceId }) {
|
|
217
|
-
const gltf = useLoader(GLTFLoader, model.url)
|
|
272
|
+
const gltf = useLoader(GLTFLoader, model.url, (loader) => {
|
|
273
|
+
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
274
|
+
});
|
|
218
275
|
const ctx = useGameContext();
|
|
219
276
|
const material = model.material;
|
|
220
|
-
const scale = model.scale ?? 1;
|
|
221
277
|
const baseY = model.y ?? 0;
|
|
222
278
|
const dims = model.dims;
|
|
223
|
-
const centered = (model.anchor ?? "center") === "center" && dims !== undefined;
|
|
224
|
-
const position = centered
|
|
225
|
-
? [-scale * dims.center.x, baseY - scale * dims.minY, -scale * dims.center.z]
|
|
226
|
-
: [0, baseY, 0];
|
|
227
279
|
const scene = useMemo(() => {
|
|
228
280
|
const cloned = cloneModelScene(gltf.scene);
|
|
229
281
|
if (material !== undefined)
|
|
230
|
-
applyMaterialOverride(cloned, material);
|
|
282
|
+
applyMaterialOverride(cloned, material, { clone: false });
|
|
231
283
|
return cloned;
|
|
232
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];
|
|
306
|
+
useEffect(() => () => {
|
|
307
|
+
disposeClonedMaterials(scene);
|
|
308
|
+
}, [scene]);
|
|
233
309
|
const animation = model.animation;
|
|
234
310
|
const mixerRef = useRef(null);
|
|
235
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);
|
|
236
317
|
useEffect(() => {
|
|
237
318
|
if (animation === undefined || gltf.animations.length === 0) {
|
|
238
319
|
mixerRef.current = null;
|
|
320
|
+
stateActionsRef.current = null;
|
|
239
321
|
return;
|
|
240
322
|
}
|
|
241
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
|
+
}
|
|
242
402
|
const clip = (animation.clip !== undefined ? THREE.AnimationClip.findByName(gltf.animations, animation.clip) : undefined) ??
|
|
243
403
|
gltf.animations[0];
|
|
244
404
|
const action = mixer.clipAction(clip);
|
|
@@ -257,16 +417,82 @@ function EntityModel({ model, instanceId }) {
|
|
|
257
417
|
mixer.stopAllAction();
|
|
258
418
|
mixerRef.current = null;
|
|
259
419
|
};
|
|
260
|
-
}, [
|
|
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]);
|
|
261
453
|
const paintCanvasRef = useRef(null);
|
|
262
454
|
const paintDrawnCountRef = useRef(0);
|
|
263
455
|
const paintVersionRef = useRef(-1);
|
|
456
|
+
const materialCacheRef = useRef(null);
|
|
264
457
|
useEffect(() => {
|
|
265
458
|
paintCanvasRef.current = null;
|
|
266
459
|
paintDrawnCountRef.current = 0;
|
|
267
460
|
paintVersionRef.current = -1;
|
|
461
|
+
materialCacheRef.current = null;
|
|
268
462
|
}, [scene]);
|
|
269
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
|
+
}
|
|
270
496
|
if (mixerRef.current !== null && !animationPausedRef.current)
|
|
271
497
|
mixerRef.current.update(delta);
|
|
272
498
|
if (instanceId === undefined)
|
|
@@ -277,45 +503,68 @@ function EntityModel({ model, instanceId }) {
|
|
|
277
503
|
return;
|
|
278
504
|
paintVersionRef.current = version;
|
|
279
505
|
const strokes = paint.strokes(instanceId);
|
|
506
|
+
const cache = cacheStandardMaterials(scene, materialCacheRef.current);
|
|
507
|
+
materialCacheRef.current = cache;
|
|
280
508
|
if (paintCanvasRef.current === null) {
|
|
281
509
|
if (strokes.length === 0)
|
|
282
510
|
return;
|
|
283
|
-
const
|
|
284
|
-
const seed = materials[0];
|
|
511
|
+
const seed = cache.materials[0];
|
|
285
512
|
if (seed === undefined)
|
|
286
513
|
return;
|
|
287
514
|
const paintCanvas = createPaintCanvas(seed);
|
|
288
515
|
paintCanvasRef.current = paintCanvas;
|
|
289
|
-
|
|
516
|
+
applyPaintTextureToMaterials(cache.materials, paintCanvas);
|
|
290
517
|
}
|
|
291
|
-
|
|
292
|
-
paintDrawnCountRef.current = syncPaintCanvas(paintCanvasRef.current, seedColor, strokes, paintDrawnCountRef.current);
|
|
518
|
+
paintDrawnCountRef.current = syncPaintCanvas(paintCanvasRef.current, cache.seedColor, strokes, paintDrawnCountRef.current);
|
|
293
519
|
});
|
|
294
|
-
return _jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] });
|
|
295
|
-
}
|
|
296
|
-
function resolveModel(value, assets) {
|
|
297
|
-
if (value === undefined)
|
|
298
|
-
return undefined;
|
|
299
|
-
if (typeof value !== "string")
|
|
300
|
-
return value;
|
|
301
|
-
const ref = assets.resolve(value);
|
|
302
|
-
if (ref === null)
|
|
303
|
-
return undefined;
|
|
304
|
-
return ref.dims === undefined ? { url: ref.url } : { url: ref.url, dims: ref.dims };
|
|
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}`)))] }));
|
|
305
521
|
}
|
|
306
522
|
function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, selected, onSelect, }) {
|
|
307
|
-
const
|
|
308
|
-
|
|
523
|
+
const groupRef = useRef(null);
|
|
524
|
+
const ctx = useGameContext();
|
|
525
|
+
const visibleRef = useRenderVisibility();
|
|
526
|
+
const entityId = entity.id;
|
|
527
|
+
const role = entity.role;
|
|
528
|
+
const name = entity.name;
|
|
529
|
+
const color = isLocal ? "#4ade80" : role === "npc" ? colorFromId(name) : "#9ca3af";
|
|
530
|
+
useFrame(() => {
|
|
531
|
+
const group = groupRef.current;
|
|
532
|
+
if (group === null)
|
|
533
|
+
return;
|
|
534
|
+
const live = ctx.scene.entity.get(entityId);
|
|
535
|
+
if (live === null)
|
|
536
|
+
return;
|
|
537
|
+
writeEntityPose(group, live);
|
|
538
|
+
group.visible = visibleRef.current(entityId);
|
|
539
|
+
});
|
|
540
|
+
return (_jsxs("group", { ref: groupRef, userData: { [POINTER_ENTITY_KEY]: entityId }, onPointerDown: (event) => {
|
|
309
541
|
event.stopPropagation();
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
|
|
542
|
+
if (isLocal)
|
|
543
|
+
return;
|
|
544
|
+
const live = ctx.scene.entity.get(entityId);
|
|
545
|
+
if (live !== null)
|
|
546
|
+
onSelect(live);
|
|
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] }));
|
|
313
548
|
}
|
|
314
549
|
function ObjectMarker({ object, custom, model, style, }) {
|
|
550
|
+
const groupRef = useRef(null);
|
|
551
|
+
const ctx = useGameContext();
|
|
552
|
+
const visibleRef = useRenderVisibility();
|
|
553
|
+
const instanceId = object.instanceId;
|
|
315
554
|
const [scaleX, scaleY, scaleZ] = objectVisualScale(object.visual);
|
|
316
555
|
const color = object.visual?.color ?? style?.color ?? colorFromId(object.catalogId);
|
|
317
556
|
const opacity = object.visual?.opacity ?? style?.opacity ?? 1;
|
|
318
|
-
|
|
557
|
+
useFrame(() => {
|
|
558
|
+
const group = groupRef.current;
|
|
559
|
+
if (group === null)
|
|
560
|
+
return;
|
|
561
|
+
const live = ctx.scene.object.get(instanceId);
|
|
562
|
+
if (live === null)
|
|
563
|
+
return;
|
|
564
|
+
writeEntityPose(group, live);
|
|
565
|
+
group.visible = visibleRef.current(instanceId);
|
|
566
|
+
});
|
|
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 })] })) }));
|
|
319
568
|
}
|
|
320
569
|
function GroundPlane() {
|
|
321
570
|
const geometry = useMemo(() => {
|
|
@@ -348,7 +597,12 @@ function RockField() {
|
|
|
348
597
|
}), []);
|
|
349
598
|
return (_jsx(_Fragment, { children: rocks.map((rock) => (_jsxs("mesh", { position: [rock.x, 0.25 * rock.scale, rock.z], rotation: [0.1, rock.rotation, -0.08], scale: [rock.scale * 1.4, rock.scale * 0.7, rock.scale], children: [_jsx("dodecahedronGeometry", { args: [0.8, 0] }), _jsx("meshStandardMaterial", { color: "#6b6f63", roughness: 1 })] }, rock.id))) }));
|
|
350
599
|
}
|
|
351
|
-
function
|
|
600
|
+
function WorldEnvironment({ environment: Environment }) {
|
|
601
|
+
if (Environment !== undefined)
|
|
602
|
+
return _jsx(Environment, {});
|
|
603
|
+
return (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx("gridHelper", { args: [160, 80, "#3a3f4a", "#2b2f38"], "position-y": 0.01 }), _jsx(RockField, {})] }));
|
|
604
|
+
}
|
|
605
|
+
function WorldActors({ entitySprites, entityModels, objectModels, objectStyles, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }) {
|
|
352
606
|
const ctx = useGameContext();
|
|
353
607
|
const entities = useSceneEntities();
|
|
354
608
|
const objects = useSceneObjects();
|
|
@@ -359,33 +613,36 @@ function WorldView({ entitySprites, entityModels, objectModels, objectStyles, en
|
|
|
359
613
|
const relation = ctx.scene.entity.canReceive(entity.id, "damage") === null ? "hostile" : "friendly";
|
|
360
614
|
ctx.scene.entity.setTarget(controlledId, relation === "hostile" || entity.role === "npc" ? entity.id : null);
|
|
361
615
|
};
|
|
362
|
-
return (_jsxs(_Fragment, { children: [
|
|
616
|
+
return (_jsxs(_Fragment, { children: [entities
|
|
363
617
|
.filter((entity) => entity.name !== WORLD_ITEM_ENTITY_NAME)
|
|
364
|
-
.
|
|
365
|
-
|
|
366
|
-
|
|
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) => {
|
|
620
|
+
const model = resolveModel(objectModels?.[object.catalogId], assets, {
|
|
621
|
+
seam: "objectModels",
|
|
622
|
+
key: object.catalogId,
|
|
623
|
+
}) ?? tryResolveCatalogModel(object.catalogId, assets);
|
|
367
624
|
return (_jsx(ObjectMarker, { object: object, custom: renderObject?.(object), model: model, style: objectStyles?.[object.catalogId] }, object.instanceId));
|
|
368
625
|
})] }));
|
|
369
626
|
}
|
|
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 })] }));
|
|
629
|
+
}
|
|
370
630
|
function RemotePlayers({ rows }) {
|
|
371
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))) }));
|
|
372
632
|
}
|
|
373
|
-
function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, }) {
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
const solidCacheRef = useRef({ count: -1, set: new Set() });
|
|
633
|
+
function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, gateRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }) {
|
|
634
|
+
const posterElapsedRef = useRef(0);
|
|
635
|
+
const posterDoneRef = useRef(false);
|
|
377
636
|
const hasReportedTickError = useRef(false);
|
|
378
637
|
const repeatFiredAtRef = useRef(new Map());
|
|
379
638
|
const slotActions = useMemo(() => findHotbarSlotActions(playable.game.input), [playable]);
|
|
380
639
|
const hotbarId = useMemo(() => hotbarIdFor(playable), [playable]);
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}), [collision]);
|
|
388
|
-
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]);
|
|
389
646
|
const autoPickupRadius = useMemo(() => {
|
|
390
647
|
const cfg = playable.worldItem?.autoPickup;
|
|
391
648
|
if (cfg === undefined || cfg === false)
|
|
@@ -393,121 +650,85 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
393
650
|
const fallback = playable.worldItem?.pickupRadius ?? DEFAULT_PICKUP_RADIUS;
|
|
394
651
|
return cfg === true ? fallback : cfg.radius ?? fallback;
|
|
395
652
|
}, [playable]);
|
|
396
|
-
const ground = useMemo(() => groundFieldFor(playable.game.world), [playable]);
|
|
397
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);
|
|
398
670
|
const inputActions = useMemo(() => Object.keys(playable.game.input ?? {}), [playable]);
|
|
399
|
-
const hasTerrain = useMemo(() => hasEnvironmentTerrain(playable.game.world), [playable]);
|
|
400
671
|
useFrame((_state, rawDt) => {
|
|
672
|
+
if (poster) {
|
|
673
|
+
if (posterDoneRef.current)
|
|
674
|
+
return;
|
|
675
|
+
posterElapsedRef.current += Math.min(rawDt, 0.05);
|
|
676
|
+
if (posterElapsedRef.current >= POSTER_SETTLE_SECONDS) {
|
|
677
|
+
posterDoneRef.current = true;
|
|
678
|
+
onPosterSettled();
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
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
|
+
}
|
|
401
697
|
const simStart = performance.now();
|
|
402
698
|
try {
|
|
699
|
+
let endPhase = devtools.profile.begin("time+input");
|
|
403
700
|
const dt = Math.min(rawDt, 0.05);
|
|
404
701
|
const gameDt = ctx.time.advance(dt);
|
|
405
702
|
ctx.input.publish(heldActionsFor(tracker, inputActions));
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
703
|
+
ctx.input.publishPointer(pointerAxisRef.current);
|
|
704
|
+
sendInput();
|
|
705
|
+
const turnInput = (tracker.isDown("turnRight") ? 1 : 0) - (tracker.isDown("turnLeft") ? 1 : 0);
|
|
706
|
+
if (turnInput !== 0)
|
|
707
|
+
yawRef.current = steerYaw(yawRef.current, turnInput, TURN_SPEED, dt);
|
|
708
|
+
endPhase();
|
|
410
709
|
const playerId = ctx.player.possession.active(ctx.player.userId);
|
|
411
710
|
const player = ctx.scene.entity.get(playerId);
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
keys.w = tracker.isDown("moveForward");
|
|
417
|
-
keys.s = tracker.isDown("moveBack");
|
|
418
|
-
keys.a = tracker.isDown("moveLeft");
|
|
419
|
-
keys.d = tracker.isDown("moveRight");
|
|
420
|
-
keys.shift = tracker.isDown("sprint");
|
|
421
|
-
keys.space = tracker.isDown("jump");
|
|
422
|
-
const intent = resolveMovementIntent(keys, true);
|
|
423
|
-
const motionBatch = ctx.player.motion.takePending();
|
|
424
|
-
if (collision?.voxel) {
|
|
425
|
-
let body = voxelBodyRef.current;
|
|
426
|
-
if (body === null) {
|
|
427
|
-
body = createVoxelPlayerBody(player.position[0], player.position[1], player.position[2]);
|
|
428
|
-
voxelBodyRef.current = body;
|
|
429
|
-
}
|
|
430
|
-
const objects = ctx.scene.object.list();
|
|
431
|
-
const cache = solidCacheRef.current;
|
|
432
|
-
if (cache.count !== objects.length) {
|
|
433
|
-
cache.set = new Set(objects.map((o) => `${o.position[0]},${o.position[1]},${o.position[2]}`));
|
|
434
|
-
cache.count = objects.length;
|
|
435
|
-
}
|
|
436
|
-
const solids = cache.set;
|
|
437
|
-
const isSolid = (x, y, z) => solids.has(`${x},${y},${z}`);
|
|
438
|
-
body.velocityY = applyMotionImpulses(body.velocityY, motionBatch);
|
|
439
|
-
advanceVoxelPlayer(body, intent, forwardX, forwardZ, player.movement.walkSpeed ?? 2, rawDt, isSolid, voxelDims, movementTuning, hasTerrain ? (x, z) => ground.sampleHeight(x, z) : undefined);
|
|
440
|
-
if (motionBatch !== null && motionBatch.y !== null)
|
|
441
|
-
body.y = motionBatch.y;
|
|
442
|
-
ctx.scene.entity.setPose(playerId, {
|
|
443
|
-
position: [body.x, body.y, body.z],
|
|
444
|
-
rotationY: intent.moving
|
|
445
|
-
? Math.atan2(body.velocityX, body.velocityZ)
|
|
446
|
-
: player.rotationY,
|
|
447
|
-
dt: rawDt,
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
else {
|
|
451
|
-
const motion = motionRef.current;
|
|
452
|
-
motion.verticalVelocity = applyMotionImpulses(motion.verticalVelocity, motionBatch);
|
|
453
|
-
const step = advancePlayerMotion(motion, intent, forwardX, forwardZ, player.movement.walkSpeed ?? 2, rawDt, movementTuning);
|
|
454
|
-
let stepX = step.stepX;
|
|
455
|
-
let stepZ = step.stepZ;
|
|
456
|
-
if (movement?.mode === "axis") {
|
|
457
|
-
const constrained = constrainStepToAxis(stepX, stepZ, movement.axis ?? "x");
|
|
458
|
-
stepX = constrained.stepX;
|
|
459
|
-
stepZ = constrained.stepZ;
|
|
460
|
-
}
|
|
461
|
-
if (movement?.collideObjects === true) {
|
|
462
|
-
const obstacles = nearbyObstacles(ctx.scene.object.list(), player.position);
|
|
463
|
-
const resolved = resolveObstacleStep(player.position, stepX, stepZ, obstacles);
|
|
464
|
-
stepX = resolved.stepX;
|
|
465
|
-
stepZ = resolved.stepZ;
|
|
466
|
-
}
|
|
467
|
-
let nextX = player.position[0] + stepX;
|
|
468
|
-
let nextZ = player.position[2] + stepZ;
|
|
469
|
-
if (movement?.mode === "grid") {
|
|
470
|
-
const snapped = snapPositionToGrid(nextX, nextZ, movement.cellSize ?? 1);
|
|
471
|
-
nextX = snapped[0];
|
|
472
|
-
nextZ = snapped[1];
|
|
473
|
-
}
|
|
474
|
-
let nextY = ground.sampleHeight(nextX, nextZ) + motion.jumpOffset;
|
|
475
|
-
if (motionBatch !== null && motionBatch.y !== null) {
|
|
476
|
-
nextY = motionBatch.y;
|
|
477
|
-
motion.jumpOffset = motionBatch.y - ground.sampleHeight(nextX, nextZ);
|
|
478
|
-
}
|
|
479
|
-
if (movement?.beforeCommit !== undefined) {
|
|
480
|
-
const frame = {
|
|
481
|
-
entityId: playerId,
|
|
482
|
-
current: player.position,
|
|
483
|
-
next: [nextX, nextY, nextZ],
|
|
484
|
-
dt: rawDt,
|
|
485
|
-
};
|
|
486
|
-
const replacement = movement.beforeCommit(frame);
|
|
487
|
-
if (replacement !== undefined) {
|
|
488
|
-
nextX = replacement[0];
|
|
489
|
-
nextY = replacement[1];
|
|
490
|
-
nextZ = replacement[2];
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
ctx.scene.entity.setPose(playerId, {
|
|
494
|
-
position: [nextX, nextY, nextZ],
|
|
495
|
-
rotationY: intent.moving
|
|
496
|
-
? Math.atan2(motion.horizontalVelocityX, motion.horizontalVelocityZ)
|
|
497
|
-
: player.rotationY,
|
|
498
|
-
dt: rawDt,
|
|
499
|
-
});
|
|
500
|
-
}
|
|
711
|
+
if (player !== null && drivesPose && !serverAuthoritative) {
|
|
712
|
+
endPhase = devtools.profile.begin("pose");
|
|
713
|
+
stepPlayerMovement(ctx, ctx.player.userId, { held: ctx.input.held(), pointer: ctx.input.pointer() }, rawDt, movementTuning, yawRef.current);
|
|
714
|
+
endPhase();
|
|
501
715
|
}
|
|
502
|
-
if (autoPickupRadius !== null) {
|
|
716
|
+
if (autoPickupRadius !== null && !serverAuthoritative) {
|
|
717
|
+
endPhase = devtools.profile.begin("pickup");
|
|
503
718
|
const self = ctx.scene.entity.get(playerId);
|
|
504
719
|
if (self !== null) {
|
|
505
720
|
const nearest = ctx.scene.worldItem.nearestInRadius(self.position, autoPickupRadius);
|
|
506
721
|
if (nearest !== null)
|
|
507
722
|
ctx.scene.worldItem.pickup(nearest, ctx.player.userId);
|
|
508
723
|
}
|
|
724
|
+
endPhase();
|
|
509
725
|
}
|
|
510
|
-
|
|
726
|
+
if (!serverAuthoritative) {
|
|
727
|
+
devtools.profile.measure("onTick", () => {
|
|
728
|
+
playable.loop.onTick(ctx, gameDt);
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
endPhase = devtools.profile.begin("actions");
|
|
511
732
|
if (tracker.wasPressed("tabTarget")) {
|
|
512
733
|
if (ctx.game.commands.has("target.cycle"))
|
|
513
734
|
ctx.game.commands.run("target.cycle", {});
|
|
@@ -523,7 +744,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
523
744
|
if (pingCommand !== undefined && tracker.wasPressed("ping")) {
|
|
524
745
|
const hit = pointerService.worldHit();
|
|
525
746
|
if (hit !== null && ctx.game.commands.has(pingCommand)) {
|
|
526
|
-
|
|
747
|
+
commandSink.run(pingCommand, {
|
|
527
748
|
point: hit.point,
|
|
528
749
|
entity: hit.entity,
|
|
529
750
|
object: hit.object,
|
|
@@ -533,6 +754,18 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
533
754
|
}
|
|
534
755
|
const aimOverride = pointerAim ? pointerAimFor(ctx, pointerService) : undefined;
|
|
535
756
|
const commandAim = aimOverride ?? { yaw: yawRef.current, pitch: pitchRef.current };
|
|
757
|
+
if (collisionDebug.getState().layers.aimLaser) {
|
|
758
|
+
const aimFrom = ctx.player.possession.active(playerId) ?? playerId;
|
|
759
|
+
collisionDebug.setAimProbe({
|
|
760
|
+
from: aimFrom,
|
|
761
|
+
aim: commandAim,
|
|
762
|
+
originPolicy: { kind: "eye" },
|
|
763
|
+
maxDistance: 100,
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
else if (collisionDebug.getAimProbe() !== null) {
|
|
767
|
+
collisionDebug.setAimProbe(null);
|
|
768
|
+
}
|
|
536
769
|
const nowMs = performance.now();
|
|
537
770
|
for (const action of Object.keys(playable.game.input ?? {})) {
|
|
538
771
|
const pressed = tracker.wasPressed(action);
|
|
@@ -546,7 +779,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
546
779
|
if (prompts !== undefined && focus !== null) {
|
|
547
780
|
const active = resolveActivePrompt({ x: focus.position[0], z: focus.position[2] }, prompts);
|
|
548
781
|
if (active !== null && active.prompt.invoke !== null) {
|
|
549
|
-
|
|
782
|
+
commandSink.run(active.prompt.invoke.name, active.prompt.invoke.input);
|
|
550
783
|
}
|
|
551
784
|
}
|
|
552
785
|
continue;
|
|
@@ -554,7 +787,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
554
787
|
if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
|
|
555
788
|
continue;
|
|
556
789
|
repeatFiredAtRef.current.set(action, nowMs);
|
|
557
|
-
dispatchBoundAction(ctx, action, yawRef.current, pitchRef.current, commandAim);
|
|
790
|
+
dispatchBoundAction(ctx, action, yawRef.current, pitchRef.current, commandAim, RESERVED_INPUT_ACTIONS, commandSink);
|
|
558
791
|
}
|
|
559
792
|
if (hotbarId !== null) {
|
|
560
793
|
for (const { action, slot } of slotActions) {
|
|
@@ -580,8 +813,10 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
580
813
|
}
|
|
581
814
|
}
|
|
582
815
|
tracker.endFrame();
|
|
816
|
+
endPhase();
|
|
583
817
|
const serverId = serverIdRef.current;
|
|
584
818
|
if (multiplayer !== null && serverId !== null) {
|
|
819
|
+
endPhase = devtools.profile.begin("presence");
|
|
585
820
|
const focus = ctx.scene.entity.get(playerId);
|
|
586
821
|
if (focus !== null) {
|
|
587
822
|
multiplayer.backend.presenceSync.syncPose(serverId, {
|
|
@@ -592,6 +827,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
592
827
|
rotationPitch: pitchRef.current,
|
|
593
828
|
});
|
|
594
829
|
}
|
|
830
|
+
endPhase();
|
|
595
831
|
}
|
|
596
832
|
}
|
|
597
833
|
catch (error) {
|
|
@@ -604,7 +840,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
604
840
|
}, GAME_SIM_FRAME_PRIORITY);
|
|
605
841
|
return null;
|
|
606
842
|
}
|
|
607
|
-
function HudOnlyDriver({ ctx, playable, tracker, onRuntimeError, }) {
|
|
843
|
+
function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, gateRef, onRuntimeError, }) {
|
|
608
844
|
const hasReportedTickError = useRef(false);
|
|
609
845
|
const repeatFiredAtRef = useRef(new Map());
|
|
610
846
|
const lastFrameRef = useRef(null);
|
|
@@ -617,21 +853,32 @@ function HudOnlyDriver({ ctx, playable, tracker, onRuntimeError, }) {
|
|
|
617
853
|
lastFrameRef.current = now;
|
|
618
854
|
if (last === null)
|
|
619
855
|
return;
|
|
856
|
+
if (gateRef.current) {
|
|
857
|
+
ctx.input.publish(heldActionsFor(tracker, NO_ACTIONS));
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
620
860
|
const rawDt = (now - last) / 1000;
|
|
621
861
|
const simStart = performance.now();
|
|
622
862
|
try {
|
|
863
|
+
let endPhase = devtools.profile.begin("time+input");
|
|
623
864
|
const dt = Math.min(rawDt, 0.05);
|
|
624
865
|
const gameDt = ctx.time.advance(dt);
|
|
625
866
|
ctx.input.publish(heldActionsFor(tracker, inputActions));
|
|
626
|
-
|
|
867
|
+
ctx.input.publishPointer(pointerAxisRef.current);
|
|
868
|
+
endPhase();
|
|
869
|
+
devtools.profile.measure("onTick", () => {
|
|
870
|
+
playable.loop.onTick(ctx, gameDt);
|
|
871
|
+
});
|
|
872
|
+
endPhase = devtools.profile.begin("actions");
|
|
627
873
|
const nowMs = performance.now();
|
|
628
874
|
for (const action of inputActions) {
|
|
629
875
|
if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
|
|
630
876
|
continue;
|
|
631
877
|
repeatFiredAtRef.current.set(action, nowMs);
|
|
632
|
-
dispatchBoundAction(ctx, action, 0, 0, { yaw: 0, pitch: 0 });
|
|
878
|
+
dispatchBoundAction(ctx, action, 0, 0, { yaw: 0, pitch: 0 }, EMPTY_RESERVED);
|
|
633
879
|
}
|
|
634
880
|
tracker.endFrame();
|
|
881
|
+
endPhase();
|
|
635
882
|
}
|
|
636
883
|
catch (error) {
|
|
637
884
|
if (!hasReportedTickError.current) {
|
|
@@ -643,7 +890,7 @@ function HudOnlyDriver({ ctx, playable, tracker, onRuntimeError, }) {
|
|
|
643
890
|
};
|
|
644
891
|
frameId = requestAnimationFrame(tick);
|
|
645
892
|
return () => cancelAnimationFrame(frameId);
|
|
646
|
-
}, [ctx, playable, tracker, onRuntimeError, inputActions]);
|
|
893
|
+
}, [ctx, playable, tracker, pointerAxisRef, gateRef, onRuntimeError, inputActions]);
|
|
647
894
|
return null;
|
|
648
895
|
}
|
|
649
896
|
class GameUiErrorBoundary extends Component {
|
|
@@ -651,8 +898,8 @@ class GameUiErrorBoundary extends Component {
|
|
|
651
898
|
static getDerivedStateFromError() {
|
|
652
899
|
return { failed: true };
|
|
653
900
|
}
|
|
654
|
-
componentDidCatch(error) {
|
|
655
|
-
this.props.onRuntimeError(error, "ui-render");
|
|
901
|
+
componentDidCatch(error, info) {
|
|
902
|
+
this.props.onRuntimeError(error, "ui-render", info.componentStack ?? undefined);
|
|
656
903
|
}
|
|
657
904
|
render() {
|
|
658
905
|
if (this.state.failed)
|
|
@@ -660,16 +907,53 @@ class GameUiErrorBoundary extends Component {
|
|
|
660
907
|
return this.props.children;
|
|
661
908
|
}
|
|
662
909
|
}
|
|
663
|
-
function
|
|
910
|
+
function expandedReactError(message) {
|
|
911
|
+
return /Minified React error #185\b/.test(message)
|
|
912
|
+
? "Maximum update depth exceeded. A component is repeatedly updating state during render or after every update."
|
|
913
|
+
: null;
|
|
914
|
+
}
|
|
915
|
+
function diagnosticReport(diagnostic, gameName) {
|
|
916
|
+
const explanation = expandedReactError(diagnostic.message);
|
|
917
|
+
return [
|
|
918
|
+
"JGengine runtime error",
|
|
919
|
+
`Game: ${gameName}`,
|
|
920
|
+
`Engine: ${VERSION}`,
|
|
921
|
+
`Phase: ${diagnostic.phase}`,
|
|
922
|
+
`Time: ${diagnostic.capturedAt}`,
|
|
923
|
+
`Page: ${window.location.origin}${window.location.pathname}`,
|
|
924
|
+
`Browser: ${navigator.userAgent}`,
|
|
925
|
+
explanation === null ? null : `Explanation: ${explanation}`,
|
|
926
|
+
"",
|
|
927
|
+
`Message: ${diagnostic.message}`,
|
|
928
|
+
diagnostic.stack === undefined ? null : `JavaScript stack:\n${diagnostic.stack}`,
|
|
929
|
+
diagnostic.componentStack === undefined ? null : `React component stack:\n${diagnostic.componentStack}`,
|
|
930
|
+
]
|
|
931
|
+
.filter((line) => line !== null)
|
|
932
|
+
.join("\n");
|
|
933
|
+
}
|
|
934
|
+
function DiagnosticOverlay({ diagnostics, gameName }) {
|
|
935
|
+
const [copied, setCopied] = useState(false);
|
|
664
936
|
if (diagnostics.length === 0)
|
|
665
937
|
return null;
|
|
666
938
|
const latest = diagnostics[diagnostics.length - 1];
|
|
667
|
-
|
|
939
|
+
const explanation = expandedReactError(latest.message);
|
|
940
|
+
const report = diagnosticReport(latest, gameName);
|
|
941
|
+
const issueBody = report.length > 8000 ? `${report.slice(0, 8000)}\n\n[Report truncated; use Copy error for the full report.]` : report;
|
|
942
|
+
const issueUrl = `https://github.com/Noisemaker111/jgengine/issues/new?title=${encodeURIComponent(`[BUG] ${latest.phase}: ${latest.message.slice(0, 100)}`)}&body=${encodeURIComponent(issueBody)}`;
|
|
943
|
+
return (_jsxs("div", { className: "pointer-events-auto absolute right-4 top-4 z-50 max-w-lg rounded border border-red-400/60 bg-red-950/95 p-3 text-xs text-red-50 shadow-2xl", children: [_jsx("div", { className: "mb-1 font-semibold uppercase tracking-wide text-red-200", children: "JG engine error" }), _jsxs("div", { className: "font-mono text-[11px] text-red-100", children: ["[", latest.phase, "] ", latest.message] }), explanation !== null ? _jsx("div", { className: "mt-2 text-red-100", children: explanation }) : null, latest.stack !== undefined ? (_jsx("pre", { className: "mt-2 max-h-36 overflow-auto whitespace-pre-wrap text-[10px] text-red-200/80", children: latest.stack })) : null, latest.componentStack !== undefined ? (_jsx("pre", { className: "mt-2 max-h-36 overflow-auto whitespace-pre-wrap text-[10px] text-red-200/80", children: latest.componentStack })) : null, _jsxs("div", { className: "mt-3 flex gap-2", children: [_jsx("button", { className: "rounded border border-red-300/50 bg-red-900 px-2 py-1 font-semibold hover:bg-red-800", type: "button", onClick: () => {
|
|
944
|
+
void navigator.clipboard.writeText(report).then(() => {
|
|
945
|
+
setCopied(true);
|
|
946
|
+
window.setTimeout(() => setCopied(false), 2000);
|
|
947
|
+
});
|
|
948
|
+
}, children: copied ? "Copied" : "Copy error" }), _jsx("a", { className: "rounded border border-red-300/50 bg-red-900 px-2 py-1 font-semibold hover:bg-red-800", href: issueUrl, rel: "noreferrer", target: "_blank", children: "File issue" })] })] }));
|
|
668
949
|
}
|
|
669
|
-
|
|
950
|
+
const POSTER_SETTLE_SECONDS = 1.6;
|
|
951
|
+
export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null, poster = false, onContextReady, }) {
|
|
670
952
|
const multiplayer = useMemo(() => (rawMultiplayer === null ? null : withDevtoolsLatency(rawMultiplayer)), [rawMultiplayer]);
|
|
671
|
-
const devtoolsEnabled = playable.devtools !== false;
|
|
953
|
+
const devtoolsEnabled = playable.devtools !== false && !poster;
|
|
672
954
|
const [devtoolsOpen, setDevtoolsOpen] = useState(false);
|
|
955
|
+
const [posterFrozen, setPosterFrozen] = useState(false);
|
|
956
|
+
const posterSettledRef = useRef(false);
|
|
673
957
|
const [ctx, setCtx] = useState(null);
|
|
674
958
|
const [diagnostics, setDiagnostics] = useState([]);
|
|
675
959
|
const [remotePlayers, setRemotePlayers] = useState([]);
|
|
@@ -680,25 +964,101 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
680
964
|
const cameraDraggingRef = useRef(false);
|
|
681
965
|
const primaryClickRef = useRef(false);
|
|
682
966
|
const pointerDownRef = useRef(null);
|
|
967
|
+
const pointerAxisRef = useRef(null);
|
|
683
968
|
const marqueeStartRef = useRef(null);
|
|
969
|
+
const f2HeldRef = useRef(false);
|
|
970
|
+
const f2ChordedRef = useRef(false);
|
|
684
971
|
const pointerService = useMemo(() => createPointerService(), []);
|
|
685
972
|
const selection = useMemo(() => createSelectionSet(), [playable]);
|
|
686
973
|
const [marquee, setMarquee] = useState(null);
|
|
687
974
|
const [selectedIds, setSelectedIds] = useState(() => new Set());
|
|
688
975
|
const [contextMenu, setContextMenu] = useState(null);
|
|
689
|
-
const
|
|
976
|
+
const settingsStore = useMemo(() => createSettingsStore(), []);
|
|
977
|
+
const [bindingOverrides, setBindingOverrides] = useState(() => loadBindingOverrides(playable.game.name));
|
|
978
|
+
useEffect(() => {
|
|
979
|
+
setBindingOverrides(loadBindingOverrides(playable.game.name));
|
|
980
|
+
}, [playable]);
|
|
981
|
+
const effectiveInput = useMemo(() => applyBindingOverrides(playable.game.input ?? {}, bindingOverrides), [playable, bindingOverrides]);
|
|
982
|
+
const rebindAction = useCallback((action, code) => setBindingOverrides(saveBindingOverride(playable.game.name, action, [code])), [playable]);
|
|
983
|
+
const resetActionBinding = useCallback((action) => setBindingOverrides(clearBindingOverride(playable.game.name, action)), [playable]);
|
|
984
|
+
const tracker = useMemo(() => createActionStateTracker(toActionStateBindingMap(withTouchCodes(effectiveInput))), [effectiveInput]);
|
|
985
|
+
const graphics = useGraphicsSettings(settingsStore, playable.shadows ?? true);
|
|
986
|
+
const trackPointerAxis = (event) => {
|
|
987
|
+
const rect = wrapperRef.current?.getBoundingClientRect();
|
|
988
|
+
if (rect === undefined)
|
|
989
|
+
return;
|
|
990
|
+
pointerAxisRef.current = normalizePointerToAxis(event.clientX, event.clientY, rect);
|
|
991
|
+
};
|
|
992
|
+
const deactivatePointerAxis = () => {
|
|
993
|
+
const state = pointerAxisRef.current;
|
|
994
|
+
if (state !== null && state.active)
|
|
995
|
+
pointerAxisRef.current = { ...state, active: false };
|
|
996
|
+
};
|
|
690
997
|
const touchScheme = useMemo(() => deriveTouchScheme(playable.game.input, {
|
|
691
|
-
reserved:
|
|
998
|
+
reserved: resolveRigKind(playable.camera) === "none" || playable.presentation === "hud"
|
|
999
|
+
? EMPTY_RESERVED
|
|
1000
|
+
: RESERVED_INPUT_ACTIONS,
|
|
692
1001
|
firstPerson: resolveRigKind(playable.camera) === "first",
|
|
693
1002
|
config: playable.touch,
|
|
694
1003
|
}), [playable]);
|
|
695
|
-
const { coarsePointer } = useDisplayProfile();
|
|
1004
|
+
const { coarsePointer, portrait, compact } = useDisplayProfile();
|
|
696
1005
|
const touchSink = useMemo(() => ({ onCodeDown: (code) => tracker.handleDown(code), onCodeUp: (code) => tracker.handleUp(code) }), [tracker]);
|
|
697
|
-
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]);
|
|
698
1020
|
useEffect(() => () => audioEngine.dispose(), [audioEngine]);
|
|
1021
|
+
useEffect(() => {
|
|
1022
|
+
if (ctx === null)
|
|
1023
|
+
return;
|
|
1024
|
+
const offPlay = ctx.game.events.on("audio.play", ({ sound, at }) => {
|
|
1025
|
+
audioEngine.playOneShot(sound, at === undefined ? undefined : { x: at[0], y: at[1], z: at[2] });
|
|
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
|
+
};
|
|
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]);
|
|
699
1059
|
const userId = multiplayer?.userId ?? DEV_USER_ID;
|
|
700
|
-
const reportRuntimeError = (error, phase) => {
|
|
701
|
-
const diagnostic = logRuntimeError(error, phase);
|
|
1060
|
+
const reportRuntimeError = (error, phase, componentStack) => {
|
|
1061
|
+
const diagnostic = logRuntimeError(error, phase, componentStack);
|
|
702
1062
|
setDiagnostics((current) => [...current.slice(-4), { ...diagnostic, id: Date.now() + current.length }]);
|
|
703
1063
|
};
|
|
704
1064
|
useEffect(() => {
|
|
@@ -712,6 +1072,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
712
1072
|
});
|
|
713
1073
|
playable.loop.onInit(context);
|
|
714
1074
|
playable.loop.onNewPlayer(context);
|
|
1075
|
+
onContextReady?.(context);
|
|
715
1076
|
setCtx(context);
|
|
716
1077
|
}
|
|
717
1078
|
catch (error) {
|
|
@@ -736,6 +1097,9 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
736
1097
|
return;
|
|
737
1098
|
}
|
|
738
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
|
+
}
|
|
739
1103
|
cleanups.push(multiplayer.backend.presenceSync.subscribe(joined.serverId, (rows) => {
|
|
740
1104
|
setRemotePlayers(rows.filter((row) => row.userId !== multiplayer.userId));
|
|
741
1105
|
}));
|
|
@@ -771,8 +1135,9 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
771
1135
|
cleanups.push(remoteUnsub);
|
|
772
1136
|
}
|
|
773
1137
|
const chatSync = multiplayer.backend.chatSyncFor?.(joined.serverId);
|
|
774
|
-
if (chatSync !== undefined) {
|
|
775
|
-
const
|
|
1138
|
+
if (chatSync !== undefined && ctx.game.chat !== undefined) {
|
|
1139
|
+
const chat = ctx.game.chat;
|
|
1140
|
+
const globalChannelIds = new Set(chat
|
|
776
1141
|
.channels()
|
|
777
1142
|
.filter((channel) => channel.kind === "global")
|
|
778
1143
|
.map((channel) => channel.id));
|
|
@@ -792,7 +1157,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
792
1157
|
if (seenRemoteChat.has(message.id))
|
|
793
1158
|
continue;
|
|
794
1159
|
seenRemoteChat.add(message.id);
|
|
795
|
-
|
|
1160
|
+
chat.send(message.fromUserId, message.channelId, message.body);
|
|
796
1161
|
}
|
|
797
1162
|
}));
|
|
798
1163
|
}
|
|
@@ -810,7 +1175,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
810
1175
|
void multiplayer.backend.transport.leaveServer({ serverId }).catch(() => undefined);
|
|
811
1176
|
}
|
|
812
1177
|
};
|
|
813
|
-
}, [ctx, multiplayer]);
|
|
1178
|
+
}, [ctx, multiplayer, playable]);
|
|
814
1179
|
useEffect(() => {
|
|
815
1180
|
wrapperRef.current?.focus();
|
|
816
1181
|
}, [ctx]);
|
|
@@ -826,16 +1191,32 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
826
1191
|
const rigKind = resolveRigKind(cameraConfig);
|
|
827
1192
|
if (rigKind === "none" || playable.presentation === "hud") {
|
|
828
1193
|
const GameUI = playable.GameUI;
|
|
829
|
-
return (
|
|
1194
|
+
return (_jsx("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
|
|
830
1195
|
if (event.code === "F2" && devtoolsEnabled) {
|
|
831
1196
|
event.preventDefault();
|
|
832
|
-
|
|
1197
|
+
f2HeldRef.current = true;
|
|
1198
|
+
f2ChordedRef.current = false;
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
if (f2HeldRef.current) {
|
|
1202
|
+
f2ChordedRef.current = true;
|
|
833
1203
|
return;
|
|
834
1204
|
}
|
|
835
1205
|
if (event.code === "Tab" || event.code === "Space")
|
|
836
1206
|
event.preventDefault();
|
|
837
1207
|
tracker.handleDown(event.code);
|
|
838
|
-
}, onKeyUp: (event) =>
|
|
1208
|
+
}, onKeyUp: (event) => {
|
|
1209
|
+
if (event.code === "F2" && devtoolsEnabled) {
|
|
1210
|
+
f2HeldRef.current = false;
|
|
1211
|
+
if (!f2ChordedRef.current)
|
|
1212
|
+
setDevtoolsOpen((current) => !current);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
tracker.handleUp(event.code);
|
|
1216
|
+
}, onBlur: () => {
|
|
1217
|
+
f2HeldRef.current = false;
|
|
1218
|
+
tracker.reset();
|
|
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 })] }) }));
|
|
839
1220
|
}
|
|
840
1221
|
const firstPerson = rigKind === "first";
|
|
841
1222
|
const showReticle = (firstPerson && playable.camera?.firstPerson?.reticle !== false) || rigKind === "shoulder";
|
|
@@ -847,6 +1228,9 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
847
1228
|
? "health"
|
|
848
1229
|
: worldBars.statId ?? "health";
|
|
849
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;
|
|
850
1234
|
const resolveEntityRole = (entity) => playable.content.entityById?.(entity.name)?.role;
|
|
851
1235
|
const pointer = playable.pointer;
|
|
852
1236
|
const pointerUsesLeft = pointer !== undefined && (pointer.select === true || pointer.moveCommand !== undefined);
|
|
@@ -864,6 +1248,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
864
1248
|
const effectiveSky = backdropSky ?? worldSky;
|
|
865
1249
|
const backgroundColor = backdrop?.background ?? (effectiveSky === undefined ? DEFAULT_BACKGROUND_COLOR : undefined);
|
|
866
1250
|
const lighting = playable.lighting;
|
|
1251
|
+
const orthographic = playable.camera?.projection === "orthographic";
|
|
867
1252
|
const localXY = (event) => {
|
|
868
1253
|
const rect = wrapperRef.current?.getBoundingClientRect();
|
|
869
1254
|
return rect === undefined
|
|
@@ -885,12 +1270,15 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
885
1270
|
}
|
|
886
1271
|
commitSelection(selectWithinRect(candidates, rect));
|
|
887
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;
|
|
888
1275
|
const handlePointerDown = (event) => {
|
|
889
1276
|
wrapperRef.current?.focus();
|
|
1277
|
+
trackPointerAxis(event);
|
|
890
1278
|
audioEngine.resume();
|
|
891
1279
|
if (contextMenu !== null)
|
|
892
1280
|
setContextMenu(null);
|
|
893
|
-
if (event.button === 0) {
|
|
1281
|
+
if (event.button === 0 && isWorldPointerTarget(event)) {
|
|
894
1282
|
const point = localXY(event);
|
|
895
1283
|
pointerDownRef.current = point;
|
|
896
1284
|
if (pointer?.select === true)
|
|
@@ -898,6 +1286,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
898
1286
|
}
|
|
899
1287
|
};
|
|
900
1288
|
const handlePointerMove = (event) => {
|
|
1289
|
+
trackPointerAxis(event);
|
|
901
1290
|
if (pointer?.select !== true)
|
|
902
1291
|
return;
|
|
903
1292
|
const start = marqueeStartRef.current;
|
|
@@ -963,6 +1352,8 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
963
1352
|
const handleContextMenu = (event) => {
|
|
964
1353
|
if (pointer === undefined)
|
|
965
1354
|
return;
|
|
1355
|
+
if (!isWorldPointerTarget(event))
|
|
1356
|
+
return;
|
|
966
1357
|
event.preventDefault();
|
|
967
1358
|
const hit = pointerService.worldHit();
|
|
968
1359
|
if (hit === null)
|
|
@@ -996,33 +1387,87 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
996
1387
|
ctx.game.commands.run(verb.command, contextVerbInput(state.menu, verb));
|
|
997
1388
|
}
|
|
998
1389
|
};
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1390
|
+
const controlsActive = playControlsActive(ctx);
|
|
1391
|
+
const settingsDisabled = playable.settings === false;
|
|
1392
|
+
const settingsConfig = playable.settings === false || playable.settings === undefined ? {} : playable.settings;
|
|
1393
|
+
const settingsSurface = settingsDisabled ? false : settingsConfig.surface ?? false;
|
|
1394
|
+
const settingsVariant = settingsConfig.variant ?? "panel";
|
|
1395
|
+
const hideCategories = settingsDisabled ? BUILT_IN_SETTING_CATEGORIES : settingsConfig.hide ?? [];
|
|
1396
|
+
const fovControlEnabled = !orthographic && playable.camera?.playerFov?.control !== false;
|
|
1397
|
+
const settingsHostsFov = !settingsDisabled && !hideCategories.includes("gameplay") && fovControlEnabled;
|
|
1398
|
+
const settingsActions = settingsDisabled
|
|
1399
|
+
? []
|
|
1400
|
+
: (settingsConfig.actions ?? []).map((action) => ({
|
|
1401
|
+
id: action.id,
|
|
1402
|
+
label: action.label,
|
|
1403
|
+
kind: action.kind ?? "default",
|
|
1404
|
+
description: action.description,
|
|
1405
|
+
run: () => action.run(ctx),
|
|
1406
|
+
}));
|
|
1407
|
+
const touchScale = compact ? 0.88 : 1;
|
|
1408
|
+
const dockMounted = !poster &&
|
|
1409
|
+
!orientationGate &&
|
|
1410
|
+
coarsePointer &&
|
|
1411
|
+
controlsActive &&
|
|
1412
|
+
touchScheme !== null &&
|
|
1413
|
+
(touchScheme.joystick !== null || touchScheme.buttons.length > 0);
|
|
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: {
|
|
1415
|
+
"--jg-hud-dock-clearance": `${dockMounted ? touchDockClearance(touchScheme, touchScale) : 0}px`,
|
|
1416
|
+
}, onKeyDown: (event) => {
|
|
1417
|
+
if (event.code === "F2" && devtoolsEnabled) {
|
|
1418
|
+
event.preventDefault();
|
|
1419
|
+
f2HeldRef.current = true;
|
|
1420
|
+
f2ChordedRef.current = false;
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
if (f2HeldRef.current) {
|
|
1424
|
+
f2ChordedRef.current = true;
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
if (event.code === "Tab" || event.code === "Space")
|
|
1428
|
+
event.preventDefault();
|
|
1429
|
+
tracker.handleDown(event.code);
|
|
1430
|
+
}, onKeyUp: (event) => {
|
|
1431
|
+
if (event.code === "F2" && devtoolsEnabled) {
|
|
1432
|
+
f2HeldRef.current = false;
|
|
1433
|
+
if (!f2ChordedRef.current) {
|
|
1434
|
+
document.exitPointerLock?.();
|
|
1435
|
+
setDevtoolsOpen((current) => !current);
|
|
1436
|
+
}
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
tracker.handleUp(event.code);
|
|
1440
|
+
}, onBlur: () => {
|
|
1441
|
+
f2HeldRef.current = false;
|
|
1442
|
+
tracker.reset();
|
|
1443
|
+
}, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerLeave: deactivatePointerAxis, onPointerCancel: deactivatePointerAxis, onContextMenu: handleContextMenu, onWheel: (event) => {
|
|
1444
|
+
if (ctx === null || !event.shiftKey)
|
|
1445
|
+
return;
|
|
1446
|
+
event.preventDefault();
|
|
1447
|
+
if (event.deltaY < 0 && ctx.game.commands.has("ui.hotbarScrollNext")) {
|
|
1448
|
+
ctx.game.commands.run("ui.hotbarScrollNext", {});
|
|
1449
|
+
}
|
|
1450
|
+
else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
|
|
1451
|
+
ctx.game.commands.run("ui.hotbarScrollPrev", {});
|
|
1452
|
+
}
|
|
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, {})] }) }) })] }) }));
|
|
1028
1473
|
}
|