@jgengine/shell 0.13.0 → 0.15.0-next.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 +529 -1
- package/dist/CombatPresentation.js +2 -1
- package/dist/GamePlayerShell.js +25 -5
- package/dist/Shell3dPresentation.d.ts +6 -6
- package/dist/Shell3dPresentation.js +4 -4
- package/dist/camera/ControllerCameraShake.d.ts +31 -0
- package/dist/camera/ControllerCameraShake.js +34 -0
- package/dist/camera/GameCameraRig.js +10 -5
- package/dist/camera/GameOrbitCamera.js +1 -10
- package/dist/camera/cameraRigs.js +16 -4
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigMath.d.ts +2 -0
- package/dist/camera/rigMath.js +7 -0
- package/dist/devtools/CollisionDebugWorld.js +15 -1
- package/dist/devtools/DevtoolsOverlay.js +2 -0
- package/dist/devtools/collisionDebugMath.d.ts +8 -0
- package/dist/devtools/collisionDebugMath.js +14 -5
- package/dist/drivers/FrameDriver.d.ts +4 -1
- package/dist/drivers/FrameDriver.js +5 -3
- package/dist/environment/DayNightSky.d.ts +33 -0
- package/dist/environment/DayNightSky.js +51 -0
- package/dist/environment/Daylight.d.ts +3 -1
- package/dist/environment/Daylight.js +7 -3
- package/dist/environment/EnvironmentScene.js +6 -6
- package/dist/environment/RoadJunctions.d.ts +8 -4
- package/dist/environment/RoadJunctions.js +16 -15
- package/dist/environment/RoadRibbons.d.ts +42 -6
- package/dist/environment/RoadRibbons.js +150 -27
- package/dist/environment/VolumetricClouds.js +3 -3
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/gameKit.d.ts +7 -4
- package/dist/gameKit.js +8 -5
- package/dist/inputSink.js +14 -0
- package/dist/materialOverride.js +30 -0
- package/dist/postfx/PostProcessing.d.ts +3 -2
- package/dist/postfx/PostProcessing.js +38 -5
- package/dist/postfx/ScreenEffectsOverlay.d.ts +32 -0
- package/dist/postfx/ScreenEffectsOverlay.js +70 -0
- package/dist/postfx/postfxOverlay.d.ts +24 -0
- package/dist/postfx/postfxOverlay.js +35 -0
- package/dist/presentationResolve.d.ts +3 -2
- package/dist/presentationResolve.js +11 -6
- package/dist/render/CascadedShadows.d.ts +13 -0
- package/dist/render/CascadedShadows.js +96 -0
- package/dist/render/EntityPreview.d.ts +53 -0
- package/dist/render/EntityPreview.js +23 -0
- package/dist/render/EnvironmentLighting.d.ts +23 -7
- package/dist/render/EnvironmentLighting.js +69 -10
- package/dist/render/PartMotion.js +4 -4
- package/dist/render/SceneLighting.js +7 -0
- package/dist/render/SceneModels.d.ts +37 -1
- package/dist/render/SceneModels.js +52 -10
- package/dist/render/measureBounds.js +4 -0
- package/dist/render/measureCollisionMesh.d.ts +26 -0
- package/dist/render/measureCollisionMesh.js +108 -0
- package/dist/render/modelLoad.d.ts +41 -5
- package/dist/render/modelLoad.js +85 -18
- package/dist/render/sceneCapture.d.ts +27 -0
- package/dist/render/sceneCapture.js +43 -0
- package/dist/render/useDisposable.d.ts +16 -0
- package/dist/render/useDisposable.js +24 -0
- package/dist/render/useModelAnimation.js +47 -4
- package/dist/scene/AuthoredScene.js +14 -24
- package/dist/scene/CityRenderer.js +103 -36
- package/dist/scene/GrassFieldRenderer.js +4 -1
- package/dist/scene/PoleLineRenderer.js +7 -16
- package/dist/settings/QuickControls.js +5 -3
- package/dist/settings/SettingsMenu.js +6 -3
- package/dist/settings/appliedSettings.d.ts +3 -1
- package/dist/settings/appliedSettings.js +17 -5
- package/dist/settings/settingsController.js +11 -5
- package/dist/shellMovement.d.ts +15 -1
- package/dist/shellMovement.js +15 -1
- package/dist/structures/GeneratedBuilding.js +4 -5
- package/dist/structures/TransformGizmo.js +6 -5
- package/dist/terrain/CarvedTerrain.js +3 -7
- package/dist/terrain/GrassField.js +2 -2
- package/dist/terrain/ProceduralGround.js +3 -5
- package/dist/terrain/grassMaterial.d.ts +8 -2
- package/dist/terrain/grassMaterial.js +24 -5
- package/dist/terrain/index.d.ts +1 -1
- package/dist/terrain/terrainDetailMaterial.js +11 -2
- package/dist/terrain/terrainMath.d.ts +38 -1
- package/dist/terrain/terrainMath.js +86 -64
- package/dist/touch/TouchControlsOverlay.d.ts +6 -2
- package/dist/touch/TouchControlsOverlay.js +136 -17
- package/dist/vfx/ParticleField.d.ts +32 -0
- package/dist/vfx/ParticleField.js +91 -0
- package/dist/vfx/WorldFloatingText.d.ts +32 -0
- package/dist/vfx/WorldFloatingText.js +32 -0
- package/dist/water/Ocean.js +3 -6
- package/dist/weather/LightningStrike.js +3 -9
- package/dist/weather/RainField.js +3 -7
- package/dist/weather/SnowField.js +3 -7
- package/dist/world/DataObjects.js +3 -6
- package/dist/world/GridWorldScene.js +3 -6
- package/dist/world/InstancedBodies.js +3 -6
- package/dist/world/InstancedJoints.js +2 -2
- package/dist/world/SpriteBatch.js +3 -6
- package/dist/world/WorldEntityFrames.d.ts +47 -0
- package/dist/world/WorldEntityFrames.js +75 -0
- package/dist/world/WorldHud.js +7 -5
- package/dist/world/WorldPings.d.ts +26 -0
- package/dist/world/WorldPings.js +68 -0
- package/dist/world/WorldScene.js +9 -4
- package/dist/world/pingPulse.d.ts +21 -0
- package/dist/world/pingPulse.js +17 -0
- package/package.json +5 -5
- package/skills/ce-handoff/SKILL.md +149 -0
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +7 -3
- package/skills/jgengine/api.md +17 -7
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine/recipes/minimal-game.md +7 -4
- package/skills/jgengine-ui/SKILL.md +8 -3
- package/skills/jgengine-ui/api.md +732 -23
- package/skills/jgengine-ui/capabilities.md +371 -2
- package/skills/jgengine-ui/recipes/overhead-entity-frames.md +136 -0
- package/skills/jgengine-ui/reference.md +7 -4
- package/skills/jgengine-world/api.md +408 -12
- package/skills/jgengine-world/capabilities.md +127 -2
- package/skills/jgengine-world/reference.md +15 -1
|
@@ -2,7 +2,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { CombatCameraShake, ProjectileTracers, WorldFloatText, WorldTelegraphs, } from "./world/WorldHud.js";
|
|
3
3
|
import { WorldSpellVfx } from "./world/WorldVfx.js";
|
|
4
4
|
import { RetainedVfx } from "./world/RetainedVfx.js";
|
|
5
|
+
import { POSTFX_OVERLAY_USERDATA } from "./postfx/postfxOverlay.js";
|
|
5
6
|
/** Mounts the opt-in combat VFX stack inside the 3D canvas. @internal */
|
|
6
7
|
export function CombatPresentation({ effects }) {
|
|
7
|
-
return (_jsxs(
|
|
8
|
+
return (_jsxs("group", { userData: POSTFX_OVERLAY_USERDATA, children: [effects.telegraphs ? _jsx(WorldTelegraphs, {}) : null, effects.vfx ? (_jsxs(_Fragment, { children: [_jsx(WorldSpellVfx, {}), _jsx(RetainedVfx, {})] })) : null, effects.floatText ? _jsx(WorldFloatText, {}) : null, effects.tracers ? _jsx(ProjectileTracers, {}) : null, effects.shake ? _jsx(CombatCameraShake, {}) : null] }));
|
|
8
9
|
}
|
package/dist/GamePlayerShell.js
CHANGED
|
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore
|
|
|
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
|
+
import { activeTouchControlsMode } from "@jgengine/core/input/touchControlsMode";
|
|
6
7
|
import { normalizePointerToAxis } from "@jgengine/core/input/pointerAxis";
|
|
7
8
|
import { createGameContext } from "@jgengine/core/runtime/gameContext";
|
|
8
9
|
import { useDisplayProfile } from "@jgengine/react/display";
|
|
@@ -11,6 +12,7 @@ import { createSettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
|
11
12
|
import { applyBindingOverrides, clearBindingOverride, loadBindingOverrides, saveBindingOverride, } from "@jgengine/core/input/bindingOverrides";
|
|
12
13
|
import { orientationGateActive, orientationHintActive, resolveOrientationRequirement, } from "@jgengine/core/ui/orientation";
|
|
13
14
|
import { armFallbackSeams } from "@jgengine/core/devtools/fallbackSeams";
|
|
15
|
+
import { armTextureErrors } from "@jgengine/core/devtools/textureErrors";
|
|
14
16
|
import { readUrlFlag, subscribeUrlChange, writeUrlParam } from "@jgengine/core/devtools/urlFlags";
|
|
15
17
|
import { createAudioEngine } from "./audio/audioEngine.js";
|
|
16
18
|
import { attachAudioEventWire } from "./audio/audioWire.js";
|
|
@@ -19,7 +21,7 @@ import { withDevtoolsLatency } from "./devtools/DevtoolsOverlay.js";
|
|
|
19
21
|
import { resolveRigKind } from "./camera/index.js";
|
|
20
22
|
import { contextModels } from "./render/resolveModel.js";
|
|
21
23
|
import { OrientationHint } from "./touch/OrientationHint.js";
|
|
22
|
-
import { useTouchStyle, useGraphicsSettings } from "./settings/appliedSettings.js";
|
|
24
|
+
import { useTouchStyle, useTouchJoystickVariant, useGraphicsSettings } from "./settings/appliedSettings.js";
|
|
23
25
|
import { logRuntimeError, } from "./diagnostics/RuntimeDiagnostics.js";
|
|
24
26
|
import { EMPTY_RESERVED } from "./shellConstants.js";
|
|
25
27
|
import { useShellMultiplayerSync } from "./useShellMultiplayerSync.js";
|
|
@@ -41,6 +43,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
41
43
|
const multiplayer = useMemo(() => (rawMultiplayer === null ? null : withDevtoolsLatency(rawMultiplayer)), [rawMultiplayer]);
|
|
42
44
|
const devtoolsEnabled = playable.devtools !== false && !poster;
|
|
43
45
|
armFallbackSeams(devtoolsEnabled);
|
|
46
|
+
armTextureErrors(devtoolsEnabled);
|
|
44
47
|
// `?debug` mirrors the devtools overlay into the URL: open with a link, strip the param to close.
|
|
45
48
|
const [devtoolsOpen, setDevtoolsOpen] = useState(() => devtoolsEnabled && readUrlFlag(DEBUG_PARAM));
|
|
46
49
|
const devtoolsOpenRef = useRef(false);
|
|
@@ -76,9 +79,12 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
76
79
|
const f2HeldRef = useRef(false);
|
|
77
80
|
const settingsStore = useMemo(() => createSettingsStore(), []);
|
|
78
81
|
const [bindingOverrides, setBindingOverrides] = useState(() => loadBindingOverrides(playable.game.name));
|
|
79
|
-
|
|
82
|
+
// Render-time reset (no stale-keybinds frame): reload persisted overrides once per playable change.
|
|
83
|
+
const [overridesPlayable, setOverridesPlayable] = useState(playable);
|
|
84
|
+
if (overridesPlayable !== playable) {
|
|
85
|
+
setOverridesPlayable(playable);
|
|
80
86
|
setBindingOverrides(loadBindingOverrides(playable.game.name));
|
|
81
|
-
}
|
|
87
|
+
}
|
|
82
88
|
const effectiveInput = useMemo(() => applyBindingOverrides(playable.game.input ?? {}, bindingOverrides), [playable, bindingOverrides]);
|
|
83
89
|
const rebindAction = useCallback((action, code) => setBindingOverrides(saveBindingOverride(playable.game.name, action, [code])), [playable]);
|
|
84
90
|
const resetActionBinding = useCallback((action) => setBindingOverrides(clearBindingOverride(playable.game.name, action)), [playable]);
|
|
@@ -95,16 +101,28 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
95
101
|
if (state !== null && state.active)
|
|
96
102
|
pointerAxisRef.current = { ...state, active: false };
|
|
97
103
|
};
|
|
104
|
+
// Reads fresh every render: the ctx-version useSyncExternalStore below re-renders on store
|
|
105
|
+
// writes, so a gameplay setTouchControlsMode("car") swaps the visible control set that frame.
|
|
106
|
+
const touchMode = ctx === null ? null : activeTouchControlsMode(ctx);
|
|
98
107
|
const touchScheme = useMemo(() => deriveTouchScheme(playable.game.input, {
|
|
99
108
|
reserved: resolveRigKind(playable.camera) === "none" || playable.presentation === "hud"
|
|
100
109
|
? EMPTY_RESERVED
|
|
101
110
|
: RESERVED_INPUT_ACTIONS,
|
|
102
111
|
firstPerson: resolveRigKind(playable.camera) === "first",
|
|
103
112
|
config: playable.touch,
|
|
104
|
-
|
|
113
|
+
mode: touchMode,
|
|
114
|
+
}), [playable, touchMode]);
|
|
105
115
|
const touchStyle = useTouchStyle(settingsStore, touchScheme?.style ?? DEFAULT_TOUCH_STYLE);
|
|
116
|
+
const touchJoystickVariant = useTouchJoystickVariant(settingsStore);
|
|
106
117
|
const { coarsePointer, portrait, compact } = useDisplayProfile();
|
|
107
|
-
const
|
|
118
|
+
const analogRef = useRef(null);
|
|
119
|
+
const touchSink = useMemo(() => ({
|
|
120
|
+
onCodeDown: (code) => tracker.handleDown(code),
|
|
121
|
+
onCodeUp: (code) => tracker.handleUp(code),
|
|
122
|
+
onAnalog: (values) => {
|
|
123
|
+
analogRef.current = values;
|
|
124
|
+
},
|
|
125
|
+
}), [tracker]);
|
|
108
126
|
const gateRef = useRef(false);
|
|
109
127
|
const orientationPlatform = coarsePointer ? "mobile" : "desktop";
|
|
110
128
|
const orientationRequirement = useMemo(() => resolveOrientationRequirement(playable.orientation, orientationPlatform), [playable, orientationPlatform]);
|
|
@@ -199,6 +217,8 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
199
217
|
pitchRef,
|
|
200
218
|
touchScheme,
|
|
201
219
|
touchSink,
|
|
220
|
+
touchJoystickVariant,
|
|
221
|
+
analogRef,
|
|
202
222
|
orientationGate,
|
|
203
223
|
orientationGateEl,
|
|
204
224
|
coarsePointer,
|
|
@@ -2,16 +2,17 @@ import { type Dispatch, type MutableRefObject, type RefObject, type SetStateActi
|
|
|
2
2
|
import type { PointerAxisState } from "@jgengine/core/input/pointerAxis";
|
|
3
3
|
import type { ActionStateTracker } from "@jgengine/core/input/actionBindings";
|
|
4
4
|
import type { BindingOverrides } from "@jgengine/core/input/bindingOverrides";
|
|
5
|
-
import type { TouchScheme, TouchStyle } from "@jgengine/core/input/touchScheme";
|
|
5
|
+
import type { TouchJoystickVariant, TouchScheme, TouchStyle } from "@jgengine/core/input/touchScheme";
|
|
6
6
|
import { type GraphicsQuality, type SettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
7
7
|
import type { PresencePoseRow } from "@jgengine/core/runtime/transport";
|
|
8
8
|
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
9
9
|
import type { AudioEngine } from "./audio/audioEngine.js";
|
|
10
10
|
import type { ShellMultiplayer } from "./multiplayer.js";
|
|
11
11
|
import type { PlayableGame } from "./registry.js";
|
|
12
|
+
import { type TouchCodeSink } from "./touch/TouchControlsOverlay.js";
|
|
12
13
|
import type { RuntimeDiagnostic } from "./diagnostics/RuntimeDiagnostics.js";
|
|
13
14
|
/** 3D play surface: canvas, world overlays, and shared chrome. @internal */
|
|
14
|
-
export declare function Shell3dPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, primaryClickRef, cameraDraggingRef, serverIdRef, remotePlayers, touchScheme, touchSink, touchStyle, orientationGate, orientationGateEl, coarsePointer, compact, graphics, settingsStore, bindingOverrides, rebindAction, resetActionBinding, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, audioEngine, poster, posterFrozen, onPosterSettled, }: {
|
|
15
|
+
export declare function Shell3dPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, primaryClickRef, cameraDraggingRef, serverIdRef, remotePlayers, touchScheme, touchSink, touchStyle, touchJoystickVariant, analogRef, orientationGate, orientationGateEl, coarsePointer, compact, graphics, settingsStore, bindingOverrides, rebindAction, resetActionBinding, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, audioEngine, poster, posterFrozen, onPosterSettled, }: {
|
|
15
16
|
playable: PlayableGame;
|
|
16
17
|
ctx: GameContext;
|
|
17
18
|
multiplayer: ShellMultiplayer | null;
|
|
@@ -27,11 +28,10 @@ export declare function Shell3dPresentation({ playable, ctx, multiplayer, tracke
|
|
|
27
28
|
serverIdRef: MutableRefObject<string | null>;
|
|
28
29
|
remotePlayers: PresencePoseRow[];
|
|
29
30
|
touchScheme: TouchScheme | null;
|
|
30
|
-
touchSink:
|
|
31
|
-
onCodeDown: (code: string) => void;
|
|
32
|
-
onCodeUp: (code: string) => void;
|
|
33
|
-
};
|
|
31
|
+
touchSink: TouchCodeSink;
|
|
34
32
|
touchStyle: TouchStyle;
|
|
33
|
+
touchJoystickVariant: TouchJoystickVariant;
|
|
34
|
+
analogRef: MutableRefObject<Readonly<Record<string, number>> | null>;
|
|
35
35
|
orientationGate: boolean;
|
|
36
36
|
orientationGateEl: React.ReactNode;
|
|
37
37
|
coarsePointer: boolean;
|
|
@@ -44,7 +44,7 @@ import { resolvePresentationEffects, resolveWorldOverlayBars, } from "./presenta
|
|
|
44
44
|
const PRIMARY_CLICK_MOVE_THRESHOLD_PX = 6;
|
|
45
45
|
const DEFAULT_BACKGROUND_COLOR = "#14161b";
|
|
46
46
|
/** 3D play surface: canvas, world overlays, and shared chrome. @internal */
|
|
47
|
-
export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, primaryClickRef, cameraDraggingRef, serverIdRef, remotePlayers, touchScheme, touchSink, touchStyle, orientationGate, orientationGateEl, coarsePointer, compact, graphics, settingsStore, bindingOverrides, rebindAction, resetActionBinding, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, audioEngine, poster, posterFrozen, onPosterSettled, }) {
|
|
47
|
+
export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, primaryClickRef, cameraDraggingRef, serverIdRef, remotePlayers, touchScheme, touchSink, touchStyle, touchJoystickVariant, analogRef, orientationGate, orientationGateEl, coarsePointer, compact, graphics, settingsStore, bindingOverrides, rebindAction, resetActionBinding, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, audioEngine, poster, posterFrozen, onPosterSettled, }) {
|
|
48
48
|
const GameUI = playable.GameUI;
|
|
49
49
|
const WorldOverlay = playable.WorldOverlay;
|
|
50
50
|
const pointerService = useMemo(() => createPointerService(), []);
|
|
@@ -289,14 +289,14 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
|
|
|
289
289
|
fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
|
|
290
290
|
near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
|
|
291
291
|
far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
|
|
292
|
-
}, shadows: graphics.shadows, dpr: [Math.min(1, graphics.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("group", { userData: CAMERA_TRANSPARENT_USERDATA, children: _jsx(WorldOverlay, { ctx: ctx }) })) : null, bars !== null ? (_jsx(WorldEntityBars, { statId: bars.statId, roles: bars.roles, resolveRole: resolveEntityRole, ...(bars.maxDistance === undefined ? {} : { maxDistance: bars.maxDistance }) })) : null, nameplates !== null ? (_jsx(WorldNameplates, { statId: nameplates.statId, roles: nameplates.roles, resolveRole: resolveEntityRole, ...(nameplates.maxDistance === undefined ? {} : { maxDistance: nameplates.maxDistance }), ...(nameplates.showHealth === undefined ? {} : { showHealth: nameplates.showHealth }) })) : null, _jsx(WorldItems, { config: playable.worldItem }), _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) => {
|
|
292
|
+
}, shadows: graphics.shadows, dpr: [Math.min(1, graphics.dpr), graphics.dpr], gl: { preserveDrawingBuffer: true }, style: { touchAction: "none" }, children: [backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, cinematicLook ? (_jsx(EnvironmentLighting, { 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 })) : 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("group", { userData: CAMERA_TRANSPARENT_USERDATA, children: _jsx(WorldOverlay, { ctx: ctx }) })) : null, bars !== null ? (_jsx(WorldEntityBars, { statId: bars.statId, roles: bars.roles, resolveRole: resolveEntityRole, ...(bars.maxDistance === undefined ? {} : { maxDistance: bars.maxDistance }) })) : null, nameplates !== null ? (_jsx(WorldNameplates, { statId: nameplates.statId, roles: nameplates.roles, resolveRole: resolveEntityRole, ...(nameplates.maxDistance === undefined ? {} : { maxDistance: nameplates.maxDistance }), ...(nameplates.showHealth === undefined ? {} : { showHealth: nameplates.showHealth }) })) : null, _jsx(WorldItems, { config: playable.worldItem }), _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) => {
|
|
293
293
|
cameraDraggingRef.current = dragging;
|
|
294
|
-
} }), _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: onPosterSettled }), _jsx(DevtoolsRendererProbe, {}), resolvedLook.postProcessing !== undefined && resolvedLook.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: resolvedLook.postProcessing, quality: graphics.quality })) : null] }), !poster &&
|
|
294
|
+
} }), _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 }), _jsx(DevtoolsRendererProbe, {}), resolvedLook.postProcessing !== undefined && resolvedLook.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: resolvedLook.postProcessing, quality: graphics.quality })) : null] }), !poster &&
|
|
295
295
|
!orientationGate &&
|
|
296
296
|
coarsePointer &&
|
|
297
297
|
controlsActive &&
|
|
298
298
|
touchScheme !== null &&
|
|
299
299
|
(touchScheme.gestures !== null || touchScheme.look) ? (_jsx(TouchPlaySurface, { scheme: touchScheme, sink: touchSink, yawRef: yawRef, pitchRef: pitchRef, maxPitch: playable.camera?.firstPerson?.maxPitch ?? 1.45, onPrimaryTap: () => {
|
|
300
300
|
primaryClickRef.current = true;
|
|
301
|
-
} })) : null, _jsx(ShellGameUiChrome, { ctx: ctx, playable: playable, GameUI: GameUI, uiScale: graphics.uiScale, orientationGate: orientationGate, onRuntimeError: reportRuntimeError }), !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, _jsx(ShellDebugOverlays, { ctx: ctx, playable: playable, multiplayer: multiplayer, diagnostics: diagnostics, devtoolsEnabled: devtoolsEnabled, devtoolsOpen: devtoolsOpen, hideDiagnostics: poster }), poster || orthographic || settingsHostsFov ? null : _jsx(PlayerFovSlider, {}), poster ? null : _jsx(SettingsChrome, {})] }) }) })] }) }));
|
|
301
|
+
} })) : null, _jsx(ShellGameUiChrome, { ctx: ctx, playable: playable, GameUI: GameUI, uiScale: graphics.uiScale, orientationGate: orientationGate, onRuntimeError: reportRuntimeError }), !poster && !orientationGate && showReticle ? _jsx(Reticle, {}) : null, dockMounted && touchScheme !== null ? (_jsx(TouchControlsDock, { scheme: touchScheme, sink: touchSink, style: touchStyle, scale: touchScale, joystickVariant: touchJoystickVariant })) : 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, _jsx(ShellDebugOverlays, { ctx: ctx, playable: playable, multiplayer: multiplayer, diagnostics: diagnostics, devtoolsEnabled: devtoolsEnabled, devtoolsOpen: devtoolsOpen, hideDiagnostics: poster }), poster || orthographic || settingsHostsFov ? null : _jsx(PlayerFovSlider, {}), poster ? null : _jsx(SettingsChrome, {})] }) }) })] }) }));
|
|
302
302
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { CameraShakeController } from "@jgengine/core/vfx/cameraShake";
|
|
3
|
+
/** Props for {@link ControllerCameraShake}. */
|
|
4
|
+
export interface ControllerCameraShakeProps {
|
|
5
|
+
/**
|
|
6
|
+
* The core camera-shake controller to drive and apply. Feed it impacts with
|
|
7
|
+
* `controller.add(amount, kind?)` from anywhere; this component ticks its decay
|
|
8
|
+
* and applies the resulting offset to the active camera every frame.
|
|
9
|
+
*/
|
|
10
|
+
controller: CameraShakeController;
|
|
11
|
+
/**
|
|
12
|
+
* `useFrame` priority. Must run AFTER the camera rig has posed the camera this
|
|
13
|
+
* frame so the shake lands on top of the freshly-set base pose (the rig
|
|
14
|
+
* overwrites the base each frame, so no manual restore is needed). Default
|
|
15
|
+
* {@link GAME_SIM_FRAME_PRIORITY} (`0`) which runs after the orbit/rig cameras
|
|
16
|
+
* (priority `-1`). Keep it `<= 0` so R3F's automatic render is preserved.
|
|
17
|
+
*/
|
|
18
|
+
priority?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The shell-side consumer of a core {@link CameraShakeController}: each frame it
|
|
22
|
+
* calls `controller.update(delta)` to bleed trauma, reads the pooled
|
|
23
|
+
* `controller.offset()`, and applies it additively to the active camera — a
|
|
24
|
+
* positional kick plus a pitch/yaw/roll rotation — so the view VISIBLY shakes on
|
|
25
|
+
* impacts. It runs after the camera rig (which re-poses the camera to its base
|
|
26
|
+
* every frame), so the shake composes with any rig without a manual save/restore
|
|
27
|
+
* and without fighting the built-in `shakeChannel`. Renders nothing.
|
|
28
|
+
*
|
|
29
|
+
* @capability controller-camera-shake R3F consumer that applies a core camera-shake controller's pooled per-frame offset to the active camera (translation + pitch/yaw/roll) so the view visibly shakes
|
|
30
|
+
*/
|
|
31
|
+
export declare function ControllerCameraShake({ controller, priority, }: ControllerCameraShakeProps): ReactNode;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
2
|
+
import * as THREE from "three";
|
|
3
|
+
import { GAME_SIM_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
4
|
+
// Reused scratch objects — never allocate inside the frame loop.
|
|
5
|
+
const shakeEuler = new THREE.Euler();
|
|
6
|
+
const shakeQuat = new THREE.Quaternion();
|
|
7
|
+
/**
|
|
8
|
+
* The shell-side consumer of a core {@link CameraShakeController}: each frame it
|
|
9
|
+
* calls `controller.update(delta)` to bleed trauma, reads the pooled
|
|
10
|
+
* `controller.offset()`, and applies it additively to the active camera — a
|
|
11
|
+
* positional kick plus a pitch/yaw/roll rotation — so the view VISIBLY shakes on
|
|
12
|
+
* impacts. It runs after the camera rig (which re-poses the camera to its base
|
|
13
|
+
* every frame), so the shake composes with any rig without a manual save/restore
|
|
14
|
+
* and without fighting the built-in `shakeChannel`. Renders nothing.
|
|
15
|
+
*
|
|
16
|
+
* @capability controller-camera-shake R3F consumer that applies a core camera-shake controller's pooled per-frame offset to the active camera (translation + pitch/yaw/roll) so the view visibly shakes
|
|
17
|
+
*/
|
|
18
|
+
export function ControllerCameraShake({ controller, priority = GAME_SIM_FRAME_PRIORITY, }) {
|
|
19
|
+
const camera = useThree((state) => state.camera);
|
|
20
|
+
useFrame((_, delta) => {
|
|
21
|
+
controller.update(delta);
|
|
22
|
+
const offset = controller.offset();
|
|
23
|
+
// Additive on top of the rig's freshly-set base pose (rig runs at priority -1).
|
|
24
|
+
camera.position.x += offset.x;
|
|
25
|
+
camera.position.y += offset.y;
|
|
26
|
+
camera.position.z += offset.z;
|
|
27
|
+
if (offset.pitch !== 0 || offset.yaw !== 0 || offset.roll !== 0) {
|
|
28
|
+
shakeEuler.set(offset.pitch, offset.yaw, offset.roll, "XYZ");
|
|
29
|
+
shakeQuat.setFromEuler(shakeEuler);
|
|
30
|
+
camera.quaternion.multiply(shakeQuat);
|
|
31
|
+
}
|
|
32
|
+
}, priority);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useCallback, useMemo, useRef, useSyncExternalStore } from "react";
|
|
3
3
|
import { ChaseRig, CinematicRig, LockOnRig, ObserverRig, RtsRig, ShoulderRig, SideScrollRig, TopDownRig, } from "./cameraRigs.js";
|
|
4
4
|
import { GameFirstPersonCamera } from "./GameFirstPersonCamera.js";
|
|
5
5
|
import { GameInspectionCamera } from "./GameInspectionCamera.js";
|
|
@@ -10,12 +10,17 @@ import { CameraShakeContext, createCameraShakeChannel } from "./shakeChannel.js"
|
|
|
10
10
|
export { resolveRigKind } from "./rigResolve.js";
|
|
11
11
|
export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, viewmodel, }) {
|
|
12
12
|
const channel = useMemo(() => createCameraShakeChannel(config?.shake?.decayPerSecond), [config?.shake?.decayPerSecond]);
|
|
13
|
-
const
|
|
14
|
-
|
|
13
|
+
const directorVersionRef = useRef(0);
|
|
14
|
+
const subscribeDirector = useCallback((onStoreChange) => {
|
|
15
15
|
if (director === undefined)
|
|
16
|
-
return undefined;
|
|
17
|
-
return director.subscribe(
|
|
16
|
+
return () => undefined;
|
|
17
|
+
return director.subscribe(() => {
|
|
18
|
+
directorVersionRef.current += 1;
|
|
19
|
+
onStoreChange();
|
|
20
|
+
});
|
|
18
21
|
}, [director]);
|
|
22
|
+
const directorVersion = useCallback(() => directorVersionRef.current, []);
|
|
23
|
+
useSyncExternalStore(subscribeDirector, directorVersion, directorVersion);
|
|
19
24
|
const directed = resolveDirectedCamera(director === undefined
|
|
20
25
|
? undefined
|
|
21
26
|
: { followEntityId: director.followedEntityId(), cinematic: director.cinematic() }, { followEntityId: config?.followEntityId, cinematic: config?.cinematic });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { OrbitControls } from "@react-three/drei";
|
|
3
3
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
4
|
-
import {
|
|
4
|
+
import { useRef } from "react";
|
|
5
5
|
import { MOUSE, PerspectiveCamera, Raycaster, Vector3 } from "three";
|
|
6
6
|
import { isCameraOccluderTransparent } from "./cameraCollision.js";
|
|
7
7
|
import { useGameContext } from "@jgengine/react/provider";
|
|
@@ -28,15 +28,6 @@ export function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followE
|
|
|
28
28
|
distance: 0,
|
|
29
29
|
});
|
|
30
30
|
const followId = followEntityId ?? userId;
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
const entity = ctx.scene.entity.get(followId);
|
|
33
|
-
if (entity === null || runtimeRef.current !== null)
|
|
34
|
-
return;
|
|
35
|
-
const seeded = seedOrbitFollowState({ entityPosition: entity.position, config });
|
|
36
|
-
runtimeRef.current = seeded;
|
|
37
|
-
camera.position.set(seeded.camera.x, seeded.camera.y, seeded.camera.z);
|
|
38
|
-
camera.lookAt(seeded.target.x, seeded.target.y, seeded.target.z);
|
|
39
|
-
}, [camera, config, ctx, followId]);
|
|
40
31
|
useFrame((_, delta) => {
|
|
41
32
|
const controls = controlsRef.current;
|
|
42
33
|
const entity = ctx.scene.entity.get(followId);
|
|
@@ -448,18 +448,30 @@ export function ChaseRig(props) {
|
|
|
448
448
|
if (lookBack) {
|
|
449
449
|
pose.position = seatPose(follow, yaw, seat, fov).position;
|
|
450
450
|
}
|
|
451
|
+
props.yawRef.current = yaw;
|
|
451
452
|
commit(pose, dt);
|
|
452
453
|
return;
|
|
453
454
|
}
|
|
454
|
-
|
|
455
|
+
// Anchor yaw always eases toward its target (#1370): with drift-lag configured the target is
|
|
456
|
+
// the velocity-blended yaw at velocityYawResponse, otherwise the raw body facing at
|
|
457
|
+
// yawResponse — so a strafe-flipped facing arcs the boom around the character instead of
|
|
458
|
+
// teleporting it to the far side. yawResponse: Infinity restores the legacy rigid follow.
|
|
459
|
+
let targetYaw = yaw;
|
|
460
|
+
let response = resolved.yawResponse;
|
|
455
461
|
if (resolved.velocityYawBlend > 0) {
|
|
456
462
|
const velocity = dt > 0
|
|
457
463
|
? { x: (follow.x - last.x) / dt, y: 0, z: (follow.z - last.z) / dt }
|
|
458
464
|
: { x: 0, y: 0, z: 0 };
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
anchorYawRef.current = anchorYaw;
|
|
465
|
+
targetYaw = velocityYawTarget(yaw, velocity, resolved);
|
|
466
|
+
response = resolved.velocityYawResponse;
|
|
462
467
|
}
|
|
468
|
+
const anchorYaw = Number.isFinite(response)
|
|
469
|
+
? smoothYaw(anchorYawRef.current ?? targetYaw, targetYaw, response, dt)
|
|
470
|
+
: targetYaw;
|
|
471
|
+
anchorYawRef.current = anchorYaw;
|
|
472
|
+
// Report camera yaw back to the shell like every other player-facing rig, so
|
|
473
|
+
// on-foot movement and aim stay camera-relative instead of frozen at yaw 0.
|
|
474
|
+
props.yawRef.current = anchorYaw;
|
|
463
475
|
const led = leadFollowPoint(follow, last, dt, resolved);
|
|
464
476
|
const desired = chaseDesiredPosition(led, anchorYaw, resolved);
|
|
465
477
|
const prev = posRef.current ?? desired;
|
package/dist/camera/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { GameInspectionCamera, type GameInspectionCameraProps } from "./GameInsp
|
|
|
5
5
|
export { GameCameraRig, resolveRigKind, type GameCameraRigProps } from "./GameCameraRig.js";
|
|
6
6
|
export { PlayerFovProvider, PlayerFovSlider, usePlayerFov, type PlayerFovState, } from "./PlayerFov.js";
|
|
7
7
|
export { CameraShakeContext, cameraShake, createCameraShakeChannel, defaultCameraShakeChannel, useCameraShake, type CameraShakeChannel, } from "./shakeChannel.js";
|
|
8
|
+
export { ControllerCameraShake, type ControllerCameraShakeProps } from "./ControllerCameraShake.js";
|
|
8
9
|
export { rtsPanKeysConflict } from "./rigMath.js";
|
|
9
10
|
export { GAME_SIM_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
10
11
|
export { CAMERA_TRANSPARENT_USERDATA, isCameraOccluderTransparent, type CameraOccluder, } from "./cameraCollision.js";
|
package/dist/camera/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { GameInspectionCamera } from "./GameInspectionCamera.js";
|
|
|
5
5
|
export { GameCameraRig, resolveRigKind } from "./GameCameraRig.js";
|
|
6
6
|
export { PlayerFovProvider, PlayerFovSlider, usePlayerFov, } from "./PlayerFov.js";
|
|
7
7
|
export { CameraShakeContext, cameraShake, createCameraShakeChannel, defaultCameraShakeChannel, useCameraShake, } from "./shakeChannel.js";
|
|
8
|
+
export { ControllerCameraShake } from "./ControllerCameraShake.js";
|
|
8
9
|
export { rtsPanKeysConflict } from "./rigMath.js";
|
|
9
10
|
export { GAME_SIM_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
10
11
|
export { CAMERA_TRANSPARENT_USERDATA, isCameraOccluderTransparent, } from "./cameraCollision.js";
|
package/dist/camera/rigMath.d.ts
CHANGED
|
@@ -134,6 +134,8 @@ export interface ResolvedChase {
|
|
|
134
134
|
velocityYawMinSpeed: number;
|
|
135
135
|
/** Exponential smoothing rate toward the blended anchor yaw (1-exp(-response*dt)). */
|
|
136
136
|
velocityYawResponse: number;
|
|
137
|
+
/** Exponential smoothing rate the anchor yaw follows the body's facing with (#1370). `Infinity` restores the legacy rigid follow. */
|
|
138
|
+
yawResponse: number;
|
|
137
139
|
}
|
|
138
140
|
/** @internal */
|
|
139
141
|
export declare function resolveChase(config: ChaseCameraConfig | undefined): ResolvedChase;
|
package/dist/camera/rigMath.js
CHANGED
|
@@ -245,6 +245,12 @@ export function lockOnPose(player, target, config, fov) {
|
|
|
245
245
|
};
|
|
246
246
|
return { pose: { position, lookAt: focus, fov }, yaw };
|
|
247
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Default chase-yaw follow response: fast enough that the camera settles behind the runner in
|
|
250
|
+
* well under a second, slow enough that a strafe-flipped body facing arcs the boom around the
|
|
251
|
+
* character instead of teleporting it to the other side.
|
|
252
|
+
*/
|
|
253
|
+
const DEFAULT_CHASE_YAW_RESPONSE = 5;
|
|
248
254
|
/** @internal */
|
|
249
255
|
export function resolveChase(config) {
|
|
250
256
|
return {
|
|
@@ -261,6 +267,7 @@ export function resolveChase(config) {
|
|
|
261
267
|
velocityYawBlend: config?.velocityYaw === undefined ? 0 : config.velocityYaw.blend ?? 0.65,
|
|
262
268
|
velocityYawMinSpeed: config?.velocityYaw?.minSpeed ?? 4,
|
|
263
269
|
velocityYawResponse: config?.velocityYaw?.response ?? 6,
|
|
270
|
+
yawResponse: config?.yawResponse ?? DEFAULT_CHASE_YAW_RESPONSE,
|
|
264
271
|
};
|
|
265
272
|
}
|
|
266
273
|
/** Velocity-lead the follow point (#286.9): aim `leadTime` seconds along the target's frame velocity, clamped to `leadMax`.
|
|
@@ -4,6 +4,8 @@ import { useFrame } from "@react-three/fiber";
|
|
|
4
4
|
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
5
5
|
import * as THREE from "three";
|
|
6
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
7
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
8
|
+
import { POSTFX_OVERLAY_USERDATA } from "../postfx/postfxOverlay.js";
|
|
7
9
|
import { aimProbeNeeded, anyCollisionLayerOn, colliderScanNeeded, collisionDebug, projectileListenNeeded, } from "./collisionDebug.js";
|
|
8
10
|
import { AIM_DAMAGE_COLOR, AIM_LASER_COLOR, AIM_MISS_COLOR, AIM_SOLID_COLOR, BODY_WIRE_COLOR, collectDebugShapes, computeAimLaser, HITBOX_WIRE_COLOR, muzzleMarkerFromOrigin, PROJECTILE_PATH_COLOR, } from "./collisionDebugMath.js";
|
|
9
11
|
function useCollisionDebugState() {
|
|
@@ -15,11 +17,23 @@ function WireSphere({ center, radius, color, label, }) {
|
|
|
15
17
|
function WireAabb({ center, halfExtents, rotationY, color, label, }) {
|
|
16
18
|
return (_jsxs("group", { position: [center[0], center[1], center[2]], rotation: [0, rotationY, 0], children: [_jsxs("mesh", { children: [_jsx("boxGeometry", { args: [halfExtents[0] * 2, halfExtents[1] * 2, halfExtents[2] * 2] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, transparent: true, opacity: 0.85, depthTest: false })] }), _jsx(Html, { position: [0, halfExtents[1] + 0.15, 0], center: true, distanceFactor: 14, zIndexRange: [25, 0], style: { pointerEvents: "none" }, children: _jsx("span", { className: "rounded bg-black/70 px-1 text-[9px] font-medium text-white/90", children: label }) })] }));
|
|
17
19
|
}
|
|
20
|
+
function WireTriangleMesh({ entry, shape, color, }) {
|
|
21
|
+
const geometry = useDisposable(() => {
|
|
22
|
+
const built = new THREE.BufferGeometry();
|
|
23
|
+
built.setAttribute("position", new THREE.BufferAttribute(shape.mesh.positions, 3));
|
|
24
|
+
built.setIndex(new THREE.BufferAttribute(shape.mesh.indices, 1));
|
|
25
|
+
return built;
|
|
26
|
+
}, [shape.mesh]);
|
|
27
|
+
return (_jsxs("group", { children: [_jsx("group", { position: [entry.position[0], entry.position[1], entry.position[2]], rotation: [0, entry.rotationY, 0], children: _jsx("group", { position: [shape.meshTranslate[0], shape.meshTranslate[1], shape.meshTranslate[2]], scale: shape.meshScale, children: _jsx("mesh", { geometry: geometry, children: _jsx("meshBasicMaterial", { color: color, wireframe: true, transparent: true, opacity: 0.85, depthTest: false }) }) }) }), _jsx(Html, { position: [shape.center[0], shape.center[1] + shape.halfExtents[1] + 0.15, shape.center[2]], center: true, distanceFactor: 14, zIndexRange: [25, 0], style: { pointerEvents: "none" }, children: _jsx("span", { className: "rounded bg-black/70 px-1 text-[9px] font-medium text-white/90", children: entry.label }) })] }));
|
|
28
|
+
}
|
|
18
29
|
function ColliderShapeMesh({ entry }) {
|
|
19
30
|
const color = entry.style === "hitbox" ? HITBOX_WIRE_COLOR : BODY_WIRE_COLOR;
|
|
20
31
|
if (entry.shape.kind === "sphere") {
|
|
21
32
|
return (_jsx(WireSphere, { center: entry.shape.center, radius: entry.shape.radius, color: color, label: entry.label }));
|
|
22
33
|
}
|
|
34
|
+
if (entry.shape.kind === "mesh") {
|
|
35
|
+
return _jsx(WireTriangleMesh, { entry: entry, shape: entry.shape, color: color });
|
|
36
|
+
}
|
|
23
37
|
return (_jsx(WireAabb, { center: entry.shape.center, halfExtents: entry.shape.halfExtents, rotationY: entry.rotationY, color: color, label: entry.label }));
|
|
24
38
|
}
|
|
25
39
|
function endpointColor(kind) {
|
|
@@ -174,7 +188,7 @@ export function CollisionDebugWorld() {
|
|
|
174
188
|
}, [state.layers.muzzles, state.layers.aimLaser, state.projectileTraces, laser]);
|
|
175
189
|
if (!anyCollisionLayerOn(state.layers))
|
|
176
190
|
return null;
|
|
177
|
-
return (_jsxs("group", { children: [shapes.map((entry) => (_jsx(ColliderShapeMesh, { entry: entry }, entry.key))), projectilePoints.map((path) => (_jsx(Line, { points: path.points, color: PROJECTILE_PATH_COLOR, lineWidth: 2, transparent: true, opacity: 0.9, depthTest: false }, path.id))), muzzleOrigins.map((entry) => (_jsx(MuzzleMark, { origin: entry.origin }, `muzzle-${entry.id}`))), laser !== null ? (_jsxs(_Fragment, { children: [_jsx(Line, { points: [
|
|
191
|
+
return (_jsxs("group", { userData: POSTFX_OVERLAY_USERDATA, children: [shapes.map((entry) => (_jsx(ColliderShapeMesh, { entry: entry }, entry.key))), projectilePoints.map((path) => (_jsx(Line, { points: path.points, color: PROJECTILE_PATH_COLOR, lineWidth: 2, transparent: true, opacity: 0.9, depthTest: false }, path.id))), muzzleOrigins.map((entry) => (_jsx(MuzzleMark, { origin: entry.origin }, `muzzle-${entry.id}`))), laser !== null ? (_jsxs(_Fragment, { children: [_jsx(Line, { points: [
|
|
178
192
|
new THREE.Vector3(laser.origin[0], laser.origin[1], laser.origin[2]),
|
|
179
193
|
new THREE.Vector3(laser.end[0], laser.end[1], laser.end[2]),
|
|
180
194
|
], color: AIM_LASER_COLOR, lineWidth: 1.5, transparent: true, opacity: 0.95, depthTest: false }), _jsx(AimEndpointMark, { point: laser.end, kind: laser.kind })] })) : null] }));
|
|
@@ -3,6 +3,7 @@ import { useFrame, useThree } from "@react-three/fiber";
|
|
|
3
3
|
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
4
|
import { devtools, instrumentLatency, } from "@jgengine/core/devtools/devtools";
|
|
5
5
|
import { fallbackSeamsSnapshot } from "@jgengine/core/devtools/fallbackSeams";
|
|
6
|
+
import { textureErrorsSnapshot } from "@jgengine/core/devtools/textureErrors";
|
|
6
7
|
import { MOVEMENT_TUNING } from "@jgengine/core/movement/movementModel";
|
|
7
8
|
import { collisionDebug } from "./collisionDebug.js";
|
|
8
9
|
import { readStoredOverrides } from "./devtoolsOverrides.js";
|
|
@@ -210,6 +211,7 @@ export function DevtoolsOverlay({ open, ctx, playable, multiplayer, }) {
|
|
|
210
211
|
devtools.probes.register("entities", () => ctx.scene.entity.list().length),
|
|
211
212
|
devtools.probes.register("objects", () => ctx.scene.object.list().length),
|
|
212
213
|
devtools.probes.register("fallbacks", () => fallbackSeamsSnapshot()),
|
|
214
|
+
devtools.probes.register("textureErrors", () => textureErrorsSnapshot()),
|
|
213
215
|
];
|
|
214
216
|
return () => {
|
|
215
217
|
for (const dispose of disposers)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EntityPosition } from "@jgengine/core/scene/entityStore";
|
|
2
2
|
import { type ColliderPurpose, type EntityColliderSet, type ResolvedCollider } from "@jgengine/core/scene/colliders";
|
|
3
|
+
import type { PreparedCollisionMesh } from "@jgengine/core/scene/collisionMesh";
|
|
3
4
|
import { type SceneRaycastApi, type SceneRaycastHit } from "@jgengine/core/scene/sceneRaycast";
|
|
4
5
|
import { type ShotOriginPolicy } from "@jgengine/core/combat/shotOrigin";
|
|
5
6
|
import type { Aim } from "@jgengine/core/scene/spatial";
|
|
@@ -24,6 +25,13 @@ export interface DebugShapeEntry {
|
|
|
24
25
|
kind: "aabb";
|
|
25
26
|
halfExtents: EntityPosition;
|
|
26
27
|
center: EntityPosition;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "mesh";
|
|
30
|
+
mesh: PreparedCollisionMesh;
|
|
31
|
+
meshScale: number;
|
|
32
|
+
meshTranslate: EntityPosition;
|
|
33
|
+
halfExtents: EntityPosition;
|
|
34
|
+
center: EntityPosition;
|
|
27
35
|
};
|
|
28
36
|
label: string;
|
|
29
37
|
style: "hitbox" | "body";
|
|
@@ -36,11 +36,20 @@ function pushResolved(out, targetKind, instanceId, catalogId, position, rotation
|
|
|
36
36
|
const center = shapeWorldCenter(collider, position, rotationY);
|
|
37
37
|
const shape = collider.shape.kind === "sphere"
|
|
38
38
|
? { kind: "sphere", radius: collider.shape.radius, center }
|
|
39
|
-
:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
: collider.shape.kind === "mesh"
|
|
40
|
+
? {
|
|
41
|
+
kind: "mesh",
|
|
42
|
+
mesh: collider.shape.mesh,
|
|
43
|
+
meshScale: collider.shape.meshScale,
|
|
44
|
+
meshTranslate: collider.shape.meshTranslate,
|
|
45
|
+
halfExtents: collider.shape.halfExtents,
|
|
46
|
+
center,
|
|
47
|
+
}
|
|
48
|
+
: {
|
|
49
|
+
kind: "aabb",
|
|
50
|
+
halfExtents: collider.shape.halfExtents,
|
|
51
|
+
center,
|
|
52
|
+
};
|
|
44
53
|
out.push({
|
|
45
54
|
key: `${targetKind}:${instanceId}:${collider.purpose}:${collider.name}`,
|
|
46
55
|
targetKind,
|
|
@@ -5,7 +5,7 @@ import type { PointerService } from "../pointer/pointerService.js";
|
|
|
5
5
|
import type { ShellMultiplayer } from "../multiplayer.js";
|
|
6
6
|
import type { PlayableGame } from "../registry.js";
|
|
7
7
|
export declare const POSTER_SETTLE_SECONDS = 1.6;
|
|
8
|
-
export declare function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, gateRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }: {
|
|
8
|
+
export declare function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, analogRef, gateRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }: {
|
|
9
9
|
ctx: GameContext;
|
|
10
10
|
playable: PlayableGame;
|
|
11
11
|
tracker: ActionStateTracker<string>;
|
|
@@ -21,6 +21,9 @@ export declare function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef,
|
|
|
21
21
|
pointerAxisRef: {
|
|
22
22
|
current: PointerAxisState | null;
|
|
23
23
|
};
|
|
24
|
+
analogRef: {
|
|
25
|
+
current: Readonly<Record<string, number>> | null;
|
|
26
|
+
};
|
|
24
27
|
gateRef: {
|
|
25
28
|
current: boolean;
|
|
26
29
|
};
|
|
@@ -19,7 +19,7 @@ import { GAME_SIM_FRAME_PRIORITY } from "../camera/index.js";
|
|
|
19
19
|
import { NO_ACTIONS } from "../shellConstants.js";
|
|
20
20
|
const TURN_SPEED = 2.4;
|
|
21
21
|
export const POSTER_SETTLE_SECONDS = 1.6;
|
|
22
|
-
export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, gateRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }) {
|
|
22
|
+
export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, analogRef, gateRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }) {
|
|
23
23
|
const posterElapsedRef = useRef(0);
|
|
24
24
|
const posterDoneRef = useRef(false);
|
|
25
25
|
const hasReportedTickError = useRef(false);
|
|
@@ -71,7 +71,7 @@ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryC
|
|
|
71
71
|
const sendInput = () => {
|
|
72
72
|
if (!serverAuthoritative)
|
|
73
73
|
return;
|
|
74
|
-
const frame = { held: ctx.input.held(), pointer: ctx.input.pointer() };
|
|
74
|
+
const frame = { held: ctx.input.held(), pointer: ctx.input.pointer(), analog: ctx.input.analog() };
|
|
75
75
|
const last = lastSentInputRef.current;
|
|
76
76
|
if (last !== null && inputFramesEqual(last, frame))
|
|
77
77
|
return;
|
|
@@ -80,6 +80,7 @@ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryC
|
|
|
80
80
|
};
|
|
81
81
|
if (gateRef.current || !playControlsActive(ctx)) {
|
|
82
82
|
ctx.input.publish(heldActionsFor(tracker, NO_ACTIONS));
|
|
83
|
+
ctx.input.publishAnalog(null);
|
|
83
84
|
sendInput();
|
|
84
85
|
return;
|
|
85
86
|
}
|
|
@@ -90,6 +91,7 @@ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryC
|
|
|
90
91
|
const gameDt = ctx.time.advance(dt);
|
|
91
92
|
ctx.input.publish(heldActionsFor(tracker, inputActions));
|
|
92
93
|
ctx.input.publishPointer(pointerAxisRef.current);
|
|
94
|
+
ctx.input.publishAnalog(analogRef.current);
|
|
93
95
|
sendInput();
|
|
94
96
|
const turnInput = (tracker.isDown("turnRight") ? 1 : 0) - (tracker.isDown("turnLeft") ? 1 : 0);
|
|
95
97
|
if (turnInput !== 0)
|
|
@@ -99,7 +101,7 @@ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryC
|
|
|
99
101
|
const player = ctx.scene.entity.get(playerId);
|
|
100
102
|
if (player !== null && drivesPose && !serverAuthoritative) {
|
|
101
103
|
endPhase = devtools.profile.begin("pose");
|
|
102
|
-
stepPlayerMovement(ctx, ctx.player.userId, { held: ctx.input.held(), pointer: ctx.input.pointer() }, rawDt, movementTuning, yawRef.current);
|
|
104
|
+
stepPlayerMovement(ctx, ctx.player.userId, { held: ctx.input.held(), pointer: ctx.input.pointer(), analog: ctx.input.analog() }, rawDt, movementTuning, yawRef.current);
|
|
103
105
|
endPhase();
|
|
104
106
|
}
|
|
105
107
|
if (autoPickupRadius !== null && !serverAuthoritative) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { DayNightCycle } from "@jgengine/core/time/dayNightCycle";
|
|
2
|
+
/** Props for {@link DayNightSky}: the cycle to bind plus optional dome shape and light-strength knobs. */
|
|
3
|
+
export interface DayNightSkyProps {
|
|
4
|
+
/** The serializable day-night brain. Its `sample()` drives the sky tint/light each frame. */
|
|
5
|
+
cycle: DayNightCycle;
|
|
6
|
+
/** Sky dome radius. Default `260`. */
|
|
7
|
+
radius?: number;
|
|
8
|
+
/** Horizon haze-band strength passed straight to the dome. Default `0.5`. */
|
|
9
|
+
hazeStrength?: number;
|
|
10
|
+
/** Sun-glow brightness passed straight to the dome. Default `1`. */
|
|
11
|
+
sunGlowStrength?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Mount a hemisphere + shadow-casting directional light whose color and intensity
|
|
14
|
+
* track the cycle. Turn off (`false`) when the game authors its own `lighting`. Default `true`.
|
|
15
|
+
*/
|
|
16
|
+
lights?: boolean;
|
|
17
|
+
/** Peak directional-light intensity at full `sample().intensity` (1). Default `1.1`. */
|
|
18
|
+
keyLightIntensity?: number;
|
|
19
|
+
/** Peak hemisphere (ambient) intensity at full `sample().intensity` (1). Default `0.6`. */
|
|
20
|
+
ambientIntensity?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Binds a {@link DayNightCycle} to the engine's existing `SkyDome` shader and a pair of
|
|
24
|
+
* lights: each frame it reads `cycle.sample()` and writes the blended tint into the dome's
|
|
25
|
+
* top/horizon/sun uniforms while the sun's arc comes from the shared `daylightStateAt`
|
|
26
|
+
* geometry. This is the turnkey presentation seam — a game mounts this one component in its
|
|
27
|
+
* scene and gets a moving, color-graded day-night sky from the serializable model, with no
|
|
28
|
+
* hand-rolled per-frame lerp and no new renderer. The cycle's `phase`/color values are
|
|
29
|
+
* free-form and never interpreted here.
|
|
30
|
+
*
|
|
31
|
+
* @capability day-night-sky drop-in R3F presenter that drives the engine's existing sky dome and lights from a serializable day-night cycle for a moving, color-graded day-night look
|
|
32
|
+
*/
|
|
33
|
+
export declare function DayNightSky({ cycle, radius, hazeStrength, sunGlowStrength, lights, keyLightIntensity, ambientIntensity, }: DayNightSkyProps): import("react").JSX.Element;
|