1568 lines
69 KiB
C#
1568 lines
69 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// Documentation text normalized.
|
|
/// Documentation text normalized.
|
|
/// Documentation text normalized.
|
|
///
|
|
/// Documentation text normalized.
|
|
///
|
|
/// Documentation text normalized.
|
|
/// </summary>
|
|
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<GameObject> _cachedAllies = new List<GameObject>(8);
|
|
private int _cachedEnemiesFrame = -1;
|
|
private readonly List<GameObject> _cachedEnemies = new List<GameObject>(8);
|
|
|
|
private readonly HashSet<int> _awaitingHitFxTargets = new HashSet<int>();
|
|
private readonly Dictionary<int, int> _pendingDamagePopupByTargetId = new Dictionary<int, int>(64);
|
|
|
|
private void Awake()
|
|
{
|
|
if (Instance == null) Instance = this;
|
|
else Destroy(gameObject);
|
|
}
|
|
|
|
private void LogVerbose(string message)
|
|
{
|
|
if (GameConfig.verboseLogs) Debug.Log(message);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Documentation text normalized.
|
|
/// Documentation text normalized.
|
|
/// Documentation text normalized.
|
|
/// Documentation text normalized.
|
|
/// Documentation text normalized.
|
|
/// Documentation text normalized.
|
|
/// Documentation text normalized.
|
|
/// Documentation text normalized.
|
|
/// </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)
|
|
{
|
|
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<EnemyCombatant>();
|
|
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<EnemyCombatant>() ?? t.GetComponentInChildren<EnemyCombatant>(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<AllyCombatant>() ?? t.GetComponentInChildren<AllyCombatant>(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<AllyCombatant>();
|
|
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<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)
|
|
{
|
|
int delta = Mathf.CeilToInt(amount);
|
|
if (duration <= 0f)
|
|
{
|
|
ally.SetMaxHP(ally.maxHP + delta, false);
|
|
if (delta > 0) ally.SetCurrentHP(ally.currentHP + 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<EnemyCombatant>(out var enemy))
|
|
{
|
|
int delta = Mathf.CeilToInt(amount);
|
|
if (duration <= 0f)
|
|
{
|
|
enemy.SetMaxHP(enemy.maxHP + delta, false);
|
|
if (delta > 0) enemy.currentHP = Mathf.Clamp(enemy.currentHP + delta, 0, enemy.maxHP);
|
|
iBudeffPrefabController.Instance?.RefreshEnemyNow(enemy);
|
|
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<AllyCombatant>();
|
|
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<EnemyCombatant>(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<AllyCombatant>();
|
|
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<EnemyCombatant>(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<AllyCombatant>();
|
|
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<EnemyCombatant>(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<AllyCombatant>();
|
|
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<AllyCombatant>();
|
|
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<AllyCombatant>();
|
|
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<EnemyCombatant>(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<AllyCombatant>();
|
|
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<EnemyCombatant>(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<AllyCombatant>() ?? source.GetComponentInChildren<AllyCombatant>(true);
|
|
if (redirector == null && targets != null && targets.Count > 0)
|
|
redirector = targets[0].GetComponent<AllyCombatant>() ?? targets[0].GetComponentInChildren<AllyCombatant>(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<AllyCombatant>();
|
|
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<EnemyCombatant>(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<AllyCombatant>();
|
|
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<EnemyCombatant>(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<AllyCombatant>();
|
|
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<AllyCombatant>();
|
|
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<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 != null ? teamUIController.Instance : UnityEngine.Object.FindAnyObjectByType<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);
|
|
|
|
// 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<AllyCombatant>();
|
|
if (ally != null) return ally.IsDead;
|
|
var enemy = go.GetComponent<EnemyCombatant>();
|
|
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<GameObject> 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<GameObject> 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<EnemyCombatant>(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<EnemyCombatant>();
|
|
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<AllyCombatant>();
|
|
if (targetAlly != null && targetAlly.IsDead) return;
|
|
var targetEnemy = target.GetComponent<EnemyCombatant>();
|
|
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<ICombatant>();
|
|
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<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
|
|
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)
|
|
{
|
|
var targetAlly = target.GetComponent<AllyCombatant>();
|
|
var targetEnemy = target.GetComponent<EnemyCombatant>();
|
|
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<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, string iconId, int allySlotIndex, int enemyInstanceId)
|
|
{
|
|
try
|
|
{
|
|
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;
|
|
}
|
|
|
|
var targetAlly = target.GetComponent<AllyCombatant>();
|
|
var targetEnemy = target.GetComponent<EnemyCombatant>();
|
|
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<AllyCombatant>();
|
|
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<AllyCombatant>();
|
|
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<ICombatant>();
|
|
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<AllyCombatant>();
|
|
var targetEnemy = target.GetComponent<EnemyCombatant>();
|
|
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<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;
|
|
var targetAllyHot = target.GetComponent<AllyCombatant>();
|
|
var targetEnemyHot = target.GetComponent<EnemyCombatant>();
|
|
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<AllyCombatant>();
|
|
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<AllyCombatant>();
|
|
if (ally != null)
|
|
{
|
|
iNumberPrefabController.SpawnForAllyStatic(ally.slotIndex, iNumberPrefabController.InstantNumberType.Damage, signedValue);
|
|
return;
|
|
}
|
|
|
|
if (target.GetComponent<EnemyCombatant>() != 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<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)
|
|
{
|
|
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<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)
|
|
{
|
|
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<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, 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<AllyCombatant>();
|
|
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<EnemyCombatant>();
|
|
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);
|
|
if (delta > 0) ally.SetCurrentHP(ally.currentHP + 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);
|
|
if (delta > 0) enemy.currentHP = Mathf.Clamp(enemy.currentHP + delta, 0, enemy.maxHP);
|
|
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
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|