using System; using System.Collections; using System.Collections.Generic; using UnityEngine; /// /// EffectSystem 提供技能效果的统一分发接口。 /// - 选择器范围(Selector)决定目标集合(自己、全体友军、除自己外友军、相邻友军、场上敌人、所有人) /// - 效果类型(EffectType)定义了即时/持续的伤害/治疗/增益/减益等。 /// /// 目标实体应实现 ICombatant 接口以便 EffectSystem 能调用统一方法来处理伤害/治疗/增益等;如果没有实现,会在日志中提醒。 /// /// 该脚本为框架:不包含具体技能配置,只提供 ApplyEffect(...) 等方法供其他脚本调用。 /// public class EffectSystem : MonoBehaviour { public static EffectSystem Instance { get; private set; } private void Awake() { if (Instance == null) Instance = this; else Destroy(gameObject); } /// /// ApplyEffect: 对指定选择器范围内目标应用效果。 /// - selector: 目标选择器 /// - effectType: 效果类型(即时/持续等) /// - amount: 数值(伤害或治疗总量或效果强度) /// - duration: 持续时间(对持续效果或 Buff/Debuff 有效),即时效果可为 0 /// - source: 发起者(用于 Self/AlliesExceptSelf 判断或记录) /// - specificTarget: 当 selector 为 CurrentEnemies 且只想指定单体时可传入具体目标 /// - tickInterval: 持续效果的 tick 间隔(秒),默认 1 秒 /// public void ApplyEffect(Selector selector, EffectType effectType, float amount, float duration = 0f, GameObject source = null, GameObject specificTarget = null, float tickInterval = 1f) { List targets = ResolveTargets(selector, source, specificTarget); if (targets == null || targets.Count == 0) { Debug.LogWarning($"[EffectSystem] No targets resolved for selector={selector}"); return; } switch (effectType) { case EffectType.DamageSingleEnemy: // apply instantaneous damage to first applicable enemy in targets if (targets.Count > 0) ApplyInstantDamage(targets[0], amount, source); break; case EffectType.DamageOverTimeEnemy: foreach (var t in targets) StartCoroutine(ApplyDamageOverTimeCoroutine(t, amount, duration, tickInterval, source)); break; case EffectType.HealSingleEnemy: // heal single enemy (first target) if (targets.Count > 0) ApplyInstantHeal(targets[0], amount, source); break; case EffectType.HealOverTimeEnemy: foreach (var t in targets) StartCoroutine(ApplyHealOverTimeCoroutine(t, amount, duration, tickInterval, source)); break; case EffectType.DamageSingleAlly: // damage a single ally (first target) if (targets.Count > 0) ApplyInstantDamage(targets[0], amount, source); break; case EffectType.DamageOverTimeAlly: foreach (var t in targets) StartCoroutine(ApplyDamageOverTimeCoroutine(t, amount, duration, tickInterval, source)); break; case EffectType.HealSingleSelf: if (targets.Count > 0) ApplyInstantHeal(targets[0], amount, source); break; case EffectType.HealOverTimeSelf: foreach (var t in targets) StartCoroutine(ApplyHealOverTimeCoroutine(t, amount, duration, tickInterval, source)); break; case EffectType.HealGroupSingle: foreach (var t in targets) ApplyInstantHeal(t, amount, source); break; case EffectType.HealGroupOverTime: foreach (var t in targets) StartCoroutine(ApplyHealOverTimeCoroutine(t, amount, duration, tickInterval, source)); break; case EffectType.BuffDuration: // create a simple buff with id+duration; additional fields can be filled by caller when using Buff instances directly Buff buff = new Buff { buffId = Guid.NewGuid().ToString(), duration = duration }; foreach (var t in targets) StartCoroutine(ApplyTimedBuffCoroutine(t, buff, source)); break; case EffectType.DebuffDuration: Buff debuff = new Buff { buffId = Guid.NewGuid().ToString(), duration = duration }; foreach (var t in targets) StartCoroutine(ApplyTimedDebuffCoroutine(t, debuff, source)); break; case EffectType.IncreaseManaOverTime: foreach (var t in targets) StartCoroutine(ApplyIncreaseManaOverTimeCoroutine(t, amount, duration, tickInterval, source)); break; case EffectType.ReduceEnemyHealOverTime: // create a debuff that marks heal reduction in its description; enforcement depends on ICombatant.ReceiveHeal implementation Buff healReductionDebuff = new Buff { buffId = Guid.NewGuid().ToString(), duration = duration, description = "ReduceHeal" }; foreach (var t in targets) StartCoroutine(ApplyTimedDebuffCoroutine(t, healReductionDebuff, source)); break; case EffectType.ScoreMultiplier: // create a buff that multiplies scoring efficiency Buff scoreBuff = new Buff { buffId = Guid.NewGuid().ToString(), duration = duration, scoreMultiplier = amount }; foreach (var t in targets) StartCoroutine(ApplyTimedBuffCoroutine(t, scoreBuff, source)); break; case EffectType.AddScore: // amount interpreted as integer delta to add immediately to ally tracks foreach (var t in targets) { if (t == null) continue; var ally = t.GetComponent(); if (ally != null) { ally.AddScoreDirect(Mathf.CeilToInt(amount)); } else { // if target does not have AllyCombatant, try to find ICombatant and warn var ic = t.GetComponent(); if (ic != null) { Debug.LogWarning($"[EffectSystem] AddScore: target {t.name} implements ICombatant but is not AllyCombatant; cannot AddScoreDirect"); } else { Debug.LogWarning($"[EffectSystem] AddScore: target {t.name} does not support AddScore"); } } } break; default: Debug.LogWarning($"[EffectSystem] Unhandled EffectType {effectType}"); break; } } // Resolve targets based on selector. Tries several strategies: // - If selector == Self -> source only // - AllAllies / AllAlliesExceptSelf -> find GameObjects named ally_01..ally_05, and also fall back to objects with tag "Ally" // - AdjacentAllies -> use AllyCombatant.slotIndex (from source) and teamUIController helpers // - CurrentEnemies -> specificTarget if provided else all objects with tag "Enemy" // - AllEntities -> find all enemies + allies private List ResolveTargets(Selector selector, GameObject source, GameObject specificTarget) { List list = new List(); switch (selector) { case Selector.Self: if (source != null) list.Add(source); break; case Selector.AllAllies: list.AddRange(FindAllAllies()); break; case Selector.AllAlliesExceptSelf: list.AddRange(FindAllAllies()); if (source != null) list.RemoveAll(g => g == null || g == source || g.gameObject == source.gameObject); break; case Selector.AdjacentAllies: // find adjacent allies of the source (left/right). Use teamUIController if available. if (source == null) { Debug.LogWarning("[EffectSystem] AdjacentAllies selector requires a non-null source GameObject"); break; } // try to use AllyCombatant.slotIndex if present var allyComp = source.GetComponent(); int slotIndex = -1; if (allyComp != null) { slotIndex = allyComp.slotIndex; } else { // try to parse name like "ally_01" var n = source.name; for (int i = 1; i <= 5; i++) { if (n.Contains($"ally_0{i}")) { slotIndex = i - 1; break; } } } if (slotIndex >= 0) { var ui = teamUIController.Instance; if (ui != null) { try { var adj = ui.GetAdjacentAllyIndices(slotIndex); foreach (var idx in adj) { var go = ui.GetAllyObjectBySlot(idx); if (go != null) list.Add(go); else { var named = GameObject.Find($"ally_0{idx + 1}"); if (named != null) list.Add(named); } } } catch (Exception ex) { Debug.LogWarning($"[EffectSystem] teamUIController does not expose adjacent helpers: {ex.Message}"); int left = slotIndex - 1; int right = slotIndex + 1; if (left >= 0) { var g = GameObject.Find($"ally_0{left + 1}"); if (g != null) list.Add(g); } if (right <= 4) { var g = GameObject.Find($"ally_0{right + 1}"); if (g != null) list.Add(g); } } } else { int left = slotIndex - 1; int right = slotIndex + 1; if (left >= 0) { var g = GameObject.Find($"ally_0{left + 1}"); if (g != null) list.Add(g); } if (right <= 4) { var g = GameObject.Find($"ally_0{right + 1}"); if (g != null) list.Add(g); } } } else { Debug.LogWarning("[EffectSystem] Could not determine slot index for source when resolving AdjacentAllies"); } break; case Selector.CurrentEnemies: if (specificTarget != null) list.Add(specificTarget); else list.AddRange(FindAllEnemies()); break; case Selector.AllEntities: list.AddRange(FindAllAllies()); list.AddRange(FindAllEnemies()); break; } // remove nulls and duplicates list.RemoveAll(x => x == null); var uniq = new List(); foreach (var g in list) if (!uniq.Contains(g)) uniq.Add(g); return uniq; } private IEnumerable FindAllAllies() { List allies = new List(); // try names ally_01..ally_05 for (int i = 1; i <= 5; i++) { string name = $"ally_0{i}"; // ally_01..ally_05 var go = GameObject.Find(name); if (go != null) allies.Add(go); } // also include objects tagged "Ally" try { var tagged = GameObject.FindGameObjectsWithTag("Ally"); foreach (var g in tagged) if (!allies.Contains(g)) allies.Add(g); } catch { /* tag might not exist; ignore */ } return allies; } private IEnumerable FindAllEnemies() { List enemies = new List(); // Prefer explicit EnemyCombatant components (more robust than tags) try { var comps = GameObject.FindObjectsOfType(true); foreach (var c in comps) { if (c != null && c.gameObject != null && !enemies.Contains(c.gameObject)) enemies.Add(c.gameObject); } } catch { } // Also include a single named instance if present var single = GameObject.Find("thisEnemy"); if (single != null && !enemies.Contains(single)) enemies.Add(single); // Finally include any objects tagged "Enemy" (if tag exists) try { var tagged = GameObject.FindGameObjectsWithTag("Enemy"); foreach (var g in tagged) if (!enemies.Contains(g)) enemies.Add(g); } catch { /* ignore missing tag */ } return enemies; } #region Instant / OverTime Helpers private void ApplyInstantDamage(GameObject target, float amount, GameObject source) { if (target == null) return; var comp = target.GetComponent(); if (comp != null) { comp.ReceiveDamage(amount, source); } else { Debug.LogWarning($"[EffectSystem] Target {target.name} does not implement ICombatant -> cannot apply damage"); } } private void ApplyInstantHeal(GameObject target, float amount, GameObject source) { if (target == null) return; var comp = target.GetComponent(); if (comp != null) { comp.ReceiveHeal(amount, source); } else { Debug.LogWarning($"[EffectSystem] Target {target.name} does not implement ICombatant -> cannot apply heal"); } } private IEnumerator ApplyDamageOverTimeCoroutine(GameObject target, float totalAmount, float duration, float tickInterval, GameObject source) { if (target == null) yield break; var comp = target.GetComponent(); if (comp == null) { Debug.LogWarning($"[EffectSystem] Target {target.name} does not implement ICombatant -> cannot apply DOT"); yield break; } if (duration <= 0f) { comp.ReceiveDamage(totalAmount, source); yield break; } int ticks = Mathf.Max(1, Mathf.CeilToInt(duration / tickInterval)); float perTick = totalAmount / ticks; float elapsed = 0f; while (elapsed < duration) { comp.ReceiveDamage(perTick, source); yield return new WaitForSeconds(tickInterval); elapsed += tickInterval; } } private IEnumerator ApplyHealOverTimeCoroutine(GameObject target, float totalAmount, float duration, float tickInterval, GameObject source) { if (target == null) yield break; var comp = target.GetComponent(); if (comp == null) { Debug.LogWarning($"[EffectSystem] Target {target.name} does not implement ICombatant -> cannot apply HOT"); yield break; } if (duration <= 0f) { comp.ReceiveHeal(totalAmount, source); yield break; } int ticks = Mathf.Max(1, Mathf.CeilToInt(duration / tickInterval)); float perTick = totalAmount / ticks; float elapsed = 0f; while (elapsed < duration) { comp.ReceiveHeal(perTick, source); yield return new WaitForSeconds(tickInterval); elapsed += tickInterval; } } private IEnumerator ApplyIncreaseManaOverTimeCoroutine(GameObject target, float totalAmount, float duration, float tickInterval, GameObject source) { if (target == null) yield break; var comp = target.GetComponent(); // If target implements a method to add mana, call it; otherwise try to find AllyCombatant component var ally = target.GetComponent(); if (ally == null && comp == null) { Debug.LogWarning($"[EffectSystem] Target {target.name} cannot receive mana -> no AllyCombatant or ICombatant found"); yield break; } if (duration <= 0f) { // immediate add totalAmount to mana if ally present if (ally != null) ally.ModifyMana(Mathf.CeilToInt(totalAmount), true); yield break; } int ticks = Mathf.Max(1, Mathf.CeilToInt(duration / tickInterval)); float perTick = totalAmount / ticks; float elapsed = 0f; while (elapsed < duration) { if (ally != null) ally.ModifyMana(Mathf.CeilToInt(perTick), true); yield return new WaitForSeconds(tickInterval); elapsed += tickInterval; } } private IEnumerator ApplyTimedBuffCoroutine(GameObject target, Buff buff, GameObject source) { if (target == null) yield break; var comp = target.GetComponent(); if (comp == null) { Debug.LogWarning($"[EffectSystem] Target {target.name} does not implement ICombatant -> cannot apply Buff"); yield break; } comp.ApplyBuff(buff, source); yield return new WaitForSeconds(buff.duration); comp.RemoveBuff(buff.buffId); } private IEnumerator ApplyTimedDebuffCoroutine(GameObject target, Buff debuff, GameObject source) { // For now debuff is same as buff but may be handled differently by ICombatant implementation if (target == null) yield break; var comp = target.GetComponent(); if (comp == null) { Debug.LogWarning($"[EffectSystem] Target {target.name} does not implement ICombatant -> cannot apply Debuff"); yield break; } comp.ApplyBuff(debuff, source); yield return new WaitForSeconds(debuff.duration); comp.RemoveBuff(debuff.buffId); } #endregion }