using UnityEngine; using System; using System.Collections; using GameFramework; using Architecture.Core; using Architecture.Variables; namespace IndianOceanAssets.Engine2_5D { // Handles health, damage, and death for entities public class HealthSystem : MonoBehaviour { [Header("SO 事件通道(替代 static onPlayerDamaged 事件)")] [SerializeField] private GameEvent onPlayerDamagedEvent; [Header("SO 事件通道(玩家死亡,替代直接调用 GameManager.GameOver)")] [SerializeField] private GameEvent onPlayerDiedEvent; [Header("SO 变量(供 EnemyHealthBar / HUD 订阅,替代反射读取私有字段)")] [SerializeField] private IntVariable healthVar; [SerializeField] private IntVariable maxHealthVar; [Range(1, 100)] [SerializeField] private int maxHealth = 5; // Maximum health private int health = 5; // Current health [SerializeField] private GameObject deathEffect; // Effect prefab on death [SerializeField] private bool isPlayer; // Is this the player? [Header("消隐特效")] [Tooltip("死亡消隐使用的溶解材质(Custom/SpriteDissolve shader)")] [SerializeField] private Material dissolveMaterial; [Tooltip("消隐动画持续时间(秒)")] [SerializeField] private float fadeDuration = 2f; private bool _isDead = false; // 防止重复触发死亡 private bool _isInvincible = false; // 无敌状态 private float _invincibleTimer = 0f; // 无敌剩余时间(<=0 表示永久无敌) private bool _warnedDamaged = false; // 防止 onPlayerDamagedEvent 空引用告警刷屏 [Header("受伤无敌")] [Tooltip("玩家受伤后的无敌时间(秒)")] [SerializeField] private float damageInvincibleDuration = 2f; /// 是否已死亡(只读) public bool IsDead => _isDead; /// 是否处于无敌状态(只读) public bool IsInvincible => _isInvincible; /// 当前血量(只读) public int CurrentHealth => health; /// 最大血量(只读) public int MaxHealth => maxHealth; /// 血量变化事件(current, max)。敌人血条等本地订阅者用于刷新显示,替代反射读取私有字段。 public event System.Action OnHealthChanged; // 编辑器实时校验:事件字段未接线时给出黄色警告三角 + 控制台告警 private void OnValidate() { if (isPlayer && onPlayerDamagedEvent == null) Debug.LogWarning($"[HealthSystem] 玩家 HealthSystem 的 On Player Damaged Event 未接线({gameObject.name})。受击泛红不会出现。", this); if (isPlayer && onPlayerDiedEvent == null) Debug.LogWarning($"[HealthSystem] 玩家 HealthSystem 的 On Player Died Event 未接线({gameObject.name})。玩家死亡不会触发过场。", this); } // Initializes health private void Start() { health = maxHealth; if (isPlayer) { if (maxHealthVar != null) maxHealthVar.Value = maxHealth; if (healthVar != null) healthVar.Value = health; } } // Applies damage and checks for death public void Damage(int damageAmount) { if (_isDead) return; // 已死亡,不再处理 if (_isInvincible) return; // 无敌状态,免疫伤害 health -= damageAmount; if (isPlayer && healthVar != null) healthVar.Value = health; OnHealthChanged?.Invoke(health, maxHealth); // 玩家受伤时触发事件(用于受击泛红特效) if (isPlayer) { if (onPlayerDamagedEvent != null) onPlayerDamagedEvent.Raise(); else if (!_warnedDamaged) { Debug.LogWarning("[HealthSystem] onPlayerDamagedEvent 未接线!受击泛红特效不会触发。请在玩家 HealthSystem 的 On Player Damaged Event 字段拖入 OnPlayerDamaged 资产。", this); _warnedDamaged = true; } } // 播放受伤音效 if (isPlayer && AudioManager.Instance != null) AudioManager.Instance.PlaySFXFromResources("SFX/Hurt", 0.7f); // 玩家受伤后进入短暂无敌(防止连续掉血) if (isPlayer && !_isDead) MakeInvincible(damageInvincibleDuration); // If dead if (health <= 0) { _isDead = true; if (isPlayer) { // 玩家死亡:通过 OnPlayerDied SO 事件通知 GameManager(解耦,不再直接调用单例) onPlayerDiedEvent?.Raise(); } else { // 非玩家:正常播放死亡特效并销毁 Die(); } } } /// /// 进入无敌状态。 /// duration <= 0 表示永久无敌(直到被外部取消)。 /// duration > 0 表示限时无敌,到期后自动解除。 /// public void MakeInvincible(float duration = 0f) { _isInvincible = true; if (duration <= 0f) { // 永久无敌 _invincibleTimer = -1f; StopCoroutine(nameof(InvincibleCountdown)); } else { // 限时无敌:刷新计时器 _invincibleTimer = duration; StopCoroutine(nameof(InvincibleCountdown)); StartCoroutine(InvincibleCountdown()); } } /// 限时无敌倒计时协程 private IEnumerator InvincibleCountdown() { while (_invincibleTimer > 0f) { _invincibleTimer -= Time.deltaTime; yield return null; } _isInvincible = false; } // Handles death logic and effects public void Die() { Debug.Log($"[HealthSystem] Die() called on {gameObject.name}, dissolveMaterial={(dissolveMaterial != null ? dissolveMaterial.name : "NULL")}"); if (deathEffect != null) Instantiate(deathEffect, transform.position + new Vector3(0f, .5f, 0f), Quaternion.identity); // 如果有溶解材质,播放消隐动画后再销毁 if (dissolveMaterial != null) { Debug.Log($"[HealthSystem] Starting DissolveAndDestroy coroutine on {gameObject.name}"); StartCoroutine(DissolveAndDestroy()); } else { Debug.Log($"[HealthSystem] No dissolve material, destroying {gameObject.name} immediately"); Destroy(gameObject); } } /// /// 溶解消隐协程:切换材质 → 禁用碰撞/AI → 动画溶解 → 销毁。 /// private IEnumerator DissolveAndDestroy() { Debug.Log($"[HealthSystem] DissolveAndDestroy START on {gameObject.name}"); // 禁用碰撞体和刚体,防止死亡过程中仍能触发碰撞或受重力下坠 foreach (var col in GetComponents()) col.enabled = false; foreach (var col in GetComponents()) col.enabled = false; foreach (var rb in GetComponents()) rb.isKinematic = true; foreach (var rb in GetComponents()) rb.isKinematic = true; // 禁用所有 MonoBehaviour(AI、动画等),防止死亡后仍移动/攻击 foreach (var mb in GetComponents()) { if (mb != this) mb.enabled = false; } // 等一帧,确保禁用生效 yield return null; // 切换所有 SpriteRenderer 到溶解材质(保留原始纹理和颜色) var renderers = GetComponentsInChildren(); Debug.Log($"[HealthSystem] DissolveAndDestroy: {renderers.Length} SpriteRenderers on {gameObject.name}"); for (int i = 0; i < renderers.Length; i++) { // 创建溶解材质实例,并复制原始纹理 + 颜色 var dissolveMat = new Material(dissolveMaterial); if (renderers[i].sprite != null) dissolveMat.mainTexture = renderers[i].sprite.texture; dissolveMat.SetColor("_Color", renderers[i].color); dissolveMat.SetFloat("_DissolveAmount", 0f); renderers[i].material = dissolveMat; } // 再等一帧,确保材质切换生效 yield return null; Debug.Log($"[HealthSystem] Starting dissolve animation, fadeDuration={fadeDuration}"); // 动画溶解 float elapsed = 0f; while (elapsed < fadeDuration) { elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / fadeDuration); for (int i = 0; i < renderers.Length; i++) { if (renderers[i] != null && renderers[i].material != null) renderers[i].material.SetFloat("_DissolveAmount", t); } yield return null; } Debug.Log($"[HealthSystem] Dissolve complete, destroying {gameObject.name}"); Destroy(gameObject); } } }