using System; using Core; using Core.Lifecycle; using Input; using UnityEngine; namespace Common.Input { /// /// Cached launch parameters calculated during drag. /// Avoids recalculating force/direction multiple times. /// public struct LaunchParameters { public Vector2 Direction; public float Force; public float DragDistance; public float DragRatio; public float Mass; public bool IsValid => Force > 0f && DragDistance > 0f; } /// /// Base class for drag-to-launch mechanics (Angry Birds style). /// Provides core drag logic, force calculation, and input handling. /// Uses SlingshotConfig for all settings - fully configuration-driven. /// Subclasses implement visual feedback and specific launch behavior. /// public abstract class DragLaunchController : ManagedBehaviour, ITouchInputConsumer { #region Events /// /// Fired when drag starts. Parameters: (Vector2 startPosition) /// public event Action OnDragStart; /// /// Fired during drag update. Parameters: (Vector2 currentPosition, Vector2 direction, float force) /// public event Action OnDragUpdate; /// /// Fired when drag ends. Parameters: (Vector2 endPosition, Vector2 direction, float force) /// public event Action OnDragEnd; /// /// Fired when launch occurs. Parameters: (Vector2 direction, float force) /// public event Action OnLaunch; #endregion #region Settings private SlingshotConfig _config; protected SlingshotConfig Config { get { if (_config == null) { _config = GetSlingshotConfig(); } return _config; } } /// /// Subclasses implement to return their slingshot configuration /// from their specific settings object /// protected abstract SlingshotConfig GetSlingshotConfig(); /// /// Subclasses implement to return the projectile prefab that will be launched. /// Used for reading Rigidbody2D properties (mass, gravityScale) for trajectory calculations. /// protected abstract GameObject GetProjectilePrefab(); #endregion #region Inspector Properties [Header("Launch Settings Overrides (leave 0 to use config)")] [Tooltip("Override max drag distance (0 = use config)")] [SerializeField] protected float maxDragDistanceOverride = 0f; [Tooltip("Override max force (0 = use config)")] [SerializeField] protected float maxForceOverride = 0f; [Header("References")] [Tooltip("Launch anchor point (spawn/slingshot position)")] [SerializeField] protected Transform launchAnchor; [Tooltip("Trajectory preview component (auto-found if not assigned)")] [SerializeField] protected Common.Visual.TrajectoryPreview trajectoryPreview; [Header("Debug")] [SerializeField] protected bool showDebugLogs; #endregion #region Computed Properties protected float MaxDragDistance => maxDragDistanceOverride > 0 ? maxDragDistanceOverride : Config?.maxDragDistance ?? 5f; protected float MaxForce => maxForceOverride > 0 ? maxForceOverride : Config?.baseLaunchForce ?? 20f; #endregion #region State private bool _isDragging; private Vector2 _dragStartPosition; private bool _isEnabled = false; private bool _isRegisteredForInput = false; // Cached launch parameters - calculated once during drag, used for both preview and launch private LaunchParameters _cachedLaunchParams; public bool IsDragging => _isDragging; public bool IsEnabled => _isEnabled; #endregion #region Lifecycle internal override void OnManagedAwake() { base.OnManagedAwake(); if (launchAnchor == null) { launchAnchor = transform; } // Auto-find trajectory preview if not assigned if (trajectoryPreview == null) { trajectoryPreview = GetComponent(); } } #endregion #region Enable/Disable /// /// Enable the launch controller and register with InputManager /// public virtual void Enable() { _isEnabled = true; // Register with InputManager as override consumer if (InputManager.Instance != null && !_isRegisteredForInput) { InputManager.Instance.RegisterOverrideConsumer(this); _isRegisteredForInput = true; if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Registered with InputManager"); } // Show preview visuals ShowPreview(); if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Enabled"); } /// /// Disable the launch controller and unregister from InputManager /// public virtual void Disable() { _isEnabled = false; _isDragging = false; // Unregister from InputManager if (InputManager.Instance != null && _isRegisteredForInput) { InputManager.Instance.UnregisterOverrideConsumer(this); _isRegisteredForInput = false; if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Unregistered from InputManager"); } // Hide preview visuals HidePreview(); if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Disabled"); } #endregion #region ITouchInputConsumer Implementation public void OnTap(Vector2 worldPosition) { // Drag-to-launch uses hold/drag, not tap } public void OnHoldStart(Vector2 worldPosition) { if (!_isEnabled) return; StartDrag(worldPosition); } public void OnHoldMove(Vector2 worldPosition) { if (!_isEnabled || !_isDragging) return; UpdateDrag(worldPosition); } public void OnHoldEnd(Vector2 worldPosition) { if (!_isEnabled || !_isDragging) return; EndDrag(worldPosition); } #endregion #region Drag Handling /// /// Start drag operation /// protected virtual void StartDrag(Vector2 worldPosition) { _isDragging = true; // Use launch anchor as the reference point (like Angry Birds) _dragStartPosition = launchAnchor.position; if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Started drag at {worldPosition}, anchor at {_dragStartPosition}"); OnDragStart?.Invoke(worldPosition); } /// /// Update drag operation /// protected virtual void UpdateDrag(Vector2 currentWorldPosition) { // Calculate launch parameters once and cache _cachedLaunchParams = CalculateLaunchParameters(currentWorldPosition); // Warn if mass is zero or invalid if (_cachedLaunchParams.Mass <= 0f && showDebugLogs) { Logging.Warning($"[{GetType().Name}] Projectile mass is {_cachedLaunchParams.Mass}! Trajectory calculation will be inaccurate. Override GetProjectileMass()."); } // Update visuals with cached parameters UpdateVisuals(currentWorldPosition, _cachedLaunchParams); OnDragUpdate?.Invoke(currentWorldPosition, _cachedLaunchParams.Direction, _cachedLaunchParams.Force); } /// /// End drag operation and potentially launch /// protected virtual void EndDrag(Vector2 currentWorldPosition) { _isDragging = false; // Hide preview HidePreview(); // Recalculate final parameters (position may have changed since last UpdateDrag) _cachedLaunchParams = CalculateLaunchParameters(currentWorldPosition); OnDragEnd?.Invoke(currentWorldPosition, _cachedLaunchParams.Direction, _cachedLaunchParams.Force); if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Launching with force {_cachedLaunchParams.Force:F2}"); PerformLaunch(_cachedLaunchParams.Direction, _cachedLaunchParams.Force); OnLaunch?.Invoke(_cachedLaunchParams.Direction, _cachedLaunchParams.Force); } /// /// Calculate launch parameters from current drag position. /// Caches results to avoid recalculating force multiple times. /// private LaunchParameters CalculateLaunchParameters(Vector2 currentWorldPosition) { // Calculate drag vector from anchor to current drag position // Pull back (away from anchor) = launch forward (toward anchor direction) Vector2 dragVector = _dragStartPosition - currentWorldPosition; // Calculate distance and ratio float dragDistance = dragVector.magnitude; float dragRatio = Mathf.Clamp01(dragDistance / MaxDragDistance); // Calculate force using config float force = Config?.CalculateForce(dragDistance, dragRatio) ?? (dragRatio * MaxForce); // Normalize direction Vector2 direction = dragDistance > 0.01f ? dragVector.normalized : Vector2.zero; // Get mass from projectile float mass = GetProjectileMass(); return new LaunchParameters { Direction = direction, Force = force, DragDistance = dragDistance, DragRatio = dragRatio, Mass = mass }; } #endregion #region Abstract Methods - Subclass Implementation /// /// Perform the actual launch (spawn projectile/airplane, apply force, etc.) /// protected abstract void PerformLaunch(Vector2 direction, float force); #endregion #region Virtual Methods - Visual Feedback (Override if needed) /// /// Update visual feedback during drag (trajectory preview, rubber band, etc.) /// Default: Updates trajectory preview using prefab's physics properties. /// Override for custom visuals. /// /// Current drag position /// Cached launch parameters (direction, force, etc.) protected virtual void UpdateVisuals(Vector2 currentPosition, LaunchParameters launchParams) { if (trajectoryPreview != null && launchParams.DragDistance > 0.1f) { GameObject prefab = GetProjectilePrefab(); if (prefab == null) return; // Get gravity from prefab's Rigidbody2D gravityScale var rb = prefab.GetComponent(); float gravityScale = rb != null ? rb.gravityScale : 1f; float gravity = Physics2D.gravity.magnitude * gravityScale; // Use mass from settings (already in launchParams) trajectoryPreview.UpdateTrajectory(launchAnchor.position, launchParams.Direction, launchParams.Force, launchParams.Mass, gravity); } } /// /// Show preview visuals when controller is enabled. /// Default: Shows trajectory preview. /// Override for custom visuals. /// protected virtual void ShowPreview() { trajectoryPreview?.Show(); } /// /// Hide preview visuals when controller is disabled. /// Default: Hides trajectory preview. /// Override for custom visuals. /// protected virtual void HidePreview() { trajectoryPreview?.Hide(); } public Transform GetLaunchAnchorTransform() { return launchAnchor; } #endregion #region Abstract Methods - Physics Configuration /// /// Get projectile mass for trajectory calculation. /// MUST read from settings - the same source that Initialize() uses. /// Subclasses implement to return the actual runtime mass. /// protected abstract float GetProjectileMass(); #endregion #region Cleanup internal override void OnManagedDestroy() { base.OnManagedDestroy(); // Ensure we unregister from InputManager if (_isRegisteredForInput && InputManager.Instance != null) { InputManager.Instance.UnregisterOverrideConsumer(this); } } #endregion } }