Files
bansonic_beta_main/Assets/scripts/Combat/EnemyCombatant.cs
T

297 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using UnityEngine;
// EnemyCombatant ǵϵ˵ʱ֣е˹ͬһ GameObject˳dz
// ʵ 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 = UnityEngine.Object.FindAnyObjectByType<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 = UnityEngine.Object.FindObjectsByType<EnemyCombatant>(FindObjectsInactive.Include, FindObjectsSortMode.None);
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 = UnityEngine.Object.FindAnyObjectByType<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);
}
}