Implement Fort Fight minigame (#75)
Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: #75
This commit is contained in:
414
Assets/Scripts/Minigames/FortFight/Fort/FortBlock.cs
Normal file
414
Assets/Scripts/Minigames/FortFight/Fort/FortBlock.cs
Normal file
@@ -0,0 +1,414 @@
|
||||
using System;
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
using UnityEngine;
|
||||
using Minigames.FortFight.Data;
|
||||
|
||||
namespace Minigames.FortFight.Fort
|
||||
{
|
||||
/// <summary>
|
||||
/// Individual fort block with HP, material properties, and physics.
|
||||
/// Component attached to each block GameObject in a fort prefab.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Rigidbody2D), typeof(Collider2D))]
|
||||
public class FortBlock : ManagedBehaviour
|
||||
{
|
||||
#region Inspector Properties
|
||||
|
||||
[Header("Block Configuration")]
|
||||
[SerializeField] private BlockMaterial material = BlockMaterial.Cardboard;
|
||||
[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;
|
||||
[Tooltip("Visual explosion effect prefab")]
|
||||
[SerializeField] private GameObject explosionEffectPrefab;
|
||||
|
||||
[Header("Visual Feedback")]
|
||||
[SerializeField] private SpriteRenderer spriteRenderer;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Fired when this block is destroyed. Parameters: (FortBlock block, float damageTaken)
|
||||
/// </summary>
|
||||
public event Action<FortBlock, float> OnBlockDestroyed;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when this block takes damage. Parameters: (float currentHP, float maxHP)
|
||||
/// </summary>
|
||||
public event Action<float, float> OnBlockDamaged;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
public BlockMaterial Material => material;
|
||||
public BlockSize Size => size;
|
||||
public bool IsWeakPoint => isWeakPoint;
|
||||
public float CurrentHp => currentHp;
|
||||
public float MaxHp => maxHp;
|
||||
public float HpPercentage => maxHp > 0 ? (currentHp / maxHp) * 100f : 0f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private State
|
||||
|
||||
private float maxHp;
|
||||
private float currentHp;
|
||||
private FortController parentFort;
|
||||
private Rigidbody2D rb2D;
|
||||
private Collider2D blockCollider;
|
||||
private bool isDestroyed = false;
|
||||
|
||||
// Cached settings
|
||||
private AppleHills.Core.Settings.IFortFightSettings _cachedSettings;
|
||||
private AppleHills.Core.Settings.IFortFightSettings CachedSettings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_cachedSettings == null)
|
||||
{
|
||||
_cachedSettings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IFortFightSettings>();
|
||||
}
|
||||
return _cachedSettings;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lifecycle
|
||||
|
||||
internal override void OnManagedDestroy()
|
||||
{
|
||||
base.OnManagedDestroy();
|
||||
|
||||
// Unsubscribe events
|
||||
OnBlockDestroyed = null;
|
||||
OnBlockDamaged = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Initialization
|
||||
|
||||
/// <summary>
|
||||
/// Initialize this block. Called explicitly by parent FortController.
|
||||
/// DO NOT call from Awake/Start - parent controls initialization timing.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
// Automatically assign block to correct layer from settings
|
||||
var settings = CachedSettings;
|
||||
if (settings != null && settings.FortBlockLayer >= 0 && gameObject.layer != settings.FortBlockLayer)
|
||||
{
|
||||
gameObject.layer = settings.FortBlockLayer;
|
||||
Logging.Debug($"[FortBlock] Assigned {gameObject.name} to layer {LayerMask.LayerToName(settings.FortBlockLayer)}");
|
||||
}
|
||||
|
||||
// Cache components
|
||||
rb2D = GetComponent<Rigidbody2D>();
|
||||
blockCollider = GetComponent<Collider2D>();
|
||||
|
||||
if (spriteRenderer == null)
|
||||
{
|
||||
spriteRenderer = GetComponent<SpriteRenderer>();
|
||||
}
|
||||
|
||||
if (isDestroyed)
|
||||
{
|
||||
Logging.Warning($"[FortBlock] Cannot initialize destroyed block {gameObject.name}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate HP based on material and size
|
||||
CalculateHp();
|
||||
|
||||
// Configure physics properties
|
||||
ConfigurePhysics();
|
||||
|
||||
// Show/hide weak point indicator
|
||||
if (weakPointVisualIndicator != null)
|
||||
{
|
||||
weakPointVisualIndicator.SetActive(isWeakPoint);
|
||||
}
|
||||
|
||||
Logging.Debug($"[FortBlock] {gameObject.name} initialized: {material} {size}, HP: {maxHp}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region HP Calculation
|
||||
|
||||
private void CalculateHp()
|
||||
{
|
||||
// Use fixed block HP value (default 10)
|
||||
maxHp = blockHp;
|
||||
currentHp = maxHp;
|
||||
|
||||
Logging.Debug($"[FortBlock] {gameObject.name} initialized: {material} {size}, HP: {maxHp}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Physics Configuration
|
||||
|
||||
private void ConfigurePhysics()
|
||||
{
|
||||
if (rb2D == null) return;
|
||||
|
||||
// Get material config
|
||||
var materialConfig = CachedSettings.GetMaterialConfig(material);
|
||||
float baseMass = materialConfig?.baseMass ?? 1f;
|
||||
|
||||
// Get size config
|
||||
var sizeConfig = CachedSettings.GetSizeConfig(size);
|
||||
float sizeMultiplier = sizeConfig?.massMultiplier ?? 1f;
|
||||
|
||||
rb2D.mass = baseMass * sizeMultiplier;
|
||||
rb2D.gravityScale = CachedSettings.PhysicsGravityScale;
|
||||
rb2D.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Damage System
|
||||
|
||||
/// <summary>
|
||||
/// Apply damage to this block
|
||||
/// </summary>
|
||||
public void TakeDamage(float damage)
|
||||
{
|
||||
if (isDestroyed) return;
|
||||
|
||||
currentHp -= damage;
|
||||
currentHp = Mathf.Max(0f, currentHp);
|
||||
|
||||
Logging.Debug($"[FortBlock] {gameObject.name} took {damage} damage. HP: {currentHp}/{maxHp} ({HpPercentage:F1}%)");
|
||||
|
||||
OnBlockDamaged?.Invoke(currentHp, maxHp);
|
||||
|
||||
// Visual feedback
|
||||
UpdateVisualDamage();
|
||||
|
||||
// Check if destroyed
|
||||
if (currentHp <= 0f)
|
||||
{
|
||||
DestroyBlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateVisualDamage()
|
||||
{
|
||||
if (spriteRenderer == null) return;
|
||||
|
||||
var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IFortFightSettings>();
|
||||
Color targetColor = settings?.DamageColorTint ?? new Color(0.5f, 0.5f, 0.5f);
|
||||
|
||||
// Darken sprite based on damage
|
||||
float damagePercent = 1f - (currentHp / maxHp);
|
||||
Color damageColor = Color.Lerp(Color.white, targetColor, damagePercent);
|
||||
spriteRenderer.color = damageColor;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Destruction
|
||||
|
||||
private void DestroyBlock()
|
||||
{
|
||||
if (isDestroyed) return;
|
||||
isDestroyed = true;
|
||||
|
||||
Logging.Debug($"[FortBlock] {gameObject.name} destroyed! Weak point: {isWeakPoint}");
|
||||
|
||||
// Trigger explosion if weak point
|
||||
if (isWeakPoint)
|
||||
{
|
||||
TriggerWeakPointExplosion();
|
||||
}
|
||||
|
||||
// Notify listeners
|
||||
OnBlockDestroyed?.Invoke(this, maxHp);
|
||||
|
||||
// Spawn destruction effects (placeholder)
|
||||
SpawnDestructionEffect();
|
||||
|
||||
// Destroy GameObject
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
private void TriggerWeakPointExplosion()
|
||||
{
|
||||
float explosionRadius = CachedSettings.WeakPointExplosionRadius;
|
||||
float explosionDamage = CachedSettings.WeakPointExplosionDamage;
|
||||
float explosionForce = CachedSettings.WeakPointExplosionForce;
|
||||
|
||||
Logging.Debug($"[FortBlock] ========================================");
|
||||
Logging.Debug($"[FortBlock] 💥 WEAK POINT EXPLOSION TRIGGERED!");
|
||||
Logging.Debug($"[FortBlock] Position: {transform.position}");
|
||||
Logging.Debug($"[FortBlock] Explosion Radius: {explosionRadius}");
|
||||
Logging.Debug($"[FortBlock] Explosion Damage: {explosionDamage}");
|
||||
Logging.Debug($"[FortBlock] Explosion Force: {explosionForce}");
|
||||
Logging.Debug($"[FortBlock] ========================================");
|
||||
|
||||
// Spawn explosion effect
|
||||
if (explosionEffectPrefab != null)
|
||||
{
|
||||
Logging.Debug($"[FortBlock] Spawning explosion effect prefab");
|
||||
GameObject explosion = Instantiate(explosionEffectPrefab, transform.position, Quaternion.identity);
|
||||
|
||||
// Dynamically determine cleanup time from particle system
|
||||
float lifetime = GetEffectLifetime(explosion);
|
||||
Destroy(explosion, lifetime);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Debug($"[FortBlock] No explosion effect prefab (visual only)");
|
||||
}
|
||||
|
||||
// Find nearby blocks and damage them
|
||||
Collider2D[] nearbyColliders = Physics2D.OverlapCircleAll(transform.position, explosionRadius);
|
||||
|
||||
Logging.Debug($"[FortBlock] Physics2D.OverlapCircleAll found {nearbyColliders.Length} colliders");
|
||||
|
||||
if (nearbyColliders.Length <= 1)
|
||||
{
|
||||
Logging.Warning($"[FortBlock] ⚠️ Only found {nearbyColliders.Length} colliders! Other blocks need BoxCollider2D with 'Is Trigger' UNCHECKED");
|
||||
}
|
||||
|
||||
int blocksHit = 0;
|
||||
foreach (Collider2D col in nearbyColliders)
|
||||
{
|
||||
if (col.gameObject == gameObject)
|
||||
{
|
||||
Logging.Debug($"[FortBlock] Skipping self");
|
||||
continue; // Skip self
|
||||
}
|
||||
|
||||
Logging.Debug($"[FortBlock] Checking collider: {col.gameObject.name}");
|
||||
|
||||
FortBlock nearbyBlock = col.GetComponent<FortBlock>();
|
||||
if (nearbyBlock != null && !nearbyBlock.isDestroyed)
|
||||
{
|
||||
Vector2 explosionCenter = transform.position;
|
||||
float distance = Vector2.Distance(explosionCenter, nearbyBlock.transform.position);
|
||||
|
||||
// Calculate damage with falloff
|
||||
float damageFalloff = 1f - (distance / explosionRadius);
|
||||
float actualDamage = explosionDamage * damageFalloff;
|
||||
|
||||
// Apply damage
|
||||
nearbyBlock.TakeDamage(actualDamage);
|
||||
|
||||
// Apply explosion force (2D equivalent of AddExplosionForce)
|
||||
Rigidbody2D nearbyRb = nearbyBlock.GetComponent<Rigidbody2D>();
|
||||
if (nearbyRb != null)
|
||||
{
|
||||
ApplyExplosionForce2D(nearbyRb, explosionForce, explosionCenter, explosionRadius);
|
||||
Logging.Debug($"[FortBlock] ✓ HIT: {nearbyBlock.gameObject.name} - Damage: {actualDamage:F1}, Force applied from center");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Debug($"[FortBlock] ✓ HIT: {nearbyBlock.gameObject.name} - Damage: {actualDamage:F1} (no Rigidbody2D)");
|
||||
}
|
||||
|
||||
blocksHit++;
|
||||
}
|
||||
else if (nearbyBlock == null)
|
||||
{
|
||||
Logging.Debug($"[FortBlock] × MISS: {col.gameObject.name} has no FortBlock component");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Debug($"[FortBlock] × SKIP: {col.gameObject.name} already destroyed");
|
||||
}
|
||||
}
|
||||
|
||||
Logging.Debug($"[FortBlock] Explosion complete. Damaged {blocksHit} blocks");
|
||||
Logging.Debug($"[FortBlock] ========================================");
|
||||
|
||||
// TODO: Add screen shake effect
|
||||
// TODO: Play explosion sound via AudioManager
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply explosion force to a Rigidbody2D (2D equivalent of Rigidbody.AddExplosionForce).
|
||||
/// Force decreases with distance from explosion center.
|
||||
/// </summary>
|
||||
private void ApplyExplosionForce2D(Rigidbody2D rb, float force, Vector2 center, float radius)
|
||||
{
|
||||
Vector2 direction = (rb.position - center);
|
||||
float distance = direction.magnitude;
|
||||
|
||||
if (distance == 0f) return; // Avoid division by zero
|
||||
|
||||
// Normalize direction
|
||||
direction /= distance;
|
||||
|
||||
// Calculate force with linear falloff (like Unity's 3D AddExplosionForce)
|
||||
float forceMagnitude = force * (1f - (distance / radius));
|
||||
|
||||
// Apply force as impulse
|
||||
rb.AddForce(direction * forceMagnitude, ForceMode2D.Impulse);
|
||||
}
|
||||
|
||||
private void SpawnDestructionEffect()
|
||||
{
|
||||
// Placeholder for destruction particles
|
||||
// TODO: Create material-specific destruction effects
|
||||
Logging.Debug($"[FortBlock] Spawning destruction effect for {material} block");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the lifetime of an effect by reading particle system StartLifetime.
|
||||
/// Falls back to 3 seconds if no particle system found.
|
||||
/// </summary>
|
||||
private float GetEffectLifetime(GameObject effect)
|
||||
{
|
||||
// Try to read from ParticleSystem
|
||||
ParticleSystem ps = effect.GetComponent<ParticleSystem>();
|
||||
if (ps != null)
|
||||
{
|
||||
return ps.main.startLifetime.constantMax + 0.5f; // Add small buffer
|
||||
}
|
||||
|
||||
// Try to read from child particle systems
|
||||
ParticleSystem childPs = effect.GetComponentInChildren<ParticleSystem>();
|
||||
if (childPs != null)
|
||||
{
|
||||
return childPs.main.startLifetime.constantMax + 0.5f;
|
||||
}
|
||||
|
||||
// Fallback for non-particle effects
|
||||
return 3f;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Debug Helpers
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (isWeakPoint)
|
||||
{
|
||||
// Draw explosion radius in editor using settings
|
||||
float radius = AppleHills.SettingsAccess.GetWeakPointExplosionRadius();
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(transform.position, radius);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ace8ce8bea324389a9955e63081ccff7
|
||||
timeCreated: 1764591745
|
||||
379
Assets/Scripts/Minigames/FortFight/Fort/FortController.cs
Normal file
379
Assets/Scripts/Minigames/FortFight/Fort/FortController.cs
Normal file
@@ -0,0 +1,379 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
using UnityEngine;
|
||||
using AppleHills.Core.Settings;
|
||||
|
||||
namespace Minigames.FortFight.Fort
|
||||
{
|
||||
/// <summary>
|
||||
/// Root component of fort prefabs. Manages collection of child FortBlocks and tracks total HP.
|
||||
/// </summary>
|
||||
public class FortController : ManagedBehaviour
|
||||
{
|
||||
#region Inspector Properties
|
||||
|
||||
[Header("Fort Configuration")]
|
||||
[SerializeField] private string fortName = "Unnamed Fort";
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool showDebugInfo = true;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Fired when fort takes damage. Parameters: (float damage, float hpPercentage)
|
||||
/// </summary>
|
||||
public event Action<float, float> OnFortDamaged;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when fort is defeated (HP < 30%)
|
||||
/// </summary>
|
||||
public event Action OnFortDefeated;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a block is destroyed. Parameters: (FortBlock block)
|
||||
/// </summary>
|
||||
public event Action<FortBlock> OnBlockDestroyed;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
public string FortName => fortName;
|
||||
public float MaxFortHp => maxFortHp;
|
||||
public float CurrentFortHp => currentFortHp;
|
||||
public float HpPercentage => maxFortHp > 0 ? (currentFortHp / maxFortHp) * 100f : 0f;
|
||||
public int TotalBlockCount => blocks.Count;
|
||||
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
|
||||
|
||||
private List<FortBlock> blocks = new List<FortBlock>();
|
||||
private float maxFortHp = 0f;
|
||||
private float currentFortHp = 0f;
|
||||
private int initialBlockCount = 0;
|
||||
private bool isInitialized = false;
|
||||
|
||||
// Cached settings
|
||||
private IFortFightSettings _cachedSettings;
|
||||
private IFortFightSettings CachedSettings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_cachedSettings == null)
|
||||
{
|
||||
_cachedSettings = GameManager.GetSettingsObject<IFortFightSettings>();
|
||||
}
|
||||
return _cachedSettings;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lifecycle
|
||||
|
||||
internal override void OnManagedStart()
|
||||
{
|
||||
base.OnManagedStart();
|
||||
|
||||
// Self-initialize: discover blocks, register with manager
|
||||
InitializeFort();
|
||||
}
|
||||
|
||||
internal override void OnManagedDestroy()
|
||||
{
|
||||
base.OnManagedDestroy();
|
||||
|
||||
// Unsubscribe from all block events
|
||||
foreach (FortBlock block in blocks)
|
||||
{
|
||||
if (block != null)
|
||||
{
|
||||
block.OnBlockDestroyed -= HandleBlockDestroyed;
|
||||
block.OnBlockDamaged -= HandleBlockDamaged;
|
||||
}
|
||||
}
|
||||
|
||||
blocks.Clear();
|
||||
|
||||
// Clear events
|
||||
OnFortDamaged = null;
|
||||
OnFortDefeated = null;
|
||||
OnBlockDestroyed = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Initialization
|
||||
|
||||
/// <summary>
|
||||
/// Initialize fort: discover child blocks, calculate HP, register with manager.
|
||||
/// Called automatically in Start() - no external calls needed.
|
||||
/// </summary>
|
||||
private void InitializeFort()
|
||||
{
|
||||
if (isInitialized)
|
||||
{
|
||||
Logging.Warning($"[FortController] {fortName} already initialized!");
|
||||
return;
|
||||
}
|
||||
|
||||
Logging.Debug($"[FortController] {fortName} - Starting self-initialization");
|
||||
|
||||
// Step 1: Discover and register child blocks
|
||||
DiscoverAndRegisterBlocks();
|
||||
|
||||
// Step 2: Register with central manager
|
||||
RegisterWithManager();
|
||||
|
||||
isInitialized = true;
|
||||
Logging.Debug($"[FortController] {fortName} - Initialization complete");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discover, initialize, and register all child blocks.
|
||||
/// This ensures deterministic initialization order.
|
||||
/// </summary>
|
||||
private void DiscoverAndRegisterBlocks()
|
||||
{
|
||||
FortBlock[] childBlocks = GetComponentsInChildren<FortBlock>();
|
||||
|
||||
if (childBlocks.Length == 0)
|
||||
{
|
||||
Logging.Error($"[FortController] {fortName} has no blocks!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (childBlocks.Length > 10)
|
||||
{
|
||||
Logging.Warning($"[FortController] {fortName} has {childBlocks.Length} blocks (max 10 recommended)");
|
||||
}
|
||||
|
||||
Logging.Debug($"[FortController] {fortName} - Discovered {childBlocks.Length} blocks, initializing...");
|
||||
|
||||
// Step 1: Initialize each block (calculate HP, configure physics)
|
||||
foreach (FortBlock block in childBlocks)
|
||||
{
|
||||
block.Initialize();
|
||||
}
|
||||
|
||||
// Step 2: Register each block (subscribe to events, track HP)
|
||||
foreach (FortBlock block in childBlocks)
|
||||
{
|
||||
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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register this fort with the central FortManager.
|
||||
/// Manager determines if player/enemy and handles UI binding.
|
||||
/// </summary>
|
||||
private void RegisterWithManager()
|
||||
{
|
||||
Core.FortManager manager = Core.FortManager.Instance;
|
||||
|
||||
if (manager == null)
|
||||
{
|
||||
Logging.Error($"[FortController] {fortName} - FortManager not found! Cannot complete initialization.");
|
||||
return;
|
||||
}
|
||||
|
||||
manager.RegisterFort(this);
|
||||
Logging.Debug($"[FortController] {fortName} - Registered with FortManager");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Block Management
|
||||
|
||||
/// <summary>
|
||||
/// Register a block with this fort controller. Called by FortBlock on start.
|
||||
/// </summary>
|
||||
public void RegisterBlock(FortBlock block)
|
||||
{
|
||||
if (block == null) return;
|
||||
|
||||
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;
|
||||
|
||||
// Subscribe to block events
|
||||
block.OnBlockDestroyed += HandleBlockDestroyed;
|
||||
block.OnBlockDamaged += HandleBlockDamaged;
|
||||
|
||||
if (showDebugInfo)
|
||||
{
|
||||
Logging.Debug($"[FortController] Registered block: {block.gameObject.name} ({block.Material} {block.Size}, HP: {block.MaxHp})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all blocks marked as weak points
|
||||
/// </summary>
|
||||
public List<FortBlock> GetWeakPoints()
|
||||
{
|
||||
return blocks.Where(b => b != null && b.IsWeakPoint).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all remaining blocks
|
||||
/// </summary>
|
||||
public List<FortBlock> GetAllBlocks()
|
||||
{
|
||||
return new List<FortBlock>(blocks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a random block (for AI targeting)
|
||||
/// </summary>
|
||||
public FortBlock GetRandomBlock()
|
||||
{
|
||||
if (blocks.Count == 0) return null;
|
||||
return blocks[UnityEngine.Random.Range(0, blocks.Count)];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
private void HandleBlockDestroyed(FortBlock block, float blockMaxHp)
|
||||
{
|
||||
if (block == null) return;
|
||||
|
||||
Logging.Debug($"[FortController] {fortName} - Block destroyed: {block.gameObject.name}");
|
||||
|
||||
// Remove from list
|
||||
blocks.Remove(block);
|
||||
|
||||
// Recalculate HP by summing all remaining blocks (consistent calculation method)
|
||||
RecalculateFortHp();
|
||||
|
||||
// Notify listeners
|
||||
OnBlockDestroyed?.Invoke(block);
|
||||
OnFortDamaged?.Invoke(blockMaxHp, HpPercentage);
|
||||
|
||||
// Check defeat condition
|
||||
CheckDefeatCondition();
|
||||
|
||||
if (showDebugInfo)
|
||||
{
|
||||
Logging.Debug($"[FortController] {fortName} - HP: {currentFortHp:F0}/{maxFortHp:F0} ({HpPercentage:F1}%), Blocks: {blocks.Count}/{initialBlockCount}");
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleBlockDamaged(float currentBlockHp, float maxBlockHp)
|
||||
{
|
||||
// Block damaged but not destroyed
|
||||
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
|
||||
|
||||
#region Defeat Condition
|
||||
|
||||
private void CheckDefeatCondition()
|
||||
{
|
||||
if (IsDefeated)
|
||||
{
|
||||
Logging.Debug($"[FortController] {fortName} - Already defeated, skipping check");
|
||||
return;
|
||||
}
|
||||
|
||||
float defeatThreshold = CachedSettings?.FortDefeatThreshold ?? 0.3f;
|
||||
float defeatThresholdPercent = defeatThreshold * 100f;
|
||||
|
||||
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;
|
||||
|
||||
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}%)");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Debug Helpers
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
if (!showDebugInfo || !Application.isPlaying) return;
|
||||
|
||||
// Display fort HP in scene view (for testing)
|
||||
Vector3 screenPos = Camera.main.WorldToScreenPoint(transform.position + Vector3.up * 2f);
|
||||
if (screenPos.z > 0)
|
||||
{
|
||||
GUI.color = IsDefeated ? Color.red : Color.white;
|
||||
GUI.Label(new Rect(screenPos.x - 50, Screen.height - screenPos.y, 100, 30),
|
||||
$"{fortName}\nHP: {HpPercentage:F0}%");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 05031222348c421ab564757f52f24952
|
||||
timeCreated: 1764591745
|
||||
Reference in New Issue
Block a user