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) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-18 20:17:49 +02:00
parent ff8f434711
commit 8f30e13317

View file

@ -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)
{