@jgengine/shell 0.7.0 → 0.9.0

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