Update double input issues, pigman is no longer unbound, limit his weapon access too

This commit is contained in:
Michal Pikulski
2025-12-16 18:58:50 +01:00
parent 0055efaee2
commit a2eb27f8f3
9 changed files with 362 additions and 78 deletions

View File

@@ -124,6 +124,15 @@ namespace Common.Input
public bool IsDragging => _isDragging; public bool IsDragging => _isDragging;
public bool IsEnabled => _isEnabled; public bool IsEnabled => _isEnabled;
/// <summary>
/// Protected property to allow derived classes to set enabled state
/// </summary>
protected bool Enabled
{
get => _isEnabled;
set => _isEnabled = value;
}
#endregion #endregion
#region Lifecycle #region Lifecycle

View File

@@ -254,6 +254,7 @@ namespace Core.Settings
// AI Difficulty Settings // AI Difficulty Settings
Minigames.FortFight.Data.AIDifficulty DefaultAIDifficulty { get; } // Default difficulty level for AI Minigames.FortFight.Data.AIDifficulty DefaultAIDifficulty { get; } // Default difficulty level for AI
Minigames.FortFight.Data.AIDifficultyData GetAIDifficultyData(Minigames.FortFight.Data.AIDifficulty difficulty); Minigames.FortFight.Data.AIDifficultyData GetAIDifficultyData(Minigames.FortFight.Data.AIDifficulty difficulty);
IReadOnlyList<Minigames.FortFight.Data.ProjectileType> AIAllowedProjectiles { get; } // Projectiles AI can use
// Weak point settings // Weak point settings
float WeakPointExplosionRadius { get; } float WeakPointExplosionRadius { get; }

View File

@@ -250,9 +250,17 @@ namespace Minigames.FortFight.AI
// Get AI player index // Get AI player index
int playerIndex = _turnManager.CurrentPlayer.PlayerIndex; 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>((ProjectileType[])System.Enum.GetValues(typeof(ProjectileType)));
}
// Filter by cooldown availability
var availableTypes = new List<ProjectileType>(); var availableTypes = new List<ProjectileType>();
foreach (ProjectileType type in System.Enum.GetValues(typeof(ProjectileType))) foreach (ProjectileType type in allowedTypes)
{ {
// Check if ammo is available (not on cooldown) // Check if ammo is available (not on cooldown)
if (_ammoManager.IsAmmoAvailable(type, playerIndex)) if (_ammoManager.IsAmmoAvailable(type, playerIndex))
@@ -269,7 +277,7 @@ namespace Minigames.FortFight.AI
float enemyHpPercent = _fortManager.PlayerFort.HpPercentage; 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)) if (enemyHpPercent > 70f && availableTypes.Contains(ProjectileType.TrashBag))
{ {
return ProjectileType.TrashBag; // Spread damage early game 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)) 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 // Default to first available

View File

@@ -59,6 +59,15 @@ namespace Minigames.FortFight.Core
[Tooltip("Default AI difficulty level for single-player games")] [Tooltip("Default AI difficulty level for single-player games")]
[SerializeField] private AIDifficulty defaultAIDifficulty = AIDifficulty.Medium; [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<ProjectileType> aiAllowedProjectiles = new List<ProjectileType>
{
ProjectileType.Toaster,
ProjectileType.Vacuum,
ProjectileType.TrashBag
// CeilingFan excluded by default - requires precise tap timing
};
[Header("Weak Point Settings")] [Header("Weak Point Settings")]
[Tooltip("Radius of explosion effect from weak points")] [Tooltip("Radius of explosion effect from weak points")]
[SerializeField] private float weakPointExplosionRadius = 2.5f; [SerializeField] private float weakPointExplosionRadius = 2.5f;
@@ -149,6 +158,8 @@ namespace Minigames.FortFight.Core
public AIDifficulty DefaultAIDifficulty => defaultAIDifficulty; public AIDifficulty DefaultAIDifficulty => defaultAIDifficulty;
public IReadOnlyList<ProjectileType> AIAllowedProjectiles => aiAllowedProjectiles;
public float WeakPointExplosionRadius => weakPointExplosionRadius; public float WeakPointExplosionRadius => weakPointExplosionRadius;
public float WeakPointExplosionDamage => weakPointExplosionDamage; public float WeakPointExplosionDamage => weakPointExplosionDamage;
public float WeakPointExplosionForce => weakPointExplosionForce; public float WeakPointExplosionForce => weakPointExplosionForce;

View File

@@ -3,6 +3,7 @@ using AppleHills.Core.Settings;
using Common.Input; using Common.Input;
using Core; using Core;
using Core.Settings; using Core.Settings;
using Input;
using Minigames.FortFight.Data; using Minigames.FortFight.Data;
using Minigames.FortFight.Projectiles; using Minigames.FortFight.Projectiles;
using UnityEngine; using UnityEngine;
@@ -74,6 +75,8 @@ namespace Minigames.FortFight.Core
private ProjectileConfig _currentAmmo; private ProjectileConfig _currentAmmo;
private ProjectileBase _activeProjectile; private ProjectileBase _activeProjectile;
private int _ownerPlayerIndex = -1;
private bool _isAIControlled = false;
public ProjectileBase ActiveProjectile => _activeProjectile; public ProjectileBase ActiveProjectile => _activeProjectile;
@@ -104,7 +107,21 @@ namespace Minigames.FortFight.Core
trajectoryPreview.ForceHide(); 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) 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"}"); if (showDebugLogs) Logging.Debug($"[SlingshotController] Ammo set to: {ammoConfig?.displayName ?? "null"}");
} }
/// <summary>
/// Set the owner of this slingshot (used for projectile ownership tracking)
/// </summary>
/// <param name="playerIndex">Player index (0 or 1)</param>
/// <param name="isAI">Whether this slingshot is AI-controlled</param>
public void SetOwner(int playerIndex, bool isAI)
{
_ownerPlayerIndex = playerIndex;
_isAIControlled = isAI;
if (showDebugLogs) Logging.Debug($"[SlingshotController] Owner set to Player {playerIndex}, AI: {isAI}");
}
/// <summary> /// <summary>
/// Launch a projectile with calculated force and direction /// Launch a projectile with calculated force and direction
/// </summary> /// </summary>
@@ -175,6 +204,9 @@ namespace Minigames.FortFight.Core
// Initialize projectile with its type (loads damage and mass from settings) // Initialize projectile with its type (loads damage and mass from settings)
_activeProjectile.Initialize(_currentAmmo.projectileType); _activeProjectile.Initialize(_currentAmmo.projectileType);
// Set projectile owner for input control
_activeProjectile.SetOwner(_ownerPlayerIndex, _isAIControlled);
// Launch it // Launch it
_activeProjectile.Launch(direction, force); _activeProjectile.Launch(direction, force);
@@ -201,7 +233,7 @@ namespace Minigames.FortFight.Core
/// <summary> /// <summary>
/// Launch projectile with a specific velocity (for AI ballistic calculations) /// 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
/// </summary> /// </summary>
public void LaunchWithVelocity(Vector2 velocity) public void LaunchWithVelocity(Vector2 velocity)
{ {
@@ -211,22 +243,57 @@ namespace Minigames.FortFight.Core
return; return;
} }
// Get projectile mass to calculate force
float mass = _currentAmmo.GetMass(); float mass = _currentAmmo.GetMass();
// Force = mass × velocity (for impulse-based launch)
Vector2 direction = velocity.normalized; Vector2 direction = velocity.normalized;
float speed = velocity.magnitude; float desiredSpeed = velocity.magnitude;
float force = mass * speed;
// Use common method - ensures same physics constraints as player
float force = CalculateClampedForce(desiredSpeed, mass);
if (showDebugLogs) 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); LaunchProjectile(direction, force);
} }
/// <summary>
/// Calculate maximum achievable velocity for current projectile given slingshot constraints.
/// This ensures both player and AI respect the same physical limits.
/// </summary>
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
}
/// <summary>
/// Convert desired velocity to force, clamped to slingshot's physical limits.
/// Used internally to ensure AI respects same constraints as player.
/// </summary>
private float CalculateClampedForce(float desiredSpeed, float mass)
{
float desiredForce = mass * desiredSpeed;
float maxForce = Config?.baseLaunchForce * Config?.maxForceMultiplier ?? 20f;
return Mathf.Min(desiredForce, maxForce);
}
/// <summary> /// <summary>
/// Get currently active projectile (in flight) /// Get currently active projectile (in flight)
/// </summary> /// </summary>

View File

@@ -70,20 +70,20 @@ namespace Minigames.FortFight.Core
#region State #region State
private TurnState currentTurnState = TurnState.PlayerOneTurn; private TurnState _currentTurnState = TurnState.PlayerOneTurn;
private PlayerData playerOne; private PlayerData _playerOne;
private PlayerData playerTwo; private PlayerData _playerTwo;
private PlayerData currentPlayer; private PlayerData _currentPlayer;
private int turnCount = 0; private int _turnCount = 0;
// Turn action management // Turn action management
private ProjectileTurnAction currentTurnAction; private ProjectileTurnAction _currentTurnAction;
private bool isTransitioning = false; private bool _isTransitioning = false;
private float transitionTimer = 0f; private float _transitionTimer = 0f;
public TurnState CurrentTurnState => currentTurnState; public TurnState CurrentTurnState => _currentTurnState;
public PlayerData CurrentPlayer => currentPlayer; public PlayerData CurrentPlayer => _currentPlayer;
public int TurnCount => turnCount; public int TurnCount => _turnCount;
#endregion #endregion
@@ -128,8 +128,8 @@ namespace Minigames.FortFight.Core
/// </summary> /// </summary>
public void Initialize(PlayerData pPlayerOne, PlayerData pPlayerTwo) public void Initialize(PlayerData pPlayerOne, PlayerData pPlayerTwo)
{ {
this.playerOne = pPlayerOne; this._playerOne = pPlayerOne;
this.playerTwo = pPlayerTwo; this._playerTwo = pPlayerTwo;
Logging.Debug($"[TurnManager] Initialized with P1: {pPlayerOne.PlayerName} (AI: {pPlayerOne.IsAI}), P2: {pPlayerTwo.PlayerName} (AI: {pPlayerTwo.IsAI})"); 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
/// </summary> /// </summary>
public void StartGame() public void StartGame()
{ {
turnCount = 0; _turnCount = 0;
currentTurnState = TurnState.PlayerOneTurn; _currentTurnState = TurnState.PlayerOneTurn;
currentPlayer = playerOne; _currentPlayer = _playerOne;
// Set initial input mode to UI // Set initial input mode to UI
if (Input.InputManager.Instance != null) if (Input.InputManager.Instance != null)
@@ -149,8 +149,8 @@ namespace Minigames.FortFight.Core
Input.InputManager.Instance.SetInputMode(Input.InputMode.UI); Input.InputManager.Instance.SetInputMode(Input.InputMode.UI);
} }
Logging.Debug($"[TurnManager] Game started. First turn: {currentPlayer.PlayerName}"); Logging.Debug($"[TurnManager] Game started. First turn: {_currentPlayer.PlayerName}");
OnTurnStarted?.Invoke(currentPlayer, currentTurnState); OnTurnStarted?.Invoke(_currentPlayer, _currentTurnState);
// Start turn action for first player // Start turn action for first player
StartTurnAction(); StartTurnAction();
@@ -163,26 +163,26 @@ namespace Minigames.FortFight.Core
private void Update() private void Update()
{ {
// Update current turn action // Update current turn action
if (currentTurnAction != null && !isTransitioning) if (_currentTurnAction != null && !_isTransitioning)
{ {
currentTurnAction.Update(); _currentTurnAction.Update();
// Check if action is complete // Check if action is complete
if (currentTurnAction.IsComplete) if (_currentTurnAction.IsComplete)
{ {
EndTurnAction(); EndTurnAction();
} }
} }
// Handle transition timing // Handle transition timing
if (isTransitioning) if (_isTransitioning)
{ {
transitionTimer += Time.deltaTime; _transitionTimer += Time.deltaTime;
var settings = GameManager.GetSettingsObject<IFortFightSettings>(); var settings = GameManager.GetSettingsObject<IFortFightSettings>();
float transitionDelay = settings?.TurnTransitionDelay ?? 1.5f; float transitionDelay = settings?.TurnTransitionDelay ?? 1.5f;
if (transitionTimer >= transitionDelay) if (_transitionTimer >= transitionDelay)
{ {
CompleteTransition(); CompleteTransition();
} }
@@ -199,38 +199,40 @@ namespace Minigames.FortFight.Core
private void StartTurnAction() private void StartTurnAction()
{ {
// Get the appropriate slingshot for current player // Get the appropriate slingshot for current player
SlingshotController activeSlingshot = GetSlingshotForPlayer(currentPlayer); SlingshotController activeSlingshot = GetSlingshotForPlayer(_currentPlayer);
if (activeSlingshot == null) if (activeSlingshot == null)
{ {
Logging.Error($"[TurnManager] No slingshot found for {currentPlayer.PlayerName}!"); Logging.Error($"[TurnManager] No slingshot found for {_currentPlayer.PlayerName}!");
return; return;
} }
// Create and execute turn action with player index // Set slingshot owner (used for projectile ownership tracking)
currentTurnAction = new ProjectileTurnAction(activeSlingshot, AmmunitionManager.Instance, cameraController, currentPlayer.PlayerIndex); activeSlingshot.SetOwner(_currentPlayer.PlayerIndex, _currentPlayer.IsAI);
// Set current ammo on slingshot for this player // Set current ammo on slingshot for this player
if (AmmunitionManager.Instance != null) if (AmmunitionManager.Instance != null)
{ {
ProjectileConfig currentAmmo = AmmunitionManager.Instance.GetSelectedAmmoConfig(currentPlayer.PlayerIndex); ProjectileConfig currentAmmo = AmmunitionManager.Instance.GetSelectedAmmoConfig(_currentPlayer.PlayerIndex);
if (currentAmmo != null) if (currentAmmo != null)
{ {
activeSlingshot.SetAmmo(currentAmmo); activeSlingshot.SetAmmo(currentAmmo);
} }
} }
// Execute the action (enables slingshot) // Create and execute turn action for BOTH player and AI
currentTurnAction.Execute(); // 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 // Only switch input mode for human players
if (Input.InputManager.Instance != null) if (!_currentPlayer.IsAI && Input.InputManager.Instance != null)
{ {
Input.InputManager.Instance.RegisterOverrideConsumer(activeSlingshot);
Input.InputManager.Instance.SetInputMode(Input.InputMode.Game); 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})");
} }
/// <summary> /// <summary>
@@ -238,10 +240,10 @@ namespace Minigames.FortFight.Core
/// </summary> /// </summary>
private void EndTurnAction() 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 // Get active slingshot and unregister from input
SlingshotController activeSlingshot = GetSlingshotForPlayer(currentPlayer); SlingshotController activeSlingshot = GetSlingshotForPlayer(_currentPlayer);
if (activeSlingshot != null && Input.InputManager.Instance != null) if (activeSlingshot != null && Input.InputManager.Instance != null)
{ {
Input.InputManager.Instance.UnregisterOverrideConsumer(activeSlingshot); Input.InputManager.Instance.UnregisterOverrideConsumer(activeSlingshot);
@@ -254,7 +256,7 @@ namespace Minigames.FortFight.Core
} }
// Clear turn action // Clear turn action
currentTurnAction = null; _currentTurnAction = null;
// End the turn // End the turn
EndTurn(); EndTurn();
@@ -265,29 +267,29 @@ namespace Minigames.FortFight.Core
/// </summary> /// </summary>
private void EndTurn() private void EndTurn()
{ {
if (currentTurnState == TurnState.GameOver) if (_currentTurnState == TurnState.GameOver)
{ {
Logging.Warning("[TurnManager] Cannot end turn - game is over"); Logging.Warning("[TurnManager] Cannot end turn - game is over");
return; return;
} }
Logging.Debug($"[TurnManager] Turn ended for {currentPlayer.PlayerName}"); Logging.Debug($"[TurnManager] Turn ended for {_currentPlayer.PlayerName}");
OnTurnEnded?.Invoke(currentPlayer); OnTurnEnded?.Invoke(_currentPlayer);
// Decrement ammunition cooldowns for this player // Decrement ammunition cooldowns for this player
if (AmmunitionManager.Instance != null) if (AmmunitionManager.Instance != null)
{ {
AmmunitionManager.Instance.DecrementCooldowns(currentPlayer.PlayerIndex); AmmunitionManager.Instance.DecrementCooldowns(_currentPlayer.PlayerIndex);
} }
// Enter transition state (triggers wide view camera via OnTurnStarted) // Enter transition state (triggers wide view camera via OnTurnStarted)
currentTurnState = TurnState.TransitioningTurn; _currentTurnState = TurnState.TransitioningTurn;
OnTurnTransitioning?.Invoke(); 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 // Start transition timer
isTransitioning = true; _isTransitioning = true;
transitionTimer = 0f; _transitionTimer = 0f;
} }
/// <summary> /// <summary>
@@ -295,8 +297,8 @@ namespace Minigames.FortFight.Core
/// </summary> /// </summary>
private void CompleteTransition() private void CompleteTransition()
{ {
isTransitioning = false; _isTransitioning = false;
transitionTimer = 0f; _transitionTimer = 0f;
AdvanceToNextPlayer(); AdvanceToNextPlayer();
} }
@@ -306,22 +308,22 @@ namespace Minigames.FortFight.Core
/// </summary> /// </summary>
private void AdvanceToNextPlayer() private void AdvanceToNextPlayer()
{ {
turnCount++; _turnCount++;
// Switch players // Switch players
if (currentPlayer == playerOne) if (_currentPlayer == _playerOne)
{ {
currentPlayer = playerTwo; _currentPlayer = _playerTwo;
currentTurnState = playerTwo.IsAI ? TurnState.AITurn : TurnState.PlayerTwoTurn; _currentTurnState = _playerTwo.IsAI ? TurnState.AITurn : TurnState.PlayerTwoTurn;
} }
else else
{ {
currentPlayer = playerOne; _currentPlayer = _playerOne;
currentTurnState = TurnState.PlayerOneTurn; _currentTurnState = TurnState.PlayerOneTurn;
} }
Logging.Debug($"[TurnManager] Advanced to turn {turnCount}. Current player: {currentPlayer.PlayerName} (State: {currentTurnState})"); Logging.Debug($"[TurnManager] Advanced to turn {_turnCount}. Current player: {_currentPlayer.PlayerName} (State: {_currentTurnState})");
OnTurnStarted?.Invoke(currentPlayer, currentTurnState); OnTurnStarted?.Invoke(_currentPlayer, _currentTurnState);
// Start turn action for next player // Start turn action for next player
StartTurnAction(); StartTurnAction();
@@ -332,11 +334,11 @@ namespace Minigames.FortFight.Core
/// </summary> /// </summary>
private SlingshotController GetSlingshotForPlayer(PlayerData player) private SlingshotController GetSlingshotForPlayer(PlayerData player)
{ {
if (player == playerOne) if (player == _playerOne)
{ {
return playerOneSlingshotController; return playerOneSlingshotController;
} }
else if (player == playerTwo) else if (player == _playerTwo)
{ {
return playerTwoSlingshotController; return playerTwoSlingshotController;
} }
@@ -349,7 +351,7 @@ namespace Minigames.FortFight.Core
/// </summary> /// </summary>
public void SetGameOver() public void SetGameOver()
{ {
currentTurnState = TurnState.GameOver; _currentTurnState = TurnState.GameOver;
Logging.Debug("[TurnManager] Game over state set"); Logging.Debug("[TurnManager] Game over state set");
} }

View File

@@ -51,11 +51,16 @@ namespace Minigames.FortFight.Projectiles
indicator.SetActive(true); indicator.SetActive(true);
} }
// Register with InputManager to capture tap-to-drop // Only register with InputManager if NOT AI-controlled
if (Input.InputManager.Instance != null) // AI projectiles should not accept player input
if (!IsAIControlled && Input.InputManager.Instance != null)
{ {
Input.InputManager.Instance.RegisterOverrideConsumer(this); 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");
} }
} }
} }

View File

@@ -57,6 +57,17 @@ namespace Minigames.FortFight.Projectiles
public Vector2 LaunchDirection { get; protected set; } public Vector2 LaunchDirection { get; protected set; }
public float LaunchForce { get; protected set; } public float LaunchForce { get; protected set; }
/// <summary>
/// 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
/// </summary>
public int OwnerPlayerIndex { get; private set; } = -1;
/// <summary>
/// Whether this projectile is controlled by AI
/// </summary>
public bool IsAIControlled { get; private set; } = false;
#endregion #endregion
#region Timeout #region Timeout
@@ -116,6 +127,17 @@ namespace Minigames.FortFight.Projectiles
} }
} }
/// <summary>
/// Set the owner of this projectile (used to control ability activation)
/// </summary>
/// <param name="playerIndex">Player index (0 or 1)</param>
/// <param name="isAI">Whether this projectile is controlled by AI</param>
public void SetOwner(int playerIndex, bool isAI)
{
OwnerPlayerIndex = playerIndex;
IsAIControlled = isAI;
}
internal override void OnManagedAwake() internal override void OnManagedAwake()
{ {
base.OnManagedAwake(); base.OnManagedAwake();

View File

@@ -1,5 +1,7 @@
using Core; using System.Collections;
using Core;
using Core.Settings; using Core.Settings;
using Input;
using UnityEngine; using UnityEngine;
namespace Minigames.FortFight.Projectiles namespace Minigames.FortFight.Projectiles
@@ -7,15 +9,76 @@ namespace Minigames.FortFight.Projectiles
/// <summary> /// <summary>
/// Trash Bag projectile - splits into multiple smaller pieces on impact. /// Trash Bag projectile - splits into multiple smaller pieces on impact.
/// Deals AOE damage in a forward cone. /// Deals AOE damage in a forward cone.
/// Can be manually detonated mid-flight by tapping.
/// </summary> /// </summary>
public class TrashBagProjectile : ProjectileBase public class TrashBagProjectile : ProjectileBase, ITouchInputConsumer
{ {
[Header("Trash Bag Specific")] [Header("Trash Bag Specific")]
[Tooltip("Prefab for individual trash pieces (small debris)")] [Tooltip("Prefab for individual trash pieces (small debris)")]
[SerializeField] private GameObject trashPiecePrefab; [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<IFortFightSettings>();
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) protected override void OnHit(Collision2D collision)
{ {
// Prevent double-explosion if already manually detonated
if (hasExploded) return;
// Deal initial damage from trash bag itself // Deal initial damage from trash bag itself
var block = collision.gameObject.GetComponent<Fort.FortBlock>(); var block = collision.gameObject.GetComponent<Fort.FortBlock>();
if (block != null) if (block != null)
@@ -28,19 +91,57 @@ namespace Minigames.FortFight.Projectiles
var settings = GameManager.GetSettingsObject<IFortFightSettings>(); var settings = GameManager.GetSettingsObject<IFortFightSettings>();
int pieceCount = settings?.TrashBagPieceCount ?? 8; 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 // Get contact normal and impact point
Vector2 hitNormal = collision.contacts[0].normal; Vector2 hitNormal = collision.contacts[0].normal;
Vector2 impactPoint = collision.contacts[0].point; 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); SpawnTrashPieces(impactPoint, hitNormal);
// Destroy trash bag after spawning pieces // Destroy trash bag after spawning pieces
DestroyProjectile(); DestroyProjectile();
} }
/// <summary>
/// Explode at current position (manual detonation)
/// </summary>
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<IFortFightSettings>();
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();
}
/// <summary> /// <summary>
/// Spawn multiple trash pieces in a cone away from the hit surface. /// Spawn multiple trash pieces in a cone away from the hit surface.
/// Uses hit normal + projectile momentum for realistic splash effect. /// 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}"); 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
}
/// <summary>
/// Unregister from input manager
/// </summary>
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();
}
} }
} }