using System; using System.Numerics; namespace AcDream.App.Rendering; /// /// Third-person chase camera that follows behind and above a player /// character. Implements so it plugs into the /// existing renderer pipeline. /// public sealed class ChaseCamera : ICamera { public Vector3 Position { get; private set; } public float Aspect { get; set; } = 16f / 9f; public float FovY { get; set; } = MathF.PI / 3f; /// Distance behind the player. Clamped to [, ]. public float Distance { get; set; } = 8f; public const float DistanceMin = 2f; public const float DistanceMax = 40f; /// Camera pitch above horizontal (radians). Positive = look down. public float Pitch { get; set; } = 0.35f; // ~20 degrees /// /// Additional yaw applied on top of the player's heading when positioning /// the camera. Used by the hold-RMB "inspect" mode to orbit around the /// player without rotating the character. Snap to 0 to return the camera /// to directly behind the player. /// public float YawOffset { get; set; } = 0f; /// Vertical offset from the player's feet to the look-at point (eye height). public float EyeHeight { get; set; } = 1.5f; // Pitch range: negative values place the camera below the player's Z // (at distance * sin(Pitch)) so the player can be viewed from a low // angle. Clamped to -0.7 to avoid pushing the camera deep underground; // at -0.7 and Distance=8 the camera is ~5m below player-Z which will // clip terrain on hills but is OK on flat ground. 1.4 ≈ looking // straight down. Wider than the old [0.05, 1.4] so mouse-Y moves the // camera in both directions from the neutral [~20°] default. private const float PitchMin = -0.7f; private const float PitchMax = 1.4f; private float _playerYaw; private Vector3 _lookAt; // K-fix12 (2026-04-26): retail-feel jump camera. The camera Z is // pinned to the LAST GROUNDED Z while the player is airborne — the // character rises above the camera on screen, visually matching // retail's "you can see yourself jump" feedback. Walking on the // ground tracks Z directly (no lag on hill transitions); falling // catches up immediately so we don't end up below ground when // landing in a pit. Only the upward-while-airborne case is pinned. private float _trackedZ; private bool _trackedZInitialised; public Matrix4x4 View => Matrix4x4.CreateLookAt(Position, _lookAt, Vector3.UnitZ); public Matrix4x4 Projection => Matrix4x4.CreatePerspectiveFieldOfView(FovY, Aspect, 1f, 5000f); /// /// Update the camera position to follow the player. /// drives the airborne-pin behavior: while airborne and rising, the /// camera stays at last-grounded Z so the jump is visible on screen. /// public void Update(Vector3 playerPosition, float playerYaw, bool isOnGround = true, float dt = 1f / 60f) { _playerYaw = playerYaw; // K-fix12: track the camera's reference Z. // - On ground: snap directly to player.Z (smooth slope walking). // - Airborne + rising: stay pinned (player visibly rises above camera). // - Airborne + falling below tracked Z: catch up so we don't lag below // ground when landing somewhere lower (a cliff / hole). if (!_trackedZInitialised) { _trackedZ = playerPosition.Z; _trackedZInitialised = true; } else if (isOnGround) { _trackedZ = playerPosition.Z; } else if (playerPosition.Z < _trackedZ) { _trackedZ = playerPosition.Z; // catch up to falls / drops } // else: airborne and rising — keep _trackedZ pinned. // Look-at uses the actual player Z so the camera always points // at the character — when the player rises above the pinned // camera the look-at tilts up to keep them centered in frame. _lookAt = playerPosition + new Vector3(0f, 0f, EyeHeight); // Camera offset: behind the player (-forward direction) plus any // YawOffset for the hold-RMB inspect orbit mode. float effectiveYaw = playerYaw + YawOffset; float forwardX = MathF.Cos(effectiveYaw); float forwardY = MathF.Sin(effectiveYaw); float horizontalDist = Distance * MathF.Cos(Pitch); float verticalDist = Distance * MathF.Sin(Pitch); Position = new Vector3( playerPosition.X - forwardX * horizontalDist, playerPosition.Y - forwardY * horizontalDist, _trackedZ + EyeHeight + verticalDist); // ← uses tracked Z (pinned to ground while airborne) } /// /// Adjust pitch by a delta (from mouse Y movement). /// public void AdjustPitch(float delta) { Pitch = Math.Clamp(Pitch + delta, PitchMin, PitchMax); } /// /// Adjust distance (zoom) by a delta, clamped to [DistanceMin, DistanceMax]. /// public void AdjustDistance(float delta) { Distance = Math.Clamp(Distance + delta, DistanceMin, DistanceMax); } }