using System; using System.Collections.Generic; using UnityEngine; // EnemyCombatant 是单个场上敌人的运行时表现(所有敌人共用同一个 GameObject,按顺序登场) // 实现 ICombatant:被 EffectSystem/SkillBuilder 调用 public class EnemyCombatant : MonoBehaviour, ICombatant { public EnemyData_SO sourceData; // runtime stats public int maxHP = 100; public int currentHP = 100; public int maxMana = 0; public int currentMana = 0; public float damageResistance = 0f; public int attack = 0; private List activeBuffs = new List(); // Events to notify manager/UI public event Action OnEnemyDied; public event Action OnEnemyRevived; private bool isDead = false; private void Awake() { // Ensure consistent name so EffectSystem can find it gameObject.name = "thisEnemy"; } // 初始化/复活为指定的 EnemyData_SO public void InitializeFromSO(EnemyData_SO so) { sourceData = so; if (so != null) { maxHP = Mathf.Max(1, so.enemy_maxHP); damageResistance = so.enemy_damageResistance; attack = so.enemy_baseAttack; maxMana = Mathf.Max(0, so.GetEffectiveMaxMana()); } else { maxHP = 100; damageResistance = 0f; attack = 0; maxMana = 0; } // revive currentHP = maxHP; currentMana = 0; isDead = false; // notify listeners (UI/manager) that enemy revived OnEnemyRevived?.Invoke(this); // Notify SkillBuilder so ally skills with OnEnemyRevive can react if (Application.isPlaying && SkillBuilder.Instance != null) { SkillBuilder.Instance.TriggerOnEnemyRevive(this); } // Trigger primary skill on revive if defined and trigger condition matches TryTriggerPrimarySkill(SkillDefinition.SkillTrigger.OnEnemyRevive); } // 尝试触发 primary skill,当触发条件匹配时调用 SkillBuilder private void TryTriggerPrimarySkill(SkillDefinition.SkillTrigger when) { if (sourceData == null) return; var def = sourceData.GetPrimarySkill(); if (def == null) return; if (def.triggerCondition != when) return; // Evaluate formula into amount using enemy fields as variables float perTick = 0f; var vars = new Dictionary(); vars["maxHP"] = maxHP; vars["maxMana"] = maxMana; vars["damageResistance"] = damageResistance; vars["attack"] = attack; vars["enemy_currentHP"] = currentHP; if (!string.IsNullOrWhiteSpace(def.formula)) { if (!SkillDefinition.TryEvaluateFormula(def.formula, vars, out float result)) perTick = 0f; else perTick = result; } float totalAmount = perTick; if (!def.IsSingleInstance) { float tickInterval = def.GetEffectiveTickInterval(); int ticks = Mathf.Max(1, Mathf.CeilToInt(def.defaultDuration / tickInterval)); totalAmount = perTick * ticks; } // Use SkillBuilder to execute effect, using this Enemy GameObject as caster if (SkillBuilder.Instance != null) { SkillBuilder.Instance.ExecuteSkill(def.displayName ?? def.skillId, def.effectType, totalAmount, def.defaultSelector, this.gameObject, null, def.defaultDuration, def.defaultTickInterval); } } public void ReceiveDamage(float amount, GameObject source) { if (isDead) return; float effective = amount * (1f - damageResistance); int delta = Mathf.CeilToInt(effective); currentHP = Mathf.Clamp(currentHP - delta, 0, maxHP); // grant mana based on sourceData setting if (sourceData != null && sourceData.manaGainOnDamagePercent > 0f && maxMana > 0) { float manaGainRaw = effective * sourceData.manaGainOnDamagePercent; int manaGain = Mathf.CeilToInt(manaGainRaw); ModifyMana(manaGain, true); } if (currentHP <= 0) { Die(); } } public void ReceiveHeal(float amount, GameObject source) { if (isDead) return; int delta = Mathf.CeilToInt(amount); currentHP = Mathf.Clamp(currentHP + delta, 0, maxHP); } public void ApplyBuff(Buff buff, GameObject source) { if (buff == null) return; activeBuffs.Add(buff); // Simple apply: only modify healReceivedMultiplier or attackMultiplier as example if (buff.attackMultiplier != 1f) attack = Mathf.RoundToInt(attack * buff.attackMultiplier); } public void RemoveBuff(string buffId) { var b = activeBuffs.Find(x => x.buffId == buffId); if (b != null) { activeBuffs.Remove(b); } } public void ModifyHP(int delta) { if (isDead && delta > 0) return; currentHP = Mathf.Clamp(currentHP + delta, 0, maxHP); if (currentHP <= 0) Die(); } public void ModifyMana(int delta, bool animate = true) { if (maxMana <= 0) return; int old = currentMana; currentMana = Mathf.Clamp(currentMana + delta, 0, maxMana); // If mana reached full, trigger primary skill and reset mana if (currentMana >= maxMana && maxMana > 0 && Application.isPlaying) { Debug.Log($"[EnemyCombatant] Mana full for {gameObject.name} (slot) - triggering OnManaFull"); try { // Trigger primary skill configured for OnManaFull TryTriggerPrimarySkill(SkillDefinition.SkillTrigger.OnManaFull); } catch (Exception ex) { Debug.LogError($"[EnemyCombatant] TryTriggerPrimarySkill threw: {ex}"); } // reset mana after casting currentMana = 0; } } private void Die() { if (isDead) return; // Notify SkillBuilder so allies can react to each enemy death BEFORE listeners that may reuse this Enemy instance if (Application.isPlaying) { // prefer singleton instance but fallback to scene lookup if singleton is null (robustness for scene initialization order) if (SkillBuilder.Instance != null) { SkillBuilder.Instance.TriggerOnEnemyDead(this); } else { var sb = FindObjectOfType(); if (sb != null) { Debug.LogWarning("[EnemyCombatant] SkillBuilder.Instance was null on Die(); using FindObjectOfType fallback"); sb.TriggerOnEnemyDead(this); } else { Debug.LogWarning("[EnemyCombatant] Die(): no SkillBuilder found to notify TriggerOnEnemyDead"); } } } // Trigger primary skill on death if configured (enemy's own skill) TryTriggerPrimarySkill(SkillDefinition.SkillTrigger.OnEnemyDead); // notify listeners (UI/manager) that enemy died AFTER triggering skills so they operate on the current instance OnEnemyDied?.Invoke(this); // Check whether all enemies are now defeated. Use currentHP/IsDead to determine status. if (Application.isPlaying) { try { var all = GameObject.FindObjectsOfType(true); bool anyAlive = false; foreach (var e in all) { if (e == null) continue; // treat an enemy as alive if its currentHP > 0 and not flagged dead if (e.currentHP > 0 && !e.IsDead) { anyAlive = true; break; } } if (!anyAlive) { // notify SkillBuilder about all enemies defeated if (SkillBuilder.Instance != null) { SkillBuilder.Instance.TriggerOnAllEnemiesDefeated(); Debug.Log("[EnemyCombatant] All enemies defeated -> TriggerOnAllEnemiesDefeated fired via singleton"); } else { var sb = FindObjectOfType(); if (sb != null) { sb.TriggerOnAllEnemiesDefeated(); Debug.LogWarning("[EnemyCombatant] SkillBuilder.Instance was null when firing AllEnemiesDefeated; used FindObjectOfType fallback"); } else { Debug.LogWarning("[EnemyCombatant] All enemies defeated but no SkillBuilder found to notify"); } } } } catch (Exception ex) { Debug.LogError($"[EnemyCombatant] Error while checking all-enemies-defeated: {ex}"); } } // Finally mark dead so further damage/heal is ignored isDead = true; } // Utility: expose a read-only status public bool IsDead => isDead; // New: allow EffectSystem/SkillBuilder to adjust enemy max HP safely public void SetMaxHP(int newMax, bool keepCurrentRatio = true) { if (newMax < 1) newMax = 1; float ratio = maxHP > 0 ? (float)currentHP / maxHP : 1f; maxHP = newMax; if (keepCurrentRatio) currentHP = Mathf.Clamp(Mathf.RoundToInt(ratio * maxHP), 0, maxHP); else currentHP = Mathf.Clamp(currentHP, 0, maxHP); } // New: allow adjusting enemy max mana (mirror of AllyCombatant) public void SetMaxMana(int newMax, bool keepCurrentRatio = true) { if (newMax < 1) newMax = 1; float ratio = maxMana > 0 ? (float)currentMana / maxMana : 1f; maxMana = newMax; if (keepCurrentRatio) currentMana = Mathf.Clamp(Mathf.RoundToInt(ratio * maxMana), 0, maxMana); else currentMana = Mathf.Clamp(currentMana, 0, maxMana); } }