@jgengine/shell 0.7.0 → 0.9.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 +63 -0
- package/README.md +37 -1
- package/dist/GameHost.d.ts +12 -0
- package/dist/GameHost.js +33 -0
- package/dist/GamePlayer.d.ts +11 -0
- package/dist/GamePlayer.js +24 -0
- package/dist/GamePlayerShell.d.ts +36 -1
- package/dist/GamePlayerShell.js +751 -80
- package/dist/GameUiPreview.js +2 -1
- package/dist/audio/AudioComponents.js +25 -8
- package/dist/audio/audioEngine.d.ts +1 -0
- package/dist/audio/audioEngine.js +4 -0
- package/dist/behaviour.d.ts +12 -0
- package/dist/behaviour.js +27 -0
- 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.d.ts +8 -3
- package/dist/camera/GameCameraRig.js +28 -14
- package/dist/camera/GameFirstPersonCamera.js +16 -0
- package/dist/camera/GameInspectionCamera.d.ts +6 -0
- package/dist/camera/GameInspectionCamera.js +24 -0
- package/dist/camera/GameOrbitCamera.js +10 -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 +8 -1
- package/dist/camera/cameraRigs.js +78 -38
- package/dist/camera/fovPreference.d.ts +25 -0
- package/dist/camera/fovPreference.js +73 -0
- package/dist/camera/index.d.ts +7 -2
- package/dist/camera/index.js +7 -2
- package/dist/camera/inspectionCameraMath.d.ts +25 -0
- package/dist/camera/inspectionCameraMath.js +44 -0
- package/dist/camera/rigMath.d.ts +50 -1
- package/dist/camera/rigMath.js +91 -1
- package/dist/camera/rigResolve.d.ts +15 -0
- package/dist/camera/rigResolve.js +52 -0
- 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 +245 -0
- package/dist/defineGame.d.ts +16 -0
- package/dist/defineGame.js +52 -0
- package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
- package/dist/devtools/CollisionDebugWorld.js +180 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +78 -0
- package/dist/devtools/DevtoolsOverlay.js +676 -0
- package/dist/devtools/collisionDebug.d.ts +57 -0
- package/dist/devtools/collisionDebug.js +127 -0
- package/dist/devtools/collisionDebugMath.d.ts +102 -0
- package/dist/devtools/collisionDebugMath.js +128 -0
- package/dist/environment/Daylight.d.ts +56 -0
- package/dist/environment/Daylight.js +101 -0
- package/dist/environment/EnvironmentScene.js +87 -48
- package/dist/environment/GroundPad.d.ts +7 -0
- package/dist/environment/GroundPad.js +12 -0
- package/dist/environment/daylightCycle.d.ts +26 -0
- package/dist/environment/daylightCycle.js +116 -0
- package/dist/environment/groundPadMath.d.ts +13 -0
- package/dist/environment/groundPadMath.js +10 -0
- package/dist/environment/index.d.ts +3 -0
- package/dist/environment/index.js +3 -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/materialOverride.d.ts +6 -0
- package/dist/materialOverride.js +28 -0
- package/dist/multiplayer.d.ts +16 -9
- package/dist/multiplayer.js +62 -12
- package/dist/pointer/PointerProbe.js +20 -3
- package/dist/pointer/pointerService.d.ts +5 -0
- package/dist/pointer/pointerService.js +51 -24
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +3 -1
- package/dist/render/modelRender.d.ts +23 -0
- package/dist/render/modelRender.js +106 -0
- 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 +2 -1
- package/dist/terrain/CarvedTerrain.js +3 -3
- 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/index.d.ts +1 -1
- package/dist/terrain/index.js +1 -1
- package/dist/terrain/terrainMath.d.ts +9 -0
- package/dist/terrain/terrainMath.js +17 -2
- package/dist/touch/OrientationHint.d.ts +3 -0
- package/dist/touch/OrientationHint.js +13 -0
- package/dist/touch/TouchControlsOverlay.d.ts +34 -0
- package/dist/touch/TouchControlsOverlay.js +279 -0
- 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/OceanShader.d.ts +1 -1
- package/dist/water/OceanShader.js +3 -3
- 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 +44 -0
- package/dist/world/DataObjects.js +77 -0
- package/dist/world/GridWorldScene.d.ts +10 -0
- package/dist/world/GridWorldScene.js +54 -0
- package/dist/world/SpriteBatch.d.ts +44 -0
- package/dist/world/SpriteBatch.js +112 -0
- package/dist/world/WorldHud.d.ts +8 -1
- package/dist/world/WorldHud.js +90 -39
- package/dist/world/WorldItems.js +21 -2
- 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 +51 -0
- package/llms.txt +2643 -0
- package/package.json +6 -5
- package/dist/demo/builderDemo.d.ts +0 -2
- package/dist/demo/builderDemo.js +0 -189
- package/dist/demo/demoGame.d.ts +0 -2
- package/dist/demo/demoGame.js +0 -208
- package/dist/demo/environmentShowcase.d.ts +0 -2
- package/dist/demo/environmentShowcase.js +0 -15
- package/dist/demo/mapDemo.d.ts +0 -2
- package/dist/demo/mapDemo.js +0 -206
- package/dist/demo/pointerDemo.d.ts +0 -2
- package/dist/demo/pointerDemo.js +0 -151
- package/dist/demo/sensorShowcase.d.ts +0 -2
- package/dist/demo/sensorShowcase.js +0 -131
- package/dist/demo/survivalDemo.d.ts +0 -2
- package/dist/demo/survivalDemo.js +0 -291
package/dist/GamePlayerShell.js
CHANGED
|
@@ -1,40 +1,78 @@
|
|
|
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, 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 { createActionStateTracker, hotbarSlotActionIndex, resolveActionCommand, toActionStateBindingMap, } from "@jgengine/core/input/actionBindings";
|
|
6
|
+
import { actionRepeatMs, createActionStateTracker, hotbarSlotActionIndex, resolveActionCommand, shouldDispatchAction, toActionStateBindingMap, } from "@jgengine/core/input/actionBindings";
|
|
7
|
+
import { deriveTouchScheme, withTouchCodes } from "@jgengine/core/input/touchScheme";
|
|
7
8
|
import { buildContextMenu, contextVerbInput, } from "@jgengine/core/interaction/contextMenu";
|
|
8
9
|
import { resolveActivePrompt } from "@jgengine/core/interaction/proximityPrompt";
|
|
9
10
|
import { aimToPoint } from "@jgengine/core/input/pointer";
|
|
11
|
+
import { normalizePointerToAxis } from "@jgengine/core/input/pointerAxis";
|
|
10
12
|
import { createSelectionSet, isMarquee, screenRect, selectWithinRect, } from "@jgengine/core/scene/selection";
|
|
11
|
-
import { advancePlayerMotion, createEmptyMovementKeys, createPlayerMotionState, resolveMovementIntent, } from "@jgengine/core/movement/movementModel";
|
|
13
|
+
import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, resolveMovementIntent, resolveObstacleStep, snapPositionToGrid, } from "@jgengine/core/movement/movementModel";
|
|
14
|
+
import { steerYaw } from "@jgengine/core/movement/steering";
|
|
15
|
+
import { advanceVoxelPlayer, createVoxelPlayerBody, } from "@jgengine/core/movement/voxelController";
|
|
12
16
|
import { createGameContext } from "@jgengine/core/runtime/gameContext";
|
|
17
|
+
import { applyHorizontalImpulses } from "@jgengine/core/runtime/motionIntents";
|
|
18
|
+
import { groundFieldFor } from "@jgengine/core/world/terrain";
|
|
19
|
+
import { objectVisualScale } from "@jgengine/core/scene/objectStore";
|
|
13
20
|
import { DEFAULT_PICKUP_RADIUS, WORLD_ITEM_ENTITY_NAME } from "@jgengine/core/game/worldItem";
|
|
14
21
|
import { useGameContext } from "@jgengine/react/provider";
|
|
22
|
+
import { useDisplayProfile } from "@jgengine/react/display";
|
|
23
|
+
import { HudViewportProvider } from "@jgengine/react/hudViewport";
|
|
15
24
|
import { useSceneEntities, useSceneObjects, usePlayer, useTarget } from "@jgengine/react/hooks";
|
|
16
25
|
import { GameProvider } from "@jgengine/react/provider";
|
|
26
|
+
import { CAMERA_FRUSTUM_DEFAULTS } from "@jgengine/core/game/playableGame";
|
|
27
|
+
import { createSettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
28
|
+
import { applyBindingOverrides, clearBindingOverride, loadBindingOverrides, saveBindingOverride, } from "@jgengine/core/input/bindingOverrides";
|
|
29
|
+
import { playControlsActive } from "@jgengine/core/game/controlGate";
|
|
30
|
+
import { sky as resolveSkyDescriptor } from "@jgengine/core/world/features";
|
|
31
|
+
import { devtools } from "@jgengine/core/devtools/devtools";
|
|
32
|
+
import { VERSION } from "@jgengine/core/meta/changelog";
|
|
17
33
|
import { AudioListener, EntityAudioEmitters, ObjectAudioEmitters } from "./audio/AudioComponents.js";
|
|
18
34
|
import { createAudioEngine } from "./audio/audioEngine.js";
|
|
19
|
-
import {
|
|
35
|
+
import { CollisionDebugWorld } from "./devtools/CollisionDebugWorld.js";
|
|
36
|
+
import { collisionDebug } from "./devtools/collisionDebug.js";
|
|
37
|
+
import { DevtoolsOverlay, DevtoolsRendererProbe, withDevtoolsLatency } from "./devtools/DevtoolsOverlay.js";
|
|
38
|
+
import { GAME_SIM_FRAME_PRIORITY, GameCameraRig, PlayerFovProvider, PlayerFovSlider, resolveRigKind, rtsPanKeysConflict, } from "./camera/index.js";
|
|
39
|
+
import { resolveModel, tryResolveCatalogModel } from "./render/resolveModel.js";
|
|
40
|
+
import { CullingProvider, useRenderVisibility } from "./visibility/CullingProvider.js";
|
|
41
|
+
import { SkyDaylight, TimeOfDayDaylight } from "./environment.js";
|
|
42
|
+
import { resolveSkyLightOwnership, skyEmitsLights } from "./environment/skyLightingPolicy.js";
|
|
43
|
+
import { EnvironmentScene } from "./environment/EnvironmentScene.js";
|
|
44
|
+
import { applyMaterialOverride } from "./materialOverride.js";
|
|
20
45
|
import { PointerProbe } from "./pointer/PointerProbe.js";
|
|
46
|
+
import { applyPaintTextureToMaterials, cacheStandardMaterials, cloneModelScene, createPaintCanvas, disposeClonedMaterials, syncPaintCanvas, } from "./render/modelRender.js";
|
|
47
|
+
import { writeEntityPose } from "./world/entityPose.js";
|
|
21
48
|
import { MarqueeBox, ContextMenuView } from "./pointer/PointerOverlays.js";
|
|
22
49
|
import { createPointerService, POINTER_ENTITY_KEY, POINTER_OBJECT_KEY, } from "./pointer/pointerService.js";
|
|
23
50
|
import { CombatCameraShake, ProjectileTracers, Reticle, WorldEntityBars, WorldFloatText, WorldTelegraphs, } from "./world/WorldHud.js";
|
|
51
|
+
import { GridWorldScene } from "./world/GridWorldScene.js";
|
|
24
52
|
import { WorldItems } from "./world/WorldItems.js";
|
|
53
|
+
import { OrientationHint } from "./touch/OrientationHint.js";
|
|
54
|
+
import { TouchControlsDock, TouchPlaySurface, touchDockClearance } from "./touch/TouchControlsOverlay.js";
|
|
55
|
+
import { BUILT_IN_SETTING_CATEGORIES } from "@jgengine/core/settings/settingsModel";
|
|
56
|
+
import { SettingsProvider } from "@jgengine/react/settings";
|
|
57
|
+
import { SettingsRuntime } from "./settings/SettingsRuntime.js";
|
|
58
|
+
import { SettingsChrome } from "./settings/SettingsChrome.js";
|
|
59
|
+
import { AudioSettingsBridge, useGraphicsSettings } from "./settings/appliedSettings.js";
|
|
25
60
|
const DEV_USER_ID = "dev-player";
|
|
26
61
|
const TURN_SPEED = 2.4;
|
|
27
62
|
const PRIMARY_CLICK_MOVE_THRESHOLD_PX = 6;
|
|
28
63
|
const GROUND_SIZE = 160;
|
|
29
64
|
const GROUND_SEGMENTS = 80;
|
|
30
|
-
|
|
65
|
+
const DEFAULT_BACKGROUND_COLOR = "#14161b";
|
|
66
|
+
const DEFAULT_BACKDROP_FOG_COLOR = "#1a1c22";
|
|
67
|
+
function errorToDiagnostic(error, phase, componentStack) {
|
|
68
|
+
const capturedAt = new Date().toISOString();
|
|
31
69
|
if (error instanceof Error) {
|
|
32
|
-
return { phase, message: error.message, stack: error.stack };
|
|
70
|
+
return { phase, message: error.message, stack: error.stack, componentStack, capturedAt };
|
|
33
71
|
}
|
|
34
|
-
return { phase, message: typeof error === "string" ? error : JSON.stringify(error) };
|
|
72
|
+
return { phase, message: typeof error === "string" ? error : JSON.stringify(error), componentStack, capturedAt };
|
|
35
73
|
}
|
|
36
|
-
function logRuntimeError(error, phase) {
|
|
37
|
-
const diagnostic = errorToDiagnostic(error, phase);
|
|
74
|
+
function logRuntimeError(error, phase, componentStack) {
|
|
75
|
+
const diagnostic = errorToDiagnostic(error, phase, componentStack);
|
|
38
76
|
console.error(`[jgengine:${phase}] ${diagnostic.message}`, error);
|
|
39
77
|
return diagnostic;
|
|
40
78
|
}
|
|
@@ -52,6 +90,11 @@ const RESERVED_INPUT_ACTIONS = new Set([
|
|
|
52
90
|
"useAbility",
|
|
53
91
|
"interact",
|
|
54
92
|
]);
|
|
93
|
+
const SHELL_MOVEMENT_ACTIONS = ["moveForward", "moveBack", "moveLeft", "moveRight", "jump"];
|
|
94
|
+
function shellDrivesPlayerPose(input) {
|
|
95
|
+
const bound = input ?? {};
|
|
96
|
+
return SHELL_MOVEMENT_ACTIONS.some((action) => action in bound);
|
|
97
|
+
}
|
|
55
98
|
function findHotbarSlotActions(input) {
|
|
56
99
|
return Object.keys(input ?? {}).flatMap((action) => {
|
|
57
100
|
const slot = hotbarSlotActionIndex(action);
|
|
@@ -95,12 +138,95 @@ function pointerContextMenu(ctx, playable, hit) {
|
|
|
95
138
|
}
|
|
96
139
|
return null;
|
|
97
140
|
}
|
|
141
|
+
/** Actions from `input` currently held down, for `ctx.input.publish` (#164.1); includes reserved movement/jump actions. */
|
|
142
|
+
export function heldActionsFor(tracker, actions) {
|
|
143
|
+
return actions.filter((action) => tracker.isDown(action));
|
|
144
|
+
}
|
|
145
|
+
/** Whether a bound action should fire this frame: on press, or on repeat interval while held (shared by `FrameDriver` and `HudOnlyDriver`). */
|
|
146
|
+
export function shouldFireBoundAction(tracker, action, input, repeatFiredAt, now) {
|
|
147
|
+
return shouldDispatchAction({
|
|
148
|
+
pressed: tracker.wasPressed(action),
|
|
149
|
+
down: tracker.isDown(action),
|
|
150
|
+
repeatMs: actionRepeatMs(input?.[action]),
|
|
151
|
+
lastFiredAt: repeatFiredAt.get(action) ?? null,
|
|
152
|
+
now,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/** Resolves and runs the command bound to `action` via the shell's action→command convention (shared by `FrameDriver` and `HudOnlyDriver`). */
|
|
156
|
+
export function dispatchBoundAction(ctx, action, yaw, pitch, aim) {
|
|
157
|
+
const command = resolveActionCommand(action, (name) => ctx.game.commands.has(name), RESERVED_INPUT_ACTIONS);
|
|
158
|
+
if (command !== null)
|
|
159
|
+
ctx.game.commands.run(command, { yaw, pitch, aim });
|
|
160
|
+
}
|
|
161
|
+
const OBSTACLE_GATHER_RADIUS = 3;
|
|
162
|
+
/** Placed scene objects within `radius` of `center`, as `CollisionObstacle`s for `resolveObstacleStep` (#162.1). */
|
|
163
|
+
export function nearbyObstacles(objects, center, radius = OBSTACLE_GATHER_RADIUS) {
|
|
164
|
+
const radiusSq = radius * radius;
|
|
165
|
+
const result = [];
|
|
166
|
+
for (const object of objects) {
|
|
167
|
+
const dx = object.position[0] - center[0];
|
|
168
|
+
const dz = object.position[2] - center[2];
|
|
169
|
+
if (dx * dx + dz * dz <= radiusSq)
|
|
170
|
+
result.push({ position: object.position });
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
/** Applies a pending `MotionIntentBatch` to a vertical velocity: impulses add, then `verticalVelocity` replaces the result outright (#162.4). */
|
|
175
|
+
export function applyMotionImpulses(currentVelocity, batch) {
|
|
176
|
+
if (batch === null)
|
|
177
|
+
return currentVelocity;
|
|
178
|
+
let velocity = currentVelocity;
|
|
179
|
+
for (const impulse of batch.impulses)
|
|
180
|
+
velocity += impulse;
|
|
181
|
+
return batch.verticalVelocity ?? velocity;
|
|
182
|
+
}
|
|
183
|
+
/** The world's declared sky, when its world feature is an environment with one (#196.1). */
|
|
184
|
+
export function resolveWorldSky(world) {
|
|
185
|
+
return world?.kind === "environment" ? world.sky : undefined;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Maps the game's declared `physics` onto the movement controllers' tuning
|
|
189
|
+
* overrides. `PhysicsConfig.gravity` is a signed world acceleration (negative
|
|
190
|
+
* points down, matching every game's config and the Y-up convention), but the
|
|
191
|
+
* controllers integrate `velocityY -= gravityAcceleration * dt` and so expect a
|
|
192
|
+
* positive downward magnitude. Negating here is what keeps a down-pointing
|
|
193
|
+
* gravity pulling the player *down*; passing the signed value straight through
|
|
194
|
+
* flipped the sign and launched airborne players upward instead.
|
|
195
|
+
*/
|
|
196
|
+
export function resolvePhysicsTuning(physics) {
|
|
197
|
+
if (physics === undefined)
|
|
198
|
+
return undefined;
|
|
199
|
+
return {
|
|
200
|
+
get gravityAcceleration() {
|
|
201
|
+
return physics.gravity === undefined ? undefined : -physics.gravity;
|
|
202
|
+
},
|
|
203
|
+
get jumpVelocity() {
|
|
204
|
+
return physics.jumpVelocity;
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/** True when the world is an environment feature with terrain, so the voxel controller should sample its height. */
|
|
209
|
+
export function hasEnvironmentTerrain(world) {
|
|
210
|
+
return world?.kind === "environment" && (world.terrain !== undefined || (world.islands?.length ?? 0) > 0);
|
|
211
|
+
}
|
|
98
212
|
function colorFromId(id) {
|
|
99
213
|
let hash = 0;
|
|
100
214
|
for (let index = 0; index < id.length; index += 1)
|
|
101
215
|
hash = (hash * 31 + id.charCodeAt(index)) >>> 0;
|
|
102
216
|
return `hsl(${hash % 360}, 65%, 55%)`;
|
|
103
217
|
}
|
|
218
|
+
function ConfiguredLighting({ lighting }) {
|
|
219
|
+
return (_jsxs(_Fragment, { children: [lighting.ambient !== undefined ? (_jsx("ambientLight", { color: lighting.ambient.color, intensity: lighting.ambient.intensity ?? 0.55 })) : null, lighting.hemisphere !== undefined ? (_jsx("hemisphereLight", { args: [
|
|
220
|
+
lighting.hemisphere.skyColor ?? "#bfe3ff",
|
|
221
|
+
lighting.hemisphere.groundColor ?? "#4c6b34",
|
|
222
|
+
lighting.hemisphere.intensity ?? 0.55,
|
|
223
|
+
] })) : null, (lighting.directional ?? []).map((entry, index) => (_jsx("directionalLight", { position: [entry.position[0], entry.position[1], entry.position[2]], intensity: entry.intensity ?? 1.3, color: entry.color, castShadow: entry.castShadow ?? false }, index)))] }));
|
|
224
|
+
}
|
|
225
|
+
function BackdropFog({ fog }) {
|
|
226
|
+
if (fog === undefined)
|
|
227
|
+
return null;
|
|
228
|
+
return fog.density !== undefined ? (_jsx("fogExp2", { attach: "fog", args: [fog.color ?? DEFAULT_BACKDROP_FOG_COLOR, fog.density] })) : (_jsx("fog", { attach: "fog", args: [fog.color ?? DEFAULT_BACKDROP_FOG_COLOR, fog.near ?? 10, fog.far ?? 200] }));
|
|
229
|
+
}
|
|
104
230
|
function EntitySprite({ sprite }) {
|
|
105
231
|
const texture = useLoader(THREE.TextureLoader, sprite.url);
|
|
106
232
|
useEffect(() => {
|
|
@@ -110,9 +236,10 @@ function EntitySprite({ sprite }) {
|
|
|
110
236
|
}, [texture]);
|
|
111
237
|
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 }) }));
|
|
112
238
|
}
|
|
113
|
-
function EntityModel({ model }) {
|
|
239
|
+
function EntityModel({ model, instanceId }) {
|
|
114
240
|
const gltf = useLoader(GLTFLoader, model.url);
|
|
115
|
-
const
|
|
241
|
+
const ctx = useGameContext();
|
|
242
|
+
const material = model.material;
|
|
116
243
|
const scale = model.scale ?? 1;
|
|
117
244
|
const baseY = model.y ?? 0;
|
|
118
245
|
const dims = model.dims;
|
|
@@ -120,25 +247,126 @@ function EntityModel({ model }) {
|
|
|
120
247
|
const position = centered
|
|
121
248
|
? [-scale * dims.center.x, baseY - scale * dims.minY, -scale * dims.center.z]
|
|
122
249
|
: [0, baseY, 0];
|
|
250
|
+
const scene = useMemo(() => {
|
|
251
|
+
const cloned = cloneModelScene(gltf.scene);
|
|
252
|
+
if (material !== undefined)
|
|
253
|
+
applyMaterialOverride(cloned, material, { clone: false });
|
|
254
|
+
return cloned;
|
|
255
|
+
}, [gltf, material]);
|
|
256
|
+
useEffect(() => () => {
|
|
257
|
+
disposeClonedMaterials(scene);
|
|
258
|
+
}, [scene]);
|
|
259
|
+
const animation = model.animation;
|
|
260
|
+
const mixerRef = useRef(null);
|
|
261
|
+
const animationPausedRef = useRef(false);
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
if (animation === undefined || gltf.animations.length === 0) {
|
|
264
|
+
mixerRef.current = null;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const mixer = new THREE.AnimationMixer(scene);
|
|
268
|
+
const clip = (animation.clip !== undefined ? THREE.AnimationClip.findByName(gltf.animations, animation.clip) : undefined) ??
|
|
269
|
+
gltf.animations[0];
|
|
270
|
+
const action = mixer.clipAction(clip);
|
|
271
|
+
action.setLoop(animation.loop === false ? THREE.LoopOnce : THREE.LoopRepeat, Infinity);
|
|
272
|
+
action.clampWhenFinished = animation.loop === false;
|
|
273
|
+
action.timeScale = animation.timeScale ?? 1;
|
|
274
|
+
action.enabled = true;
|
|
275
|
+
action.paused = animation.paused === true;
|
|
276
|
+
action.play();
|
|
277
|
+
if (animation.time !== undefined)
|
|
278
|
+
action.time = animation.time;
|
|
279
|
+
mixer.update(0);
|
|
280
|
+
mixerRef.current = mixer;
|
|
281
|
+
animationPausedRef.current = animation.paused === true;
|
|
282
|
+
return () => {
|
|
283
|
+
mixer.stopAllAction();
|
|
284
|
+
mixerRef.current = null;
|
|
285
|
+
};
|
|
286
|
+
}, [scene, gltf, animation?.clip, animation?.loop, animation?.timeScale, animation?.paused, animation?.time]);
|
|
287
|
+
const paintCanvasRef = useRef(null);
|
|
288
|
+
const paintDrawnCountRef = useRef(0);
|
|
289
|
+
const paintVersionRef = useRef(-1);
|
|
290
|
+
const materialCacheRef = useRef(null);
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
paintCanvasRef.current = null;
|
|
293
|
+
paintDrawnCountRef.current = 0;
|
|
294
|
+
paintVersionRef.current = -1;
|
|
295
|
+
materialCacheRef.current = null;
|
|
296
|
+
}, [scene]);
|
|
297
|
+
useFrame((_state, delta) => {
|
|
298
|
+
if (mixerRef.current !== null && !animationPausedRef.current)
|
|
299
|
+
mixerRef.current.update(delta);
|
|
300
|
+
if (instanceId === undefined)
|
|
301
|
+
return;
|
|
302
|
+
const paint = ctx.scene.entity.paint;
|
|
303
|
+
const version = paint.version(instanceId);
|
|
304
|
+
if (version === paintVersionRef.current)
|
|
305
|
+
return;
|
|
306
|
+
paintVersionRef.current = version;
|
|
307
|
+
const strokes = paint.strokes(instanceId);
|
|
308
|
+
const cache = cacheStandardMaterials(scene, materialCacheRef.current);
|
|
309
|
+
materialCacheRef.current = cache;
|
|
310
|
+
if (paintCanvasRef.current === null) {
|
|
311
|
+
if (strokes.length === 0)
|
|
312
|
+
return;
|
|
313
|
+
const seed = cache.materials[0];
|
|
314
|
+
if (seed === undefined)
|
|
315
|
+
return;
|
|
316
|
+
const paintCanvas = createPaintCanvas(seed);
|
|
317
|
+
paintCanvasRef.current = paintCanvas;
|
|
318
|
+
applyPaintTextureToMaterials(cache.materials, paintCanvas);
|
|
319
|
+
}
|
|
320
|
+
paintDrawnCountRef.current = syncPaintCanvas(paintCanvasRef.current, cache.seedColor, strokes, paintDrawnCountRef.current);
|
|
321
|
+
});
|
|
123
322
|
return _jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] });
|
|
124
323
|
}
|
|
125
|
-
function resolveModel(value, assets) {
|
|
126
|
-
if (value === undefined)
|
|
127
|
-
return undefined;
|
|
128
|
-
if (typeof value !== "string")
|
|
129
|
-
return value;
|
|
130
|
-
const ref = assets.resolve(value);
|
|
131
|
-
if (ref === null)
|
|
132
|
-
return undefined;
|
|
133
|
-
return ref.dims === undefined ? { url: ref.url } : { url: ref.url, dims: ref.dims };
|
|
134
|
-
}
|
|
135
324
|
function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, selected, onSelect, }) {
|
|
136
|
-
const
|
|
137
|
-
|
|
325
|
+
const groupRef = useRef(null);
|
|
326
|
+
const ctx = useGameContext();
|
|
327
|
+
const visibleRef = useRenderVisibility();
|
|
328
|
+
const entityId = entity.id;
|
|
329
|
+
const role = entity.role;
|
|
330
|
+
const name = entity.name;
|
|
331
|
+
const color = isLocal ? "#4ade80" : role === "npc" ? colorFromId(name) : "#9ca3af";
|
|
332
|
+
useFrame(() => {
|
|
333
|
+
const group = groupRef.current;
|
|
334
|
+
if (group === null)
|
|
335
|
+
return;
|
|
336
|
+
const live = ctx.scene.entity.get(entityId);
|
|
337
|
+
if (live === null)
|
|
338
|
+
return;
|
|
339
|
+
writeEntityPose(group, live);
|
|
340
|
+
group.visible = visibleRef.current(entityId);
|
|
341
|
+
});
|
|
342
|
+
return (_jsxs("group", { ref: groupRef, userData: { [POINTER_ENTITY_KEY]: entityId }, onPointerDown: (event) => {
|
|
138
343
|
event.stopPropagation();
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
344
|
+
if (isLocal)
|
|
345
|
+
return;
|
|
346
|
+
const live = ctx.scene.entity.get(entityId);
|
|
347
|
+
if (live !== null)
|
|
348
|
+
onSelect(live);
|
|
349
|
+
}, children: [selected ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.02, children: [_jsx("ringGeometry", { args: [0.8, 0.95, 32] }), _jsx("meshBasicMaterial", { color: "#34d399", transparent: true, opacity: 0.9 })] })) : null, custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(EntityModel, { model: model, instanceId: entityId })) : sprite !== undefined ? (_jsx(EntitySprite, { sprite: sprite })) : role === "prop" ? (_jsxs("mesh", { "position-y": 0.5, children: [_jsx("sphereGeometry", { args: [0.45, 16, 16] }), _jsx("meshStandardMaterial", { color: color })] })) : (_jsxs(_Fragment, { children: [_jsxs("mesh", { "position-y": 0.95, children: [_jsx("capsuleGeometry", { args: [0.35, 1.1, 6, 14] }), _jsx("meshStandardMaterial", { color: color })] }), _jsxs("mesh", { position: [0, 1.35, 0.32], children: [_jsx("boxGeometry", { args: [0.16, 0.16, 0.16] }), _jsx("meshStandardMaterial", { color: "#f8fafc" })] })] })), targeted ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.03, children: [_jsx("ringGeometry", { args: [0.6, 0.75, 28] }), _jsx("meshBasicMaterial", { color: "#f87171" })] })) : null] }));
|
|
350
|
+
}
|
|
351
|
+
function ObjectMarker({ object, custom, model, style, }) {
|
|
352
|
+
const groupRef = useRef(null);
|
|
353
|
+
const ctx = useGameContext();
|
|
354
|
+
const visibleRef = useRenderVisibility();
|
|
355
|
+
const instanceId = object.instanceId;
|
|
356
|
+
const [scaleX, scaleY, scaleZ] = objectVisualScale(object.visual);
|
|
357
|
+
const color = object.visual?.color ?? style?.color ?? colorFromId(object.catalogId);
|
|
358
|
+
const opacity = object.visual?.opacity ?? style?.opacity ?? 1;
|
|
359
|
+
useFrame(() => {
|
|
360
|
+
const group = groupRef.current;
|
|
361
|
+
if (group === null)
|
|
362
|
+
return;
|
|
363
|
+
const live = ctx.scene.object.get(instanceId);
|
|
364
|
+
if (live === null)
|
|
365
|
+
return;
|
|
366
|
+
writeEntityPose(group, live);
|
|
367
|
+
group.visible = visibleRef.current(instanceId);
|
|
368
|
+
});
|
|
369
|
+
return (_jsx("group", { ref: groupRef, userData: { [POINTER_OBJECT_KEY]: instanceId }, children: custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(EntityModel, { 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 })] })) }));
|
|
142
370
|
}
|
|
143
371
|
function GroundPlane() {
|
|
144
372
|
const geometry = useMemo(() => {
|
|
@@ -171,7 +399,12 @@ function RockField() {
|
|
|
171
399
|
}), []);
|
|
172
400
|
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))) }));
|
|
173
401
|
}
|
|
174
|
-
function
|
|
402
|
+
function WorldEnvironment({ environment: Environment }) {
|
|
403
|
+
if (Environment !== undefined)
|
|
404
|
+
return _jsx(Environment, {});
|
|
405
|
+
return (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx("gridHelper", { args: [160, 80, "#3a3f4a", "#2b2f38"], "position-y": 0.01 }), _jsx(RockField, {})] }));
|
|
406
|
+
}
|
|
407
|
+
function WorldActors({ entitySprites, entityModels, objectModels, objectStyles, assets, renderEntity, renderObject, selectedIds, }) {
|
|
175
408
|
const ctx = useGameContext();
|
|
176
409
|
const entities = useSceneEntities();
|
|
177
410
|
const objects = useSceneObjects();
|
|
@@ -182,54 +415,193 @@ function WorldView({ entitySprites, entityModels, objectModels, environment: Env
|
|
|
182
415
|
const relation = ctx.scene.entity.canReceive(entity.id, "damage") === null ? "hostile" : "friendly";
|
|
183
416
|
ctx.scene.entity.setTarget(controlledId, relation === "hostile" || entity.role === "npc" ? entity.id : null);
|
|
184
417
|
};
|
|
185
|
-
return (_jsxs(_Fragment, { children: [
|
|
418
|
+
return (_jsxs(_Fragment, { children: [entities
|
|
186
419
|
.filter((entity) => entity.name !== WORLD_ITEM_ENTITY_NAME)
|
|
187
|
-
.map((entity) => (_jsx(EntityMarker, { entity: entity, custom: renderEntity?.(entity), model: resolveModel(entityModels?.[entity.name], assets
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
420
|
+
.map((entity) => (_jsx(EntityMarker, { entity: entity, custom: renderEntity?.(entity), model: resolveModel(entityModels?.[entity.name], assets, {
|
|
421
|
+
seam: "entityModels",
|
|
422
|
+
key: entity.name,
|
|
423
|
+
}), sprite: entitySprites?.[entity.name], isLocal: entity.id === controlledId, targeted: entity.id === targetId, selected: selectedIds.has(entity.id), onSelect: handleSelect }, entity.id))), objects.map((object) => {
|
|
424
|
+
const model = resolveModel(objectModels?.[object.catalogId], assets, {
|
|
425
|
+
seam: "objectModels",
|
|
426
|
+
key: object.catalogId,
|
|
427
|
+
}) ?? tryResolveCatalogModel(object.catalogId, assets);
|
|
428
|
+
return (_jsx(ObjectMarker, { object: object, custom: renderObject?.(object), model: model, style: objectStyles?.[object.catalogId] }, object.instanceId));
|
|
191
429
|
})] }));
|
|
192
430
|
}
|
|
431
|
+
function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, }) {
|
|
432
|
+
return (_jsxs(_Fragment, { children: [_jsx(WorldEnvironment, { environment: environment }), _jsx(WorldActors, { entitySprites: entitySprites, entityModels: entityModels, objectModels: objectModels, objectStyles: objectStyles, assets: assets, renderEntity: renderEntity, renderObject: renderObject, selectedIds: selectedIds })] }));
|
|
433
|
+
}
|
|
193
434
|
function RemotePlayers({ rows }) {
|
|
194
435
|
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))) }));
|
|
195
436
|
}
|
|
196
|
-
function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, }) {
|
|
437
|
+
function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }) {
|
|
438
|
+
const posterElapsedRef = useRef(0);
|
|
439
|
+
const posterDoneRef = useRef(false);
|
|
197
440
|
const motionRef = useRef(createPlayerMotionState());
|
|
441
|
+
const voxelBodyRef = useRef(null);
|
|
442
|
+
const solidCacheRef = useRef({ count: -1, set: new Set() });
|
|
198
443
|
const hasReportedTickError = useRef(false);
|
|
444
|
+
const repeatFiredAtRef = useRef(new Map());
|
|
199
445
|
const slotActions = useMemo(() => findHotbarSlotActions(playable.game.input), [playable]);
|
|
200
446
|
const hotbarId = useMemo(() => hotbarIdFor(playable), [playable]);
|
|
447
|
+
const collision = playable.collision;
|
|
448
|
+
const movement = playable.movement;
|
|
449
|
+
const voxelDims = useMemo(() => ({
|
|
450
|
+
get halfWidth() {
|
|
451
|
+
return collision?.halfWidth ?? 0.3;
|
|
452
|
+
},
|
|
453
|
+
get height() {
|
|
454
|
+
return collision?.height ?? 1.8;
|
|
455
|
+
},
|
|
456
|
+
get stepHeight() {
|
|
457
|
+
return collision?.stepHeight ?? 0.6;
|
|
458
|
+
},
|
|
459
|
+
}), [collision]);
|
|
460
|
+
const movementTuning = useMemo(() => resolvePhysicsTuning(playable.game.physics), [playable]);
|
|
461
|
+
const autoPickupRadius = useMemo(() => {
|
|
462
|
+
const cfg = playable.worldItem?.autoPickup;
|
|
463
|
+
if (cfg === undefined || cfg === false)
|
|
464
|
+
return null;
|
|
465
|
+
const fallback = playable.worldItem?.pickupRadius ?? DEFAULT_PICKUP_RADIUS;
|
|
466
|
+
return cfg === true ? fallback : cfg.radius ?? fallback;
|
|
467
|
+
}, [playable]);
|
|
468
|
+
const ground = useMemo(() => groundFieldFor(playable.game.world), [playable]);
|
|
469
|
+
const drivesPose = useMemo(() => shellDrivesPlayerPose(playable.game.input), [playable]);
|
|
470
|
+
const inputActions = useMemo(() => Object.keys(playable.game.input ?? {}), [playable]);
|
|
471
|
+
const hasTerrain = useMemo(() => hasEnvironmentTerrain(playable.game.world), [playable]);
|
|
201
472
|
useFrame((_state, rawDt) => {
|
|
473
|
+
if (poster) {
|
|
474
|
+
if (posterDoneRef.current)
|
|
475
|
+
return;
|
|
476
|
+
posterElapsedRef.current += Math.min(rawDt, 0.05);
|
|
477
|
+
if (posterElapsedRef.current >= POSTER_SETTLE_SECONDS) {
|
|
478
|
+
posterDoneRef.current = true;
|
|
479
|
+
onPosterSettled();
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const simStart = performance.now();
|
|
202
484
|
try {
|
|
485
|
+
let endPhase = devtools.profile.begin("time+input");
|
|
203
486
|
const dt = Math.min(rawDt, 0.05);
|
|
204
487
|
const gameDt = ctx.time.advance(dt);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
488
|
+
ctx.input.publish(heldActionsFor(tracker, inputActions));
|
|
489
|
+
ctx.input.publishPointer(pointerAxisRef.current);
|
|
490
|
+
const turnInput = (tracker.isDown("turnRight") ? 1 : 0) - (tracker.isDown("turnLeft") ? 1 : 0);
|
|
491
|
+
if (turnInput !== 0)
|
|
492
|
+
yawRef.current = steerYaw(yawRef.current, turnInput, TURN_SPEED, dt);
|
|
493
|
+
endPhase();
|
|
209
494
|
const playerId = ctx.player.possession.active(ctx.player.userId);
|
|
210
495
|
const player = ctx.scene.entity.get(playerId);
|
|
211
496
|
const forwardX = Math.sin(yawRef.current);
|
|
212
497
|
const forwardZ = Math.cos(yawRef.current);
|
|
213
|
-
if (player !== null) {
|
|
498
|
+
if (player !== null && drivesPose) {
|
|
499
|
+
endPhase = devtools.profile.begin("pose");
|
|
214
500
|
const keys = createEmptyMovementKeys();
|
|
215
501
|
keys.w = tracker.isDown("moveForward");
|
|
216
502
|
keys.s = tracker.isDown("moveBack");
|
|
217
503
|
keys.a = tracker.isDown("moveLeft");
|
|
218
504
|
keys.d = tracker.isDown("moveRight");
|
|
219
|
-
keys.shift = tracker.isDown("sprint");
|
|
505
|
+
keys.shift = tracker.isDown("sprint") && (movement?.canSprint?.(ctx) ?? true);
|
|
220
506
|
keys.space = tracker.isDown("jump");
|
|
221
507
|
const intent = resolveMovementIntent(keys, true);
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
508
|
+
const motionBatch = ctx.player.motion.takePending();
|
|
509
|
+
if (collision?.voxel) {
|
|
510
|
+
let body = voxelBodyRef.current;
|
|
511
|
+
if (body === null) {
|
|
512
|
+
body = createVoxelPlayerBody(player.position[0], player.position[1], player.position[2]);
|
|
513
|
+
voxelBodyRef.current = body;
|
|
514
|
+
}
|
|
515
|
+
const objects = ctx.scene.object.list();
|
|
516
|
+
const cache = solidCacheRef.current;
|
|
517
|
+
if (cache.count !== objects.length) {
|
|
518
|
+
cache.set = new Set(objects.map((o) => `${o.position[0]},${o.position[1]},${o.position[2]}`));
|
|
519
|
+
cache.count = objects.length;
|
|
520
|
+
}
|
|
521
|
+
const solids = cache.set;
|
|
522
|
+
const isSolid = (x, y, z) => solids.has(`${x},${y},${z}`);
|
|
523
|
+
body.velocityY = applyMotionImpulses(body.velocityY, motionBatch);
|
|
524
|
+
[body.velocityX, body.velocityZ] = applyHorizontalImpulses(body.velocityX, body.velocityZ, motionBatch);
|
|
525
|
+
advanceVoxelPlayer(body, intent, forwardX, forwardZ, player.movement.walkSpeed ?? 2, rawDt, isSolid, voxelDims, movementTuning, hasTerrain ? (x, z) => ground.sampleHeight(x, z) : undefined);
|
|
526
|
+
if (motionBatch !== null && motionBatch.y !== null)
|
|
527
|
+
body.y = motionBatch.y;
|
|
528
|
+
ctx.scene.entity.setPose(playerId, {
|
|
529
|
+
position: [body.x, body.y, body.z],
|
|
530
|
+
rotationY: intent.moving
|
|
531
|
+
? Math.atan2(body.velocityX, body.velocityZ)
|
|
532
|
+
: player.rotationY,
|
|
533
|
+
dt: rawDt,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
const motion = motionRef.current;
|
|
538
|
+
motion.verticalVelocity = applyMotionImpulses(motion.verticalVelocity, motionBatch);
|
|
539
|
+
[motion.horizontalVelocityX, motion.horizontalVelocityZ] = applyHorizontalImpulses(motion.horizontalVelocityX, motion.horizontalVelocityZ, motionBatch);
|
|
540
|
+
const step = advancePlayerMotion(motion, intent, forwardX, forwardZ, player.movement.walkSpeed ?? 2, rawDt, movementTuning);
|
|
541
|
+
let stepX = step.stepX;
|
|
542
|
+
let stepZ = step.stepZ;
|
|
543
|
+
if (movement?.mode === "axis") {
|
|
544
|
+
const constrained = constrainStepToAxis(stepX, stepZ, movement.axis ?? "x");
|
|
545
|
+
stepX = constrained.stepX;
|
|
546
|
+
stepZ = constrained.stepZ;
|
|
547
|
+
}
|
|
548
|
+
if (movement?.collideObjects === true) {
|
|
549
|
+
const obstacles = nearbyObstacles(ctx.scene.object.list(), player.position);
|
|
550
|
+
const resolved = resolveObstacleStep(player.position, stepX, stepZ, obstacles);
|
|
551
|
+
stepX = resolved.stepX;
|
|
552
|
+
stepZ = resolved.stepZ;
|
|
553
|
+
}
|
|
554
|
+
let nextX = player.position[0] + stepX;
|
|
555
|
+
let nextZ = player.position[2] + stepZ;
|
|
556
|
+
if (movement?.mode === "grid") {
|
|
557
|
+
const snapped = snapPositionToGrid(nextX, nextZ, movement.cellSize ?? 1);
|
|
558
|
+
nextX = snapped[0];
|
|
559
|
+
nextZ = snapped[1];
|
|
560
|
+
}
|
|
561
|
+
let nextY = ground.sampleHeight(nextX, nextZ) + motion.jumpOffset;
|
|
562
|
+
if (motionBatch !== null && motionBatch.y !== null) {
|
|
563
|
+
nextY = motionBatch.y;
|
|
564
|
+
motion.jumpOffset = motionBatch.y - ground.sampleHeight(nextX, nextZ);
|
|
565
|
+
}
|
|
566
|
+
if (movement?.beforeCommit !== undefined) {
|
|
567
|
+
const frame = {
|
|
568
|
+
entityId: playerId,
|
|
569
|
+
current: player.position,
|
|
570
|
+
next: [nextX, nextY, nextZ],
|
|
571
|
+
dt: rawDt,
|
|
572
|
+
ctx,
|
|
573
|
+
};
|
|
574
|
+
const replacement = movement.beforeCommit(frame);
|
|
575
|
+
if (replacement !== undefined) {
|
|
576
|
+
nextX = replacement[0];
|
|
577
|
+
nextY = replacement[1];
|
|
578
|
+
nextZ = replacement[2];
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
ctx.scene.entity.setPose(playerId, {
|
|
582
|
+
position: [nextX, nextY, nextZ],
|
|
583
|
+
rotationY: intent.moving
|
|
584
|
+
? Math.atan2(motion.horizontalVelocityX, motion.horizontalVelocityZ)
|
|
585
|
+
: player.rotationY,
|
|
586
|
+
dt: rawDt,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
endPhase();
|
|
231
590
|
}
|
|
232
|
-
|
|
591
|
+
if (autoPickupRadius !== null) {
|
|
592
|
+
endPhase = devtools.profile.begin("pickup");
|
|
593
|
+
const self = ctx.scene.entity.get(playerId);
|
|
594
|
+
if (self !== null) {
|
|
595
|
+
const nearest = ctx.scene.worldItem.nearestInRadius(self.position, autoPickupRadius);
|
|
596
|
+
if (nearest !== null)
|
|
597
|
+
ctx.scene.worldItem.pickup(nearest, ctx.player.userId);
|
|
598
|
+
}
|
|
599
|
+
endPhase();
|
|
600
|
+
}
|
|
601
|
+
devtools.profile.measure("onTick", () => {
|
|
602
|
+
playable.loop.onTick(ctx, gameDt);
|
|
603
|
+
});
|
|
604
|
+
endPhase = devtools.profile.begin("actions");
|
|
233
605
|
if (tracker.wasPressed("tabTarget")) {
|
|
234
606
|
if (ctx.game.commands.has("target.cycle"))
|
|
235
607
|
ctx.game.commands.run("target.cycle", {});
|
|
@@ -253,12 +625,28 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
253
625
|
});
|
|
254
626
|
}
|
|
255
627
|
}
|
|
628
|
+
const aimOverride = pointerAim ? pointerAimFor(ctx, pointerService) : undefined;
|
|
629
|
+
const commandAim = aimOverride ?? { yaw: yawRef.current, pitch: pitchRef.current };
|
|
630
|
+
if (collisionDebug.getState().layers.aimLaser) {
|
|
631
|
+
const aimFrom = ctx.player.possession.active(playerId) ?? playerId;
|
|
632
|
+
collisionDebug.setAimProbe({
|
|
633
|
+
from: aimFrom,
|
|
634
|
+
aim: commandAim,
|
|
635
|
+
originPolicy: { kind: "muzzle" },
|
|
636
|
+
maxDistance: 100,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
else if (collisionDebug.getAimProbe() !== null) {
|
|
640
|
+
collisionDebug.setAimProbe(null);
|
|
641
|
+
}
|
|
642
|
+
const nowMs = performance.now();
|
|
256
643
|
for (const action of Object.keys(playable.game.input ?? {})) {
|
|
257
|
-
|
|
258
|
-
continue;
|
|
644
|
+
const pressed = tracker.wasPressed(action);
|
|
259
645
|
if (action === "ping" && pingCommand !== undefined)
|
|
260
646
|
continue;
|
|
261
647
|
if (action === "interact") {
|
|
648
|
+
if (!pressed)
|
|
649
|
+
continue;
|
|
262
650
|
const prompts = playable.prompts?.(ctx);
|
|
263
651
|
const focus = prompts === undefined ? null : ctx.scene.entity.get(playerId);
|
|
264
652
|
if (prompts !== undefined && focus !== null) {
|
|
@@ -269,12 +657,12 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
269
657
|
}
|
|
270
658
|
continue;
|
|
271
659
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
660
|
+
if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
|
|
661
|
+
continue;
|
|
662
|
+
repeatFiredAtRef.current.set(action, nowMs);
|
|
663
|
+
dispatchBoundAction(ctx, action, yawRef.current, pitchRef.current, commandAim);
|
|
275
664
|
}
|
|
276
665
|
if (hotbarId !== null) {
|
|
277
|
-
const aimOverride = pointerAim ? pointerAimFor(ctx, pointerService) : undefined;
|
|
278
666
|
for (const { action, slot } of slotActions) {
|
|
279
667
|
if (!tracker.wasPressed(action))
|
|
280
668
|
continue;
|
|
@@ -298,8 +686,10 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
298
686
|
}
|
|
299
687
|
}
|
|
300
688
|
tracker.endFrame();
|
|
689
|
+
endPhase();
|
|
301
690
|
const serverId = serverIdRef.current;
|
|
302
691
|
if (multiplayer !== null && serverId !== null) {
|
|
692
|
+
endPhase = devtools.profile.begin("presence");
|
|
303
693
|
const focus = ctx.scene.entity.get(playerId);
|
|
304
694
|
if (focus !== null) {
|
|
305
695
|
multiplayer.backend.presenceSync.syncPose(serverId, {
|
|
@@ -310,6 +700,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
310
700
|
rotationPitch: pitchRef.current,
|
|
311
701
|
});
|
|
312
702
|
}
|
|
703
|
+
endPhase();
|
|
313
704
|
}
|
|
314
705
|
}
|
|
315
706
|
catch (error) {
|
|
@@ -318,16 +709,66 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
318
709
|
onRuntimeError(error, "tick");
|
|
319
710
|
}
|
|
320
711
|
}
|
|
712
|
+
devtools.frame.record({ frameMs: rawDt * 1000, simMs: performance.now() - simStart });
|
|
321
713
|
}, GAME_SIM_FRAME_PRIORITY);
|
|
322
714
|
return null;
|
|
323
715
|
}
|
|
716
|
+
function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, onRuntimeError, }) {
|
|
717
|
+
const hasReportedTickError = useRef(false);
|
|
718
|
+
const repeatFiredAtRef = useRef(new Map());
|
|
719
|
+
const lastFrameRef = useRef(null);
|
|
720
|
+
const inputActions = useMemo(() => Object.keys(playable.game.input ?? {}), [playable]);
|
|
721
|
+
useEffect(() => {
|
|
722
|
+
let frameId;
|
|
723
|
+
const tick = (now) => {
|
|
724
|
+
frameId = requestAnimationFrame(tick);
|
|
725
|
+
const last = lastFrameRef.current;
|
|
726
|
+
lastFrameRef.current = now;
|
|
727
|
+
if (last === null)
|
|
728
|
+
return;
|
|
729
|
+
const rawDt = (now - last) / 1000;
|
|
730
|
+
const simStart = performance.now();
|
|
731
|
+
try {
|
|
732
|
+
let endPhase = devtools.profile.begin("time+input");
|
|
733
|
+
const dt = Math.min(rawDt, 0.05);
|
|
734
|
+
const gameDt = ctx.time.advance(dt);
|
|
735
|
+
ctx.input.publish(heldActionsFor(tracker, inputActions));
|
|
736
|
+
ctx.input.publishPointer(pointerAxisRef.current);
|
|
737
|
+
endPhase();
|
|
738
|
+
devtools.profile.measure("onTick", () => {
|
|
739
|
+
playable.loop.onTick(ctx, gameDt);
|
|
740
|
+
});
|
|
741
|
+
endPhase = devtools.profile.begin("actions");
|
|
742
|
+
const nowMs = performance.now();
|
|
743
|
+
for (const action of inputActions) {
|
|
744
|
+
if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
|
|
745
|
+
continue;
|
|
746
|
+
repeatFiredAtRef.current.set(action, nowMs);
|
|
747
|
+
dispatchBoundAction(ctx, action, 0, 0, { yaw: 0, pitch: 0 });
|
|
748
|
+
}
|
|
749
|
+
tracker.endFrame();
|
|
750
|
+
endPhase();
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
if (!hasReportedTickError.current) {
|
|
754
|
+
hasReportedTickError.current = true;
|
|
755
|
+
onRuntimeError(error, "tick");
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
devtools.frame.record({ frameMs: rawDt * 1000, simMs: performance.now() - simStart });
|
|
759
|
+
};
|
|
760
|
+
frameId = requestAnimationFrame(tick);
|
|
761
|
+
return () => cancelAnimationFrame(frameId);
|
|
762
|
+
}, [ctx, playable, tracker, pointerAxisRef, onRuntimeError, inputActions]);
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
324
765
|
class GameUiErrorBoundary extends Component {
|
|
325
766
|
state = { failed: false };
|
|
326
767
|
static getDerivedStateFromError() {
|
|
327
768
|
return { failed: true };
|
|
328
769
|
}
|
|
329
|
-
componentDidCatch(error) {
|
|
330
|
-
this.props.onRuntimeError(error, "ui-render");
|
|
770
|
+
componentDidCatch(error, info) {
|
|
771
|
+
this.props.onRuntimeError(error, "ui-render", info.componentStack ?? undefined);
|
|
331
772
|
}
|
|
332
773
|
render() {
|
|
333
774
|
if (this.state.failed)
|
|
@@ -335,13 +776,53 @@ class GameUiErrorBoundary extends Component {
|
|
|
335
776
|
return this.props.children;
|
|
336
777
|
}
|
|
337
778
|
}
|
|
338
|
-
function
|
|
779
|
+
function expandedReactError(message) {
|
|
780
|
+
return /Minified React error #185\b/.test(message)
|
|
781
|
+
? "Maximum update depth exceeded. A component is repeatedly updating state during render or after every update."
|
|
782
|
+
: null;
|
|
783
|
+
}
|
|
784
|
+
function diagnosticReport(diagnostic, gameName) {
|
|
785
|
+
const explanation = expandedReactError(diagnostic.message);
|
|
786
|
+
return [
|
|
787
|
+
"JGengine runtime error",
|
|
788
|
+
`Game: ${gameName}`,
|
|
789
|
+
`Engine: ${VERSION}`,
|
|
790
|
+
`Phase: ${diagnostic.phase}`,
|
|
791
|
+
`Time: ${diagnostic.capturedAt}`,
|
|
792
|
+
`Page: ${window.location.origin}${window.location.pathname}`,
|
|
793
|
+
`Browser: ${navigator.userAgent}`,
|
|
794
|
+
explanation === null ? null : `Explanation: ${explanation}`,
|
|
795
|
+
"",
|
|
796
|
+
`Message: ${diagnostic.message}`,
|
|
797
|
+
diagnostic.stack === undefined ? null : `JavaScript stack:\n${diagnostic.stack}`,
|
|
798
|
+
diagnostic.componentStack === undefined ? null : `React component stack:\n${diagnostic.componentStack}`,
|
|
799
|
+
]
|
|
800
|
+
.filter((line) => line !== null)
|
|
801
|
+
.join("\n");
|
|
802
|
+
}
|
|
803
|
+
function DiagnosticOverlay({ diagnostics, gameName }) {
|
|
804
|
+
const [copied, setCopied] = useState(false);
|
|
339
805
|
if (diagnostics.length === 0)
|
|
340
806
|
return null;
|
|
341
807
|
const latest = diagnostics[diagnostics.length - 1];
|
|
342
|
-
|
|
808
|
+
const explanation = expandedReactError(latest.message);
|
|
809
|
+
const report = diagnosticReport(latest, gameName);
|
|
810
|
+
const issueBody = report.length > 8000 ? `${report.slice(0, 8000)}\n\n[Report truncated; use Copy error for the full report.]` : report;
|
|
811
|
+
const issueUrl = `https://github.com/Noisemaker111/jgengine/issues/new?title=${encodeURIComponent(`[BUG] ${latest.phase}: ${latest.message.slice(0, 100)}`)}&body=${encodeURIComponent(issueBody)}`;
|
|
812
|
+
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: () => {
|
|
813
|
+
void navigator.clipboard.writeText(report).then(() => {
|
|
814
|
+
setCopied(true);
|
|
815
|
+
window.setTimeout(() => setCopied(false), 2000);
|
|
816
|
+
});
|
|
817
|
+
}, 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" })] })] }));
|
|
343
818
|
}
|
|
344
|
-
|
|
819
|
+
const POSTER_SETTLE_SECONDS = 1.6;
|
|
820
|
+
export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null, poster = false, onContextReady, }) {
|
|
821
|
+
const multiplayer = useMemo(() => (rawMultiplayer === null ? null : withDevtoolsLatency(rawMultiplayer)), [rawMultiplayer]);
|
|
822
|
+
const devtoolsEnabled = playable.devtools !== false && !poster;
|
|
823
|
+
const [devtoolsOpen, setDevtoolsOpen] = useState(false);
|
|
824
|
+
const [posterFrozen, setPosterFrozen] = useState(false);
|
|
825
|
+
const posterSettledRef = useRef(false);
|
|
345
826
|
const [ctx, setCtx] = useState(null);
|
|
346
827
|
const [diagnostics, setDiagnostics] = useState([]);
|
|
347
828
|
const [remotePlayers, setRemotePlayers] = useState([]);
|
|
@@ -352,18 +833,55 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
352
833
|
const cameraDraggingRef = useRef(false);
|
|
353
834
|
const primaryClickRef = useRef(false);
|
|
354
835
|
const pointerDownRef = useRef(null);
|
|
836
|
+
const pointerAxisRef = useRef(null);
|
|
355
837
|
const marqueeStartRef = useRef(null);
|
|
838
|
+
const f2HeldRef = useRef(false);
|
|
839
|
+
const f2ChordedRef = useRef(false);
|
|
356
840
|
const pointerService = useMemo(() => createPointerService(), []);
|
|
357
841
|
const selection = useMemo(() => createSelectionSet(), [playable]);
|
|
358
842
|
const [marquee, setMarquee] = useState(null);
|
|
359
843
|
const [selectedIds, setSelectedIds] = useState(() => new Set());
|
|
360
844
|
const [contextMenu, setContextMenu] = useState(null);
|
|
361
|
-
const
|
|
845
|
+
const settingsStore = useMemo(() => createSettingsStore(), []);
|
|
846
|
+
const [bindingOverrides, setBindingOverrides] = useState(() => loadBindingOverrides(playable.game.name));
|
|
847
|
+
useEffect(() => {
|
|
848
|
+
setBindingOverrides(loadBindingOverrides(playable.game.name));
|
|
849
|
+
}, [playable]);
|
|
850
|
+
const effectiveInput = useMemo(() => applyBindingOverrides(playable.game.input ?? {}, bindingOverrides), [playable, bindingOverrides]);
|
|
851
|
+
const rebindAction = useCallback((action, code) => setBindingOverrides(saveBindingOverride(playable.game.name, action, [code])), [playable]);
|
|
852
|
+
const resetActionBinding = useCallback((action) => setBindingOverrides(clearBindingOverride(playable.game.name, action)), [playable]);
|
|
853
|
+
const tracker = useMemo(() => createActionStateTracker(toActionStateBindingMap(withTouchCodes(effectiveInput))), [effectiveInput]);
|
|
854
|
+
const graphics = useGraphicsSettings(settingsStore, playable.shadows ?? true);
|
|
855
|
+
const trackPointerAxis = (event) => {
|
|
856
|
+
const rect = wrapperRef.current?.getBoundingClientRect();
|
|
857
|
+
if (rect === undefined)
|
|
858
|
+
return;
|
|
859
|
+
pointerAxisRef.current = normalizePointerToAxis(event.clientX, event.clientY, rect);
|
|
860
|
+
};
|
|
861
|
+
const deactivatePointerAxis = () => {
|
|
862
|
+
const state = pointerAxisRef.current;
|
|
863
|
+
if (state !== null && state.active)
|
|
864
|
+
pointerAxisRef.current = { ...state, active: false };
|
|
865
|
+
};
|
|
866
|
+
const touchScheme = useMemo(() => deriveTouchScheme(playable.game.input, {
|
|
867
|
+
reserved: RESERVED_INPUT_ACTIONS,
|
|
868
|
+
firstPerson: resolveRigKind(playable.camera) === "first",
|
|
869
|
+
config: playable.touch,
|
|
870
|
+
}), [playable]);
|
|
871
|
+
const { coarsePointer, portrait, compact } = useDisplayProfile();
|
|
872
|
+
const touchSink = useMemo(() => ({ onCodeDown: (code) => tracker.handleDown(code), onCodeUp: (code) => tracker.handleUp(code) }), [tracker]);
|
|
362
873
|
const audioEngine = useMemo(() => createAudioEngine({ sounds: playable.audio?.sounds, buses: playable.audio?.buses }), [playable]);
|
|
363
874
|
useEffect(() => () => audioEngine.dispose(), [audioEngine]);
|
|
875
|
+
useEffect(() => {
|
|
876
|
+
if (ctx === null)
|
|
877
|
+
return;
|
|
878
|
+
return ctx.game.events.on("audio.play", ({ sound, at }) => {
|
|
879
|
+
audioEngine.playOneShot(sound, at === undefined ? undefined : { x: at[0], y: at[1], z: at[2] });
|
|
880
|
+
});
|
|
881
|
+
}, [ctx, audioEngine]);
|
|
364
882
|
const userId = multiplayer?.userId ?? DEV_USER_ID;
|
|
365
|
-
const reportRuntimeError = (error, phase) => {
|
|
366
|
-
const diagnostic = logRuntimeError(error, phase);
|
|
883
|
+
const reportRuntimeError = (error, phase, componentStack) => {
|
|
884
|
+
const diagnostic = logRuntimeError(error, phase, componentStack);
|
|
367
885
|
setDiagnostics((current) => [...current.slice(-4), { ...diagnostic, id: Date.now() + current.length }]);
|
|
368
886
|
};
|
|
369
887
|
useEffect(() => {
|
|
@@ -377,6 +895,7 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
377
895
|
});
|
|
378
896
|
playable.loop.onInit(context);
|
|
379
897
|
playable.loop.onNewPlayer(context);
|
|
898
|
+
onContextReady?.(context);
|
|
380
899
|
setCtx(context);
|
|
381
900
|
}
|
|
382
901
|
catch (error) {
|
|
@@ -435,6 +954,33 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
435
954
|
if (remoteUnsub !== undefined)
|
|
436
955
|
cleanups.push(remoteUnsub);
|
|
437
956
|
}
|
|
957
|
+
const chatSync = multiplayer.backend.chatSyncFor?.(joined.serverId);
|
|
958
|
+
if (chatSync !== undefined) {
|
|
959
|
+
const globalChannelIds = new Set(ctx.game.chat
|
|
960
|
+
.channels()
|
|
961
|
+
.filter((channel) => channel.kind === "global")
|
|
962
|
+
.map((channel) => channel.id));
|
|
963
|
+
const seenRemoteChat = new Set();
|
|
964
|
+
cleanups.push(ctx.game.events.subscribe("chat.message", (event) => {
|
|
965
|
+
if (event.fromUserId !== multiplayer.userId)
|
|
966
|
+
return;
|
|
967
|
+
if (!globalChannelIds.has(event.channelId))
|
|
968
|
+
return;
|
|
969
|
+
void chatSync.send(event.channelId, event.body).catch(() => undefined);
|
|
970
|
+
}));
|
|
971
|
+
for (const channelId of globalChannelIds) {
|
|
972
|
+
cleanups.push(chatSync.subscribe(channelId, (messages) => {
|
|
973
|
+
for (const message of messages) {
|
|
974
|
+
if (message.fromUserId === multiplayer.userId)
|
|
975
|
+
continue;
|
|
976
|
+
if (seenRemoteChat.has(message.id))
|
|
977
|
+
continue;
|
|
978
|
+
seenRemoteChat.add(message.id);
|
|
979
|
+
ctx.game.chat.send(message.fromUserId, message.channelId, message.body);
|
|
980
|
+
}
|
|
981
|
+
}));
|
|
982
|
+
}
|
|
983
|
+
}
|
|
438
984
|
})
|
|
439
985
|
.catch(() => undefined);
|
|
440
986
|
return () => {
|
|
@@ -462,17 +1008,63 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
462
1008
|
? playable.camera
|
|
463
1009
|
: { ...playable.camera, followEntityId: controlledEntityId };
|
|
464
1010
|
const rigKind = resolveRigKind(cameraConfig);
|
|
1011
|
+
if (rigKind === "none" || playable.presentation === "hud") {
|
|
1012
|
+
const GameUI = playable.GameUI;
|
|
1013
|
+
return (_jsxs("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
|
|
1014
|
+
if (event.code === "F2" && devtoolsEnabled) {
|
|
1015
|
+
event.preventDefault();
|
|
1016
|
+
f2HeldRef.current = true;
|
|
1017
|
+
f2ChordedRef.current = false;
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (f2HeldRef.current) {
|
|
1021
|
+
f2ChordedRef.current = true;
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
if (event.code === "Tab" || event.code === "Space")
|
|
1025
|
+
event.preventDefault();
|
|
1026
|
+
tracker.handleDown(event.code);
|
|
1027
|
+
}, onKeyUp: (event) => {
|
|
1028
|
+
if (event.code === "F2" && devtoolsEnabled) {
|
|
1029
|
+
f2HeldRef.current = false;
|
|
1030
|
+
if (!f2ChordedRef.current)
|
|
1031
|
+
setDevtoolsOpen((current) => !current);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
tracker.handleUp(event.code);
|
|
1035
|
+
}, onBlur: () => {
|
|
1036
|
+
f2HeldRef.current = false;
|
|
1037
|
+
tracker.reset();
|
|
1038
|
+
}, onPointerMove: trackPointerAxis, onPointerLeave: deactivatePointerAxis, onPointerCancel: deactivatePointerAxis, children: [_jsx(HudOnlyDriver, { ctx: ctx, playable: playable, tracker: tracker, pointerAxisRef: pointerAxisRef, onRuntimeError: reportRuntimeError }), _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(HudViewportProvider, { platforms: playable.platforms, config: playable.hudFit, userScale: graphics.uiScale, children: _jsx(GameUI, {}) }) }) }), devtoolsEnabled ? (_jsx(DevtoolsOverlay, { open: devtoolsOpen, ctx: ctx, playable: playable, multiplayer: multiplayer })) : null, _jsx(DiagnosticOverlay, { diagnostics: diagnostics, gameName: playable.game.name })] }));
|
|
1039
|
+
}
|
|
465
1040
|
const firstPerson = rigKind === "first";
|
|
466
1041
|
const showReticle = (firstPerson && playable.camera?.firstPerson?.reticle !== false) || rigKind === "shoulder";
|
|
1042
|
+
const rtsPanKeysEnabled = !rtsPanKeysConflict(playable.game.input);
|
|
467
1043
|
const worldBars = playable.worldHealthBars;
|
|
468
1044
|
const barsStatId = worldBars === undefined || worldBars === false
|
|
469
1045
|
? null
|
|
470
1046
|
: worldBars === true
|
|
471
1047
|
? "health"
|
|
472
1048
|
: worldBars.statId ?? "health";
|
|
1049
|
+
const barsRoles = worldBars === undefined || worldBars === true || worldBars === false ? undefined : worldBars.roles;
|
|
1050
|
+
const resolveEntityRole = (entity) => playable.content.entityById?.(entity.name)?.role;
|
|
473
1051
|
const pointer = playable.pointer;
|
|
474
1052
|
const pointerUsesLeft = pointer !== undefined && (pointer.select === true || pointer.moveCommand !== undefined);
|
|
475
1053
|
const selectFilter = pointer?.selectFilter;
|
|
1054
|
+
const worldSky = resolveWorldSky(playable.game.world);
|
|
1055
|
+
const world = playable.game.world;
|
|
1056
|
+
const AutoEnvironment = playable.environment ??
|
|
1057
|
+
(world?.kind === "environment"
|
|
1058
|
+
? () => _jsx(EnvironmentScene, { feature: world })
|
|
1059
|
+
: world?.kind === "biomes" || world?.kind === "voxel" || world?.kind === "plots" || world?.kind === "tilemap"
|
|
1060
|
+
? () => _jsx(GridWorldScene, { feature: world })
|
|
1061
|
+
: undefined);
|
|
1062
|
+
const backdrop = playable.backdrop;
|
|
1063
|
+
const backdropSky = backdrop?.sky !== undefined ? resolveSkyDescriptor(backdrop.sky) : undefined;
|
|
1064
|
+
const effectiveSky = backdropSky ?? worldSky;
|
|
1065
|
+
const backgroundColor = backdrop?.background ?? (effectiveSky === undefined ? DEFAULT_BACKGROUND_COLOR : undefined);
|
|
1066
|
+
const lighting = playable.lighting;
|
|
1067
|
+
const orthographic = playable.camera?.projection === "orthographic";
|
|
476
1068
|
const localXY = (event) => {
|
|
477
1069
|
const rect = wrapperRef.current?.getBoundingClientRect();
|
|
478
1070
|
return rect === undefined
|
|
@@ -496,6 +1088,7 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
496
1088
|
};
|
|
497
1089
|
const handlePointerDown = (event) => {
|
|
498
1090
|
wrapperRef.current?.focus();
|
|
1091
|
+
trackPointerAxis(event);
|
|
499
1092
|
audioEngine.resume();
|
|
500
1093
|
if (contextMenu !== null)
|
|
501
1094
|
setContextMenu(null);
|
|
@@ -507,6 +1100,7 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
507
1100
|
}
|
|
508
1101
|
};
|
|
509
1102
|
const handlePointerMove = (event) => {
|
|
1103
|
+
trackPointerAxis(event);
|
|
510
1104
|
if (pointer?.select !== true)
|
|
511
1105
|
return;
|
|
512
1106
|
const start = marqueeStartRef.current;
|
|
@@ -588,6 +1182,14 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
588
1182
|
if (ctx.game.commands.has(pointer.orderCommand)) {
|
|
589
1183
|
ctx.game.commands.run(pointer.orderCommand, { selection: selection.list(), point: hit.point });
|
|
590
1184
|
}
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
if (pointer.secondaryCommand !== undefined && ctx.game.commands.has(pointer.secondaryCommand)) {
|
|
1188
|
+
const aim = (pointer.aim === true ? pointerAimFor(ctx, pointerService) : undefined) ?? {
|
|
1189
|
+
yaw: yawRef.current,
|
|
1190
|
+
pitch: pitchRef.current,
|
|
1191
|
+
};
|
|
1192
|
+
ctx.game.commands.run(pointer.secondaryCommand, { point: hit.point, entity: hit.entity, object: hit.object, aim });
|
|
591
1193
|
}
|
|
592
1194
|
};
|
|
593
1195
|
const handleVerbPick = (verb) => {
|
|
@@ -597,21 +1199,90 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
597
1199
|
ctx.game.commands.run(verb.command, contextVerbInput(state.menu, verb));
|
|
598
1200
|
}
|
|
599
1201
|
};
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
1202
|
+
const controlsActive = playControlsActive(ctx);
|
|
1203
|
+
const settingsDisabled = playable.settings === false;
|
|
1204
|
+
const settingsConfig = playable.settings === false || playable.settings === undefined ? {} : playable.settings;
|
|
1205
|
+
const settingsSurface = settingsDisabled ? false : settingsConfig.surface ?? false;
|
|
1206
|
+
const settingsVariant = settingsConfig.variant ?? "panel";
|
|
1207
|
+
const hideCategories = settingsDisabled ? BUILT_IN_SETTING_CATEGORIES : settingsConfig.hide ?? [];
|
|
1208
|
+
const fovControlEnabled = !orthographic && playable.camera?.playerFov?.control !== false;
|
|
1209
|
+
const settingsHostsFov = !settingsDisabled && !hideCategories.includes("gameplay") && fovControlEnabled;
|
|
1210
|
+
const settingsActions = settingsDisabled
|
|
1211
|
+
? []
|
|
1212
|
+
: (settingsConfig.actions ?? []).map((action) => ({
|
|
1213
|
+
id: action.id,
|
|
1214
|
+
label: action.label,
|
|
1215
|
+
kind: action.kind ?? "default",
|
|
1216
|
+
description: action.description,
|
|
1217
|
+
run: () => action.run(ctx),
|
|
1218
|
+
}));
|
|
1219
|
+
const touchScale = compact ? 0.88 : 1;
|
|
1220
|
+
const dockMounted = !poster &&
|
|
1221
|
+
coarsePointer &&
|
|
1222
|
+
controlsActive &&
|
|
1223
|
+
touchScheme !== null &&
|
|
1224
|
+
(touchScheme.joystick !== null || touchScheme.buttons.length > 0);
|
|
1225
|
+
const orientationMismatch = !poster &&
|
|
1226
|
+
coarsePointer &&
|
|
1227
|
+
playable.orientation !== undefined &&
|
|
1228
|
+
(playable.orientation === "landscape") === portrait;
|
|
1229
|
+
return (_jsx(SettingsProvider, { store: settingsStore, children: _jsxs(PlayerFovProvider, { config: playable.camera, orthographic: orthographic, children: [_jsx(AudioSettingsBridge, { store: settingsStore, engine: audioEngine, buses: playable.audio?.buses }), _jsx(SettingsRuntime, { variant: settingsVariant, surface: settingsSurface, actions: settingsActions, input: playable.game.input ?? {}, buses: playable.audio?.buses, extra: settingsConfig.extra ?? [], categories: settingsConfig.categories ?? [], hide: hideCategories, fovEnabled: fovControlEnabled, hideBindings: settingsConfig.hideBindings ?? [], overrides: bindingOverrides, rebind: rebindAction, resetBinding: resetActionBinding, children: _jsxs("div", { ref: wrapperRef, tabIndex: 0, ...(poster && posterFrozen ? { "data-poster-ready": "" } : {}), className: "relative h-full w-full bg-neutral-950 outline-none", style: {
|
|
1230
|
+
"--jg-hud-dock-clearance": `${dockMounted ? touchDockClearance(touchScheme, touchScale) : 0}px`,
|
|
1231
|
+
}, onKeyDown: (event) => {
|
|
1232
|
+
if (event.code === "F2" && devtoolsEnabled) {
|
|
1233
|
+
event.preventDefault();
|
|
1234
|
+
f2HeldRef.current = true;
|
|
1235
|
+
f2ChordedRef.current = false;
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (f2HeldRef.current) {
|
|
1239
|
+
f2ChordedRef.current = true;
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
if (event.code === "Tab" || event.code === "Space")
|
|
1243
|
+
event.preventDefault();
|
|
1244
|
+
tracker.handleDown(event.code);
|
|
1245
|
+
}, onKeyUp: (event) => {
|
|
1246
|
+
if (event.code === "F2" && devtoolsEnabled) {
|
|
1247
|
+
f2HeldRef.current = false;
|
|
1248
|
+
if (!f2ChordedRef.current) {
|
|
1249
|
+
document.exitPointerLock?.();
|
|
1250
|
+
setDevtoolsOpen((current) => !current);
|
|
1251
|
+
}
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
tracker.handleUp(event.code);
|
|
1255
|
+
}, onBlur: () => {
|
|
1256
|
+
f2HeldRef.current = false;
|
|
1257
|
+
tracker.reset();
|
|
1258
|
+
}, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerLeave: deactivatePointerAxis, onPointerCancel: deactivatePointerAxis, onContextMenu: handleContextMenu, onWheel: (event) => {
|
|
1259
|
+
if (ctx === null || !event.shiftKey)
|
|
1260
|
+
return;
|
|
1261
|
+
event.preventDefault();
|
|
1262
|
+
if (event.deltaY < 0 && ctx.game.commands.has("ui.hotbarScrollNext")) {
|
|
1263
|
+
ctx.game.commands.run("ui.hotbarScrollNext", {});
|
|
1264
|
+
}
|
|
1265
|
+
else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
|
|
1266
|
+
ctx.game.commands.run("ui.hotbarScrollPrev", {});
|
|
1267
|
+
}
|
|
1268
|
+
}, children: [_jsxs(Canvas, { frameloop: poster && posterFrozen ? "demand" : "always", orthographic: orthographic, camera: orthographic
|
|
1269
|
+
? {
|
|
1270
|
+
zoom: playable.camera?.frustum?.zoom ?? CAMERA_FRUSTUM_DEFAULTS.zoom,
|
|
1271
|
+
near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
|
|
1272
|
+
far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
|
|
1273
|
+
}
|
|
1274
|
+
: {
|
|
1275
|
+
fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
|
|
1276
|
+
near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
|
|
1277
|
+
far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
|
|
1278
|
+
}, 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 }) }), WorldOverlay !== undefined ? _jsx(WorldOverlay, {}) : null, barsStatId !== null ? (_jsx(WorldEntityBars, { statId: barsStatId, roles: barsRoles, resolveRole: resolveEntityRole })) : 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) => {
|
|
1279
|
+
cameraDraggingRef.current = dragging;
|
|
1280
|
+
} }), _jsx(PointerProbe, { service: pointerService })] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, pointerAxisRef: pointerAxisRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand, poster: poster, onPosterSettled: () => {
|
|
1281
|
+
if (posterSettledRef.current)
|
|
1282
|
+
return;
|
|
1283
|
+
posterSettledRef.current = true;
|
|
1284
|
+
setPosterFrozen(true);
|
|
1285
|
+
} }), _jsx(DevtoolsRendererProbe, {})] }), !poster && 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: () => {
|
|
1286
|
+
primaryClickRef.current = true;
|
|
1287
|
+
} })) : null, _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(HudViewportProvider, { platforms: playable.platforms, config: playable.hudFit, userScale: graphics.uiScale, children: _jsx(GameUI, {}) }) }) }), !poster && showReticle ? _jsx(Reticle, {}) : null, dockMounted && touchScheme !== null ? (_jsx(TouchControlsDock, { scheme: touchScheme, sink: touchSink, scale: touchScale })) : null, orientationMismatch && playable.orientation !== undefined ? (_jsx(OrientationHint, { wanted: playable.orientation })) : null, 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, {})] }) })] }) }));
|
|
617
1288
|
}
|