using Godot; namespace Cirno.Scripts.Misc; public partial class CameraController3D : Camera3D { public static CameraController3D Instance { get; private set; } [Export] public bool EnableSmoothing = true; [Export] public bool FollowTargeting = true; [Export] public float SmoothTime = 0.2f; [Export] public float MaxAimOffsetDistance = 2.0f; [Export] public float AimLerpSpeed = 8.0f; [Export] public float AimDeadzone = 0.2f; [Export] public Vector3 CameraOffset = new Vector3(0, 12, -12); // Relative to target [Export] public StringName AimUpName = "aim_up"; [Export] public StringName AimDownName = "aim_down"; [Export] public StringName AimLeftName = "aim_left"; [Export] public StringName AimRightName = "aim_right"; [Export] public NodePath TargetPath; private Node3D _target; private Vector3 _currentPosition = Vector3.Zero; private Vector3 _currentAimOffset = Vector3.Zero; public override void _Ready() { Instance = this; _target = GetNode(TargetPath); if (_target == null) { GD.PushError("Camera target not found."); return; } _currentPosition = GlobalPosition; // Set fixed isometric angle once: -45° X tilt, 45° Y pan RotationDegrees = new Vector3(-45f, 45f, 0f); Projection = ProjectionType.Orthogonal; } public override void _Process(double delta) { if (_target == null) return; float dt = (float)delta; Vector3 targetPos = _target.GlobalTransform.Origin; // Aim offset if (FollowTargeting) { Vector3 desiredOffset = GetAimOffsetWorldSpace(); _currentAimOffset = _currentAimOffset.Lerp(desiredOffset, AimLerpSpeed * dt); if (_currentAimOffset.Length() > MaxAimOffsetDistance) _currentAimOffset = _currentAimOffset.Normalized() * MaxAimOffsetDistance; } else { _currentAimOffset = Vector3.Zero; } // Final target position Vector3 targetWithOffset = targetPos + _currentAimOffset; Vector3 desiredCameraPos = targetWithOffset + CameraOffset; if (EnableSmoothing) { float smoothingFactor = 1f - Mathf.Exp(-dt / SmoothTime); _currentPosition = _currentPosition.Lerp(desiredCameraPos, smoothingFactor); } else { _currentPosition = desiredCameraPos; } GlobalPosition = _currentPosition; // No LookAt or dynamic rotation — angle is fixed } private Vector3 GetAimOffsetWorldSpace() { Vector2 stickDir = new Vector2( Input.GetActionStrength(AimRightName) - Input.GetActionStrength(AimLeftName), Input.GetActionStrength(AimDownName) - Input.GetActionStrength(AimUpName) ); float stickLen = stickDir.Length(); if (stickLen > AimDeadzone) { float scaled = (stickLen - AimDeadzone) / (1f - AimDeadzone); Vector2 aimDir2D = stickDir.Normalized() * Mathf.Clamp(scaled, 0f, 1f); return new Vector3(aimDir2D.X, 0, aimDir2D.Y); } // Mouse fallback Vector2 mousePos = GetViewport().GetMousePosition(); Vector3 rayOrigin = ProjectRayOrigin(mousePos); Vector3 rayDir = ProjectRayNormal(mousePos) * 1000f; var plane = new Plane(Vector3.Up, 0); var hit = plane.IntersectsRay(rayOrigin, rayDir); if (hit is Vector3 hitPoint) { Vector3 offset = hitPoint - _target.GlobalTransform.Origin; offset.Y = 0; float dist = offset.Length(); if (dist > 0.01f) { float scaled = Mathf.Clamp((dist - AimDeadzone) / (10f - AimDeadzone), 0f, 1f); return offset.Normalized() * scaled; } } return Vector3.Zero; } }