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

View File

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

View File

@@ -11,9 +11,27 @@ namespace Minigames.FortFight.Core
/// <summary>
/// Manages ammunition selection and cooldowns for Fort Fight.
/// Tracks which ammo types are available and handles cooldown timers.
/// Singleton pattern for easy access throughout the game.
/// </summary>
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
private const int MaxPlayers = 2; // Support 2 players (indices 0 and 1)
@@ -85,6 +103,20 @@ namespace Minigames.FortFight.Core
#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()
{
base.OnManagedStart();

View File

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

View File

@@ -25,13 +25,14 @@ namespace Minigames.FortFight.Core
#region Inspector References
[Header("Core Systems")]
[SerializeField] private TurnManager turnManager;
[SerializeField] private FortFightAIController aiController;
[SerializeField] private FortManager fortManager;
[Header("UI References")]
[SerializeField] private ModeSelectionPage modeSelectionPage;
[SerializeField] private GameplayPage gameplayPage;
[SerializeField] private UI.GameOverUI gameOverUI;
// Note: TurnManager and FortManager accessed via singletons
#endregion
@@ -60,10 +61,23 @@ namespace Minigames.FortFight.Core
private PlayerData playerOne;
private PlayerData playerTwo;
private bool isGameActive = false;
private float gameStartTime = 0f;
public FortFightGameMode CurrentGameMode => currentGameMode;
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
#region Lifecycle
@@ -97,9 +111,19 @@ namespace Minigames.FortFight.Core
{
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
@@ -114,11 +138,6 @@ namespace Minigames.FortFight.Core
private void ValidateReferences()
{
if (turnManager == null)
{
Logging.Error("[FortFightGameManager] TurnManager reference not assigned!");
}
if (aiController == null)
{
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!");
}
if (gameOverUI == null)
{
Logging.Error("[FortFightGameManager] GameOverUI reference not assigned!");
}
}
#endregion
@@ -151,10 +175,16 @@ namespace Minigames.FortFight.Core
Logging.Debug("[FortFightGameManager] Showing mode selection page");
}
// Hide other UI pages
if (gameplayPage != null)
{
gameplayPage.gameObject.SetActive(false);
}
if (gameOverUI != null)
{
gameOverUI.Hide();
}
}
/// <summary>
@@ -192,15 +222,15 @@ namespace Minigames.FortFight.Core
Logging.Debug($"[FortFightGameManager] Players initialized - P1: {playerOne.PlayerName}, P2: {playerTwo.PlayerName}");
// Spawn forts for both players
if (fortManager != null)
// Spawn forts for both players via singleton
if (FortManager.Instance != null)
{
fortManager.SpawnForts();
FortManager.Instance.SpawnForts();
Logging.Debug("[FortFightGameManager] Forts spawned for both players");
}
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();
}
// Initialize turn manager
if (turnManager != null)
// Initialize turn manager via singleton
if (TurnManager.Instance != null)
{
turnManager.Initialize(playerOne, playerTwo);
turnManager.StartGame();
TurnManager.Instance.Initialize(playerOne, playerTwo);
TurnManager.Instance.StartGame();
}
// Initialize AI if in single player mode
@@ -234,29 +264,122 @@ namespace Minigames.FortFight.Core
aiController.Initialize();
}
// Subscribe to fort defeated events (may need to wait for forts to spawn)
StartCoroutine(SubscribeToFortEventsWhenReady());
isGameActive = true;
gameStartTime = Time.time; // Start tracking elapsed time
OnGameStarted?.Invoke();
Logging.Debug("[FortFightGameManager] Game started!");
}
/// <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>
public void EndGame()
{
if (!isGameActive)
{
Logging.Warning("[FortFightGameManager] EndGame called but game is not active");
return;
}
isGameActive = false;
if (turnManager != null)
// Stop turn manager
if (TurnManager.Instance != null)
{
turnManager.SetGameOver();
TurnManager.Instance.SetGameOver();
}
// Manage UI transitions
ShowGameOver();
OnGameEnded?.Invoke();
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
}
}

View File

@@ -9,9 +9,27 @@ namespace Minigames.FortFight.Core
{
/// <summary>
/// Manages fort prefab spawning and references during gameplay.
/// Singleton pattern for easy access to fort references.
/// </summary>
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
[Header("Fort Prefabs")]
@@ -56,6 +74,15 @@ namespace Minigames.FortFight.Core
{
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
if (playerSpawnPoint == null)
{

View File

@@ -10,9 +10,27 @@ namespace Minigames.FortFight.Core
/// Manages turn order and turn state for Fort Fight minigame.
/// Handles transitions between Player One, Player Two/AI turns.
/// Manages turn actions and input delegation.
/// Singleton pattern for easy access to turn state.
/// </summary>
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
[Header("Slingshot Controllers")]
@@ -23,12 +41,11 @@ namespace Minigames.FortFight.Core
[SerializeField] private SlingshotController playerTwoSlingshotController;
[Header("Systems")]
[Tooltip("Ammunition manager")]
[SerializeField] private AmmunitionManager ammunitionManager;
[Tooltip("Camera controller for projectile tracking")]
[SerializeField] private CameraController cameraController;
// Note: AmmunitionManager accessed via singleton (AmmunitionManager.Instance)
#endregion
#region Events
@@ -75,6 +92,15 @@ namespace Minigames.FortFight.Core
{
base.OnManagedAwake();
// Register singleton
if (_instance != null && _instance != this)
{
Logging.Warning("[TurnManager] Multiple instances detected! Destroying duplicate.");
Destroy(gameObject);
return;
}
_instance = this;
// Validate references
if (playerOneSlingshotController == null)
{
@@ -86,11 +112,6 @@ namespace Minigames.FortFight.Core
Logging.Error("[TurnManager] Player Two slingshot controller not assigned!");
}
if (ammunitionManager == null)
{
Logging.Error("[TurnManager] Ammunition manager not assigned!");
}
if (cameraController == null)
{
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
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
if (ammunitionManager != null)
if (AmmunitionManager.Instance != null)
{
ProjectileConfig currentAmmo = ammunitionManager.GetSelectedAmmoConfig(currentPlayer.PlayerIndex);
ProjectileConfig currentAmmo = AmmunitionManager.Instance.GetSelectedAmmoConfig(currentPlayer.PlayerIndex);
if (currentAmmo != null)
{
activeSlingshot.SetAmmo(currentAmmo);
@@ -253,9 +274,9 @@ namespace Minigames.FortFight.Core
OnTurnEnded?.Invoke(currentPlayer);
// 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)

View File

@@ -20,6 +20,9 @@ namespace Minigames.FortFight.Fort
[SerializeField] private BlockSize size = BlockSize.Medium;
[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)")]
[Tooltip("Visual indicator shown in editor/game for weak points")]
[SerializeField] private GameObject weakPointVisualIndicator;
@@ -146,15 +149,8 @@ namespace Minigames.FortFight.Fort
private void CalculateHp()
{
// Get material config
var materialConfig = CachedSettings.GetMaterialConfig(material);
float baseMaterialHp = materialConfig?.baseHp ?? 20f;
// Get size config
var sizeConfig = CachedSettings.GetSizeConfig(size);
float sizeMultiplier = sizeConfig?.hpMultiplier ?? 1f;
maxHp = baseMaterialHp * sizeMultiplier;
// Use fixed block HP value (default 10)
maxHp = blockHp;
currentHp = 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 bool IsDefeated { get; private set; }
// Aliases for consistency
public float MaxHp => maxFortHp;
public float CurrentHp => currentFortHp;
#endregion
#region Private State
@@ -171,6 +175,9 @@ namespace Minigames.FortFight.Fort
RegisterBlock(block);
}
// Step 3: Initialize current HP to match max HP (sum of all blocks)
currentFortHp = maxFortHp;
initialBlockCount = blocks.Count;
Logging.Debug($"[FortController] {fortName} - Initialized and registered {blocks.Count} blocks, Total HP: {maxFortHp:F0}");
}
@@ -181,7 +188,7 @@ namespace Minigames.FortFight.Fort
/// </summary>
private void RegisterWithManager()
{
Core.FortManager manager = FindFirstObjectByType<Core.FortManager>();
Core.FortManager manager = Core.FortManager.Instance;
if (manager == null)
{
@@ -207,8 +214,9 @@ namespace Minigames.FortFight.Fort
if (!blocks.Contains(block))
{
blocks.Add(block);
// Only add to max HP, current HP will be calculated once at end of initialization
maxFortHp += block.MaxHp;
currentFortHp += block.MaxHp;
// Subscribe to block events
block.OnBlockDestroyed += HandleBlockDestroyed;
@@ -259,9 +267,8 @@ namespace Minigames.FortFight.Fort
// Remove from list
blocks.Remove(block);
// Update current HP
currentFortHp -= blockMaxHp;
currentFortHp = Mathf.Max(0f, currentFortHp);
// Recalculate HP by summing all remaining blocks (consistent calculation method)
RecalculateFortHp();
// Notify listeners
OnBlockDestroyed?.Invoke(block);
@@ -279,7 +286,38 @@ namespace Minigames.FortFight.Fort
private void HandleBlockDamaged(float currentBlockHp, float maxBlockHp)
{
// 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
@@ -288,17 +326,32 @@ namespace Minigames.FortFight.Fort
private void CheckDefeatCondition()
{
if (IsDefeated) return;
if (IsDefeated)
{
Logging.Debug($"[FortController] {fortName} - Already defeated, skipping check");
return;
}
float defeatThreshold = CachedSettings?.FortDefeatThreshold ?? 0.3f;
float defeatThresholdPercent = defeatThreshold * 100f;
// Defeat if HP below threshold
if (HpPercentage < defeatThresholdPercent)
Logging.Debug($"[FortController] {fortName} - Checking defeat: HP={currentFortHp:F1}/{maxFortHp:F1} ({HpPercentage:F1}%) vs threshold={defeatThresholdPercent:F1}%");
// Defeat if HP at or below threshold
if (HpPercentage <= defeatThresholdPercent)
{
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();
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 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
#region Components
@@ -181,6 +188,41 @@ namespace Minigames.FortFight.Projectiles
// Fire event
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
@@ -305,6 +347,13 @@ namespace Minigames.FortFight.Projectiles
{
Logging.Debug($"[ProjectileBase] Destroying {gameObject.name}");
// Stop timeout coroutine if running
if (timeoutCoroutine != null)
{
StopCoroutine(timeoutCoroutine);
timeoutCoroutine = null;
}
// Fire destroyed event
OnDestroyed?.Invoke(this);

View File

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

View File

@@ -31,6 +31,12 @@ namespace Minigames.FortFight.UI
[Tooltip("Leave empty to auto-find")]
[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
#region Private State
@@ -45,11 +51,17 @@ namespace Minigames.FortFight.UI
{
base.OnManagedStart();
Logging.Debug($"[FortHealthUI] OnManagedStart - autoBindToFort: {autoBindToFort}, isPlayerFort: {isPlayerFort}");
// Auto-bind to dynamically spawned forts
if (autoBindToFort)
{
SetupAutoBinding();
}
else
{
Logging.Warning($"[FortHealthUI] Auto-bind disabled! HP UI will not update.");
}
}
internal override void OnManagedDestroy()
@@ -76,33 +88,53 @@ namespace Minigames.FortFight.UI
private void SetupAutoBinding()
{
// Find FortManager if not assigned
if (fortManager == null)
{
fortManager = FindFirstObjectByType<Core.FortManager>();
}
Logging.Debug($"[FortHealthUI] SetupAutoBinding called for {(isPlayerFort ? "PLAYER" : "ENEMY")} fort");
// Get FortManager via singleton
fortManager = Core.FortManager.Instance;
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;
}
// 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)
{
fortManager.OnPlayerFortSpawned += OnFortSpawned;
Logging.Debug($"[FortHealthUI] Subscribed to PLAYER fort spawn");
Logging.Debug($"[FortHealthUI] Subscribed to OnPlayerFortSpawned event");
}
else
{
fortManager.OnEnemyFortSpawned += OnFortSpawned;
Logging.Debug($"[FortHealthUI] Subscribed to ENEMY fort spawn");
Logging.Debug($"[FortHealthUI] Subscribed to OnEnemyFortSpawned event");
}
}
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);
}
@@ -140,7 +172,7 @@ namespace Minigames.FortFight.UI
UpdateDisplay();
Logging.Debug($"[FortHealthUI] Bound to fort: {fort.FortName}");
Logging.Debug($"[FortHealthUI] Bound to fort: {fort.FortName}. Event subscription successful.");
}
#endregion
@@ -149,6 +181,7 @@ namespace Minigames.FortFight.UI
private void OnFortDamaged(float damage, float hpPercentage)
{
Logging.Debug($"[FortHealthUI] OnFortDamaged received! Damage: {damage}, HP%: {hpPercentage:F1}%, Fort: {trackedFort?.FortName}");
UpdateDisplay();
}
@@ -158,20 +191,52 @@ namespace Minigames.FortFight.UI
private void UpdateDisplay()
{
if (trackedFort == null) return;
if (trackedFort == null)
{
Logging.Warning("[FortHealthUI] UpdateDisplay called but trackedFort is null!");
return;
}
float hpPercent = trackedFort.HpPercentage;
Logging.Debug($"[FortHealthUI] UpdateDisplay - Fort: {trackedFort.FortName}, HP: {hpPercent:F1}%");
// Update slider
if (hpSlider != null)
{
hpSlider.value = hpPercent / 100f;
Logging.Debug($"[FortHealthUI] Slider updated to {hpSlider.value:F2}");
}
else
{
Logging.Warning("[FortHealthUI] hpSlider is null!");
}
// Update text
if (hpPercentageText != null)
{
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

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