2025-12-04 15:10:20 +01:00
using System ;
using Core ;
using Core.Lifecycle ;
using Input ;
using UnityEngine ;
namespace Common.Input
{
2025-12-05 11:03:47 +01:00
/// <summary>
/// Cached launch parameters calculated during drag.
/// Avoids recalculating force/direction multiple times.
/// </summary>
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 ;
}
2025-12-04 15:10:20 +01:00
/// <summary>
/// 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.
/// </summary>
public abstract class DragLaunchController : ManagedBehaviour , ITouchInputConsumer
{
#region Events
/// <summary>
/// Fired when drag starts. Parameters: (Vector2 startPosition)
/// </summary>
public event Action < Vector2 > OnDragStart ;
/// <summary>
/// Fired during drag update. Parameters: (Vector2 currentPosition, Vector2 direction, float force)
/// </summary>
public event Action < Vector2 , Vector2 , float > OnDragUpdate ;
/// <summary>
/// Fired when drag ends. Parameters: (Vector2 endPosition, Vector2 direction, float force)
/// </summary>
public event Action < Vector2 , Vector2 , float > OnDragEnd ;
/// <summary>
/// Fired when launch occurs. Parameters: (Vector2 direction, float force)
/// </summary>
public event Action < Vector2 , float > OnLaunch ;
#endregion
#region Settings
private SlingshotConfig _config ;
protected SlingshotConfig Config
{
get
{
if ( _config = = null )
{
_config = GetSlingshotConfig ( ) ;
}
return _config ;
}
}
/// <summary>
/// Subclasses implement to return their slingshot configuration
/// from their specific settings object
/// </summary>
protected abstract SlingshotConfig GetSlingshotConfig ( ) ;
/// <summary>
/// Subclasses implement to return the projectile prefab that will be launched.
/// Used for reading Rigidbody2D properties (mass, gravityScale) for trajectory calculations.
/// </summary>
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 ;
2025-12-05 11:03:47 +01:00
[Tooltip("Trajectory preview component (auto-found if not assigned)")]
[SerializeField] protected Common . Visual . TrajectoryPreview trajectoryPreview ;
2025-12-04 15:10:20 +01:00
[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 ;
2025-12-05 11:03:47 +01:00
private bool _isRegisteredForInput = false ;
// Cached launch parameters - calculated once during drag, used for both preview and launch
private LaunchParameters _cachedLaunchParams ;
2025-12-04 15:10:20 +01:00
public bool IsDragging = > _isDragging ;
public bool IsEnabled = > _isEnabled ;
#endregion
#region Lifecycle
internal override void OnManagedAwake ( )
{
base . OnManagedAwake ( ) ;
if ( launchAnchor = = null )
{
launchAnchor = transform ;
}
2025-12-04 16:23:53 +01:00
// Auto-find trajectory preview if not assigned
if ( trajectoryPreview = = null )
{
trajectoryPreview = GetComponent < Common . Visual . TrajectoryPreview > ( ) ;
}
2025-12-04 15:10:20 +01:00
}
#endregion
#region Enable / Disable
/// <summary>
/// Enable the launch controller and register with InputManager
/// </summary>
public virtual void Enable ( )
{
_isEnabled = true ;
// Register with InputManager as override consumer
2025-12-05 11:03:47 +01:00
if ( InputManager . Instance ! = null & & ! _isRegisteredForInput )
2025-12-04 15:10:20 +01:00
{
InputManager . Instance . RegisterOverrideConsumer ( this ) ;
2025-12-05 11:03:47 +01:00
_isRegisteredForInput = true ;
2025-12-04 15:10:20 +01:00
if ( showDebugLogs ) Logging . Debug ( $"[{GetType().Name}] Registered with InputManager" ) ;
}
// Show preview visuals
ShowPreview ( ) ;
if ( showDebugLogs ) Logging . Debug ( $"[{GetType().Name}] Enabled" ) ;
}
/// <summary>
/// Disable the launch controller and unregister from InputManager
/// </summary>
public virtual void Disable ( )
{
_isEnabled = false ;
_isDragging = false ;
// Unregister from InputManager
2025-12-05 11:03:47 +01:00
if ( InputManager . Instance ! = null & & _isRegisteredForInput )
2025-12-04 15:10:20 +01:00
{
InputManager . Instance . UnregisterOverrideConsumer ( this ) ;
2025-12-05 11:03:47 +01:00
_isRegisteredForInput = false ;
2025-12-04 15:10:20 +01:00
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
/// <summary>
/// Start drag operation
/// </summary>
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 ) ;
}
/// <summary>
/// Update drag operation
/// </summary>
protected virtual void UpdateDrag ( Vector2 currentWorldPosition )
{
2025-12-05 11:03:47 +01:00
// Calculate launch parameters once and cache
_cachedLaunchParams = CalculateLaunchParameters ( currentWorldPosition ) ;
2025-12-04 15:10:20 +01:00
// Warn if mass is zero or invalid
2025-12-05 11:03:47 +01:00
if ( _cachedLaunchParams . Mass < = 0f & & showDebugLogs )
2025-12-04 15:10:20 +01:00
{
2025-12-05 11:03:47 +01:00
Logging . Warning ( $"[{GetType().Name}] Projectile mass is {_cachedLaunchParams.Mass}! Trajectory calculation will be inaccurate. Override GetProjectileMass()." ) ;
2025-12-04 15:10:20 +01:00
}
2025-12-05 11:03:47 +01:00
// Update visuals with cached parameters
UpdateVisuals ( currentWorldPosition , _cachedLaunchParams ) ;
2025-12-04 15:10:20 +01:00
2025-12-05 11:03:47 +01:00
OnDragUpdate ? . Invoke ( currentWorldPosition , _cachedLaunchParams . Direction , _cachedLaunchParams . Force ) ;
2025-12-04 15:10:20 +01:00
}
/// <summary>
/// End drag operation and potentially launch
/// </summary>
protected virtual void EndDrag ( Vector2 currentWorldPosition )
{
_isDragging = false ;
// Hide preview
HidePreview ( ) ;
2025-12-05 11:03:47 +01:00
// 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 ) ;
}
/// <summary>
/// Calculate launch parameters from current drag position.
/// Caches results to avoid recalculating force multiple times.
/// </summary>
private LaunchParameters CalculateLaunchParameters ( Vector2 currentWorldPosition )
{
// Calculate drag vector from anchor to current drag position
// Pull back (away from anchor) = launch forward (toward anchor direction)
2025-12-04 15:10:20 +01:00
Vector2 dragVector = _dragStartPosition - currentWorldPosition ;
2025-12-05 11:03:47 +01:00
// Calculate distance and ratio
2025-12-04 15:10:20 +01:00
float dragDistance = dragVector . magnitude ;
float dragRatio = Mathf . Clamp01 ( dragDistance / MaxDragDistance ) ;
2025-12-05 11:03:47 +01:00
// Calculate force using config
2025-12-04 15:10:20 +01:00
float force = Config ? . CalculateForce ( dragDistance , dragRatio ) ? ? ( dragRatio * MaxForce ) ;
2025-12-05 11:03:47 +01:00
// Normalize direction
Vector2 direction = dragDistance > 0.01f ? dragVector . normalized : Vector2 . zero ;
2025-12-04 15:10:20 +01:00
2025-12-05 11:03:47 +01:00
// Get mass from projectile
float mass = GetProjectileMass ( ) ;
2025-12-04 15:10:20 +01:00
2025-12-05 11:03:47 +01:00
return new LaunchParameters
2025-12-04 15:10:20 +01:00
{
2025-12-05 11:03:47 +01:00
Direction = direction ,
Force = force ,
DragDistance = dragDistance ,
DragRatio = dragRatio ,
Mass = mass
} ;
2025-12-04 15:10:20 +01:00
}
#endregion
#region Abstract Methods - Subclass Implementation
/// <summary>
2025-12-04 16:23:53 +01:00
/// Perform the actual launch (spawn projectile/airplane, apply force, etc.)
2025-12-04 15:10:20 +01:00
/// </summary>
2025-12-04 16:23:53 +01:00
protected abstract void PerformLaunch ( Vector2 direction , float force ) ;
#endregion
#region Virtual Methods - Visual Feedback ( Override if needed )
2025-12-04 15:10:20 +01:00
/// <summary>
2025-12-04 16:23:53 +01:00
/// Update visual feedback during drag (trajectory preview, rubber band, etc.)
/// Default: Updates trajectory preview using prefab's physics properties.
/// Override for custom visuals.
2025-12-04 15:10:20 +01:00
/// </summary>
2025-12-05 11:03:47 +01:00
/// <param name="currentPosition">Current drag position</param>
/// <param name="launchParams">Cached launch parameters (direction, force, etc.)</param>
protected virtual void UpdateVisuals ( Vector2 currentPosition , LaunchParameters launchParams )
2025-12-04 16:23:53 +01:00
{
2025-12-05 11:03:47 +01:00
if ( trajectoryPreview ! = null & & launchParams . DragDistance > 0.1f )
2025-12-04 16:23:53 +01:00
{
GameObject prefab = GetProjectilePrefab ( ) ;
2025-12-05 11:03:47 +01:00
if ( prefab = = null ) return ;
// Get gravity from prefab's Rigidbody2D gravityScale
var rb = prefab . GetComponent < Rigidbody2D > ( ) ;
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 ) ;
2025-12-04 16:23:53 +01:00
}
}
2025-12-04 15:10:20 +01:00
/// <summary>
2025-12-04 16:23:53 +01:00
/// Show preview visuals when controller is enabled.
/// Default: Shows trajectory preview.
/// Override for custom visuals.
2025-12-04 15:10:20 +01:00
/// </summary>
2025-12-04 16:23:53 +01:00
protected virtual void ShowPreview ( )
{
trajectoryPreview ? . Show ( ) ;
}
2025-12-04 15:10:20 +01:00
/// <summary>
2025-12-04 16:23:53 +01:00
/// Hide preview visuals when controller is disabled.
/// Default: Hides trajectory preview.
/// Override for custom visuals.
2025-12-04 15:10:20 +01:00
/// </summary>
2025-12-04 16:23:53 +01:00
protected virtual void HidePreview ( )
{
trajectoryPreview ? . Hide ( ) ;
}
2025-12-04 15:10:20 +01:00
2025-12-05 16:24:44 +01:00
public Transform GetLaunchAnchorTransform ( )
{
return launchAnchor ;
}
2025-12-04 15:10:20 +01:00
#endregion
2025-12-05 11:03:47 +01:00
#region Abstract Methods - Physics Configuration
2025-12-04 15:10:20 +01:00
/// <summary>
/// Get projectile mass for trajectory calculation.
2025-12-05 11:03:47 +01:00
/// MUST read from settings - the same source that Initialize() uses.
/// Subclasses implement to return the actual runtime mass.
2025-12-04 15:10:20 +01:00
/// </summary>
2025-12-05 11:03:47 +01:00
protected abstract float GetProjectileMass ( ) ;
2025-12-04 15:10:20 +01:00
#endregion
#region Cleanup
internal override void OnManagedDestroy ( )
{
base . OnManagedDestroy ( ) ;
// Ensure we unregister from InputManager
2025-12-05 11:03:47 +01:00
if ( _isRegisteredForInput & & InputManager . Instance ! = null )
2025-12-04 15:10:20 +01:00
{
InputManager . Instance . UnregisterOverrideConsumer ( this ) ;
}
}
#endregion
}
}