Finish work

This commit is contained in:
Michal Pikulski
2025-12-04 02:16:38 +01:00
parent 88049ac97c
commit 9b71b00441
17 changed files with 1684 additions and 127 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -27,8 +27,8 @@ namespace Minigames.FortFight.AI
/// </summary> /// </summary>
public void Initialize() public void Initialize()
{ {
// Get reference to turn manager // Get reference to turn manager via singleton
turnManager = FindFirstObjectByType<TurnManager>(); turnManager = TurnManager.Instance;
if (turnManager == null) if (turnManager == null)
{ {

View File

@@ -11,9 +11,27 @@ namespace Minigames.FortFight.Core
/// <summary> /// <summary>
/// Manages ammunition selection and cooldowns for Fort Fight. /// Manages ammunition selection and cooldowns for Fort Fight.
/// Tracks which ammo types are available and handles cooldown timers. /// Tracks which ammo types are available and handles cooldown timers.
/// Singleton pattern for easy access throughout the game.
/// </summary> /// </summary>
public class AmmunitionManager : ManagedBehaviour public class AmmunitionManager : ManagedBehaviour
{ {
#region Singleton
private static AmmunitionManager _instance;
public static AmmunitionManager Instance
{
get
{
if (_instance == null)
{
_instance = FindFirstObjectByType<AmmunitionManager>();
}
return _instance;
}
}
#endregion
#region Constants #region Constants
private const int MaxPlayers = 2; // Support 2 players (indices 0 and 1) private const int MaxPlayers = 2; // Support 2 players (indices 0 and 1)
@@ -85,6 +103,20 @@ namespace Minigames.FortFight.Core
#region Lifecycle #region Lifecycle
internal override void OnManagedAwake()
{
base.OnManagedAwake();
// Register singleton
if (_instance != null && _instance != this)
{
Logging.Warning("[AmmunitionManager] Multiple instances detected! Destroying duplicate.");
Destroy(gameObject);
return;
}
_instance = this;
}
internal override void OnManagedStart() internal override void OnManagedStart()
{ {
base.OnManagedStart(); base.OnManagedStart();

View File

@@ -10,9 +10,27 @@ namespace Minigames.FortFight.Core
/// Manages camera states and transitions for the Fort Fight minigame. /// Manages camera states and transitions for the Fort Fight minigame.
/// Subscribes to turn events and switches camera views accordingly. /// Subscribes to turn events and switches camera views accordingly.
/// Uses Cinemachine for smooth camera blending. /// Uses Cinemachine for smooth camera blending.
/// Singleton pattern for easy access.
/// </summary> /// </summary>
public class CameraController : ManagedBehaviour public class CameraController : ManagedBehaviour
{ {
#region Singleton
private static CameraController _instance;
public static CameraController Instance
{
get
{
if (_instance == null)
{
_instance = FindFirstObjectByType<CameraController>();
}
return _instance;
}
}
#endregion
#region Inspector References #region Inspector References
[Header("Cinemachine Cameras")] [Header("Cinemachine Cameras")]
@@ -28,9 +46,7 @@ namespace Minigames.FortFight.Core
[Tooltip("Camera that follows projectiles in flight (should have CinemachineFollow component)")] [Tooltip("Camera that follows projectiles in flight (should have CinemachineFollow component)")]
[SerializeField] private CinemachineCamera projectileCamera; [SerializeField] private CinemachineCamera projectileCamera;
[Header("References")] // Note: TurnManager accessed via singleton
[Tooltip("Turn manager to subscribe to turn events")]
[SerializeField] private TurnManager turnManager;
#endregion #endregion
@@ -49,6 +65,15 @@ namespace Minigames.FortFight.Core
{ {
base.OnManagedAwake(); base.OnManagedAwake();
// Register singleton
if (_instance != null && _instance != this)
{
Logging.Warning("[CameraController] Multiple instances detected! Destroying duplicate.");
Destroy(gameObject);
return;
}
_instance = this;
// Validate references // Validate references
if (wideViewCamera == null) if (wideViewCamera == null)
{ {
@@ -70,21 +95,17 @@ namespace Minigames.FortFight.Core
Logging.Warning("[CameraController] Projectile camera not assigned - projectiles won't be followed!"); Logging.Warning("[CameraController] Projectile camera not assigned - projectiles won't be followed!");
} }
if (turnManager == null)
{
Logging.Error("[CameraController] Turn manager not assigned!");
}
} }
internal override void OnManagedStart() internal override void OnManagedStart()
{ {
base.OnManagedStart(); base.OnManagedStart();
// Subscribe to turn events // Subscribe to turn events via singleton
if (turnManager != null) if (TurnManager.Instance != null)
{ {
turnManager.OnTurnStarted += HandleTurnStarted; TurnManager.Instance.OnTurnStarted += HandleTurnStarted;
turnManager.OnTurnEnded += HandleTurnEnded; TurnManager.Instance.OnTurnEnded += HandleTurnEnded;
Logging.Debug("[CameraController] Subscribed to turn events"); Logging.Debug("[CameraController] Subscribed to turn events");
} }
@@ -95,13 +116,18 @@ namespace Minigames.FortFight.Core
internal override void OnManagedDestroy() internal override void OnManagedDestroy()
{ {
if (_instance == this)
{
_instance = null;
}
base.OnManagedDestroy(); base.OnManagedDestroy();
// Unsubscribe from events // Unsubscribe from events
if (turnManager != null) if (TurnManager.Instance != null)
{ {
turnManager.OnTurnStarted -= HandleTurnStarted; TurnManager.Instance.OnTurnStarted -= HandleTurnStarted;
turnManager.OnTurnEnded -= HandleTurnEnded; TurnManager.Instance.OnTurnEnded -= HandleTurnEnded;
} }
} }
@@ -259,11 +285,6 @@ namespace Minigames.FortFight.Core
#if UNITY_EDITOR #if UNITY_EDITOR
private void OnValidate() private void OnValidate()
{ {
// Auto-find TurnManager if not assigned
if (turnManager == null)
{
turnManager = FindFirstObjectByType<TurnManager>();
}
} }
#endif #endif

View File

@@ -25,13 +25,14 @@ namespace Minigames.FortFight.Core
#region Inspector References #region Inspector References
[Header("Core Systems")] [Header("Core Systems")]
[SerializeField] private TurnManager turnManager;
[SerializeField] private FortFightAIController aiController; [SerializeField] private FortFightAIController aiController;
[SerializeField] private FortManager fortManager;
[Header("UI References")] [Header("UI References")]
[SerializeField] private ModeSelectionPage modeSelectionPage; [SerializeField] private ModeSelectionPage modeSelectionPage;
[SerializeField] private GameplayPage gameplayPage; [SerializeField] private GameplayPage gameplayPage;
[SerializeField] private UI.GameOverUI gameOverUI;
// Note: TurnManager and FortManager accessed via singletons
#endregion #endregion
@@ -60,10 +61,23 @@ namespace Minigames.FortFight.Core
private PlayerData playerOne; private PlayerData playerOne;
private PlayerData playerTwo; private PlayerData playerTwo;
private bool isGameActive = false; private bool isGameActive = false;
private float gameStartTime = 0f;
public FortFightGameMode CurrentGameMode => currentGameMode; public FortFightGameMode CurrentGameMode => currentGameMode;
public bool IsGameActive => isGameActive; public bool IsGameActive => isGameActive;
/// <summary>
/// Get elapsed game time in seconds since game started
/// </summary>
public float ElapsedGameTime
{
get
{
if (!isGameActive) return 0f;
return Time.time - gameStartTime;
}
}
#endregion #endregion
#region Lifecycle #region Lifecycle
@@ -97,9 +111,19 @@ namespace Minigames.FortFight.Core
{ {
base.OnManagedDestroy(); base.OnManagedDestroy();
if (_instance == this) // Unsubscribe from fort defeated events
var fortManager = FortManager.Instance;
if (fortManager != null)
{ {
_instance = null; if (fortManager.PlayerFort != null)
{
fortManager.PlayerFort.OnFortDefeated -= OnFortDefeated;
}
if (fortManager.EnemyFort != null)
{
fortManager.EnemyFort.OnFortDefeated -= OnFortDefeated;
}
} }
// Clear events // Clear events
@@ -114,11 +138,6 @@ namespace Minigames.FortFight.Core
private void ValidateReferences() private void ValidateReferences()
{ {
if (turnManager == null)
{
Logging.Error("[FortFightGameManager] TurnManager reference not assigned!");
}
if (aiController == null) if (aiController == null)
{ {
Logging.Warning("[FortFightGameManager] AIController reference not assigned! AI mode will not work."); Logging.Warning("[FortFightGameManager] AIController reference not assigned! AI mode will not work.");
@@ -133,6 +152,11 @@ namespace Minigames.FortFight.Core
{ {
Logging.Error("[FortFightGameManager] GameplayPage reference not assigned!"); Logging.Error("[FortFightGameManager] GameplayPage reference not assigned!");
} }
if (gameOverUI == null)
{
Logging.Error("[FortFightGameManager] GameOverUI reference not assigned!");
}
} }
#endregion #endregion
@@ -151,10 +175,16 @@ namespace Minigames.FortFight.Core
Logging.Debug("[FortFightGameManager] Showing mode selection page"); Logging.Debug("[FortFightGameManager] Showing mode selection page");
} }
// Hide other UI pages
if (gameplayPage != null) if (gameplayPage != null)
{ {
gameplayPage.gameObject.SetActive(false); gameplayPage.gameObject.SetActive(false);
} }
if (gameOverUI != null)
{
gameOverUI.Hide();
}
} }
/// <summary> /// <summary>
@@ -192,15 +222,15 @@ namespace Minigames.FortFight.Core
Logging.Debug($"[FortFightGameManager] Players initialized - P1: {playerOne.PlayerName}, P2: {playerTwo.PlayerName}"); Logging.Debug($"[FortFightGameManager] Players initialized - P1: {playerOne.PlayerName}, P2: {playerTwo.PlayerName}");
// Spawn forts for both players // Spawn forts for both players via singleton
if (fortManager != null) if (FortManager.Instance != null)
{ {
fortManager.SpawnForts(); FortManager.Instance.SpawnForts();
Logging.Debug("[FortFightGameManager] Forts spawned for both players"); Logging.Debug("[FortFightGameManager] Forts spawned for both players");
} }
else else
{ {
Logging.Warning("[FortFightGameManager] FortManager not assigned! Forts will not spawn."); Logging.Warning("[FortFightGameManager] FortManager not found! Forts will not spawn.");
} }
} }
@@ -221,11 +251,11 @@ namespace Minigames.FortFight.Core
gameplayPage.TransitionIn(); gameplayPage.TransitionIn();
} }
// Initialize turn manager // Initialize turn manager via singleton
if (turnManager != null) if (TurnManager.Instance != null)
{ {
turnManager.Initialize(playerOne, playerTwo); TurnManager.Instance.Initialize(playerOne, playerTwo);
turnManager.StartGame(); TurnManager.Instance.StartGame();
} }
// Initialize AI if in single player mode // Initialize AI if in single player mode
@@ -234,29 +264,122 @@ namespace Minigames.FortFight.Core
aiController.Initialize(); aiController.Initialize();
} }
// Subscribe to fort defeated events (may need to wait for forts to spawn)
StartCoroutine(SubscribeToFortEventsWhenReady());
isGameActive = true; isGameActive = true;
gameStartTime = Time.time; // Start tracking elapsed time
OnGameStarted?.Invoke(); OnGameStarted?.Invoke();
Logging.Debug("[FortFightGameManager] Game started!"); Logging.Debug("[FortFightGameManager] Game started!");
} }
/// <summary> /// <summary>
/// End the game /// Wait for forts to be spawned and ready, then subscribe to their defeat events
/// </summary>
private System.Collections.IEnumerator SubscribeToFortEventsWhenReady()
{
Logging.Debug("[FortFightGameManager] Waiting for forts to be ready...");
var fortManager = FortManager.Instance;
if (fortManager == null)
{
Logging.Error("[FortFightGameManager] FortManager not found! Cannot subscribe to fort events.");
yield break;
}
// Wait up to 5 seconds for forts to spawn
float timeout = 5f;
float elapsed = 0f;
while ((fortManager.PlayerFort == null || fortManager.EnemyFort == null) && elapsed < timeout)
{
yield return new WaitForSeconds(0.1f);
elapsed += 0.1f;
}
if (fortManager.PlayerFort == null || fortManager.EnemyFort == null)
{
Logging.Error($"[FortFightGameManager] Forts not ready after {timeout}s! PlayerFort: {fortManager.PlayerFort != null}, EnemyFort: {fortManager.EnemyFort != null}");
yield break;
}
// Subscribe to both forts
Logging.Debug($"[FortFightGameManager] Forts ready! Subscribing to defeat events...");
fortManager.PlayerFort.OnFortDefeated += OnFortDefeated;
fortManager.EnemyFort.OnFortDefeated += OnFortDefeated;
Logging.Debug($"[FortFightGameManager] Successfully subscribed to fort defeat events: PlayerFort={fortManager.PlayerFort.FortName}, EnemyFort={fortManager.EnemyFort.FortName}");
}
/// <summary>
/// Called when any fort is defeated
/// </summary>
private void OnFortDefeated()
{
Logging.Debug("[FortFightGameManager] Fort defeated, ending game...");
EndGame();
}
/// <summary>
/// End the game and show game over UI
/// </summary> /// </summary>
public void EndGame() public void EndGame()
{ {
if (!isGameActive)
{
Logging.Warning("[FortFightGameManager] EndGame called but game is not active");
return;
}
isGameActive = false; isGameActive = false;
if (turnManager != null) // Stop turn manager
if (TurnManager.Instance != null)
{ {
turnManager.SetGameOver(); TurnManager.Instance.SetGameOver();
} }
// Manage UI transitions
ShowGameOver();
OnGameEnded?.Invoke(); OnGameEnded?.Invoke();
Logging.Debug("[FortFightGameManager] Game ended"); Logging.Debug("[FortFightGameManager] Game ended");
} }
/// <summary>
/// Show game over UI and hide gameplay UI
/// </summary>
private void ShowGameOver()
{
// Hide gameplay page
if (gameplayPage != null)
{
gameplayPage.gameObject.SetActive(false);
}
// Show game over UI
if (gameOverUI != null)
{
gameOverUI.Show();
}
else
{
Logging.Error("[FortFightGameManager] Cannot show game over UI - reference not assigned!");
}
// Switch camera to wide view
var cameraController = CameraController.Instance;
if (cameraController != null)
{
cameraController.ShowWideView();
}
Logging.Debug("[FortFightGameManager] Game over UI shown");
}
#endregion #endregion
} }
} }

View File

@@ -9,9 +9,27 @@ namespace Minigames.FortFight.Core
{ {
/// <summary> /// <summary>
/// Manages fort prefab spawning and references during gameplay. /// Manages fort prefab spawning and references during gameplay.
/// Singleton pattern for easy access to fort references.
/// </summary> /// </summary>
public class FortManager : ManagedBehaviour public class FortManager : ManagedBehaviour
{ {
#region Singleton
private static FortManager _instance;
public static FortManager Instance
{
get
{
if (_instance == null)
{
_instance = FindFirstObjectByType<FortManager>();
}
return _instance;
}
}
#endregion
#region Inspector Properties #region Inspector Properties
[Header("Fort Prefabs")] [Header("Fort Prefabs")]
@@ -56,6 +74,15 @@ namespace Minigames.FortFight.Core
{ {
base.OnManagedAwake(); base.OnManagedAwake();
// Register singleton
if (_instance != null && _instance != this)
{
Logging.Warning("[FortManager] Multiple instances detected! Destroying duplicate.");
Destroy(gameObject);
return;
}
_instance = this;
// Validate spawn points // Validate spawn points
if (playerSpawnPoint == null) if (playerSpawnPoint == null)
{ {

View File

@@ -10,9 +10,27 @@ namespace Minigames.FortFight.Core
/// Manages turn order and turn state for Fort Fight minigame. /// Manages turn order and turn state for Fort Fight minigame.
/// Handles transitions between Player One, Player Two/AI turns. /// Handles transitions between Player One, Player Two/AI turns.
/// Manages turn actions and input delegation. /// Manages turn actions and input delegation.
/// Singleton pattern for easy access to turn state.
/// </summary> /// </summary>
public class TurnManager : ManagedBehaviour public class TurnManager : ManagedBehaviour
{ {
#region Singleton
private static TurnManager _instance;
public static TurnManager Instance
{
get
{
if (_instance == null)
{
_instance = FindFirstObjectByType<TurnManager>();
}
return _instance;
}
}
#endregion
#region Inspector References #region Inspector References
[Header("Slingshot Controllers")] [Header("Slingshot Controllers")]
@@ -23,12 +41,11 @@ namespace Minigames.FortFight.Core
[SerializeField] private SlingshotController playerTwoSlingshotController; [SerializeField] private SlingshotController playerTwoSlingshotController;
[Header("Systems")] [Header("Systems")]
[Tooltip("Ammunition manager")]
[SerializeField] private AmmunitionManager ammunitionManager;
[Tooltip("Camera controller for projectile tracking")] [Tooltip("Camera controller for projectile tracking")]
[SerializeField] private CameraController cameraController; [SerializeField] private CameraController cameraController;
// Note: AmmunitionManager accessed via singleton (AmmunitionManager.Instance)
#endregion #endregion
#region Events #region Events
@@ -75,6 +92,15 @@ namespace Minigames.FortFight.Core
{ {
base.OnManagedAwake(); base.OnManagedAwake();
// Register singleton
if (_instance != null && _instance != this)
{
Logging.Warning("[TurnManager] Multiple instances detected! Destroying duplicate.");
Destroy(gameObject);
return;
}
_instance = this;
// Validate references // Validate references
if (playerOneSlingshotController == null) if (playerOneSlingshotController == null)
{ {
@@ -86,11 +112,6 @@ namespace Minigames.FortFight.Core
Logging.Error("[TurnManager] Player Two slingshot controller not assigned!"); Logging.Error("[TurnManager] Player Two slingshot controller not assigned!");
} }
if (ammunitionManager == null)
{
Logging.Error("[TurnManager] Ammunition manager not assigned!");
}
if (cameraController == null) if (cameraController == null)
{ {
Logging.Warning("[TurnManager] Camera controller not assigned - projectiles won't be followed by camera!"); Logging.Warning("[TurnManager] Camera controller not assigned - projectiles won't be followed by camera!");
@@ -186,12 +207,12 @@ namespace Minigames.FortFight.Core
} }
// Create and execute turn action with player index // Create and execute turn action with player index
currentTurnAction = new ProjectileTurnAction(activeSlingshot, ammunitionManager, cameraController, currentPlayer.PlayerIndex); currentTurnAction = new ProjectileTurnAction(activeSlingshot, AmmunitionManager.Instance, cameraController, currentPlayer.PlayerIndex);
// Set current ammo on slingshot for this player // Set current ammo on slingshot for this player
if (ammunitionManager != null) if (AmmunitionManager.Instance != null)
{ {
ProjectileConfig currentAmmo = ammunitionManager.GetSelectedAmmoConfig(currentPlayer.PlayerIndex); ProjectileConfig currentAmmo = AmmunitionManager.Instance.GetSelectedAmmoConfig(currentPlayer.PlayerIndex);
if (currentAmmo != null) if (currentAmmo != null)
{ {
activeSlingshot.SetAmmo(currentAmmo); activeSlingshot.SetAmmo(currentAmmo);
@@ -253,9 +274,9 @@ namespace Minigames.FortFight.Core
OnTurnEnded?.Invoke(currentPlayer); OnTurnEnded?.Invoke(currentPlayer);
// Decrement ammunition cooldowns for this player // Decrement ammunition cooldowns for this player
if (ammunitionManager != null) if (AmmunitionManager.Instance != null)
{ {
ammunitionManager.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)

View File

@@ -20,6 +20,9 @@ namespace Minigames.FortFight.Fort
[SerializeField] private BlockSize size = BlockSize.Medium; [SerializeField] private BlockSize size = BlockSize.Medium;
[SerializeField] private bool isWeakPoint = false; [SerializeField] private bool isWeakPoint = false;
[Tooltip("Fixed HP value for this block (default: 10)")]
[SerializeField] private float blockHp = 10f;
[Header("Weak Point Settings (if applicable)")] [Header("Weak Point Settings (if applicable)")]
[Tooltip("Visual indicator shown in editor/game for weak points")] [Tooltip("Visual indicator shown in editor/game for weak points")]
[SerializeField] private GameObject weakPointVisualIndicator; [SerializeField] private GameObject weakPointVisualIndicator;
@@ -146,15 +149,8 @@ namespace Minigames.FortFight.Fort
private void CalculateHp() private void CalculateHp()
{ {
// Get material config // Use fixed block HP value (default 10)
var materialConfig = CachedSettings.GetMaterialConfig(material); maxHp = blockHp;
float baseMaterialHp = materialConfig?.baseHp ?? 20f;
// Get size config
var sizeConfig = CachedSettings.GetSizeConfig(size);
float sizeMultiplier = sizeConfig?.hpMultiplier ?? 1f;
maxHp = baseMaterialHp * sizeMultiplier;
currentHp = maxHp; currentHp = maxHp;
Logging.Debug($"[FortBlock] {gameObject.name} initialized: {material} {size}, HP: {maxHp}"); Logging.Debug($"[FortBlock] {gameObject.name} initialized: {material} {size}, HP: {maxHp}");

View File

@@ -52,6 +52,10 @@ namespace Minigames.FortFight.Fort
public int InitialBlockCount => initialBlockCount; public int InitialBlockCount => initialBlockCount;
public bool IsDefeated { get; private set; } public bool IsDefeated { get; private set; }
// Aliases for consistency
public float MaxHp => maxFortHp;
public float CurrentHp => currentFortHp;
#endregion #endregion
#region Private State #region Private State
@@ -171,6 +175,9 @@ namespace Minigames.FortFight.Fort
RegisterBlock(block); RegisterBlock(block);
} }
// Step 3: Initialize current HP to match max HP (sum of all blocks)
currentFortHp = maxFortHp;
initialBlockCount = blocks.Count; initialBlockCount = blocks.Count;
Logging.Debug($"[FortController] {fortName} - Initialized and registered {blocks.Count} blocks, Total HP: {maxFortHp:F0}"); Logging.Debug($"[FortController] {fortName} - Initialized and registered {blocks.Count} blocks, Total HP: {maxFortHp:F0}");
} }
@@ -181,7 +188,7 @@ namespace Minigames.FortFight.Fort
/// </summary> /// </summary>
private void RegisterWithManager() private void RegisterWithManager()
{ {
Core.FortManager manager = FindFirstObjectByType<Core.FortManager>(); Core.FortManager manager = Core.FortManager.Instance;
if (manager == null) if (manager == null)
{ {
@@ -207,8 +214,9 @@ namespace Minigames.FortFight.Fort
if (!blocks.Contains(block)) if (!blocks.Contains(block))
{ {
blocks.Add(block); blocks.Add(block);
// Only add to max HP, current HP will be calculated once at end of initialization
maxFortHp += block.MaxHp; maxFortHp += block.MaxHp;
currentFortHp += block.MaxHp;
// Subscribe to block events // Subscribe to block events
block.OnBlockDestroyed += HandleBlockDestroyed; block.OnBlockDestroyed += HandleBlockDestroyed;
@@ -259,9 +267,8 @@ namespace Minigames.FortFight.Fort
// Remove from list // Remove from list
blocks.Remove(block); blocks.Remove(block);
// Update current HP // Recalculate HP by summing all remaining blocks (consistent calculation method)
currentFortHp -= blockMaxHp; RecalculateFortHp();
currentFortHp = Mathf.Max(0f, currentFortHp);
// Notify listeners // Notify listeners
OnBlockDestroyed?.Invoke(block); OnBlockDestroyed?.Invoke(block);
@@ -279,7 +286,38 @@ namespace Minigames.FortFight.Fort
private void HandleBlockDamaged(float currentBlockHp, float maxBlockHp) private void HandleBlockDamaged(float currentBlockHp, float maxBlockHp)
{ {
// Block damaged but not destroyed // Block damaged but not destroyed
// Could add visual feedback here if needed Logging.Debug($"[FortController] {fortName} - Block damaged! CurrentBlockHP: {currentBlockHp}/{maxBlockHp}");
// Recalculate current fort HP based on all block HP
RecalculateFortHp();
// Notify UI to update
int listenerCount = OnFortDamaged?.GetInvocationList()?.Length ?? 0;
Logging.Debug($"[FortController] {fortName} - Firing OnFortDamaged event. HP: {HpPercentage:F1}%. Listeners: {listenerCount}");
OnFortDamaged?.Invoke(0f, HpPercentage);
// Check defeat condition after damage
CheckDefeatCondition();
}
/// <summary>
/// Recalculate total fort HP by summing all block HP
/// </summary>
private void RecalculateFortHp()
{
currentFortHp = 0f;
foreach (var block in blocks)
{
if (block != null)
{
currentFortHp += block.CurrentHp;
}
}
if (showDebugInfo)
{
Logging.Debug($"[FortController] {fortName} - HP recalculated: {currentFortHp:F0}/{maxFortHp:F0} ({HpPercentage:F1}%)");
}
} }
#endregion #endregion
@@ -288,17 +326,32 @@ namespace Minigames.FortFight.Fort
private void CheckDefeatCondition() private void CheckDefeatCondition()
{ {
if (IsDefeated) return; if (IsDefeated)
{
Logging.Debug($"[FortController] {fortName} - Already defeated, skipping check");
return;
}
float defeatThreshold = CachedSettings?.FortDefeatThreshold ?? 0.3f; float defeatThreshold = CachedSettings?.FortDefeatThreshold ?? 0.3f;
float defeatThresholdPercent = defeatThreshold * 100f; float defeatThresholdPercent = defeatThreshold * 100f;
// Defeat if HP below threshold Logging.Debug($"[FortController] {fortName} - Checking defeat: HP={currentFortHp:F1}/{maxFortHp:F1} ({HpPercentage:F1}%) vs threshold={defeatThresholdPercent:F1}%");
if (HpPercentage < defeatThresholdPercent)
// Defeat if HP at or below threshold
if (HpPercentage <= defeatThresholdPercent)
{ {
IsDefeated = true; IsDefeated = true;
Logging.Debug($"[FortController] {fortName} DEFEATED! Final HP: {HpPercentage:F1}% (threshold: {defeatThresholdPercent:F1}%)");
int listeners = OnFortDefeated?.GetInvocationList()?.Length ?? 0;
Logging.Debug($"[FortController] {fortName} DEFEATED! Final HP: {HpPercentage:F1}% (threshold: {defeatThresholdPercent:F1}%). Firing event to {listeners} listeners...");
OnFortDefeated?.Invoke(); OnFortDefeated?.Invoke();
Logging.Debug($"[FortController] {fortName} - OnFortDefeated event fired");
}
else
{
Logging.Debug($"[FortController] {fortName} - Not defeated yet ({HpPercentage:F1}% >= {defeatThresholdPercent:F1}%)");
} }
} }

View File

@@ -56,6 +56,13 @@ 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; }
#endregion
#region Timeout
private const float ProjectileTimeout = 10f; // Destroy projectile after 10 seconds if stuck/off-map
private Coroutine timeoutCoroutine;
#endregion #endregion
#region Components #region Components
@@ -181,6 +188,41 @@ namespace Minigames.FortFight.Projectiles
// Fire event // Fire event
OnLaunched?.Invoke(this); OnLaunched?.Invoke(this);
// Start timeout - destroy projectile after configured time if it hasn't been destroyed
StartTimeoutTimer();
}
#endregion
#region Timeout
/// <summary>
/// Start timeout timer. Projectile will auto-destroy after timeout to prevent stuck/lost projectiles.
/// </summary>
private void StartTimeoutTimer()
{
if (timeoutCoroutine != null)
{
StopCoroutine(timeoutCoroutine);
}
timeoutCoroutine = StartCoroutine(TimeoutCoroutine());
}
/// <summary>
/// Timeout coroutine - destroys projectile after configured time
/// </summary>
private System.Collections.IEnumerator TimeoutCoroutine()
{
yield return new WaitForSeconds(ProjectileTimeout);
// Only destroy if still exists (might have been destroyed by collision already)
if (this != null && gameObject != null)
{
Logging.Debug($"[ProjectileBase] {gameObject.name} timed out after {ProjectileTimeout}s, destroying...");
DestroyProjectile();
}
} }
#endregion #endregion
@@ -305,6 +347,13 @@ namespace Minigames.FortFight.Projectiles
{ {
Logging.Debug($"[ProjectileBase] Destroying {gameObject.name}"); Logging.Debug($"[ProjectileBase] Destroying {gameObject.name}");
// Stop timeout coroutine if running
if (timeoutCoroutine != null)
{
StopCoroutine(timeoutCoroutine);
timeoutCoroutine = null;
}
// Fire destroyed event // Fire destroyed event
OnDestroyed?.Invoke(this); OnDestroyed?.Invoke(this);

View File

@@ -51,6 +51,9 @@ namespace Minigames.FortFight.Projectiles
float lifetime = GetEffectLifetime(effect); float lifetime = GetEffectLifetime(effect);
Destroy(effect, lifetime); Destroy(effect, lifetime);
} }
// Destroy trash piece immediately after dealing damage
Destroy(gameObject);
} }
} }

View File

@@ -19,14 +19,10 @@ namespace Minigames.FortFight.UI
[SerializeField] private int playerIndex = 0; [SerializeField] private int playerIndex = 0;
[Header("References")] [Header("References")]
[Tooltip("Ammunition manager (shared between both players)")]
[SerializeField] private AmmunitionManager ammunitionManager;
[Tooltip("This player's slingshot controller")] [Tooltip("This player's slingshot controller")]
[SerializeField] private SlingshotController slingshotController; [SerializeField] private SlingshotController slingshotController;
[Tooltip("Turn manager to subscribe to turn events")] // Note: AmmunitionManager and TurnManager accessed via singletons
[SerializeField] private TurnManager turnManager;
[Header("UI")] [Header("UI")]
[Tooltip("Array of ammo buttons in this panel")] [Tooltip("Array of ammo buttons in this panel")]
@@ -44,21 +40,11 @@ namespace Minigames.FortFight.UI
base.OnManagedAwake(); base.OnManagedAwake();
// Validate references // Validate references
if (ammunitionManager == null)
{
Logging.Error($"[AmmunitionPanel] Player {playerIndex}: Ammunition manager not assigned!");
}
if (slingshotController == null) if (slingshotController == null)
{ {
Logging.Error($"[AmmunitionPanel] Player {playerIndex}: Slingshot controller not assigned!"); Logging.Error($"[AmmunitionPanel] Player {playerIndex}: Slingshot controller not assigned!");
} }
if (turnManager == null)
{
Logging.Error($"[AmmunitionPanel] Player {playerIndex}: Turn manager not assigned!");
}
if (ammoButtons == null || ammoButtons.Length == 0) if (ammoButtons == null || ammoButtons.Length == 0)
{ {
Logging.Warning($"[AmmunitionPanel] Player {playerIndex}: No ammo buttons assigned!"); Logging.Warning($"[AmmunitionPanel] Player {playerIndex}: No ammo buttons assigned!");
@@ -78,10 +64,10 @@ namespace Minigames.FortFight.UI
// Initialize ammo buttons with player context // Initialize ammo buttons with player context
InitializeButtons(); InitializeButtons();
// Subscribe to turn events // Subscribe to turn events via singleton
if (turnManager != null) if (TurnManager.Instance != null)
{ {
turnManager.OnTurnStarted += HandleTurnStarted; TurnManager.Instance.OnTurnStarted += HandleTurnStarted;
} }
// Start hidden // Start hidden
@@ -93,9 +79,9 @@ namespace Minigames.FortFight.UI
base.OnManagedDestroy(); base.OnManagedDestroy();
// Unsubscribe from events // Unsubscribe from events
if (turnManager != null) if (TurnManager.Instance != null)
{ {
turnManager.OnTurnStarted -= HandleTurnStarted; TurnManager.Instance.OnTurnStarted -= HandleTurnStarted;
} }
} }
@@ -104,13 +90,13 @@ namespace Minigames.FortFight.UI
/// </summary> /// </summary>
private void InitializeButtons() private void InitializeButtons()
{ {
if (ammunitionManager == null || slingshotController == null || ammoButtons == null) if (AmmunitionManager.Instance == null || slingshotController == null || ammoButtons == null)
{ {
return; return;
} }
// Get available projectile types from settings // Get available projectile types from settings
var availableTypes = ammunitionManager.GetAvailableProjectileTypes(); var availableTypes = AmmunitionManager.Instance.GetAvailableProjectileTypes();
var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IFortFightSettings>(); var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IFortFightSettings>();
if (settings == null) if (settings == null)
@@ -126,7 +112,7 @@ namespace Minigames.FortFight.UI
var config = settings.GetProjectileConfig(availableTypes[i]); var config = settings.GetProjectileConfig(availableTypes[i]);
if (config != null) if (config != null)
{ {
ammoButtons[i].Initialize(config, ammunitionManager, slingshotController, playerIndex); ammoButtons[i].Initialize(config, AmmunitionManager.Instance, slingshotController, playerIndex);
Logging.Debug($"[AmmunitionPanel] Player {playerIndex}: Initialized button for {config.displayName}"); Logging.Debug($"[AmmunitionPanel] Player {playerIndex}: Initialized button for {config.displayName}");
} }
} }

View File

@@ -31,6 +31,12 @@ namespace Minigames.FortFight.UI
[Tooltip("Leave empty to auto-find")] [Tooltip("Leave empty to auto-find")]
[SerializeField] private Core.FortManager fortManager; [SerializeField] private Core.FortManager fortManager;
[Header("Debug Display")]
[Tooltip("Show numerical HP values (current/max)")]
[SerializeField] private bool debugDisplay = false;
[Tooltip("Text field to display 'current/max' HP values")]
[SerializeField] private TextMeshProUGUI debugHpText;
#endregion #endregion
#region Private State #region Private State
@@ -45,11 +51,17 @@ namespace Minigames.FortFight.UI
{ {
base.OnManagedStart(); base.OnManagedStart();
Logging.Debug($"[FortHealthUI] OnManagedStart - autoBindToFort: {autoBindToFort}, isPlayerFort: {isPlayerFort}");
// Auto-bind to dynamically spawned forts // Auto-bind to dynamically spawned forts
if (autoBindToFort) if (autoBindToFort)
{ {
SetupAutoBinding(); SetupAutoBinding();
} }
else
{
Logging.Warning($"[FortHealthUI] Auto-bind disabled! HP UI will not update.");
}
} }
internal override void OnManagedDestroy() internal override void OnManagedDestroy()
@@ -76,33 +88,53 @@ namespace Minigames.FortFight.UI
private void SetupAutoBinding() private void SetupAutoBinding()
{ {
// Find FortManager if not assigned Logging.Debug($"[FortHealthUI] SetupAutoBinding called for {(isPlayerFort ? "PLAYER" : "ENEMY")} fort");
if (fortManager == null)
{ // Get FortManager via singleton
fortManager = FindFirstObjectByType<Core.FortManager>(); fortManager = Core.FortManager.Instance;
}
if (fortManager == null) if (fortManager == null)
{ {
Logging.Warning($"[FortHealthUI] Cannot auto-bind: FortManager not found. HP UI will not update."); Logging.Error($"[FortHealthUI] CRITICAL: FortManager.Instance is NULL! HP UI will not work.");
return; return;
} }
// Subscribe to appropriate spawn event Logging.Debug($"[FortHealthUI] FortManager found. Checking if fort already spawned...");
// Check if fort already spawned (missed the spawn event)
FortController existingFort = isPlayerFort ? fortManager.PlayerFort : fortManager.EnemyFort;
if (existingFort != null)
{
Logging.Debug($"[FortHealthUI] Fort already exists, binding immediately: {existingFort.FortName}");
BindToFort(existingFort);
return;
}
Logging.Debug($"[FortHealthUI] Fort not spawned yet. Subscribing to spawn events...");
// Subscribe to appropriate spawn event for future spawn
if (isPlayerFort) if (isPlayerFort)
{ {
fortManager.OnPlayerFortSpawned += OnFortSpawned; fortManager.OnPlayerFortSpawned += OnFortSpawned;
Logging.Debug($"[FortHealthUI] Subscribed to PLAYER fort spawn"); Logging.Debug($"[FortHealthUI] Subscribed to OnPlayerFortSpawned event");
} }
else else
{ {
fortManager.OnEnemyFortSpawned += OnFortSpawned; fortManager.OnEnemyFortSpawned += OnFortSpawned;
Logging.Debug($"[FortHealthUI] Subscribed to ENEMY fort spawn"); Logging.Debug($"[FortHealthUI] Subscribed to OnEnemyFortSpawned event");
} }
} }
private void OnFortSpawned(FortController fort) private void OnFortSpawned(FortController fort)
{ {
Logging.Debug($"[FortHealthUI] 🎯 OnFortSpawned event received! Fort: {fort?.FortName ?? "NULL"}");
if (fort == null)
{
Logging.Error($"[FortHealthUI] Fort is NULL in spawn callback!");
return;
}
BindToFort(fort); BindToFort(fort);
} }
@@ -140,7 +172,7 @@ namespace Minigames.FortFight.UI
UpdateDisplay(); UpdateDisplay();
Logging.Debug($"[FortHealthUI] Bound to fort: {fort.FortName}"); Logging.Debug($"[FortHealthUI] Bound to fort: {fort.FortName}. Event subscription successful.");
} }
#endregion #endregion
@@ -149,6 +181,7 @@ namespace Minigames.FortFight.UI
private void OnFortDamaged(float damage, float hpPercentage) private void OnFortDamaged(float damage, float hpPercentage)
{ {
Logging.Debug($"[FortHealthUI] OnFortDamaged received! Damage: {damage}, HP%: {hpPercentage:F1}%, Fort: {trackedFort?.FortName}");
UpdateDisplay(); UpdateDisplay();
} }
@@ -158,20 +191,52 @@ namespace Minigames.FortFight.UI
private void UpdateDisplay() private void UpdateDisplay()
{ {
if (trackedFort == null) return; if (trackedFort == null)
{
Logging.Warning("[FortHealthUI] UpdateDisplay called but trackedFort is null!");
return;
}
float hpPercent = trackedFort.HpPercentage; float hpPercent = trackedFort.HpPercentage;
Logging.Debug($"[FortHealthUI] UpdateDisplay - Fort: {trackedFort.FortName}, HP: {hpPercent:F1}%");
// Update slider // Update slider
if (hpSlider != null) if (hpSlider != null)
{ {
hpSlider.value = hpPercent / 100f; hpSlider.value = hpPercent / 100f;
Logging.Debug($"[FortHealthUI] Slider updated to {hpSlider.value:F2}");
}
else
{
Logging.Warning("[FortHealthUI] hpSlider is null!");
} }
// Update text // Update text
if (hpPercentageText != null) if (hpPercentageText != null)
{ {
hpPercentageText.text = $"{hpPercent:F0}%"; hpPercentageText.text = $"{hpPercent:F0}%";
Logging.Debug($"[FortHealthUI] Text updated to {hpPercentageText.text}");
}
else
{
Logging.Warning("[FortHealthUI] hpPercentageText is null!");
}
// Update debug HP display (current/max)
if (debugHpText != null)
{
if (debugDisplay)
{
debugHpText.gameObject.SetActive(true);
float currentHp = trackedFort.CurrentHp;
float maxHp = trackedFort.MaxHp;
debugHpText.text = $"{currentHp:F0}/{maxHp:F0}";
}
else
{
debugHpText.gameObject.SetActive(false);
}
} }
// Update color based on HP // Update color based on HP

View File

@@ -0,0 +1,234 @@
using Core;
using Core.Lifecycle;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Minigames.FortFight.UI
{
/// <summary>
/// Game Over UI - displays when a fort is defeated.
/// Shows match time, winner, and restart button.
/// </summary>
public class GameOverUI : ManagedBehaviour
{
#region Inspector Properties
[Header("UI References")]
[Tooltip("Root GameObject to show/hide the entire UI")]
[SerializeField] private GameObject rootPanel;
[Tooltip("Text showing elapsed time")]
[SerializeField] private TextMeshProUGUI elapsedTimeText;
[Tooltip("Text showing winner")]
[SerializeField] private TextMeshProUGUI winnerText;
[Tooltip("Restart button")]
[SerializeField] private Button restartButton;
[Header("Optional Visuals")]
[Tooltip("Optional canvas group for fade-in")]
[SerializeField] private CanvasGroup canvasGroup;
#endregion
#region Lifecycle
internal override void OnManagedAwake()
{
base.OnManagedAwake();
// Validate references
if (rootPanel == null)
{
Logging.Error("[GameOverUI] Root panel not assigned!");
}
if (elapsedTimeText == null)
{
Logging.Warning("[GameOverUI] Elapsed time text not assigned!");
}
if (winnerText == null)
{
Logging.Warning("[GameOverUI] Winner text not assigned!");
}
if (restartButton == null)
{
Logging.Error("[GameOverUI] Restart button not assigned!");
}
// Setup button listener
if (restartButton != null)
{
restartButton.onClick.AddListener(OnRestartClicked);
}
// Ensure canvas group exists for fade
if (canvasGroup == null && rootPanel != null)
{
canvasGroup = rootPanel.GetComponent<CanvasGroup>();
}
// Start hidden
Hide();
}
internal override void OnManagedDestroy()
{
base.OnManagedDestroy();
// Remove button listener
if (restartButton != null)
{
restartButton.onClick.RemoveListener(OnRestartClicked);
}
}
#endregion
#region Event Handlers
private void OnRestartClicked()
{
Logging.Debug("[GameOverUI] Restart button clicked, reloading scene...");
RestartGame();
}
#endregion
#region Display
/// <summary>
/// Show the game over UI with match results
/// Called by FortFightGameManager when game ends
/// </summary>
public void Show()
{
if (rootPanel != null)
{
rootPanel.SetActive(true);
}
// Get game manager for elapsed time
var gameManager = Core.FortFightGameManager.Instance;
if (gameManager != null)
{
float elapsedTime = gameManager.ElapsedGameTime;
UpdateElapsedTime(elapsedTime);
// Determine winner
DetermineWinner();
}
// Optional: Fade in
if (canvasGroup != null)
{
canvasGroup.alpha = 0f;
StartCoroutine(FadeIn());
}
Logging.Debug("[GameOverUI] Game over UI shown");
}
/// <summary>
/// Hide the game over UI
/// </summary>
public void Hide()
{
if (rootPanel != null)
{
rootPanel.SetActive(false);
}
}
/// <summary>
/// Update the elapsed time display
/// </summary>
private void UpdateElapsedTime(float seconds)
{
if (elapsedTimeText == null) return;
// Format as MM:SS
int minutes = Mathf.FloorToInt(seconds / 60f);
int secs = Mathf.FloorToInt(seconds % 60f);
elapsedTimeText.text = $"{minutes:00}:{secs:00}";
}
/// <summary>
/// Determine and display the winner
/// </summary>
private void DetermineWinner()
{
if (winnerText == null) return;
var fortManager = Core.FortManager.Instance;
if (fortManager == null) return;
bool playerDefeated = fortManager.PlayerFort?.IsDefeated ?? false;
bool enemyDefeated = fortManager.EnemyFort?.IsDefeated ?? false;
if (playerDefeated && enemyDefeated)
{
winnerText.text = "DRAW!";
}
else if (playerDefeated)
{
winnerText.text = "PLAYER TWO WINS!";
}
else if (enemyDefeated)
{
winnerText.text = "PLAYER ONE WINS!";
}
else
{
winnerText.text = "GAME OVER";
}
}
/// <summary>
/// Fade in the UI over time
/// </summary>
private System.Collections.IEnumerator FadeIn()
{
float duration = 0.5f;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
if (canvasGroup != null)
{
canvasGroup.alpha = Mathf.Lerp(0f, 1f, elapsed / duration);
}
yield return null;
}
if (canvasGroup != null)
{
canvasGroup.alpha = 1f;
}
}
#endregion
#region Restart
/// <summary>
/// Restart the game by reloading the current scene
/// </summary>
private void RestartGame()
{
// Use Unity's SceneManager to reload current scene
string currentScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
Logging.Debug($"[GameOverUI] Reloading scene: {currentScene}");
UnityEngine.SceneManagement.SceneManager.LoadScene(currentScene);
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ffb13ad5109340eba06f9c02082ece94
timeCreated: 1764806274

View File

@@ -1,6 +1,5 @@
using Core; using Core;
using UnityEngine; using UnityEngine;
using UnityEngine.UI;
using TMPro; using TMPro;
using UI.Core; using UI.Core;
using Minigames.FortFight.Core; using Minigames.FortFight.Core;
@@ -48,8 +47,8 @@ namespace Minigames.FortFight.UI
{ {
base.OnManagedStart(); base.OnManagedStart();
// Get turn manager reference // Get turn manager reference via singleton
turnManager = FindFirstObjectByType<TurnManager>(); turnManager = TurnManager.Instance;
if (turnManager != null) if (turnManager != null)
{ {

View File

@@ -35,7 +35,7 @@ MonoBehaviour:
weakPointExplosionRadius: 5 weakPointExplosionRadius: 5
weakPointExplosionDamage: 50 weakPointExplosionDamage: 50
weakPointExplosionForce: 50 weakPointExplosionForce: 50
fortDefeatThreshold: 0.3 fortDefeatThreshold: 0
blockGravityScale: 2 blockGravityScale: 2
projectileGravityScale: 1 projectileGravityScale: 1
projectileSettleDelay: 2.5 projectileSettleDelay: 2.5
@@ -77,13 +77,17 @@ MonoBehaviour:
description: description:
damage: 20 damage: 20
mass: 5 mass: 5
vacuumSlideForce: 15 vacuumSlideSpeed: 10
vacuumDestroyBlockCount: 2 vacuumDestroyBlockCount: 2
vacuumBlockDamage: 999 vacuumBlockDamage: 999
trashBagPieceCount: 5 trashBagPieceCount: 5
trashBagPieceForce: 10 trashBagPieceForce: 10
trashBagSpreadAngle: 60 trashBagSpreadAngle: 60
trashPieceDamage: 5
trashPieceLifetime: 5
ceilingFanActivationDelay: 1 ceilingFanActivationDelay: 1
ceilingFanDropDelay: 0.2
ceilingFanDropSpeed: 20
baseLaunchForce: 150 baseLaunchForce: 150
minForceMultiplier: 0.1 minForceMultiplier: 0.1
maxForceMultiplier: 1 maxForceMultiplier: 1