@jgengine/shell 0.10.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 +13 -2
- 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 +0 -12
- package/dist/GamePlayerShell.js +88 -1043
- package/dist/GameUiPreview.js +0 -2
- package/dist/audio/audioEngine.js +30 -8
- package/dist/audio/audioWire.d.ts +22 -0
- package/dist/audio/audioWire.js +19 -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 +9 -2
- package/dist/camera/GameFirstPersonCamera.js +8 -3
- package/dist/camera/GameInspectionCamera.d.ts +1 -1
- package/dist/camera/GameInspectionCamera.js +2 -2
- package/dist/camera/GameOrbitCamera.js +24 -9
- 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 +36 -6
- package/dist/camera/orbitCameraMath.js +56 -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 +3 -3
- package/dist/commandSink.d.ts +8 -3
- package/dist/commandSink.js +8 -3
- package/dist/defineGame.js +25 -6
- package/dist/devtools/ColPanel.d.ts +1 -0
- package/dist/devtools/ColPanel.js +14 -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 +8 -2
- package/dist/devtools/collisionDebugMath.js +24 -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 +23 -7
- package/dist/environment/Daylight.js +106 -13
- package/dist/environment/EnvironmentScene.js +32 -6
- 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 +16 -4
- package/dist/inputSink.js +57 -7
- package/dist/materialOverride.d.ts +10 -0
- package/dist/materialOverride.js +15 -3
- package/dist/multiplayer.js +8 -2
- package/dist/postfx/PostProcessing.js +47 -9
- package/dist/postfx/gradeShader.js +9 -1
- 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/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 +19 -2
- package/dist/terrain/terrainDetailMaterial.js +72 -5
- 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 +201 -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 +49 -2
- package/dist/world/WorldHud.js +87 -2
- 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 +25 -0
- package/dist/world/worldBarSamples.js +38 -0
- package/dist/worldSky.d.ts +5 -0
- package/dist/worldSky.js +11 -0
- package/dist/worldSync.d.ts +2 -1
- package/dist/worldSync.js +2 -1
- 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 -2761
package/dist/GamePlayerShell.js
CHANGED
|
@@ -1,61 +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 { deriveTouchScheme, withTouchCodes } from "@jgengine/core/input/touchScheme";
|
|
9
|
-
import {
|
|
10
|
-
import { resolveActivePrompt } from "@jgengine/core/interaction/proximityPrompt";
|
|
11
|
-
import { aimToPoint } from "@jgengine/core/input/pointer";
|
|
12
|
-
import { eyeHeightFromColliders } from "@jgengine/core/combat/shotOrigin";
|
|
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";
|
|
13
10
|
import { normalizePointerToAxis } from "@jgengine/core/input/pointerAxis";
|
|
14
11
|
import { createSelectionSet, isMarquee, screenRect, selectWithinRect, } from "@jgengine/core/scene/selection";
|
|
15
|
-
import { steerYaw } from "@jgengine/core/movement/steering";
|
|
16
|
-
import { stepPlayerMovement, resolvePlayerMovementTuning } from "@jgengine/core/movement/playerMovement";
|
|
17
12
|
import { createGameContext } from "@jgengine/core/runtime/gameContext";
|
|
18
|
-
import {
|
|
19
|
-
import { attachWorldSync } from "./worldSync.js";
|
|
20
|
-
import { localCommandSink, resolveCommandSink } from "./commandSink.js";
|
|
21
|
-
import { inputFramesEqual, resolveInputSink } from "./inputSink.js";
|
|
22
|
-
import { objectVisualScale } from "@jgengine/core/scene/objectStore";
|
|
23
|
-
import { DEFAULT_PICKUP_RADIUS, WORLD_ITEM_ENTITY_NAME } from "@jgengine/core/game/worldItem";
|
|
24
|
-
import { useGameContext } from "@jgengine/react/provider";
|
|
13
|
+
import { DEFAULT_PICKUP_RADIUS } from "@jgengine/core/game/worldItem";
|
|
25
14
|
import { useDisplayProfile } from "@jgengine/react/display";
|
|
26
15
|
import { HudViewportProvider } from "@jgengine/react/hudViewport";
|
|
27
16
|
import { GameViewportProvider } from "@jgengine/react/gameViewport";
|
|
28
17
|
import { RotateDeviceScreen } from "@jgengine/react/rotateDevice";
|
|
29
|
-
import { useSceneEntities, useSceneObjects, usePlayer, useTarget } from "@jgengine/react/hooks";
|
|
30
18
|
import { GameProvider } from "@jgengine/react/provider";
|
|
31
19
|
import { CAMERA_FRUSTUM_DEFAULTS } from "@jgengine/core/game/playableGame";
|
|
32
20
|
import { createSettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
33
21
|
import { applyBindingOverrides, clearBindingOverride, loadBindingOverrides, saveBindingOverride, } from "@jgengine/core/input/bindingOverrides";
|
|
34
22
|
import { playControlsActive } from "@jgengine/core/game/controlGate";
|
|
35
23
|
import { orientationGateActive, orientationHintActive, resolveOrientationRequirement, } from "@jgengine/core/ui/orientation";
|
|
36
|
-
import { resolveOneShotClip } from "@jgengine/core/game/modelAnimation";
|
|
37
24
|
import { sky as resolveSkyDescriptor } from "@jgengine/core/world/features";
|
|
38
|
-
import {
|
|
39
|
-
import { VERSION } from "@jgengine/core/meta/changelog";
|
|
25
|
+
import { resolveGameLook } from "@jgengine/core/render/lookPreset";
|
|
40
26
|
import { AudioListener, EntityAudioEmitters, ObjectAudioEmitters } from "./audio/AudioComponents.js";
|
|
41
27
|
import { createAudioEngine } from "./audio/audioEngine.js";
|
|
28
|
+
import { attachAudioEventWire } from "./audio/audioWire.js";
|
|
42
29
|
import { PostProcessing } from "./postfx/PostProcessing.js";
|
|
30
|
+
import { EnvironmentLighting } from "./render/EnvironmentLighting.js";
|
|
43
31
|
import { CollisionDebugWorld } from "./devtools/CollisionDebugWorld.js";
|
|
44
|
-
import { collisionDebug } from "./devtools/collisionDebug.js";
|
|
45
32
|
import { DevtoolsOverlay, DevtoolsRendererProbe, withDevtoolsLatency } from "./devtools/DevtoolsOverlay.js";
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
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";
|
|
49
36
|
import { SkyDaylight, TimeOfDayDaylight } from "./environment.js";
|
|
50
37
|
import { resolveSkyLightOwnership, skyEmitsLights } from "./environment/skyLightingPolicy.js";
|
|
51
38
|
import { EnvironmentScene } from "./environment/EnvironmentScene.js";
|
|
52
|
-
import { applyMaterialOverride } from "./materialOverride.js";
|
|
53
39
|
import { PointerProbe } from "./pointer/PointerProbe.js";
|
|
54
|
-
import { applyPaintTextureToMaterials, cacheStandardMaterials, cloneModelScene, createPaintCanvas, disposeClonedMaterials, syncPaintCanvas, } from "./render/modelRender.js";
|
|
55
|
-
import { writeEntityPose } from "./world/entityPose.js";
|
|
56
40
|
import { MarqueeBox, ContextMenuView } from "./pointer/PointerOverlays.js";
|
|
57
|
-
import { createPointerService
|
|
58
|
-
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";
|
|
59
45
|
import { GridWorldScene } from "./world/GridWorldScene.js";
|
|
60
46
|
import { WorldItems } from "./world/WorldItems.js";
|
|
61
47
|
import { OrientationHint } from "./touch/OrientationHint.js";
|
|
@@ -64,894 +50,34 @@ import { BUILT_IN_SETTING_CATEGORIES } from "@jgengine/core/settings/settingsMod
|
|
|
64
50
|
import { SettingsProvider } from "@jgengine/react/settings";
|
|
65
51
|
import { SettingsRuntime } from "./settings/SettingsRuntime.js";
|
|
66
52
|
import { SettingsChrome } from "./settings/SettingsChrome.js";
|
|
67
|
-
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";
|
|
68
62
|
const DEV_USER_ID = "dev-player";
|
|
69
|
-
const TURN_SPEED = 2.4;
|
|
70
63
|
const PRIMARY_CLICK_MOVE_THRESHOLD_PX = 6;
|
|
71
|
-
const GROUND_SIZE = 160;
|
|
72
|
-
const GROUND_SEGMENTS = 80;
|
|
73
64
|
const DEFAULT_BACKGROUND_COLOR = "#14161b";
|
|
74
|
-
const DEFAULT_BACKDROP_FOG_COLOR = "#1a1c22";
|
|
75
|
-
function errorToDiagnostic(error, phase, componentStack) {
|
|
76
|
-
const capturedAt = new Date().toISOString();
|
|
77
|
-
if (error instanceof Error) {
|
|
78
|
-
return { phase, message: error.message, stack: error.stack, componentStack, capturedAt };
|
|
79
|
-
}
|
|
80
|
-
return { phase, message: typeof error === "string" ? error : JSON.stringify(error), componentStack, capturedAt };
|
|
81
|
-
}
|
|
82
|
-
function logRuntimeError(error, phase, componentStack) {
|
|
83
|
-
const diagnostic = errorToDiagnostic(error, phase, componentStack);
|
|
84
|
-
console.error(`[jgengine:${phase}] ${diagnostic.message}`, error);
|
|
85
|
-
return diagnostic;
|
|
86
|
-
}
|
|
87
|
-
const RESERVED_INPUT_ACTIONS = new Set([
|
|
88
|
-
"moveForward",
|
|
89
|
-
"moveBack",
|
|
90
|
-
"moveLeft",
|
|
91
|
-
"moveRight",
|
|
92
|
-
"turnLeft",
|
|
93
|
-
"turnRight",
|
|
94
|
-
"sprint",
|
|
95
|
-
"jump",
|
|
96
|
-
"tabTarget",
|
|
97
|
-
"clearTarget",
|
|
98
|
-
"useAbility",
|
|
99
|
-
"interact",
|
|
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 = [];
|
|
105
|
-
const SHELL_MOVEMENT_ACTIONS = ["moveForward", "moveBack", "moveLeft", "moveRight", "jump"];
|
|
106
|
-
function shellDrivesPlayerPose(input) {
|
|
107
|
-
const bound = input ?? {};
|
|
108
|
-
return SHELL_MOVEMENT_ACTIONS.some((action) => action in bound);
|
|
109
|
-
}
|
|
110
|
-
function findHotbarSlotActions(input) {
|
|
111
|
-
return Object.keys(input ?? {}).flatMap((action) => {
|
|
112
|
-
const slot = hotbarSlotActionIndex(action);
|
|
113
|
-
return slot === null ? [] : [{ action, slot }];
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
function hotbarIdFor(playable) {
|
|
117
|
-
const declarations = Object.entries(playable.game.inventories ?? {});
|
|
118
|
-
const hud = declarations.find(([, declaration]) => declaration.hud === "hotbar");
|
|
119
|
-
return (hud ?? declarations[0])?.[0] ?? null;
|
|
120
|
-
}
|
|
121
|
-
function executeHotbarSlot(ctx, fromId, hotbarId, slot, yaw, pitch, aimOverride) {
|
|
122
|
-
const stack = ctx.player.inventory.state(hotbarId).slots[slot];
|
|
123
|
-
if (stack === undefined || stack === null)
|
|
124
|
-
return { ok: false, error: `Hotbar slot ${slot + 1} is empty` };
|
|
125
|
-
const result = ctx.item.use.use({
|
|
126
|
-
from: fromId,
|
|
127
|
-
itemId: stack.itemId,
|
|
128
|
-
inventoryId: hotbarId,
|
|
129
|
-
aim: aimOverride ?? { yaw, pitch },
|
|
130
|
-
});
|
|
131
|
-
return result.error === undefined ? { ok: true } : { ok: false, error: result.error };
|
|
132
|
-
}
|
|
133
|
-
function pointerAimFor(ctx, service) {
|
|
134
|
-
const hit = service.worldHit();
|
|
135
|
-
if (hit === null)
|
|
136
|
-
return undefined;
|
|
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);
|
|
143
|
-
}
|
|
144
|
-
function pointerContextMenu(ctx, playable, hit) {
|
|
145
|
-
if (hit.entity !== null) {
|
|
146
|
-
const entity = ctx.scene.entity.get(hit.entity);
|
|
147
|
-
const verbs = entity === null ? undefined : playable.content.entityById?.(entity.name)?.verbs;
|
|
148
|
-
return buildContextMenu({ kind: "entity", targetId: hit.entity, verbs, point: hit.point });
|
|
149
|
-
}
|
|
150
|
-
if (hit.object !== null) {
|
|
151
|
-
const verbs = ctx.scene.object.catalog(hit.object)?.verbs;
|
|
152
|
-
return buildContextMenu({ kind: "object", targetId: hit.object, verbs, point: hit.point });
|
|
153
|
-
}
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
/** Actions from `input` currently held down, for `ctx.input.publish` (#164.1); includes reserved movement/jump actions. */
|
|
157
|
-
export function heldActionsFor(tracker, actions) {
|
|
158
|
-
return actions.filter((action) => tracker.isDown(action));
|
|
159
|
-
}
|
|
160
|
-
/** Whether a bound action should fire this frame: on press, or on repeat interval while held (shared by `FrameDriver` and `HudOnlyDriver`). */
|
|
161
|
-
export function shouldFireBoundAction(tracker, action, input, repeatFiredAt, now) {
|
|
162
|
-
return shouldDispatchAction({
|
|
163
|
-
pressed: tracker.wasPressed(action),
|
|
164
|
-
down: tracker.isDown(action),
|
|
165
|
-
repeatMs: actionRepeatMs(input?.[action]),
|
|
166
|
-
lastFiredAt: repeatFiredAt.get(action) ?? null,
|
|
167
|
-
now,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
/** Resolves and runs the command bound to `action` via the shell's action→command convention (shared by `FrameDriver` and `HudOnlyDriver`). */
|
|
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);
|
|
173
|
-
if (command !== null)
|
|
174
|
-
sink.run(command, { yaw, pitch, aim });
|
|
175
|
-
}
|
|
176
65
|
export { applyMotionImpulses } from "@jgengine/core/runtime/motionIntents";
|
|
177
66
|
export { nearbyObstacles } from "@jgengine/core/movement/movementModel";
|
|
178
67
|
export { resolvePhysicsTuning } from "@jgengine/core/movement/playerMovement";
|
|
179
68
|
export { hasEnvironmentTerrain } from "@jgengine/core/world/terrain";
|
|
180
|
-
/** The world's declared sky, when its world feature is an environment with one (#196.1). */
|
|
181
|
-
export function resolveWorldSky(world) {
|
|
182
|
-
return world?.kind === "environment" ? world.sky : undefined;
|
|
183
|
-
}
|
|
184
|
-
function colorFromId(id) {
|
|
185
|
-
let hash = 0;
|
|
186
|
-
for (let index = 0; index < id.length; index += 1)
|
|
187
|
-
hash = (hash * 31 + id.charCodeAt(index)) >>> 0;
|
|
188
|
-
return `hsl(${hash % 360}, 65%, 55%)`;
|
|
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
|
-
}
|
|
194
|
-
function ConfiguredLighting({ lighting }) {
|
|
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: [
|
|
196
|
-
lighting.hemisphere.skyColor ?? "#bfe3ff",
|
|
197
|
-
lighting.hemisphere.groundColor ?? "#4c6b34",
|
|
198
|
-
lighting.hemisphere.intensity ?? 0.55,
|
|
199
|
-
] })) : null, (lighting.directional ?? []).map((entry, index) => (_jsx(DirectionalShadowLight, { entry: entry }, index)))] }));
|
|
200
|
-
}
|
|
201
|
-
function BackdropFog({ fog }) {
|
|
202
|
-
if (fog === undefined)
|
|
203
|
-
return null;
|
|
204
|
-
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] }));
|
|
205
|
-
}
|
|
206
|
-
function EntitySprite({ sprite }) {
|
|
207
|
-
const texture = useLoader(THREE.TextureLoader, sprite.url);
|
|
208
|
-
useEffect(() => {
|
|
209
|
-
texture.colorSpace = THREE.SRGBColorSpace;
|
|
210
|
-
texture.anisotropy = 4;
|
|
211
|
-
texture.needsUpdate = true;
|
|
212
|
-
}, [texture]);
|
|
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 }) }));
|
|
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
|
-
}
|
|
271
|
-
function EntityModel({ model, instanceId }) {
|
|
272
|
-
const gltf = useLoader(GLTFLoader, model.url, (loader) => {
|
|
273
|
-
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
274
|
-
});
|
|
275
|
-
const ctx = useGameContext();
|
|
276
|
-
const material = model.material;
|
|
277
|
-
const baseY = model.y ?? 0;
|
|
278
|
-
const dims = model.dims;
|
|
279
|
-
const scene = useMemo(() => {
|
|
280
|
-
const cloned = cloneModelScene(gltf.scene);
|
|
281
|
-
if (material !== undefined)
|
|
282
|
-
applyMaterialOverride(cloned, material, { clone: false });
|
|
283
|
-
return cloned;
|
|
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];
|
|
306
|
-
useEffect(() => () => {
|
|
307
|
-
disposeClonedMaterials(scene);
|
|
308
|
-
}, [scene]);
|
|
309
|
-
const animation = model.animation;
|
|
310
|
-
const mixerRef = useRef(null);
|
|
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);
|
|
317
|
-
useEffect(() => {
|
|
318
|
-
if (animation === undefined || gltf.animations.length === 0) {
|
|
319
|
-
mixerRef.current = null;
|
|
320
|
-
stateActionsRef.current = null;
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
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
|
-
}
|
|
402
|
-
const clip = (animation.clip !== undefined ? THREE.AnimationClip.findByName(gltf.animations, animation.clip) : undefined) ??
|
|
403
|
-
gltf.animations[0];
|
|
404
|
-
const action = mixer.clipAction(clip);
|
|
405
|
-
action.setLoop(animation.loop === false ? THREE.LoopOnce : THREE.LoopRepeat, Infinity);
|
|
406
|
-
action.clampWhenFinished = animation.loop === false;
|
|
407
|
-
action.timeScale = animation.timeScale ?? 1;
|
|
408
|
-
action.enabled = true;
|
|
409
|
-
action.paused = animation.paused === true;
|
|
410
|
-
action.play();
|
|
411
|
-
if (animation.time !== undefined)
|
|
412
|
-
action.time = animation.time;
|
|
413
|
-
mixer.update(0);
|
|
414
|
-
mixerRef.current = mixer;
|
|
415
|
-
animationPausedRef.current = animation.paused === true;
|
|
416
|
-
return () => {
|
|
417
|
-
mixer.stopAllAction();
|
|
418
|
-
mixerRef.current = null;
|
|
419
|
-
};
|
|
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]);
|
|
453
|
-
const paintCanvasRef = useRef(null);
|
|
454
|
-
const paintDrawnCountRef = useRef(0);
|
|
455
|
-
const paintVersionRef = useRef(-1);
|
|
456
|
-
const materialCacheRef = useRef(null);
|
|
457
|
-
useEffect(() => {
|
|
458
|
-
paintCanvasRef.current = null;
|
|
459
|
-
paintDrawnCountRef.current = 0;
|
|
460
|
-
paintVersionRef.current = -1;
|
|
461
|
-
materialCacheRef.current = null;
|
|
462
|
-
}, [scene]);
|
|
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
|
-
}
|
|
496
|
-
if (mixerRef.current !== null && !animationPausedRef.current)
|
|
497
|
-
mixerRef.current.update(delta);
|
|
498
|
-
if (instanceId === undefined)
|
|
499
|
-
return;
|
|
500
|
-
const paint = ctx.scene.entity.paint;
|
|
501
|
-
const version = paint.version(instanceId);
|
|
502
|
-
if (version === paintVersionRef.current)
|
|
503
|
-
return;
|
|
504
|
-
paintVersionRef.current = version;
|
|
505
|
-
const strokes = paint.strokes(instanceId);
|
|
506
|
-
const cache = cacheStandardMaterials(scene, materialCacheRef.current);
|
|
507
|
-
materialCacheRef.current = cache;
|
|
508
|
-
if (paintCanvasRef.current === null) {
|
|
509
|
-
if (strokes.length === 0)
|
|
510
|
-
return;
|
|
511
|
-
const seed = cache.materials[0];
|
|
512
|
-
if (seed === undefined)
|
|
513
|
-
return;
|
|
514
|
-
const paintCanvas = createPaintCanvas(seed);
|
|
515
|
-
paintCanvasRef.current = paintCanvas;
|
|
516
|
-
applyPaintTextureToMaterials(cache.materials, paintCanvas);
|
|
517
|
-
}
|
|
518
|
-
paintDrawnCountRef.current = syncPaintCanvas(paintCanvasRef.current, cache.seedColor, strokes, paintDrawnCountRef.current);
|
|
519
|
-
});
|
|
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}`)))] }));
|
|
521
|
-
}
|
|
522
|
-
function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, selected, onSelect, }) {
|
|
523
|
-
const groupRef = useRef(null);
|
|
524
|
-
const ctx = useGameContext();
|
|
525
|
-
const visibleRef = useRenderVisibility();
|
|
526
|
-
const entityId = entity.id;
|
|
527
|
-
const role = entity.role;
|
|
528
|
-
const name = entity.name;
|
|
529
|
-
const color = isLocal ? "#4ade80" : role === "npc" ? colorFromId(name) : "#9ca3af";
|
|
530
|
-
useFrame(() => {
|
|
531
|
-
const group = groupRef.current;
|
|
532
|
-
if (group === null)
|
|
533
|
-
return;
|
|
534
|
-
const live = ctx.scene.entity.get(entityId);
|
|
535
|
-
if (live === null)
|
|
536
|
-
return;
|
|
537
|
-
writeEntityPose(group, live);
|
|
538
|
-
group.visible = visibleRef.current(entityId);
|
|
539
|
-
});
|
|
540
|
-
return (_jsxs("group", { ref: groupRef, userData: { [POINTER_ENTITY_KEY]: entityId }, onPointerDown: (event) => {
|
|
541
|
-
event.stopPropagation();
|
|
542
|
-
if (isLocal)
|
|
543
|
-
return;
|
|
544
|
-
const live = ctx.scene.entity.get(entityId);
|
|
545
|
-
if (live !== null)
|
|
546
|
-
onSelect(live);
|
|
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] }));
|
|
548
|
-
}
|
|
549
|
-
function ObjectMarker({ object, custom, model, style, }) {
|
|
550
|
-
const groupRef = useRef(null);
|
|
551
|
-
const ctx = useGameContext();
|
|
552
|
-
const visibleRef = useRenderVisibility();
|
|
553
|
-
const instanceId = object.instanceId;
|
|
554
|
-
const [scaleX, scaleY, scaleZ] = objectVisualScale(object.visual);
|
|
555
|
-
const color = object.visual?.color ?? style?.color ?? colorFromId(object.catalogId);
|
|
556
|
-
const opacity = object.visual?.opacity ?? style?.opacity ?? 1;
|
|
557
|
-
useFrame(() => {
|
|
558
|
-
const group = groupRef.current;
|
|
559
|
-
if (group === null)
|
|
560
|
-
return;
|
|
561
|
-
const live = ctx.scene.object.get(instanceId);
|
|
562
|
-
if (live === null)
|
|
563
|
-
return;
|
|
564
|
-
writeEntityPose(group, live);
|
|
565
|
-
group.visible = visibleRef.current(instanceId);
|
|
566
|
-
});
|
|
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 })] })) }));
|
|
568
|
-
}
|
|
569
|
-
function GroundPlane() {
|
|
570
|
-
const geometry = useMemo(() => {
|
|
571
|
-
const next = new THREE.PlaneGeometry(GROUND_SIZE, GROUND_SIZE, GROUND_SEGMENTS, GROUND_SEGMENTS);
|
|
572
|
-
const positions = next.attributes.position;
|
|
573
|
-
for (let index = 0; index < positions.count; index += 1) {
|
|
574
|
-
const x = positions.getX(index);
|
|
575
|
-
const y = positions.getY(index);
|
|
576
|
-
const height = Math.sin(x * 0.16) * 0.12 +
|
|
577
|
-
Math.cos(y * 0.11) * 0.1 +
|
|
578
|
-
Math.sin((x + y) * 0.05) * 0.16;
|
|
579
|
-
positions.setZ(index, height);
|
|
580
|
-
}
|
|
581
|
-
next.computeVertexNormals();
|
|
582
|
-
return next;
|
|
583
|
-
}, []);
|
|
584
|
-
return (_jsx("mesh", { "rotation-x": -Math.PI / 2, geometry: geometry, children: _jsx("meshStandardMaterial", { color: "#283729", roughness: 0.9, metalness: 0 }) }));
|
|
585
|
-
}
|
|
586
|
-
function RockField() {
|
|
587
|
-
const rocks = useMemo(() => Array.from({ length: 18 }, (_, index) => {
|
|
588
|
-
const angle = index * 2.399963229728653;
|
|
589
|
-
const radius = 10 + ((index * 17) % 58);
|
|
590
|
-
return {
|
|
591
|
-
id: `rock-${index}`,
|
|
592
|
-
x: Math.cos(angle) * radius,
|
|
593
|
-
z: Math.sin(angle) * radius,
|
|
594
|
-
scale: 0.45 + ((index * 13) % 11) / 10,
|
|
595
|
-
rotation: angle,
|
|
596
|
-
};
|
|
597
|
-
}), []);
|
|
598
|
-
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))) }));
|
|
599
|
-
}
|
|
600
|
-
function WorldEnvironment({ environment: Environment }) {
|
|
601
|
-
if (Environment !== undefined)
|
|
602
|
-
return _jsx(Environment, {});
|
|
603
|
-
return (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx("gridHelper", { args: [160, 80, "#3a3f4a", "#2b2f38"], "position-y": 0.01 }), _jsx(RockField, {})] }));
|
|
604
|
-
}
|
|
605
|
-
function WorldActors({ entitySprites, entityModels, objectModels, objectStyles, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }) {
|
|
606
|
-
const ctx = useGameContext();
|
|
607
|
-
const entities = useSceneEntities();
|
|
608
|
-
const objects = useSceneObjects();
|
|
609
|
-
const player = usePlayer();
|
|
610
|
-
const targetId = useTarget(player.userId);
|
|
611
|
-
const controlledId = ctx.player.possession.active(player.userId);
|
|
612
|
-
const handleSelect = (entity) => {
|
|
613
|
-
const relation = ctx.scene.entity.canReceive(entity.id, "damage") === null ? "hostile" : "friendly";
|
|
614
|
-
ctx.scene.entity.setTarget(controlledId, relation === "hostile" || entity.role === "npc" ? entity.id : null);
|
|
615
|
-
};
|
|
616
|
-
return (_jsxs(_Fragment, { children: [entities
|
|
617
|
-
.filter((entity) => entity.name !== WORLD_ITEM_ENTITY_NAME)
|
|
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) => {
|
|
620
|
-
const model = resolveModel(objectModels?.[object.catalogId], assets, {
|
|
621
|
-
seam: "objectModels",
|
|
622
|
-
key: object.catalogId,
|
|
623
|
-
}) ?? tryResolveCatalogModel(object.catalogId, assets);
|
|
624
|
-
return (_jsx(ObjectMarker, { object: object, custom: renderObject?.(object), model: model, style: objectStyles?.[object.catalogId] }, object.instanceId));
|
|
625
|
-
})] }));
|
|
626
|
-
}
|
|
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 })] }));
|
|
629
|
-
}
|
|
630
|
-
function RemotePlayers({ rows }) {
|
|
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))) }));
|
|
632
|
-
}
|
|
633
|
-
function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, gateRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }) {
|
|
634
|
-
const posterElapsedRef = useRef(0);
|
|
635
|
-
const posterDoneRef = useRef(false);
|
|
636
|
-
const hasReportedTickError = useRef(false);
|
|
637
|
-
const repeatFiredAtRef = useRef(new Map());
|
|
638
|
-
const slotActions = useMemo(() => findHotbarSlotActions(playable.game.input), [playable]);
|
|
639
|
-
const hotbarId = useMemo(() => hotbarIdFor(playable), [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]);
|
|
646
|
-
const autoPickupRadius = useMemo(() => {
|
|
647
|
-
const cfg = playable.worldItem?.autoPickup;
|
|
648
|
-
if (cfg === undefined || cfg === false)
|
|
649
|
-
return null;
|
|
650
|
-
const fallback = playable.worldItem?.pickupRadius ?? DEFAULT_PICKUP_RADIUS;
|
|
651
|
-
return cfg === true ? fallback : cfg.radius ?? fallback;
|
|
652
|
-
}, [playable]);
|
|
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);
|
|
670
|
-
const inputActions = useMemo(() => Object.keys(playable.game.input ?? {}), [playable]);
|
|
671
|
-
useFrame((_state, rawDt) => {
|
|
672
|
-
if (poster) {
|
|
673
|
-
if (posterDoneRef.current)
|
|
674
|
-
return;
|
|
675
|
-
posterElapsedRef.current += Math.min(rawDt, 0.05);
|
|
676
|
-
if (posterElapsedRef.current >= POSTER_SETTLE_SECONDS) {
|
|
677
|
-
posterDoneRef.current = true;
|
|
678
|
-
onPosterSettled();
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
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
|
-
}
|
|
697
|
-
const simStart = performance.now();
|
|
698
|
-
try {
|
|
699
|
-
let endPhase = devtools.profile.begin("time+input");
|
|
700
|
-
const dt = Math.min(rawDt, 0.05);
|
|
701
|
-
const gameDt = ctx.time.advance(dt);
|
|
702
|
-
ctx.input.publish(heldActionsFor(tracker, inputActions));
|
|
703
|
-
ctx.input.publishPointer(pointerAxisRef.current);
|
|
704
|
-
sendInput();
|
|
705
|
-
const turnInput = (tracker.isDown("turnRight") ? 1 : 0) - (tracker.isDown("turnLeft") ? 1 : 0);
|
|
706
|
-
if (turnInput !== 0)
|
|
707
|
-
yawRef.current = steerYaw(yawRef.current, turnInput, TURN_SPEED, dt);
|
|
708
|
-
endPhase();
|
|
709
|
-
const playerId = ctx.player.possession.active(ctx.player.userId);
|
|
710
|
-
const player = ctx.scene.entity.get(playerId);
|
|
711
|
-
if (player !== null && drivesPose && !serverAuthoritative) {
|
|
712
|
-
endPhase = devtools.profile.begin("pose");
|
|
713
|
-
stepPlayerMovement(ctx, ctx.player.userId, { held: ctx.input.held(), pointer: ctx.input.pointer() }, rawDt, movementTuning, yawRef.current);
|
|
714
|
-
endPhase();
|
|
715
|
-
}
|
|
716
|
-
if (autoPickupRadius !== null && !serverAuthoritative) {
|
|
717
|
-
endPhase = devtools.profile.begin("pickup");
|
|
718
|
-
const self = ctx.scene.entity.get(playerId);
|
|
719
|
-
if (self !== null) {
|
|
720
|
-
const nearest = ctx.scene.worldItem.nearestInRadius(self.position, autoPickupRadius);
|
|
721
|
-
if (nearest !== null)
|
|
722
|
-
ctx.scene.worldItem.pickup(nearest, ctx.player.userId);
|
|
723
|
-
}
|
|
724
|
-
endPhase();
|
|
725
|
-
}
|
|
726
|
-
if (!serverAuthoritative) {
|
|
727
|
-
devtools.profile.measure("onTick", () => {
|
|
728
|
-
playable.loop.onTick(ctx, gameDt);
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
endPhase = devtools.profile.begin("actions");
|
|
732
|
-
if (tracker.wasPressed("tabTarget")) {
|
|
733
|
-
if (ctx.game.commands.has("target.cycle"))
|
|
734
|
-
ctx.game.commands.run("target.cycle", {});
|
|
735
|
-
else
|
|
736
|
-
ctx.scene.entity.cycleTarget(playerId, { filter: "hostile" });
|
|
737
|
-
}
|
|
738
|
-
if (tracker.wasPressed("clearTarget")) {
|
|
739
|
-
if (ctx.game.commands.has("target.clear"))
|
|
740
|
-
ctx.game.commands.run("target.clear", {});
|
|
741
|
-
else
|
|
742
|
-
ctx.scene.entity.setTarget(playerId, null);
|
|
743
|
-
}
|
|
744
|
-
if (pingCommand !== undefined && tracker.wasPressed("ping")) {
|
|
745
|
-
const hit = pointerService.worldHit();
|
|
746
|
-
if (hit !== null && ctx.game.commands.has(pingCommand)) {
|
|
747
|
-
commandSink.run(pingCommand, {
|
|
748
|
-
point: hit.point,
|
|
749
|
-
entity: hit.entity,
|
|
750
|
-
object: hit.object,
|
|
751
|
-
normal: hit.normal,
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
const aimOverride = pointerAim ? pointerAimFor(ctx, pointerService) : undefined;
|
|
756
|
-
const commandAim = aimOverride ?? { yaw: yawRef.current, pitch: pitchRef.current };
|
|
757
|
-
if (collisionDebug.getState().layers.aimLaser) {
|
|
758
|
-
const aimFrom = ctx.player.possession.active(playerId) ?? playerId;
|
|
759
|
-
collisionDebug.setAimProbe({
|
|
760
|
-
from: aimFrom,
|
|
761
|
-
aim: commandAim,
|
|
762
|
-
originPolicy: { kind: "eye" },
|
|
763
|
-
maxDistance: 100,
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
else if (collisionDebug.getAimProbe() !== null) {
|
|
767
|
-
collisionDebug.setAimProbe(null);
|
|
768
|
-
}
|
|
769
|
-
const nowMs = performance.now();
|
|
770
|
-
for (const action of Object.keys(playable.game.input ?? {})) {
|
|
771
|
-
const pressed = tracker.wasPressed(action);
|
|
772
|
-
if (action === "ping" && pingCommand !== undefined)
|
|
773
|
-
continue;
|
|
774
|
-
if (action === "interact") {
|
|
775
|
-
if (!pressed)
|
|
776
|
-
continue;
|
|
777
|
-
const prompts = playable.prompts?.(ctx);
|
|
778
|
-
const focus = prompts === undefined ? null : ctx.scene.entity.get(playerId);
|
|
779
|
-
if (prompts !== undefined && focus !== null) {
|
|
780
|
-
const active = resolveActivePrompt({ x: focus.position[0], z: focus.position[2] }, prompts);
|
|
781
|
-
if (active !== null && active.prompt.invoke !== null) {
|
|
782
|
-
commandSink.run(active.prompt.invoke.name, active.prompt.invoke.input);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
continue;
|
|
786
|
-
}
|
|
787
|
-
if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
|
|
788
|
-
continue;
|
|
789
|
-
repeatFiredAtRef.current.set(action, nowMs);
|
|
790
|
-
dispatchBoundAction(ctx, action, yawRef.current, pitchRef.current, commandAim, RESERVED_INPUT_ACTIONS, commandSink);
|
|
791
|
-
}
|
|
792
|
-
if (hotbarId !== null) {
|
|
793
|
-
for (const { action, slot } of slotActions) {
|
|
794
|
-
if (!tracker.wasPressed(action))
|
|
795
|
-
continue;
|
|
796
|
-
const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
|
|
797
|
-
if (!result.ok)
|
|
798
|
-
console.warn(`[jgengine:item-use] ${result.error}`);
|
|
799
|
-
}
|
|
800
|
-
const usePrimary = tracker.wasPressed("useAbility") || (primaryClickRef.current && hotbarId !== null);
|
|
801
|
-
if (usePrimary) {
|
|
802
|
-
primaryClickRef.current = false;
|
|
803
|
-
const slots = ctx.player.inventory.state(hotbarId).slots;
|
|
804
|
-
const preferred = playable.hotbarSelection?.() ?? -1;
|
|
805
|
-
const slot = preferred >= 0 && slots[preferred] !== null && slots[preferred] !== undefined
|
|
806
|
-
? preferred
|
|
807
|
-
: slots.findIndex((stack) => stack !== null);
|
|
808
|
-
if (slot >= 0) {
|
|
809
|
-
const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
|
|
810
|
-
if (!result.ok)
|
|
811
|
-
console.warn(`[jgengine:item-use] ${result.error}`);
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
tracker.endFrame();
|
|
816
|
-
endPhase();
|
|
817
|
-
const serverId = serverIdRef.current;
|
|
818
|
-
if (multiplayer !== null && serverId !== null) {
|
|
819
|
-
endPhase = devtools.profile.begin("presence");
|
|
820
|
-
const focus = ctx.scene.entity.get(playerId);
|
|
821
|
-
if (focus !== null) {
|
|
822
|
-
multiplayer.backend.presenceSync.syncPose(serverId, {
|
|
823
|
-
x: focus.position[0],
|
|
824
|
-
y: focus.position[1],
|
|
825
|
-
z: focus.position[2],
|
|
826
|
-
rotationY: focus.rotationY,
|
|
827
|
-
rotationPitch: pitchRef.current,
|
|
828
|
-
});
|
|
829
|
-
}
|
|
830
|
-
endPhase();
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
catch (error) {
|
|
834
|
-
if (!hasReportedTickError.current) {
|
|
835
|
-
hasReportedTickError.current = true;
|
|
836
|
-
onRuntimeError(error, "tick");
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
devtools.frame.record({ frameMs: rawDt * 1000, simMs: performance.now() - simStart });
|
|
840
|
-
}, GAME_SIM_FRAME_PRIORITY);
|
|
841
|
-
return null;
|
|
842
|
-
}
|
|
843
|
-
function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, gateRef, onRuntimeError, }) {
|
|
844
|
-
const hasReportedTickError = useRef(false);
|
|
845
|
-
const repeatFiredAtRef = useRef(new Map());
|
|
846
|
-
const lastFrameRef = useRef(null);
|
|
847
|
-
const inputActions = useMemo(() => Object.keys(playable.game.input ?? {}), [playable]);
|
|
848
|
-
useEffect(() => {
|
|
849
|
-
let frameId;
|
|
850
|
-
const tick = (now) => {
|
|
851
|
-
frameId = requestAnimationFrame(tick);
|
|
852
|
-
const last = lastFrameRef.current;
|
|
853
|
-
lastFrameRef.current = now;
|
|
854
|
-
if (last === null)
|
|
855
|
-
return;
|
|
856
|
-
if (gateRef.current) {
|
|
857
|
-
ctx.input.publish(heldActionsFor(tracker, NO_ACTIONS));
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
const rawDt = (now - last) / 1000;
|
|
861
|
-
const simStart = performance.now();
|
|
862
|
-
try {
|
|
863
|
-
let endPhase = devtools.profile.begin("time+input");
|
|
864
|
-
const dt = Math.min(rawDt, 0.05);
|
|
865
|
-
const gameDt = ctx.time.advance(dt);
|
|
866
|
-
ctx.input.publish(heldActionsFor(tracker, inputActions));
|
|
867
|
-
ctx.input.publishPointer(pointerAxisRef.current);
|
|
868
|
-
endPhase();
|
|
869
|
-
devtools.profile.measure("onTick", () => {
|
|
870
|
-
playable.loop.onTick(ctx, gameDt);
|
|
871
|
-
});
|
|
872
|
-
endPhase = devtools.profile.begin("actions");
|
|
873
|
-
const nowMs = performance.now();
|
|
874
|
-
for (const action of inputActions) {
|
|
875
|
-
if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
|
|
876
|
-
continue;
|
|
877
|
-
repeatFiredAtRef.current.set(action, nowMs);
|
|
878
|
-
dispatchBoundAction(ctx, action, 0, 0, { yaw: 0, pitch: 0 }, EMPTY_RESERVED);
|
|
879
|
-
}
|
|
880
|
-
tracker.endFrame();
|
|
881
|
-
endPhase();
|
|
882
|
-
}
|
|
883
|
-
catch (error) {
|
|
884
|
-
if (!hasReportedTickError.current) {
|
|
885
|
-
hasReportedTickError.current = true;
|
|
886
|
-
onRuntimeError(error, "tick");
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
devtools.frame.record({ frameMs: rawDt * 1000, simMs: performance.now() - simStart });
|
|
890
|
-
};
|
|
891
|
-
frameId = requestAnimationFrame(tick);
|
|
892
|
-
return () => cancelAnimationFrame(frameId);
|
|
893
|
-
}, [ctx, playable, tracker, pointerAxisRef, gateRef, onRuntimeError, inputActions]);
|
|
894
|
-
return null;
|
|
895
|
-
}
|
|
896
|
-
class GameUiErrorBoundary extends Component {
|
|
897
|
-
state = { failed: false };
|
|
898
|
-
static getDerivedStateFromError() {
|
|
899
|
-
return { failed: true };
|
|
900
|
-
}
|
|
901
|
-
componentDidCatch(error, info) {
|
|
902
|
-
this.props.onRuntimeError(error, "ui-render", info.componentStack ?? undefined);
|
|
903
|
-
}
|
|
904
|
-
render() {
|
|
905
|
-
if (this.state.failed)
|
|
906
|
-
return null;
|
|
907
|
-
return this.props.children;
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
function expandedReactError(message) {
|
|
911
|
-
return /Minified React error #185\b/.test(message)
|
|
912
|
-
? "Maximum update depth exceeded. A component is repeatedly updating state during render or after every update."
|
|
913
|
-
: null;
|
|
914
|
-
}
|
|
915
|
-
function diagnosticReport(diagnostic, gameName) {
|
|
916
|
-
const explanation = expandedReactError(diagnostic.message);
|
|
917
|
-
return [
|
|
918
|
-
"JGengine runtime error",
|
|
919
|
-
`Game: ${gameName}`,
|
|
920
|
-
`Engine: ${VERSION}`,
|
|
921
|
-
`Phase: ${diagnostic.phase}`,
|
|
922
|
-
`Time: ${diagnostic.capturedAt}`,
|
|
923
|
-
`Page: ${window.location.origin}${window.location.pathname}`,
|
|
924
|
-
`Browser: ${navigator.userAgent}`,
|
|
925
|
-
explanation === null ? null : `Explanation: ${explanation}`,
|
|
926
|
-
"",
|
|
927
|
-
`Message: ${diagnostic.message}`,
|
|
928
|
-
diagnostic.stack === undefined ? null : `JavaScript stack:\n${diagnostic.stack}`,
|
|
929
|
-
diagnostic.componentStack === undefined ? null : `React component stack:\n${diagnostic.componentStack}`,
|
|
930
|
-
]
|
|
931
|
-
.filter((line) => line !== null)
|
|
932
|
-
.join("\n");
|
|
933
|
-
}
|
|
934
|
-
function DiagnosticOverlay({ diagnostics, gameName }) {
|
|
935
|
-
const [copied, setCopied] = useState(false);
|
|
936
|
-
if (diagnostics.length === 0)
|
|
937
|
-
return null;
|
|
938
|
-
const latest = diagnostics[diagnostics.length - 1];
|
|
939
|
-
const explanation = expandedReactError(latest.message);
|
|
940
|
-
const report = diagnosticReport(latest, gameName);
|
|
941
|
-
const issueBody = report.length > 8000 ? `${report.slice(0, 8000)}\n\n[Report truncated; use Copy error for the full report.]` : report;
|
|
942
|
-
const issueUrl = `https://github.com/Noisemaker111/jgengine/issues/new?title=${encodeURIComponent(`[BUG] ${latest.phase}: ${latest.message.slice(0, 100)}`)}&body=${encodeURIComponent(issueBody)}`;
|
|
943
|
-
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: () => {
|
|
944
|
-
void navigator.clipboard.writeText(report).then(() => {
|
|
945
|
-
setCopied(true);
|
|
946
|
-
window.setTimeout(() => setCopied(false), 2000);
|
|
947
|
-
});
|
|
948
|
-
}, 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" })] })] }));
|
|
949
|
-
}
|
|
950
|
-
const POSTER_SETTLE_SECONDS = 1.6;
|
|
951
69
|
export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null, poster = false, onContextReady, }) {
|
|
952
70
|
const multiplayer = useMemo(() => (rawMultiplayer === null ? null : withDevtoolsLatency(rawMultiplayer)), [rawMultiplayer]);
|
|
953
71
|
const devtoolsEnabled = playable.devtools !== false && !poster;
|
|
954
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]);
|
|
955
81
|
const [posterFrozen, setPosterFrozen] = useState(false);
|
|
956
82
|
const posterSettledRef = useRef(false);
|
|
957
83
|
const [ctx, setCtx] = useState(null);
|
|
@@ -967,7 +93,6 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
967
93
|
const pointerAxisRef = useRef(null);
|
|
968
94
|
const marqueeStartRef = useRef(null);
|
|
969
95
|
const f2HeldRef = useRef(false);
|
|
970
|
-
const f2ChordedRef = useRef(false);
|
|
971
96
|
const pointerService = useMemo(() => createPointerService(), []);
|
|
972
97
|
const selection = useMemo(() => createSelectionSet(), [playable]);
|
|
973
98
|
const [marquee, setMarquee] = useState(null);
|
|
@@ -1001,6 +126,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1001
126
|
firstPerson: resolveRigKind(playable.camera) === "first",
|
|
1002
127
|
config: playable.touch,
|
|
1003
128
|
}), [playable]);
|
|
129
|
+
const touchStyle = useTouchStyle(settingsStore, touchScheme?.style ?? DEFAULT_TOUCH_STYLE);
|
|
1004
130
|
const { coarsePointer, portrait, compact } = useDisplayProfile();
|
|
1005
131
|
const touchSink = useMemo(() => ({ onCodeDown: (code) => tracker.handleDown(code), onCodeUp: (code) => tracker.handleUp(code) }), [tracker]);
|
|
1006
132
|
const gateRef = useRef(false);
|
|
@@ -1021,18 +147,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1021
147
|
useEffect(() => {
|
|
1022
148
|
if (ctx === null)
|
|
1023
149
|
return;
|
|
1024
|
-
|
|
1025
|
-
audioEngine.playOneShot(sound, at === undefined ? undefined : { x: at[0], y: at[1], z: at[2] });
|
|
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
|
-
};
|
|
150
|
+
return attachAudioEventWire(ctx.game.events, audioEngine);
|
|
1036
151
|
}, [ctx, audioEngine]);
|
|
1037
152
|
useEffect(() => {
|
|
1038
153
|
if (typeof document === "undefined")
|
|
@@ -1062,7 +177,6 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1062
177
|
setDiagnostics((current) => [...current.slice(-4), { ...diagnostic, id: Date.now() + current.length }]);
|
|
1063
178
|
};
|
|
1064
179
|
useEffect(() => {
|
|
1065
|
-
playable.game.scene.clear();
|
|
1066
180
|
setDiagnostics([]);
|
|
1067
181
|
try {
|
|
1068
182
|
const context = createGameContext({
|
|
@@ -1080,102 +194,10 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1080
194
|
setCtx(null);
|
|
1081
195
|
}
|
|
1082
196
|
return () => {
|
|
1083
|
-
playable.game.scene.clear();
|
|
1084
197
|
setCtx(null);
|
|
1085
198
|
};
|
|
1086
199
|
}, [playable, userId]);
|
|
1087
|
-
|
|
1088
|
-
if (ctx === null || multiplayer === null)
|
|
1089
|
-
return;
|
|
1090
|
-
let disposed = false;
|
|
1091
|
-
const cleanups = [];
|
|
1092
|
-
void multiplayer.backend.transport
|
|
1093
|
-
.joinServer({ gameId: multiplayer.gameId })
|
|
1094
|
-
.then((joined) => {
|
|
1095
|
-
if (disposed) {
|
|
1096
|
-
void multiplayer.backend.transport.leaveServer({ serverId: joined.serverId });
|
|
1097
|
-
return;
|
|
1098
|
-
}
|
|
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
|
-
}
|
|
1103
|
-
cleanups.push(multiplayer.backend.presenceSync.subscribe(joined.serverId, (rows) => {
|
|
1104
|
-
setRemotePlayers(rows.filter((row) => row.userId !== multiplayer.userId));
|
|
1105
|
-
}));
|
|
1106
|
-
const seen = new Set();
|
|
1107
|
-
let injecting = false;
|
|
1108
|
-
for (const action of multiplayer.feedActions) {
|
|
1109
|
-
cleanups.push(ctx.game.feed.subscribe(action, (entry) => {
|
|
1110
|
-
if (injecting)
|
|
1111
|
-
return;
|
|
1112
|
-
void multiplayer.backend
|
|
1113
|
-
.pushFeedEntry({
|
|
1114
|
-
serverId: joined.serverId,
|
|
1115
|
-
action,
|
|
1116
|
-
entry: { at: entry.at, data: entry.data, from: multiplayer.userId },
|
|
1117
|
-
})
|
|
1118
|
-
.catch(() => undefined);
|
|
1119
|
-
}));
|
|
1120
|
-
const remoteUnsub = multiplayer.backend.feeds?.subscribeFeed({ serverId: joined.serverId, action }, (view) => {
|
|
1121
|
-
for (const raw of view.entries) {
|
|
1122
|
-
const remote = raw;
|
|
1123
|
-
if (typeof remote.from !== "string" || remote.from === multiplayer.userId)
|
|
1124
|
-
continue;
|
|
1125
|
-
const key = `${action}|${remote.from}|${remote.at ?? 0}`;
|
|
1126
|
-
if (seen.has(key))
|
|
1127
|
-
continue;
|
|
1128
|
-
seen.add(key);
|
|
1129
|
-
injecting = true;
|
|
1130
|
-
ctx.game.feed.push(action, remote.data);
|
|
1131
|
-
injecting = false;
|
|
1132
|
-
}
|
|
1133
|
-
});
|
|
1134
|
-
if (remoteUnsub !== undefined)
|
|
1135
|
-
cleanups.push(remoteUnsub);
|
|
1136
|
-
}
|
|
1137
|
-
const chatSync = multiplayer.backend.chatSyncFor?.(joined.serverId);
|
|
1138
|
-
if (chatSync !== undefined && ctx.game.chat !== undefined) {
|
|
1139
|
-
const chat = ctx.game.chat;
|
|
1140
|
-
const globalChannelIds = new Set(chat
|
|
1141
|
-
.channels()
|
|
1142
|
-
.filter((channel) => channel.kind === "global")
|
|
1143
|
-
.map((channel) => channel.id));
|
|
1144
|
-
const seenRemoteChat = new Set();
|
|
1145
|
-
cleanups.push(ctx.game.events.subscribe("chat.message", (event) => {
|
|
1146
|
-
if (event.fromUserId !== multiplayer.userId)
|
|
1147
|
-
return;
|
|
1148
|
-
if (!globalChannelIds.has(event.channelId))
|
|
1149
|
-
return;
|
|
1150
|
-
void chatSync.send(event.channelId, event.body).catch(() => undefined);
|
|
1151
|
-
}));
|
|
1152
|
-
for (const channelId of globalChannelIds) {
|
|
1153
|
-
cleanups.push(chatSync.subscribe(channelId, (messages) => {
|
|
1154
|
-
for (const message of messages) {
|
|
1155
|
-
if (message.fromUserId === multiplayer.userId)
|
|
1156
|
-
continue;
|
|
1157
|
-
if (seenRemoteChat.has(message.id))
|
|
1158
|
-
continue;
|
|
1159
|
-
seenRemoteChat.add(message.id);
|
|
1160
|
-
chat.send(message.fromUserId, message.channelId, message.body);
|
|
1161
|
-
}
|
|
1162
|
-
}));
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
})
|
|
1166
|
-
.catch(() => undefined);
|
|
1167
|
-
return () => {
|
|
1168
|
-
disposed = true;
|
|
1169
|
-
for (const cleanup of cleanups)
|
|
1170
|
-
cleanup();
|
|
1171
|
-
const serverId = serverIdRef.current;
|
|
1172
|
-
serverIdRef.current = null;
|
|
1173
|
-
setRemotePlayers([]);
|
|
1174
|
-
if (serverId !== null) {
|
|
1175
|
-
void multiplayer.backend.transport.leaveServer({ serverId }).catch(() => undefined);
|
|
1176
|
-
}
|
|
1177
|
-
};
|
|
1178
|
-
}, [ctx, multiplayer, playable]);
|
|
200
|
+
useShellMultiplayerSync(ctx, multiplayer, playable, serverIdRef, setRemotePlayers);
|
|
1179
201
|
useEffect(() => {
|
|
1180
202
|
wrapperRef.current?.focus();
|
|
1181
203
|
}, [ctx]);
|
|
@@ -1192,31 +214,33 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1192
214
|
if (rigKind === "none" || playable.presentation === "hud") {
|
|
1193
215
|
const GameUI = playable.GameUI;
|
|
1194
216
|
return (_jsx("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
|
|
1195
|
-
if (event.code === "F2"
|
|
217
|
+
if (event.code === "F2") {
|
|
1196
218
|
event.preventDefault();
|
|
1197
219
|
f2HeldRef.current = true;
|
|
1198
|
-
f2ChordedRef.current = false;
|
|
1199
220
|
return;
|
|
1200
221
|
}
|
|
1201
222
|
if (f2HeldRef.current) {
|
|
1202
|
-
|
|
223
|
+
if (event.code === "KeyD" && devtoolsEnabled) {
|
|
224
|
+
event.preventDefault();
|
|
225
|
+
setDevtoolsOpen((current) => !current);
|
|
226
|
+
}
|
|
1203
227
|
return;
|
|
1204
228
|
}
|
|
1205
229
|
if (event.code === "Tab" || event.code === "Space")
|
|
1206
230
|
event.preventDefault();
|
|
1207
|
-
|
|
231
|
+
if (playControlsActive(ctx))
|
|
232
|
+
tracker.handleDown(event.code);
|
|
1208
233
|
}, onKeyUp: (event) => {
|
|
1209
|
-
if (event.code === "F2"
|
|
234
|
+
if (event.code === "F2") {
|
|
1210
235
|
f2HeldRef.current = false;
|
|
1211
|
-
if (!f2ChordedRef.current)
|
|
1212
|
-
setDevtoolsOpen((current) => !current);
|
|
1213
236
|
return;
|
|
1214
237
|
}
|
|
1215
|
-
|
|
238
|
+
if (playControlsActive(ctx))
|
|
239
|
+
tracker.handleUp(event.code);
|
|
1216
240
|
}, onBlur: () => {
|
|
1217
241
|
f2HeldRef.current = false;
|
|
1218
242
|
tracker.reset();
|
|
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:
|
|
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 })] }) }));
|
|
1220
244
|
}
|
|
1221
245
|
const firstPerson = rigKind === "first";
|
|
1222
246
|
const showReticle = (firstPerson && playable.camera?.firstPerson?.reticle !== false) || rigKind === "shoulder";
|
|
@@ -1231,23 +255,40 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1231
255
|
const barsMaxDistance = worldBars === undefined || worldBars === true || worldBars === false
|
|
1232
256
|
? undefined
|
|
1233
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;
|
|
1234
266
|
const resolveEntityRole = (entity) => playable.content.entityById?.(entity.name)?.role;
|
|
1235
267
|
const pointer = playable.pointer;
|
|
1236
268
|
const pointerUsesLeft = pointer !== undefined && (pointer.select === true || pointer.moveCommand !== undefined);
|
|
1237
269
|
const selectFilter = pointer?.selectFilter;
|
|
1238
270
|
const worldSky = resolveWorldSky(playable.game.world);
|
|
1239
271
|
const world = playable.game.world;
|
|
272
|
+
const biomeBands = world?.kind === "environment" ? world.terrain?.biomeBands : undefined;
|
|
1240
273
|
const AutoEnvironment = playable.environment ??
|
|
1241
274
|
(world?.kind === "environment"
|
|
1242
275
|
? () => _jsx(EnvironmentScene, { feature: world })
|
|
1243
276
|
: world?.kind === "biomes" || world?.kind === "voxel" || world?.kind === "plots" || world?.kind === "tilemap"
|
|
1244
277
|
? () => _jsx(GridWorldScene, { feature: world })
|
|
1245
278
|
: undefined);
|
|
1246
|
-
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;
|
|
1247
288
|
const backdropSky = backdrop?.sky !== undefined ? resolveSkyDescriptor(backdrop.sky) : undefined;
|
|
1248
289
|
const effectiveSky = backdropSky ?? worldSky;
|
|
1249
290
|
const backgroundColor = backdrop?.background ?? (effectiveSky === undefined ? DEFAULT_BACKGROUND_COLOR : undefined);
|
|
1250
|
-
const lighting =
|
|
291
|
+
const lighting = resolvedLook.lighting;
|
|
1251
292
|
const orthographic = playable.camera?.projection === "orthographic";
|
|
1252
293
|
const localXY = (event) => {
|
|
1253
294
|
const rect = wrapperRef.current?.getBoundingClientRect();
|
|
@@ -1273,7 +314,8 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1273
314
|
/** DOM UI (menus, HUD buttons) sits in the same wrapper as the canvas — only canvas-targeted clicks are world input. */
|
|
1274
315
|
const isWorldPointerTarget = (event) => event.target instanceof HTMLCanvasElement;
|
|
1275
316
|
const handlePointerDown = (event) => {
|
|
1276
|
-
|
|
317
|
+
if (isWorldPointerTarget(event))
|
|
318
|
+
wrapperRef.current?.focus();
|
|
1277
319
|
trackPointerAxis(event);
|
|
1278
320
|
audioEngine.resume();
|
|
1279
321
|
if (contextMenu !== null)
|
|
@@ -1411,32 +453,35 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1411
453
|
controlsActive &&
|
|
1412
454
|
touchScheme !== null &&
|
|
1413
455
|
(touchScheme.joystick !== null || touchScheme.buttons.length > 0);
|
|
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 ?? [],
|
|
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: {
|
|
1415
459
|
"--jg-hud-dock-clearance": `${dockMounted ? touchDockClearance(touchScheme, touchScale) : 0}px`,
|
|
1416
460
|
}, onKeyDown: (event) => {
|
|
1417
|
-
if (event.code === "F2"
|
|
461
|
+
if (event.code === "F2") {
|
|
1418
462
|
event.preventDefault();
|
|
1419
463
|
f2HeldRef.current = true;
|
|
1420
|
-
f2ChordedRef.current = false;
|
|
1421
464
|
return;
|
|
1422
465
|
}
|
|
1423
466
|
if (f2HeldRef.current) {
|
|
1424
|
-
|
|
467
|
+
if (event.code === "KeyD" && devtoolsEnabled) {
|
|
468
|
+
event.preventDefault();
|
|
469
|
+
document.exitPointerLock?.();
|
|
470
|
+
setDevtoolsOpen((current) => !current);
|
|
471
|
+
}
|
|
1425
472
|
return;
|
|
1426
473
|
}
|
|
1427
474
|
if (event.code === "Tab" || event.code === "Space")
|
|
1428
475
|
event.preventDefault();
|
|
1429
|
-
|
|
476
|
+
if (playControlsActive(ctx))
|
|
477
|
+
tracker.handleDown(event.code);
|
|
1430
478
|
}, onKeyUp: (event) => {
|
|
1431
|
-
if (event.code === "F2"
|
|
479
|
+
if (event.code === "F2") {
|
|
1432
480
|
f2HeldRef.current = false;
|
|
1433
|
-
if (!f2ChordedRef.current) {
|
|
1434
|
-
document.exitPointerLock?.();
|
|
1435
|
-
setDevtoolsOpen((current) => !current);
|
|
1436
|
-
}
|
|
1437
481
|
return;
|
|
1438
482
|
}
|
|
1439
|
-
|
|
483
|
+
if (playControlsActive(ctx))
|
|
484
|
+
tracker.handleUp(event.code);
|
|
1440
485
|
}, onBlur: () => {
|
|
1441
486
|
f2HeldRef.current = false;
|
|
1442
487
|
tracker.reset();
|
|
@@ -1460,14 +505,14 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
1460
505
|
fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
|
|
1461
506
|
near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
|
|
1462
507
|
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) => {
|
|
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) => {
|
|
1464
509
|
cameraDraggingRef.current = dragging;
|
|
1465
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: () => {
|
|
1466
511
|
if (posterSettledRef.current)
|
|
1467
512
|
return;
|
|
1468
513
|
posterSettledRef.current = true;
|
|
1469
514
|
setPosterFrozen(true);
|
|
1470
|
-
} }), _jsx(DevtoolsRendererProbe, {}),
|
|
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: () => {
|
|
1471
516
|
primaryClickRef.current = true;
|
|
1472
|
-
} })) : null, _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children:
|
|
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, {})] }) }) })] }) }));
|
|
1473
518
|
}
|