using System; using System.Collections; using System.Collections.Generic; using UnityEngine; /// /// Documentation text normalized. /// Documentation text normalized. /// Documentation text normalized. /// /// Documentation text normalized. /// /// Documentation text normalized. /// 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 readonly HashSet _awaitingHitFxTargets = new HashSet(); private readonly Dictionary _pendingDamagePopupByTargetId = new Dictionary(64); private void Awake() { if (Instance == null) Instance = this; else Destroy(gameObject); } private void LogVerbose(string message) { if (GameConfig.verboseLogs) Debug.Log(message); } /// /// Documentation text normalized. /// Documentation text normalized. /// Documentation text normalized. /// Documentation text normalized. /// Documentation text normalized. /// Documentation text normalized. /// Documentation text normalized. /// Documentation text normalized. /// 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) { LogVerbose($"[EffectSystem] ApplyEffect: no targets (selector={selector}, source={(source ? source.name : "null")}, specificTarget={(specificTarget ? specificTarget.name : "null")})"); return; } // --- Projectile Logic --- // Trigger projectile if source exists and is targeting the opposite side if (GfxController.Instance != null && source != null) { foreach (var t in targets) { if (t != null && t != source) { // Identify if this is an offensive targeting (Ally -> Enemy or Enemy -> Ally) bool sourceIsAlly = source.name.StartsWith("ally_0"); bool sourceIsEnemy = source.name == "thisEnemy"; bool targetIsAlly = t.name.StartsWith("ally_0"); bool targetIsEnemy = t.name == "thisEnemy"; if ((sourceIsAlly && targetIsEnemy) || (sourceIsEnemy && targetIsAlly)) { // Captured target for callback GameObject targetToHit = t; bool isEnemyTarget = targetIsEnemy; // Calculate damage info for explosion effect (Ally -> Enemy only) (float damage, float maxHealth)? damageInfo = null; if (isEnemyTarget && effectType == EffectType.DamageSingleEnemy) { var ec = targetToHit.GetComponent(); if (ec != null) { float effectiveDamage = amount * (1f - ec.damageResistance); damageInfo = (effectiveDamage, (float)ec.maxHP); } } GfxController.Instance.PlayProjectile(source, t, (finalPos) => { // Only play hit FX when projectile arrives if (targetToHit != null && GfxController.Instance != null) { GfxController.Instance.PlayHitFX(targetToHit, null, isEnemyTarget, finalPos, null, damageInfo); } FlushPendingDamagePopupAfterHit(targetToHit); }); if (targetToHit != null) MarkAwaitingHitFx(targetToHit); } } } } 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) { if (t == null) { StartCoroutine(ApplyDamageOverTimeCoroutine(t, amount, duration, tickInterval, source, null, -1, 0)); continue; } var enemyDot = t.GetComponent() ?? t.GetComponentInChildren(true); string iconId = null; int enemyInstanceId = 0; if (enemyDot != null && duration > 0f) { int ticks = Mathf.Max(1, Mathf.CeilToInt(duration / tickInterval)); float perTick = amount / ticks; iconId = iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemyDot, PlayerBudeffIconType.ot_bleeding, perTick, duration); enemyInstanceId = enemyDot.GetInstanceID(); } StartCoroutine(ApplyDamageOverTimeCoroutine(t, amount, duration, tickInterval, source, iconId, -1, enemyInstanceId)); } 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) { if (t == null) { StartCoroutine(ApplyDamageOverTimeCoroutine(t, amount, duration, tickInterval, source, null, -1, 0)); continue; } var allyDot = t.GetComponent() ?? t.GetComponentInChildren(true); string iconId = null; if (allyDot != null && duration > 0f) { int ticks = Mathf.Max(1, Mathf.CeilToInt(duration / tickInterval)); float perTick = amount / ticks; iconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(allyDot, PlayerBudeffIconType.ot_bleeding, perTick, duration); } StartCoroutine(ApplyDamageOverTimeCoroutine(t, amount, duration, tickInterval, source, iconId, allyDot != null ? allyDot.slotIndex : -1, 0)); } 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: // Generic timed buff: treat amount as a score multiplier (1.5 = +50%, 0.5 = -50%). Buff buff = new Buff { buffId = Guid.NewGuid().ToString(), duration = duration, scoreMultiplier = amount > 0f ? amount : 1f }; foreach (var t in targets) StartCoroutine(ApplyTimedBuffCoroutine(t, buff, source)); break; case EffectType.DebuffDuration: // Generic timed debuff: treat amount as a score multiplier (usually < 1). Buff debuff = new Buff { buffId = Guid.NewGuid().ToString(), duration = duration, scoreMultiplier = amount > 0f ? amount : 1f }; 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: // Debuff: reduce heals received (healReceivedMultiplier < 1). // amount is interpreted as a reduction ratio in [0..1], so 0.2 means -20% heals, 1 means -100% heals. float reduction = Mathf.Clamp01(amount); Buff healReductionDebuff = new Buff { buffId = Guid.NewGuid().ToString(), duration = duration, description = "ReduceHeal", healReceivedMultiplier = 1f - reduction }; 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) { int before = ally.currentScore; ally.AddScoreDirect(Mathf.CeilToInt(amount)); int delta = ally.currentScore - before; if (delta != 0) { var type = delta > 0 ? iNumberPrefabController.InstantNumberType.IscorePlus : iNumberPrefabController.InstantNumberType.IscoreMinus; iNumberPrefabController.SpawnForAllyStatic(ally.slotIndex, type, delta); } } 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) { int delta = Mathf.CeilToInt(amount); if (duration <= 0f) { ally.SetMaxHP(ally.maxHP + delta, false); iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_maxHP_up, delta, 0f); } else { var iconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_maxHP_up, delta, duration); StartCoroutine(ApplyTemporaryMaxHPDeltaCoroutine(ally, delta, duration, iconId)); } } else if (t.TryGetComponent(out var enemy)) { int delta = Mathf.CeilToInt(amount); if (duration <= 0f) { enemy.SetMaxHP(enemy.maxHP + delta, false); iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemy, PlayerBudeffIconType.ot_maxHP_up, delta, 0f); } else { var iconId = iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemy, PlayerBudeffIconType.ot_maxHP_up, delta, duration); StartCoroutine(ApplyTemporaryMaxHPDeltaCoroutine(enemy, delta, duration, iconId, enemy.GetInstanceID())); } } } break; case EffectType.DecreaseMaxHP: 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.SetMaxHP(Mathf.Max(1, ally.maxHP - delta), false); iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_maxHP_down, -delta, 0f); } else { var iconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_maxHP_down, -delta, duration); StartCoroutine(ApplyTemporaryMaxHPDeltaCoroutine(ally, -delta, duration, iconId)); } } else if (t.TryGetComponent(out var enemy2)) { int delta = Mathf.CeilToInt(amount); if (duration <= 0f) { enemy2.SetMaxHP(Mathf.Max(1, enemy2.maxHP - delta), false); iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemy2, PlayerBudeffIconType.ot_maxHP_down, -delta, 0f); } else { var iconId = iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemy2, PlayerBudeffIconType.ot_maxHP_down, -delta, duration); StartCoroutine(ApplyTemporaryMaxHPDeltaCoroutine(enemy2, -delta, duration, iconId, enemy2.GetInstanceID())); } } } break; case EffectType.IncreaseMaxMana: 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.SetMaxMana(ally.maxMana + delta, false); iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_maxMana_up, delta, 0f); } else { var iconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_maxMana_up, delta, duration); StartCoroutine(ApplyTemporaryMaxManaDeltaCoroutine(ally, delta, duration, iconId)); } } else if (t.TryGetComponent(out var enemy3)) { int delta = Mathf.CeilToInt(amount); if (duration <= 0f) { enemy3.SetMaxMana(enemy3.maxMana + delta, false); iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemy3, PlayerBudeffIconType.ot_maxMana_up, delta, 0f); } else { var iconId = iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemy3, PlayerBudeffIconType.ot_maxMana_up, delta, duration); StartCoroutine(ApplyTemporaryMaxManaDeltaCoroutine(enemy3, delta, duration, iconId, enemy3.GetInstanceID())); } } } break; case EffectType.DecreaseMaxMana: 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.SetMaxMana(Mathf.Max(1, ally.maxMana - delta), false); iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_maxMana_down, -delta, 0f); } else { var iconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_maxMana_down, -delta, duration); StartCoroutine(ApplyTemporaryMaxManaDeltaCoroutine(ally, -delta, duration, iconId)); } } else if (t.TryGetComponent(out var enemy4)) { int delta = Mathf.CeilToInt(amount); if (duration <= 0f) { enemy4.SetMaxMana(Mathf.Max(1, enemy4.maxMana - delta), false); iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemy4, PlayerBudeffIconType.ot_maxMana_down, -delta, 0f); } else { var iconId = iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemy4, PlayerBudeffIconType.ot_maxMana_down, -delta, duration); StartCoroutine(ApplyTemporaryMaxManaDeltaCoroutine(enemy4, -delta, duration, iconId, enemy4.GetInstanceID())); } } } 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; iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_scoreEfficiency_up, amount, 0f); } else { // temporary additive change, revert after duration var iconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_scoreEfficiency_up, amount, duration); StartCoroutine(ApplyTemporaryScoreEfficiencyChangeCoroutine(ally, amount, duration, iconId)); } } } 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); iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_scoreEfficiency_down, -amount, 0f); } else { var iconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_scoreEfficiency_down, -amount, duration); StartCoroutine(ApplyTemporaryScoreEfficiencyChangeCoroutine(ally, -amount, duration, iconId)); } } } 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); iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_atk_up, delta, 0f); } else { var iconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_atk_up, delta, duration); StartCoroutine(ApplyTemporaryAttackChangeCoroutine(ally, delta, duration, iconId)); } } else if (t.TryGetComponent(out var enemy5)) { int deltaE = Mathf.CeilToInt(amount); if (duration <= 0f) { enemy5.ModifyAttack(deltaE); iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemy5, PlayerBudeffIconType.ot_atk_up, deltaE, 0f); } else { var iconId = iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemy5, PlayerBudeffIconType.ot_atk_up, deltaE, duration); StartCoroutine(ApplyTemporaryAttackChangeCoroutine(enemy5, deltaE, duration, iconId, enemy5.GetInstanceID())); } } } 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); iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_atk_down, -delta, 0f); } else { // apply decrease now, revert after duration var iconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_atk_down, -delta, duration); StartCoroutine(ApplyTemporaryAttackChangeCoroutine(ally, -delta, duration, iconId)); } } else if (t.TryGetComponent(out var enemy6)) { int deltaE = Mathf.CeilToInt(amount); if (duration <= 0f) { enemy6.ModifyAttack(-deltaE); iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemy6, PlayerBudeffIconType.ot_atk_down, -deltaE, 0f); } else { var iconId = iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemy6, PlayerBudeffIconType.ot_atk_down, -deltaE, duration); StartCoroutine(ApplyTemporaryAttackChangeCoroutine(enemy6, -deltaE, duration, iconId, enemy6.GetInstanceID())); } } } 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) { if (duration <= 0f) { ally.damageResistance = ClampDamageResistance(ally.damageResistance + amount); LogVerbose($"[EffectSystem] IncreaseDamageResistance applied to {t.name}: new resistance={ally.damageResistance}"); iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_defend_up, amount, 0f); } else { var iconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_defend_up, amount, duration); StartCoroutine(ApplyTemporaryDamageResistanceChangeCoroutine(ally.gameObject, amount, duration, iconId, ally.slotIndex, 0)); } } else if (t.TryGetComponent(out var enemyR)) { if (duration <= 0f) { enemyR.damageResistance = ClampDamageResistance(enemyR.damageResistance + amount); LogVerbose($"[EffectSystem] IncreaseDamageResistance applied to enemy {t.name}: new resistance={enemyR.damageResistance}"); iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemyR, PlayerBudeffIconType.ot_defend_up, amount, 0f); } else { var iconId = iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemyR, PlayerBudeffIconType.ot_defend_up, amount, duration); StartCoroutine(ApplyTemporaryDamageResistanceChangeCoroutine(enemyR.gameObject, amount, duration, iconId, -1, enemyR.GetInstanceID())); } } } 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 = ClampDamageResistance(ally.damageResistance - amount); LogVerbose($"[EffectSystem] DecreaseDamageResistance applied to {t.name}: new resistance={ally.damageResistance}"); iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_defend_down, -amount, 0f); } else { // apply negative delta var iconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_defend_down, -amount, duration); StartCoroutine(ApplyTemporaryDamageResistanceChangeCoroutine(ally.gameObject, -amount, duration, iconId, ally.slotIndex, 0)); } } else if (t.TryGetComponent(out var enemyR2)) { if (duration <= 0f) { enemyR2.damageResistance = ClampDamageResistance(enemyR2.damageResistance - amount); LogVerbose($"[EffectSystem] DecreaseDamageResistance applied to enemy {t.name}: new resistance={enemyR2.damageResistance}"); iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemyR2, PlayerBudeffIconType.ot_defend_down, -amount, 0f); } else { var iconId = iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemyR2, PlayerBudeffIconType.ot_defend_down, -amount, duration); StartCoroutine(ApplyTemporaryDamageResistanceChangeCoroutine(enemyR2.gameObject, -amount, duration, iconId, -1, enemyR2.GetInstanceID())); } } } break; case EffectType.GrantExtraPerfect: foreach (var t in targets) { if (t == null) continue; var ally = t.GetComponent(); if (ally == null) continue; if (ally.IsDead) continue; int slot = ally.slotIndex; // Update judgement counts (for settlement UI). var sm = ScoreManager.Instance; if (sm != null) { sm.countPerfect += 1; if (slot >= 0 && slot < sm.trackPerfectCounts.Length) sm.trackPerfectCounts[slot] += 1; } // Grant score as if a Perfect happened on this track. try { int pmDelta = ally.AddScoreForJudge("Perfect"); float eff = ally.scoreEfficiency; sm?.AddPmScoreForTrack(slot, pmDelta, eff, true); } catch { } // Trigger note-hit hooks as a virtual Perfect event (mana gain, damage, and Perfect-triggered skills). try { if (SkillBuilder.Instance != null) { string uid = $"virtualPerfect:{slot}:{Time.frameCount}:{Guid.NewGuid()}"; SkillBuilder.Instance.NotifyNoteHit(slot, "Perfect", SkillDefinition.NoteTypeTrigger.Tap, uid); } } catch { } } break; case EffectType.RedirectSelfDamageToAdjacent: foreach (var t in targets) { if (t == null) continue; var ally = t.GetComponent(); if (ally == null) continue; ally.ActivateSelfDamageRedirectToAdjacent(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); // Filter out dead combatants so they won't receive further effects (damage/score/etc.). // This prevents "dead units still get hit / still gain score" edge cases. uniq.RemoveAll(go => { if (go == null) return true; var ally = go.GetComponent(); if (ally != null) return ally.IsDead; var enemy = go.GetComponent(); if (enemy != null) return enemy.IsDead || enemy.currentHP <= 0; return false; }); // Debug: log resolved targets for diagnosis (guarded to avoid log spam in normal gameplay). if (uniq.Count == 0) LogVerbose($"[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 // Note: GameObject.Find only works on active objects. // We already use FindObjectsByType with Include Inactive above, which is safer. // If we really need to find "thisEnemy" by name even if inactive, we should do it differently. try { var single = GameObject.Find("thisEnemy"); if (single != null && !_cachedEnemies.Contains(single)) _cachedEnemies.Add(single); } catch { } // 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 public 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; // Skip dead targets (prevents edge cases where something bypasses ResolveTargets filtering). var targetAlly = target.GetComponent(); if (targetAlly != null && targetAlly.IsDead) return; var targetEnemy = target.GetComponent(); if (targetEnemy != null && (targetEnemy.IsDead || targetEnemy.currentHP <= 0)) return; // If target is an enemy, check if all enemies are already dead if (targetEnemy != null && AreAllEnemiesDead()) { LogVerbose($"[EffectSystem] Skip ApplyInstantDamage to {target.name} because all enemies are dead."); return; } var comp = target.GetComponent(); if (comp != null) { int beforeHP = 0; if (targetAlly != null) beforeHP = targetAlly.currentHP; else if (targetEnemy != null) beforeHP = targetEnemy.currentHP; comp.ReceiveDamage(amount, source); int afterHP = 0; if (targetAlly != null) afterHP = targetAlly.currentHP; else if (targetEnemy != null) afterHP = targetEnemy.currentHP; int actualDelta = beforeHP - afterHP; if (actualDelta != 0) QueueOrSpawnDamagePopup(target, targetAlly, targetEnemy, actualDelta); // 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 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) { var targetAlly = target.GetComponent(); var targetEnemy = target.GetComponent(); int beforeHP = 0; if (targetAlly != null) beforeHP = targetAlly.currentHP; else if (targetEnemy != null) beforeHP = targetEnemy.currentHP; comp.ReceiveHeal(amount, source); int afterHP = 0; if (targetAlly != null) afterHP = targetAlly.currentHP; else if (targetEnemy != null) afterHP = targetEnemy.currentHP; int actualDelta = afterHP - beforeHP; if (actualDelta != 0) { if (targetAlly != null) iNumberPrefabController.SpawnForAllyStatic(targetAlly.slotIndex, iNumberPrefabController.InstantNumberType.Heal, Mathf.Abs(actualDelta)); else if (targetEnemy != null) iNumberPrefabController.SpawnForEnemyStatic(iNumberPrefabController.InstantNumberType.Heal, Mathf.Abs(actualDelta)); } // 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, string iconId, int allySlotIndex, int enemyInstanceId) { try { 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; } var targetAlly = target.GetComponent(); var targetEnemy = target.GetComponent(); if (targetAlly != null && targetAlly.IsDead) yield break; if (targetEnemy != null && (targetEnemy.IsDead || targetEnemy.currentHP <= 0)) yield break; if (duration <= 0f) { if (targetEnemy != null && AreAllEnemiesDead()) { LogVerbose($"[EffectSystem] Skip DOT (immediate) to {target.name} because all enemies are dead."); yield break; } comp.ReceiveDamage(totalAmount, source); if (source != null && targetEnemy != null) { var ally = source.GetComponent(); if (ally != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamage(ally.slotIndex, totalAmount); } 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; if (targetAlly != null && targetAlly.IsDead) yield break; if (targetEnemy != null && (targetEnemy.IsDead || targetEnemy.currentHP <= 0)) yield break; if (targetEnemy != null && AreAllEnemiesDead()) { LogVerbose($"[EffectSystem] Stopping DOT to {target.name} because all enemies are dead."); yield break; } int beforeHP = 0; if (targetAlly != null) beforeHP = targetAlly.currentHP; else if (targetEnemy != null) beforeHP = targetEnemy.currentHP; comp.ReceiveDamage(perTick, source); int afterHP = 0; if (targetAlly != null) afterHP = targetAlly.currentHP; else if (targetEnemy != null) afterHP = targetEnemy.currentHP; int actualDelta = beforeHP - afterHP; if (actualDelta != 0) QueueOrSpawnDamagePopup(target, targetAlly, targetEnemy, actualDelta); if (source != null && targetEnemy != null) { var ally = source.GetComponent(); if (ally != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamage(ally.slotIndex, perTick); } if (targetAlly != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamageTaken(targetAlly.slotIndex, perTick); yield return new WaitForSeconds(tickInterval); elapsed += tickInterval; } } finally { if (!string.IsNullOrEmpty(iconId)) { if (allySlotIndex >= 0) iBudeffPrefabController.Instance?.UnregisterTimedEffect(allySlotIndex, iconId); else if (enemyInstanceId != 0) iBudeffPrefabController.Instance?.UnregisterEnemyTimedEffect(enemyInstanceId, iconId); } } } 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) { var targetAlly = target.GetComponent(); var targetEnemy = target.GetComponent(); int beforeHP = 0; if (targetAlly != null) beforeHP = targetAlly.currentHP; else if (targetEnemy != null) beforeHP = targetEnemy.currentHP; comp.ReceiveHeal(totalAmount, source); int afterHP = 0; if (targetAlly != null) afterHP = targetAlly.currentHP; else if (targetEnemy != null) afterHP = targetEnemy.currentHP; int actualDelta = afterHP - beforeHP; if (actualDelta != 0) { if (targetAlly != null) iNumberPrefabController.SpawnForAllyStatic(targetAlly.slotIndex, iNumberPrefabController.InstantNumberType.Heal, Mathf.Abs(actualDelta)); else if (targetEnemy != null) iNumberPrefabController.SpawnForEnemyStatic(iNumberPrefabController.InstantNumberType.Heal, Mathf.Abs(actualDelta)); } // 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; var targetAllyHot = target.GetComponent(); var targetEnemyHot = target.GetComponent(); while (elapsed < duration) { if (target == null) yield break; int beforeHP = 0; if (targetAllyHot != null) beforeHP = targetAllyHot.currentHP; else if (targetEnemyHot != null) beforeHP = targetEnemyHot.currentHP; comp.ReceiveHeal(perTick, source); int afterHP = 0; if (targetAllyHot != null) afterHP = targetAllyHot.currentHP; else if (targetEnemyHot != null) afterHP = targetEnemyHot.currentHP; int actualDelta = afterHP - beforeHP; if (actualDelta != 0) { if (targetAllyHot != null) iNumberPrefabController.SpawnForAllyStatic(targetAllyHot.slotIndex, iNumberPrefabController.InstantNumberType.Heal, Mathf.Abs(actualDelta)); else if (targetEnemyHot != null) iNumberPrefabController.SpawnForEnemyStatic(iNumberPrefabController.InstantNumberType.Heal, Mathf.Abs(actualDelta)); } // 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 void MarkAwaitingHitFx(GameObject target) { if (target == null) return; int id = target.GetInstanceID(); _awaitingHitFxTargets.Add(id); } private void FlushPendingDamagePopupAfterHit(GameObject target) { if (target == null) return; int id = target.GetInstanceID(); if (_awaitingHitFxTargets.Contains(id)) _awaitingHitFxTargets.Remove(id); if (!_pendingDamagePopupByTargetId.TryGetValue(id, out var signedValue) || signedValue == 0) return; _pendingDamagePopupByTargetId.Remove(id); var ally = target.GetComponent(); if (ally != null) { iNumberPrefabController.SpawnForAllyStatic(ally.slotIndex, iNumberPrefabController.InstantNumberType.Damage, signedValue); return; } if (target.GetComponent() != null) { iNumberPrefabController.SpawnForEnemyStatic(iNumberPrefabController.InstantNumberType.Damage, signedValue); } } private void QueueOrSpawnDamagePopup(GameObject target, AllyCombatant targetAlly, EnemyCombatant targetEnemy, int actualDelta) { if (target == null) return; if (actualDelta == 0) return; int id = target.GetInstanceID(); int signedValue = -Mathf.Abs(actualDelta); if (_awaitingHitFxTargets.Contains(id)) { if (_pendingDamagePopupByTargetId.TryGetValue(id, out var prev)) _pendingDamagePopupByTargetId[id] = prev + signedValue; else _pendingDamagePopupByTargetId[id] = signedValue; return; } if (targetAlly != null) { iNumberPrefabController.SpawnForAllyStatic(targetAlly.slotIndex, iNumberPrefabController.InstantNumberType.Damage, signedValue); return; } if (targetEnemy != null) { iNumberPrefabController.SpawnForEnemyStatic(iNumberPrefabController.InstantNumberType.Damage, signedValue); } } 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) { int before = ally.currentMana; ally.ModifyMana(Mathf.CeilToInt(totalAmount), true, true); int delta = ally.currentMana - before; if (delta != 0) { var type = delta > 0 ? iNumberPrefabController.InstantNumberType.ManaPlus : iNumberPrefabController.InstantNumberType.ManaMinus; iNumberPrefabController.SpawnForAllyStatic(ally.slotIndex, type, delta); } // Record stats if (totalAmount > 0f && 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) { int before = ally.currentMana; ally.ModifyMana(Mathf.CeilToInt(perTick), true, true); int delta = ally.currentMana - before; if (delta != 0) { var type = delta > 0 ? iNumberPrefabController.InstantNumberType.ManaPlus : iNumberPrefabController.InstantNumberType.ManaMinus; iNumberPrefabController.SpawnForAllyStatic(ally.slotIndex, type, delta); } // Record stats if (perTick > 0f && 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, string iconId) { try { if (ally == null) yield break; ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency + delta); yield return new WaitForSeconds(duration); if (ally == null) yield break; ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency - delta); } finally { if (ally != null && !string.IsNullOrEmpty(iconId)) iBudeffPrefabController.Instance?.UnregisterTimedEffect(ally.slotIndex, iconId); } } // Coroutine to apply temporary damageResistance change and revert after duration private IEnumerator ApplyTemporaryDamageResistanceChangeCoroutine(GameObject target, float delta, float duration, string iconId = null, int allySlotIndex = -1, int enemyInstanceId = 0) { try { if (target == null) yield break; var ally = target.GetComponent(); if (ally != null) { ally.damageResistance = ClampDamageResistance(ally.damageResistance + delta); yield return new WaitForSeconds(duration); if (ally == null) yield break; ally.damageResistance = ClampDamageResistance(ally.damageResistance - delta); yield break; } var enemy = target.GetComponent(); if (enemy != null) { enemy.damageResistance = ClampDamageResistance(enemy.damageResistance + delta); iBudeffPrefabController.Instance?.RefreshEnemyNow(enemy); yield return new WaitForSeconds(duration); if (enemy == null) yield break; enemy.damageResistance = ClampDamageResistance(enemy.damageResistance - delta); yield break; } } finally { if (!string.IsNullOrEmpty(iconId)) { if (allySlotIndex >= 0) iBudeffPrefabController.Instance?.UnregisterTimedEffect(allySlotIndex, iconId); else if (enemyInstanceId != 0) iBudeffPrefabController.Instance?.UnregisterEnemyTimedEffect(enemyInstanceId, iconId); } } } // Coroutine to apply temporary attack change for AllyCombatant private IEnumerator ApplyTemporaryAttackChangeCoroutine(AllyCombatant ally, int delta, float duration, string iconId) { try { if (ally == null) yield break; ally.ModifyAttack(delta); yield return new WaitForSeconds(duration); if (ally == null) yield break; ally.ModifyAttack(-delta); } finally { if (ally != null && !string.IsNullOrEmpty(iconId)) iBudeffPrefabController.Instance?.UnregisterTimedEffect(ally.slotIndex, iconId); } } private static float ClampDamageResistance(float value) { return Mathf.Min(value, 1f); } // Coroutine to apply temporary attack change for EnemyCombatant private IEnumerator ApplyTemporaryAttackChangeCoroutine(EnemyCombatant enemy, int delta, float duration, string iconId, int enemyInstanceId) { try { if (enemy == null) yield break; enemy.ModifyAttack(delta); iBudeffPrefabController.Instance?.RefreshEnemyNow(enemy); yield return new WaitForSeconds(duration); if (enemy == null) yield break; enemy.ModifyAttack(-delta); } finally { if (!string.IsNullOrEmpty(iconId) && enemyInstanceId != 0) iBudeffPrefabController.Instance?.UnregisterEnemyTimedEffect(enemyInstanceId, iconId); } } private IEnumerator ApplyTemporaryMaxHPDeltaCoroutine(AllyCombatant ally, int delta, float duration, string iconId) { try { if (ally == null) yield break; ally.SetMaxHP(Mathf.Max(1, ally.maxHP + delta), false); yield return new WaitForSeconds(duration); if (ally == null) yield break; ally.SetMaxHP(Mathf.Max(1, ally.maxHP - delta), false); } finally { if (ally != null && !string.IsNullOrEmpty(iconId)) iBudeffPrefabController.Instance?.UnregisterTimedEffect(ally.slotIndex, iconId); } } private IEnumerator ApplyTemporaryMaxHPDeltaCoroutine(EnemyCombatant enemy, int delta, float duration, string iconId, int enemyInstanceId) { try { if (enemy == null) yield break; enemy.SetMaxHP(Mathf.Max(1, enemy.maxHP + delta), false); iBudeffPrefabController.Instance?.RefreshEnemyNow(enemy); yield return new WaitForSeconds(duration); if (enemy == null) yield break; enemy.SetMaxHP(Mathf.Max(1, enemy.maxHP - delta), false); } finally { if (!string.IsNullOrEmpty(iconId) && enemyInstanceId != 0) iBudeffPrefabController.Instance?.UnregisterEnemyTimedEffect(enemyInstanceId, iconId); } } private IEnumerator ApplyTemporaryMaxManaDeltaCoroutine(AllyCombatant ally, int delta, float duration, string iconId) { try { if (ally == null) yield break; ally.SetMaxMana(Mathf.Max(1, ally.maxMana + delta), false); yield return new WaitForSeconds(duration); if (ally == null) yield break; ally.SetMaxMana(Mathf.Max(1, ally.maxMana - delta), false); } finally { if (ally != null && !string.IsNullOrEmpty(iconId)) iBudeffPrefabController.Instance?.UnregisterTimedEffect(ally.slotIndex, iconId); } } private IEnumerator ApplyTemporaryMaxManaDeltaCoroutine(EnemyCombatant enemy, int delta, float duration, string iconId, int enemyInstanceId) { try { if (enemy == null) yield break; enemy.SetMaxMana(Mathf.Max(1, enemy.maxMana + delta), false); iBudeffPrefabController.Instance?.RefreshEnemyNow(enemy); yield return new WaitForSeconds(duration); if (enemy == null) yield break; enemy.SetMaxMana(Mathf.Max(1, enemy.maxMana - delta), false); } finally { if (!string.IsNullOrEmpty(iconId) && enemyInstanceId != 0) iBudeffPrefabController.Instance?.UnregisterEnemyTimedEffect(enemyInstanceId, iconId); } } #endregion }