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

955 lines
40 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;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// EffectSystem Чͳһַӿڡ
/// - ѡΧSelectorĿϣԼȫѾԼѾѾϵˡˣ
/// - ЧͣEffectType˼ʱ/˺///ȡ
///
/// ĿʵӦʵ ICombatant ӿԱ EffectSystem ܵͳһ˺//ȣûʵ֣־ѡ
///
/// ýűΪܣãֻṩ ApplyEffect(...) ȷűá
/// </summary>
public class EffectSystem : MonoBehaviour
{
public static EffectSystem Instance { get; private set; }
private void Awake()
{
if (Instance == null) Instance = this;
else Destroy(gameObject);
}
/// <summary>
/// ApplyEffect: ָѡΧĿӦЧ
/// - selector: Ŀѡ
/// - effectType: Чͣʱ/ȣ
/// - amount: ֵ˺Чǿȣ
/// - duration: ʱԳЧ Buff/Debuff ЧʱЧΪ 0
/// - source: ߣ Self/AlliesExceptSelf жϻ¼
/// - specificTarget: selector Ϊ CurrentEnemies ָֻʱɴĿ
/// - tickInterval: Ч tick Ĭ 1
/// </summary>
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<GameObject> targets;
if (specificTarget != null)
{
targets = new List<GameObject> { 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<AllyCombatant>();
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<ICombatant>();
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<AllyCombatant>();
if (ally != null)
{
ally.SetMaxHP(ally.maxHP + Mathf.CeilToInt(amount), false);
}
else if (t.TryGetComponent<EnemyCombatant>(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<AllyCombatant>();
if (ally != null)
{
ally.SetMaxHP(Mathf.Max(1, ally.maxHP - Mathf.CeilToInt(amount)), false);
}
else if (t.TryGetComponent<EnemyCombatant>(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<AllyCombatant>();
if (ally != null)
{
ally.SetMaxMana(ally.maxMana + Mathf.CeilToInt(amount), false);
}
else if (t.TryGetComponent<EnemyCombatant>(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<AllyCombatant>();
if (ally != null)
{
ally.SetMaxMana(Mathf.Max(1, ally.maxMana - Mathf.CeilToInt(amount)), false);
}
else if (t.TryGetComponent<EnemyCombatant>(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<AllyCombatant>();
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<AllyCombatant>();
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<AllyCombatant>();
if (ally != null)
{
int delta = Mathf.CeilToInt(amount);
if (duration <= 0f)
{
ally.attack += delta;
}
else
{
StartCoroutine(ApplyTemporaryAttackChangeCoroutine(ally, delta, duration));
}
}
else if (t.TryGetComponent<EnemyCombatant>(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<AllyCombatant>();
if (ally != null)
{
int delta = Mathf.CeilToInt(amount);
if (duration <= 0f)
{
ally.attack = Mathf.Max(0, ally.attack - delta);
}
else
{
// apply decrease now, revert after duration
StartCoroutine(ApplyTemporaryAttackChangeCoroutine(ally, -delta, duration));
}
}
else if (t.TryGetComponent<EnemyCombatant>(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.IncreaseDamageResistance:
foreach (var t in targets)
{
if (t == null) continue;
var ally = t.GetComponent<AllyCombatant>();
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);
Debug.Log($"[EffectSystem] IncreaseDamageResistance applied to {t.name}: new resistance={ally.damageResistance}");
}
else
{
StartCoroutine(ApplyTemporaryDamageResistanceChangeCoroutine(ally.gameObject, amount, duration));
}
}
else if (t.TryGetComponent<EnemyCombatant>(out var enemyR))
{
if (duration <= 0f)
{
enemyR.damageResistance = Mathf.Clamp01(enemyR.damageResistance + amount);
Debug.Log($"[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<AllyCombatant>();
if (ally != null)
{
if (duration <= 0f)
{
ally.damageResistance = Mathf.Clamp01(ally.damageResistance - amount);
Debug.Log($"[EffectSystem] DecreaseDamageResistance applied to {t.name}: new resistance={ally.damageResistance}");
}
else
{
// apply negative delta
StartCoroutine(ApplyTemporaryDamageResistanceChangeCoroutine(ally.gameObject, -amount, duration));
}
}
else if (t.TryGetComponent<EnemyCombatant>(out var enemyR2))
{
if (duration <= 0f)
{
enemyR2.damageResistance = Mathf.Clamp01(enemyR2.damageResistance - amount);
Debug.Log($"[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<GameObject> ResolveTargets(Selector selector, GameObject source, GameObject specificTarget)
{
List<GameObject> list = new List<GameObject>();
// prefer teamUIController when available for authoritative ally/enemy objects
var ui = teamUIController.Instance ?? FindObjectOfType<teamUIController>();
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<AllyCombatant>();
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<GameObject>();
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
Debug.Log($"[EffectSystem] ResolveTargets -> selector={selector} resolved {uniq.Count} targets: {string.Join(",", uniq.ConvertAll(x => x != null ? x.name : "null"))}");
return uniq;
}
private IEnumerable<GameObject> FindAllAllies()
{
List<GameObject> allies = new List<GameObject>();
// 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 && !allies.Contains(go)) allies.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 && !allies.Contains(go)) 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<GameObject> FindAllEnemies()
{
List<GameObject> enemies = new List<GameObject>();
// Prefer explicit EnemyCombatant components (more robust than tags)
try
{
var comps = GameObject.FindObjectsOfType<EnemyCombatant>(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 bool AreAllEnemiesDead()
{
var enemies = GameObject.FindObjectsOfType<EnemyCombatant>(true);
if (enemies.Length == 0) return true;
foreach (var e in enemies)
{
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<EnemyCombatant>() != null && AreAllEnemiesDead())
{
Debug.Log($"[EffectSystem] Skip ApplyInstantDamage to {target.name} because all enemies are dead.");
return;
}
var comp = target.GetComponent<ICombatant>();
if (comp != null)
{
comp.ReceiveDamage(amount, source);
// Record stats if source is an ally
if (source != null && target.GetComponent<EnemyCombatant>() != null)
{
var ally = source.GetComponent<AllyCombatant>();
if (ally != null && teamUIController.Instance != null)
{
teamUIController.Instance.RecordDamage(ally.slotIndex, amount);
}
}
// Record damage taken if target is an ally
var targetAlly = target.GetComponent<AllyCombatant>();
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<ICombatant>();
if (comp != null)
{
comp.ReceiveHeal(amount, source);
// Record stats if source is an ally
if (source != null)
{
var ally = source.GetComponent<AllyCombatant>();
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<ICombatant>();
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<EnemyCombatant>() != null && AreAllEnemiesDead())
{
Debug.Log($"[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<EnemyCombatant>() != null)
{
var ally = source.GetComponent<AllyCombatant>();
if (ally != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamage(ally.slotIndex, totalAmount);
}
// Record damage taken
var targetAlly = target.GetComponent<AllyCombatant>();
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<EnemyCombatant>() != null && AreAllEnemiesDead())
{
Debug.Log($"[EffectSystem] Stopping DOT to {target.name} because all enemies are dead.");
yield break;
}
comp.ReceiveDamage(perTick, source);
// Record stats
if (source != null && target.GetComponent<EnemyCombatant>() != null)
{
var ally = source.GetComponent<AllyCombatant>();
if (ally != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamage(ally.slotIndex, perTick);
}
// Record damage taken
var targetAlly = target.GetComponent<AllyCombatant>();
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<ICombatant>();
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<AllyCombatant>();
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<AllyCombatant>();
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<ICombatant>();
// If target implements a method to add mana, call it; otherwise try to find AllyCombatant component
var ally = target.GetComponent<AllyCombatant>();
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<AllyCombatant>();
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<AllyCombatant>();
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<ICombatant>();
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<ICombatant>();
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<AllyCombatant>();
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<EnemyCombatant>();
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.attack = Mathf.Max(0, ally.attack + delta);
yield return new WaitForSeconds(duration);
ally.attack = Mathf.Max(0, ally.attack - 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
}