297 lines
10 KiB
C#
297 lines
10 KiB
C#
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<Buff> activeBuffs = new List<Buff>();
|
|
|
|
// Events to notify manager/UI
|
|
public event Action<EnemyCombatant> OnEnemyDied;
|
|
public event Action<EnemyCombatant> 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<string, float>();
|
|
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<SkillBuilder>();
|
|
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<EnemyCombatant>(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<SkillBuilder>();
|
|
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);
|
|
}
|
|
}
|