@jgengine/shell 0.18.1 → 0.21.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 +504 -7
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +132 -17
- package/dist/Shell3dPresentation.d.ts +5 -11
- package/dist/Shell3dPresentation.js +73 -11
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +20 -2
- package/dist/ShellHudPresentation.js +41 -6
- package/dist/audio/AudioComponents.js +6 -2
- package/dist/audio/audioEngine.d.ts +24 -0
- package/dist/audio/audioEngine.js +231 -83
- package/dist/audio/audioWire.d.ts +3 -0
- package/dist/audio/audioWire.js +9 -3
- package/dist/audio/loopParams.d.ts +39 -0
- package/dist/audio/loopParams.js +43 -0
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +3 -2
- package/dist/camera/GameFirstPersonCamera.d.ts +6 -4
- package/dist/camera/GameFirstPersonCamera.js +68 -27
- package/dist/camera/Viewports.d.ts +34 -0
- package/dist/camera/Viewports.js +123 -0
- package/dist/camera/cameraRigs.js +76 -62
- package/dist/camera/chaseHeading.d.ts +9 -0
- package/dist/camera/chaseHeading.js +10 -0
- package/dist/camera/customMuzzle.d.ts +14 -0
- package/dist/camera/customMuzzle.js +68 -0
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/orbitCameraMath.js +2 -2
- package/dist/camera/rigMath.d.ts +82 -4
- package/dist/camera/rigMath.js +133 -15
- package/dist/camera/rigResolve.d.ts +3 -0
- package/dist/camera/rigResolve.js +11 -0
- package/dist/combat/ProjectileModels.d.ts +31 -0
- package/dist/combat/ProjectileModels.js +70 -0
- package/dist/commandSink.d.ts +1 -2
- package/dist/commandSink.js +3 -2
- package/dist/defineGame.d.ts +19 -3
- package/dist/defineGame.js +39 -7
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +15 -167
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.d.ts +6 -0
- package/dist/devtools/agentBridge.js +16 -2
- package/dist/devtools/devtoolsReports.d.ts +68 -0
- package/dist/devtools/devtoolsReports.js +100 -0
- package/dist/devtools/latencyInstrumentation.d.ts +3 -0
- package/dist/devtools/latencyInstrumentation.js +11 -0
- package/dist/devtools/playtestProbe.d.ts +4 -0
- package/dist/devtools/playtestProbe.js +22 -0
- package/dist/diagnostics/FpsCounter.d.ts +17 -0
- package/dist/diagnostics/FpsCounter.js +32 -0
- package/dist/drivers/FrameDriver.js +35 -20
- package/dist/drivers/FrameRateLimiter.d.ts +9 -0
- package/dist/drivers/FrameRateLimiter.js +39 -0
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- package/dist/environment/DayNightSky.js +5 -5
- package/dist/environment/Daylight.d.ts +35 -1
- package/dist/environment/Daylight.js +98 -28
- package/dist/environment/EnvironmentScene.js +11 -10
- package/dist/environment/VolumetricClouds.js +20 -2
- package/dist/environment/daylightCycle.d.ts +20 -0
- package/dist/environment/daylightCycle.js +59 -7
- package/dist/environment/sunShadowMath.d.ts +45 -0
- package/dist/environment/sunShadowMath.js +50 -0
- package/dist/input/gamepadPoll.d.ts +47 -0
- package/dist/input/gamepadPoll.js +142 -0
- package/dist/input/gamepadSource.d.ts +11 -1
- package/dist/input/gamepadSource.js +117 -71
- package/dist/input/mouseLook.js +2 -1
- package/dist/input/padHaptics.d.ts +17 -0
- package/dist/input/padHaptics.js +32 -0
- package/dist/input/pointerLock.d.ts +11 -0
- package/dist/input/pointerLock.js +19 -0
- package/dist/inputSink.d.ts +6 -6
- package/dist/inputSink.js +60 -21
- package/dist/lazyShell3dPresentation.d.ts +9 -0
- package/dist/lazyShell3dPresentation.js +24 -0
- package/dist/materialOverride.d.ts +42 -0
- package/dist/materialOverride.js +95 -36
- package/dist/multiplayer.d.ts +33 -3
- package/dist/multiplayer.js +105 -22
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +12 -2
- package/dist/postfx/PostProcessing.d.ts +11 -1
- package/dist/postfx/PostProcessing.js +14 -42
- package/dist/postfx/gradeShader.d.ts +1 -1
- package/dist/postfx/gradeShader.js +30 -2
- package/dist/postfx/shadowPrepass.d.ts +3 -0
- package/dist/postfx/shadowPrepass.js +18 -0
- package/dist/postfx/stylizePass.d.ts +23 -0
- package/dist/postfx/stylizePass.js +216 -0
- package/dist/presentationRecovery.d.ts +75 -0
- package/dist/presentationRecovery.js +204 -0
- package/dist/presentationRecovery3d.d.ts +4 -0
- package/dist/presentationRecovery3d.js +32 -0
- package/dist/presentationResolve.d.ts +8 -1
- package/dist/presentationResolve.js +5 -2
- package/dist/render/CascadedShadows.d.ts +7 -2
- package/dist/render/CascadedShadows.js +98 -20
- package/dist/render/EnvironmentLighting.js +74 -35
- package/dist/render/HairCards.d.ts +7 -0
- package/dist/render/HairCards.js +23 -0
- package/dist/render/SceneModels.js +52 -78
- package/dist/render/SkinnedInstances.d.ts +91 -0
- package/dist/render/SkinnedInstances.js +249 -0
- package/dist/render/TextureFiltering.d.ts +16 -0
- package/dist/render/TextureFiltering.js +64 -0
- package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -0
- package/dist/render/displayColor.d.ts +33 -0
- package/dist/render/displayColor.js +138 -0
- package/dist/render/materialAppearanceSignals.d.ts +23 -0
- package/dist/render/materialAppearanceSignals.js +58 -0
- package/dist/render/materialAsset.d.ts +98 -0
- package/dist/render/materialAsset.js +454 -0
- package/dist/render/measureBounds.js +16 -3
- package/dist/render/measureCollisionMesh.js +8 -1
- package/dist/render/modelAssets.d.ts +8 -0
- package/dist/render/modelAssets.js +43 -0
- package/dist/render/modelBindPose.d.ts +5 -0
- package/dist/render/modelBindPose.js +35 -0
- package/dist/render/modelLoad.d.ts +32 -0
- package/dist/render/modelLoad.js +70 -1
- package/dist/render/modelNodeVisibility.d.ts +6 -0
- package/dist/render/modelNodeVisibility.js +18 -0
- package/dist/render/modelRender.d.ts +18 -1
- package/dist/render/modelRender.js +81 -3
- package/dist/render/projectedOverdraw.d.ts +35 -0
- package/dist/render/projectedOverdraw.js +204 -0
- package/dist/render/resolveModel.d.ts +12 -2
- package/dist/render/resolveModel.js +25 -4
- package/dist/render/rigNode.d.ts +26 -0
- package/dist/render/rigNode.js +32 -0
- package/dist/render/spriteRender.d.ts +4 -0
- package/dist/render/spriteRender.js +5 -0
- package/dist/render/staticShapeInstances.d.ts +34 -0
- package/dist/render/staticShapeInstances.js +78 -0
- package/dist/render/useFootIk.d.ts +53 -4
- package/dist/render/useFootIk.js +262 -70
- package/dist/render/useModelAnimation.d.ts +50 -0
- package/dist/render/useModelAnimation.js +245 -99
- package/dist/render/useModelInstance.d.ts +25 -0
- package/dist/render/useModelInstance.js +43 -0
- package/dist/scatter/scatterModels.d.ts +3 -2
- package/dist/scatter/scatterModels.js +15 -7
- package/dist/scene/AuthoredScene.d.ts +23 -6
- package/dist/scene/AuthoredScene.js +46 -9
- package/dist/scene/CityRenderer.js +6 -6
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/settings/QuickControls.js +1 -1
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- package/dist/settings/appliedSettings.d.ts +9 -3
- package/dist/settings/appliedSettings.js +6 -5
- package/dist/settings/settingsController.d.ts +3 -0
- package/dist/settings/settingsController.js +47 -3
- package/dist/shellControlSuspension.d.ts +18 -0
- package/dist/shellControlSuspension.js +28 -0
- package/dist/shellInputPublication.d.ts +28 -0
- package/dist/shellInputPublication.js +49 -0
- package/dist/shellPointerInput.d.ts +18 -0
- package/dist/shellPointerInput.js +49 -0
- package/dist/structures/BuildingKitBatch.d.ts +3 -1
- package/dist/structures/BuildingKitBatch.js +35 -56
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.d.ts +3 -15
- package/dist/structures/GeneratedBuilding.js +33 -22
- package/dist/structures/buildingKitMaterials.d.ts +20 -0
- package/dist/structures/buildingKitMaterials.js +70 -0
- package/dist/structures/buildingSpatialBatch.d.ts +11 -0
- package/dist/structures/buildingSpatialBatch.js +45 -0
- package/dist/structures/buildingSurface.d.ts +21 -0
- package/dist/structures/buildingSurface.js +134 -0
- package/dist/terrain/GrassField.d.ts +2 -1
- package/dist/terrain/GrassField.js +59 -5
- package/dist/terrain/grassGeometry.d.ts +10 -0
- package/dist/terrain/grassGeometry.js +55 -0
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- package/dist/touch/TouchControlsOverlay.js +4 -4
- package/dist/useShellMultiplayerSync.d.ts +1 -1
- package/dist/useShellMultiplayerSync.js +9 -3
- package/dist/vfx/ParticleField.d.ts +23 -20
- package/dist/vfx/ParticleField.js +168 -50
- package/dist/visibility/CullingProvider.js +2 -62
- package/dist/visibility/cullingDriver.d.ts +9 -0
- package/dist/visibility/cullingDriver.js +68 -0
- package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
- package/dist/weather/AuthoredWeatherLayer.js +31 -0
- package/dist/weather/DustField.d.ts +2 -1
- package/dist/weather/DustField.js +17 -3
- package/dist/weather/FireSpreadLayer.d.ts +4 -2
- package/dist/weather/FireSpreadLayer.js +5 -11
- package/dist/weather/LightningStrike.d.ts +4 -1
- package/dist/weather/LightningStrike.js +13 -17
- package/dist/weather/RainField.d.ts +2 -1
- package/dist/weather/RainField.js +17 -3
- package/dist/weather/RainImpactField.d.ts +16 -0
- package/dist/weather/RainImpactField.js +89 -0
- package/dist/weather/SnowField.d.ts +2 -1
- package/dist/weather/SnowField.js +17 -3
- package/dist/weather/WeatherLayer.d.ts +11 -2
- package/dist/weather/WeatherLayer.js +30 -6
- package/dist/weather/index.d.ts +2 -0
- package/dist/weather/index.js +2 -0
- package/dist/weather/weatherMath.d.ts +2 -0
- package/dist/weather/weatherMath.js +8 -0
- package/dist/weather/weatherShelter.d.ts +18 -0
- package/dist/weather/weatherShelter.js +75 -0
- package/dist/weather/weatherUniforms.d.ts +40 -1
- package/dist/weather/weatherUniforms.js +29 -9
- package/dist/world/AuthoredFlocks.d.ts +22 -0
- package/dist/world/AuthoredFlocks.js +87 -0
- package/dist/world/StaticObjectModels.d.ts +8 -0
- package/dist/world/StaticObjectModels.js +75 -0
- package/dist/world/WorldHud.d.ts +14 -6
- package/dist/world/WorldHud.js +25 -12
- package/dist/world/WorldParticles.d.ts +10 -23
- package/dist/world/WorldParticles.js +210 -62
- package/dist/world/WorldScene.d.ts +3 -1
- package/dist/world/WorldScene.js +41 -26
- package/dist/world/staticObjectBatches.d.ts +35 -0
- package/dist/world/staticObjectBatches.js +163 -0
- package/dist/world/worldBarSamples.d.ts +37 -33
- package/dist/world/worldBarSamples.js +117 -75
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +28 -6
- package/skills/jgengine/api.md +139 -99
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +10 -2
- package/skills/jgengine-ui/SKILL.md +16 -17
- package/skills/jgengine-ui/api.md +603 -365
- package/skills/jgengine-ui/capabilities.md +131 -1
- package/skills/jgengine-ui/reference-primitives.md +179 -0
- package/skills/jgengine-ui/reference.md +4 -81
- package/skills/jgengine-world/SKILL.md +25 -4
- package/skills/jgengine-world/api.md +1045 -655
- package/skills/jgengine-world/capabilities.md +246 -9
- package/skills/jgengine-world/recipes/character-animation.md +62 -0
- package/skills/jgengine-world/recipes/controls.md +48 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +55 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
- package/skills/jgengine-world/reference.md +180 -4
- package/skills/jgengine-world/visibility.md +139 -0
- package/skills/level-design/SKILL.md +1 -1
- package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +0 -15
- package/dist/render/testFixtures/proceduralSpriteAtlas.js +0 -30
- package/dist/render/testFixtures/proceduralTileLayer.d.ts +0 -13
- package/dist/render/testFixtures/proceduralTileLayer.js +0 -30
package/dist/GameHost.d.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { type ComponentType } from "react";
|
|
2
2
|
import type { EditorCatalogDefinition } from "@jgengine/core/editor/types";
|
|
3
|
+
import type { CreatorConfig } from "@jgengine/core/editor/creatorStorage";
|
|
3
4
|
import { type ResolveShellMultiplayerArgs, type ShellMultiplayer } from "./multiplayer.js";
|
|
4
5
|
import type { PlayableGame } from "./registry.js";
|
|
5
6
|
/** Structural shape of the module `GameHost`'s `editor` loader resolves — `import("@jgengine/editor")` satisfies it. */
|
|
6
7
|
export interface EditorSummonModule {
|
|
8
|
+
CreatorApp?: ComponentType<{
|
|
9
|
+
gameId: string;
|
|
10
|
+
config: CreatorConfig<PlayableGame>;
|
|
11
|
+
onExit: () => void;
|
|
12
|
+
}>;
|
|
7
13
|
EditorApp: ComponentType<{
|
|
8
14
|
gameId: string;
|
|
9
15
|
playable: PlayableGame;
|
|
@@ -28,6 +34,11 @@ export interface GameHostProps {
|
|
|
28
34
|
* stays a lazy chunk the game bundles only when this prop is set.
|
|
29
35
|
*/
|
|
30
36
|
editor?: () => Promise<EditorSummonModule>;
|
|
37
|
+
/** Player creator: injected durable storage, bounded catalog and a fresh runtime factory. */
|
|
38
|
+
creator?: CreatorConfig<PlayableGame>;
|
|
39
|
+
/** Controlled by the game's production menu. */
|
|
40
|
+
creatorOpen?: boolean;
|
|
41
|
+
onCreatorOpenChange?: (open: boolean) => void;
|
|
31
42
|
}
|
|
32
43
|
/**
|
|
33
44
|
* The one documented mount: resolves multiplayer for the playable and renders the shell. With the
|
|
@@ -35,4 +46,4 @@ export interface GameHostProps {
|
|
|
35
46
|
*
|
|
36
47
|
* @capability mount-game mount a defined game in the browser — `<GameHost playable={game} editor={() => import("@jgengine/editor")} />`
|
|
37
48
|
*/
|
|
38
|
-
export declare function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultiplayer, editor }: GameHostProps): import("react").JSX.Element;
|
|
49
|
+
export declare function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultiplayer, editor, creator, creatorOpen, onCreatorOpenChange }: GameHostProps): import("react").JSX.Element;
|
package/dist/GameHost.js
CHANGED
|
@@ -27,7 +27,7 @@ function initialEditorMode(enabled) {
|
|
|
27
27
|
*
|
|
28
28
|
* @capability mount-game mount a defined game in the browser — `<GameHost playable={game} editor={() => import("@jgengine/editor")} />`
|
|
29
29
|
*/
|
|
30
|
-
export function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultiplayer, editor }) {
|
|
30
|
+
export function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultiplayer, editor, creator, creatorOpen = false, onCreatorOpenChange }) {
|
|
31
31
|
const resolvedGameId = gameId ?? playable.game.name;
|
|
32
32
|
const [editorOpen, setEditorOpen] = useState(() => initialEditorMode(editor !== undefined));
|
|
33
33
|
useEffect(() => {
|
|
@@ -88,6 +88,16 @@ export function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultipla
|
|
|
88
88
|
return null;
|
|
89
89
|
return lazy(async () => ({ default: (await editor()).EditorApp }));
|
|
90
90
|
}, [editor]);
|
|
91
|
+
const CreatorLazy = useMemo(() => {
|
|
92
|
+
if (editor === undefined)
|
|
93
|
+
return null;
|
|
94
|
+
return lazy(async () => {
|
|
95
|
+
const module = await editor();
|
|
96
|
+
if (module.CreatorApp === undefined)
|
|
97
|
+
throw new Error("Editor module does not expose CreatorApp");
|
|
98
|
+
return { default: module.CreatorApp };
|
|
99
|
+
});
|
|
100
|
+
}, [editor]);
|
|
91
101
|
const resolved = useMemo(() => {
|
|
92
102
|
if (multiplayer !== undefined)
|
|
93
103
|
return multiplayer;
|
|
@@ -105,6 +115,9 @@ export function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultipla
|
|
|
105
115
|
}
|
|
106
116
|
return session;
|
|
107
117
|
}, [playable, resolvedGameId, wsUrl, multiplayer, resolveMultiplayer]);
|
|
118
|
+
if (creatorOpen && creator !== undefined && CreatorLazy !== null) {
|
|
119
|
+
return _jsx(Suspense, { fallback: null, children: _jsx(CreatorLazy, { gameId: resolvedGameId, config: creator, onExit: () => onCreatorOpenChange?.(false) }) });
|
|
120
|
+
}
|
|
108
121
|
if (editorOpen && EditorLazy !== null) {
|
|
109
122
|
return (_jsx(Suspense, { fallback: null, children: _jsx(EditorLazy, { gameId: resolvedGameId, playable: playable, catalogs: playable.editorCatalogs, onExitEditor: () => setEditorOpen(false) }) }));
|
|
110
123
|
}
|
package/dist/GamePlayerShell.js
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
3
3
|
import { createActionStateTracker, toActionStateBindingMap, } from "@jgengine/core/input/actionBindings";
|
|
4
4
|
import { RESERVED_INPUT_ACTIONS } from "./boundActionDispatch.js";
|
|
5
5
|
import { deriveTouchScheme, withTouchCodes, DEFAULT_TOUCH_STYLE } from "@jgengine/core/input/touchScheme";
|
|
6
6
|
import { activeTouchControlsMode } from "@jgengine/core/input/touchControlsMode";
|
|
7
7
|
import { normalizePointerToAxis } from "@jgengine/core/input/pointerAxis";
|
|
8
8
|
import { createGameContext } from "@jgengine/core/runtime/gameContext";
|
|
9
|
-
import {
|
|
9
|
+
import { localPlayers } from "@jgengine/core/runtime/localPlayers";
|
|
10
|
+
import { actionContextStack, activeActionCodes } from "@jgengine/core/game/controlGate";
|
|
10
11
|
import { useDisplayProfile } from "@jgengine/react/display";
|
|
11
12
|
import { RotateDeviceScreen } from "@jgengine/react/rotateDevice";
|
|
12
13
|
import { createSettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
13
14
|
import { applyBindingOverrides, clearBindingOverride, loadBindingOverrides, saveBindingOverride, } from "@jgengine/core/input/bindingOverrides";
|
|
14
15
|
import { orientationGateActive, orientationHintActive, resolveOrientationRequirement, } from "@jgengine/core/ui/orientation";
|
|
16
|
+
import { devtools } from "@jgengine/core/devtools/devtools";
|
|
15
17
|
import { armFallbackSeams } from "@jgengine/core/devtools/fallbackSeams";
|
|
16
18
|
import { armTextureErrors } from "@jgengine/core/devtools/textureErrors";
|
|
17
19
|
import { readUrlFlag, subscribeUrlChange, writeUrlParam } from "@jgengine/core/devtools/urlFlags";
|
|
18
20
|
import { createAudioEngine } from "./audio/audioEngine.js";
|
|
19
21
|
import { attachAudioEventWire } from "./audio/audioWire.js";
|
|
20
22
|
import { installAgentBridge } from "./devtools/agentBridge.js";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
+
import { attachPlaytestProbe } from "./devtools/playtestProbe.js";
|
|
24
|
+
import { withDevtoolsLatency } from "./devtools/latencyInstrumentation.js";
|
|
25
|
+
import { resolveRigKind } from "./camera/rigResolve.js";
|
|
23
26
|
import { contextModels } from "./render/resolveModel.js";
|
|
24
27
|
import { OrientationHint } from "./touch/OrientationHint.js";
|
|
25
28
|
import { useTouchStyle, useTouchJoystickVariant, useGraphicsSettings } from "./settings/appliedSettings.js";
|
|
@@ -28,10 +31,18 @@ import { EMPTY_RESERVED } from "./shellConstants.js";
|
|
|
28
31
|
import { JoinGate } from "./JoinGate.js";
|
|
29
32
|
import { useShellMultiplayerSync } from "./useShellMultiplayerSync.js";
|
|
30
33
|
import { ShellHudPresentation } from "./ShellHudPresentation.js";
|
|
31
|
-
import {
|
|
34
|
+
import { isServerAuthoritative } from "@jgengine/core/runtime/adapter";
|
|
35
|
+
import { playControlsActive } from "@jgengine/core/game/controlGate";
|
|
36
|
+
import { resolveInputSink } from "./inputSink.js";
|
|
37
|
+
import { observableShellTracker, attachShellInputPublication } from "./shellInputPublication.js";
|
|
38
|
+
import { attachShellControlSuspension } from "./shellControlSuspension.js";
|
|
39
|
+
import { LazyShell3dPresentation } from "./lazyShell3dPresentation.js";
|
|
40
|
+
import { createPresentationDiagnosticOwnership, createPlaySurfaceFocusOwnership, PresentationRecovery } from "./presentationRecovery.js";
|
|
32
41
|
const DEV_USER_ID = "dev-player";
|
|
33
42
|
/** The query param that mirrors the devtools overlay into the URL — present = open. */
|
|
34
43
|
const DEBUG_PARAM = "debug";
|
|
44
|
+
const NO_SUBSCRIBE = () => () => undefined;
|
|
45
|
+
const ZERO_VERSION = () => 0;
|
|
35
46
|
export { applyMotionImpulses } from "@jgengine/core/runtime/motionIntents";
|
|
36
47
|
export { nearbyObstacles } from "@jgengine/core/movement/movementModel";
|
|
37
48
|
export { resolvePhysicsTuning } from "@jgengine/core/movement/playerMovement";
|
|
@@ -70,8 +81,11 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
70
81
|
const posterSettledRef = useRef(false);
|
|
71
82
|
const [ctx, setCtx] = useState(null);
|
|
72
83
|
const [diagnostics, setDiagnostics] = useState([]);
|
|
84
|
+
const diagnosticSequence = useRef(0);
|
|
85
|
+
const presentationDiagnostics = useRef(createPresentationDiagnosticOwnership());
|
|
73
86
|
const [remotePlayers, setRemotePlayers] = useState([]);
|
|
74
87
|
const wrapperRef = useRef(null);
|
|
88
|
+
const recoveredFocus = useRef(createPlaySurfaceFocusOwnership());
|
|
75
89
|
const yawRef = useRef(0);
|
|
76
90
|
const pitchRef = useRef(0);
|
|
77
91
|
const serverIdRef = useRef(null);
|
|
@@ -90,7 +104,19 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
90
104
|
const effectiveInput = useMemo(() => applyBindingOverrides(playable.game.input ?? {}, bindingOverrides), [playable, bindingOverrides]);
|
|
91
105
|
const rebindAction = useCallback((action, code) => setBindingOverrides(saveBindingOverride(playable.game.name, action, [code])), [playable]);
|
|
92
106
|
const resetActionBinding = useCallback((action) => setBindingOverrides(clearBindingOverride(playable.game.name, action)), [playable]);
|
|
93
|
-
const
|
|
107
|
+
const contextStack = ctx === null ? null : actionContextStack(ctx);
|
|
108
|
+
const contextVersion = useSyncExternalStore(contextStack?.subscribe ?? NO_SUBSCRIBE, contextStack?.version ?? ZERO_VERSION, contextStack?.version ?? ZERO_VERSION);
|
|
109
|
+
const activeInput = useMemo(() => (ctx === null ? effectiveInput : activeActionCodes(ctx, effectiveInput)),
|
|
110
|
+
// contextVersion re-derives the layered map when a context is pushed or popped mid-game.
|
|
111
|
+
[ctx, effectiveInput, contextVersion]);
|
|
112
|
+
const activeBindingMap = useMemo(() => toActionStateBindingMap(withTouchCodes(activeInput)), [activeInput]);
|
|
113
|
+
// One tracker per context, rebound in place so keys held across a context swap stay held.
|
|
114
|
+
const tracker = useMemo(() => observableShellTracker(createActionStateTracker(activeBindingMap)), [ctx]);
|
|
115
|
+
const boundMapRef = useRef(activeBindingMap);
|
|
116
|
+
if (boundMapRef.current !== activeBindingMap) {
|
|
117
|
+
boundMapRef.current = activeBindingMap;
|
|
118
|
+
tracker.rebind(activeBindingMap);
|
|
119
|
+
}
|
|
94
120
|
const graphics = useGraphicsSettings(settingsStore, playable.shadows ?? true, playable.graphics);
|
|
95
121
|
const trackPointerAxis = (event) => {
|
|
96
122
|
const rect = wrapperRef.current?.getBoundingClientRect();
|
|
@@ -100,8 +126,10 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
100
126
|
};
|
|
101
127
|
const deactivatePointerAxis = () => {
|
|
102
128
|
const state = pointerAxisRef.current;
|
|
103
|
-
if (state !== null && state.active)
|
|
129
|
+
if (state !== null && state.active) {
|
|
104
130
|
pointerAxisRef.current = { ...state, active: false };
|
|
131
|
+
tracker.notify("discrete");
|
|
132
|
+
}
|
|
105
133
|
};
|
|
106
134
|
// Reads fresh every render: the ctx-version useSyncExternalStore below re-renders on store
|
|
107
135
|
// writes, so a gameplay setTouchControlsMode("car") swaps the visible control set that frame.
|
|
@@ -123,9 +151,31 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
123
151
|
onCodeUp: (code) => tracker.handleUp(code),
|
|
124
152
|
onAnalog: (values) => {
|
|
125
153
|
analogRef.current = values;
|
|
154
|
+
tracker.notify("analog");
|
|
126
155
|
},
|
|
127
156
|
}), [tracker]);
|
|
157
|
+
useLayoutEffect(() => {
|
|
158
|
+
if (ctx === null)
|
|
159
|
+
return;
|
|
160
|
+
return attachShellControlSuspension({
|
|
161
|
+
ctx, tracker, pointerAxisRef, analogRef, primaryClickRef, cameraDraggingRef, f2HeldRef,
|
|
162
|
+
});
|
|
163
|
+
}, [ctx, tracker]);
|
|
128
164
|
const gateRef = useRef(false);
|
|
165
|
+
useLayoutEffect(() => {
|
|
166
|
+
if (ctx === null || poster)
|
|
167
|
+
return;
|
|
168
|
+
return attachShellInputPublication({
|
|
169
|
+
ctx, tracker,
|
|
170
|
+
active: () => !gateRef.current && playControlsActive(ctx),
|
|
171
|
+
analog: () => analogRef.current,
|
|
172
|
+
pointer: () => pointerAxisRef.current,
|
|
173
|
+
sink: () => resolveInputSink({
|
|
174
|
+
serverAuthoritative: isServerAuthoritative(playable.game.multiplayer) && multiplayer !== null,
|
|
175
|
+
backend: multiplayer?.backend ?? null, serverId: serverIdRef.current,
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
}, [ctx, tracker, multiplayer, playable, poster]);
|
|
129
179
|
const orientationPlatform = coarsePointer ? "mobile" : "desktop";
|
|
130
180
|
const orientationRequirement = useMemo(() => resolveOrientationRequirement(playable.orientation, orientationPlatform), [playable, orientationPlatform]);
|
|
131
181
|
const liveOrientation = portrait ? "portrait" : "landscape";
|
|
@@ -135,6 +185,8 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
135
185
|
const orientationGateEl = orientationGate ? (_jsx(RotateDeviceScreen, { requiredOrientation: orientationRequirement.required ?? "landscape", title: orientationRequirement.required === "portrait" ? "Turn your phone upright" : "Turn your phone sideways", description: `${playable.game.name} is built for ${orientationRequirement.required ?? "landscape"} play.` })) : orientationHint && orientationRequirement.preferred !== null ? (_jsx(OrientationHint, { wanted: orientationRequirement.preferred })) : null;
|
|
136
186
|
const audioEngine = useMemo(() => createAudioEngine({
|
|
137
187
|
sounds: playable.audio?.sounds,
|
|
188
|
+
maxVoices: playable.audio?.maxVoices,
|
|
189
|
+
voiceOverflow: playable.audio?.voiceOverflow,
|
|
138
190
|
buses: playable.audio?.buses,
|
|
139
191
|
music: playable.audio?.music,
|
|
140
192
|
musicBus: playable.audio?.musicBus,
|
|
@@ -170,10 +222,59 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
170
222
|
const userId = multiplayer?.userId ?? DEV_USER_ID;
|
|
171
223
|
const reportRuntimeError = (error, phase, componentStack) => {
|
|
172
224
|
const diagnostic = logRuntimeError(error, phase, componentStack);
|
|
173
|
-
|
|
225
|
+
const id = ++diagnosticSequence.current;
|
|
226
|
+
presentationDiagnostics.current.register(error, id, phase);
|
|
227
|
+
setDiagnostics((current) => [...current.slice(-4), { ...diagnostic, id }]);
|
|
228
|
+
};
|
|
229
|
+
const recoveryTrace = useRef([]);
|
|
230
|
+
const traceRecovery = (event) => {
|
|
231
|
+
if (!devtoolsEnabled)
|
|
232
|
+
return;
|
|
233
|
+
const wrapper = wrapperRef.current;
|
|
234
|
+
const active = document.activeElement;
|
|
235
|
+
const rect = wrapper?.getBoundingClientRect();
|
|
236
|
+
recoveryTrace.current.push({ ...event, at: performance.now(), connected: wrapper?.isConnected ?? false,
|
|
237
|
+
rects: wrapper?.getClientRects().length ?? 0, width: rect?.width ?? null, height: rect?.height ?? null,
|
|
238
|
+
display: wrapper === null ? null : getComputedStyle(wrapper).display,
|
|
239
|
+
active: active?.tagName ?? null, ownsFocus: active === wrapper, focusInside: wrapper?.contains(active) ?? false });
|
|
240
|
+
};
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
recoveryTrace.current = [];
|
|
243
|
+
if (!devtoolsEnabled)
|
|
244
|
+
return;
|
|
245
|
+
return devtools.probes.register("presentationRecovery", () => recoveryTrace.current);
|
|
246
|
+
}, [ctx, devtoolsEnabled]);
|
|
247
|
+
const revealRecoveredPresentation = () => {
|
|
248
|
+
if (ctx === null)
|
|
249
|
+
return;
|
|
250
|
+
traceRecovery({ phase: "shell-reveal-layout" });
|
|
251
|
+
recoveredFocus.current.reveal(ctx, wrapperRef.current, document.activeElement, document.body);
|
|
252
|
+
traceRecovery({ phase: "shell-focus-result" });
|
|
253
|
+
};
|
|
254
|
+
const retireRecoveredPresentation = (error) => {
|
|
255
|
+
traceRecovery({ phase: "shell-recovered-callback" });
|
|
256
|
+
const owned = presentationDiagnostics.current.recovered(error);
|
|
257
|
+
setDiagnostics((current) => current.filter((diagnostic) => !owned.has(diagnostic.id)));
|
|
258
|
+
if (ctx !== null)
|
|
259
|
+
recoveredFocus.current.request(ctx);
|
|
260
|
+
revealRecoveredPresentation();
|
|
174
261
|
};
|
|
175
262
|
useEffect(() => {
|
|
176
263
|
setDiagnostics([]);
|
|
264
|
+
let detachProbe = () => { };
|
|
265
|
+
let ownedContext = null;
|
|
266
|
+
const disposeOwnedContext = () => {
|
|
267
|
+
const context = ownedContext;
|
|
268
|
+
ownedContext = null;
|
|
269
|
+
if (context === null)
|
|
270
|
+
return;
|
|
271
|
+
try {
|
|
272
|
+
playable.loop.onDispose(context);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
reportRuntimeError(error, "dispose");
|
|
276
|
+
}
|
|
277
|
+
};
|
|
177
278
|
try {
|
|
178
279
|
const models = contextModels(playable);
|
|
179
280
|
const context = createGameContext({
|
|
@@ -182,17 +283,31 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
182
283
|
player: { userId, isNew: true },
|
|
183
284
|
...(models === undefined ? {} : { models }),
|
|
184
285
|
});
|
|
286
|
+
ownedContext = context;
|
|
287
|
+
if (playable.localPlayers !== undefined)
|
|
288
|
+
localPlayers(context).retune(playable.localPlayers);
|
|
185
289
|
playable.loop.onInit(context);
|
|
186
290
|
playable.loop.onNewPlayer(context);
|
|
187
291
|
onContextReady?.(context);
|
|
292
|
+
const probe = playable.capture?.probe;
|
|
293
|
+
if (probe !== undefined && typeof window !== "undefined") {
|
|
294
|
+
detachProbe = attachPlaytestProbe(window, () => probe(context));
|
|
295
|
+
}
|
|
188
296
|
setCtx(context);
|
|
189
297
|
}
|
|
190
298
|
catch (error) {
|
|
299
|
+
disposeOwnedContext();
|
|
191
300
|
reportRuntimeError(error, "init");
|
|
192
301
|
setCtx(null);
|
|
193
302
|
}
|
|
194
303
|
return () => {
|
|
195
|
-
|
|
304
|
+
try {
|
|
305
|
+
detachProbe();
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
disposeOwnedContext();
|
|
309
|
+
setCtx(null);
|
|
310
|
+
}
|
|
196
311
|
};
|
|
197
312
|
}, [playable, userId]);
|
|
198
313
|
const authoritativeFrameRef = useRef(null);
|
|
@@ -224,7 +339,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
224
339
|
touchSink,
|
|
225
340
|
touchJoystickVariant,
|
|
226
341
|
analogRef,
|
|
227
|
-
inputBindings:
|
|
342
|
+
inputBindings: activeInput,
|
|
228
343
|
orientationGate,
|
|
229
344
|
orientationGateEl,
|
|
230
345
|
coarsePointer,
|
|
@@ -237,12 +352,12 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
237
352
|
deactivatePointerAxis,
|
|
238
353
|
};
|
|
239
354
|
if (rigKind === "none" || playable.presentation === "hud") {
|
|
240
|
-
return (_jsx(ShellHudPresentation, { ...shared, uiScale: graphics.uiScale, onPointerResumeAudio: () => audioEngine.resume() }));
|
|
355
|
+
return (_jsx(PresentationRecovery, { ctx: ctx, onRuntimeError: reportRuntimeError, onRetryCommitted: () => wrapperRef.current?.focus({ preventScroll: true }), onRecoveredDraw: retireRecoveredPresentation, onRecoveryEvent: traceRecovery, children: _jsx(ShellHudPresentation, { ...shared, serverIdRef: serverIdRef, uiScale: graphics.uiScale, onPointerResumeAudio: () => audioEngine.resume(), settingsStore: settingsStore, bindingOverrides: bindingOverrides, rebindAction: rebindAction, resetActionBinding: resetActionBinding, audioEngine: audioEngine, poster: poster }) }));
|
|
241
356
|
}
|
|
242
|
-
return (_jsx(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
357
|
+
return (_jsx(PresentationRecovery, { ctx: ctx, onRuntimeError: reportRuntimeError, onRetryCommitted: () => wrapperRef.current?.focus({ preventScroll: true }), onRecoveredDraw: retireRecoveredPresentation, onRecoveryEvent: traceRecovery, children: _jsx(LazyShell3dPresentation, { ...shared, onPresentationReveal: revealRecoveredPresentation, primaryClickRef: primaryClickRef, cameraDraggingRef: cameraDraggingRef, serverIdRef: serverIdRef, remotePlayers: remotePlayers, touchStyle: touchStyle, compact: compact, graphics: graphics, settingsStore: settingsStore, bindingOverrides: bindingOverrides, rebindAction: rebindAction, resetActionBinding: resetActionBinding, audioEngine: audioEngine, poster: poster, posterFrozen: posterFrozen, onPosterSettled: () => {
|
|
358
|
+
if (posterSettledRef.current)
|
|
359
|
+
return;
|
|
360
|
+
posterSettledRef.current = true;
|
|
361
|
+
setPosterFrozen(true);
|
|
362
|
+
}, authoritativeFrameRef: authoritativeFrameRef }) }));
|
|
248
363
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { type Dispatch, type MutableRefObject, type RefObject, type SetStateAction } from "react";
|
|
2
2
|
import type { PointerAxisState } from "@jgengine/core/input/pointerAxis";
|
|
3
|
-
import type {
|
|
3
|
+
import type { observableShellTracker } from "./shellInputPublication.js";
|
|
4
4
|
import type { BindingOverrides } from "@jgengine/core/input/bindingOverrides";
|
|
5
5
|
import type { TouchJoystickVariant, TouchScheme, TouchStyle } from "@jgengine/core/input/touchScheme";
|
|
6
|
-
import type
|
|
7
|
-
import { type GraphicsQuality, type SettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
6
|
+
import { type SettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
8
7
|
import type { PresencePoseRow } from "@jgengine/core/runtime/transport";
|
|
9
8
|
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
10
9
|
import type { AudioEngine } from "./audio/audioEngine.js";
|
|
@@ -12,13 +11,14 @@ import type { ShellMultiplayer } from "./multiplayer.js";
|
|
|
12
11
|
import type { PlayableGame } from "./registry.js";
|
|
13
12
|
import type { AuthoritativeFrameHandler } from "./worldSync.js";
|
|
14
13
|
import { type TouchCodeSink } from "./touch/TouchControlsOverlay.js";
|
|
14
|
+
import { type AppliedGraphicsSettings } from "./settings/appliedSettings.js";
|
|
15
15
|
import type { RuntimeDiagnostic } from "./diagnostics/RuntimeDiagnostics.js";
|
|
16
16
|
/** 3D play surface: canvas, world overlays, and shared chrome. @internal */
|
|
17
17
|
export declare function Shell3dPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, primaryClickRef, cameraDraggingRef, serverIdRef, remotePlayers, touchScheme, touchSink, touchStyle, touchJoystickVariant, analogRef, inputBindings, orientationGate, orientationGateEl, coarsePointer, compact, graphics, settingsStore, bindingOverrides, rebindAction, resetActionBinding, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, audioEngine, poster, posterFrozen, onPosterSettled, authoritativeFrameRef, }: {
|
|
18
18
|
playable: PlayableGame;
|
|
19
19
|
ctx: GameContext;
|
|
20
20
|
multiplayer: ShellMultiplayer | null;
|
|
21
|
-
tracker:
|
|
21
|
+
tracker: ReturnType<typeof observableShellTracker>;
|
|
22
22
|
pointerAxisRef: MutableRefObject<PointerAxisState | null>;
|
|
23
23
|
gateRef: MutableRefObject<boolean>;
|
|
24
24
|
wrapperRef: RefObject<HTMLDivElement | null>;
|
|
@@ -39,13 +39,7 @@ export declare function Shell3dPresentation({ playable, ctx, multiplayer, tracke
|
|
|
39
39
|
orientationGateEl: React.ReactNode;
|
|
40
40
|
coarsePointer: boolean;
|
|
41
41
|
compact: boolean;
|
|
42
|
-
graphics:
|
|
43
|
-
shadows: boolean;
|
|
44
|
-
dpr: number;
|
|
45
|
-
uiScale: number;
|
|
46
|
-
quality: GraphicsQuality;
|
|
47
|
-
profile: GraphicsProfile;
|
|
48
|
-
};
|
|
42
|
+
graphics: AppliedGraphicsSettings;
|
|
49
43
|
settingsStore: SettingsStore;
|
|
50
44
|
bindingOverrides: BindingOverrides;
|
|
51
45
|
rebindAction: (action: string, code: string) => void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Canvas } from "@react-three/fiber";
|
|
3
|
-
import { useMemo, useRef, useState, } from "react";
|
|
3
|
+
import { useMemo, useEffect, useRef, useState, useSyncExternalStore, } from "react";
|
|
4
4
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
5
5
|
import { contextVerbInput, } from "@jgengine/core/interaction/contextMenu";
|
|
6
6
|
import { createSelectionSet, isMarquee, screenRect, selectWithinRect, } from "@jgengine/core/scene/selection";
|
|
@@ -15,7 +15,9 @@ import { GameViewportProvider } from "@jgengine/react/gameViewport";
|
|
|
15
15
|
import { GameProvider } from "@jgengine/react/provider";
|
|
16
16
|
import { SettingsProvider } from "@jgengine/react/settings";
|
|
17
17
|
import { resolveWorldSky } from "./worldSky.js";
|
|
18
|
+
import { resolveRuntimeCameraConfig } from "./camera/rigResolve.js";
|
|
18
19
|
import { pointerAimFor, pointerContextMenu } from "./shellPointer.js";
|
|
20
|
+
import { shellPointerInput } from "./shellPointerInput.js";
|
|
19
21
|
import { AudioListener, EntityAudioEmitters, ObjectAudioEmitters } from "./audio/AudioComponents.js";
|
|
20
22
|
import { PostProcessing } from "./postfx/PostProcessing.js";
|
|
21
23
|
import { EnvironmentLighting } from "./render/EnvironmentLighting.js";
|
|
@@ -23,7 +25,9 @@ import { CollisionDebugWorld } from "./devtools/CollisionDebugWorld.js";
|
|
|
23
25
|
import { DevtoolsRendererProbe } from "./devtools/DevtoolsOverlay.js";
|
|
24
26
|
import { CAMERA_TRANSPARENT_USERDATA, GameCameraRig, PlayerFovProvider, PlayerFovSlider, resolveRigKind, rtsPanKeysConflict, } from "./camera/index.js";
|
|
25
27
|
import { CullingProvider } from "./visibility/CullingProvider.js";
|
|
28
|
+
import { BuildingRenderBudget } from "./structures/BuildingRenderBudget.js";
|
|
26
29
|
import { SkyDaylight, TimeOfDayDaylight } from "./environment.js";
|
|
30
|
+
import { bearingFromDirection } from "./environment/daylightCycle.js";
|
|
27
31
|
import { resolveSkyLightOwnership, skyEmitsLights } from "./environment/skyLightingPolicy.js";
|
|
28
32
|
import { EnvironmentScene } from "./environment/EnvironmentScene.js";
|
|
29
33
|
import { PointerProbe } from "./pointer/PointerProbe.js";
|
|
@@ -37,10 +41,15 @@ import { TouchControlsDock, TouchPlaySurface, touchDockClearance } from "./touch
|
|
|
37
41
|
import { SettingsRuntime } from "./settings/SettingsRuntime.js";
|
|
38
42
|
import { SettingsChrome } from "./settings/SettingsChrome.js";
|
|
39
43
|
import { AudioSettingsBridge } from "./settings/appliedSettings.js";
|
|
44
|
+
import { TextureFiltering } from "./render/TextureFiltering.js";
|
|
45
|
+
import { FrameRateLimiter } from "./drivers/FrameRateLimiter.js";
|
|
46
|
+
import { FpsCounter, FpsProbe } from "./diagnostics/FpsCounter.js";
|
|
40
47
|
import { ConfiguredLighting, BackdropFog } from "./render/SceneLighting.js";
|
|
41
48
|
import { WorldView, RemotePlayers } from "./world/WorldScene.js";
|
|
42
49
|
import { FrameDriver } from "./drivers/FrameDriver.js";
|
|
43
50
|
import { GamepadSource } from "./input/gamepadSource.js";
|
|
51
|
+
import { SplitScreenRenderer, useSeatViewports } from "./camera/Viewports.js";
|
|
52
|
+
import { localPlayers } from "@jgengine/core/runtime/localPlayers";
|
|
44
53
|
import { createShellKeyHandlers, ShellDebugOverlays, ShellGameUiChrome } from "./ShellChrome.js";
|
|
45
54
|
import { CombatPresentation } from "./CombatPresentation.js";
|
|
46
55
|
import { WorldParticles } from "./world/WorldParticles.js";
|
|
@@ -70,16 +79,19 @@ function PixelPerfectCamera({ pixelsPerUnit, integerScale }) {
|
|
|
70
79
|
export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, primaryClickRef, cameraDraggingRef, serverIdRef, remotePlayers, touchScheme, touchSink, touchStyle, touchJoystickVariant, analogRef, inputBindings, orientationGate, orientationGateEl, coarsePointer, compact, graphics, settingsStore, bindingOverrides, rebindAction, resetActionBinding, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, audioEngine, poster, posterFrozen, onPosterSettled, authoritativeFrameRef, }) {
|
|
71
80
|
const GameUI = playable.GameUI;
|
|
72
81
|
const ktx2SupportDetected = useRef(false);
|
|
82
|
+
const frameTally = useRef({ frames: 0 }).current;
|
|
73
83
|
const WorldOverlay = playable.WorldOverlay;
|
|
74
84
|
const pointerService = useMemo(() => createPointerService(), []);
|
|
75
85
|
const selection = useMemo(() => createSelectionSet(), [playable]);
|
|
76
86
|
const controlledEntityId = ctx.player.possession.active(ctx.player.userId);
|
|
77
|
-
const
|
|
87
|
+
const cameraOverride = useSyncExternalStore(ctx.camera.subscribe, ctx.camera.rig, ctx.camera.rig);
|
|
88
|
+
const configuredCamera = playable.camera?.followEntityId !== undefined
|
|
78
89
|
? playable.camera
|
|
79
90
|
: { ...playable.camera, followEntityId: controlledEntityId };
|
|
91
|
+
const cameraConfig = resolveRuntimeCameraConfig(configuredCamera, cameraOverride);
|
|
80
92
|
const rigKind = resolveRigKind(cameraConfig);
|
|
81
93
|
const firstPerson = rigKind === "first";
|
|
82
|
-
const showReticle = (firstPerson &&
|
|
94
|
+
const showReticle = (firstPerson && cameraConfig?.firstPerson?.reticle !== false) || rigKind === "shoulder";
|
|
83
95
|
const rtsPanKeysEnabled = !rtsPanKeysConflict(playable.game.input);
|
|
84
96
|
const bars = resolveWorldOverlayBars(playable.worldHealthBars);
|
|
85
97
|
const nameplates = resolveWorldOverlayBars(playable.nameplates);
|
|
@@ -109,9 +121,13 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
|
|
|
109
121
|
const cinematicLook = (playable.look ?? "cinematic") !== "flat";
|
|
110
122
|
const backdrop = resolvedLook.backdrop;
|
|
111
123
|
const backdropSky = backdrop?.sky !== undefined ? resolveSkyDescriptor(backdrop.sky) : undefined;
|
|
112
|
-
const
|
|
113
|
-
const backgroundColor = backdrop?.background ?? (effectiveSky === undefined ? DEFAULT_BACKGROUND_COLOR : undefined);
|
|
124
|
+
const authoredSky = backdropSky ?? worldSky;
|
|
114
125
|
const lighting = resolvedLook.lighting;
|
|
126
|
+
const sunFromLighting = lighting?.directional?.[0]?.position;
|
|
127
|
+
const effectiveSky = useMemo(() => authoredSky === undefined || authoredSky.sun !== undefined || authoredSky.timeOfDay || sunFromLighting === undefined
|
|
128
|
+
? authoredSky
|
|
129
|
+
: { ...authoredSky, sun: bearingFromDirection(sunFromLighting) }, [authoredSky, sunFromLighting]);
|
|
130
|
+
const backgroundColor = backdrop?.background ?? (effectiveSky === undefined ? DEFAULT_BACKGROUND_COLOR : undefined);
|
|
115
131
|
const orthographic = playable.camera?.projection === "orthographic";
|
|
116
132
|
const pointerDownRef = useRef(null);
|
|
117
133
|
const marqueeStartRef = useRef(null);
|
|
@@ -141,7 +157,36 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
|
|
|
141
157
|
};
|
|
142
158
|
/** DOM UI sits in the same wrapper as the canvas — only canvas-targeted clicks are world input. */
|
|
143
159
|
const isWorldPointerTarget = (event) => event.target instanceof HTMLCanvasElement;
|
|
160
|
+
const pointerInput = useMemo(() => shellPointerInput(tracker, () => playControlsActive(ctx)), [tracker, ctx]);
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
const clearGesture = () => { pointerDownRef.current = null; marqueeStartRef.current = null; setMarquee(null); };
|
|
163
|
+
const up = (event) => {
|
|
164
|
+
pointerInput.up(event);
|
|
165
|
+
if (!(event.target instanceof Node) || !wrapperRef.current?.contains(event.target))
|
|
166
|
+
clearGesture();
|
|
167
|
+
};
|
|
168
|
+
const cancel = (event) => { pointerInput.cancel(event.pointerId); clearGesture(); };
|
|
169
|
+
const retire = () => { pointerInput.cancel(); clearGesture(); };
|
|
170
|
+
const blur = () => { tracker.reset(); retire(); };
|
|
171
|
+
const move = (event) => { pointerInput.move(event, false); };
|
|
172
|
+
const detachReset = pointerInput.attachReset(tracker, clearGesture);
|
|
173
|
+
window.addEventListener("pointerup", up);
|
|
174
|
+
window.addEventListener("pointermove", move);
|
|
175
|
+
window.addEventListener("pointercancel", cancel);
|
|
176
|
+
window.addEventListener("blur", blur);
|
|
177
|
+
return () => {
|
|
178
|
+
window.removeEventListener("pointerup", up);
|
|
179
|
+
window.removeEventListener("pointermove", move);
|
|
180
|
+
window.removeEventListener("pointercancel", cancel);
|
|
181
|
+
window.removeEventListener("blur", blur);
|
|
182
|
+
detachReset();
|
|
183
|
+
pointerInput.cancel();
|
|
184
|
+
};
|
|
185
|
+
}, [pointerInput, tracker]);
|
|
144
186
|
const handlePointerDown = (event) => {
|
|
187
|
+
pointerInput.down(event, isWorldPointerTarget(event));
|
|
188
|
+
if (!playControlsActive(ctx))
|
|
189
|
+
return;
|
|
145
190
|
if (isWorldPointerTarget(event))
|
|
146
191
|
wrapperRef.current?.focus();
|
|
147
192
|
trackPointerAxis(event);
|
|
@@ -156,6 +201,7 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
|
|
|
156
201
|
}
|
|
157
202
|
};
|
|
158
203
|
const handlePointerMove = (event) => {
|
|
204
|
+
pointerInput.move(event, isWorldPointerTarget(event));
|
|
159
205
|
trackPointerAxis(event);
|
|
160
206
|
if (pointer?.select !== true)
|
|
161
207
|
return;
|
|
@@ -166,6 +212,13 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
|
|
|
166
212
|
setMarquee(screenRect(start.x, start.y, point.x, point.y));
|
|
167
213
|
};
|
|
168
214
|
const handlePointerUp = (event) => {
|
|
215
|
+
const boundPointer = pointerInput.up(event);
|
|
216
|
+
if (!playControlsActive(ctx)) {
|
|
217
|
+
pointerDownRef.current = null;
|
|
218
|
+
marqueeStartRef.current = null;
|
|
219
|
+
setMarquee(null);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
169
222
|
if (event.button !== 0 || pointerDownRef.current === null)
|
|
170
223
|
return;
|
|
171
224
|
const start = pointerDownRef.current;
|
|
@@ -216,7 +269,7 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
|
|
|
216
269
|
return;
|
|
217
270
|
}
|
|
218
271
|
const moved = (end.x - start.x) ** 2 + (end.y - start.y) ** 2;
|
|
219
|
-
if (moved <= PRIMARY_CLICK_MOVE_THRESHOLD_PX * PRIMARY_CLICK_MOVE_THRESHOLD_PX)
|
|
272
|
+
if (!boundPointer && moved <= PRIMARY_CLICK_MOVE_THRESHOLD_PX * PRIMARY_CLICK_MOVE_THRESHOLD_PX)
|
|
220
273
|
primaryClickRef.current = true;
|
|
221
274
|
};
|
|
222
275
|
const handleContextMenu = (event) => {
|
|
@@ -258,6 +311,8 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
|
|
|
258
311
|
}
|
|
259
312
|
};
|
|
260
313
|
const controlsActive = playControlsActive(ctx);
|
|
314
|
+
const seatViewports = useSeatViewports(ctx, playable.viewports);
|
|
315
|
+
const splitScreen = playable.viewports !== undefined && seatViewports.length > 1;
|
|
261
316
|
const settingsDisabled = playable.settings === false;
|
|
262
317
|
const settingsConfig = playable.settings === false || playable.settings === undefined ? {} : playable.settings;
|
|
263
318
|
const settingsSurface = settingsDisabled ? false : settingsConfig.surface ?? false;
|
|
@@ -289,7 +344,7 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
|
|
|
289
344
|
controlsActive: () => playControlsActive(ctx),
|
|
290
345
|
exitPointerLockOnDevtools: true,
|
|
291
346
|
});
|
|
292
|
-
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 &&
|
|
347
|
+
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, graphics: playable.graphics, hideBindings: settingsConfig.hideBindings ?? [], touchStyle: coarsePointer &&
|
|
293
348
|
touchScheme !== null &&
|
|
294
349
|
(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: {
|
|
295
350
|
"--jg-hud-dock-clearance": `${dockMounted ? touchDockClearance(touchScheme, touchScale) : 0}px`,
|
|
@@ -308,7 +363,7 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
|
|
|
308
363
|
return;
|
|
309
364
|
ktx2SupportDetected.current = true;
|
|
310
365
|
detectKtx2Support(gl);
|
|
311
|
-
}, frameloop: poster && posterFrozen ? "demand" : "always", orthographic: orthographic, camera: orthographic
|
|
366
|
+
}, frameloop: poster && posterFrozen ? "demand" : graphics.frameRateLimit > 0 && !poster ? "never" : "always", orthographic: orthographic, camera: orthographic
|
|
312
367
|
? {
|
|
313
368
|
zoom: playable.camera?.frustum?.zoom ?? CAMERA_FRUSTUM_DEFAULTS.zoom,
|
|
314
369
|
near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
|
|
@@ -318,9 +373,16 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
|
|
|
318
373
|
fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
|
|
319
374
|
near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
|
|
320
375
|
far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
|
|
321
|
-
}, shadows: graphics.shadows, dpr: [Math.min(1, graphics.dpr), graphics.dpr], gl: { preserveDrawingBuffer: true }, style: { touchAction: "none" }, children: [orthographic && playable.camera?.pixelPerfect !== undefined ? (_jsx(PixelPerfectCamera, { ...playable.camera.pixelPerfect })) : null, backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, cinematicLook ? (_jsx(EnvironmentLighting, { source: playable.environmentSource, skyColor: lighting?.hemisphere?.skyColor ?? "#87b5e0", groundColor: lighting?.hemisphere?.groundColor ?? "#3d4a38", sunDirection: lighting?.directional?.[0]?.position, sunColor: lighting?.directional?.[0]?.color ?? "#fff2d6" })) : null, lighting !== undefined ? (_jsx(ConfiguredLighting, { lighting: lighting, profile: graphics.profile })) : 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 }),
|
|
322
|
-
|
|
323
|
-
|
|
376
|
+
}, shadows: graphics.shadows, dpr: [Math.min(1, graphics.dpr), graphics.dpr], gl: { preserveDrawingBuffer: true, powerPreference: "high-performance" }, style: { touchAction: "none" }, children: [_jsx(TextureFiltering, { anisotropy: graphics.profile.anisotropy }), graphics.frameRateLimit > 0 && !poster ? _jsx(FrameRateLimiter, { fps: graphics.frameRateLimit }) : null, graphics.showFps ? _jsx(FpsProbe, { tally: frameTally }) : null, orthographic && playable.camera?.pixelPerfect !== undefined ? (_jsx(PixelPerfectCamera, { ...playable.camera.pixelPerfect })) : null, backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, cinematicLook ? (_jsx(EnvironmentLighting, { source: playable.environmentSource, skyColor: lighting?.hemisphere?.skyColor ?? "#87b5e0", groundColor: lighting?.hemisphere?.groundColor ?? "#3d4a38", sunDirection: lighting?.directional?.[0]?.position, sunColor: lighting?.directional?.[0]?.color ?? "#fff2d6" })) : null, lighting !== undefined ? (_jsx(ConfiguredLighting, { lighting: lighting, profile: graphics.profile })) : 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 }), _jsx(GameProvider, { context: ctx, children: _jsxs(BuildingRenderBudget, { drawDistance: graphics.profile.drawDistance, visibility: playable.visibility, cullingDisabled: splitScreen, children: [_jsxs(CullingProvider, { config: playable.visibility, drawDistance: graphics.profile.drawDistance, 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, editorLayers: playable.editorLayers }), bars !== null ? (_jsx(WorldEntityBars, { statId: bars.statId, roles: bars.roles, resolveRole: resolveEntityRole, ...(bars.maxSamples === undefined ? {} : { maxSamples: bars.maxSamples }), ...(bars.tickMs === undefined ? {} : { tickMs: bars.tickMs }), ...(bars.maxDistance === undefined ? {} : { maxDistance: bars.maxDistance }), ...(bars.occlude === undefined ? {} : { occlude: bars.occlude }) })) : null, nameplates !== null ? (_jsx(WorldNameplates, { statId: nameplates.statId, roles: nameplates.roles, resolveRole: resolveEntityRole, ...(nameplates.maxSamples === undefined ? {} : { maxSamples: nameplates.maxSamples }), ...(nameplates.tickMs === undefined ? {} : { tickMs: nameplates.tickMs }), ...(nameplates.maxDistance === undefined ? {} : { maxDistance: nameplates.maxDistance }), occlude: nameplates.occlude, ...(nameplates.resolveName === undefined ? {} : { resolveName: nameplates.resolveName }), ...(nameplates.showHealth === undefined ? {} : { showHealth: nameplates.showHealth }) })) : null, WorldOverlay !== undefined ? (_jsx("group", { userData: CAMERA_TRANSPARENT_USERDATA, children: _jsx(WorldOverlay, { ctx: ctx }) })) : null] }), _jsx(WorldItems, { config: playable.worldItem }), _jsx(WorldParticles, { quality: graphics.quality, particleCap: graphics.profile.particleCap, diagnostics: devtoolsEnabled }), _jsx(CombatPresentation, { effects: effects }), devtoolsEnabled ? _jsx(CollisionDebugWorld, {}) : null, _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) => {
|
|
377
|
+
cameraDraggingRef.current = dragging;
|
|
378
|
+
} }), _jsx(PointerProbe, { service: pointerService })] }) }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, pointerAxisRef: pointerAxisRef, analogRef: analogRef, gateRef: gateRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand, poster: poster, onPosterSettled: onPosterSettled, authoritativeFrameRef: authoritativeFrameRef }), _jsx(GamepadSource, { tracker: tracker, bindings: inputBindings ?? {}, analogRef: analogRef, input: ctx.input, feel: playable.gamepad, seats: localPlayers(ctx), onSeatJoin: (slot) => {
|
|
379
|
+
try {
|
|
380
|
+
playable.loop.onNewPlayer(ctx, { userId: slot.userId, isNew: true });
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
reportRuntimeError(error, "tick");
|
|
384
|
+
}
|
|
385
|
+
}, seatsActive: () => !gateRef.current && playControlsActive(ctx) }), _jsx(DevtoolsRendererProbe, {}), splitScreen ? (_jsx(SplitScreenRenderer, { ctx: ctx, viewports: seatViewports, config: playable.viewports, mainCamera: playable.camera })) : null, !splitScreen && resolvedLook.postProcessing !== undefined && resolvedLook.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: resolvedLook.postProcessing, quality: graphics.quality, stages: graphics.profile.postStages })) : null] }), graphics.showFps && !poster ? _jsx(FpsCounter, { tally: frameTally }) : null, !poster &&
|
|
324
386
|
!orientationGate &&
|
|
325
387
|
coarsePointer &&
|
|
326
388
|
controlsActive &&
|
package/dist/ShellChrome.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ComponentType } from "react";
|
|
2
2
|
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
3
3
|
import { type RuntimeDiagnostic } from "./diagnostics/RuntimeDiagnostics.js";
|
|
4
4
|
import type { ShellMultiplayer } from "./multiplayer.js";
|
|
@@ -27,6 +27,7 @@ export type ShellKeyHandlers = {
|
|
|
27
27
|
onKeyDown: (event: {
|
|
28
28
|
code: string;
|
|
29
29
|
target?: EventTarget | null;
|
|
30
|
+
defaultPrevented?: boolean;
|
|
30
31
|
preventDefault: () => void;
|
|
31
32
|
}) => void;
|
|
32
33
|
onKeyUp: (event: {
|