@jgengine/shell 0.7.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 +47 -0
- 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 +439 -28
- package/dist/behaviour.d.ts +31 -0
- package/dist/behaviour.js +63 -0
- package/dist/camera/GameCameraRig.d.ts +8 -3
- package/dist/camera/GameCameraRig.js +28 -14
- 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/cameraRigs.d.ts +5 -1
- package/dist/camera/cameraRigs.js +43 -6
- package/dist/camera/index.d.ts +4 -2
- package/dist/camera/index.js +4 -2
- package/dist/camera/inspectionCameraMath.d.ts +25 -0
- package/dist/camera/inspectionCameraMath.js +44 -0
- package/dist/camera/rigMath.d.ts +40 -1
- package/dist/camera/rigMath.js +55 -0
- package/dist/camera/rigResolve.d.ts +15 -0
- package/dist/camera/rigResolve.js +52 -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/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/PointerProbe.js +14 -1
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +45 -24
- 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/terrain/index.d.ts +1 -1
- package/dist/terrain/index.js +1 -1
- package/dist/terrain/terrainMath.d.ts +1 -0
- package/dist/terrain/terrainMath.js +8 -2
- package/dist/touch/TouchControlsOverlay.d.ts +18 -0
- package/dist/touch/TouchControlsOverlay.js +173 -0
- package/dist/water/OceanShader.d.ts +1 -1
- package/dist/water/OceanShader.js +3 -3
- 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/WorldHud.d.ts +5 -1
- package/dist/world/WorldHud.js +6 -2
- package/dist/world/WorldItems.js +21 -2
- package/llms.txt +2382 -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
|
@@ -3,30 +3,47 @@ import { Canvas, useFrame, useLoader } from "@react-three/fiber";
|
|
|
3
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";
|
|
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";
|
|
10
11
|
import { createSelectionSet, isMarquee, screenRect, selectWithinRect, } from "@jgengine/core/scene/selection";
|
|
11
|
-
import { advancePlayerMotion, createEmptyMovementKeys, createPlayerMotionState, resolveMovementIntent, } from "@jgengine/core/movement/movementModel";
|
|
12
|
+
import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, resolveMovementIntent, resolveObstacleStep, snapPositionToGrid, } from "@jgengine/core/movement/movementModel";
|
|
13
|
+
import { advanceVoxelPlayer, createVoxelPlayerBody, } from "@jgengine/core/movement/voxelController";
|
|
12
14
|
import { createGameContext } from "@jgengine/core/runtime/gameContext";
|
|
15
|
+
import { groundFieldFor } from "@jgengine/core/world/terrain";
|
|
16
|
+
import { objectVisualScale } from "@jgengine/core/scene/objectStore";
|
|
13
17
|
import { DEFAULT_PICKUP_RADIUS, WORLD_ITEM_ENTITY_NAME } from "@jgengine/core/game/worldItem";
|
|
14
18
|
import { useGameContext } from "@jgengine/react/provider";
|
|
19
|
+
import { useDisplayProfile } from "@jgengine/react/display";
|
|
15
20
|
import { useSceneEntities, useSceneObjects, usePlayer, useTarget } from "@jgengine/react/hooks";
|
|
16
21
|
import { GameProvider } from "@jgengine/react/provider";
|
|
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";
|
|
17
25
|
import { AudioListener, EntityAudioEmitters, ObjectAudioEmitters } from "./audio/AudioComponents.js";
|
|
18
26
|
import { createAudioEngine } from "./audio/audioEngine.js";
|
|
19
|
-
import {
|
|
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";
|
|
20
32
|
import { PointerProbe } from "./pointer/PointerProbe.js";
|
|
33
|
+
import { applyPaintTexture, cloneModelScene, createPaintCanvas, standardMaterialsOf, syncPaintCanvas, } from "./render/modelRender.js";
|
|
21
34
|
import { MarqueeBox, ContextMenuView } from "./pointer/PointerOverlays.js";
|
|
22
35
|
import { createPointerService, POINTER_ENTITY_KEY, POINTER_OBJECT_KEY, } from "./pointer/pointerService.js";
|
|
23
36
|
import { CombatCameraShake, ProjectileTracers, Reticle, WorldEntityBars, WorldFloatText, WorldTelegraphs, } from "./world/WorldHud.js";
|
|
37
|
+
import { GridWorldScene } from "./world/GridWorldScene.js";
|
|
24
38
|
import { WorldItems } from "./world/WorldItems.js";
|
|
39
|
+
import { TouchControlsDock, TouchPlaySurface } from "./touch/TouchControlsOverlay.js";
|
|
25
40
|
const DEV_USER_ID = "dev-player";
|
|
26
41
|
const TURN_SPEED = 2.4;
|
|
27
42
|
const PRIMARY_CLICK_MOVE_THRESHOLD_PX = 6;
|
|
28
43
|
const GROUND_SIZE = 160;
|
|
29
44
|
const GROUND_SEGMENTS = 80;
|
|
45
|
+
const DEFAULT_BACKGROUND_COLOR = "#14161b";
|
|
46
|
+
const DEFAULT_BACKDROP_FOG_COLOR = "#1a1c22";
|
|
30
47
|
function errorToDiagnostic(error, phase) {
|
|
31
48
|
if (error instanceof Error) {
|
|
32
49
|
return { phase, message: error.message, stack: error.stack };
|
|
@@ -52,6 +69,11 @@ const RESERVED_INPUT_ACTIONS = new Set([
|
|
|
52
69
|
"useAbility",
|
|
53
70
|
"interact",
|
|
54
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
|
+
}
|
|
55
77
|
function findHotbarSlotActions(input) {
|
|
56
78
|
return Object.keys(input ?? {}).flatMap((action) => {
|
|
57
79
|
const slot = hotbarSlotActionIndex(action);
|
|
@@ -95,12 +117,93 @@ function pointerContextMenu(ctx, playable, hit) {
|
|
|
95
117
|
}
|
|
96
118
|
return null;
|
|
97
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
|
+
}
|
|
98
189
|
function colorFromId(id) {
|
|
99
190
|
let hash = 0;
|
|
100
191
|
for (let index = 0; index < id.length; index += 1)
|
|
101
192
|
hash = (hash * 31 + id.charCodeAt(index)) >>> 0;
|
|
102
193
|
return `hsl(${hash % 360}, 65%, 55%)`;
|
|
103
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
|
+
}
|
|
104
207
|
function EntitySprite({ sprite }) {
|
|
105
208
|
const texture = useLoader(THREE.TextureLoader, sprite.url);
|
|
106
209
|
useEffect(() => {
|
|
@@ -110,9 +213,10 @@ function EntitySprite({ sprite }) {
|
|
|
110
213
|
}, [texture]);
|
|
111
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 }) }));
|
|
112
215
|
}
|
|
113
|
-
function EntityModel({ model }) {
|
|
216
|
+
function EntityModel({ model, instanceId }) {
|
|
114
217
|
const gltf = useLoader(GLTFLoader, model.url);
|
|
115
|
-
const
|
|
218
|
+
const ctx = useGameContext();
|
|
219
|
+
const material = model.material;
|
|
116
220
|
const scale = model.scale ?? 1;
|
|
117
221
|
const baseY = model.y ?? 0;
|
|
118
222
|
const dims = model.dims;
|
|
@@ -120,6 +224,73 @@ function EntityModel({ model }) {
|
|
|
120
224
|
const position = centered
|
|
121
225
|
? [-scale * dims.center.x, baseY - scale * dims.minY, -scale * dims.center.z]
|
|
122
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
|
+
});
|
|
123
294
|
return _jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] });
|
|
124
295
|
}
|
|
125
296
|
function resolveModel(value, assets) {
|
|
@@ -138,7 +309,13 @@ function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, select
|
|
|
138
309
|
event.stopPropagation();
|
|
139
310
|
if (!isLocal)
|
|
140
311
|
onSelect(entity);
|
|
141
|
-
}, 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 })) : 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 })] })) }));
|
|
142
319
|
}
|
|
143
320
|
function GroundPlane() {
|
|
144
321
|
const geometry = useMemo(() => {
|
|
@@ -171,7 +348,7 @@ function RockField() {
|
|
|
171
348
|
}), []);
|
|
172
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))) }));
|
|
173
350
|
}
|
|
174
|
-
function WorldView({ entitySprites, entityModels, objectModels, environment: Environment, assets, renderEntity, selectedIds, }) {
|
|
351
|
+
function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment: Environment, assets, renderEntity, renderObject, selectedIds, }) {
|
|
175
352
|
const ctx = useGameContext();
|
|
176
353
|
const entities = useSceneEntities();
|
|
177
354
|
const objects = useSceneObjects();
|
|
@@ -187,7 +364,7 @@ function WorldView({ entitySprites, entityModels, objectModels, environment: Env
|
|
|
187
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) => {
|
|
188
365
|
const model = resolveModel(objectModels?.[object.catalogId], assets) ??
|
|
189
366
|
resolveModel(object.catalogId, assets);
|
|
190
|
-
return (_jsx(
|
|
367
|
+
return (_jsx(ObjectMarker, { object: object, custom: renderObject?.(object), model: model, style: objectStyles?.[object.catalogId] }, object.instanceId));
|
|
191
368
|
})] }));
|
|
192
369
|
}
|
|
193
370
|
function RemotePlayers({ rows }) {
|
|
@@ -195,13 +372,37 @@ function RemotePlayers({ rows }) {
|
|
|
195
372
|
}
|
|
196
373
|
function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, }) {
|
|
197
374
|
const motionRef = useRef(createPlayerMotionState());
|
|
375
|
+
const voxelBodyRef = useRef(null);
|
|
376
|
+
const solidCacheRef = useRef({ count: -1, set: new Set() });
|
|
198
377
|
const hasReportedTickError = useRef(false);
|
|
378
|
+
const repeatFiredAtRef = useRef(new Map());
|
|
199
379
|
const slotActions = useMemo(() => findHotbarSlotActions(playable.game.input), [playable]);
|
|
200
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]);
|
|
201
400
|
useFrame((_state, rawDt) => {
|
|
401
|
+
const simStart = performance.now();
|
|
202
402
|
try {
|
|
203
403
|
const dt = Math.min(rawDt, 0.05);
|
|
204
404
|
const gameDt = ctx.time.advance(dt);
|
|
405
|
+
ctx.input.publish(heldActionsFor(tracker, inputActions));
|
|
205
406
|
if (tracker.isDown("turnLeft"))
|
|
206
407
|
yawRef.current += TURN_SPEED * dt;
|
|
207
408
|
if (tracker.isDown("turnRight"))
|
|
@@ -210,7 +411,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
210
411
|
const player = ctx.scene.entity.get(playerId);
|
|
211
412
|
const forwardX = Math.sin(yawRef.current);
|
|
212
413
|
const forwardZ = Math.cos(yawRef.current);
|
|
213
|
-
if (player !== null) {
|
|
414
|
+
if (player !== null && drivesPose) {
|
|
214
415
|
const keys = createEmptyMovementKeys();
|
|
215
416
|
keys.w = tracker.isDown("moveForward");
|
|
216
417
|
keys.s = tracker.isDown("moveBack");
|
|
@@ -219,15 +420,92 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
219
420
|
keys.shift = tracker.isDown("sprint");
|
|
220
421
|
keys.space = tracker.isDown("jump");
|
|
221
422
|
const intent = resolveMovementIntent(keys, true);
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
+
}
|
|
231
509
|
}
|
|
232
510
|
playable.loop.onTick(ctx, gameDt);
|
|
233
511
|
if (tracker.wasPressed("tabTarget")) {
|
|
@@ -253,12 +531,16 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
253
531
|
});
|
|
254
532
|
}
|
|
255
533
|
}
|
|
534
|
+
const aimOverride = pointerAim ? pointerAimFor(ctx, pointerService) : undefined;
|
|
535
|
+
const commandAim = aimOverride ?? { yaw: yawRef.current, pitch: pitchRef.current };
|
|
536
|
+
const nowMs = performance.now();
|
|
256
537
|
for (const action of Object.keys(playable.game.input ?? {})) {
|
|
257
|
-
|
|
258
|
-
continue;
|
|
538
|
+
const pressed = tracker.wasPressed(action);
|
|
259
539
|
if (action === "ping" && pingCommand !== undefined)
|
|
260
540
|
continue;
|
|
261
541
|
if (action === "interact") {
|
|
542
|
+
if (!pressed)
|
|
543
|
+
continue;
|
|
262
544
|
const prompts = playable.prompts?.(ctx);
|
|
263
545
|
const focus = prompts === undefined ? null : ctx.scene.entity.get(playerId);
|
|
264
546
|
if (prompts !== undefined && focus !== null) {
|
|
@@ -269,12 +551,12 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
269
551
|
}
|
|
270
552
|
continue;
|
|
271
553
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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);
|
|
275
558
|
}
|
|
276
559
|
if (hotbarId !== null) {
|
|
277
|
-
const aimOverride = pointerAim ? pointerAimFor(ctx, pointerService) : undefined;
|
|
278
560
|
for (const { action, slot } of slotActions) {
|
|
279
561
|
if (!tracker.wasPressed(action))
|
|
280
562
|
continue;
|
|
@@ -318,9 +600,52 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
318
600
|
onRuntimeError(error, "tick");
|
|
319
601
|
}
|
|
320
602
|
}
|
|
603
|
+
devtools.frame.record({ frameMs: rawDt * 1000, simMs: performance.now() - simStart });
|
|
321
604
|
}, GAME_SIM_FRAME_PRIORITY);
|
|
322
605
|
return null;
|
|
323
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
|
+
}
|
|
324
649
|
class GameUiErrorBoundary extends Component {
|
|
325
650
|
state = { failed: false };
|
|
326
651
|
static getDerivedStateFromError() {
|
|
@@ -341,7 +666,10 @@ function DiagnosticOverlay({ diagnostics }) {
|
|
|
341
666
|
const latest = diagnostics[diagnostics.length - 1];
|
|
342
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] }));
|
|
343
668
|
}
|
|
344
|
-
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);
|
|
345
673
|
const [ctx, setCtx] = useState(null);
|
|
346
674
|
const [diagnostics, setDiagnostics] = useState([]);
|
|
347
675
|
const [remotePlayers, setRemotePlayers] = useState([]);
|
|
@@ -358,7 +686,14 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
358
686
|
const [marquee, setMarquee] = useState(null);
|
|
359
687
|
const [selectedIds, setSelectedIds] = useState(() => new Set());
|
|
360
688
|
const [contextMenu, setContextMenu] = useState(null);
|
|
361
|
-
const tracker = useMemo(() => createActionStateTracker(toActionStateBindingMap(playable.game.input
|
|
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]);
|
|
362
697
|
const audioEngine = useMemo(() => createAudioEngine({ sounds: playable.audio?.sounds, buses: playable.audio?.buses }), [playable]);
|
|
363
698
|
useEffect(() => () => audioEngine.dispose(), [audioEngine]);
|
|
364
699
|
const userId = multiplayer?.userId ?? DEV_USER_ID;
|
|
@@ -435,6 +770,33 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
435
770
|
if (remoteUnsub !== undefined)
|
|
436
771
|
cleanups.push(remoteUnsub);
|
|
437
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
|
+
}
|
|
438
800
|
})
|
|
439
801
|
.catch(() => undefined);
|
|
440
802
|
return () => {
|
|
@@ -462,17 +824,46 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
462
824
|
? playable.camera
|
|
463
825
|
: { ...playable.camera, followEntityId: controlledEntityId };
|
|
464
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
|
+
}
|
|
465
840
|
const firstPerson = rigKind === "first";
|
|
466
841
|
const showReticle = (firstPerson && playable.camera?.firstPerson?.reticle !== false) || rigKind === "shoulder";
|
|
842
|
+
const rtsPanKeysEnabled = !rtsPanKeysConflict(playable.game.input);
|
|
467
843
|
const worldBars = playable.worldHealthBars;
|
|
468
844
|
const barsStatId = worldBars === undefined || worldBars === false
|
|
469
845
|
? null
|
|
470
846
|
: worldBars === true
|
|
471
847
|
? "health"
|
|
472
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;
|
|
473
851
|
const pointer = playable.pointer;
|
|
474
852
|
const pointerUsesLeft = pointer !== undefined && (pointer.select === true || pointer.moveCommand !== undefined);
|
|
475
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;
|
|
476
867
|
const localXY = (event) => {
|
|
477
868
|
const rect = wrapperRef.current?.getBoundingClientRect();
|
|
478
869
|
return rect === undefined
|
|
@@ -588,6 +979,14 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
588
979
|
if (ctx.game.commands.has(pointer.orderCommand)) {
|
|
589
980
|
ctx.game.commands.run(pointer.orderCommand, { selection: selection.list(), point: hit.point });
|
|
590
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 });
|
|
591
990
|
}
|
|
592
991
|
};
|
|
593
992
|
const handleVerbPick = (verb) => {
|
|
@@ -598,6 +997,12 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
598
997
|
}
|
|
599
998
|
};
|
|
600
999
|
return (_jsxs("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
|
|
1000
|
+
if (event.code === "F2" && devtoolsEnabled) {
|
|
1001
|
+
event.preventDefault();
|
|
1002
|
+
document.exitPointerLock?.();
|
|
1003
|
+
setDevtoolsOpen((current) => !current);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
601
1006
|
if (event.code === "Tab" || event.code === "Space")
|
|
602
1007
|
event.preventDefault();
|
|
603
1008
|
tracker.handleDown(event.code);
|
|
@@ -611,7 +1016,13 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
611
1016
|
else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
|
|
612
1017
|
ctx.game.commands.run("ui.hotbarScrollPrev", {});
|
|
613
1018
|
}
|
|
614
|
-
}, 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) => {
|
|
615
1024
|
cameraDraggingRef.current = dragging;
|
|
616
|
-
} }), _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 })
|
|
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 })] }));
|
|
617
1028
|
}
|