2025-12-07 19:36:57 +00:00
using System ;
using Core ;
using Core.Lifecycle ;
using Input ;
using UnityEngine ;
namespace Common.Input
{
/// <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 ;
}
/// <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 ;
[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 ;
2025-12-16 18:58:50 +01:00
/// <summary>
/// Protected property to allow derived classes to set enabled state
/// </summary>
protected bool Enabled
{
get = > _isEnabled ;
set = > _isEnabled = value ;
}
2025-12-07 19:36:57 +00:00
#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 < Common . Visual . TrajectoryPreview > ( ) ;
}
}
#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
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" ) ;
}
/// <summary>
/// Disable the launch controller and unregister from InputManager
/// </summary>
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
/// <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 )
{
// 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 ) ;
}
/// <summary>
/// End drag operation and potentially launch
/// </summary>
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 ) ;
}
/// <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)
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
/// <summary>
/// Perform the actual launch (spawn projectile/airplane, apply force, etc.)
/// </summary>
protected abstract void PerformLaunch ( Vector2 direction , float force ) ;
#endregion
#region Virtual Methods - Visual Feedback ( Override if needed )
/// <summary>
/// Update visual feedback during drag (trajectory preview, rubber band, etc.)
/// Default: Updates trajectory preview using prefab's physics properties.
/// Override for custom visuals.
/// </summary>
/// <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 )
{
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 < 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 ) ;
}
}
/// <summary>
/// Show preview visuals when controller is enabled.
/// Default: Shows trajectory preview.
/// Override for custom visuals.
/// </summary>
protected virtual void ShowPreview ( )
{
trajectoryPreview ? . Show ( ) ;
}
/// <summary>
/// Hide preview visuals when controller is disabled.
/// Default: Hides trajectory preview.
/// Override for custom visuals.
/// </summary>
protected virtual void HidePreview ( )
{
trajectoryPreview ? . Hide ( ) ;
}
public Transform GetLaunchAnchorTransform ( )
{
return launchAnchor ;
}
#endregion
#region Abstract Methods - Physics Configuration
/// <summary>
/// Get projectile mass for trajectory calculation.
/// MUST read from settings - the same source that Initialize() uses.
/// Subclasses implement to return the actual runtime mass.
/// </summary>
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
}
}