using System; using Unity.Netcode; using UnityEngine; namespace Northbound { public abstract class DamageableNetworkBehaviour : NetworkBehaviour, IDamageable { [Header("Health Settings")] [SerializeField] protected int maxHealth = 100; [SerializeField] protected bool showHealthBar = true; [Header("Visual Effects")] [SerializeField] protected GameObject damageEffectPrefab; [SerializeField] protected GameObject destroyEffectPrefab; [SerializeField] protected Transform effectSpawnPoint; protected NetworkVariable _currentHealth = new NetworkVariable( 0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); public event Action OnHealthChanged; public event Action OnDestroyed; protected void InvokeOnHealthChanged(int currentHealth, int maxHealth) { OnHealthChanged?.Invoke(currentHealth, maxHealth); } protected void InvokeOnDestroyed() { OnDestroyed?.Invoke(); } public override void OnNetworkSpawn() { base.OnNetworkSpawn(); if (IsOwner) { InitializeHealthServerRpc(maxHealth); } _currentHealth.OnValueChanged += OnHealthValueChanged; InitializeHealthBar(); UpdateHealthUI(); } [ServerRpc] private void InitializeHealthServerRpc(int health) { if (_currentHealth.Value == 0) _currentHealth.Value = health; } public override void OnNetworkDespawn() { _currentHealth.OnValueChanged -= OnHealthValueChanged; base.OnNetworkDespawn(); } protected virtual void InitializeHealthBar() { } protected virtual void UpdateHealthUI() { } public virtual void TakeDamage(int damage, ulong attackerId) { if (!IsOwner) { TakeDamageServerRpc(damage, attackerId); return; } TakeDamageServerRpc(damage, attackerId); } [ServerRpc] private void TakeDamageServerRpc(int damage, ulong attackerId) { if (_currentHealth.Value <= 0) return; int actualDamage = Mathf.Min(damage, _currentHealth.Value); _currentHealth.Value -= actualDamage; Debug.Log($"[{GetType().Name}] {gameObject.name} received {actualDamage} damage. Health: {_currentHealth.Value}/{maxHealth}"); ShowDamageEffectClientRpc(); if (_currentHealth.Value <= 0) { Die(attackerId); } } protected virtual void Die(ulong killerId) { Debug.Log($"[{GetType().Name}] {gameObject.name} destroyed! Killer: {killerId}"); OnDestroyed?.Invoke(); ShowDeathEffectClientRpc(); } [ClientRpc] protected void ShowDamageEffectClientRpc() { if (damageEffectPrefab != null) { Transform spawnPoint = effectSpawnPoint != null ? effectSpawnPoint : transform; GameObject effect = Instantiate(damageEffectPrefab, spawnPoint.position + Vector3.up, Quaternion.identity); Destroy(effect, 2f); } } [ClientRpc] protected void ShowDeathEffectClientRpc() { if (destroyEffectPrefab != null) { Transform spawnPoint = effectSpawnPoint != null ? effectSpawnPoint : transform; GameObject effect = Instantiate(destroyEffectPrefab, spawnPoint.position, Quaternion.identity); Destroy(effect, 3f); } } protected virtual void OnHealthValueChanged(int previousValue, int newValue) { OnHealthChanged?.Invoke(newValue, maxHealth); UpdateHealthUI(); } public int GetCurrentHealth() => _currentHealth.Value; public int GetMaxHealth() => maxHealth; public float GetHealthPercentage() => maxHealth > 0 ? (float)_currentHealth.Value / maxHealth : 0f; public bool IsDead() => _currentHealth.Value <= 0; public virtual void Heal(int amount) { if (!IsOwner) { HealServerRpc(amount); return; } HealServerRpc(amount); } [ServerRpc] private void HealServerRpc(int amount) { int healAmount = Mathf.Min(amount, maxHealth - _currentHealth.Value); _currentHealth.Value += healAmount; Debug.Log($"[{GetType().Name}] {gameObject.name} healed {healAmount}. Health: {_currentHealth.Value}/{maxHealth}"); } } }