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(); + } } }