Files
AppleHillsProduction/Assets/Scripts/Common/Input/DragLaunchController.cs
Michal Pikulski 421c4d5cbd Stash work
2025-12-05 16:24:44 +01:00

406 lines
14 KiB
C#

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;
#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
}
}