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; }
// Per-frame caches to avoid repeated Find calls when multiple skills trigger in the same frame
private int _cachedAlliesFrame = -1;
private readonly List _cachedAllies = new List(8);
private int _cachedEnemiesFrame = -1;
private readonly List _cachedEnemies = new List(8);
private void Awake()
{
if (Instance == null) Instance = this;
else Destroy(gameObject);
}
private void LogVerbose(string message)
{
if (GameConfig.verboseLogs) Debug.Log(message);
}
///
/// 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)
{
// If caller provided a specificTarget, prefer it as the single target regardless of selector.
// Many callers pass specificTarget when they intend to hit a single unit (ally or enemy).
List targets;
if (specificTarget != null)
{
targets = new List { specificTarget };
}
else
{
targets = ResolveTargets(selector, source, specificTarget);
}
if (targets == null || targets.Count == 0)
{
Debug.LogWarning($"[EffectSystem] No targets resolved for selector={selector} specificTarget={(specificTarget ? specificTarget.name : "null")}");
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 selector resolved multiple allies, apply to all to support "AllAllies + DamageSingleAlly" designer usage
if (targets.Count == 1)
{
ApplyInstantDamage(targets[0], amount, source);
}
else if (targets.Count > 1)
{
foreach (var t in targets)
ApplyInstantDamage(t, 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;
// New handling for max HP / max Mana modifications via EffectSystem (mirrors direct path in SkillBuilder)
case EffectType.IncreaseMaxHP:
foreach (var t in targets)
{
if (t == null) continue;
var ally = t.GetComponent();
if (ally != null)
{
ally.SetMaxHP(ally.maxHP + Mathf.CeilToInt(amount), false);
}
else if (t.TryGetComponent(out var enemy))
{
enemy.SetMaxHP(enemy.maxHP + Mathf.CeilToInt(amount), false);
}
}
break;
case EffectType.DecreaseMaxHP:
foreach (var t in targets)
{
if (t == null) continue;
var ally = t.GetComponent();
if (ally != null)
{
ally.SetMaxHP(Mathf.Max(1, ally.maxHP - Mathf.CeilToInt(amount)), false);
}
else if (t.TryGetComponent(out var enemy2))
{
enemy2.SetMaxHP(Mathf.Max(1, enemy2.maxHP - Mathf.CeilToInt(amount)), false);
}
}
break;
case EffectType.IncreaseMaxMana:
foreach (var t in targets)
{
if (t == null) continue;
var ally = t.GetComponent();
if (ally != null)
{
ally.SetMaxMana(ally.maxMana + Mathf.CeilToInt(amount), false);
}
else if (t.TryGetComponent(out var enemy3))
{
enemy3.SetMaxMana(enemy3.maxMana + Mathf.CeilToInt(amount), false);
}
}
break;
case EffectType.DecreaseMaxMana:
foreach (var t in targets)
{
if (t == null) continue;
var ally = t.GetComponent();
if (ally != null)
{
ally.SetMaxMana(Mathf.Max(1, ally.maxMana - Mathf.CeilToInt(amount)), false);
}
else if (t.TryGetComponent(out var enemy4))
{
enemy4.SetMaxMana(Mathf.Max(1, enemy4.maxMana - Mathf.CeilToInt(amount)), false);
}
}
break;
// New: Increase/Decrease ScoreEfficiency handled by EffectSystem (mirrors SkillBuilder direct path)
case EffectType.IncreaseScoreEfficiency:
foreach (var t in targets)
{
if (t == null) continue;
var ally = t.GetComponent();
if (ally != null)
{
if (duration <= 0f)
{
ally.scoreEfficiency += amount;
}
else
{
// temporary additive change, revert after duration
StartCoroutine(ApplyTemporaryScoreEfficiencyChangeCoroutine(ally, amount, duration));
}
}
}
break;
case EffectType.DecreaseScoreEfficiency:
foreach (var t in targets)
{
if (t == null) continue;
var ally = t.GetComponent();
if (ally != null)
{
if (duration <= 0f)
{
ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency - amount);
}
else
{
StartCoroutine(ApplyTemporaryScoreEfficiencyChangeCoroutine(ally, -amount, duration));
}
}
}
break;
// New: Increase/Decrease Attack handled by EffectSystem (mirrors SkillBuilder direct path)
case EffectType.IncreaseAttack:
foreach (var t in targets)
{
if (t == null) continue;
var ally = t.GetComponent();
if (ally != null)
{
int delta = Mathf.CeilToInt(amount);
if (duration <= 0f)
{
ally.ModifyAttack(delta);
}
else
{
StartCoroutine(ApplyTemporaryAttackChangeCoroutine(ally, delta, duration));
}
}
else if (t.TryGetComponent(out var enemy5))
{
int deltaE = Mathf.CeilToInt(amount);
if (duration <= 0f)
enemy5.attack += deltaE;
else
StartCoroutine(ApplyTemporaryAttackChangeCoroutine(enemy5, deltaE, duration));
}
}
break;
case EffectType.DecreaseAttack:
foreach (var t in targets)
{
if (t == null) continue;
var ally = t.GetComponent();
if (ally != null)
{
int delta = Mathf.CeilToInt(amount);
if (duration <= 0f)
{
ally.ModifyAttack(-delta);
}
else
{
// apply decrease now, revert after duration
StartCoroutine(ApplyTemporaryAttackChangeCoroutine(ally, -delta, duration));
}
}
else if (t.TryGetComponent(out var enemy6))
{
int deltaE = Mathf.CeilToInt(amount);
if (duration <= 0f)
enemy6.attack = Mathf.Max(0, enemy6.attack - deltaE);
else
StartCoroutine(ApplyTemporaryAttackChangeCoroutine(enemy6, -deltaE, duration));
}
}
break;
case EffectType.RedirectNextDamageToSelf:
{
AllyCombatant redirector = null;
if (source != null)
redirector = source.GetComponent() ?? source.GetComponentInChildren(true);
if (redirector == null && targets != null && targets.Count > 0)
redirector = targets[0].GetComponent() ?? targets[0].GetComponentInChildren(true);
if (redirector != null)
{
AllyCombatant.ActivateNextDamageRedirect(redirector, duration);
}
else
{
Debug.LogWarning("[EffectSystem] RedirectNextDamageToSelf: no AllyCombatant found to apply.");
}
}
break;
case EffectType.IncreaseDamageResistance:
foreach (var t in targets)
{
if (t == null) continue;
var ally = t.GetComponent();
if (ally != null)
{
// amount interpreted as additive delta (0..1). Clamp result 0..0.9 to avoid invulnerability
if (duration <= 0f)
{
ally.damageResistance = Mathf.Clamp01(ally.damageResistance + amount);
LogVerbose($"[EffectSystem] IncreaseDamageResistance applied to {t.name}: new resistance={ally.damageResistance}");
}
else
{
StartCoroutine(ApplyTemporaryDamageResistanceChangeCoroutine(ally.gameObject, amount, duration));
}
}
else if (t.TryGetComponent(out var enemyR))
{
if (duration <= 0f)
{
enemyR.damageResistance = Mathf.Clamp01(enemyR.damageResistance + amount);
LogVerbose($"[EffectSystem] IncreaseDamageResistance applied to enemy {t.name}: new resistance={enemyR.damageResistance}");
}
else
{
StartCoroutine(ApplyTemporaryDamageResistanceChangeCoroutine(enemyR.gameObject, amount, duration));
}
}
}
break;
case EffectType.DecreaseDamageResistance:
foreach (var t in targets)
{
if (t == null) continue;
var ally = t.GetComponent();
if (ally != null)
{
if (duration <= 0f)
{
ally.damageResistance = Mathf.Clamp01(ally.damageResistance - amount);
LogVerbose($"[EffectSystem] DecreaseDamageResistance applied to {t.name}: new resistance={ally.damageResistance}");
}
else
{
// apply negative delta
StartCoroutine(ApplyTemporaryDamageResistanceChangeCoroutine(ally.gameObject, -amount, duration));
}
}
else if (t.TryGetComponent(out var enemyR2))
{
if (duration <= 0f)
{
enemyR2.damageResistance = Mathf.Clamp01(enemyR2.damageResistance - amount);
LogVerbose($"[EffectSystem] DecreaseDamageResistance applied to enemy {t.name}: new resistance={enemyR2.damageResistance}");
}
else
{
StartCoroutine(ApplyTemporaryDamageResistanceChangeCoroutine(enemyR2.gameObject, -amount, duration));
}
}
}
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();
// prefer teamUIController when available for authoritative ally/enemy objects
var ui = teamUIController.Instance != null ? teamUIController.Instance : UnityEngine.Object.FindAnyObjectByType();
switch (selector)
{
case Selector.Self:
if (source != null) list.Add(source);
break;
case Selector.AllAllies:
if (ui != null)
{
int count = ui.allySlotIds != null ? ui.allySlotIds.Count : 5;
for (int i = 0; i < count; i++)
{
var go = ui.GetAllyObjectBySlot(i);
if (go != null && !list.Contains(go)) list.Add(go);
}
}
else
{
list.AddRange(FindAllAllies());
}
break;
case Selector.AllAlliesExceptSelf:
if (ui != null)
{
int count = ui.allySlotIds != null ? ui.allySlotIds.Count : 5;
for (int i = 0; i < count; i++)
{
var go = ui.GetAllyObjectBySlot(i);
if (go != null && !list.Contains(go)) list.Add(go);
}
if (source != null)
list.RemoveAll(g => g == null || g == source || g.gameObject == source.gameObject);
}
else
{
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)
{
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);
// Debug: log resolved targets for diagnosis
if (uniq.Count == 0)
Debug.LogWarning($"[EffectSystem] ResolveTargets -> selector={selector} returned 0 targets (source={(source ? source.name : "null")}, specificTarget={(specificTarget ? specificTarget.name : "null")})");
else
LogVerbose($"[EffectSystem] ResolveTargets -> selector={selector} resolved {uniq.Count} targets: {string.Join(",", uniq.ConvertAll(x => x != null ? x.name : "null"))}");
return uniq;
}
private IEnumerable FindAllAllies()
{
if (_cachedAlliesFrame == Time.frameCount) return _cachedAllies;
_cachedAlliesFrame = Time.frameCount;
_cachedAllies.Clear();
// prefer teamUIController if available to get authoritative ally objects
var ui = teamUIController.Instance;
if (ui != null)
{
try
{
int count = ui.allySlotIds != null ? ui.allySlotIds.Count : 5;
for (int i = 0; i < count; i++)
{
var go = ui.GetAllyObjectBySlot(i);
if (go != null && !_cachedAllies.Contains(go)) _cachedAllies.Add(go);
}
}
catch { /* ignore and fallback */ }
}
// fallback: try names ally_01..ally_05 (keeps compatibility with legacy scene setups)
for (int i = 1; i <= 5; i++)
{
string name = $"ally_0{i}"; // ally_01..ally_05
var go = GameObject.Find(name);
if (go != null && !_cachedAllies.Contains(go)) _cachedAllies.Add(go);
}
// also include objects tagged "Ally"
try
{
var tagged = GameObject.FindGameObjectsWithTag("Ally");
foreach (var g in tagged)
if (g != null && !_cachedAllies.Contains(g)) _cachedAllies.Add(g);
}
catch { /* tag might not exist; ignore */ }
return _cachedAllies;
}
private IEnumerable FindAllEnemies()
{
if (_cachedEnemiesFrame == Time.frameCount) return _cachedEnemies;
_cachedEnemiesFrame = Time.frameCount;
_cachedEnemies.Clear();
// Prefer explicit EnemyCombatant components (more robust than tags)
try
{
var comps = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None);
foreach (var c in comps)
{
if (c != null && c.gameObject != null && !_cachedEnemies.Contains(c.gameObject)) _cachedEnemies.Add(c.gameObject);
}
}
catch { }
// Also include a single named instance if present
var single = GameObject.Find("thisEnemy");
if (single != null && !_cachedEnemies.Contains(single)) _cachedEnemies.Add(single);
// Finally include any objects tagged "Enemy" (if tag exists)
try
{
var tagged = GameObject.FindGameObjectsWithTag("Enemy");
foreach (var g in tagged)
if (g != null && !_cachedEnemies.Contains(g)) _cachedEnemies.Add(g);
}
catch { /* ignore missing tag */ }
return _cachedEnemies;
}
#region Instant / OverTime Helpers
private bool AreAllEnemiesDead()
{
var enemies = FindAllEnemies();
foreach (var go in enemies)
{
if (go == null) continue;
var e = go.GetComponent();
if (e != null && !e.IsDead && e.currentHP > 0)
{
return false;
}
}
return true;
}
private void ApplyInstantDamage(GameObject target, float amount, GameObject source)
{
if (target == null) return;
// If target is an enemy, check if all enemies are already dead
if (target.GetComponent() != null && AreAllEnemiesDead())
{
LogVerbose($"[EffectSystem] Skip ApplyInstantDamage to {target.name} because all enemies are dead.");
return;
}
var comp = target.GetComponent();
if (comp != null)
{
comp.ReceiveDamage(amount, source);
// Record stats if source is an ally
if (source != null && target.GetComponent() != null)
{
var ally = source.GetComponent();
if (ally != null && teamUIController.Instance != null)
{
teamUIController.Instance.RecordDamage(ally.slotIndex, amount);
}
}
// Record damage taken if target is an ally
var targetAlly = target.GetComponent();
if (targetAlly != null && teamUIController.Instance != null)
{
teamUIController.Instance.RecordDamageTaken(targetAlly.slotIndex, amount);
}
}
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);
// Record stats if source is an ally
if (source != null)
{
var ally = source.GetComponent();
if (ally != null && teamUIController.Instance != null)
{
teamUIController.Instance.RecordHeal(ally.slotIndex, amount);
}
}
}
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)
{
// check death state for immediate damage
if (target.GetComponent() != null && AreAllEnemiesDead())
{
LogVerbose($"[EffectSystem] Skip DOT (immediate) to {target.name} because all enemies are dead.");
yield break;
}
comp.ReceiveDamage(totalAmount, source);
// Record stats
if (source != null && target.GetComponent() != null)
{
var ally = source.GetComponent();
if (ally != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamage(ally.slotIndex, totalAmount);
}
// Record damage taken
var targetAlly = target.GetComponent();
if (targetAlly != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamageTaken(targetAlly.slotIndex, totalAmount);
yield break;
}
int ticks = Mathf.Max(1, Mathf.CeilToInt(duration / tickInterval));
float perTick = totalAmount / ticks;
float elapsed = 0f;
while (elapsed < duration)
{
if (target == null) yield break;
// check death state inside loop
if (target.GetComponent() != null && AreAllEnemiesDead())
{
LogVerbose($"[EffectSystem] Stopping DOT to {target.name} because all enemies are dead.");
yield break;
}
comp.ReceiveDamage(perTick, source);
// Record stats
if (source != null && target.GetComponent() != null)
{
var ally = source.GetComponent();
if (ally != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamage(ally.slotIndex, perTick);
}
// Record damage taken
var targetAlly = target.GetComponent();
if (targetAlly != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamageTaken(targetAlly.slotIndex, perTick);
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);
// Record stats
if (source != null)
{
var ally = source.GetComponent();
if (ally != null && teamUIController.Instance != null) teamUIController.Instance.RecordHeal(ally.slotIndex, totalAmount);
}
yield break;
}
int ticks = Mathf.Max(1, Mathf.CeilToInt(duration / tickInterval));
float perTick = totalAmount / ticks;
float elapsed = 0f;
while (elapsed < duration)
{
if (target == null) yield break;
comp.ReceiveHeal(perTick, source);
// Record stats
if (source != null)
{
var ally = source.GetComponent();
if (ally != null && teamUIController.Instance != null) teamUIController.Instance.RecordHeal(ally.slotIndex, perTick);
}
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, true);
// Record stats
if (source != null)
{
var sourceAlly = source.GetComponent();
if (sourceAlly != null && teamUIController.Instance != null) teamUIController.Instance.RecordMana(sourceAlly.slotIndex, totalAmount);
}
}
yield break;
}
int ticks = Mathf.Max(1, Mathf.CeilToInt(duration / tickInterval));
float perTick = totalAmount / ticks;
float elapsed = 0f;
while (elapsed < duration)
{
if (target == null) yield break;
if (ally != null)
{
ally.ModifyMana(Mathf.CeilToInt(perTick), true, true);
// Record stats
if (source != null)
{
var sourceAlly = source.GetComponent();
if (sourceAlly != null && teamUIController.Instance != null) teamUIController.Instance.RecordMana(sourceAlly.slotIndex, perTick);
}
}
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);
}
// Coroutine to apply temporary scoreEfficiency change and revert after duration
private IEnumerator ApplyTemporaryScoreEfficiencyChangeCoroutine(AllyCombatant ally, float delta, float duration)
{
if (ally == null) yield break;
ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency + delta);
yield return new WaitForSeconds(duration);
ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency - delta);
}
// Coroutine to apply temporary damageResistance change and revert after duration
private IEnumerator ApplyTemporaryDamageResistanceChangeCoroutine(GameObject target, float delta, float duration)
{
if (target == null) yield break;
// support both ally and enemy
var ally = target.GetComponent();
if (ally != null)
{
float before = ally.damageResistance;
ally.damageResistance = Mathf.Clamp01(ally.damageResistance + delta);
yield return new WaitForSeconds(duration);
// revert by subtracting delta, clamp
ally.damageResistance = Mathf.Clamp01(ally.damageResistance - delta);
yield break;
}
var enemy = target.GetComponent();
if (enemy != null)
{
float beforeE = enemy.damageResistance;
enemy.damageResistance = Mathf.Clamp01(enemy.damageResistance + delta);
yield return new WaitForSeconds(duration);
enemy.damageResistance = Mathf.Clamp01(enemy.damageResistance - delta);
yield break;
}
}
// Coroutine to apply temporary attack change for AllyCombatant
private IEnumerator ApplyTemporaryAttackChangeCoroutine(AllyCombatant ally, int delta, float duration)
{
if (ally == null) yield break;
ally.ModifyAttack(delta);
yield return new WaitForSeconds(duration);
ally.ModifyAttack(-delta);
}
// Coroutine to apply temporary attack change for EnemyCombatant
private IEnumerator ApplyTemporaryAttackChangeCoroutine(EnemyCombatant enemy, int delta, float duration)
{
if (enemy == null) yield break;
enemy.attack = Mathf.Max(0, enemy.attack + delta);
yield return new WaitForSeconds(duration);
enemy.attack = Mathf.Max(0, enemy.attack - delta);
}
#endregion
}