@jgengine/shell 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -3
- package/LICENSE +203 -661
- package/README.md +1 -1
- package/dist/GamePhaseStamp.d.ts +1 -0
- package/dist/GamePhaseStamp.js +9 -0
- package/dist/GamePlayerShell.d.ts +4 -31
- package/dist/GamePlayerShell.js +162 -932
- package/dist/GameUiPreview.js +0 -2
- package/dist/audio/audioEngine.d.ts +8 -0
- package/dist/audio/audioEngine.js +63 -8
- package/dist/audio/audioWire.d.ts +22 -0
- package/dist/audio/audioWire.js +19 -0
- package/dist/audio/musicDirector.d.ts +31 -0
- package/dist/audio/musicDirector.js +125 -0
- package/dist/audio/musicVoices.d.ts +10 -0
- package/dist/audio/musicVoices.js +338 -0
- package/dist/audio/synthEngine.d.ts +9 -0
- package/dist/audio/synthEngine.js +57 -0
- package/dist/behaviour.d.ts +2 -1
- package/dist/behaviour.js +2 -1
- package/dist/behaviourAttach.d.ts +1 -0
- package/dist/behaviourAttach.js +1 -0
- package/dist/behaviourDriver.d.ts +1 -0
- package/dist/behaviourDriver.js +1 -0
- package/dist/boundActionDispatch.d.ts +13 -0
- package/dist/boundActionDispatch.js +37 -0
- package/dist/camera/GameCameraRig.d.ts +5 -2
- package/dist/camera/GameCameraRig.js +2 -2
- package/dist/camera/GameFirstPersonCamera.d.ts +12 -2
- package/dist/camera/GameFirstPersonCamera.js +29 -7
- package/dist/camera/GameInspectionCamera.d.ts +1 -1
- package/dist/camera/GameInspectionCamera.js +13 -2
- package/dist/camera/GameOrbitCamera.js +59 -10
- package/dist/camera/cameraBlendMath.d.ts +3 -0
- package/dist/camera/cameraBlendMath.js +3 -0
- package/dist/camera/fovPreference.d.ts +6 -1
- package/dist/camera/fovPreference.js +6 -1
- package/dist/camera/index.d.ts +4 -7
- package/dist/camera/index.js +4 -7
- package/dist/camera/inspectionCameraMath.d.ts +7 -2
- package/dist/camera/inspectionCameraMath.js +7 -2
- package/dist/camera/orbitCameraMath.d.ts +54 -6
- package/dist/camera/orbitCameraMath.js +61 -6
- package/dist/camera/rigMath.d.ts +94 -24
- package/dist/camera/rigMath.js +100 -24
- package/dist/camera/rigResolve.d.ts +4 -2
- package/dist/camera/rigResolve.js +4 -2
- package/dist/camera/shakeChannel.d.ts +6 -2
- package/dist/camera/shakeChannel.js +6 -2
- package/dist/camera/shakeChannelMath.d.ts +1 -0
- package/dist/camera/shakeChannelMath.js +1 -0
- package/dist/cartridge/runtime.d.ts +15 -0
- package/dist/cartridge/runtime.js +556 -0
- package/dist/cartridge/spec.d.ts +188 -0
- package/dist/cartridge/spec.js +19 -0
- package/dist/cartridge/validate.d.ts +2 -0
- package/dist/cartridge/validate.js +78 -0
- package/dist/cartridge.d.ts +1 -1
- package/dist/cartridge.js +4 -3
- package/dist/commandSink.d.ts +25 -0
- package/dist/commandSink.js +32 -0
- package/dist/defineGame.js +26 -5
- package/dist/devtools/ColPanel.d.ts +1 -0
- package/dist/devtools/ColPanel.js +14 -0
- package/dist/devtools/CollisionDebugWorld.js +1 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +7 -3
- package/dist/devtools/DevtoolsOverlay.js +19 -450
- package/dist/devtools/KeysPanel.d.ts +4 -0
- package/dist/devtools/KeysPanel.js +31 -0
- package/dist/devtools/LogsPanel.d.ts +1 -0
- package/dist/devtools/LogsPanel.js +12 -0
- package/dist/devtools/NetPanel.d.ts +4 -0
- package/dist/devtools/NetPanel.js +10 -0
- package/dist/devtools/PerfPanel.d.ts +4 -0
- package/dist/devtools/PerfPanel.js +53 -0
- package/dist/devtools/TunePanel.d.ts +3 -0
- package/dist/devtools/TunePanel.js +309 -0
- package/dist/devtools/agentBridge.d.ts +41 -0
- package/dist/devtools/agentBridge.js +262 -0
- package/dist/devtools/collisionDebug.d.ts +6 -0
- package/dist/devtools/collisionDebug.js +6 -0
- package/dist/devtools/collisionDebugMath.d.ts +9 -2
- package/dist/devtools/collisionDebugMath.js +25 -5
- package/dist/devtools/devtoolsOverrides.d.ts +6 -0
- package/dist/devtools/devtoolsOverrides.js +48 -0
- package/dist/devtools/panelAtoms.d.ts +9 -0
- package/dist/devtools/panelAtoms.js +10 -0
- package/dist/devtools/perfDiagnose.d.ts +2 -0
- package/dist/devtools/perfDiagnose.js +43 -0
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +29 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +69 -0
- package/dist/drivers/FrameDriver.d.ts +37 -0
- package/dist/drivers/FrameDriver.js +232 -0
- package/dist/drivers/HudOnlyDriver.d.ts +16 -0
- package/dist/drivers/HudOnlyDriver.js +60 -0
- package/dist/environment/Daylight.d.ts +29 -7
- package/dist/environment/Daylight.js +153 -13
- package/dist/environment/EnvironmentScene.js +42 -7
- package/dist/environment/RoadRibbons.d.ts +7 -0
- package/dist/environment/RoadRibbons.js +57 -0
- package/dist/environment/VolumetricClouds.d.ts +17 -0
- package/dist/environment/VolumetricClouds.js +202 -0
- package/dist/environment/daylightCycle.d.ts +3 -1
- package/dist/environment/daylightCycle.js +3 -1
- package/dist/environment/groundPadMath.d.ts +3 -0
- package/dist/environment/groundPadMath.js +3 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/environment/skyLightingPolicy.d.ts +2 -0
- package/dist/environment/skyLightingPolicy.js +2 -0
- package/dist/hotbarActions.d.ts +26 -0
- package/dist/hotbarActions.js +27 -0
- package/dist/inputSink.d.ts +30 -0
- package/dist/inputSink.js +85 -0
- package/dist/materialOverride.d.ts +10 -0
- package/dist/materialOverride.js +15 -3
- package/dist/multiplayer.js +14 -4
- package/dist/postfx/PostProcessing.d.ts +10 -0
- package/dist/postfx/PostProcessing.js +120 -0
- package/dist/postfx/gradeShader.d.ts +4 -0
- package/dist/postfx/gradeShader.js +72 -0
- package/dist/registry.d.ts +3 -2
- package/dist/render/EnvironmentLighting.d.ts +10 -0
- package/dist/render/EnvironmentLighting.js +34 -0
- package/dist/render/SceneLighting.d.ts +7 -0
- package/dist/render/SceneLighting.js +18 -0
- package/dist/render/SceneModels.d.ts +14 -0
- package/dist/render/SceneModels.js +337 -0
- package/dist/render/defaultSurface.d.ts +29 -0
- package/dist/render/defaultSurface.js +101 -0
- package/dist/render/modelLoad.d.ts +24 -0
- package/dist/render/modelLoad.js +73 -0
- package/dist/render/modelRender.d.ts +11 -0
- package/dist/render/modelRender.js +15 -0
- package/dist/render/resolveModel.d.ts +39 -3
- package/dist/render/resolveModel.js +79 -2
- package/dist/render/useEntityRenderCues.d.ts +13 -0
- package/dist/render/useEntityRenderCues.js +62 -0
- package/dist/scatter/InstancedScatter.d.ts +24 -0
- package/dist/scatter/InstancedScatter.js +130 -0
- package/dist/scatter/scatterModels.d.ts +25 -0
- package/dist/scatter/scatterModels.js +61 -0
- package/dist/scatter/scatterProxies.d.ts +6 -0
- package/dist/scatter/scatterProxies.js +94 -0
- package/dist/scatter.d.ts +1 -0
- package/dist/scatter.js +1 -0
- package/dist/scene/AuthoredScene.d.ts +82 -0
- package/dist/scene/AuthoredScene.js +173 -0
- package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
- package/dist/scene/GeneratedAssetRenderer.js +59 -0
- package/dist/scene/GrassFieldRenderer.d.ts +2 -0
- package/dist/scene/GrassFieldRenderer.js +25 -0
- package/dist/scene/SoilPatchRenderer.d.ts +2 -0
- package/dist/scene/SoilPatchRenderer.js +21 -0
- package/dist/scene/StudioStage.d.ts +37 -0
- package/dist/scene/StudioStage.js +42 -0
- package/dist/scene/WaterRenderer.d.ts +2 -0
- package/dist/scene/WaterRenderer.js +37 -0
- package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
- package/dist/scene/builtinSceneKindRenderers.js +22 -0
- package/dist/scene/sceneKindRenderers.d.ts +28 -0
- package/dist/scene/sceneKindRenderers.js +17 -0
- package/dist/scene.d.ts +1 -0
- package/dist/scene.js +1 -0
- package/dist/settings/appliedSettings.d.ts +5 -0
- package/dist/settings/appliedSettings.js +9 -0
- package/dist/settings/settingsController.d.ts +2 -0
- package/dist/settings/settingsController.js +22 -1
- package/dist/shellConstants.d.ts +4 -0
- package/dist/shellConstants.js +4 -0
- package/dist/shellMovement.d.ts +8 -0
- package/dist/shellMovement.js +11 -0
- package/dist/shellPointer.d.ts +20 -0
- package/dist/shellPointer.js +28 -0
- package/dist/structures/PlacementGhost.d.ts +4 -0
- package/dist/structures/PlacementGhost.js +4 -0
- package/dist/structures/TransformGizmo.d.ts +43 -0
- package/dist/structures/TransformGizmo.js +55 -0
- package/dist/structures/index.d.ts +1 -0
- package/dist/structures/index.js +1 -0
- package/dist/terrain/CarvedTerrain.d.ts +4 -1
- package/dist/terrain/CarvedTerrain.js +4 -3
- package/dist/terrain/GrassField.js +1 -1
- package/dist/terrain/TerraformBrushCursor.js +3 -0
- package/dist/terrain/grassBudget.d.ts +11 -2
- package/dist/terrain/grassBudget.js +20 -4
- package/dist/terrain/grassGeometry.d.ts +3 -0
- package/dist/terrain/grassGeometry.js +3 -0
- package/dist/terrain/grassMaterial.d.ts +2 -0
- package/dist/terrain/grassMaterial.js +2 -0
- package/dist/terrain/random.d.ts +3 -0
- package/dist/terrain/random.js +3 -0
- package/dist/terrain/soilPatchMaterial.d.ts +10 -0
- package/dist/terrain/soilPatchMaterial.js +109 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +31 -0
- package/dist/terrain/terrainDetailMaterial.js +157 -0
- package/dist/terrain/terrainMath.d.ts +8 -1
- package/dist/terrain/terrainMath.js +8 -1
- package/dist/touch/TouchControlsOverlay.d.ts +9 -5
- package/dist/touch/TouchControlsOverlay.js +211 -22
- package/dist/useShellMultiplayerSync.d.ts +9 -0
- package/dist/useShellMultiplayerSync.js +98 -0
- package/dist/visibility/CullingProvider.js +1 -1
- package/dist/water/OceanConfig.d.ts +2 -0
- package/dist/water/OceanConfig.js +2 -0
- package/dist/weather/fireSpreadPose.d.ts +1 -0
- package/dist/weather/fireSpreadPose.js +1 -0
- package/dist/weather/weatherGeometry.d.ts +1 -0
- package/dist/weather/weatherGeometry.js +1 -0
- package/dist/weather/weatherMath.d.ts +3 -0
- package/dist/weather/weatherMath.js +3 -0
- package/dist/weather/weatherUniforms.d.ts +3 -0
- package/dist/weather/weatherUniforms.js +3 -0
- package/dist/world/RetainedVfx.d.ts +10 -0
- package/dist/world/RetainedVfx.js +136 -0
- package/dist/world/WorldHud.d.ts +52 -3
- package/dist/world/WorldHud.js +94 -9
- package/dist/world/WorldScene.d.ts +21 -0
- package/dist/world/WorldScene.js +159 -0
- package/dist/world/WorldVfx.d.ts +9 -0
- package/dist/world/WorldVfx.js +184 -0
- package/dist/world/entityPose.d.ts +2 -0
- package/dist/world/entityPose.js +2 -0
- package/dist/world/floatTextStyle.d.ts +1 -0
- package/dist/world/floatTextStyle.js +1 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +1 -0
- package/dist/world/worldBarSamples.d.ts +26 -1
- package/dist/world/worldBarSamples.js +45 -1
- package/dist/worldSky.d.ts +5 -0
- package/dist/worldSky.js +11 -0
- package/dist/worldSync.d.ts +11 -0
- package/dist/worldSync.js +20 -0
- package/package.json +6 -6
- package/skills/jgengine/SKILL.md +51 -0
- package/skills/jgengine/api.md +434 -0
- package/skills/jgengine/capabilities.md +29 -0
- package/skills/jgengine/reference-cartridge.md +72 -0
- package/skills/jgengine-ui/SKILL.md +36 -0
- package/skills/jgengine-ui/api.md +1852 -0
- package/skills/jgengine-ui/capabilities.md +142 -0
- package/skills/jgengine-ui/reference.md +500 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/skills/jgengine-world/SKILL.md +38 -0
- package/skills/jgengine-world/api.md +2652 -0
- package/skills/jgengine-world/capabilities.md +370 -0
- package/skills/jgengine-world/reference.md +222 -0
- package/llms.txt +0 -2643
package/dist/GamePlayerShell.js
CHANGED
|
@@ -1,53 +1,47 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Canvas
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { aimToPoint } from "@jgengine/core/input/pointer";
|
|
2
|
+
import { Canvas } from "@react-three/fiber";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
4
|
+
import { createActionStateTracker, toActionStateBindingMap, } from "@jgengine/core/input/actionBindings";
|
|
5
|
+
import { RESERVED_INPUT_ACTIONS } from "./boundActionDispatch.js";
|
|
6
|
+
import { resolveWorldSky } from "./worldSky.js";
|
|
7
|
+
import { pointerAimFor, pointerContextMenu } from "./shellPointer.js";
|
|
8
|
+
import { deriveTouchScheme, withTouchCodes, DEFAULT_TOUCH_STYLE } from "@jgengine/core/input/touchScheme";
|
|
9
|
+
import { contextVerbInput, } from "@jgengine/core/interaction/contextMenu";
|
|
11
10
|
import { normalizePointerToAxis } from "@jgengine/core/input/pointerAxis";
|
|
12
11
|
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
|
-
import { steerYaw } from "@jgengine/core/movement/steering";
|
|
15
|
-
import { advanceVoxelPlayer, createVoxelPlayerBody, } from "@jgengine/core/movement/voxelController";
|
|
16
12
|
import { createGameContext } from "@jgengine/core/runtime/gameContext";
|
|
17
|
-
import {
|
|
18
|
-
import { groundFieldFor } from "@jgengine/core/world/terrain";
|
|
19
|
-
import { objectVisualScale } from "@jgengine/core/scene/objectStore";
|
|
20
|
-
import { DEFAULT_PICKUP_RADIUS, WORLD_ITEM_ENTITY_NAME } from "@jgengine/core/game/worldItem";
|
|
21
|
-
import { useGameContext } from "@jgengine/react/provider";
|
|
13
|
+
import { DEFAULT_PICKUP_RADIUS } from "@jgengine/core/game/worldItem";
|
|
22
14
|
import { useDisplayProfile } from "@jgengine/react/display";
|
|
23
15
|
import { HudViewportProvider } from "@jgengine/react/hudViewport";
|
|
24
|
-
import {
|
|
16
|
+
import { GameViewportProvider } from "@jgengine/react/gameViewport";
|
|
17
|
+
import { RotateDeviceScreen } from "@jgengine/react/rotateDevice";
|
|
25
18
|
import { GameProvider } from "@jgengine/react/provider";
|
|
26
19
|
import { CAMERA_FRUSTUM_DEFAULTS } from "@jgengine/core/game/playableGame";
|
|
27
20
|
import { createSettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
28
21
|
import { applyBindingOverrides, clearBindingOverride, loadBindingOverrides, saveBindingOverride, } from "@jgengine/core/input/bindingOverrides";
|
|
29
22
|
import { playControlsActive } from "@jgengine/core/game/controlGate";
|
|
23
|
+
import { orientationGateActive, orientationHintActive, resolveOrientationRequirement, } from "@jgengine/core/ui/orientation";
|
|
30
24
|
import { sky as resolveSkyDescriptor } from "@jgengine/core/world/features";
|
|
31
|
-
import {
|
|
32
|
-
import { VERSION } from "@jgengine/core/meta/changelog";
|
|
25
|
+
import { resolveGameLook } from "@jgengine/core/render/lookPreset";
|
|
33
26
|
import { AudioListener, EntityAudioEmitters, ObjectAudioEmitters } from "./audio/AudioComponents.js";
|
|
34
27
|
import { createAudioEngine } from "./audio/audioEngine.js";
|
|
28
|
+
import { attachAudioEventWire } from "./audio/audioWire.js";
|
|
29
|
+
import { PostProcessing } from "./postfx/PostProcessing.js";
|
|
30
|
+
import { EnvironmentLighting } from "./render/EnvironmentLighting.js";
|
|
35
31
|
import { CollisionDebugWorld } from "./devtools/CollisionDebugWorld.js";
|
|
36
|
-
import { collisionDebug } from "./devtools/collisionDebug.js";
|
|
37
32
|
import { DevtoolsOverlay, DevtoolsRendererProbe, withDevtoolsLatency } from "./devtools/DevtoolsOverlay.js";
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import { CullingProvider
|
|
33
|
+
import { installAgentBridge } from "./devtools/agentBridge.js";
|
|
34
|
+
import { GameCameraRig, PlayerFovProvider, PlayerFovSlider, resolveRigKind, rtsPanKeysConflict, } from "./camera/index.js";
|
|
35
|
+
import { CullingProvider } from "./visibility/CullingProvider.js";
|
|
41
36
|
import { SkyDaylight, TimeOfDayDaylight } from "./environment.js";
|
|
42
37
|
import { resolveSkyLightOwnership, skyEmitsLights } from "./environment/skyLightingPolicy.js";
|
|
43
38
|
import { EnvironmentScene } from "./environment/EnvironmentScene.js";
|
|
44
|
-
import { applyMaterialOverride } from "./materialOverride.js";
|
|
45
39
|
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";
|
|
48
40
|
import { MarqueeBox, ContextMenuView } from "./pointer/PointerOverlays.js";
|
|
49
|
-
import { createPointerService
|
|
50
|
-
import { CombatCameraShake, ProjectileTracers, Reticle, WorldEntityBars, WorldFloatText, WorldTelegraphs, } from "./world/WorldHud.js";
|
|
41
|
+
import { createPointerService } from "./pointer/pointerService.js";
|
|
42
|
+
import { CombatCameraShake, ProjectileTracers, Reticle, WorldEntityBars, WorldFloatText, WorldNameplates, WorldTelegraphs, } from "./world/WorldHud.js";
|
|
43
|
+
import { WorldSpellVfx } from "./world/WorldVfx.js";
|
|
44
|
+
import { RetainedVfx } from "./world/RetainedVfx.js";
|
|
51
45
|
import { GridWorldScene } from "./world/GridWorldScene.js";
|
|
52
46
|
import { WorldItems } from "./world/WorldItems.js";
|
|
53
47
|
import { OrientationHint } from "./touch/OrientationHint.js";
|
|
@@ -56,771 +50,34 @@ import { BUILT_IN_SETTING_CATEGORIES } from "@jgengine/core/settings/settingsMod
|
|
|
56
50
|
import { SettingsProvider } from "@jgengine/react/settings";
|
|
57
51
|
import { SettingsRuntime } from "./settings/SettingsRuntime.js";
|
|
58
52
|
import { SettingsChrome } from "./settings/SettingsChrome.js";
|
|
59
|
-
import { AudioSettingsBridge, useGraphicsSettings } from "./settings/appliedSettings.js";
|
|
53
|
+
import { AudioSettingsBridge, useGraphicsSettings, useTouchStyle } from "./settings/appliedSettings.js";
|
|
54
|
+
import { ConfiguredLighting, BackdropFog } from "./render/SceneLighting.js";
|
|
55
|
+
import { WorldView, RemotePlayers } from "./world/WorldScene.js";
|
|
56
|
+
import { FrameDriver } from "./drivers/FrameDriver.js";
|
|
57
|
+
import { HudOnlyDriver } from "./drivers/HudOnlyDriver.js";
|
|
58
|
+
import { DiagnosticOverlay, GameUiErrorBoundary, logRuntimeError, } from "./diagnostics/RuntimeDiagnostics.js";
|
|
59
|
+
import { GamePhaseStamp } from "./GamePhaseStamp.js";
|
|
60
|
+
import { EMPTY_RESERVED } from "./shellConstants.js";
|
|
61
|
+
import { useShellMultiplayerSync } from "./useShellMultiplayerSync.js";
|
|
60
62
|
const DEV_USER_ID = "dev-player";
|
|
61
|
-
const TURN_SPEED = 2.4;
|
|
62
63
|
const PRIMARY_CLICK_MOVE_THRESHOLD_PX = 6;
|
|
63
|
-
const GROUND_SIZE = 160;
|
|
64
|
-
const GROUND_SEGMENTS = 80;
|
|
65
64
|
const DEFAULT_BACKGROUND_COLOR = "#14161b";
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return { phase, message: error.message, stack: error.stack, componentStack, capturedAt };
|
|
71
|
-
}
|
|
72
|
-
return { phase, message: typeof error === "string" ? error : JSON.stringify(error), componentStack, capturedAt };
|
|
73
|
-
}
|
|
74
|
-
function logRuntimeError(error, phase, componentStack) {
|
|
75
|
-
const diagnostic = errorToDiagnostic(error, phase, componentStack);
|
|
76
|
-
console.error(`[jgengine:${phase}] ${diagnostic.message}`, error);
|
|
77
|
-
return diagnostic;
|
|
78
|
-
}
|
|
79
|
-
const RESERVED_INPUT_ACTIONS = new Set([
|
|
80
|
-
"moveForward",
|
|
81
|
-
"moveBack",
|
|
82
|
-
"moveLeft",
|
|
83
|
-
"moveRight",
|
|
84
|
-
"turnLeft",
|
|
85
|
-
"turnRight",
|
|
86
|
-
"sprint",
|
|
87
|
-
"jump",
|
|
88
|
-
"tabTarget",
|
|
89
|
-
"clearTarget",
|
|
90
|
-
"useAbility",
|
|
91
|
-
"interact",
|
|
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
|
-
}
|
|
98
|
-
function findHotbarSlotActions(input) {
|
|
99
|
-
return Object.keys(input ?? {}).flatMap((action) => {
|
|
100
|
-
const slot = hotbarSlotActionIndex(action);
|
|
101
|
-
return slot === null ? [] : [{ action, slot }];
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
function hotbarIdFor(playable) {
|
|
105
|
-
const declarations = Object.entries(playable.game.inventories ?? {});
|
|
106
|
-
const hud = declarations.find(([, declaration]) => declaration.hud === "hotbar");
|
|
107
|
-
return (hud ?? declarations[0])?.[0] ?? null;
|
|
108
|
-
}
|
|
109
|
-
function executeHotbarSlot(ctx, fromId, hotbarId, slot, yaw, pitch, aimOverride) {
|
|
110
|
-
const stack = ctx.player.inventory.state(hotbarId).slots[slot];
|
|
111
|
-
if (stack === undefined || stack === null)
|
|
112
|
-
return { ok: false, error: `Hotbar slot ${slot + 1} is empty` };
|
|
113
|
-
const result = ctx.item.use.use({
|
|
114
|
-
from: fromId,
|
|
115
|
-
itemId: stack.itemId,
|
|
116
|
-
inventoryId: hotbarId,
|
|
117
|
-
aim: aimOverride ?? { yaw, pitch },
|
|
118
|
-
});
|
|
119
|
-
return result.error === undefined ? { ok: true } : { ok: false, error: result.error };
|
|
120
|
-
}
|
|
121
|
-
function pointerAimFor(ctx, service) {
|
|
122
|
-
const hit = service.worldHit();
|
|
123
|
-
if (hit === null)
|
|
124
|
-
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);
|
|
128
|
-
}
|
|
129
|
-
function pointerContextMenu(ctx, playable, hit) {
|
|
130
|
-
if (hit.entity !== null) {
|
|
131
|
-
const entity = ctx.scene.entity.get(hit.entity);
|
|
132
|
-
const verbs = entity === null ? undefined : playable.content.entityById?.(entity.name)?.verbs;
|
|
133
|
-
return buildContextMenu({ kind: "entity", targetId: hit.entity, verbs, point: hit.point });
|
|
134
|
-
}
|
|
135
|
-
if (hit.object !== null) {
|
|
136
|
-
const verbs = ctx.scene.object.catalog(hit.object)?.verbs;
|
|
137
|
-
return buildContextMenu({ kind: "object", targetId: hit.object, verbs, point: hit.point });
|
|
138
|
-
}
|
|
139
|
-
return null;
|
|
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
|
-
}
|
|
212
|
-
function colorFromId(id) {
|
|
213
|
-
let hash = 0;
|
|
214
|
-
for (let index = 0; index < id.length; index += 1)
|
|
215
|
-
hash = (hash * 31 + id.charCodeAt(index)) >>> 0;
|
|
216
|
-
return `hsl(${hash % 360}, 65%, 55%)`;
|
|
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
|
-
}
|
|
230
|
-
function EntitySprite({ sprite }) {
|
|
231
|
-
const texture = useLoader(THREE.TextureLoader, sprite.url);
|
|
232
|
-
useEffect(() => {
|
|
233
|
-
texture.colorSpace = THREE.SRGBColorSpace;
|
|
234
|
-
texture.anisotropy = 4;
|
|
235
|
-
texture.needsUpdate = true;
|
|
236
|
-
}, [texture]);
|
|
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 }) }));
|
|
238
|
-
}
|
|
239
|
-
function EntityModel({ model, instanceId }) {
|
|
240
|
-
const gltf = useLoader(GLTFLoader, model.url);
|
|
241
|
-
const ctx = useGameContext();
|
|
242
|
-
const material = model.material;
|
|
243
|
-
const scale = model.scale ?? 1;
|
|
244
|
-
const baseY = model.y ?? 0;
|
|
245
|
-
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
|
-
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
|
-
});
|
|
322
|
-
return _jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] });
|
|
323
|
-
}
|
|
324
|
-
function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, selected, onSelect, }) {
|
|
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) => {
|
|
343
|
-
event.stopPropagation();
|
|
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 })] })) }));
|
|
370
|
-
}
|
|
371
|
-
function GroundPlane() {
|
|
372
|
-
const geometry = useMemo(() => {
|
|
373
|
-
const next = new THREE.PlaneGeometry(GROUND_SIZE, GROUND_SIZE, GROUND_SEGMENTS, GROUND_SEGMENTS);
|
|
374
|
-
const positions = next.attributes.position;
|
|
375
|
-
for (let index = 0; index < positions.count; index += 1) {
|
|
376
|
-
const x = positions.getX(index);
|
|
377
|
-
const y = positions.getY(index);
|
|
378
|
-
const height = Math.sin(x * 0.16) * 0.12 +
|
|
379
|
-
Math.cos(y * 0.11) * 0.1 +
|
|
380
|
-
Math.sin((x + y) * 0.05) * 0.16;
|
|
381
|
-
positions.setZ(index, height);
|
|
382
|
-
}
|
|
383
|
-
next.computeVertexNormals();
|
|
384
|
-
return next;
|
|
385
|
-
}, []);
|
|
386
|
-
return (_jsx("mesh", { "rotation-x": -Math.PI / 2, geometry: geometry, children: _jsx("meshStandardMaterial", { color: "#283729", roughness: 0.9, metalness: 0 }) }));
|
|
387
|
-
}
|
|
388
|
-
function RockField() {
|
|
389
|
-
const rocks = useMemo(() => Array.from({ length: 18 }, (_, index) => {
|
|
390
|
-
const angle = index * 2.399963229728653;
|
|
391
|
-
const radius = 10 + ((index * 17) % 58);
|
|
392
|
-
return {
|
|
393
|
-
id: `rock-${index}`,
|
|
394
|
-
x: Math.cos(angle) * radius,
|
|
395
|
-
z: Math.sin(angle) * radius,
|
|
396
|
-
scale: 0.45 + ((index * 13) % 11) / 10,
|
|
397
|
-
rotation: angle,
|
|
398
|
-
};
|
|
399
|
-
}), []);
|
|
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))) }));
|
|
401
|
-
}
|
|
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, }) {
|
|
408
|
-
const ctx = useGameContext();
|
|
409
|
-
const entities = useSceneEntities();
|
|
410
|
-
const objects = useSceneObjects();
|
|
411
|
-
const player = usePlayer();
|
|
412
|
-
const targetId = useTarget(player.userId);
|
|
413
|
-
const controlledId = ctx.player.possession.active(player.userId);
|
|
414
|
-
const handleSelect = (entity) => {
|
|
415
|
-
const relation = ctx.scene.entity.canReceive(entity.id, "damage") === null ? "hostile" : "friendly";
|
|
416
|
-
ctx.scene.entity.setTarget(controlledId, relation === "hostile" || entity.role === "npc" ? entity.id : null);
|
|
417
|
-
};
|
|
418
|
-
return (_jsxs(_Fragment, { children: [entities
|
|
419
|
-
.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) => {
|
|
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));
|
|
429
|
-
})] }));
|
|
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
|
-
}
|
|
434
|
-
function RemotePlayers({ rows }) {
|
|
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))) }));
|
|
436
|
-
}
|
|
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);
|
|
440
|
-
const motionRef = useRef(createPlayerMotionState());
|
|
441
|
-
const voxelBodyRef = useRef(null);
|
|
442
|
-
const solidCacheRef = useRef({ count: -1, set: new Set() });
|
|
443
|
-
const hasReportedTickError = useRef(false);
|
|
444
|
-
const repeatFiredAtRef = useRef(new Map());
|
|
445
|
-
const slotActions = useMemo(() => findHotbarSlotActions(playable.game.input), [playable]);
|
|
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]);
|
|
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();
|
|
484
|
-
try {
|
|
485
|
-
let endPhase = devtools.profile.begin("time+input");
|
|
486
|
-
const dt = Math.min(rawDt, 0.05);
|
|
487
|
-
const gameDt = ctx.time.advance(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();
|
|
494
|
-
const playerId = ctx.player.possession.active(ctx.player.userId);
|
|
495
|
-
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) {
|
|
499
|
-
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
|
-
}
|
|
589
|
-
endPhase();
|
|
590
|
-
}
|
|
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");
|
|
605
|
-
if (tracker.wasPressed("tabTarget")) {
|
|
606
|
-
if (ctx.game.commands.has("target.cycle"))
|
|
607
|
-
ctx.game.commands.run("target.cycle", {});
|
|
608
|
-
else
|
|
609
|
-
ctx.scene.entity.cycleTarget(playerId, { filter: "hostile" });
|
|
610
|
-
}
|
|
611
|
-
if (tracker.wasPressed("clearTarget")) {
|
|
612
|
-
if (ctx.game.commands.has("target.clear"))
|
|
613
|
-
ctx.game.commands.run("target.clear", {});
|
|
614
|
-
else
|
|
615
|
-
ctx.scene.entity.setTarget(playerId, null);
|
|
616
|
-
}
|
|
617
|
-
if (pingCommand !== undefined && tracker.wasPressed("ping")) {
|
|
618
|
-
const hit = pointerService.worldHit();
|
|
619
|
-
if (hit !== null && ctx.game.commands.has(pingCommand)) {
|
|
620
|
-
ctx.game.commands.run(pingCommand, {
|
|
621
|
-
point: hit.point,
|
|
622
|
-
entity: hit.entity,
|
|
623
|
-
object: hit.object,
|
|
624
|
-
normal: hit.normal,
|
|
625
|
-
});
|
|
626
|
-
}
|
|
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();
|
|
643
|
-
for (const action of Object.keys(playable.game.input ?? {})) {
|
|
644
|
-
const pressed = tracker.wasPressed(action);
|
|
645
|
-
if (action === "ping" && pingCommand !== undefined)
|
|
646
|
-
continue;
|
|
647
|
-
if (action === "interact") {
|
|
648
|
-
if (!pressed)
|
|
649
|
-
continue;
|
|
650
|
-
const prompts = playable.prompts?.(ctx);
|
|
651
|
-
const focus = prompts === undefined ? null : ctx.scene.entity.get(playerId);
|
|
652
|
-
if (prompts !== undefined && focus !== null) {
|
|
653
|
-
const active = resolveActivePrompt({ x: focus.position[0], z: focus.position[2] }, prompts);
|
|
654
|
-
if (active !== null && active.prompt.invoke !== null) {
|
|
655
|
-
ctx.game.commands.run(active.prompt.invoke.name, active.prompt.invoke.input);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
continue;
|
|
659
|
-
}
|
|
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);
|
|
664
|
-
}
|
|
665
|
-
if (hotbarId !== null) {
|
|
666
|
-
for (const { action, slot } of slotActions) {
|
|
667
|
-
if (!tracker.wasPressed(action))
|
|
668
|
-
continue;
|
|
669
|
-
const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
|
|
670
|
-
if (!result.ok)
|
|
671
|
-
console.warn(`[jgengine:item-use] ${result.error}`);
|
|
672
|
-
}
|
|
673
|
-
const usePrimary = tracker.wasPressed("useAbility") || (primaryClickRef.current && hotbarId !== null);
|
|
674
|
-
if (usePrimary) {
|
|
675
|
-
primaryClickRef.current = false;
|
|
676
|
-
const slots = ctx.player.inventory.state(hotbarId).slots;
|
|
677
|
-
const preferred = playable.hotbarSelection?.() ?? -1;
|
|
678
|
-
const slot = preferred >= 0 && slots[preferred] !== null && slots[preferred] !== undefined
|
|
679
|
-
? preferred
|
|
680
|
-
: slots.findIndex((stack) => stack !== null);
|
|
681
|
-
if (slot >= 0) {
|
|
682
|
-
const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
|
|
683
|
-
if (!result.ok)
|
|
684
|
-
console.warn(`[jgengine:item-use] ${result.error}`);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
tracker.endFrame();
|
|
689
|
-
endPhase();
|
|
690
|
-
const serverId = serverIdRef.current;
|
|
691
|
-
if (multiplayer !== null && serverId !== null) {
|
|
692
|
-
endPhase = devtools.profile.begin("presence");
|
|
693
|
-
const focus = ctx.scene.entity.get(playerId);
|
|
694
|
-
if (focus !== null) {
|
|
695
|
-
multiplayer.backend.presenceSync.syncPose(serverId, {
|
|
696
|
-
x: focus.position[0],
|
|
697
|
-
y: focus.position[1],
|
|
698
|
-
z: focus.position[2],
|
|
699
|
-
rotationY: focus.rotationY,
|
|
700
|
-
rotationPitch: pitchRef.current,
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
endPhase();
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
catch (error) {
|
|
707
|
-
if (!hasReportedTickError.current) {
|
|
708
|
-
hasReportedTickError.current = true;
|
|
709
|
-
onRuntimeError(error, "tick");
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
devtools.frame.record({ frameMs: rawDt * 1000, simMs: performance.now() - simStart });
|
|
713
|
-
}, GAME_SIM_FRAME_PRIORITY);
|
|
714
|
-
return null;
|
|
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
|
-
}
|
|
765
|
-
class GameUiErrorBoundary extends Component {
|
|
766
|
-
state = { failed: false };
|
|
767
|
-
static getDerivedStateFromError() {
|
|
768
|
-
return { failed: true };
|
|
769
|
-
}
|
|
770
|
-
componentDidCatch(error, info) {
|
|
771
|
-
this.props.onRuntimeError(error, "ui-render", info.componentStack ?? undefined);
|
|
772
|
-
}
|
|
773
|
-
render() {
|
|
774
|
-
if (this.state.failed)
|
|
775
|
-
return null;
|
|
776
|
-
return this.props.children;
|
|
777
|
-
}
|
|
778
|
-
}
|
|
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);
|
|
805
|
-
if (diagnostics.length === 0)
|
|
806
|
-
return null;
|
|
807
|
-
const latest = diagnostics[diagnostics.length - 1];
|
|
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" })] })] }));
|
|
818
|
-
}
|
|
819
|
-
const POSTER_SETTLE_SECONDS = 1.6;
|
|
65
|
+
export { applyMotionImpulses } from "@jgengine/core/runtime/motionIntents";
|
|
66
|
+
export { nearbyObstacles } from "@jgengine/core/movement/movementModel";
|
|
67
|
+
export { resolvePhysicsTuning } from "@jgengine/core/movement/playerMovement";
|
|
68
|
+
export { hasEnvironmentTerrain } from "@jgengine/core/world/terrain";
|
|
820
69
|
export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null, poster = false, onContextReady, }) {
|
|
821
70
|
const multiplayer = useMemo(() => (rawMultiplayer === null ? null : withDevtoolsLatency(rawMultiplayer)), [rawMultiplayer]);
|
|
822
71
|
const devtoolsEnabled = playable.devtools !== false && !poster;
|
|
823
72
|
const [devtoolsOpen, setDevtoolsOpen] = useState(false);
|
|
73
|
+
const devtoolsOpenRef = useRef(false);
|
|
74
|
+
devtoolsOpenRef.current = devtoolsOpen;
|
|
75
|
+
useEffect(() => installAgentBridge({
|
|
76
|
+
playable,
|
|
77
|
+
devtoolsEnabled,
|
|
78
|
+
isDevtoolsOpen: () => devtoolsOpenRef.current,
|
|
79
|
+
setDevtoolsOpen,
|
|
80
|
+
}), [playable, devtoolsEnabled]);
|
|
824
81
|
const [posterFrozen, setPosterFrozen] = useState(false);
|
|
825
82
|
const posterSettledRef = useRef(false);
|
|
826
83
|
const [ctx, setCtx] = useState(null);
|
|
@@ -836,7 +93,6 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
836
93
|
const pointerAxisRef = useRef(null);
|
|
837
94
|
const marqueeStartRef = useRef(null);
|
|
838
95
|
const f2HeldRef = useRef(false);
|
|
839
|
-
const f2ChordedRef = useRef(false);
|
|
840
96
|
const pointerService = useMemo(() => createPointerService(), []);
|
|
841
97
|
const selection = useMemo(() => createSelectionSet(), [playable]);
|
|
842
98
|
const [marquee, setMarquee] = useState(null);
|
|
@@ -864,28 +120,63 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
864
120
|
pointerAxisRef.current = { ...state, active: false };
|
|
865
121
|
};
|
|
866
122
|
const touchScheme = useMemo(() => deriveTouchScheme(playable.game.input, {
|
|
867
|
-
reserved:
|
|
123
|
+
reserved: resolveRigKind(playable.camera) === "none" || playable.presentation === "hud"
|
|
124
|
+
? EMPTY_RESERVED
|
|
125
|
+
: RESERVED_INPUT_ACTIONS,
|
|
868
126
|
firstPerson: resolveRigKind(playable.camera) === "first",
|
|
869
127
|
config: playable.touch,
|
|
870
128
|
}), [playable]);
|
|
129
|
+
const touchStyle = useTouchStyle(settingsStore, touchScheme?.style ?? DEFAULT_TOUCH_STYLE);
|
|
871
130
|
const { coarsePointer, portrait, compact } = useDisplayProfile();
|
|
872
131
|
const touchSink = useMemo(() => ({ onCodeDown: (code) => tracker.handleDown(code), onCodeUp: (code) => tracker.handleUp(code) }), [tracker]);
|
|
873
|
-
const
|
|
132
|
+
const gateRef = useRef(false);
|
|
133
|
+
const orientationPlatform = coarsePointer ? "mobile" : "desktop";
|
|
134
|
+
const orientationRequirement = useMemo(() => resolveOrientationRequirement(playable.orientation, orientationPlatform), [playable, orientationPlatform]);
|
|
135
|
+
const liveOrientation = portrait ? "portrait" : "landscape";
|
|
136
|
+
const orientationGate = !poster && coarsePointer && orientationGateActive(orientationRequirement, liveOrientation);
|
|
137
|
+
const orientationHint = !poster && coarsePointer && orientationHintActive(orientationRequirement, liveOrientation);
|
|
138
|
+
gateRef.current = orientationGate;
|
|
139
|
+
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;
|
|
140
|
+
const audioEngine = useMemo(() => createAudioEngine({
|
|
141
|
+
sounds: playable.audio?.sounds,
|
|
142
|
+
buses: playable.audio?.buses,
|
|
143
|
+
music: playable.audio?.music,
|
|
144
|
+
musicBus: playable.audio?.musicBus,
|
|
145
|
+
}), [playable]);
|
|
874
146
|
useEffect(() => () => audioEngine.dispose(), [audioEngine]);
|
|
875
147
|
useEffect(() => {
|
|
876
148
|
if (ctx === null)
|
|
877
149
|
return;
|
|
878
|
-
return ctx.game.events
|
|
879
|
-
audioEngine.playOneShot(sound, at === undefined ? undefined : { x: at[0], y: at[1], z: at[2] });
|
|
880
|
-
});
|
|
150
|
+
return attachAudioEventWire(ctx.game.events, audioEngine);
|
|
881
151
|
}, [ctx, audioEngine]);
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (typeof document === "undefined")
|
|
154
|
+
return;
|
|
155
|
+
document.documentElement.dataset.jgPresentation = playable.presentation ?? "3d";
|
|
156
|
+
return () => {
|
|
157
|
+
delete document.documentElement.dataset.jgPresentation;
|
|
158
|
+
};
|
|
159
|
+
}, [playable]);
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (ctx === null || typeof document === "undefined")
|
|
162
|
+
return;
|
|
163
|
+
const hud = resolveRigKind(playable.camera) === "none" || playable.presentation === "hud";
|
|
164
|
+
if (!hud)
|
|
165
|
+
return;
|
|
166
|
+
const frame = requestAnimationFrame(() => {
|
|
167
|
+
document.documentElement.dataset.jgFrameReady = "1";
|
|
168
|
+
});
|
|
169
|
+
return () => {
|
|
170
|
+
cancelAnimationFrame(frame);
|
|
171
|
+
delete document.documentElement.dataset.jgFrameReady;
|
|
172
|
+
};
|
|
173
|
+
}, [ctx, playable]);
|
|
882
174
|
const userId = multiplayer?.userId ?? DEV_USER_ID;
|
|
883
175
|
const reportRuntimeError = (error, phase, componentStack) => {
|
|
884
176
|
const diagnostic = logRuntimeError(error, phase, componentStack);
|
|
885
177
|
setDiagnostics((current) => [...current.slice(-4), { ...diagnostic, id: Date.now() + current.length }]);
|
|
886
178
|
};
|
|
887
179
|
useEffect(() => {
|
|
888
|
-
playable.game.scene.clear();
|
|
889
180
|
setDiagnostics([]);
|
|
890
181
|
try {
|
|
891
182
|
const context = createGameContext({
|
|
@@ -903,98 +194,10 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
903
194
|
setCtx(null);
|
|
904
195
|
}
|
|
905
196
|
return () => {
|
|
906
|
-
playable.game.scene.clear();
|
|
907
197
|
setCtx(null);
|
|
908
198
|
};
|
|
909
199
|
}, [playable, userId]);
|
|
910
|
-
|
|
911
|
-
if (ctx === null || multiplayer === null)
|
|
912
|
-
return;
|
|
913
|
-
let disposed = false;
|
|
914
|
-
const cleanups = [];
|
|
915
|
-
void multiplayer.backend.transport
|
|
916
|
-
.joinServer({ gameId: multiplayer.gameId })
|
|
917
|
-
.then((joined) => {
|
|
918
|
-
if (disposed) {
|
|
919
|
-
void multiplayer.backend.transport.leaveServer({ serverId: joined.serverId });
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
serverIdRef.current = joined.serverId;
|
|
923
|
-
cleanups.push(multiplayer.backend.presenceSync.subscribe(joined.serverId, (rows) => {
|
|
924
|
-
setRemotePlayers(rows.filter((row) => row.userId !== multiplayer.userId));
|
|
925
|
-
}));
|
|
926
|
-
const seen = new Set();
|
|
927
|
-
let injecting = false;
|
|
928
|
-
for (const action of multiplayer.feedActions) {
|
|
929
|
-
cleanups.push(ctx.game.feed.subscribe(action, (entry) => {
|
|
930
|
-
if (injecting)
|
|
931
|
-
return;
|
|
932
|
-
void multiplayer.backend
|
|
933
|
-
.pushFeedEntry({
|
|
934
|
-
serverId: joined.serverId,
|
|
935
|
-
action,
|
|
936
|
-
entry: { at: entry.at, data: entry.data, from: multiplayer.userId },
|
|
937
|
-
})
|
|
938
|
-
.catch(() => undefined);
|
|
939
|
-
}));
|
|
940
|
-
const remoteUnsub = multiplayer.backend.feeds?.subscribeFeed({ serverId: joined.serverId, action }, (view) => {
|
|
941
|
-
for (const raw of view.entries) {
|
|
942
|
-
const remote = raw;
|
|
943
|
-
if (typeof remote.from !== "string" || remote.from === multiplayer.userId)
|
|
944
|
-
continue;
|
|
945
|
-
const key = `${action}|${remote.from}|${remote.at ?? 0}`;
|
|
946
|
-
if (seen.has(key))
|
|
947
|
-
continue;
|
|
948
|
-
seen.add(key);
|
|
949
|
-
injecting = true;
|
|
950
|
-
ctx.game.feed.push(action, remote.data);
|
|
951
|
-
injecting = false;
|
|
952
|
-
}
|
|
953
|
-
});
|
|
954
|
-
if (remoteUnsub !== undefined)
|
|
955
|
-
cleanups.push(remoteUnsub);
|
|
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
|
-
}
|
|
984
|
-
})
|
|
985
|
-
.catch(() => undefined);
|
|
986
|
-
return () => {
|
|
987
|
-
disposed = true;
|
|
988
|
-
for (const cleanup of cleanups)
|
|
989
|
-
cleanup();
|
|
990
|
-
const serverId = serverIdRef.current;
|
|
991
|
-
serverIdRef.current = null;
|
|
992
|
-
setRemotePlayers([]);
|
|
993
|
-
if (serverId !== null) {
|
|
994
|
-
void multiplayer.backend.transport.leaveServer({ serverId }).catch(() => undefined);
|
|
995
|
-
}
|
|
996
|
-
};
|
|
997
|
-
}, [ctx, multiplayer]);
|
|
200
|
+
useShellMultiplayerSync(ctx, multiplayer, playable, serverIdRef, setRemotePlayers);
|
|
998
201
|
useEffect(() => {
|
|
999
202
|
wrapperRef.current?.focus();
|
|
1000
203
|
}, [ctx]);
|
|
@@ -1010,32 +213,34 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1010
213
|
const rigKind = resolveRigKind(cameraConfig);
|
|
1011
214
|
if (rigKind === "none" || playable.presentation === "hud") {
|
|
1012
215
|
const GameUI = playable.GameUI;
|
|
1013
|
-
return (
|
|
1014
|
-
if (event.code === "F2"
|
|
216
|
+
return (_jsx("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
|
|
217
|
+
if (event.code === "F2") {
|
|
1015
218
|
event.preventDefault();
|
|
1016
219
|
f2HeldRef.current = true;
|
|
1017
|
-
f2ChordedRef.current = false;
|
|
1018
220
|
return;
|
|
1019
221
|
}
|
|
1020
222
|
if (f2HeldRef.current) {
|
|
1021
|
-
|
|
223
|
+
if (event.code === "KeyD" && devtoolsEnabled) {
|
|
224
|
+
event.preventDefault();
|
|
225
|
+
setDevtoolsOpen((current) => !current);
|
|
226
|
+
}
|
|
1022
227
|
return;
|
|
1023
228
|
}
|
|
1024
229
|
if (event.code === "Tab" || event.code === "Space")
|
|
1025
230
|
event.preventDefault();
|
|
1026
|
-
|
|
231
|
+
if (playControlsActive(ctx))
|
|
232
|
+
tracker.handleDown(event.code);
|
|
1027
233
|
}, onKeyUp: (event) => {
|
|
1028
|
-
if (event.code === "F2"
|
|
234
|
+
if (event.code === "F2") {
|
|
1029
235
|
f2HeldRef.current = false;
|
|
1030
|
-
if (!f2ChordedRef.current)
|
|
1031
|
-
setDevtoolsOpen((current) => !current);
|
|
1032
236
|
return;
|
|
1033
237
|
}
|
|
1034
|
-
|
|
238
|
+
if (playControlsActive(ctx))
|
|
239
|
+
tracker.handleUp(event.code);
|
|
1035
240
|
}, onBlur: () => {
|
|
1036
241
|
f2HeldRef.current = false;
|
|
1037
242
|
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:
|
|
243
|
+
}, 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: _jsxs(GameProvider, { context: ctx, children: [_jsx(GamePhaseStamp, {}), _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
244
|
}
|
|
1040
245
|
const firstPerson = rigKind === "first";
|
|
1041
246
|
const showReticle = (firstPerson && playable.camera?.firstPerson?.reticle !== false) || rigKind === "shoulder";
|
|
@@ -1047,23 +252,43 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1047
252
|
? "health"
|
|
1048
253
|
: worldBars.statId ?? "health";
|
|
1049
254
|
const barsRoles = worldBars === undefined || worldBars === true || worldBars === false ? undefined : worldBars.roles;
|
|
255
|
+
const barsMaxDistance = worldBars === undefined || worldBars === true || worldBars === false
|
|
256
|
+
? undefined
|
|
257
|
+
: worldBars.maxDistance;
|
|
258
|
+
const nameplates = playable.nameplates;
|
|
259
|
+
const nameplatesStatId = nameplates === undefined || nameplates === false
|
|
260
|
+
? null
|
|
261
|
+
: nameplates === true
|
|
262
|
+
? "health"
|
|
263
|
+
: nameplates.statId ?? "health";
|
|
264
|
+
const nameplatesRoles = nameplates === undefined || nameplates === true || nameplates === false ? undefined : nameplates.roles;
|
|
265
|
+
const nameplatesMaxDistance = nameplates === undefined || nameplates === true || nameplates === false ? undefined : nameplates.maxDistance;
|
|
1050
266
|
const resolveEntityRole = (entity) => playable.content.entityById?.(entity.name)?.role;
|
|
1051
267
|
const pointer = playable.pointer;
|
|
1052
268
|
const pointerUsesLeft = pointer !== undefined && (pointer.select === true || pointer.moveCommand !== undefined);
|
|
1053
269
|
const selectFilter = pointer?.selectFilter;
|
|
1054
270
|
const worldSky = resolveWorldSky(playable.game.world);
|
|
1055
271
|
const world = playable.game.world;
|
|
272
|
+
const biomeBands = world?.kind === "environment" ? world.terrain?.biomeBands : undefined;
|
|
1056
273
|
const AutoEnvironment = playable.environment ??
|
|
1057
274
|
(world?.kind === "environment"
|
|
1058
275
|
? () => _jsx(EnvironmentScene, { feature: world })
|
|
1059
276
|
: world?.kind === "biomes" || world?.kind === "voxel" || world?.kind === "plots" || world?.kind === "tilemap"
|
|
1060
277
|
? () => _jsx(GridWorldScene, { feature: world })
|
|
1061
278
|
: undefined);
|
|
1062
|
-
const
|
|
279
|
+
const resolvedLook = resolveGameLook({
|
|
280
|
+
look: playable.look,
|
|
281
|
+
lighting: playable.lighting,
|
|
282
|
+
backdrop: playable.backdrop,
|
|
283
|
+
postProcessing: playable.postProcessing,
|
|
284
|
+
hasWorldSky: worldSky !== undefined,
|
|
285
|
+
});
|
|
286
|
+
const cinematicLook = (playable.look ?? "cinematic") !== "flat";
|
|
287
|
+
const backdrop = resolvedLook.backdrop;
|
|
1063
288
|
const backdropSky = backdrop?.sky !== undefined ? resolveSkyDescriptor(backdrop.sky) : undefined;
|
|
1064
289
|
const effectiveSky = backdropSky ?? worldSky;
|
|
1065
290
|
const backgroundColor = backdrop?.background ?? (effectiveSky === undefined ? DEFAULT_BACKGROUND_COLOR : undefined);
|
|
1066
|
-
const lighting =
|
|
291
|
+
const lighting = resolvedLook.lighting;
|
|
1067
292
|
const orthographic = playable.camera?.projection === "orthographic";
|
|
1068
293
|
const localXY = (event) => {
|
|
1069
294
|
const rect = wrapperRef.current?.getBoundingClientRect();
|
|
@@ -1086,13 +311,16 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1086
311
|
}
|
|
1087
312
|
commitSelection(selectWithinRect(candidates, rect));
|
|
1088
313
|
};
|
|
314
|
+
/** DOM UI (menus, HUD buttons) sits in the same wrapper as the canvas — only canvas-targeted clicks are world input. */
|
|
315
|
+
const isWorldPointerTarget = (event) => event.target instanceof HTMLCanvasElement;
|
|
1089
316
|
const handlePointerDown = (event) => {
|
|
1090
|
-
|
|
317
|
+
if (isWorldPointerTarget(event))
|
|
318
|
+
wrapperRef.current?.focus();
|
|
1091
319
|
trackPointerAxis(event);
|
|
1092
320
|
audioEngine.resume();
|
|
1093
321
|
if (contextMenu !== null)
|
|
1094
322
|
setContextMenu(null);
|
|
1095
|
-
if (event.button === 0) {
|
|
323
|
+
if (event.button === 0 && isWorldPointerTarget(event)) {
|
|
1096
324
|
const point = localXY(event);
|
|
1097
325
|
pointerDownRef.current = point;
|
|
1098
326
|
if (pointer?.select === true)
|
|
@@ -1166,6 +394,8 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1166
394
|
const handleContextMenu = (event) => {
|
|
1167
395
|
if (pointer === undefined)
|
|
1168
396
|
return;
|
|
397
|
+
if (!isWorldPointerTarget(event))
|
|
398
|
+
return;
|
|
1169
399
|
event.preventDefault();
|
|
1170
400
|
const hit = pointerService.worldHit();
|
|
1171
401
|
if (hit === null)
|
|
@@ -1218,40 +448,40 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1218
448
|
}));
|
|
1219
449
|
const touchScale = compact ? 0.88 : 1;
|
|
1220
450
|
const dockMounted = !poster &&
|
|
451
|
+
!orientationGate &&
|
|
1221
452
|
coarsePointer &&
|
|
1222
453
|
controlsActive &&
|
|
1223
454
|
touchScheme !== null &&
|
|
1224
455
|
(touchScheme.joystick !== null || touchScheme.buttons.length > 0);
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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: {
|
|
456
|
+
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 ?? [], touchStyle: coarsePointer &&
|
|
457
|
+
touchScheme !== null &&
|
|
458
|
+
(touchScheme.joystick !== null || touchScheme.buttons.length > 0), 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
459
|
"--jg-hud-dock-clearance": `${dockMounted ? touchDockClearance(touchScheme, touchScale) : 0}px`,
|
|
1231
460
|
}, onKeyDown: (event) => {
|
|
1232
|
-
if (event.code === "F2"
|
|
461
|
+
if (event.code === "F2") {
|
|
1233
462
|
event.preventDefault();
|
|
1234
463
|
f2HeldRef.current = true;
|
|
1235
|
-
f2ChordedRef.current = false;
|
|
1236
464
|
return;
|
|
1237
465
|
}
|
|
1238
466
|
if (f2HeldRef.current) {
|
|
1239
|
-
|
|
467
|
+
if (event.code === "KeyD" && devtoolsEnabled) {
|
|
468
|
+
event.preventDefault();
|
|
469
|
+
document.exitPointerLock?.();
|
|
470
|
+
setDevtoolsOpen((current) => !current);
|
|
471
|
+
}
|
|
1240
472
|
return;
|
|
1241
473
|
}
|
|
1242
474
|
if (event.code === "Tab" || event.code === "Space")
|
|
1243
475
|
event.preventDefault();
|
|
1244
|
-
|
|
476
|
+
if (playControlsActive(ctx))
|
|
477
|
+
tracker.handleDown(event.code);
|
|
1245
478
|
}, onKeyUp: (event) => {
|
|
1246
|
-
if (event.code === "F2"
|
|
479
|
+
if (event.code === "F2") {
|
|
1247
480
|
f2HeldRef.current = false;
|
|
1248
|
-
if (!f2ChordedRef.current) {
|
|
1249
|
-
document.exitPointerLock?.();
|
|
1250
|
-
setDevtoolsOpen((current) => !current);
|
|
1251
|
-
}
|
|
1252
481
|
return;
|
|
1253
482
|
}
|
|
1254
|
-
|
|
483
|
+
if (playControlsActive(ctx))
|
|
484
|
+
tracker.handleUp(event.code);
|
|
1255
485
|
}, onBlur: () => {
|
|
1256
486
|
f2HeldRef.current = false;
|
|
1257
487
|
tracker.reset();
|
|
@@ -1265,24 +495,24 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1265
495
|
else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
|
|
1266
496
|
ctx.game.commands.run("ui.hotbarScrollPrev", {});
|
|
1267
497
|
}
|
|
1268
|
-
}, children: [_jsxs(Canvas, { frameloop: poster && posterFrozen ? "demand" : "always", orthographic: orthographic, camera: orthographic
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
498
|
+
}, children: _jsxs(GameViewportProvider, { platforms: playable.platforms, children: [_jsxs(Canvas, { frameloop: poster && posterFrozen ? "demand" : "always", orthographic: orthographic, camera: orthographic
|
|
499
|
+
? {
|
|
500
|
+
zoom: playable.camera?.frustum?.zoom ?? CAMERA_FRUSTUM_DEFAULTS.zoom,
|
|
501
|
+
near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
|
|
502
|
+
far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
|
|
503
|
+
}
|
|
504
|
+
: {
|
|
505
|
+
fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
|
|
506
|
+
near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
|
|
507
|
+
far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
|
|
508
|
+
}, shadows: graphics.shadows, dpr: graphics.dpr, gl: { preserveDrawingBuffer: true }, style: { touchAction: "none" }, children: [backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, cinematicLook ? _jsx(EnvironmentLighting, {}) : 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, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) })) : (_jsx(SkyDaylight, { sky: effectiveSky, bands: effectiveSky === worldSky ? biomeBands : undefined, 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, { ctx: ctx }) : null, barsStatId !== null ? (_jsx(WorldEntityBars, { statId: barsStatId, roles: barsRoles, resolveRole: resolveEntityRole, ...(barsMaxDistance === undefined ? {} : { maxDistance: barsMaxDistance }) })) : null, nameplatesStatId !== null ? (_jsx(WorldNameplates, { statId: nameplatesStatId, roles: nameplatesRoles, resolveRole: resolveEntityRole, ...(nameplatesMaxDistance === undefined ? {} : { maxDistance: nameplatesMaxDistance }) })) : null, _jsx(WorldItems, { config: playable.worldItem }), _jsx(WorldTelegraphs, {}), _jsx(WorldSpellVfx, {}), _jsx(RetainedVfx, {}), _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, viewmodel: playable.viewmodel, pointerControls: pointerUsesLeft, panKeysEnabled: rtsPanKeysEnabled, director: ctx.camera, onDragChange: (dragging) => {
|
|
509
|
+
cameraDraggingRef.current = dragging;
|
|
510
|
+
} }), _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: () => {
|
|
511
|
+
if (posterSettledRef.current)
|
|
512
|
+
return;
|
|
513
|
+
posterSettledRef.current = true;
|
|
514
|
+
setPosterFrozen(true);
|
|
515
|
+
} }), _jsx(DevtoolsRendererProbe, {}), resolvedLook.postProcessing !== undefined && resolvedLook.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: resolvedLook.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: () => {
|
|
516
|
+
primaryClickRef.current = true;
|
|
517
|
+
} })) : null, _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsxs(GameProvider, { context: ctx, children: [_jsx(GamePhaseStamp, {}), _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, style: touchStyle, 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
518
|
}
|