diff --git a/Assets/Scripts/Common/Input/DragLaunchController.cs b/Assets/Scripts/Common/Input/DragLaunchController.cs
index 075d4f43..4495d69b 100644
--- a/Assets/Scripts/Common/Input/DragLaunchController.cs
+++ b/Assets/Scripts/Common/Input/DragLaunchController.cs
@@ -124,6 +124,15 @@ namespace Common.Input
public bool IsDragging => _isDragging;
public bool IsEnabled => _isEnabled;
+ ///
+ /// Protected property to allow derived classes to set enabled state
+ ///
+ protected bool Enabled
+ {
+ get => _isEnabled;
+ set => _isEnabled = value;
+ }
+
#endregion
#region Lifecycle
diff --git a/Assets/Scripts/Core/Settings/SettingsInterfaces.cs b/Assets/Scripts/Core/Settings/SettingsInterfaces.cs
index d34c0630..d1e6a2bb 100644
--- a/Assets/Scripts/Core/Settings/SettingsInterfaces.cs
+++ b/Assets/Scripts/Core/Settings/SettingsInterfaces.cs
@@ -254,6 +254,7 @@ namespace Core.Settings
// AI Difficulty Settings
Minigames.FortFight.Data.AIDifficulty DefaultAIDifficulty { get; } // Default difficulty level for AI
Minigames.FortFight.Data.AIDifficultyData GetAIDifficultyData(Minigames.FortFight.Data.AIDifficulty difficulty);
+ IReadOnlyList AIAllowedProjectiles { get; } // Projectiles AI can use
// Weak point settings
float WeakPointExplosionRadius { get; }
diff --git a/Assets/Scripts/Minigames/FortFight/AI/FortFightAIController.cs b/Assets/Scripts/Minigames/FortFight/AI/FortFightAIController.cs
index 0bf81e2d..840ab5e9 100644
--- a/Assets/Scripts/Minigames/FortFight/AI/FortFightAIController.cs
+++ b/Assets/Scripts/Minigames/FortFight/AI/FortFightAIController.cs
@@ -250,9 +250,17 @@ namespace Minigames.FortFight.AI
// Get AI player index
int playerIndex = _turnManager.CurrentPlayer.PlayerIndex;
- // Get available projectiles (check cooldowns via ammo manager)
+ // Get allowed projectiles for AI from settings
+ var allowedTypes = _cachedSettings?.AIAllowedProjectiles;
+ if (allowedTypes == null || allowedTypes.Count == 0)
+ {
+ Logging.Warning("[FortFightAIController] No allowed projectiles configured! Using all types as fallback.");
+ allowedTypes = new List((ProjectileType[])System.Enum.GetValues(typeof(ProjectileType)));
+ }
+
+ // Filter by cooldown availability
var availableTypes = new List();
- foreach (ProjectileType type in System.Enum.GetValues(typeof(ProjectileType)))
+ foreach (ProjectileType type in allowedTypes)
{
// Check if ammo is available (not on cooldown)
if (_ammoManager.IsAmmoAvailable(type, playerIndex))
@@ -269,7 +277,7 @@ namespace Minigames.FortFight.AI
float enemyHpPercent = _fortManager.PlayerFort.HpPercentage;
- // Strategic selection based on fort HP
+ // Strategic selection based on fort HP (only from allowed projectiles)
if (enemyHpPercent > 70f && availableTypes.Contains(ProjectileType.TrashBag))
{
return ProjectileType.TrashBag; // Spread damage early game
@@ -280,7 +288,7 @@ namespace Minigames.FortFight.AI
}
else if (_targetBlock != null && _targetBlock.IsWeakPoint && availableTypes.Contains(ProjectileType.CeilingFan))
{
- return ProjectileType.CeilingFan; // Drop on weak points
+ return ProjectileType.CeilingFan; // Drop on weak points (if allowed)
}
// Default to first available
diff --git a/Assets/Scripts/Minigames/FortFight/Core/FortFightSettings.cs b/Assets/Scripts/Minigames/FortFight/Core/FortFightSettings.cs
index e3bc0b6f..742a5850 100644
--- a/Assets/Scripts/Minigames/FortFight/Core/FortFightSettings.cs
+++ b/Assets/Scripts/Minigames/FortFight/Core/FortFightSettings.cs
@@ -59,6 +59,15 @@ namespace Minigames.FortFight.Core
[Tooltip("Default AI difficulty level for single-player games")]
[SerializeField] private AIDifficulty defaultAIDifficulty = AIDifficulty.Medium;
+ [Tooltip("Projectile types the AI is allowed to use (excludes CeilingFan by default as it requires precise timing)")]
+ [SerializeField] private List aiAllowedProjectiles = new List
+ {
+ ProjectileType.Toaster,
+ ProjectileType.Vacuum,
+ ProjectileType.TrashBag
+ // CeilingFan excluded by default - requires precise tap timing
+ };
+
[Header("Weak Point Settings")]
[Tooltip("Radius of explosion effect from weak points")]
[SerializeField] private float weakPointExplosionRadius = 2.5f;
@@ -149,6 +158,8 @@ namespace Minigames.FortFight.Core
public AIDifficulty DefaultAIDifficulty => defaultAIDifficulty;
+ public IReadOnlyList AIAllowedProjectiles => aiAllowedProjectiles;
+
public float WeakPointExplosionRadius => weakPointExplosionRadius;
public float WeakPointExplosionDamage => weakPointExplosionDamage;
public float WeakPointExplosionForce => weakPointExplosionForce;
diff --git a/Assets/Scripts/Minigames/FortFight/Core/SlingshotController.cs b/Assets/Scripts/Minigames/FortFight/Core/SlingshotController.cs
index 49040627..7a884eed 100644
--- a/Assets/Scripts/Minigames/FortFight/Core/SlingshotController.cs
+++ b/Assets/Scripts/Minigames/FortFight/Core/SlingshotController.cs
@@ -3,6 +3,7 @@ using AppleHills.Core.Settings;
using Common.Input;
using Core;
using Core.Settings;
+using Input;
using Minigames.FortFight.Data;
using Minigames.FortFight.Projectiles;
using UnityEngine;
@@ -74,6 +75,8 @@ namespace Minigames.FortFight.Core
private ProjectileConfig _currentAmmo;
private ProjectileBase _activeProjectile;
+ private int _ownerPlayerIndex = -1;
+ private bool _isAIControlled = false;
public ProjectileBase ActiveProjectile => _activeProjectile;
@@ -104,7 +107,21 @@ namespace Minigames.FortFight.Core
trajectoryPreview.ForceHide();
}
- base.Enable();
+ // Only register input for human players, not AI
+ if (!_isAIControlled)
+ {
+ // Call base class Enable which handles input registration and enabling
+ base.Enable();
+ if (showDebugLogs) Logging.Debug($"[SlingshotController] Enabled with input (Player controlled)");
+ }
+ else
+ {
+ // For AI, only show trajectory preview without registering for input
+ // We don't call base.Enable() to avoid input registration
+ Enabled = true; // Use protected property from base class
+ ShowPreview();
+ if (showDebugLogs) Logging.Debug($"[SlingshotController] Enabled without input (AI controlled)");
+ }
}
protected override void StartDrag(Vector2 worldPosition)
@@ -150,6 +167,18 @@ namespace Minigames.FortFight.Core
if (showDebugLogs) Logging.Debug($"[SlingshotController] Ammo set to: {ammoConfig?.displayName ?? "null"}");
}
+ ///
+ /// Set the owner of this slingshot (used for projectile ownership tracking)
+ ///
+ /// Player index (0 or 1)
+ /// Whether this slingshot is AI-controlled
+ public void SetOwner(int playerIndex, bool isAI)
+ {
+ _ownerPlayerIndex = playerIndex;
+ _isAIControlled = isAI;
+ if (showDebugLogs) Logging.Debug($"[SlingshotController] Owner set to Player {playerIndex}, AI: {isAI}");
+ }
+
///
/// Launch a projectile with calculated force and direction
///
@@ -175,6 +204,9 @@ namespace Minigames.FortFight.Core
// Initialize projectile with its type (loads damage and mass from settings)
_activeProjectile.Initialize(_currentAmmo.projectileType);
+ // Set projectile owner for input control
+ _activeProjectile.SetOwner(_ownerPlayerIndex, _isAIControlled);
+
// Launch it
_activeProjectile.Launch(direction, force);
@@ -201,7 +233,7 @@ namespace Minigames.FortFight.Core
///
/// Launch projectile with a specific velocity (for AI ballistic calculations)
- /// Calculates the required force based on projectile mass
+ /// Calculates the required force based on projectile mass and respects slingshot limits
///
public void LaunchWithVelocity(Vector2 velocity)
{
@@ -211,22 +243,57 @@ namespace Minigames.FortFight.Core
return;
}
- // Get projectile mass to calculate force
float mass = _currentAmmo.GetMass();
-
- // Force = mass × velocity (for impulse-based launch)
Vector2 direction = velocity.normalized;
- float speed = velocity.magnitude;
- float force = mass * speed;
+ float desiredSpeed = velocity.magnitude;
+
+ // Use common method - ensures same physics constraints as player
+ float force = CalculateClampedForce(desiredSpeed, mass);
if (showDebugLogs)
{
- Logging.Debug($"[Slingshot] LaunchWithVelocity - Velocity: {velocity}, Mass: {mass:F2}, Force: {force:F2}");
+ float actualSpeed = force / mass;
+ if (actualSpeed < desiredSpeed)
+ {
+ float maxForce = Config?.baseLaunchForce * Config?.maxForceMultiplier ?? 20f;
+ Logging.Debug($"[Slingshot] AI velocity clamped - Requested: {desiredSpeed:F2} m/s, Actual: {actualSpeed:F2} m/s (mass: {mass:F2}, maxForce: {maxForce:F2})");
+ }
+ else
+ {
+ Logging.Debug($"[Slingshot] LaunchWithVelocity - Velocity: {velocity}, Mass: {mass:F2}, Force: {force:F2}");
+ }
}
LaunchProjectile(direction, force);
}
+ ///
+ /// Calculate maximum achievable velocity for current projectile given slingshot constraints.
+ /// This ensures both player and AI respect the same physical limits.
+ ///
+ public float GetMaxVelocity()
+ {
+ if (_currentAmmo == null)
+ {
+ return 20f; // Default fallback
+ }
+
+ float maxForce = Config?.baseLaunchForce * Config?.maxForceMultiplier ?? 20f;
+ float mass = _currentAmmo.GetMass();
+ return maxForce / mass; // Physics: v = F / m
+ }
+
+ ///
+ /// Convert desired velocity to force, clamped to slingshot's physical limits.
+ /// Used internally to ensure AI respects same constraints as player.
+ ///
+ private float CalculateClampedForce(float desiredSpeed, float mass)
+ {
+ float desiredForce = mass * desiredSpeed;
+ float maxForce = Config?.baseLaunchForce * Config?.maxForceMultiplier ?? 20f;
+ return Mathf.Min(desiredForce, maxForce);
+ }
+
///
/// Get currently active projectile (in flight)
///
diff --git a/Assets/Scripts/Minigames/FortFight/Core/TurnManager.cs b/Assets/Scripts/Minigames/FortFight/Core/TurnManager.cs
index 5475ff8d..c8322f90 100644
--- a/Assets/Scripts/Minigames/FortFight/Core/TurnManager.cs
+++ b/Assets/Scripts/Minigames/FortFight/Core/TurnManager.cs
@@ -70,20 +70,20 @@ namespace Minigames.FortFight.Core
#region State
- private TurnState currentTurnState = TurnState.PlayerOneTurn;
- private PlayerData playerOne;
- private PlayerData playerTwo;
- private PlayerData currentPlayer;
- private int turnCount = 0;
+ private TurnState _currentTurnState = TurnState.PlayerOneTurn;
+ private PlayerData _playerOne;
+ private PlayerData _playerTwo;
+ private PlayerData _currentPlayer;
+ private int _turnCount = 0;
// Turn action management
- private ProjectileTurnAction currentTurnAction;
- private bool isTransitioning = false;
- private float transitionTimer = 0f;
+ private ProjectileTurnAction _currentTurnAction;
+ private bool _isTransitioning = false;
+ private float _transitionTimer = 0f;
- public TurnState CurrentTurnState => currentTurnState;
- public PlayerData CurrentPlayer => currentPlayer;
- public int TurnCount => turnCount;
+ public TurnState CurrentTurnState => _currentTurnState;
+ public PlayerData CurrentPlayer => _currentPlayer;
+ public int TurnCount => _turnCount;
#endregion
@@ -128,8 +128,8 @@ namespace Minigames.FortFight.Core
///
public void Initialize(PlayerData pPlayerOne, PlayerData pPlayerTwo)
{
- this.playerOne = pPlayerOne;
- this.playerTwo = pPlayerTwo;
+ this._playerOne = pPlayerOne;
+ this._playerTwo = pPlayerTwo;
Logging.Debug($"[TurnManager] Initialized with P1: {pPlayerOne.PlayerName} (AI: {pPlayerOne.IsAI}), P2: {pPlayerTwo.PlayerName} (AI: {pPlayerTwo.IsAI})");
}
@@ -139,9 +139,9 @@ namespace Minigames.FortFight.Core
///
public void StartGame()
{
- turnCount = 0;
- currentTurnState = TurnState.PlayerOneTurn;
- currentPlayer = playerOne;
+ _turnCount = 0;
+ _currentTurnState = TurnState.PlayerOneTurn;
+ _currentPlayer = _playerOne;
// Set initial input mode to UI
if (Input.InputManager.Instance != null)
@@ -149,8 +149,8 @@ namespace Minigames.FortFight.Core
Input.InputManager.Instance.SetInputMode(Input.InputMode.UI);
}
- Logging.Debug($"[TurnManager] Game started. First turn: {currentPlayer.PlayerName}");
- OnTurnStarted?.Invoke(currentPlayer, currentTurnState);
+ Logging.Debug($"[TurnManager] Game started. First turn: {_currentPlayer.PlayerName}");
+ OnTurnStarted?.Invoke(_currentPlayer, _currentTurnState);
// Start turn action for first player
StartTurnAction();
@@ -163,26 +163,26 @@ namespace Minigames.FortFight.Core
private void Update()
{
// Update current turn action
- if (currentTurnAction != null && !isTransitioning)
+ if (_currentTurnAction != null && !_isTransitioning)
{
- currentTurnAction.Update();
+ _currentTurnAction.Update();
// Check if action is complete
- if (currentTurnAction.IsComplete)
+ if (_currentTurnAction.IsComplete)
{
EndTurnAction();
}
}
// Handle transition timing
- if (isTransitioning)
+ if (_isTransitioning)
{
- transitionTimer += Time.deltaTime;
+ _transitionTimer += Time.deltaTime;
var settings = GameManager.GetSettingsObject();
float transitionDelay = settings?.TurnTransitionDelay ?? 1.5f;
- if (transitionTimer >= transitionDelay)
+ if (_transitionTimer >= transitionDelay)
{
CompleteTransition();
}
@@ -199,38 +199,40 @@ namespace Minigames.FortFight.Core
private void StartTurnAction()
{
// Get the appropriate slingshot for current player
- SlingshotController activeSlingshot = GetSlingshotForPlayer(currentPlayer);
+ SlingshotController activeSlingshot = GetSlingshotForPlayer(_currentPlayer);
if (activeSlingshot == null)
{
- Logging.Error($"[TurnManager] No slingshot found for {currentPlayer.PlayerName}!");
+ Logging.Error($"[TurnManager] No slingshot found for {_currentPlayer.PlayerName}!");
return;
}
- // Create and execute turn action with player index
- currentTurnAction = new ProjectileTurnAction(activeSlingshot, AmmunitionManager.Instance, cameraController, currentPlayer.PlayerIndex);
+ // Set slingshot owner (used for projectile ownership tracking)
+ activeSlingshot.SetOwner(_currentPlayer.PlayerIndex, _currentPlayer.IsAI);
// Set current ammo on slingshot for this player
if (AmmunitionManager.Instance != null)
{
- ProjectileConfig currentAmmo = AmmunitionManager.Instance.GetSelectedAmmoConfig(currentPlayer.PlayerIndex);
+ ProjectileConfig currentAmmo = AmmunitionManager.Instance.GetSelectedAmmoConfig(_currentPlayer.PlayerIndex);
if (currentAmmo != null)
{
activeSlingshot.SetAmmo(currentAmmo);
}
}
- // Execute the action (enables slingshot)
- currentTurnAction.Execute();
+ // Create and execute turn action for BOTH player and AI
+ // This enables slingshot (trajectory preview) and subscribes to launch events
+ _currentTurnAction = new ProjectileTurnAction(activeSlingshot, AmmunitionManager.Instance, cameraController, _currentPlayer.PlayerIndex);
+ _currentTurnAction.Execute(); // Always execute - handles trajectory & event subscription
- // Register slingshot as input consumer and switch to Game mode
- if (Input.InputManager.Instance != null)
+ // Only switch input mode for human players
+ if (!_currentPlayer.IsAI && Input.InputManager.Instance != null)
{
- Input.InputManager.Instance.RegisterOverrideConsumer(activeSlingshot);
Input.InputManager.Instance.SetInputMode(Input.InputMode.Game);
}
+ // Note: Input registration now handled by SlingshotController.Enable() based on _isAIControlled flag
- Logging.Debug($"[TurnManager] Started turn action for {currentPlayer.PlayerName}");
+ Logging.Debug($"[TurnManager] Started turn action for {_currentPlayer.PlayerName} (AI: {_currentPlayer.IsAI})");
}
///
@@ -238,10 +240,10 @@ namespace Minigames.FortFight.Core
///
private void EndTurnAction()
{
- Logging.Debug($"[TurnManager] Ending turn action for {currentPlayer.PlayerName}");
+ Logging.Debug($"[TurnManager] Ending turn action for {_currentPlayer.PlayerName}");
// Get active slingshot and unregister from input
- SlingshotController activeSlingshot = GetSlingshotForPlayer(currentPlayer);
+ SlingshotController activeSlingshot = GetSlingshotForPlayer(_currentPlayer);
if (activeSlingshot != null && Input.InputManager.Instance != null)
{
Input.InputManager.Instance.UnregisterOverrideConsumer(activeSlingshot);
@@ -254,7 +256,7 @@ namespace Minigames.FortFight.Core
}
// Clear turn action
- currentTurnAction = null;
+ _currentTurnAction = null;
// End the turn
EndTurn();
@@ -265,29 +267,29 @@ namespace Minigames.FortFight.Core
///
private void EndTurn()
{
- if (currentTurnState == TurnState.GameOver)
+ if (_currentTurnState == TurnState.GameOver)
{
Logging.Warning("[TurnManager] Cannot end turn - game is over");
return;
}
- Logging.Debug($"[TurnManager] Turn ended for {currentPlayer.PlayerName}");
- OnTurnEnded?.Invoke(currentPlayer);
+ Logging.Debug($"[TurnManager] Turn ended for {_currentPlayer.PlayerName}");
+ OnTurnEnded?.Invoke(_currentPlayer);
// Decrement ammunition cooldowns for this player
if (AmmunitionManager.Instance != null)
{
- AmmunitionManager.Instance.DecrementCooldowns(currentPlayer.PlayerIndex);
+ AmmunitionManager.Instance.DecrementCooldowns(_currentPlayer.PlayerIndex);
}
// Enter transition state (triggers wide view camera via OnTurnStarted)
- currentTurnState = TurnState.TransitioningTurn;
+ _currentTurnState = TurnState.TransitioningTurn;
OnTurnTransitioning?.Invoke();
- OnTurnStarted?.Invoke(currentPlayer, currentTurnState); // Fire for camera switch to wide view
+ OnTurnStarted?.Invoke(_currentPlayer, _currentTurnState); // Fire for camera switch to wide view
// Start transition timer
- isTransitioning = true;
- transitionTimer = 0f;
+ _isTransitioning = true;
+ _transitionTimer = 0f;
}
///
@@ -295,8 +297,8 @@ namespace Minigames.FortFight.Core
///
private void CompleteTransition()
{
- isTransitioning = false;
- transitionTimer = 0f;
+ _isTransitioning = false;
+ _transitionTimer = 0f;
AdvanceToNextPlayer();
}
@@ -306,22 +308,22 @@ namespace Minigames.FortFight.Core
///
private void AdvanceToNextPlayer()
{
- turnCount++;
+ _turnCount++;
// Switch players
- if (currentPlayer == playerOne)
+ if (_currentPlayer == _playerOne)
{
- currentPlayer = playerTwo;
- currentTurnState = playerTwo.IsAI ? TurnState.AITurn : TurnState.PlayerTwoTurn;
+ _currentPlayer = _playerTwo;
+ _currentTurnState = _playerTwo.IsAI ? TurnState.AITurn : TurnState.PlayerTwoTurn;
}
else
{
- currentPlayer = playerOne;
- currentTurnState = TurnState.PlayerOneTurn;
+ _currentPlayer = _playerOne;
+ _currentTurnState = TurnState.PlayerOneTurn;
}
- Logging.Debug($"[TurnManager] Advanced to turn {turnCount}. Current player: {currentPlayer.PlayerName} (State: {currentTurnState})");
- OnTurnStarted?.Invoke(currentPlayer, currentTurnState);
+ Logging.Debug($"[TurnManager] Advanced to turn {_turnCount}. Current player: {_currentPlayer.PlayerName} (State: {_currentTurnState})");
+ OnTurnStarted?.Invoke(_currentPlayer, _currentTurnState);
// Start turn action for next player
StartTurnAction();
@@ -332,11 +334,11 @@ namespace Minigames.FortFight.Core
///
private SlingshotController GetSlingshotForPlayer(PlayerData player)
{
- if (player == playerOne)
+ if (player == _playerOne)
{
return playerOneSlingshotController;
}
- else if (player == playerTwo)
+ else if (player == _playerTwo)
{
return playerTwoSlingshotController;
}
@@ -349,7 +351,7 @@ namespace Minigames.FortFight.Core
///
public void SetGameOver()
{
- currentTurnState = TurnState.GameOver;
+ _currentTurnState = TurnState.GameOver;
Logging.Debug("[TurnManager] Game over state set");
}
diff --git a/Assets/Scripts/Minigames/FortFight/Projectiles/CeilingFanProjectile.cs b/Assets/Scripts/Minigames/FortFight/Projectiles/CeilingFanProjectile.cs
index 97b48d7f..97b836fe 100644
--- a/Assets/Scripts/Minigames/FortFight/Projectiles/CeilingFanProjectile.cs
+++ b/Assets/Scripts/Minigames/FortFight/Projectiles/CeilingFanProjectile.cs
@@ -51,11 +51,16 @@ namespace Minigames.FortFight.Projectiles
indicator.SetActive(true);
}
- // Register with InputManager to capture tap-to-drop
- if (Input.InputManager.Instance != null)
+ // Only register with InputManager if NOT AI-controlled
+ // AI projectiles should not accept player input
+ if (!IsAIControlled && Input.InputManager.Instance != null)
{
Input.InputManager.Instance.RegisterOverrideConsumer(this);
- Logging.Debug("[CeilingFanProjectile] Tap-to-drop now available");
+ Logging.Debug("[CeilingFanProjectile] Tap-to-drop now available (Player controlled)");
+ }
+ else if (IsAIControlled)
+ {
+ Logging.Debug("[CeilingFanProjectile] AI-controlled projectile, input disabled");
}
}
}
diff --git a/Assets/Scripts/Minigames/FortFight/Projectiles/ProjectileBase.cs b/Assets/Scripts/Minigames/FortFight/Projectiles/ProjectileBase.cs
index 45a72ac6..6121937f 100644
--- a/Assets/Scripts/Minigames/FortFight/Projectiles/ProjectileBase.cs
+++ b/Assets/Scripts/Minigames/FortFight/Projectiles/ProjectileBase.cs
@@ -57,6 +57,17 @@ namespace Minigames.FortFight.Projectiles
public Vector2 LaunchDirection { get; protected set; }
public float LaunchForce { get; protected set; }
+ ///
+ /// Player index who owns this projectile (0 = Player 1, 1 = Player 2/AI)
+ /// Used to determine if player input should be accepted for ability activation
+ ///
+ public int OwnerPlayerIndex { get; private set; } = -1;
+
+ ///
+ /// Whether this projectile is controlled by AI
+ ///
+ public bool IsAIControlled { get; private set; } = false;
+
#endregion
#region Timeout
@@ -116,6 +127,17 @@ namespace Minigames.FortFight.Projectiles
}
}
+ ///
+ /// Set the owner of this projectile (used to control ability activation)
+ ///
+ /// Player index (0 or 1)
+ /// Whether this projectile is controlled by AI
+ public void SetOwner(int playerIndex, bool isAI)
+ {
+ OwnerPlayerIndex = playerIndex;
+ IsAIControlled = isAI;
+ }
+
internal override void OnManagedAwake()
{
base.OnManagedAwake();
diff --git a/Assets/Scripts/Minigames/FortFight/Projectiles/TrashBagProjectile.cs b/Assets/Scripts/Minigames/FortFight/Projectiles/TrashBagProjectile.cs
index 7e8c9e73..904d3d58 100644
--- a/Assets/Scripts/Minigames/FortFight/Projectiles/TrashBagProjectile.cs
+++ b/Assets/Scripts/Minigames/FortFight/Projectiles/TrashBagProjectile.cs
@@ -1,5 +1,7 @@
-using Core;
+using System.Collections;
+using Core;
using Core.Settings;
+using Input;
using UnityEngine;
namespace Minigames.FortFight.Projectiles
@@ -7,15 +9,76 @@ namespace Minigames.FortFight.Projectiles
///
/// Trash Bag projectile - splits into multiple smaller pieces on impact.
/// Deals AOE damage in a forward cone.
+ /// Can be manually detonated mid-flight by tapping.
///
- public class TrashBagProjectile : ProjectileBase
+ public class TrashBagProjectile : ProjectileBase, ITouchInputConsumer
{
[Header("Trash Bag Specific")]
[Tooltip("Prefab for individual trash pieces (small debris)")]
[SerializeField] private GameObject trashPiecePrefab;
+ [Tooltip("Visual indicator showing tap-to-explode is available")]
+ [SerializeField] private GameObject indicator;
+
+ private bool inputEnabled = false;
+ private bool hasExploded = false;
+
+ public override void Launch(Vector2 direction, float force)
+ {
+ base.Launch(direction, force);
+
+ // Start activation delay coroutine for tap-to-explode
+ StartCoroutine(ActivationDelayCoroutine());
+ }
+
+ private IEnumerator ActivationDelayCoroutine()
+ {
+ // Get activation delay from settings (use TrashBag-specific or default to 0.3s)
+ var settings = GameManager.GetSettingsObject();
+ float activationDelay = settings?.CeilingFanActivationDelay ?? 0.3f; // Reuse CeilingFan delay setting
+
+ // Wait for delay
+ yield return new WaitForSeconds(activationDelay);
+
+ // Enable input and show indicator (if not already exploded)
+ if (!hasExploded && !AbilityActivated)
+ {
+ inputEnabled = true;
+
+ if (indicator != null)
+ {
+ indicator.SetActive(true);
+ }
+
+ // Only register with InputManager if NOT AI-controlled
+ if (!IsAIControlled && InputManager.Instance != null)
+ {
+ InputManager.Instance.RegisterOverrideConsumer(this);
+ Logging.Debug("[TrashBagProjectile] Tap-to-explode now available (Player controlled)");
+ }
+ else if (IsAIControlled)
+ {
+ Logging.Debug("[TrashBagProjectile] AI-controlled projectile, input disabled");
+ }
+ }
+ }
+
+ public override void ActivateAbility()
+ {
+ base.ActivateAbility();
+
+ if (AbilityActivated && !hasExploded)
+ {
+ Logging.Debug("[TrashBagProjectile] Ability activated - manual detonation");
+ ExplodeAtCurrentPosition();
+ }
+ }
+
protected override void OnHit(Collision2D collision)
{
+ // Prevent double-explosion if already manually detonated
+ if (hasExploded) return;
+
// Deal initial damage from trash bag itself
var block = collision.gameObject.GetComponent();
if (block != null)
@@ -28,19 +91,57 @@ namespace Minigames.FortFight.Projectiles
var settings = GameManager.GetSettingsObject();
int pieceCount = settings?.TrashBagPieceCount ?? 8;
- Logging.Debug($"[TrashBagProjectile] Splitting into {pieceCount} pieces");
+ Logging.Debug($"[TrashBagProjectile] Impact explosion - splitting into {pieceCount} pieces");
// Get contact normal and impact point
Vector2 hitNormal = collision.contacts[0].normal;
Vector2 impactPoint = collision.contacts[0].point;
- // Spawn trash pieces (NOT parented, so they persist as debris)
+ // Mark as exploded and spawn trash pieces
+ hasExploded = true;
SpawnTrashPieces(impactPoint, hitNormal);
// Destroy trash bag after spawning pieces
DestroyProjectile();
}
+ ///
+ /// Explode at current position (manual detonation)
+ ///
+ private void ExplodeAtCurrentPosition()
+ {
+ if (hasExploded) return;
+
+ hasExploded = true;
+
+ // Unregister from input immediately
+ UnregisterFromInput();
+
+ // Hide indicator
+ if (indicator != null)
+ {
+ indicator.SetActive(false);
+ }
+
+ // Get current position and use velocity as "hit normal" direction
+ Vector2 explosionPoint = transform.position;
+ Vector2 explosionDirection = rb2D.linearVelocity.normalized;
+ if (explosionDirection == Vector2.zero)
+ {
+ explosionDirection = LaunchDirection;
+ }
+
+ var settings = GameManager.GetSettingsObject();
+ int pieceCount = settings?.TrashBagPieceCount ?? 8;
+ Logging.Debug($"[TrashBagProjectile] Manual detonation - splitting into {pieceCount} pieces");
+
+ // Spawn trash pieces
+ SpawnTrashPieces(explosionPoint, explosionDirection);
+
+ // Destroy the trash bag
+ DestroyProjectile();
+ }
+
///
/// Spawn multiple trash pieces in a cone away from the hit surface.
/// Uses hit normal + projectile momentum for realistic splash effect.
@@ -103,6 +204,64 @@ namespace Minigames.FortFight.Projectiles
Logging.Debug($"[TrashBagProjectile] Spawned trash piece {i} in direction {pieceDirection}");
}
}
+
+ #region ITouchInputConsumer Implementation
+
+ public void OnTap(Vector2 worldPosition)
+ {
+ // Only respond if input is enabled
+ if (inputEnabled && !hasExploded && !AbilityActivated)
+ {
+ Logging.Debug("[TrashBagProjectile] Tap detected - activating manual detonation");
+
+ // Hide indicator
+ if (indicator != null)
+ {
+ indicator.SetActive(false);
+ }
+
+ ActivateAbility();
+
+ // Unregister immediately after tap
+ UnregisterFromInput();
+ }
+ }
+
+ public void OnHoldStart(Vector2 worldPosition)
+ {
+ // Not used for trash bag
+ }
+
+ public void OnHoldMove(Vector2 worldPosition)
+ {
+ // Not used for trash bag
+ }
+
+ public void OnHoldEnd(Vector2 worldPosition)
+ {
+ // Not used for trash bag
+ }
+
+ ///
+ /// Unregister from input manager
+ ///
+ private void UnregisterFromInput()
+ {
+ if (InputManager.Instance != null)
+ {
+ InputManager.Instance.UnregisterOverrideConsumer(this);
+ inputEnabled = false;
+ }
+ }
+
+ #endregion
+
+ protected override void DestroyProjectile()
+ {
+ // Ensure we unregister from input before destruction
+ UnregisterFromInput();
+ base.DestroyProjectile();
+ }
}
}