@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +26 -1
  3. package/dist/GameHost.d.ts +12 -0
  4. package/dist/GameHost.js +33 -0
  5. package/dist/GamePlayer.d.ts +11 -0
  6. package/dist/GamePlayer.js +24 -0
  7. package/dist/GamePlayerShell.d.ts +33 -1
  8. package/dist/GamePlayerShell.js +439 -28
  9. package/dist/behaviour.d.ts +31 -0
  10. package/dist/behaviour.js +63 -0
  11. package/dist/camera/GameCameraRig.d.ts +8 -3
  12. package/dist/camera/GameCameraRig.js +28 -14
  13. package/dist/camera/GameFirstPersonCamera.js +2 -0
  14. package/dist/camera/GameInspectionCamera.d.ts +6 -0
  15. package/dist/camera/GameInspectionCamera.js +24 -0
  16. package/dist/camera/cameraRigs.d.ts +5 -1
  17. package/dist/camera/cameraRigs.js +43 -6
  18. package/dist/camera/index.d.ts +4 -2
  19. package/dist/camera/index.js +4 -2
  20. package/dist/camera/inspectionCameraMath.d.ts +25 -0
  21. package/dist/camera/inspectionCameraMath.js +44 -0
  22. package/dist/camera/rigMath.d.ts +40 -1
  23. package/dist/camera/rigMath.js +55 -0
  24. package/dist/camera/rigResolve.d.ts +15 -0
  25. package/dist/camera/rigResolve.js +52 -0
  26. package/dist/defineGame.d.ts +16 -0
  27. package/dist/defineGame.js +51 -0
  28. package/dist/devtools/DevtoolsOverlay.d.ts +14 -0
  29. package/dist/devtools/DevtoolsOverlay.js +334 -0
  30. package/dist/environment/Daylight.d.ts +50 -0
  31. package/dist/environment/Daylight.js +100 -0
  32. package/dist/environment/EnvironmentScene.js +18 -26
  33. package/dist/environment/GroundPad.d.ts +7 -0
  34. package/dist/environment/GroundPad.js +12 -0
  35. package/dist/environment/daylightCycle.d.ts +26 -0
  36. package/dist/environment/daylightCycle.js +116 -0
  37. package/dist/environment/groundPadMath.d.ts +13 -0
  38. package/dist/environment/groundPadMath.js +10 -0
  39. package/dist/environment/index.d.ts +2 -0
  40. package/dist/environment/index.js +2 -0
  41. package/dist/materialOverride.d.ts +8 -0
  42. package/dist/materialOverride.js +32 -0
  43. package/dist/multiplayer.d.ts +16 -9
  44. package/dist/multiplayer.js +62 -12
  45. package/dist/pointer/PointerProbe.js +14 -1
  46. package/dist/pointer/pointerService.d.ts +2 -0
  47. package/dist/pointer/pointerService.js +45 -24
  48. package/dist/registry.d.ts +4 -1
  49. package/dist/registry.js +3 -1
  50. package/dist/render/modelRender.d.ts +14 -0
  51. package/dist/render/modelRender.js +79 -0
  52. package/dist/terrain/index.d.ts +1 -1
  53. package/dist/terrain/index.js +1 -1
  54. package/dist/terrain/terrainMath.d.ts +1 -0
  55. package/dist/terrain/terrainMath.js +8 -2
  56. package/dist/touch/TouchControlsOverlay.d.ts +18 -0
  57. package/dist/touch/TouchControlsOverlay.js +173 -0
  58. package/dist/water/OceanShader.d.ts +1 -1
  59. package/dist/water/OceanShader.js +3 -3
  60. package/dist/world/DataObjects.d.ts +44 -0
  61. package/dist/world/DataObjects.js +75 -0
  62. package/dist/world/GridWorldScene.d.ts +10 -0
  63. package/dist/world/GridWorldScene.js +54 -0
  64. package/dist/world/WorldHud.d.ts +5 -1
  65. package/dist/world/WorldHud.js +6 -2
  66. package/dist/world/WorldItems.js +21 -2
  67. package/llms.txt +2382 -0
  68. package/package.json +6 -5
  69. package/dist/demo/builderDemo.d.ts +0 -2
  70. package/dist/demo/builderDemo.js +0 -189
  71. package/dist/demo/demoGame.d.ts +0 -2
  72. package/dist/demo/demoGame.js +0 -208
  73. package/dist/demo/environmentShowcase.d.ts +0 -2
  74. package/dist/demo/environmentShowcase.js +0 -15
  75. package/dist/demo/mapDemo.d.ts +0 -2
  76. package/dist/demo/mapDemo.js +0 -206
  77. package/dist/demo/pointerDemo.d.ts +0 -2
  78. package/dist/demo/pointerDemo.js +0 -151
  79. package/dist/demo/sensorShowcase.d.ts +0 -2
  80. package/dist/demo/sensorShowcase.js +0 -131
  81. package/dist/demo/survivalDemo.d.ts +0 -2
  82. package/dist/demo/survivalDemo.js +0 -291
@@ -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 { GAME_SIM_FRAME_PRIORITY, GameCameraRig, resolveRigKind } from "./camera/index.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";
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 scene = useMemo(() => gltf.scene.clone(true), [gltf]);
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("group", { position: [object.position[0], object.position[1], object.position[2]], "rotation-y": object.rotationY, userData: { [POINTER_OBJECT_KEY]: object.instanceId }, children: model !== undefined ? (_jsx(EntityModel, { model: model })) : (_jsxs("mesh", { "position-y": 0.5, children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshStandardMaterial", { color: colorFromId(object.catalogId) })] })) }, object.instanceId));
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 motion = motionRef.current;
223
- const step = advancePlayerMotion(motion, intent, forwardX, forwardZ, player.movement.walkSpeed ?? 2, rawDt);
224
- ctx.scene.entity.setPose(playerId, {
225
- position: [player.position[0] + step.stepX, motion.jumpOffset, player.position[2] + step.stepZ],
226
- rotationY: intent.moving
227
- ? Math.atan2(motion.horizontalVelocityX, motion.horizontalVelocityZ)
228
- : player.rotationY,
229
- dt: rawDt,
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
- if (!tracker.wasPressed(action))
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
- const command = resolveActionCommand(action, (name) => ctx.game.commands.has(name), RESERVED_INPUT_ACTIONS);
273
- if (command !== null)
274
- ctx.game.commands.run(command, {});
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 ?? {})), [playable]);
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: { fov: 55, near: 0.1, far: 300 }, children: [_jsx("color", { attach: "background", args: ["#14161b"] }), _jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 }), _jsxs(GameProvider, { context: ctx, children: [_jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, environment: playable.environment, assets: playable.game.assets, renderEntity: playable.renderEntity, selectedIds: selectedIds }), WorldOverlay !== undefined ? _jsx(WorldOverlay, {}) : null, barsStatId !== null ? _jsx(WorldEntityBars, { statId: barsStatId }) : 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, onDragChange: (dragging) => {
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 })] }), _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(GameUI, {}) }) }), showReticle ? _jsx(Reticle, {}) : 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, _jsx(DiagnosticOverlay, { diagnostics: diagnostics })] }));
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
  }