From 8f30e1331742650f98ae96c85bb30b8a0d570637 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 18 May 2026 20:17:49 +0200 Subject: [PATCH] feat(camera): wire RetailChaseCamera through GameWindow GameWindow now constructs both ChaseCamera + RetailChaseCamera at player-mode entry, updates both per frame (legacy with isOnGround, retail with BodyVelocity), and routes mouse/wheel/held-key input to whichever the CameraDiagnostics flag selects. Mouse-Y goes through RetailChaseCamera.FilterMouseDelta before AdjustPitch when retail is active; legacy path is unchanged. Held-key bindings (CameraZoomIn/Out, CameraRaise/Lower; default-unbound) integrate distance/pitch at CameraDiagnostics.CameraAdjustmentSpeed per second. Default behavior: ACDREAM_RETAIL_CHASE unset -> legacy camera as before. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 88 +++++++++++++++++++++---- 1 file changed, 74 insertions(+), 14 deletions(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 2d219dc..1fefb22 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -627,6 +627,7 @@ public sealed class GameWindow : IDisposable // Phase B.2: player movement mode. private AcDream.App.Input.PlayerMovementController? _playerController; private AcDream.App.Rendering.ChaseCamera? _chaseCamera; + private AcDream.App.Rendering.RetailChaseCamera? _retailChaseCamera; private bool _playerMode; private uint _playerServerGuid; private uint? _playerCurrentAnimCommand; @@ -993,7 +994,17 @@ public sealed class GameWindow : IDisposable // here goes ONLY through ApplyDelta — no separate // YawOffset write. dy still pitches the camera only. _mouseLook.ApplyDelta(dx, sens); - _chaseCamera.AdjustPitch(dy * 0.003f * sens); + if (AcDream.Core.Rendering.CameraDiagnostics.UseRetailChaseCamera && _retailChaseCamera is not null) + { + float nowSec = (float)(Environment.TickCount64 / 1000.0); + var (_, filteredDy) = _retailChaseCamera.FilterMouseDelta( + rawX: 0f, rawY: dy, weight: 0.5f, nowSec: nowSec); + _retailChaseCamera.AdjustPitch(filteredDy * 0.003f * sens); + } + else + { + _chaseCamera.AdjustPitch(dy * 0.003f * sens); + } } else if (_rmbHeld) { @@ -1004,8 +1015,19 @@ public sealed class GameWindow : IDisposable // ANYTHING when in player mode — character yaw is // dispatcher-only (A/D keys). K.2: MMB mouse-look path // above takes precedence when active. - _chaseCamera.YawOffset -= dx * 0.004f * sens; - _chaseCamera.AdjustPitch(dy * 0.003f * sens); + if (AcDream.Core.Rendering.CameraDiagnostics.UseRetailChaseCamera && _retailChaseCamera is not null) + { + float nowSec = (float)(Environment.TickCount64 / 1000.0); + var (filteredDx, filteredDy) = _retailChaseCamera.FilterMouseDelta( + rawX: dx, rawY: dy, weight: 0.5f, nowSec: nowSec); + _retailChaseCamera.YawOffset -= filteredDx * 0.004f * sens; + _retailChaseCamera.AdjustPitch(filteredDy * 0.003f * sens); + } + else + { + _chaseCamera.YawOffset -= dx * 0.004f * sens; + _chaseCamera.AdjustPitch(dy * 0.003f * sens); + } } // K-fix1 (2026-04-26): no default-pitch path. With // neither MMB nor RMB held, mouse moves the cursor @@ -4735,6 +4757,7 @@ public sealed class GameWindow : IDisposable // 4. Recenter chase camera on the new position. _chaseCamera?.Update(snappedPos, _playerController.Yaw); + _retailChaseCamera?.Update(snappedPos, _playerController.Yaw, System.Numerics.Vector3.Zero, dt: 1f / 60f); // 5. Return to InWorld. _playerController.State = AcDream.App.Input.PlayerState.InWorld; @@ -6324,6 +6347,21 @@ public sealed class GameWindow : IDisposable if (_inputDispatcher is null) return; _playerMouseDeltaX = 0f; // defensive: ensure no leakage even if some path writes it + // Retail-style held-key offset integration. Only active when + // retail chase is selected; legacy camera ignores these. + if (AcDream.Core.Rendering.CameraDiagnostics.UseRetailChaseCamera && _retailChaseCamera is not null) + { + float adj = AcDream.Core.Rendering.CameraDiagnostics.CameraAdjustmentSpeed * (float)dt; + if (_inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.CameraZoomIn)) + _retailChaseCamera.AdjustDistance(-adj); + if (_inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.CameraZoomOut)) + _retailChaseCamera.AdjustDistance(+adj); + if (_inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.CameraRaise)) + _retailChaseCamera.AdjustPitch(+adj * 0.02f); + if (_inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.CameraLower)) + _retailChaseCamera.AdjustPitch(-adj * 0.02f); + } + // K-fix1 (2026-04-26): retail-faithful movement semantics. // * Default speed = RUN. Forward / backward / strafe all run // by default; holding Shift (MovementWalkMode) drops to @@ -6380,16 +6418,22 @@ public sealed class GameWindow : IDisposable _worldState.RelocateEntity(pe, currentLb); } - // Update chase camera. K-fix12 (2026-04-26): pass isOnGround - // so the camera pins its Z to last-grounded while the - // player is airborne — without this the camera follows - // player.Z 1:1 during a jump and the player's screen - // position never changes. With the pin: player visibly - // rises above the camera, matching retail "you can see - // yourself jump" feedback. + // Update chase camera(s). The CameraController exposes whichever + // is currently selected via CameraDiagnostics.UseRetailChaseCamera; + // both update every frame so toggling the flag swaps instantly + // with the new camera already warm. + // + // Legacy ChaseCamera: pre-K-fix12 args (isOnGround pins Z during + // jumps as a workaround for the visual feel retail gets from + // low-stiffness damping). _chaseCamera.Update(result.RenderPosition, _playerController.Yaw, isOnGround: result.IsOnGround, dt: (float)dt); + // RetailChaseCamera: takes world velocity for slope-aligned heading; + // jump-feedback falls out of damping naturally, no isOnGround needed. + _retailChaseCamera!.Update(result.RenderPosition, _playerController.Yaw, + playerVelocity: _playerController.BodyVelocity, + dt: (float)dt); // Send outbound movement messages to the live server. if (_liveSession is not null) @@ -8963,6 +9007,7 @@ public sealed class GameWindow : IDisposable _cameraController?.ExitChaseMode(); _playerController = null; _chaseCamera = null; + _retailChaseCamera = null; _playerCurrentAnimCommand = null; } else @@ -9801,6 +9846,7 @@ public sealed class GameWindow : IDisposable _cameraController?.ExitChaseMode(); _playerController = null; _chaseCamera = null; + _retailChaseCamera = null; _playerCurrentAnimCommand = null; _playerMouseDeltaX = 0f; } @@ -9856,7 +9902,14 @@ public sealed class GameWindow : IDisposable && _playerMode && _chaseCamera is not null) { - _cameraController.EnterChaseMode(_chaseCamera, new RetailChaseCamera { Aspect = _chaseCamera.Aspect }); + if (_retailChaseCamera is null) + { + _retailChaseCamera = new AcDream.App.Rendering.RetailChaseCamera + { + Aspect = _chaseCamera.Aspect, + }; + } + _cameraController.EnterChaseMode(_chaseCamera, _retailChaseCamera); return; } _cameraController.ToggleFly(); @@ -9957,12 +10010,16 @@ public sealed class GameWindow : IDisposable { Aspect = _window!.Size.X / (float)_window.Size.Y, }; + _retailChaseCamera = new AcDream.App.Rendering.RetailChaseCamera + { + Aspect = _window!.Size.X / (float)_window.Size.Y, + }; // K.1b: _playerMouseDeltaX is no longer consumed by // MovementInput, but we still reset it here so any stale // accumulated value from a previous session doesn't leak // into a future code path that re-enables mouse-yaw. _playerMouseDeltaX = 0f; - _cameraController?.EnterChaseMode(_chaseCamera, new RetailChaseCamera { Aspect = _chaseCamera.Aspect }); + _cameraController?.EnterChaseMode(_chaseCamera, _retailChaseCamera); // K-fix1 (2026-04-26): latch the "we have entered chase at least // once" flag so the live-mode pre-login render gate stops // suppressing the scene. From here on, the orbit camera (if the @@ -10106,10 +10163,13 @@ public sealed class GameWindow : IDisposable if (_cameraController is null) return; float dir = (action == AcDream.UI.Abstractions.Input.InputAction.ScrollUp) ? 1f : -1f; - if (_playerMode && _cameraController.IsChaseMode && _chaseCamera is not null) + if (_playerMode && _cameraController.IsChaseMode) { // Chase mode: zoom (closer on ScrollUp). - _chaseCamera.AdjustDistance(-dir * 0.8f); + if (AcDream.Core.Rendering.CameraDiagnostics.UseRetailChaseCamera && _retailChaseCamera is not null) + _retailChaseCamera.AdjustDistance(-dir * 0.8f); + else if (_chaseCamera is not null) + _chaseCamera.AdjustDistance(-dir * 0.8f); } else if (_cameraController.IsFlyMode) {