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
}