@jgengine/shell 0.6.0 → 0.8.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 +214 -2
- package/README.md +26 -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 +33 -1
- package/dist/GamePlayerShell.js +641 -59
- package/dist/audio/AudioComponents.d.ts +12 -0
- package/dist/audio/AudioComponents.js +53 -0
- package/dist/audio/audioEngine.d.ts +23 -0
- package/dist/audio/audioEngine.js +110 -0
- package/dist/behaviour.d.ts +31 -0
- package/dist/behaviour.js +63 -0
- package/dist/camera/GameCameraRig.d.ts +16 -0
- package/dist/camera/GameCameraRig.js +56 -0
- package/dist/camera/GameFirstPersonCamera.js +2 -0
- package/dist/camera/GameInspectionCamera.d.ts +6 -0
- package/dist/camera/GameInspectionCamera.js +24 -0
- package/dist/camera/GameOrbitCamera.d.ts +3 -1
- package/dist/camera/GameOrbitCamera.js +4 -2
- package/dist/camera/cameraRigs.d.ts +26 -0
- package/dist/camera/cameraRigs.js +522 -0
- package/dist/camera/index.d.ts +7 -0
- package/dist/camera/index.js +7 -0
- package/dist/camera/inspectionCameraMath.d.ts +25 -0
- package/dist/camera/inspectionCameraMath.js +44 -0
- package/dist/camera/rigMath.d.ts +169 -0
- package/dist/camera/rigMath.js +393 -0
- package/dist/camera/rigResolve.d.ts +15 -0
- package/dist/camera/rigResolve.js +52 -0
- package/dist/camera/shakeChannel.d.ts +29 -0
- package/dist/camera/shakeChannel.js +38 -0
- package/dist/defineGame.d.ts +16 -0
- package/dist/defineGame.js +51 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +14 -0
- package/dist/devtools/DevtoolsOverlay.js +334 -0
- package/dist/environment/Daylight.d.ts +50 -0
- package/dist/environment/Daylight.js +100 -0
- package/dist/environment/EnvironmentScene.js +18 -26
- 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 +2 -0
- package/dist/environment/index.js +2 -0
- package/dist/map/MapMarkerBeacons.d.ts +12 -0
- package/dist/map/MapMarkerBeacons.js +16 -0
- package/dist/map/index.d.ts +2 -0
- package/dist/map/index.js +2 -0
- package/dist/map/terrainMap.d.ts +23 -0
- package/dist/map/terrainMap.js +79 -0
- package/dist/materialOverride.d.ts +8 -0
- package/dist/materialOverride.js +32 -0
- package/dist/multiplayer.d.ts +16 -9
- package/dist/multiplayer.js +62 -12
- package/dist/pointer/PointerOverlays.d.ts +12 -0
- package/dist/pointer/PointerOverlays.js +17 -0
- package/dist/pointer/PointerProbe.d.ts +4 -0
- package/dist/pointer/PointerProbe.js +40 -0
- package/dist/pointer/pointerService.d.ts +26 -0
- package/dist/pointer/pointerService.js +89 -0
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +3 -1
- package/dist/render/modelRender.d.ts +14 -0
- package/dist/render/modelRender.js +79 -0
- package/dist/replay/useSessionRecorder.d.ts +14 -0
- package/dist/replay/useSessionRecorder.js +22 -0
- package/dist/structures/PlacementGhost.d.ts +8 -0
- package/dist/structures/PlacementGhost.js +11 -0
- package/dist/structures/index.d.ts +2 -0
- package/dist/structures/index.js +2 -0
- package/dist/structures.d.ts +1 -0
- package/dist/structures.js +1 -0
- package/dist/terrain/CarvedTerrain.d.ts +22 -0
- package/dist/terrain/CarvedTerrain.js +19 -0
- package/dist/terrain/EditableGround.d.ts +11 -0
- package/dist/terrain/EditableGround.js +38 -0
- package/dist/terrain/TerraformBrushCursor.d.ts +8 -0
- package/dist/terrain/TerraformBrushCursor.js +13 -0
- package/dist/terrain/index.d.ts +4 -1
- package/dist/terrain/index.js +4 -1
- package/dist/terrain/terrainMath.d.ts +15 -1
- package/dist/terrain/terrainMath.js +87 -1
- package/dist/touch/TouchControlsOverlay.d.ts +18 -0
- package/dist/touch/TouchControlsOverlay.js +173 -0
- package/dist/vision/FrustumSensorHud.d.ts +21 -0
- package/dist/vision/FrustumSensorHud.js +67 -0
- package/dist/vision/HiddenStateProbeHud.d.ts +11 -0
- package/dist/vision/HiddenStateProbeHud.js +11 -0
- package/dist/vision/RevealVision.d.ts +28 -0
- package/dist/vision/RevealVision.js +53 -0
- package/dist/water/OceanShader.d.ts +1 -1
- package/dist/water/OceanShader.js +3 -3
- package/dist/weather/FireSpreadLayer.d.ts +14 -0
- package/dist/weather/FireSpreadLayer.js +68 -0
- package/dist/weather/index.d.ts +1 -0
- package/dist/weather/index.js +1 -0
- package/dist/world/DataObjects.d.ts +44 -0
- package/dist/world/DataObjects.js +75 -0
- package/dist/world/GridWorldScene.d.ts +10 -0
- package/dist/world/GridWorldScene.js +54 -0
- package/dist/world/InstancedJoints.d.ts +14 -0
- package/dist/world/InstancedJoints.js +32 -0
- package/dist/world/WorldHud.d.ts +7 -1
- package/dist/world/WorldHud.js +76 -14
- package/dist/world/WorldItems.d.ts +5 -0
- package/dist/world/WorldItems.js +50 -0
- package/dist/world/floatTextStyle.d.ts +14 -0
- package/dist/world/floatTextStyle.js +39 -0
- package/llms.txt +2382 -0
- package/package.json +10 -5
- 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/GamePlayerShell.js
CHANGED
|
@@ -1,23 +1,49 @@
|
|
|
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 } from "react";
|
|
3
|
+
import { Component, 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";
|
|
8
|
+
import { buildContextMenu, contextVerbInput, } from "@jgengine/core/interaction/contextMenu";
|
|
7
9
|
import { resolveActivePrompt } from "@jgengine/core/interaction/proximityPrompt";
|
|
8
|
-
import {
|
|
10
|
+
import { aimToPoint } from "@jgengine/core/input/pointer";
|
|
11
|
+
import { createSelectionSet, isMarquee, screenRect, selectWithinRect, } from "@jgengine/core/scene/selection";
|
|
12
|
+
import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, resolveMovementIntent, resolveObstacleStep, snapPositionToGrid, } from "@jgengine/core/movement/movementModel";
|
|
13
|
+
import { advanceVoxelPlayer, createVoxelPlayerBody, } from "@jgengine/core/movement/voxelController";
|
|
9
14
|
import { createGameContext } from "@jgengine/core/runtime/gameContext";
|
|
15
|
+
import { groundFieldFor } from "@jgengine/core/world/terrain";
|
|
16
|
+
import { objectVisualScale } from "@jgengine/core/scene/objectStore";
|
|
17
|
+
import { DEFAULT_PICKUP_RADIUS, WORLD_ITEM_ENTITY_NAME } from "@jgengine/core/game/worldItem";
|
|
10
18
|
import { useGameContext } from "@jgengine/react/provider";
|
|
19
|
+
import { useDisplayProfile } from "@jgengine/react/display";
|
|
11
20
|
import { useSceneEntities, useSceneObjects, usePlayer, useTarget } from "@jgengine/react/hooks";
|
|
12
21
|
import { GameProvider } from "@jgengine/react/provider";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
22
|
+
import { CAMERA_FRUSTUM_DEFAULTS } from "@jgengine/core/game/playableGame";
|
|
23
|
+
import { sky as resolveSkyDescriptor } from "@jgengine/core/world/features";
|
|
24
|
+
import { devtools } from "@jgengine/core/devtools/devtools";
|
|
25
|
+
import { AudioListener, EntityAudioEmitters, ObjectAudioEmitters } from "./audio/AudioComponents.js";
|
|
26
|
+
import { createAudioEngine } from "./audio/audioEngine.js";
|
|
27
|
+
import { DevtoolsOverlay, DevtoolsRendererProbe, withDevtoolsLatency } from "./devtools/DevtoolsOverlay.js";
|
|
28
|
+
import { GAME_SIM_FRAME_PRIORITY, GameCameraRig, resolveRigKind, rtsPanKeysConflict } from "./camera/index.js";
|
|
29
|
+
import { SkyDaylight, TimeOfDayDaylight } from "./environment.js";
|
|
30
|
+
import { EnvironmentScene } from "./environment/EnvironmentScene.js";
|
|
31
|
+
import { applyMaterialOverride } from "./materialOverride.js";
|
|
32
|
+
import { PointerProbe } from "./pointer/PointerProbe.js";
|
|
33
|
+
import { applyPaintTexture, cloneModelScene, createPaintCanvas, standardMaterialsOf, syncPaintCanvas, } from "./render/modelRender.js";
|
|
34
|
+
import { MarqueeBox, ContextMenuView } from "./pointer/PointerOverlays.js";
|
|
35
|
+
import { createPointerService, POINTER_ENTITY_KEY, POINTER_OBJECT_KEY, } from "./pointer/pointerService.js";
|
|
36
|
+
import { CombatCameraShake, ProjectileTracers, Reticle, WorldEntityBars, WorldFloatText, WorldTelegraphs, } from "./world/WorldHud.js";
|
|
37
|
+
import { GridWorldScene } from "./world/GridWorldScene.js";
|
|
38
|
+
import { WorldItems } from "./world/WorldItems.js";
|
|
39
|
+
import { TouchControlsDock, TouchPlaySurface } from "./touch/TouchControlsOverlay.js";
|
|
16
40
|
const DEV_USER_ID = "dev-player";
|
|
17
41
|
const TURN_SPEED = 2.4;
|
|
18
42
|
const PRIMARY_CLICK_MOVE_THRESHOLD_PX = 6;
|
|
19
43
|
const GROUND_SIZE = 160;
|
|
20
44
|
const GROUND_SEGMENTS = 80;
|
|
45
|
+
const DEFAULT_BACKGROUND_COLOR = "#14161b";
|
|
46
|
+
const DEFAULT_BACKDROP_FOG_COLOR = "#1a1c22";
|
|
21
47
|
function errorToDiagnostic(error, phase) {
|
|
22
48
|
if (error instanceof Error) {
|
|
23
49
|
return { phase, message: error.message, stack: error.stack };
|
|
@@ -43,6 +69,11 @@ const RESERVED_INPUT_ACTIONS = new Set([
|
|
|
43
69
|
"useAbility",
|
|
44
70
|
"interact",
|
|
45
71
|
]);
|
|
72
|
+
const SHELL_MOVEMENT_ACTIONS = ["moveForward", "moveBack", "moveLeft", "moveRight", "jump"];
|
|
73
|
+
function shellDrivesPlayerPose(input) {
|
|
74
|
+
const bound = input ?? {};
|
|
75
|
+
return SHELL_MOVEMENT_ACTIONS.some((action) => action in bound);
|
|
76
|
+
}
|
|
46
77
|
function findHotbarSlotActions(input) {
|
|
47
78
|
return Object.keys(input ?? {}).flatMap((action) => {
|
|
48
79
|
const slot = hotbarSlotActionIndex(action);
|
|
@@ -54,24 +85,125 @@ function hotbarIdFor(playable) {
|
|
|
54
85
|
const hud = declarations.find(([, declaration]) => declaration.hud === "hotbar");
|
|
55
86
|
return (hud ?? declarations[0])?.[0] ?? null;
|
|
56
87
|
}
|
|
57
|
-
function executeHotbarSlot(ctx, hotbarId, slot, yaw, pitch) {
|
|
88
|
+
function executeHotbarSlot(ctx, fromId, hotbarId, slot, yaw, pitch, aimOverride) {
|
|
58
89
|
const stack = ctx.player.inventory.state(hotbarId).slots[slot];
|
|
59
90
|
if (stack === undefined || stack === null)
|
|
60
91
|
return { ok: false, error: `Hotbar slot ${slot + 1} is empty` };
|
|
61
92
|
const result = ctx.item.use.use({
|
|
62
|
-
from:
|
|
93
|
+
from: fromId,
|
|
63
94
|
itemId: stack.itemId,
|
|
64
95
|
inventoryId: hotbarId,
|
|
65
|
-
aim: { yaw, pitch },
|
|
96
|
+
aim: aimOverride ?? { yaw, pitch },
|
|
66
97
|
});
|
|
67
98
|
return result.error === undefined ? { ok: true } : { ok: false, error: result.error };
|
|
68
99
|
}
|
|
100
|
+
function pointerAimFor(ctx, service) {
|
|
101
|
+
const hit = service.worldHit();
|
|
102
|
+
if (hit === null)
|
|
103
|
+
return undefined;
|
|
104
|
+
const player = ctx.scene.entity.get(ctx.player.userId);
|
|
105
|
+
const origin = player === null ? hit.point : player.position;
|
|
106
|
+
return aimToPoint([origin[0], origin[1] + 1, origin[2]], hit.point);
|
|
107
|
+
}
|
|
108
|
+
function pointerContextMenu(ctx, playable, hit) {
|
|
109
|
+
if (hit.entity !== null) {
|
|
110
|
+
const entity = ctx.scene.entity.get(hit.entity);
|
|
111
|
+
const verbs = entity === null ? undefined : playable.content.entityById?.(entity.name)?.verbs;
|
|
112
|
+
return buildContextMenu({ kind: "entity", targetId: hit.entity, verbs, point: hit.point });
|
|
113
|
+
}
|
|
114
|
+
if (hit.object !== null) {
|
|
115
|
+
const verbs = ctx.scene.object.catalog(hit.object)?.verbs;
|
|
116
|
+
return buildContextMenu({ kind: "object", targetId: hit.object, verbs, point: hit.point });
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
/** Actions from `input` currently held down, for `ctx.input.publish` (#164.1); includes reserved movement/jump actions. */
|
|
121
|
+
export function heldActionsFor(tracker, actions) {
|
|
122
|
+
return actions.filter((action) => tracker.isDown(action));
|
|
123
|
+
}
|
|
124
|
+
/** Whether a bound action should fire this frame: on press, or on repeat interval while held (shared by `FrameDriver` and `HudOnlyDriver`). */
|
|
125
|
+
export function shouldFireBoundAction(tracker, action, input, repeatFiredAt, now) {
|
|
126
|
+
return shouldDispatchAction({
|
|
127
|
+
pressed: tracker.wasPressed(action),
|
|
128
|
+
down: tracker.isDown(action),
|
|
129
|
+
repeatMs: actionRepeatMs(input?.[action]),
|
|
130
|
+
lastFiredAt: repeatFiredAt.get(action) ?? null,
|
|
131
|
+
now,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/** 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), RESERVED_INPUT_ACTIONS);
|
|
137
|
+
if (command !== null)
|
|
138
|
+
ctx.game.commands.run(command, { yaw, pitch, aim });
|
|
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;
|
|
161
|
+
}
|
|
162
|
+
/** The world's declared sky, when its world feature is an environment with one (#196.1). */
|
|
163
|
+
export function resolveWorldSky(world) {
|
|
164
|
+
return world?.kind === "environment" ? world.sky : undefined;
|
|
165
|
+
}
|
|
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
|
+
}
|
|
69
189
|
function colorFromId(id) {
|
|
70
190
|
let hash = 0;
|
|
71
191
|
for (let index = 0; index < id.length; index += 1)
|
|
72
192
|
hash = (hash * 31 + id.charCodeAt(index)) >>> 0;
|
|
73
193
|
return `hsl(${hash % 360}, 65%, 55%)`;
|
|
74
194
|
}
|
|
195
|
+
function ConfiguredLighting({ lighting }) {
|
|
196
|
+
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
|
+
lighting.hemisphere.skyColor ?? "#bfe3ff",
|
|
198
|
+
lighting.hemisphere.groundColor ?? "#4c6b34",
|
|
199
|
+
lighting.hemisphere.intensity ?? 0.55,
|
|
200
|
+
] })) : 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)))] }));
|
|
201
|
+
}
|
|
202
|
+
function BackdropFog({ fog }) {
|
|
203
|
+
if (fog === undefined)
|
|
204
|
+
return null;
|
|
205
|
+
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] }));
|
|
206
|
+
}
|
|
75
207
|
function EntitySprite({ sprite }) {
|
|
76
208
|
const texture = useLoader(THREE.TextureLoader, sprite.url);
|
|
77
209
|
useEffect(() => {
|
|
@@ -81,9 +213,10 @@ function EntitySprite({ sprite }) {
|
|
|
81
213
|
}, [texture]);
|
|
82
214
|
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 }) }));
|
|
83
215
|
}
|
|
84
|
-
function EntityModel({ model }) {
|
|
216
|
+
function EntityModel({ model, instanceId }) {
|
|
85
217
|
const gltf = useLoader(GLTFLoader, model.url);
|
|
86
|
-
const
|
|
218
|
+
const ctx = useGameContext();
|
|
219
|
+
const material = model.material;
|
|
87
220
|
const scale = model.scale ?? 1;
|
|
88
221
|
const baseY = model.y ?? 0;
|
|
89
222
|
const dims = model.dims;
|
|
@@ -91,6 +224,73 @@ function EntityModel({ model }) {
|
|
|
91
224
|
const position = centered
|
|
92
225
|
? [-scale * dims.center.x, baseY - scale * dims.minY, -scale * dims.center.z]
|
|
93
226
|
: [0, baseY, 0];
|
|
227
|
+
const scene = useMemo(() => {
|
|
228
|
+
const cloned = cloneModelScene(gltf.scene);
|
|
229
|
+
if (material !== undefined)
|
|
230
|
+
applyMaterialOverride(cloned, material);
|
|
231
|
+
return cloned;
|
|
232
|
+
}, [gltf, material]);
|
|
233
|
+
const animation = model.animation;
|
|
234
|
+
const mixerRef = useRef(null);
|
|
235
|
+
const animationPausedRef = useRef(false);
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
if (animation === undefined || gltf.animations.length === 0) {
|
|
238
|
+
mixerRef.current = null;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const mixer = new THREE.AnimationMixer(scene);
|
|
242
|
+
const clip = (animation.clip !== undefined ? THREE.AnimationClip.findByName(gltf.animations, animation.clip) : undefined) ??
|
|
243
|
+
gltf.animations[0];
|
|
244
|
+
const action = mixer.clipAction(clip);
|
|
245
|
+
action.setLoop(animation.loop === false ? THREE.LoopOnce : THREE.LoopRepeat, Infinity);
|
|
246
|
+
action.clampWhenFinished = animation.loop === false;
|
|
247
|
+
action.timeScale = animation.timeScale ?? 1;
|
|
248
|
+
action.enabled = true;
|
|
249
|
+
action.paused = animation.paused === true;
|
|
250
|
+
action.play();
|
|
251
|
+
if (animation.time !== undefined)
|
|
252
|
+
action.time = animation.time;
|
|
253
|
+
mixer.update(0);
|
|
254
|
+
mixerRef.current = mixer;
|
|
255
|
+
animationPausedRef.current = animation.paused === true;
|
|
256
|
+
return () => {
|
|
257
|
+
mixer.stopAllAction();
|
|
258
|
+
mixerRef.current = null;
|
|
259
|
+
};
|
|
260
|
+
}, [scene, gltf, animation?.clip, animation?.loop, animation?.timeScale, animation?.paused, animation?.time]);
|
|
261
|
+
const paintCanvasRef = useRef(null);
|
|
262
|
+
const paintDrawnCountRef = useRef(0);
|
|
263
|
+
const paintVersionRef = useRef(-1);
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
paintCanvasRef.current = null;
|
|
266
|
+
paintDrawnCountRef.current = 0;
|
|
267
|
+
paintVersionRef.current = -1;
|
|
268
|
+
}, [scene]);
|
|
269
|
+
useFrame((_state, delta) => {
|
|
270
|
+
if (mixerRef.current !== null && !animationPausedRef.current)
|
|
271
|
+
mixerRef.current.update(delta);
|
|
272
|
+
if (instanceId === undefined)
|
|
273
|
+
return;
|
|
274
|
+
const paint = ctx.scene.entity.paint;
|
|
275
|
+
const version = paint.version(instanceId);
|
|
276
|
+
if (version === paintVersionRef.current)
|
|
277
|
+
return;
|
|
278
|
+
paintVersionRef.current = version;
|
|
279
|
+
const strokes = paint.strokes(instanceId);
|
|
280
|
+
if (paintCanvasRef.current === null) {
|
|
281
|
+
if (strokes.length === 0)
|
|
282
|
+
return;
|
|
283
|
+
const materials = standardMaterialsOf(scene);
|
|
284
|
+
const seed = materials[0];
|
|
285
|
+
if (seed === undefined)
|
|
286
|
+
return;
|
|
287
|
+
const paintCanvas = createPaintCanvas(seed);
|
|
288
|
+
paintCanvasRef.current = paintCanvas;
|
|
289
|
+
applyPaintTexture(scene, paintCanvas);
|
|
290
|
+
}
|
|
291
|
+
const seedColor = standardMaterialsOf(scene)[0]?.color ?? new THREE.Color("#ffffff");
|
|
292
|
+
paintDrawnCountRef.current = syncPaintCanvas(paintCanvasRef.current, seedColor, strokes, paintDrawnCountRef.current);
|
|
293
|
+
});
|
|
94
294
|
return _jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] });
|
|
95
295
|
}
|
|
96
296
|
function resolveModel(value, assets) {
|
|
@@ -103,13 +303,19 @@ function resolveModel(value, assets) {
|
|
|
103
303
|
return undefined;
|
|
104
304
|
return ref.dims === undefined ? { url: ref.url } : { url: ref.url, dims: ref.dims };
|
|
105
305
|
}
|
|
106
|
-
function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, onSelect, }) {
|
|
306
|
+
function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, selected, onSelect, }) {
|
|
107
307
|
const color = isLocal ? "#4ade80" : entity.role === "npc" ? colorFromId(entity.name) : "#9ca3af";
|
|
108
|
-
return (_jsxs("group", { position: [entity.position[0], entity.position[1], entity.position[2]], "rotation-y": entity.rotationY, onPointerDown: (event) => {
|
|
308
|
+
return (_jsxs("group", { position: [entity.position[0], entity.position[1], entity.position[2]], "rotation-y": entity.rotationY, userData: { [POINTER_ENTITY_KEY]: entity.id }, onPointerDown: (event) => {
|
|
109
309
|
event.stopPropagation();
|
|
110
310
|
if (!isLocal)
|
|
111
311
|
onSelect(entity);
|
|
112
|
-
}, children: [custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(EntityModel, { model: model })) : sprite !== undefined ? (_jsx(EntitySprite, { sprite: sprite })) : entity.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] }));
|
|
312
|
+
}, 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: entity.id })) : sprite !== undefined ? (_jsx(EntitySprite, { sprite: sprite })) : entity.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
|
+
}
|
|
314
|
+
function ObjectMarker({ object, custom, model, style, }) {
|
|
315
|
+
const [scaleX, scaleY, scaleZ] = objectVisualScale(object.visual);
|
|
316
|
+
const color = object.visual?.color ?? style?.color ?? colorFromId(object.catalogId);
|
|
317
|
+
const opacity = object.visual?.opacity ?? style?.opacity ?? 1;
|
|
318
|
+
return (_jsx("group", { position: [object.position[0], object.position[1], object.position[2]], "rotation-y": object.rotationY, userData: { [POINTER_OBJECT_KEY]: object.instanceId }, children: custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(EntityModel, { model: model, instanceId: object.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 })] })) }));
|
|
113
319
|
}
|
|
114
320
|
function GroundPlane() {
|
|
115
321
|
const geometry = useMemo(() => {
|
|
@@ -142,43 +348,70 @@ function RockField() {
|
|
|
142
348
|
}), []);
|
|
143
349
|
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))) }));
|
|
144
350
|
}
|
|
145
|
-
function WorldView({ entitySprites, entityModels, objectModels, environment: Environment, assets, renderEntity, }) {
|
|
351
|
+
function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment: Environment, assets, renderEntity, renderObject, selectedIds, }) {
|
|
146
352
|
const ctx = useGameContext();
|
|
147
353
|
const entities = useSceneEntities();
|
|
148
354
|
const objects = useSceneObjects();
|
|
149
355
|
const player = usePlayer();
|
|
150
356
|
const targetId = useTarget(player.userId);
|
|
357
|
+
const controlledId = ctx.player.possession.active(player.userId);
|
|
151
358
|
const handleSelect = (entity) => {
|
|
152
359
|
const relation = ctx.scene.entity.canReceive(entity.id, "damage") === null ? "hostile" : "friendly";
|
|
153
|
-
ctx.scene.entity.setTarget(
|
|
360
|
+
ctx.scene.entity.setTarget(controlledId, relation === "hostile" || entity.role === "npc" ? entity.id : null);
|
|
154
361
|
};
|
|
155
|
-
return (_jsxs(_Fragment, { children: [Environment !== undefined ? (_jsx(Environment, {})) : (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx("gridHelper", { args: [160, 80, "#3a3f4a", "#2b2f38"], "position-y": 0.01 }), _jsx(RockField, {})] })), entities
|
|
362
|
+
return (_jsxs(_Fragment, { children: [Environment !== undefined ? (_jsx(Environment, {})) : (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx("gridHelper", { args: [160, 80, "#3a3f4a", "#2b2f38"], "position-y": 0.01 }), _jsx(RockField, {})] })), entities
|
|
363
|
+
.filter((entity) => entity.name !== WORLD_ITEM_ENTITY_NAME)
|
|
364
|
+
.map((entity) => (_jsx(EntityMarker, { entity: entity, custom: renderEntity?.(entity), model: resolveModel(entityModels?.[entity.name], assets), sprite: entitySprites?.[entity.name], isLocal: entity.id === controlledId, targeted: entity.id === targetId, selected: selectedIds.has(entity.id), onSelect: handleSelect }, entity.id))), objects.map((object) => {
|
|
156
365
|
const model = resolveModel(objectModels?.[object.catalogId], assets) ??
|
|
157
366
|
resolveModel(object.catalogId, assets);
|
|
158
|
-
return (_jsx(
|
|
367
|
+
return (_jsx(ObjectMarker, { object: object, custom: renderObject?.(object), model: model, style: objectStyles?.[object.catalogId] }, object.instanceId));
|
|
159
368
|
})] }));
|
|
160
369
|
}
|
|
161
370
|
function RemotePlayers({ rows }) {
|
|
162
371
|
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))) }));
|
|
163
372
|
}
|
|
164
|
-
function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, onRuntimeError, multiplayer, serverIdRef, }) {
|
|
373
|
+
function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, }) {
|
|
165
374
|
const motionRef = useRef(createPlayerMotionState());
|
|
375
|
+
const voxelBodyRef = useRef(null);
|
|
376
|
+
const solidCacheRef = useRef({ count: -1, set: new Set() });
|
|
166
377
|
const hasReportedTickError = useRef(false);
|
|
378
|
+
const repeatFiredAtRef = useRef(new Map());
|
|
167
379
|
const slotActions = useMemo(() => findHotbarSlotActions(playable.game.input), [playable]);
|
|
168
380
|
const hotbarId = useMemo(() => hotbarIdFor(playable), [playable]);
|
|
381
|
+
const collision = playable.collision;
|
|
382
|
+
const movement = playable.movement;
|
|
383
|
+
const voxelDims = useMemo(() => ({
|
|
384
|
+
halfWidth: collision?.halfWidth ?? 0.3,
|
|
385
|
+
height: collision?.height ?? 1.8,
|
|
386
|
+
stepHeight: collision?.stepHeight ?? 0.6,
|
|
387
|
+
}), [collision]);
|
|
388
|
+
const movementTuning = useMemo(() => resolvePhysicsTuning(playable.game.physics), [playable]);
|
|
389
|
+
const autoPickupRadius = useMemo(() => {
|
|
390
|
+
const cfg = playable.worldItem?.autoPickup;
|
|
391
|
+
if (cfg === undefined || cfg === false)
|
|
392
|
+
return null;
|
|
393
|
+
const fallback = playable.worldItem?.pickupRadius ?? DEFAULT_PICKUP_RADIUS;
|
|
394
|
+
return cfg === true ? fallback : cfg.radius ?? fallback;
|
|
395
|
+
}, [playable]);
|
|
396
|
+
const ground = useMemo(() => groundFieldFor(playable.game.world), [playable]);
|
|
397
|
+
const drivesPose = useMemo(() => shellDrivesPlayerPose(playable.game.input), [playable]);
|
|
398
|
+
const inputActions = useMemo(() => Object.keys(playable.game.input ?? {}), [playable]);
|
|
399
|
+
const hasTerrain = useMemo(() => hasEnvironmentTerrain(playable.game.world), [playable]);
|
|
169
400
|
useFrame((_state, rawDt) => {
|
|
401
|
+
const simStart = performance.now();
|
|
170
402
|
try {
|
|
171
403
|
const dt = Math.min(rawDt, 0.05);
|
|
172
404
|
const gameDt = ctx.time.advance(dt);
|
|
405
|
+
ctx.input.publish(heldActionsFor(tracker, inputActions));
|
|
173
406
|
if (tracker.isDown("turnLeft"))
|
|
174
407
|
yawRef.current += TURN_SPEED * dt;
|
|
175
408
|
if (tracker.isDown("turnRight"))
|
|
176
409
|
yawRef.current -= TURN_SPEED * dt;
|
|
177
|
-
const playerId = ctx.player.userId;
|
|
410
|
+
const playerId = ctx.player.possession.active(ctx.player.userId);
|
|
178
411
|
const player = ctx.scene.entity.get(playerId);
|
|
179
412
|
const forwardX = Math.sin(yawRef.current);
|
|
180
413
|
const forwardZ = Math.cos(yawRef.current);
|
|
181
|
-
if (player !== null) {
|
|
414
|
+
if (player !== null && drivesPose) {
|
|
182
415
|
const keys = createEmptyMovementKeys();
|
|
183
416
|
keys.w = tracker.isDown("moveForward");
|
|
184
417
|
keys.s = tracker.isDown("moveBack");
|
|
@@ -187,15 +420,92 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
187
420
|
keys.shift = tracker.isDown("sprint");
|
|
188
421
|
keys.space = tracker.isDown("jump");
|
|
189
422
|
const intent = resolveMovementIntent(keys, true);
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (autoPickupRadius !== null) {
|
|
503
|
+
const self = ctx.scene.entity.get(playerId);
|
|
504
|
+
if (self !== null) {
|
|
505
|
+
const nearest = ctx.scene.worldItem.nearestInRadius(self.position, autoPickupRadius);
|
|
506
|
+
if (nearest !== null)
|
|
507
|
+
ctx.scene.worldItem.pickup(nearest, ctx.player.userId);
|
|
508
|
+
}
|
|
199
509
|
}
|
|
200
510
|
playable.loop.onTick(ctx, gameDt);
|
|
201
511
|
if (tracker.wasPressed("tabTarget")) {
|
|
@@ -210,10 +520,27 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
210
520
|
else
|
|
211
521
|
ctx.scene.entity.setTarget(playerId, null);
|
|
212
522
|
}
|
|
523
|
+
if (pingCommand !== undefined && tracker.wasPressed("ping")) {
|
|
524
|
+
const hit = pointerService.worldHit();
|
|
525
|
+
if (hit !== null && ctx.game.commands.has(pingCommand)) {
|
|
526
|
+
ctx.game.commands.run(pingCommand, {
|
|
527
|
+
point: hit.point,
|
|
528
|
+
entity: hit.entity,
|
|
529
|
+
object: hit.object,
|
|
530
|
+
normal: hit.normal,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const aimOverride = pointerAim ? pointerAimFor(ctx, pointerService) : undefined;
|
|
535
|
+
const commandAim = aimOverride ?? { yaw: yawRef.current, pitch: pitchRef.current };
|
|
536
|
+
const nowMs = performance.now();
|
|
213
537
|
for (const action of Object.keys(playable.game.input ?? {})) {
|
|
214
|
-
|
|
538
|
+
const pressed = tracker.wasPressed(action);
|
|
539
|
+
if (action === "ping" && pingCommand !== undefined)
|
|
215
540
|
continue;
|
|
216
541
|
if (action === "interact") {
|
|
542
|
+
if (!pressed)
|
|
543
|
+
continue;
|
|
217
544
|
const prompts = playable.prompts?.(ctx);
|
|
218
545
|
const focus = prompts === undefined ? null : ctx.scene.entity.get(playerId);
|
|
219
546
|
if (prompts !== undefined && focus !== null) {
|
|
@@ -224,15 +551,16 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
224
551
|
}
|
|
225
552
|
continue;
|
|
226
553
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
554
|
+
if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
|
|
555
|
+
continue;
|
|
556
|
+
repeatFiredAtRef.current.set(action, nowMs);
|
|
557
|
+
dispatchBoundAction(ctx, action, yawRef.current, pitchRef.current, commandAim);
|
|
230
558
|
}
|
|
231
559
|
if (hotbarId !== null) {
|
|
232
560
|
for (const { action, slot } of slotActions) {
|
|
233
561
|
if (!tracker.wasPressed(action))
|
|
234
562
|
continue;
|
|
235
|
-
const result = executeHotbarSlot(ctx, hotbarId, slot, yawRef.current, pitchRef.current);
|
|
563
|
+
const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
|
|
236
564
|
if (!result.ok)
|
|
237
565
|
console.warn(`[jgengine:item-use] ${result.error}`);
|
|
238
566
|
}
|
|
@@ -245,7 +573,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
245
573
|
? preferred
|
|
246
574
|
: slots.findIndex((stack) => stack !== null);
|
|
247
575
|
if (slot >= 0) {
|
|
248
|
-
const result = executeHotbarSlot(ctx, hotbarId, slot, yawRef.current, pitchRef.current);
|
|
576
|
+
const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
|
|
249
577
|
if (!result.ok)
|
|
250
578
|
console.warn(`[jgengine:item-use] ${result.error}`);
|
|
251
579
|
}
|
|
@@ -272,9 +600,52 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
272
600
|
onRuntimeError(error, "tick");
|
|
273
601
|
}
|
|
274
602
|
}
|
|
603
|
+
devtools.frame.record({ frameMs: rawDt * 1000, simMs: performance.now() - simStart });
|
|
275
604
|
}, GAME_SIM_FRAME_PRIORITY);
|
|
276
605
|
return null;
|
|
277
606
|
}
|
|
607
|
+
function HudOnlyDriver({ ctx, playable, tracker, onRuntimeError, }) {
|
|
608
|
+
const hasReportedTickError = useRef(false);
|
|
609
|
+
const repeatFiredAtRef = useRef(new Map());
|
|
610
|
+
const lastFrameRef = useRef(null);
|
|
611
|
+
const inputActions = useMemo(() => Object.keys(playable.game.input ?? {}), [playable]);
|
|
612
|
+
useEffect(() => {
|
|
613
|
+
let frameId;
|
|
614
|
+
const tick = (now) => {
|
|
615
|
+
frameId = requestAnimationFrame(tick);
|
|
616
|
+
const last = lastFrameRef.current;
|
|
617
|
+
lastFrameRef.current = now;
|
|
618
|
+
if (last === null)
|
|
619
|
+
return;
|
|
620
|
+
const rawDt = (now - last) / 1000;
|
|
621
|
+
const simStart = performance.now();
|
|
622
|
+
try {
|
|
623
|
+
const dt = Math.min(rawDt, 0.05);
|
|
624
|
+
const gameDt = ctx.time.advance(dt);
|
|
625
|
+
ctx.input.publish(heldActionsFor(tracker, inputActions));
|
|
626
|
+
playable.loop.onTick(ctx, gameDt);
|
|
627
|
+
const nowMs = performance.now();
|
|
628
|
+
for (const action of inputActions) {
|
|
629
|
+
if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
|
|
630
|
+
continue;
|
|
631
|
+
repeatFiredAtRef.current.set(action, nowMs);
|
|
632
|
+
dispatchBoundAction(ctx, action, 0, 0, { yaw: 0, pitch: 0 });
|
|
633
|
+
}
|
|
634
|
+
tracker.endFrame();
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
if (!hasReportedTickError.current) {
|
|
638
|
+
hasReportedTickError.current = true;
|
|
639
|
+
onRuntimeError(error, "tick");
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
devtools.frame.record({ frameMs: rawDt * 1000, simMs: performance.now() - simStart });
|
|
643
|
+
};
|
|
644
|
+
frameId = requestAnimationFrame(tick);
|
|
645
|
+
return () => cancelAnimationFrame(frameId);
|
|
646
|
+
}, [ctx, playable, tracker, onRuntimeError, inputActions]);
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
278
649
|
class GameUiErrorBoundary extends Component {
|
|
279
650
|
state = { failed: false };
|
|
280
651
|
static getDerivedStateFromError() {
|
|
@@ -295,7 +666,10 @@ function DiagnosticOverlay({ diagnostics }) {
|
|
|
295
666
|
const latest = diagnostics[diagnostics.length - 1];
|
|
296
667
|
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] }), 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] }));
|
|
297
668
|
}
|
|
298
|
-
export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
669
|
+
export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null, }) {
|
|
670
|
+
const multiplayer = useMemo(() => (rawMultiplayer === null ? null : withDevtoolsLatency(rawMultiplayer)), [rawMultiplayer]);
|
|
671
|
+
const devtoolsEnabled = playable.devtools !== false;
|
|
672
|
+
const [devtoolsOpen, setDevtoolsOpen] = useState(false);
|
|
299
673
|
const [ctx, setCtx] = useState(null);
|
|
300
674
|
const [diagnostics, setDiagnostics] = useState([]);
|
|
301
675
|
const [remotePlayers, setRemotePlayers] = useState([]);
|
|
@@ -306,7 +680,22 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
306
680
|
const cameraDraggingRef = useRef(false);
|
|
307
681
|
const primaryClickRef = useRef(false);
|
|
308
682
|
const pointerDownRef = useRef(null);
|
|
309
|
-
const
|
|
683
|
+
const marqueeStartRef = useRef(null);
|
|
684
|
+
const pointerService = useMemo(() => createPointerService(), []);
|
|
685
|
+
const selection = useMemo(() => createSelectionSet(), [playable]);
|
|
686
|
+
const [marquee, setMarquee] = useState(null);
|
|
687
|
+
const [selectedIds, setSelectedIds] = useState(() => new Set());
|
|
688
|
+
const [contextMenu, setContextMenu] = useState(null);
|
|
689
|
+
const tracker = useMemo(() => createActionStateTracker(toActionStateBindingMap(withTouchCodes(playable.game.input))), [playable]);
|
|
690
|
+
const touchScheme = useMemo(() => deriveTouchScheme(playable.game.input, {
|
|
691
|
+
reserved: RESERVED_INPUT_ACTIONS,
|
|
692
|
+
firstPerson: resolveRigKind(playable.camera) === "first",
|
|
693
|
+
config: playable.touch,
|
|
694
|
+
}), [playable]);
|
|
695
|
+
const { coarsePointer } = useDisplayProfile();
|
|
696
|
+
const touchSink = useMemo(() => ({ onCodeDown: (code) => tracker.handleDown(code), onCodeUp: (code) => tracker.handleUp(code) }), [tracker]);
|
|
697
|
+
const audioEngine = useMemo(() => createAudioEngine({ sounds: playable.audio?.sounds, buses: playable.audio?.buses }), [playable]);
|
|
698
|
+
useEffect(() => () => audioEngine.dispose(), [audioEngine]);
|
|
310
699
|
const userId = multiplayer?.userId ?? DEV_USER_ID;
|
|
311
700
|
const reportRuntimeError = (error, phase) => {
|
|
312
701
|
const diagnostic = logRuntimeError(error, phase);
|
|
@@ -381,6 +770,33 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
381
770
|
if (remoteUnsub !== undefined)
|
|
382
771
|
cleanups.push(remoteUnsub);
|
|
383
772
|
}
|
|
773
|
+
const chatSync = multiplayer.backend.chatSyncFor?.(joined.serverId);
|
|
774
|
+
if (chatSync !== undefined) {
|
|
775
|
+
const globalChannelIds = new Set(ctx.game.chat
|
|
776
|
+
.channels()
|
|
777
|
+
.filter((channel) => channel.kind === "global")
|
|
778
|
+
.map((channel) => channel.id));
|
|
779
|
+
const seenRemoteChat = new Set();
|
|
780
|
+
cleanups.push(ctx.game.events.subscribe("chat.message", (event) => {
|
|
781
|
+
if (event.fromUserId !== multiplayer.userId)
|
|
782
|
+
return;
|
|
783
|
+
if (!globalChannelIds.has(event.channelId))
|
|
784
|
+
return;
|
|
785
|
+
void chatSync.send(event.channelId, event.body).catch(() => undefined);
|
|
786
|
+
}));
|
|
787
|
+
for (const channelId of globalChannelIds) {
|
|
788
|
+
cleanups.push(chatSync.subscribe(channelId, (messages) => {
|
|
789
|
+
for (const message of messages) {
|
|
790
|
+
if (message.fromUserId === multiplayer.userId)
|
|
791
|
+
continue;
|
|
792
|
+
if (seenRemoteChat.has(message.id))
|
|
793
|
+
continue;
|
|
794
|
+
seenRemoteChat.add(message.id);
|
|
795
|
+
ctx.game.chat.send(message.fromUserId, message.channelId, message.body);
|
|
796
|
+
}
|
|
797
|
+
}));
|
|
798
|
+
}
|
|
799
|
+
}
|
|
384
800
|
})
|
|
385
801
|
.catch(() => undefined);
|
|
386
802
|
return () => {
|
|
@@ -398,39 +814,199 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
398
814
|
useEffect(() => {
|
|
399
815
|
wrapperRef.current?.focus();
|
|
400
816
|
}, [ctx]);
|
|
817
|
+
useSyncExternalStore(ctx?.subscribe ?? (() => () => undefined), ctx?.version ?? (() => 0), ctx?.version ?? (() => 0));
|
|
401
818
|
if (ctx === null)
|
|
402
819
|
return _jsx("div", { className: "h-full w-full bg-neutral-950" });
|
|
403
820
|
const GameUI = playable.GameUI;
|
|
404
821
|
const WorldOverlay = playable.WorldOverlay;
|
|
405
|
-
const
|
|
406
|
-
const
|
|
822
|
+
const controlledEntityId = ctx.player.possession.active(userId);
|
|
823
|
+
const cameraConfig = playable.camera?.followEntityId !== undefined
|
|
824
|
+
? playable.camera
|
|
825
|
+
: { ...playable.camera, followEntityId: controlledEntityId };
|
|
826
|
+
const rigKind = resolveRigKind(cameraConfig);
|
|
827
|
+
if (rigKind === "none" || playable.presentation === "hud") {
|
|
828
|
+
const GameUI = playable.GameUI;
|
|
829
|
+
return (_jsxs("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
|
|
830
|
+
if (event.code === "F2" && devtoolsEnabled) {
|
|
831
|
+
event.preventDefault();
|
|
832
|
+
setDevtoolsOpen((current) => !current);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (event.code === "Tab" || event.code === "Space")
|
|
836
|
+
event.preventDefault();
|
|
837
|
+
tracker.handleDown(event.code);
|
|
838
|
+
}, onKeyUp: (event) => tracker.handleUp(event.code), onBlur: () => tracker.reset(), children: [_jsx(HudOnlyDriver, { ctx: ctx, playable: playable, tracker: tracker, onRuntimeError: reportRuntimeError }), _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(GameUI, {}) }) }), devtoolsEnabled ? (_jsx(DevtoolsOverlay, { open: devtoolsOpen, ctx: ctx, playable: playable, multiplayer: multiplayer })) : null, _jsx(DiagnosticOverlay, { diagnostics: diagnostics })] }));
|
|
839
|
+
}
|
|
840
|
+
const firstPerson = rigKind === "first";
|
|
841
|
+
const showReticle = (firstPerson && playable.camera?.firstPerson?.reticle !== false) || rigKind === "shoulder";
|
|
842
|
+
const rtsPanKeysEnabled = !rtsPanKeysConflict(playable.game.input);
|
|
407
843
|
const worldBars = playable.worldHealthBars;
|
|
408
844
|
const barsStatId = worldBars === undefined || worldBars === false
|
|
409
845
|
? null
|
|
410
846
|
: worldBars === true
|
|
411
847
|
? "health"
|
|
412
848
|
: worldBars.statId ?? "health";
|
|
849
|
+
const barsRoles = worldBars === undefined || worldBars === true || worldBars === false ? undefined : worldBars.roles;
|
|
850
|
+
const resolveEntityRole = (entity) => playable.content.entityById?.(entity.name)?.role;
|
|
851
|
+
const pointer = playable.pointer;
|
|
852
|
+
const pointerUsesLeft = pointer !== undefined && (pointer.select === true || pointer.moveCommand !== undefined);
|
|
853
|
+
const selectFilter = pointer?.selectFilter;
|
|
854
|
+
const worldSky = resolveWorldSky(playable.game.world);
|
|
855
|
+
const world = playable.game.world;
|
|
856
|
+
const AutoEnvironment = playable.environment ??
|
|
857
|
+
(world?.kind === "environment"
|
|
858
|
+
? () => _jsx(EnvironmentScene, { feature: world })
|
|
859
|
+
: world?.kind === "biomes" || world?.kind === "voxel" || world?.kind === "plots" || world?.kind === "tilemap"
|
|
860
|
+
? () => _jsx(GridWorldScene, { feature: world })
|
|
861
|
+
: undefined);
|
|
862
|
+
const backdrop = playable.backdrop;
|
|
863
|
+
const backdropSky = backdrop?.sky !== undefined ? resolveSkyDescriptor(backdrop.sky) : undefined;
|
|
864
|
+
const effectiveSky = backdropSky ?? worldSky;
|
|
865
|
+
const backgroundColor = backdrop?.background ?? (effectiveSky === undefined ? DEFAULT_BACKGROUND_COLOR : undefined);
|
|
866
|
+
const lighting = playable.lighting;
|
|
867
|
+
const localXY = (event) => {
|
|
868
|
+
const rect = wrapperRef.current?.getBoundingClientRect();
|
|
869
|
+
return rect === undefined
|
|
870
|
+
? { x: event.clientX, y: event.clientY }
|
|
871
|
+
: { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
|
872
|
+
};
|
|
873
|
+
const commitSelection = (ids) => {
|
|
874
|
+
selection.replace(ids);
|
|
875
|
+
setSelectedIds(new Set(ids));
|
|
876
|
+
};
|
|
877
|
+
const finishMarquee = (rect) => {
|
|
878
|
+
const candidates = [];
|
|
879
|
+
for (const entity of ctx.scene.entity.list()) {
|
|
880
|
+
if (selectFilter !== undefined && !selectFilter(entity.id))
|
|
881
|
+
continue;
|
|
882
|
+
const screen = pointerService.screenOf(entity.position);
|
|
883
|
+
if (screen !== null)
|
|
884
|
+
candidates.push({ id: entity.id, x: screen.x, y: screen.y });
|
|
885
|
+
}
|
|
886
|
+
commitSelection(selectWithinRect(candidates, rect));
|
|
887
|
+
};
|
|
888
|
+
const handlePointerDown = (event) => {
|
|
889
|
+
wrapperRef.current?.focus();
|
|
890
|
+
audioEngine.resume();
|
|
891
|
+
if (contextMenu !== null)
|
|
892
|
+
setContextMenu(null);
|
|
893
|
+
if (event.button === 0) {
|
|
894
|
+
const point = localXY(event);
|
|
895
|
+
pointerDownRef.current = point;
|
|
896
|
+
if (pointer?.select === true)
|
|
897
|
+
marqueeStartRef.current = point;
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
const handlePointerMove = (event) => {
|
|
901
|
+
if (pointer?.select !== true)
|
|
902
|
+
return;
|
|
903
|
+
const start = marqueeStartRef.current;
|
|
904
|
+
if (start === null || (event.buttons & 1) === 0)
|
|
905
|
+
return;
|
|
906
|
+
const point = localXY(event);
|
|
907
|
+
setMarquee(screenRect(start.x, start.y, point.x, point.y));
|
|
908
|
+
};
|
|
909
|
+
const handlePointerUp = (event) => {
|
|
910
|
+
if (event.button !== 0 || pointerDownRef.current === null)
|
|
911
|
+
return;
|
|
912
|
+
const start = pointerDownRef.current;
|
|
913
|
+
pointerDownRef.current = null;
|
|
914
|
+
marqueeStartRef.current = null;
|
|
915
|
+
const end = localXY(event);
|
|
916
|
+
const rect = screenRect(start.x, start.y, end.x, end.y);
|
|
917
|
+
const wasMarquee = marquee !== null && isMarquee(rect, PRIMARY_CLICK_MOVE_THRESHOLD_PX);
|
|
918
|
+
setMarquee(null);
|
|
919
|
+
if (pointer?.select === true && wasMarquee) {
|
|
920
|
+
finishMarquee(rect);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (cameraDraggingRef.current)
|
|
924
|
+
return;
|
|
925
|
+
if (pointer?.grabWorldItems === true) {
|
|
926
|
+
const hit = pointerService.worldHit();
|
|
927
|
+
const itemInstanceId = hit?.entity ?? null;
|
|
928
|
+
const record = itemInstanceId === null ? null : ctx.scene.worldItem.get(itemInstanceId);
|
|
929
|
+
if (record !== null && itemInstanceId !== null) {
|
|
930
|
+
const itemEntity = ctx.scene.entity.get(itemInstanceId);
|
|
931
|
+
const localPlayer = ctx.scene.entity.get(ctx.player.userId);
|
|
932
|
+
const pickupRadius = playable.worldItem?.pickupRadius ?? DEFAULT_PICKUP_RADIUS;
|
|
933
|
+
const withinRadius = itemEntity !== null &&
|
|
934
|
+
localPlayer !== null &&
|
|
935
|
+
Math.hypot(localPlayer.position[0] - itemEntity.position[0], localPlayer.position[1] - itemEntity.position[1], localPlayer.position[2] - itemEntity.position[2]) <= pickupRadius;
|
|
936
|
+
if (withinRadius) {
|
|
937
|
+
ctx.scene.worldItem.pickup(itemInstanceId, ctx.player.userId);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (pointer?.select === true) {
|
|
943
|
+
const hit = pointerService.worldHit();
|
|
944
|
+
if (hit !== null && hit.entity !== null && (selectFilter === undefined || selectFilter(hit.entity))) {
|
|
945
|
+
commitSelection([hit.entity]);
|
|
946
|
+
}
|
|
947
|
+
else {
|
|
948
|
+
commitSelection([]);
|
|
949
|
+
}
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (pointer?.moveCommand !== undefined) {
|
|
953
|
+
const hit = pointerService.worldHit();
|
|
954
|
+
if (hit !== null && ctx.game.commands.has(pointer.moveCommand)) {
|
|
955
|
+
ctx.game.commands.run(pointer.moveCommand, { point: hit.point, entity: hit.entity, object: hit.object });
|
|
956
|
+
}
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
const moved = (end.x - start.x) ** 2 + (end.y - start.y) ** 2;
|
|
960
|
+
if (moved <= PRIMARY_CLICK_MOVE_THRESHOLD_PX * PRIMARY_CLICK_MOVE_THRESHOLD_PX)
|
|
961
|
+
primaryClickRef.current = true;
|
|
962
|
+
};
|
|
963
|
+
const handleContextMenu = (event) => {
|
|
964
|
+
if (pointer === undefined)
|
|
965
|
+
return;
|
|
966
|
+
event.preventDefault();
|
|
967
|
+
const hit = pointerService.worldHit();
|
|
968
|
+
if (hit === null)
|
|
969
|
+
return;
|
|
970
|
+
if (pointer.contextMenu === true && (hit.entity !== null || hit.object !== null)) {
|
|
971
|
+
const menu = pointerContextMenu(ctx, playable, hit);
|
|
972
|
+
if (menu !== null) {
|
|
973
|
+
const point = localXY(event);
|
|
974
|
+
setContextMenu({ menu, x: point.x, y: point.y });
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (pointer.select === true && pointer.orderCommand !== undefined && selection.size() > 0) {
|
|
979
|
+
if (ctx.game.commands.has(pointer.orderCommand)) {
|
|
980
|
+
ctx.game.commands.run(pointer.orderCommand, { selection: selection.list(), point: hit.point });
|
|
981
|
+
}
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (pointer.secondaryCommand !== undefined && ctx.game.commands.has(pointer.secondaryCommand)) {
|
|
985
|
+
const aim = (pointer.aim === true ? pointerAimFor(ctx, pointerService) : undefined) ?? {
|
|
986
|
+
yaw: yawRef.current,
|
|
987
|
+
pitch: pitchRef.current,
|
|
988
|
+
};
|
|
989
|
+
ctx.game.commands.run(pointer.secondaryCommand, { point: hit.point, entity: hit.entity, object: hit.object, aim });
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
const handleVerbPick = (verb) => {
|
|
993
|
+
const state = contextMenu;
|
|
994
|
+
setContextMenu(null);
|
|
995
|
+
if (state !== null && ctx.game.commands.has(verb.command)) {
|
|
996
|
+
ctx.game.commands.run(verb.command, contextVerbInput(state.menu, verb));
|
|
997
|
+
}
|
|
998
|
+
};
|
|
413
999
|
return (_jsxs("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
|
|
414
|
-
if (event.code === "
|
|
1000
|
+
if (event.code === "F2" && devtoolsEnabled) {
|
|
415
1001
|
event.preventDefault();
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
wrapperRef.current?.focus();
|
|
419
|
-
if (event.button === 0) {
|
|
420
|
-
pointerDownRef.current = { x: event.clientX, y: event.clientY };
|
|
421
|
-
}
|
|
422
|
-
}, onPointerUp: (event) => {
|
|
423
|
-
if (event.button !== 0 || pointerDownRef.current === null)
|
|
1002
|
+
document.exitPointerLock?.();
|
|
1003
|
+
setDevtoolsOpen((current) => !current);
|
|
424
1004
|
return;
|
|
425
|
-
const start = pointerDownRef.current;
|
|
426
|
-
pointerDownRef.current = null;
|
|
427
|
-
const dx = event.clientX - start.x;
|
|
428
|
-
const dy = event.clientY - start.y;
|
|
429
|
-
const moved = dx * dx + dy * dy;
|
|
430
|
-
if (!cameraDraggingRef.current && moved <= PRIMARY_CLICK_MOVE_THRESHOLD_PX * PRIMARY_CLICK_MOVE_THRESHOLD_PX) {
|
|
431
|
-
primaryClickRef.current = true;
|
|
432
1005
|
}
|
|
433
|
-
|
|
1006
|
+
if (event.code === "Tab" || event.code === "Space")
|
|
1007
|
+
event.preventDefault();
|
|
1008
|
+
tracker.handleDown(event.code);
|
|
1009
|
+
}, onKeyUp: (event) => tracker.handleUp(event.code), onBlur: () => tracker.reset(), onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onContextMenu: handleContextMenu, onWheel: (event) => {
|
|
434
1010
|
if (ctx === null || !event.shiftKey)
|
|
435
1011
|
return;
|
|
436
1012
|
event.preventDefault();
|
|
@@ -440,7 +1016,13 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
440
1016
|
else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
|
|
441
1017
|
ctx.game.commands.run("ui.hotbarScrollPrev", {});
|
|
442
1018
|
}
|
|
443
|
-
}, children: [_jsxs(Canvas, { camera: {
|
|
1019
|
+
}, children: [_jsxs(Canvas, { camera: {
|
|
1020
|
+
fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
|
|
1021
|
+
near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
|
|
1022
|
+
far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
|
|
1023
|
+
}, shadows: playable.shadows ?? true, gl: { preserveDrawingBuffer: true }, style: { touchAction: "none" }, children: [backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, effectiveSky === undefined ? (lighting !== undefined ? (_jsx(ConfiguredLighting, { lighting: lighting })) : (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 })] }))) : effectiveSky.timeOfDay ? (_jsx(TimeOfDayDaylight, { sky: effectiveSky, clock: ctx.time })) : backdropSky !== undefined ? (_jsx(SkyDaylight, { sky: backdropSky })) : null, _jsx(BackdropFog, { fog: backdrop?.fog }), _jsxs(GameProvider, { context: ctx, 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, {}), _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) => {
|
|
444
1024
|
cameraDraggingRef.current = dragging;
|
|
445
|
-
} }))] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef
|
|
1025
|
+
} }), _jsx(PointerProbe, { service: pointerService })] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand }), _jsx(DevtoolsRendererProbe, {})] }), coarsePointer && 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: () => {
|
|
1026
|
+
primaryClickRef.current = true;
|
|
1027
|
+
} })) : null, _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(GameUI, {}) }) }), showReticle ? _jsx(Reticle, {}) : null, coarsePointer && touchScheme !== null && (touchScheme.joystick !== null || touchScheme.buttons.length > 0) ? (_jsx(TouchControlsDock, { scheme: touchScheme, sink: touchSink })) : 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, _jsx(DiagnosticOverlay, { diagnostics: diagnostics })] }));
|
|
446
1028
|
}
|