@jgengine/shell 0.9.0 → 0.10.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 (49) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/dist/GamePlayerShell.d.ts +6 -21
  3. package/dist/GamePlayerShell.js +419 -234
  4. package/dist/audio/audioEngine.d.ts +8 -0
  5. package/dist/audio/audioEngine.js +33 -0
  6. package/dist/audio/musicDirector.d.ts +31 -0
  7. package/dist/audio/musicDirector.js +125 -0
  8. package/dist/audio/musicVoices.d.ts +10 -0
  9. package/dist/audio/musicVoices.js +338 -0
  10. package/dist/audio/synthEngine.d.ts +9 -0
  11. package/dist/audio/synthEngine.js +57 -0
  12. package/dist/camera/GameFirstPersonCamera.d.ts +3 -0
  13. package/dist/camera/GameFirstPersonCamera.js +21 -4
  14. package/dist/camera/GameInspectionCamera.js +12 -1
  15. package/dist/camera/GameOrbitCamera.js +35 -1
  16. package/dist/camera/orbitCameraMath.d.ts +18 -0
  17. package/dist/camera/orbitCameraMath.js +6 -1
  18. package/dist/cartridge.js +1 -0
  19. package/dist/commandSink.d.ts +20 -0
  20. package/dist/commandSink.js +27 -0
  21. package/dist/defineGame.js +3 -1
  22. package/dist/devtools/CollisionDebugWorld.js +1 -0
  23. package/dist/devtools/collisionDebugMath.d.ts +1 -0
  24. package/dist/devtools/collisionDebugMath.js +2 -1
  25. package/dist/environment/Daylight.d.ts +7 -1
  26. package/dist/environment/Daylight.js +52 -5
  27. package/dist/environment/EnvironmentScene.js +14 -5
  28. package/dist/environment/RoadRibbons.d.ts +7 -0
  29. package/dist/environment/RoadRibbons.js +57 -0
  30. package/dist/inputSink.d.ts +18 -0
  31. package/dist/inputSink.js +35 -0
  32. package/dist/multiplayer.js +7 -3
  33. package/dist/postfx/PostProcessing.d.ts +10 -0
  34. package/dist/postfx/PostProcessing.js +82 -0
  35. package/dist/postfx/gradeShader.d.ts +4 -0
  36. package/dist/postfx/gradeShader.js +64 -0
  37. package/dist/terrain/CarvedTerrain.d.ts +4 -1
  38. package/dist/terrain/CarvedTerrain.js +4 -3
  39. package/dist/terrain/terrainDetailMaterial.d.ts +14 -0
  40. package/dist/terrain/terrainDetailMaterial.js +90 -0
  41. package/dist/touch/TouchControlsOverlay.js +11 -1
  42. package/dist/world/WorldHud.d.ts +3 -1
  43. package/dist/world/WorldHud.js +7 -7
  44. package/dist/world/worldBarSamples.d.ts +1 -1
  45. package/dist/world/worldBarSamples.js +7 -1
  46. package/dist/worldSync.d.ts +10 -0
  47. package/dist/worldSync.js +19 -0
  48. package/llms.txt +140 -22
  49. package/package.json +4 -4
@@ -1,37 +1,45 @@
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, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
3
+ import { Component, Suspense, 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 { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
6
7
  import { actionRepeatMs, createActionStateTracker, hotbarSlotActionIndex, resolveActionCommand, shouldDispatchAction, toActionStateBindingMap, } from "@jgengine/core/input/actionBindings";
7
8
  import { deriveTouchScheme, withTouchCodes } from "@jgengine/core/input/touchScheme";
8
9
  import { buildContextMenu, contextVerbInput, } from "@jgengine/core/interaction/contextMenu";
9
10
  import { resolveActivePrompt } from "@jgengine/core/interaction/proximityPrompt";
10
11
  import { aimToPoint } from "@jgengine/core/input/pointer";
12
+ import { eyeHeightFromColliders } from "@jgengine/core/combat/shotOrigin";
11
13
  import { normalizePointerToAxis } from "@jgengine/core/input/pointerAxis";
12
14
  import { createSelectionSet, isMarquee, screenRect, selectWithinRect, } from "@jgengine/core/scene/selection";
13
- import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, resolveMovementIntent, resolveObstacleStep, snapPositionToGrid, } from "@jgengine/core/movement/movementModel";
14
15
  import { steerYaw } from "@jgengine/core/movement/steering";
15
- import { advanceVoxelPlayer, createVoxelPlayerBody, } from "@jgengine/core/movement/voxelController";
16
+ import { stepPlayerMovement, resolvePlayerMovementTuning } from "@jgengine/core/movement/playerMovement";
16
17
  import { createGameContext } from "@jgengine/core/runtime/gameContext";
17
- import { applyHorizontalImpulses } from "@jgengine/core/runtime/motionIntents";
18
- import { groundFieldFor } from "@jgengine/core/world/terrain";
18
+ import { isServerAuthoritative } from "@jgengine/core/runtime/adapter";
19
+ import { attachWorldSync } from "./worldSync.js";
20
+ import { localCommandSink, resolveCommandSink } from "./commandSink.js";
21
+ import { inputFramesEqual, resolveInputSink } from "./inputSink.js";
19
22
  import { objectVisualScale } from "@jgengine/core/scene/objectStore";
20
23
  import { DEFAULT_PICKUP_RADIUS, WORLD_ITEM_ENTITY_NAME } from "@jgengine/core/game/worldItem";
21
24
  import { useGameContext } from "@jgengine/react/provider";
22
25
  import { useDisplayProfile } from "@jgengine/react/display";
23
26
  import { HudViewportProvider } from "@jgengine/react/hudViewport";
27
+ import { GameViewportProvider } from "@jgengine/react/gameViewport";
28
+ import { RotateDeviceScreen } from "@jgengine/react/rotateDevice";
24
29
  import { useSceneEntities, useSceneObjects, usePlayer, useTarget } from "@jgengine/react/hooks";
25
30
  import { GameProvider } from "@jgengine/react/provider";
26
31
  import { CAMERA_FRUSTUM_DEFAULTS } from "@jgengine/core/game/playableGame";
27
32
  import { createSettingsStore } from "@jgengine/core/settings/settingsModel";
28
33
  import { applyBindingOverrides, clearBindingOverride, loadBindingOverrides, saveBindingOverride, } from "@jgengine/core/input/bindingOverrides";
29
34
  import { playControlsActive } from "@jgengine/core/game/controlGate";
35
+ import { orientationGateActive, orientationHintActive, resolveOrientationRequirement, } from "@jgengine/core/ui/orientation";
36
+ import { resolveOneShotClip } from "@jgengine/core/game/modelAnimation";
30
37
  import { sky as resolveSkyDescriptor } from "@jgengine/core/world/features";
31
38
  import { devtools } from "@jgengine/core/devtools/devtools";
32
39
  import { VERSION } from "@jgengine/core/meta/changelog";
33
40
  import { AudioListener, EntityAudioEmitters, ObjectAudioEmitters } from "./audio/AudioComponents.js";
34
41
  import { createAudioEngine } from "./audio/audioEngine.js";
42
+ import { PostProcessing } from "./postfx/PostProcessing.js";
35
43
  import { CollisionDebugWorld } from "./devtools/CollisionDebugWorld.js";
36
44
  import { collisionDebug } from "./devtools/collisionDebug.js";
37
45
  import { DevtoolsOverlay, DevtoolsRendererProbe, withDevtoolsLatency } from "./devtools/DevtoolsOverlay.js";
@@ -90,6 +98,10 @@ const RESERVED_INPUT_ACTIONS = new Set([
90
98
  "useAbility",
91
99
  "interact",
92
100
  ]);
101
+ /** No action names are reserved when no camera rig is active (hud/none presentation): games may bind `turnLeft`/`interact`/etc. as their own. */
102
+ const EMPTY_RESERVED = new Set();
103
+ /** Empty action list — published while the orientation gate is up to suppress all held input without touching the tracker. */
104
+ const NO_ACTIONS = [];
93
105
  const SHELL_MOVEMENT_ACTIONS = ["moveForward", "moveBack", "moveLeft", "moveRight", "jump"];
94
106
  function shellDrivesPlayerPose(input) {
95
107
  const bound = input ?? {};
@@ -122,9 +134,12 @@ function pointerAimFor(ctx, service) {
122
134
  const hit = service.worldHit();
123
135
  if (hit === null)
124
136
  return undefined;
125
- const player = ctx.scene.entity.get(ctx.player.userId);
126
- const origin = player === null ? hit.point : player.position;
127
- return aimToPoint([origin[0], origin[1] + 1, origin[2]], hit.point);
137
+ const shooter = ctx.scene.entity.get(ctx.player.possession.active(ctx.player.userId)) ??
138
+ ctx.scene.entity.get(ctx.player.userId);
139
+ if (shooter === null)
140
+ return undefined;
141
+ const eye = eyeHeightFromColliders(ctx.scene.entity.collidersOf(shooter.id));
142
+ return aimToPoint([shooter.position[0], shooter.position[1] + eye, shooter.position[2]], hit.point);
128
143
  }
129
144
  function pointerContextMenu(ctx, playable, hit) {
130
145
  if (hit.entity !== null) {
@@ -153,74 +168,35 @@ export function shouldFireBoundAction(tracker, action, input, repeatFiredAt, now
153
168
  });
154
169
  }
155
170
  /** 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);
171
+ export function dispatchBoundAction(ctx, action, yaw, pitch, aim, reserved = RESERVED_INPUT_ACTIONS, sink = localCommandSink(ctx)) {
172
+ const command = resolveActionCommand(action, (name) => ctx.game.commands.has(name), reserved);
158
173
  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;
174
+ sink.run(command, { yaw, pitch, aim });
182
175
  }
176
+ export { applyMotionImpulses } from "@jgengine/core/runtime/motionIntents";
177
+ export { nearbyObstacles } from "@jgengine/core/movement/movementModel";
178
+ export { resolvePhysicsTuning } from "@jgengine/core/movement/playerMovement";
179
+ export { hasEnvironmentTerrain } from "@jgengine/core/world/terrain";
183
180
  /** The world's declared sky, when its world feature is an environment with one (#196.1). */
184
181
  export function resolveWorldSky(world) {
185
182
  return world?.kind === "environment" ? world.sky : undefined;
186
183
  }
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
- }
212
184
  function colorFromId(id) {
213
185
  let hash = 0;
214
186
  for (let index = 0; index < id.length; index += 1)
215
187
  hash = (hash * 31 + id.charCodeAt(index)) >>> 0;
216
188
  return `hsl(${hash % 360}, 65%, 55%)`;
217
189
  }
190
+ function DirectionalShadowLight({ entry }) {
191
+ const size = entry.shadowCameraSize ?? 40;
192
+ return (_jsx("directionalLight", { position: [entry.position[0], entry.position[1], entry.position[2]], intensity: entry.intensity ?? 1.3, color: entry.color, castShadow: entry.castShadow ?? false, "shadow-mapSize-width": entry.shadowMapSize ?? 1024, "shadow-mapSize-height": entry.shadowMapSize ?? 1024, "shadow-camera-left": -size, "shadow-camera-right": size, "shadow-camera-top": size, "shadow-camera-bottom": -size, "shadow-camera-near": 0.5, "shadow-camera-far": Math.max(200, size * 6), "shadow-bias": entry.shadowBias ?? -0.0004, "shadow-normalBias": entry.shadowNormalBias ?? 0.02 }));
193
+ }
218
194
  function ConfiguredLighting({ lighting }) {
219
195
  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
196
  lighting.hemisphere.skyColor ?? "#bfe3ff",
221
197
  lighting.hemisphere.groundColor ?? "#4c6b34",
222
198
  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)))] }));
199
+ ] })) : null, (lighting.directional ?? []).map((entry, index) => (_jsx(DirectionalShadowLight, { entry: entry }, index)))] }));
224
200
  }
225
201
  function BackdropFog({ fog }) {
226
202
  if (fog === undefined)
@@ -236,35 +212,193 @@ function EntitySprite({ sprite }) {
236
212
  }, [texture]);
237
213
  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 }) }));
238
214
  }
215
+ class ModelFallbackBoundary extends Component {
216
+ state = { failed: false };
217
+ static getDerivedStateFromError() {
218
+ return { failed: true };
219
+ }
220
+ render() {
221
+ return this.state.failed ? this.props.fallback : this.props.children;
222
+ }
223
+ }
224
+ function IsolatedEntityModel({ model, instanceId, fallback, }) {
225
+ return (_jsx(ModelFallbackBoundary, { fallback: fallback ?? null, children: _jsx(Suspense, { fallback: null, children: _jsx(EntityModel, { model: model, instanceId: instanceId }) }) }));
226
+ }
227
+ function BoneAttachment({ rig, model, slot, position, rotation, scale, }) {
228
+ const gltf = useLoader(GLTFLoader, model.url, (loader) => {
229
+ loader.setMeshoptDecoder(MeshoptDecoder);
230
+ });
231
+ const weaponScene = useMemo(() => cloneModelScene(gltf.scene), [gltf]);
232
+ const px = position?.[0] ?? 0;
233
+ const py = position?.[1] ?? 0;
234
+ const pz = position?.[2] ?? 0;
235
+ const rx = rotation?.[0] ?? 0;
236
+ const ry = rotation?.[1] ?? 0;
237
+ const rz = rotation?.[2] ?? 0;
238
+ const s = scale ?? 1;
239
+ useEffect(() => {
240
+ const bone = rig.getObjectByName(slot);
241
+ if (bone === undefined) {
242
+ if (typeof console !== "undefined") {
243
+ console.warn(`[jgengine] entityModels attachment: bone/slot "${slot}" not found on the rig`);
244
+ }
245
+ return;
246
+ }
247
+ weaponScene.position.set(px, py, pz);
248
+ weaponScene.rotation.set(rx, ry, rz);
249
+ weaponScene.scale.setScalar(s);
250
+ bone.add(weaponScene);
251
+ return () => {
252
+ bone.remove(weaponScene);
253
+ };
254
+ }, [rig, weaponScene, slot, px, py, pz, rx, ry, rz, s]);
255
+ useEffect(() => () => disposeClonedMaterials(weaponScene), [weaponScene]);
256
+ return null;
257
+ }
258
+ /** Resolves an entity model plus any bone attachments' models through the asset catalog, so `EntityModel` receives fully-resolved `ModelConfig`s. */
259
+ function resolveEntityModel(value, assets, key) {
260
+ const model = resolveModel(value, assets, { seam: "entityModels", key });
261
+ if (model?.attachments === undefined)
262
+ return model;
263
+ return {
264
+ ...model,
265
+ attachments: model.attachments.map((attachment) => ({
266
+ ...attachment,
267
+ model: resolveModel(attachment.model, assets) ?? attachment.model,
268
+ })),
269
+ };
270
+ }
239
271
  function EntityModel({ model, instanceId }) {
240
- const gltf = useLoader(GLTFLoader, model.url);
272
+ const gltf = useLoader(GLTFLoader, model.url, (loader) => {
273
+ loader.setMeshoptDecoder(MeshoptDecoder);
274
+ });
241
275
  const ctx = useGameContext();
242
276
  const material = model.material;
243
- const scale = model.scale ?? 1;
244
277
  const baseY = model.y ?? 0;
245
278
  const dims = model.dims;
246
- const centered = (model.anchor ?? "center") === "center" && dims !== undefined;
247
- const position = centered
248
- ? [-scale * dims.center.x, baseY - scale * dims.minY, -scale * dims.center.z]
249
- : [0, baseY, 0];
250
279
  const scene = useMemo(() => {
251
280
  const cloned = cloneModelScene(gltf.scene);
252
281
  if (material !== undefined)
253
282
  applyMaterialOverride(cloned, material, { clone: false });
254
283
  return cloned;
255
284
  }, [gltf, material]);
285
+ const measured = useMemo(() => {
286
+ if (model.targetHeight === undefined)
287
+ return null;
288
+ const box = new THREE.Box3().setFromObject(scene);
289
+ const height = box.max.y - box.min.y;
290
+ if (!Number.isFinite(height) || height <= 0)
291
+ return null;
292
+ return {
293
+ normalize: model.targetHeight / height,
294
+ minY: box.min.y,
295
+ centerX: (box.min.x + box.max.x) / 2,
296
+ centerZ: (box.min.z + box.max.z) / 2,
297
+ };
298
+ }, [scene, model.targetHeight]);
299
+ const scale = (model.scale ?? 1) * (measured?.normalize ?? 1);
300
+ const centered = (model.anchor ?? "center") === "center" && dims !== undefined;
301
+ const position = measured !== null
302
+ ? [-scale * measured.centerX, baseY - scale * measured.minY, -scale * measured.centerZ]
303
+ : centered
304
+ ? [-scale * dims.center.x, baseY - scale * dims.minY, -scale * dims.center.z]
305
+ : [0, baseY, 0];
256
306
  useEffect(() => () => {
257
307
  disposeClonedMaterials(scene);
258
308
  }, [scene]);
259
309
  const animation = model.animation;
260
310
  const mixerRef = useRef(null);
261
311
  const animationPausedRef = useRef(false);
312
+ const stateActionsRef = useRef(null);
313
+ const states = animation?.states;
314
+ const oneShots = animation?.oneShots;
315
+ const oneShotPlayRef = useRef(null);
316
+ const activeOneShotRef = useRef(null);
262
317
  useEffect(() => {
263
318
  if (animation === undefined || gltf.animations.length === 0) {
264
319
  mixerRef.current = null;
320
+ stateActionsRef.current = null;
265
321
  return;
266
322
  }
267
323
  const mixer = new THREE.AnimationMixer(scene);
324
+ if (states !== undefined) {
325
+ const clipFor = (name) => THREE.AnimationClip.findByName(gltf.animations, name) ?? gltf.animations[0];
326
+ const actions = {
327
+ idle: mixer.clipAction(clipFor(states.idle)),
328
+ walk: mixer.clipAction(clipFor(states.walk)),
329
+ ...(states.run === undefined ? {} : { run: mixer.clipAction(clipFor(states.run)) }),
330
+ };
331
+ for (const action of Object.values(actions)) {
332
+ action.setLoop(THREE.LoopRepeat, Infinity);
333
+ action.timeScale = animation.timeScale ?? 1;
334
+ action.enabled = true;
335
+ }
336
+ actions.idle.play();
337
+ mixer.update(0);
338
+ mixerRef.current = mixer;
339
+ stateActionsRef.current = { actions, active: "idle", lastPos: null, smoothedSpeed: 0 };
340
+ animationPausedRef.current = false;
341
+ let onOneShotFinished = null;
342
+ if (oneShots !== undefined) {
343
+ const clipNames = new Set();
344
+ for (const spec of Object.values(oneShots)) {
345
+ if (typeof spec === "string")
346
+ clipNames.add(spec);
347
+ else
348
+ for (const name of spec)
349
+ clipNames.add(name);
350
+ }
351
+ const oneShotActions = new Map();
352
+ for (const name of clipNames) {
353
+ const found = THREE.AnimationClip.findByName(gltf.animations, name);
354
+ if (found === null)
355
+ continue;
356
+ const oneShotAction = mixer.clipAction(found);
357
+ oneShotAction.setLoop(THREE.LoopOnce, 1);
358
+ oneShotAction.enabled = true;
359
+ oneShotActions.set(name, oneShotAction);
360
+ }
361
+ onOneShotFinished = ({ action }) => {
362
+ const active = activeOneShotRef.current;
363
+ if (active === null || action !== active.action || active.isDeath)
364
+ return;
365
+ const machine = stateActionsRef.current;
366
+ const back = machine?.actions[machine.active];
367
+ action.fadeOut(0.15);
368
+ if (back !== undefined)
369
+ back.reset().fadeIn(0.15).play();
370
+ activeOneShotRef.current = null;
371
+ };
372
+ mixer.addEventListener("finished", onOneShotFinished);
373
+ oneShotPlayRef.current = (event) => {
374
+ const active = activeOneShotRef.current;
375
+ if (active !== null && active.isDeath)
376
+ return;
377
+ const clipName = resolveOneShotClip(oneShots, event, Math.random());
378
+ if (clipName === null)
379
+ return;
380
+ const oneShotAction = oneShotActions.get(clipName);
381
+ if (oneShotAction === undefined)
382
+ return;
383
+ const machine = stateActionsRef.current;
384
+ machine?.actions[machine.active]?.fadeOut(0.1);
385
+ if (active !== null && active.action !== oneShotAction)
386
+ active.action.stop();
387
+ oneShotAction.clampWhenFinished = event === "death";
388
+ oneShotAction.reset().fadeIn(0.1).play();
389
+ activeOneShotRef.current = { action: oneShotAction, isDeath: event === "death" };
390
+ };
391
+ }
392
+ return () => {
393
+ if (onOneShotFinished !== null)
394
+ mixer.removeEventListener("finished", onOneShotFinished);
395
+ oneShotPlayRef.current = null;
396
+ activeOneShotRef.current = null;
397
+ mixer.stopAllAction();
398
+ mixerRef.current = null;
399
+ stateActionsRef.current = null;
400
+ };
401
+ }
268
402
  const clip = (animation.clip !== undefined ? THREE.AnimationClip.findByName(gltf.animations, animation.clip) : undefined) ??
269
403
  gltf.animations[0];
270
404
  const action = mixer.clipAction(clip);
@@ -283,7 +417,39 @@ function EntityModel({ model, instanceId }) {
283
417
  mixer.stopAllAction();
284
418
  mixerRef.current = null;
285
419
  };
286
- }, [scene, gltf, animation?.clip, animation?.loop, animation?.timeScale, animation?.paused, animation?.time]);
420
+ }, [
421
+ scene,
422
+ gltf,
423
+ animation?.clip,
424
+ animation?.loop,
425
+ animation?.timeScale,
426
+ animation?.paused,
427
+ animation?.time,
428
+ states,
429
+ oneShots,
430
+ ]);
431
+ useEffect(() => {
432
+ if (instanceId === undefined || oneShots === undefined)
433
+ return;
434
+ const fire = (event) => oneShotPlayRef.current?.(event);
435
+ const offAnimation = ctx.game.events.on("entity.animation", (event) => {
436
+ if (event.instanceId === instanceId)
437
+ fire(event.event);
438
+ });
439
+ const offHit = ctx.game.events.on("combat.hitReaction", (event) => {
440
+ if (event.instanceId === instanceId)
441
+ fire("hit");
442
+ });
443
+ const offDied = ctx.game.events.on("entity.died", (event) => {
444
+ if (event.instanceId === instanceId)
445
+ fire("death");
446
+ });
447
+ return () => {
448
+ offAnimation();
449
+ offHit();
450
+ offDied();
451
+ };
452
+ }, [ctx, instanceId, oneShots]);
287
453
  const paintCanvasRef = useRef(null);
288
454
  const paintDrawnCountRef = useRef(0);
289
455
  const paintVersionRef = useRef(-1);
@@ -295,6 +461,38 @@ function EntityModel({ model, instanceId }) {
295
461
  materialCacheRef.current = null;
296
462
  }, [scene]);
297
463
  useFrame((_state, delta) => {
464
+ const stateMachine = stateActionsRef.current;
465
+ if (stateMachine !== null && states !== undefined && instanceId !== undefined && delta > 0) {
466
+ const entity = ctx.scene.entity.get(instanceId);
467
+ if (entity !== null) {
468
+ const [x, , z] = entity.position;
469
+ if (stateMachine.lastPos !== null) {
470
+ const instantSpeed = Math.hypot(x - stateMachine.lastPos[0], z - stateMachine.lastPos[2]) / delta;
471
+ stateMachine.smoothedSpeed +=
472
+ (instantSpeed - stateMachine.smoothedSpeed) * Math.min(1, delta * 12);
473
+ }
474
+ stateMachine.lastPos = [x, entity.position[1], z];
475
+ const walkSpeed = states.walkSpeed ?? 0.5;
476
+ const runSpeed = states.runSpeed ?? 6;
477
+ const next = stateMachine.smoothedSpeed < walkSpeed
478
+ ? "idle"
479
+ : stateMachine.actions.run !== undefined && stateMachine.smoothedSpeed >= runSpeed
480
+ ? "run"
481
+ : "walk";
482
+ if (next !== stateMachine.active) {
483
+ if (activeOneShotRef.current === null) {
484
+ const fade = states.fadeSec ?? 0.2;
485
+ const from = stateMachine.actions[stateMachine.active];
486
+ const to = stateMachine.actions[next];
487
+ if (from !== undefined && to !== undefined) {
488
+ to.reset().fadeIn(fade).play();
489
+ from.fadeOut(fade);
490
+ }
491
+ }
492
+ stateMachine.active = next;
493
+ }
494
+ }
495
+ }
298
496
  if (mixerRef.current !== null && !animationPausedRef.current)
299
497
  mixerRef.current.update(delta);
300
498
  if (instanceId === undefined)
@@ -319,7 +517,7 @@ function EntityModel({ model, instanceId }) {
319
517
  }
320
518
  paintDrawnCountRef.current = syncPaintCanvas(paintCanvasRef.current, cache.seedColor, strokes, paintDrawnCountRef.current);
321
519
  });
322
- return _jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] });
520
+ return (_jsxs(_Fragment, { children: [_jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] }), (model.attachments ?? []).map((attachment, index) => typeof attachment.model === "string" ? null : (_jsx(BoneAttachment, { rig: scene, model: attachment.model, slot: attachment.slot, position: attachment.position, rotation: attachment.rotation, scale: attachment.scale }, `${attachment.slot}-${index}`)))] }));
323
521
  }
324
522
  function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, selected, onSelect, }) {
325
523
  const groupRef = useRef(null);
@@ -346,7 +544,7 @@ function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, select
346
544
  const live = ctx.scene.entity.get(entityId);
347
545
  if (live !== null)
348
546
  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] }));
547
+ }, 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(IsolatedEntityModel, { model: model, instanceId: entityId, fallback: sprite !== undefined ? _jsx(EntitySprite, { sprite: sprite }) : undefined })) : 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
548
  }
351
549
  function ObjectMarker({ object, custom, model, style, }) {
352
550
  const groupRef = useRef(null);
@@ -366,7 +564,7 @@ function ObjectMarker({ object, custom, model, style, }) {
366
564
  writeEntityPose(group, live);
367
565
  group.visible = visibleRef.current(instanceId);
368
566
  });
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 })] })) }));
567
+ return (_jsx("group", { ref: groupRef, userData: { [POINTER_OBJECT_KEY]: instanceId }, children: custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(IsolatedEntityModel, { 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 })] })) }));
370
568
  }
371
569
  function GroundPlane() {
372
570
  const geometry = useMemo(() => {
@@ -404,7 +602,7 @@ function WorldEnvironment({ environment: Environment }) {
404
602
  return _jsx(Environment, {});
405
603
  return (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx("gridHelper", { args: [160, 80, "#3a3f4a", "#2b2f38"], "position-y": 0.01 }), _jsx(RockField, {})] }));
406
604
  }
407
- function WorldActors({ entitySprites, entityModels, objectModels, objectStyles, assets, renderEntity, renderObject, selectedIds, }) {
605
+ function WorldActors({ entitySprites, entityModels, objectModels, objectStyles, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }) {
408
606
  const ctx = useGameContext();
409
607
  const entities = useSceneEntities();
410
608
  const objects = useSceneObjects();
@@ -417,10 +615,8 @@ function WorldActors({ entitySprites, entityModels, objectModels, objectStyles,
417
615
  };
418
616
  return (_jsxs(_Fragment, { children: [entities
419
617
  .filter((entity) => entity.name !== WORLD_ITEM_ENTITY_NAME)
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) => {
618
+ .filter((entity) => !(hideLocalActor && entity.id === controlledId))
619
+ .map((entity) => (_jsx(EntityMarker, { entity: entity, custom: renderEntity?.(entity), model: resolveEntityModel(entityModels?.[entity.name], assets, entity.name), 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
620
  const model = resolveModel(objectModels?.[object.catalogId], assets, {
425
621
  seam: "objectModels",
426
622
  key: object.catalogId,
@@ -428,36 +624,25 @@ function WorldActors({ entitySprites, entityModels, objectModels, objectStyles,
428
624
  return (_jsx(ObjectMarker, { object: object, custom: renderObject?.(object), model: model, style: objectStyles?.[object.catalogId] }, object.instanceId));
429
625
  })] }));
430
626
  }
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 })] }));
627
+ function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }) {
628
+ 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, hideLocalActor: hideLocalActor })] }));
433
629
  }
434
630
  function RemotePlayers({ rows }) {
435
631
  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))) }));
436
632
  }
437
- function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }) {
633
+ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, gateRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }) {
438
634
  const posterElapsedRef = useRef(0);
439
635
  const posterDoneRef = useRef(false);
440
- const motionRef = useRef(createPlayerMotionState());
441
- const voxelBodyRef = useRef(null);
442
- const solidCacheRef = useRef({ count: -1, set: new Set() });
443
636
  const hasReportedTickError = useRef(false);
444
637
  const repeatFiredAtRef = useRef(new Map());
445
638
  const slotActions = useMemo(() => findHotbarSlotActions(playable.game.input), [playable]);
446
639
  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]);
640
+ const movementTuning = useMemo(() => resolvePlayerMovementTuning({
641
+ collision: playable.collision,
642
+ movement: playable.movement,
643
+ physics: playable.game.physics,
644
+ world: playable.game.world,
645
+ }), [playable]);
461
646
  const autoPickupRadius = useMemo(() => {
462
647
  const cfg = playable.worldItem?.autoPickup;
463
648
  if (cfg === undefined || cfg === false)
@@ -465,10 +650,24 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
465
650
  const fallback = playable.worldItem?.pickupRadius ?? DEFAULT_PICKUP_RADIUS;
466
651
  return cfg === true ? fallback : cfg.radius ?? fallback;
467
652
  }, [playable]);
468
- const ground = useMemo(() => groundFieldFor(playable.game.world), [playable]);
469
653
  const drivesPose = useMemo(() => shellDrivesPlayerPose(playable.game.input), [playable]);
654
+ const serverAuthoritative = useMemo(() => isServerAuthoritative(playable.game.multiplayer) && multiplayer !== null, [playable, multiplayer]);
655
+ const commandSink = useMemo(() => ({
656
+ run: (name, input) => resolveCommandSink(ctx, {
657
+ serverAuthoritative,
658
+ backend: multiplayer?.backend ?? null,
659
+ serverId: serverIdRef.current,
660
+ }).run(name, input),
661
+ }), [ctx, multiplayer, serverAuthoritative, serverIdRef]);
662
+ const inputSink = useMemo(() => ({
663
+ send: (frame) => resolveInputSink({
664
+ serverAuthoritative,
665
+ backend: multiplayer?.backend ?? null,
666
+ serverId: serverIdRef.current,
667
+ }).send(frame),
668
+ }), [multiplayer, serverAuthoritative, serverIdRef]);
669
+ const lastSentInputRef = useRef(null);
470
670
  const inputActions = useMemo(() => Object.keys(playable.game.input ?? {}), [playable]);
471
- const hasTerrain = useMemo(() => hasEnvironmentTerrain(playable.game.world), [playable]);
472
671
  useFrame((_state, rawDt) => {
473
672
  if (poster) {
474
673
  if (posterDoneRef.current)
@@ -480,6 +679,21 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
480
679
  return;
481
680
  }
482
681
  }
682
+ const sendInput = () => {
683
+ if (!serverAuthoritative)
684
+ return;
685
+ const frame = { held: ctx.input.held(), pointer: ctx.input.pointer() };
686
+ const last = lastSentInputRef.current;
687
+ if (last !== null && inputFramesEqual(last, frame))
688
+ return;
689
+ lastSentInputRef.current = frame;
690
+ inputSink.send(frame);
691
+ };
692
+ if (gateRef.current) {
693
+ ctx.input.publish(heldActionsFor(tracker, NO_ACTIONS));
694
+ sendInput();
695
+ return;
696
+ }
483
697
  const simStart = performance.now();
484
698
  try {
485
699
  let endPhase = devtools.profile.begin("time+input");
@@ -487,108 +701,19 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
487
701
  const gameDt = ctx.time.advance(dt);
488
702
  ctx.input.publish(heldActionsFor(tracker, inputActions));
489
703
  ctx.input.publishPointer(pointerAxisRef.current);
704
+ sendInput();
490
705
  const turnInput = (tracker.isDown("turnRight") ? 1 : 0) - (tracker.isDown("turnLeft") ? 1 : 0);
491
706
  if (turnInput !== 0)
492
707
  yawRef.current = steerYaw(yawRef.current, turnInput, TURN_SPEED, dt);
493
708
  endPhase();
494
709
  const playerId = ctx.player.possession.active(ctx.player.userId);
495
710
  const player = ctx.scene.entity.get(playerId);
496
- const forwardX = Math.sin(yawRef.current);
497
- const forwardZ = Math.cos(yawRef.current);
498
- if (player !== null && drivesPose) {
711
+ if (player !== null && drivesPose && !serverAuthoritative) {
499
712
  endPhase = devtools.profile.begin("pose");
500
- const keys = createEmptyMovementKeys();
501
- keys.w = tracker.isDown("moveForward");
502
- keys.s = tracker.isDown("moveBack");
503
- keys.a = tracker.isDown("moveLeft");
504
- keys.d = tracker.isDown("moveRight");
505
- keys.shift = tracker.isDown("sprint") && (movement?.canSprint?.(ctx) ?? true);
506
- keys.space = tracker.isDown("jump");
507
- const intent = resolveMovementIntent(keys, true);
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
- }
713
+ stepPlayerMovement(ctx, ctx.player.userId, { held: ctx.input.held(), pointer: ctx.input.pointer() }, rawDt, movementTuning, yawRef.current);
589
714
  endPhase();
590
715
  }
591
- if (autoPickupRadius !== null) {
716
+ if (autoPickupRadius !== null && !serverAuthoritative) {
592
717
  endPhase = devtools.profile.begin("pickup");
593
718
  const self = ctx.scene.entity.get(playerId);
594
719
  if (self !== null) {
@@ -598,9 +723,11 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
598
723
  }
599
724
  endPhase();
600
725
  }
601
- devtools.profile.measure("onTick", () => {
602
- playable.loop.onTick(ctx, gameDt);
603
- });
726
+ if (!serverAuthoritative) {
727
+ devtools.profile.measure("onTick", () => {
728
+ playable.loop.onTick(ctx, gameDt);
729
+ });
730
+ }
604
731
  endPhase = devtools.profile.begin("actions");
605
732
  if (tracker.wasPressed("tabTarget")) {
606
733
  if (ctx.game.commands.has("target.cycle"))
@@ -617,7 +744,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
617
744
  if (pingCommand !== undefined && tracker.wasPressed("ping")) {
618
745
  const hit = pointerService.worldHit();
619
746
  if (hit !== null && ctx.game.commands.has(pingCommand)) {
620
- ctx.game.commands.run(pingCommand, {
747
+ commandSink.run(pingCommand, {
621
748
  point: hit.point,
622
749
  entity: hit.entity,
623
750
  object: hit.object,
@@ -632,7 +759,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
632
759
  collisionDebug.setAimProbe({
633
760
  from: aimFrom,
634
761
  aim: commandAim,
635
- originPolicy: { kind: "muzzle" },
762
+ originPolicy: { kind: "eye" },
636
763
  maxDistance: 100,
637
764
  });
638
765
  }
@@ -652,7 +779,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
652
779
  if (prompts !== undefined && focus !== null) {
653
780
  const active = resolveActivePrompt({ x: focus.position[0], z: focus.position[2] }, prompts);
654
781
  if (active !== null && active.prompt.invoke !== null) {
655
- ctx.game.commands.run(active.prompt.invoke.name, active.prompt.invoke.input);
782
+ commandSink.run(active.prompt.invoke.name, active.prompt.invoke.input);
656
783
  }
657
784
  }
658
785
  continue;
@@ -660,7 +787,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
660
787
  if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
661
788
  continue;
662
789
  repeatFiredAtRef.current.set(action, nowMs);
663
- dispatchBoundAction(ctx, action, yawRef.current, pitchRef.current, commandAim);
790
+ dispatchBoundAction(ctx, action, yawRef.current, pitchRef.current, commandAim, RESERVED_INPUT_ACTIONS, commandSink);
664
791
  }
665
792
  if (hotbarId !== null) {
666
793
  for (const { action, slot } of slotActions) {
@@ -713,7 +840,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
713
840
  }, GAME_SIM_FRAME_PRIORITY);
714
841
  return null;
715
842
  }
716
- function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, onRuntimeError, }) {
843
+ function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, gateRef, onRuntimeError, }) {
717
844
  const hasReportedTickError = useRef(false);
718
845
  const repeatFiredAtRef = useRef(new Map());
719
846
  const lastFrameRef = useRef(null);
@@ -726,6 +853,10 @@ function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, onRuntimeError,
726
853
  lastFrameRef.current = now;
727
854
  if (last === null)
728
855
  return;
856
+ if (gateRef.current) {
857
+ ctx.input.publish(heldActionsFor(tracker, NO_ACTIONS));
858
+ return;
859
+ }
729
860
  const rawDt = (now - last) / 1000;
730
861
  const simStart = performance.now();
731
862
  try {
@@ -744,7 +875,7 @@ function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, onRuntimeError,
744
875
  if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
745
876
  continue;
746
877
  repeatFiredAtRef.current.set(action, nowMs);
747
- dispatchBoundAction(ctx, action, 0, 0, { yaw: 0, pitch: 0 });
878
+ dispatchBoundAction(ctx, action, 0, 0, { yaw: 0, pitch: 0 }, EMPTY_RESERVED);
748
879
  }
749
880
  tracker.endFrame();
750
881
  endPhase();
@@ -759,7 +890,7 @@ function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, onRuntimeError,
759
890
  };
760
891
  frameId = requestAnimationFrame(tick);
761
892
  return () => cancelAnimationFrame(frameId);
762
- }, [ctx, playable, tracker, pointerAxisRef, onRuntimeError, inputActions]);
893
+ }, [ctx, playable, tracker, pointerAxisRef, gateRef, onRuntimeError, inputActions]);
763
894
  return null;
764
895
  }
765
896
  class GameUiErrorBoundary extends Component {
@@ -864,21 +995,67 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
864
995
  pointerAxisRef.current = { ...state, active: false };
865
996
  };
866
997
  const touchScheme = useMemo(() => deriveTouchScheme(playable.game.input, {
867
- reserved: RESERVED_INPUT_ACTIONS,
998
+ reserved: resolveRigKind(playable.camera) === "none" || playable.presentation === "hud"
999
+ ? EMPTY_RESERVED
1000
+ : RESERVED_INPUT_ACTIONS,
868
1001
  firstPerson: resolveRigKind(playable.camera) === "first",
869
1002
  config: playable.touch,
870
1003
  }), [playable]);
871
1004
  const { coarsePointer, portrait, compact } = useDisplayProfile();
872
1005
  const touchSink = useMemo(() => ({ onCodeDown: (code) => tracker.handleDown(code), onCodeUp: (code) => tracker.handleUp(code) }), [tracker]);
873
- const audioEngine = useMemo(() => createAudioEngine({ sounds: playable.audio?.sounds, buses: playable.audio?.buses }), [playable]);
1006
+ const gateRef = useRef(false);
1007
+ const orientationPlatform = coarsePointer ? "mobile" : "desktop";
1008
+ const orientationRequirement = useMemo(() => resolveOrientationRequirement(playable.orientation, orientationPlatform), [playable, orientationPlatform]);
1009
+ const liveOrientation = portrait ? "portrait" : "landscape";
1010
+ const orientationGate = !poster && coarsePointer && orientationGateActive(orientationRequirement, liveOrientation);
1011
+ const orientationHint = !poster && coarsePointer && orientationHintActive(orientationRequirement, liveOrientation);
1012
+ gateRef.current = orientationGate;
1013
+ const orientationGateEl = orientationGate ? (_jsx(RotateDeviceScreen, { requiredOrientation: orientationRequirement.required ?? "landscape", title: orientationRequirement.required === "portrait" ? "Turn your phone upright" : "Turn your phone sideways", description: `${playable.game.name} is built for ${orientationRequirement.required ?? "landscape"} play.` })) : orientationHint && orientationRequirement.preferred !== null ? (_jsx(OrientationHint, { wanted: orientationRequirement.preferred })) : null;
1014
+ const audioEngine = useMemo(() => createAudioEngine({
1015
+ sounds: playable.audio?.sounds,
1016
+ buses: playable.audio?.buses,
1017
+ music: playable.audio?.music,
1018
+ musicBus: playable.audio?.musicBus,
1019
+ }), [playable]);
874
1020
  useEffect(() => () => audioEngine.dispose(), [audioEngine]);
875
1021
  useEffect(() => {
876
1022
  if (ctx === null)
877
1023
  return;
878
- return ctx.game.events.on("audio.play", ({ sound, at }) => {
1024
+ const offPlay = ctx.game.events.on("audio.play", ({ sound, at }) => {
879
1025
  audioEngine.playOneShot(sound, at === undefined ? undefined : { x: at[0], y: at[1], z: at[2] });
880
1026
  });
1027
+ const offMusic = ctx.game.events.on("audio.music", ({ theme, transpose }) => {
1028
+ audioEngine.playMusic(theme, transpose === undefined ? undefined : { transpose });
1029
+ });
1030
+ const offResume = ctx.game.events.on("audio.resume", () => audioEngine.resume());
1031
+ return () => {
1032
+ offPlay();
1033
+ offMusic();
1034
+ offResume();
1035
+ };
881
1036
  }, [ctx, audioEngine]);
1037
+ useEffect(() => {
1038
+ if (typeof document === "undefined")
1039
+ return;
1040
+ document.documentElement.dataset.jgPresentation = playable.presentation ?? "3d";
1041
+ return () => {
1042
+ delete document.documentElement.dataset.jgPresentation;
1043
+ };
1044
+ }, [playable]);
1045
+ useEffect(() => {
1046
+ if (ctx === null || typeof document === "undefined")
1047
+ return;
1048
+ const hud = resolveRigKind(playable.camera) === "none" || playable.presentation === "hud";
1049
+ if (!hud)
1050
+ return;
1051
+ const frame = requestAnimationFrame(() => {
1052
+ document.documentElement.dataset.jgFrameReady = "1";
1053
+ });
1054
+ return () => {
1055
+ cancelAnimationFrame(frame);
1056
+ delete document.documentElement.dataset.jgFrameReady;
1057
+ };
1058
+ }, [ctx, playable]);
882
1059
  const userId = multiplayer?.userId ?? DEV_USER_ID;
883
1060
  const reportRuntimeError = (error, phase, componentStack) => {
884
1061
  const diagnostic = logRuntimeError(error, phase, componentStack);
@@ -920,6 +1097,9 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
920
1097
  return;
921
1098
  }
922
1099
  serverIdRef.current = joined.serverId;
1100
+ if (isServerAuthoritative(playable.game.multiplayer) && multiplayer.backend.feeds !== undefined) {
1101
+ cleanups.push(attachWorldSync(multiplayer.backend.feeds, joined.serverId, ctx));
1102
+ }
923
1103
  cleanups.push(multiplayer.backend.presenceSync.subscribe(joined.serverId, (rows) => {
924
1104
  setRemotePlayers(rows.filter((row) => row.userId !== multiplayer.userId));
925
1105
  }));
@@ -955,8 +1135,9 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
955
1135
  cleanups.push(remoteUnsub);
956
1136
  }
957
1137
  const chatSync = multiplayer.backend.chatSyncFor?.(joined.serverId);
958
- if (chatSync !== undefined) {
959
- const globalChannelIds = new Set(ctx.game.chat
1138
+ if (chatSync !== undefined && ctx.game.chat !== undefined) {
1139
+ const chat = ctx.game.chat;
1140
+ const globalChannelIds = new Set(chat
960
1141
  .channels()
961
1142
  .filter((channel) => channel.kind === "global")
962
1143
  .map((channel) => channel.id));
@@ -976,7 +1157,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
976
1157
  if (seenRemoteChat.has(message.id))
977
1158
  continue;
978
1159
  seenRemoteChat.add(message.id);
979
- ctx.game.chat.send(message.fromUserId, message.channelId, message.body);
1160
+ chat.send(message.fromUserId, message.channelId, message.body);
980
1161
  }
981
1162
  }));
982
1163
  }
@@ -994,7 +1175,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
994
1175
  void multiplayer.backend.transport.leaveServer({ serverId }).catch(() => undefined);
995
1176
  }
996
1177
  };
997
- }, [ctx, multiplayer]);
1178
+ }, [ctx, multiplayer, playable]);
998
1179
  useEffect(() => {
999
1180
  wrapperRef.current?.focus();
1000
1181
  }, [ctx]);
@@ -1010,7 +1191,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1010
1191
  const rigKind = resolveRigKind(cameraConfig);
1011
1192
  if (rigKind === "none" || playable.presentation === "hud") {
1012
1193
  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) => {
1194
+ return (_jsx("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
1014
1195
  if (event.code === "F2" && devtoolsEnabled) {
1015
1196
  event.preventDefault();
1016
1197
  f2HeldRef.current = true;
@@ -1035,7 +1216,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1035
1216
  }, onBlur: () => {
1036
1217
  f2HeldRef.current = false;
1037
1218
  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 })] }));
1219
+ }, onPointerDown: () => audioEngine.resume(), onPointerMove: trackPointerAxis, onPointerLeave: deactivatePointerAxis, onPointerCancel: deactivatePointerAxis, children: _jsxs(GameViewportProvider, { platforms: playable.platforms, children: [_jsx(HudOnlyDriver, { ctx: ctx, playable: playable, tracker: tracker, pointerAxisRef: pointerAxisRef, gateRef: gateRef, onRuntimeError: reportRuntimeError }), !orientationGate && coarsePointer && touchScheme !== null && touchScheme.gestures !== null && playControlsActive(ctx) ? (_jsx(TouchPlaySurface, { scheme: touchScheme, sink: touchSink, yawRef: yawRef, pitchRef: pitchRef, maxPitch: 0, onPrimaryTap: () => undefined })) : null, _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(HudViewportProvider, { platforms: playable.platforms, config: playable.hudFit, userScale: graphics.uiScale, children: orientationGate ? null : _jsx(GameUI, {}) }) }) }), orientationGateEl, devtoolsEnabled ? (_jsx(DevtoolsOverlay, { open: devtoolsOpen, ctx: ctx, playable: playable, multiplayer: multiplayer })) : null, _jsx(DiagnosticOverlay, { diagnostics: diagnostics, gameName: playable.game.name })] }) }));
1039
1220
  }
1040
1221
  const firstPerson = rigKind === "first";
1041
1222
  const showReticle = (firstPerson && playable.camera?.firstPerson?.reticle !== false) || rigKind === "shoulder";
@@ -1047,6 +1228,9 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1047
1228
  ? "health"
1048
1229
  : worldBars.statId ?? "health";
1049
1230
  const barsRoles = worldBars === undefined || worldBars === true || worldBars === false ? undefined : worldBars.roles;
1231
+ const barsMaxDistance = worldBars === undefined || worldBars === true || worldBars === false
1232
+ ? undefined
1233
+ : worldBars.maxDistance;
1050
1234
  const resolveEntityRole = (entity) => playable.content.entityById?.(entity.name)?.role;
1051
1235
  const pointer = playable.pointer;
1052
1236
  const pointerUsesLeft = pointer !== undefined && (pointer.select === true || pointer.moveCommand !== undefined);
@@ -1086,13 +1270,15 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1086
1270
  }
1087
1271
  commitSelection(selectWithinRect(candidates, rect));
1088
1272
  };
1273
+ /** DOM UI (menus, HUD buttons) sits in the same wrapper as the canvas — only canvas-targeted clicks are world input. */
1274
+ const isWorldPointerTarget = (event) => event.target instanceof HTMLCanvasElement;
1089
1275
  const handlePointerDown = (event) => {
1090
1276
  wrapperRef.current?.focus();
1091
1277
  trackPointerAxis(event);
1092
1278
  audioEngine.resume();
1093
1279
  if (contextMenu !== null)
1094
1280
  setContextMenu(null);
1095
- if (event.button === 0) {
1281
+ if (event.button === 0 && isWorldPointerTarget(event)) {
1096
1282
  const point = localXY(event);
1097
1283
  pointerDownRef.current = point;
1098
1284
  if (pointer?.select === true)
@@ -1166,6 +1352,8 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1166
1352
  const handleContextMenu = (event) => {
1167
1353
  if (pointer === undefined)
1168
1354
  return;
1355
+ if (!isWorldPointerTarget(event))
1356
+ return;
1169
1357
  event.preventDefault();
1170
1358
  const hit = pointerService.worldHit();
1171
1359
  if (hit === null)
@@ -1218,15 +1406,12 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1218
1406
  }));
1219
1407
  const touchScale = compact ? 0.88 : 1;
1220
1408
  const dockMounted = !poster &&
1409
+ !orientationGate &&
1221
1410
  coarsePointer &&
1222
1411
  controlsActive &&
1223
1412
  touchScheme !== null &&
1224
1413
  (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: {
1414
+ 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: _jsx("div", { ref: wrapperRef, tabIndex: 0, ...(poster && posterFrozen ? { "data-poster-ready": "" } : {}), className: "relative h-full w-full bg-neutral-950 outline-none", style: {
1230
1415
  "--jg-hud-dock-clearance": `${dockMounted ? touchDockClearance(touchScheme, touchScale) : 0}px`,
1231
1416
  }, onKeyDown: (event) => {
1232
1417
  if (event.code === "F2" && devtoolsEnabled) {
@@ -1265,24 +1450,24 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1265
1450
  else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
1266
1451
  ctx.game.commands.run("ui.hotbarScrollPrev", {});
1267
1452
  }
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, {})] }) })] }) }));
1453
+ }, children: _jsxs(GameViewportProvider, { platforms: playable.platforms, children: [_jsxs(Canvas, { frameloop: poster && posterFrozen ? "demand" : "always", orthographic: orthographic, camera: orthographic
1454
+ ? {
1455
+ zoom: playable.camera?.frustum?.zoom ?? CAMERA_FRUSTUM_DEFAULTS.zoom,
1456
+ near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
1457
+ far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
1458
+ }
1459
+ : {
1460
+ fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
1461
+ near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
1462
+ far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
1463
+ }, 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, hideLocalActor: firstPerson }) }), WorldOverlay !== undefined ? _jsx(WorldOverlay, {}) : null, barsStatId !== null ? (_jsx(WorldEntityBars, { statId: barsStatId, roles: barsRoles, resolveRole: resolveEntityRole, ...(barsMaxDistance === undefined ? {} : { maxDistance: barsMaxDistance }) })) : 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) => {
1464
+ cameraDraggingRef.current = dragging;
1465
+ } }), _jsx(PointerProbe, { service: pointerService })] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, pointerAxisRef: pointerAxisRef, gateRef: gateRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand, poster: poster, onPosterSettled: () => {
1466
+ if (posterSettledRef.current)
1467
+ return;
1468
+ posterSettledRef.current = true;
1469
+ setPosterFrozen(true);
1470
+ } }), _jsx(DevtoolsRendererProbe, {}), playable.postProcessing !== undefined && playable.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: playable.postProcessing })) : null] }), !poster && !orientationGate && 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: () => {
1471
+ primaryClickRef.current = true;
1472
+ } })) : null, _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(HudViewportProvider, { platforms: playable.platforms, config: playable.hudFit, userScale: graphics.uiScale, children: orientationGate ? null : _jsx(GameUI, {}) }) }) }), !poster && !orientationGate && showReticle ? _jsx(Reticle, {}) : null, dockMounted && touchScheme !== null ? (_jsx(TouchControlsDock, { scheme: touchScheme, sink: touchSink, scale: touchScale })) : null, orientationGateEl, 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, {})] }) }) })] }) }));
1288
1473
  }