955 lines
40 KiB
C#
955 lines
40 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
/// <summary>
|
||
/// EffectSystem �ṩ����Ч����ͳһ�ַ��ӿڡ�
|
||
/// - ѡ������Χ��Selector������Ŀ�꼯�ϣ��Լ���ȫ���Ѿ������Լ����Ѿ��������Ѿ������ϵ��ˡ������ˣ�
|
||
/// - Ч�����ͣ�EffectType�������˼�ʱ/�������˺�/����/����/����ȡ�
|
||
///
|
||
/// Ŀ��ʵ��Ӧʵ�� ICombatant �ӿ��Ա� EffectSystem �ܵ���ͳһ�����������˺�/����/����ȣ����û��ʵ�֣�������־�����ѡ�
|
||
///
|
||
/// �ýű�Ϊ��ܣ����������弼�����ã�ֻ�ṩ ApplyEffect(...) �ȷ����������ű����á�
|
||
/// </summary>
|
||
public class EffectSystem : MonoBehaviour
|
||
{
|
||
public static EffectSystem Instance { get; private set; }
|
||
|
||
private void Awake()
|
||
{
|
||
if (Instance == null) Instance = this;
|
||
else Destroy(gameObject);
|
||
}
|
||
|
||
/// <summary>
|
||
/// ApplyEffect: ��ָ��ѡ������Χ��Ŀ��Ӧ��Ч����
|
||
/// - selector: Ŀ��ѡ����
|
||
/// - effectType: Ч�����ͣ���ʱ/�����ȣ�
|
||
/// - amount: ��ֵ���˺�������������Ч��ǿ�ȣ�
|
||
/// - duration: ����ʱ�䣨�Գ���Ч���� Buff/Debuff ��Ч������ʱЧ����Ϊ 0
|
||
/// - source: �����ߣ����� Self/AlliesExceptSelf �жϻ��¼��
|
||
/// - specificTarget: �� selector Ϊ CurrentEnemies ��ֻ��ָ������ʱ�ɴ������Ŀ��
|
||
/// - tickInterval: ����Ч���� tick ������룩��Ĭ�� 1 ��
|
||
/// </summary>
|
||
public void ApplyEffect(Selector selector, EffectType effectType, float amount, float duration = 0f, GameObject source = null, GameObject specificTarget = null, float tickInterval = 1f)
|
||
{
|
||
// If caller provided a specificTarget, prefer it as the single target regardless of selector.
|
||
// Many callers pass specificTarget when they intend to hit a single unit (ally or enemy).
|
||
List<GameObject> targets;
|
||
if (specificTarget != null)
|
||
{
|
||
targets = new List<GameObject> { specificTarget };
|
||
}
|
||
else
|
||
{
|
||
targets = ResolveTargets(selector, source, specificTarget);
|
||
}
|
||
|
||
if (targets == null || targets.Count == 0)
|
||
{
|
||
Debug.LogWarning($"[EffectSystem] No targets resolved for selector={selector} specificTarget={(specificTarget ? specificTarget.name : "null")}");
|
||
return;
|
||
}
|
||
|
||
switch (effectType)
|
||
{
|
||
case EffectType.DamageSingleEnemy:
|
||
// apply instantaneous damage to first applicable enemy in targets
|
||
if (targets.Count > 0)
|
||
ApplyInstantDamage(targets[0], amount, source);
|
||
break;
|
||
|
||
case EffectType.DamageOverTimeEnemy:
|
||
foreach (var t in targets)
|
||
StartCoroutine(ApplyDamageOverTimeCoroutine(t, amount, duration, tickInterval, source));
|
||
break;
|
||
|
||
case EffectType.HealSingleEnemy:
|
||
// heal single enemy (first target)
|
||
if (targets.Count > 0)
|
||
ApplyInstantHeal(targets[0], amount, source);
|
||
break;
|
||
|
||
case EffectType.HealOverTimeEnemy:
|
||
foreach (var t in targets)
|
||
StartCoroutine(ApplyHealOverTimeCoroutine(t, amount, duration, tickInterval, source));
|
||
break;
|
||
|
||
case EffectType.DamageSingleAlly:
|
||
// damage a single ally (first target) - if selector resolved multiple allies, apply to all to support "AllAllies + DamageSingleAlly" designer usage
|
||
if (targets.Count == 1)
|
||
{
|
||
ApplyInstantDamage(targets[0], amount, source);
|
||
}
|
||
else if (targets.Count > 1)
|
||
{
|
||
foreach (var t in targets)
|
||
ApplyInstantDamage(t, amount, source);
|
||
}
|
||
break;
|
||
|
||
case EffectType.DamageOverTimeAlly:
|
||
foreach (var t in targets)
|
||
StartCoroutine(ApplyDamageOverTimeCoroutine(t, amount, duration, tickInterval, source));
|
||
break;
|
||
|
||
case EffectType.HealSingleSelf:
|
||
if (targets.Count > 0)
|
||
ApplyInstantHeal(targets[0], amount, source);
|
||
break;
|
||
|
||
case EffectType.HealOverTimeSelf:
|
||
foreach (var t in targets)
|
||
StartCoroutine(ApplyHealOverTimeCoroutine(t, amount, duration, tickInterval, source));
|
||
break;
|
||
|
||
case EffectType.HealGroupSingle:
|
||
foreach (var t in targets)
|
||
ApplyInstantHeal(t, amount, source);
|
||
break;
|
||
|
||
case EffectType.HealGroupOverTime:
|
||
foreach (var t in targets)
|
||
StartCoroutine(ApplyHealOverTimeCoroutine(t, amount, duration, tickInterval, source));
|
||
break;
|
||
|
||
case EffectType.BuffDuration:
|
||
// create a simple buff with id+duration; additional fields can be filled by caller when using Buff instances directly
|
||
Buff buff = new Buff { buffId = Guid.NewGuid().ToString(), duration = duration };
|
||
foreach (var t in targets)
|
||
StartCoroutine(ApplyTimedBuffCoroutine(t, buff, source));
|
||
break;
|
||
|
||
case EffectType.DebuffDuration:
|
||
Buff debuff = new Buff { buffId = Guid.NewGuid().ToString(), duration = duration };
|
||
foreach (var t in targets)
|
||
StartCoroutine(ApplyTimedDebuffCoroutine(t, debuff, source));
|
||
break;
|
||
|
||
case EffectType.IncreaseManaOverTime:
|
||
foreach (var t in targets)
|
||
StartCoroutine(ApplyIncreaseManaOverTimeCoroutine(t, amount, duration, tickInterval, source));
|
||
break;
|
||
|
||
case EffectType.ReduceEnemyHealOverTime:
|
||
// create a debuff that marks heal reduction in its description; enforcement depends on ICombatant.ReceiveHeal implementation
|
||
Buff healReductionDebuff = new Buff { buffId = Guid.NewGuid().ToString(), duration = duration, description = "ReduceHeal" };
|
||
foreach (var t in targets)
|
||
StartCoroutine(ApplyTimedDebuffCoroutine(t, healReductionDebuff, source));
|
||
break;
|
||
|
||
case EffectType.ScoreMultiplier:
|
||
// create a buff that multiplies scoring efficiency
|
||
Buff scoreBuff = new Buff { buffId = Guid.NewGuid().ToString(), duration = duration, scoreMultiplier = amount };
|
||
foreach (var t in targets)
|
||
StartCoroutine(ApplyTimedBuffCoroutine(t, scoreBuff, source));
|
||
break;
|
||
|
||
case EffectType.AddScore:
|
||
// amount interpreted as integer delta to add immediately to ally tracks
|
||
foreach (var t in targets)
|
||
{
|
||
if (t == null) continue;
|
||
var ally = t.GetComponent<AllyCombatant>();
|
||
if (ally != null)
|
||
{
|
||
ally.AddScoreDirect(Mathf.CeilToInt(amount));
|
||
}
|
||
else
|
||
{
|
||
// if target does not have AllyCombatant, try to find ICombatant and warn
|
||
var ic = t.GetComponent<ICombatant>();
|
||
if (ic != null)
|
||
{
|
||
Debug.LogWarning($"[EffectSystem] AddScore: target {t.name} implements ICombatant but is not AllyCombatant; cannot AddScoreDirect");
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"[EffectSystem] AddScore: target {t.name} does not support AddScore");
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
|
||
// New handling for max HP / max Mana modifications via EffectSystem (mirrors direct path in SkillBuilder)
|
||
case EffectType.IncreaseMaxHP:
|
||
foreach (var t in targets)
|
||
{
|
||
if (t == null) continue;
|
||
var ally = t.GetComponent<AllyCombatant>();
|
||
if (ally != null)
|
||
{
|
||
ally.SetMaxHP(ally.maxHP + Mathf.CeilToInt(amount), false);
|
||
}
|
||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy))
|
||
{
|
||
enemy.SetMaxHP(enemy.maxHP + Mathf.CeilToInt(amount), false);
|
||
}
|
||
}
|
||
break;
|
||
|
||
case EffectType.DecreaseMaxHP:
|
||
foreach (var t in targets)
|
||
{
|
||
if (t == null) continue;
|
||
var ally = t.GetComponent<AllyCombatant>();
|
||
if (ally != null)
|
||
{
|
||
ally.SetMaxHP(Mathf.Max(1, ally.maxHP - Mathf.CeilToInt(amount)), false);
|
||
}
|
||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy2))
|
||
{
|
||
enemy2.SetMaxHP(Mathf.Max(1, enemy2.maxHP - Mathf.CeilToInt(amount)), false);
|
||
}
|
||
}
|
||
break;
|
||
|
||
case EffectType.IncreaseMaxMana:
|
||
foreach (var t in targets)
|
||
{
|
||
if (t == null) continue;
|
||
var ally = t.GetComponent<AllyCombatant>();
|
||
if (ally != null)
|
||
{
|
||
ally.SetMaxMana(ally.maxMana + Mathf.CeilToInt(amount), false);
|
||
}
|
||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy3))
|
||
{
|
||
enemy3.SetMaxMana(enemy3.maxMana + Mathf.CeilToInt(amount), false);
|
||
}
|
||
}
|
||
break;
|
||
|
||
case EffectType.DecreaseMaxMana:
|
||
foreach (var t in targets)
|
||
{
|
||
if (t == null) continue;
|
||
var ally = t.GetComponent<AllyCombatant>();
|
||
if (ally != null)
|
||
{
|
||
ally.SetMaxMana(Mathf.Max(1, ally.maxMana - Mathf.CeilToInt(amount)), false);
|
||
}
|
||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy4))
|
||
{
|
||
enemy4.SetMaxMana(Mathf.Max(1, enemy4.maxMana - Mathf.CeilToInt(amount)), false);
|
||
}
|
||
}
|
||
break;
|
||
|
||
// New: Increase/Decrease ScoreEfficiency handled by EffectSystem (mirrors SkillBuilder direct path)
|
||
case EffectType.IncreaseScoreEfficiency:
|
||
foreach (var t in targets)
|
||
{
|
||
if (t == null) continue;
|
||
var ally = t.GetComponent<AllyCombatant>();
|
||
if (ally != null)
|
||
{
|
||
if (duration <= 0f)
|
||
{
|
||
ally.scoreEfficiency += amount;
|
||
}
|
||
else
|
||
{
|
||
// temporary additive change, revert after duration
|
||
StartCoroutine(ApplyTemporaryScoreEfficiencyChangeCoroutine(ally, amount, duration));
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
|
||
case EffectType.DecreaseScoreEfficiency:
|
||
foreach (var t in targets)
|
||
{
|
||
if (t == null) continue;
|
||
var ally = t.GetComponent<AllyCombatant>();
|
||
if (ally != null)
|
||
{
|
||
if (duration <= 0f)
|
||
{
|
||
ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency - amount);
|
||
}
|
||
else
|
||
{
|
||
StartCoroutine(ApplyTemporaryScoreEfficiencyChangeCoroutine(ally, -amount, duration));
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
|
||
// New: Increase/Decrease Attack handled by EffectSystem (mirrors SkillBuilder direct path)
|
||
case EffectType.IncreaseAttack:
|
||
foreach (var t in targets)
|
||
{
|
||
if (t == null) continue;
|
||
var ally = t.GetComponent<AllyCombatant>();
|
||
if (ally != null)
|
||
{
|
||
int delta = Mathf.CeilToInt(amount);
|
||
if (duration <= 0f)
|
||
{
|
||
ally.attack += delta;
|
||
}
|
||
else
|
||
{
|
||
StartCoroutine(ApplyTemporaryAttackChangeCoroutine(ally, delta, duration));
|
||
}
|
||
}
|
||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy5))
|
||
{
|
||
int deltaE = Mathf.CeilToInt(amount);
|
||
if (duration <= 0f)
|
||
enemy5.attack += deltaE;
|
||
else
|
||
StartCoroutine(ApplyTemporaryAttackChangeCoroutine(enemy5, deltaE, duration));
|
||
}
|
||
}
|
||
break;
|
||
|
||
case EffectType.DecreaseAttack:
|
||
foreach (var t in targets)
|
||
{
|
||
if (t == null) continue;
|
||
var ally = t.GetComponent<AllyCombatant>();
|
||
if (ally != null)
|
||
{
|
||
int delta = Mathf.CeilToInt(amount);
|
||
if (duration <= 0f)
|
||
{
|
||
ally.attack = Mathf.Max(0, ally.attack - delta);
|
||
}
|
||
else
|
||
{
|
||
// apply decrease now, revert after duration
|
||
StartCoroutine(ApplyTemporaryAttackChangeCoroutine(ally, -delta, duration));
|
||
}
|
||
}
|
||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy6))
|
||
{
|
||
int deltaE = Mathf.CeilToInt(amount);
|
||
if (duration <= 0f)
|
||
enemy6.attack = Mathf.Max(0, enemy6.attack - deltaE);
|
||
else
|
||
StartCoroutine(ApplyTemporaryAttackChangeCoroutine(enemy6, -deltaE, duration));
|
||
}
|
||
}
|
||
break;
|
||
|
||
case EffectType.IncreaseDamageResistance:
|
||
foreach (var t in targets)
|
||
{
|
||
if (t == null) continue;
|
||
var ally = t.GetComponent<AllyCombatant>();
|
||
if (ally != null)
|
||
{
|
||
// amount interpreted as additive delta (0..1). Clamp result 0..0.9 to avoid invulnerability
|
||
if (duration <= 0f)
|
||
{
|
||
ally.damageResistance = Mathf.Clamp01(ally.damageResistance + amount);
|
||
Debug.Log($"[EffectSystem] IncreaseDamageResistance applied to {t.name}: new resistance={ally.damageResistance}");
|
||
}
|
||
else
|
||
{
|
||
StartCoroutine(ApplyTemporaryDamageResistanceChangeCoroutine(ally.gameObject, amount, duration));
|
||
}
|
||
}
|
||
else if (t.TryGetComponent<EnemyCombatant>(out var enemyR))
|
||
{
|
||
if (duration <= 0f)
|
||
{
|
||
enemyR.damageResistance = Mathf.Clamp01(enemyR.damageResistance + amount);
|
||
Debug.Log($"[EffectSystem] IncreaseDamageResistance applied to enemy {t.name}: new resistance={enemyR.damageResistance}");
|
||
}
|
||
else
|
||
{
|
||
StartCoroutine(ApplyTemporaryDamageResistanceChangeCoroutine(enemyR.gameObject, amount, duration));
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
|
||
case EffectType.DecreaseDamageResistance:
|
||
foreach (var t in targets)
|
||
{
|
||
if (t == null) continue;
|
||
var ally = t.GetComponent<AllyCombatant>();
|
||
if (ally != null)
|
||
{
|
||
if (duration <= 0f)
|
||
{
|
||
ally.damageResistance = Mathf.Clamp01(ally.damageResistance - amount);
|
||
Debug.Log($"[EffectSystem] DecreaseDamageResistance applied to {t.name}: new resistance={ally.damageResistance}");
|
||
}
|
||
else
|
||
{
|
||
// apply negative delta
|
||
StartCoroutine(ApplyTemporaryDamageResistanceChangeCoroutine(ally.gameObject, -amount, duration));
|
||
}
|
||
}
|
||
else if (t.TryGetComponent<EnemyCombatant>(out var enemyR2))
|
||
{
|
||
if (duration <= 0f)
|
||
{
|
||
enemyR2.damageResistance = Mathf.Clamp01(enemyR2.damageResistance - amount);
|
||
Debug.Log($"[EffectSystem] DecreaseDamageResistance applied to enemy {t.name}: new resistance={enemyR2.damageResistance}");
|
||
}
|
||
else
|
||
{
|
||
StartCoroutine(ApplyTemporaryDamageResistanceChangeCoroutine(enemyR2.gameObject, -amount, duration));
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
|
||
default:
|
||
Debug.LogWarning($"[EffectSystem] Unhandled EffectType {effectType}");
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Resolve targets based on selector. Tries several strategies:
|
||
// - If selector == Self -> source only
|
||
// - AllAllies / AllAlliesExceptSelf -> find GameObjects named ally_01..ally_05, and also fall back to objects with tag "Ally"
|
||
// - AdjacentAllies -> use AllyCombatant.slotIndex (from source) and teamUIController helpers
|
||
// - CurrentEnemies -> specificTarget if provided else all objects with tag "Enemy"
|
||
// - AllEntities -> find all enemies + allies
|
||
private List<GameObject> ResolveTargets(Selector selector, GameObject source, GameObject specificTarget)
|
||
{
|
||
List<GameObject> list = new List<GameObject>();
|
||
|
||
// prefer teamUIController when available for authoritative ally/enemy objects
|
||
var ui = teamUIController.Instance ?? FindObjectOfType<teamUIController>();
|
||
|
||
switch (selector)
|
||
{
|
||
case Selector.Self:
|
||
if (source != null) list.Add(source);
|
||
break;
|
||
|
||
case Selector.AllAllies:
|
||
if (ui != null)
|
||
{
|
||
int count = ui.allySlotIds != null ? ui.allySlotIds.Count : 5;
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var go = ui.GetAllyObjectBySlot(i);
|
||
if (go != null && !list.Contains(go)) list.Add(go);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
list.AddRange(FindAllAllies());
|
||
}
|
||
break;
|
||
|
||
case Selector.AllAlliesExceptSelf:
|
||
if (ui != null)
|
||
{
|
||
int count = ui.allySlotIds != null ? ui.allySlotIds.Count : 5;
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var go = ui.GetAllyObjectBySlot(i);
|
||
if (go != null && !list.Contains(go)) list.Add(go);
|
||
}
|
||
if (source != null)
|
||
list.RemoveAll(g => g == null || g == source || g.gameObject == source.gameObject);
|
||
}
|
||
else
|
||
{
|
||
list.AddRange(FindAllAllies());
|
||
if (source != null)
|
||
list.RemoveAll(g => g == null || g == source || g.gameObject == source.gameObject);
|
||
}
|
||
break;
|
||
|
||
case Selector.AdjacentAllies:
|
||
// find adjacent allies of the source (left/right). Use teamUIController if available.
|
||
if (source == null)
|
||
{
|
||
Debug.LogWarning("[EffectSystem] AdjacentAllies selector requires a non-null source GameObject");
|
||
break;
|
||
}
|
||
|
||
// try to use AllyCombatant.slotIndex if present
|
||
var allyComp = source.GetComponent<AllyCombatant>();
|
||
int slotIndex = -1;
|
||
if (allyComp != null)
|
||
{
|
||
slotIndex = allyComp.slotIndex;
|
||
}
|
||
else
|
||
{
|
||
// try to parse name like "ally_01"
|
||
var n = source.name;
|
||
for (int i = 1; i <= 5; i++)
|
||
{
|
||
if (n.Contains($"ally_0{i}")) { slotIndex = i - 1; break; }
|
||
}
|
||
}
|
||
|
||
if (slotIndex >= 0)
|
||
{
|
||
if (ui != null)
|
||
{
|
||
try
|
||
{
|
||
var adj = ui.GetAdjacentAllyIndices(slotIndex);
|
||
foreach (var idx in adj)
|
||
{
|
||
var go = ui.GetAllyObjectBySlot(idx);
|
||
if (go != null) list.Add(go);
|
||
else
|
||
{
|
||
var named = GameObject.Find($"ally_0{idx + 1}");
|
||
if (named != null) list.Add(named);
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.LogWarning($"[EffectSystem] teamUIController does not expose adjacent helpers: {ex.Message}");
|
||
int left = slotIndex - 1;
|
||
int right = slotIndex + 1;
|
||
if (left >= 0)
|
||
{
|
||
var g = GameObject.Find($"ally_0{left + 1}"); if (g != null) list.Add(g);
|
||
}
|
||
if (right <= 4)
|
||
{
|
||
var g = GameObject.Find($"ally_0{right + 1}"); if (g != null) list.Add(g);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
int left = slotIndex - 1;
|
||
int right = slotIndex + 1;
|
||
if (left >= 0)
|
||
{
|
||
var g = GameObject.Find($"ally_0{left + 1}"); if (g != null) list.Add(g);
|
||
}
|
||
if (right <= 4)
|
||
{
|
||
var g = GameObject.Find($"ally_0{right + 1}"); if (g != null) list.Add(g);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning("[EffectSystem] Could not determine slot index for source when resolving AdjacentAllies");
|
||
}
|
||
|
||
break;
|
||
|
||
case Selector.CurrentEnemies:
|
||
if (specificTarget != null) list.Add(specificTarget);
|
||
else list.AddRange(FindAllEnemies());
|
||
break;
|
||
|
||
case Selector.AllEntities:
|
||
list.AddRange(FindAllAllies());
|
||
list.AddRange(FindAllEnemies());
|
||
break;
|
||
}
|
||
|
||
// remove nulls and duplicates
|
||
list.RemoveAll(x => x == null);
|
||
var uniq = new List<GameObject>();
|
||
foreach (var g in list)
|
||
if (!uniq.Contains(g)) uniq.Add(g);
|
||
|
||
// Debug: log resolved targets for diagnosis
|
||
if (uniq.Count == 0)
|
||
Debug.LogWarning($"[EffectSystem] ResolveTargets -> selector={selector} returned 0 targets (source={(source ? source.name : "null")}, specificTarget={(specificTarget ? specificTarget.name : "null")})");
|
||
else
|
||
Debug.Log($"[EffectSystem] ResolveTargets -> selector={selector} resolved {uniq.Count} targets: {string.Join(",", uniq.ConvertAll(x => x != null ? x.name : "null"))}");
|
||
|
||
return uniq;
|
||
}
|
||
|
||
private IEnumerable<GameObject> FindAllAllies()
|
||
{
|
||
List<GameObject> allies = new List<GameObject>();
|
||
// prefer teamUIController if available to get authoritative ally objects
|
||
var ui = teamUIController.Instance;
|
||
if (ui != null)
|
||
{
|
||
try
|
||
{
|
||
int count = ui.allySlotIds != null ? ui.allySlotIds.Count : 5;
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var go = ui.GetAllyObjectBySlot(i);
|
||
if (go != null && !allies.Contains(go)) allies.Add(go);
|
||
}
|
||
}
|
||
catch { /* ignore and fallback */ }
|
||
}
|
||
|
||
// fallback: try names ally_01..ally_05 (keeps compatibility with legacy scene setups)
|
||
for (int i = 1; i <= 5; i++)
|
||
{
|
||
string name = $"ally_0{i}"; // ally_01..ally_05
|
||
var go = GameObject.Find(name);
|
||
if (go != null && !allies.Contains(go)) allies.Add(go);
|
||
}
|
||
|
||
// also include objects tagged "Ally"
|
||
try
|
||
{
|
||
var tagged = GameObject.FindGameObjectsWithTag("Ally");
|
||
foreach (var g in tagged)
|
||
if (!allies.Contains(g)) allies.Add(g);
|
||
}
|
||
catch { /* tag might not exist; ignore */ }
|
||
|
||
return allies;
|
||
}
|
||
|
||
private IEnumerable<GameObject> FindAllEnemies()
|
||
{
|
||
List<GameObject> enemies = new List<GameObject>();
|
||
|
||
// Prefer explicit EnemyCombatant components (more robust than tags)
|
||
try
|
||
{
|
||
var comps = GameObject.FindObjectsOfType<EnemyCombatant>(true);
|
||
foreach (var c in comps)
|
||
{
|
||
if (c != null && c.gameObject != null && !enemies.Contains(c.gameObject)) enemies.Add(c.gameObject);
|
||
}
|
||
}
|
||
catch { }
|
||
|
||
// Also include a single named instance if present
|
||
var single = GameObject.Find("thisEnemy");
|
||
if (single != null && !enemies.Contains(single)) enemies.Add(single);
|
||
|
||
// Finally include any objects tagged "Enemy" (if tag exists)
|
||
try
|
||
{
|
||
var tagged = GameObject.FindGameObjectsWithTag("Enemy");
|
||
foreach (var g in tagged)
|
||
if (!enemies.Contains(g)) enemies.Add(g);
|
||
}
|
||
catch { /* ignore missing tag */ }
|
||
|
||
return enemies;
|
||
}
|
||
|
||
#region Instant / OverTime Helpers
|
||
private bool AreAllEnemiesDead()
|
||
{
|
||
var enemies = GameObject.FindObjectsOfType<EnemyCombatant>(true);
|
||
if (enemies.Length == 0) return true;
|
||
|
||
foreach (var e in enemies)
|
||
{
|
||
if (e != null && !e.IsDead && e.currentHP > 0)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
private void ApplyInstantDamage(GameObject target, float amount, GameObject source)
|
||
{
|
||
if (target == null) return;
|
||
|
||
// If target is an enemy, check if all enemies are already dead
|
||
if (target.GetComponent<EnemyCombatant>() != null && AreAllEnemiesDead())
|
||
{
|
||
Debug.Log($"[EffectSystem] Skip ApplyInstantDamage to {target.name} because all enemies are dead.");
|
||
return;
|
||
}
|
||
|
||
var comp = target.GetComponent<ICombatant>();
|
||
if (comp != null)
|
||
{
|
||
comp.ReceiveDamage(amount, source);
|
||
|
||
// Record stats if source is an ally
|
||
if (source != null && target.GetComponent<EnemyCombatant>() != null)
|
||
{
|
||
var ally = source.GetComponent<AllyCombatant>();
|
||
if (ally != null && teamUIController.Instance != null)
|
||
{
|
||
teamUIController.Instance.RecordDamage(ally.slotIndex, amount);
|
||
}
|
||
}
|
||
|
||
// Record damage taken if target is an ally
|
||
var targetAlly = target.GetComponent<AllyCombatant>();
|
||
if (targetAlly != null && teamUIController.Instance != null)
|
||
{
|
||
teamUIController.Instance.RecordDamageTaken(targetAlly.slotIndex, amount);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"[EffectSystem] Target {target.name} does not implement ICombatant -> cannot apply damage");
|
||
}
|
||
}
|
||
|
||
private void ApplyInstantHeal(GameObject target, float amount, GameObject source)
|
||
{
|
||
if (target == null) return;
|
||
var comp = target.GetComponent<ICombatant>();
|
||
if (comp != null)
|
||
{
|
||
comp.ReceiveHeal(amount, source);
|
||
|
||
// Record stats if source is an ally
|
||
if (source != null)
|
||
{
|
||
var ally = source.GetComponent<AllyCombatant>();
|
||
if (ally != null && teamUIController.Instance != null)
|
||
{
|
||
teamUIController.Instance.RecordHeal(ally.slotIndex, amount);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"[EffectSystem] Target {target.name} does not implement ICombatant -> cannot apply heal");
|
||
}
|
||
}
|
||
|
||
private IEnumerator ApplyDamageOverTimeCoroutine(GameObject target, float totalAmount, float duration, float tickInterval, GameObject source)
|
||
{
|
||
if (target == null) yield break;
|
||
var comp = target.GetComponent<ICombatant>();
|
||
if (comp == null)
|
||
{
|
||
Debug.LogWarning($"[EffectSystem] Target {target.name} does not implement ICombatant -> cannot apply DOT");
|
||
yield break;
|
||
}
|
||
|
||
if (duration <= 0f)
|
||
{
|
||
// check death state for immediate damage
|
||
if (target.GetComponent<EnemyCombatant>() != null && AreAllEnemiesDead())
|
||
{
|
||
Debug.Log($"[EffectSystem] Skip DOT (immediate) to {target.name} because all enemies are dead.");
|
||
yield break;
|
||
}
|
||
|
||
comp.ReceiveDamage(totalAmount, source);
|
||
// Record stats
|
||
if (source != null && target.GetComponent<EnemyCombatant>() != null)
|
||
{
|
||
var ally = source.GetComponent<AllyCombatant>();
|
||
if (ally != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamage(ally.slotIndex, totalAmount);
|
||
}
|
||
// Record damage taken
|
||
var targetAlly = target.GetComponent<AllyCombatant>();
|
||
if (targetAlly != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamageTaken(targetAlly.slotIndex, totalAmount);
|
||
yield break;
|
||
}
|
||
|
||
int ticks = Mathf.Max(1, Mathf.CeilToInt(duration / tickInterval));
|
||
float perTick = totalAmount / ticks;
|
||
float elapsed = 0f;
|
||
while (elapsed < duration)
|
||
{
|
||
if (target == null) yield break;
|
||
|
||
// check death state inside loop
|
||
if (target.GetComponent<EnemyCombatant>() != null && AreAllEnemiesDead())
|
||
{
|
||
Debug.Log($"[EffectSystem] Stopping DOT to {target.name} because all enemies are dead.");
|
||
yield break;
|
||
}
|
||
|
||
comp.ReceiveDamage(perTick, source);
|
||
// Record stats
|
||
if (source != null && target.GetComponent<EnemyCombatant>() != null)
|
||
{
|
||
var ally = source.GetComponent<AllyCombatant>();
|
||
if (ally != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamage(ally.slotIndex, perTick);
|
||
}
|
||
// Record damage taken
|
||
var targetAlly = target.GetComponent<AllyCombatant>();
|
||
if (targetAlly != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamageTaken(targetAlly.slotIndex, perTick);
|
||
yield return new WaitForSeconds(tickInterval);
|
||
elapsed += tickInterval;
|
||
}
|
||
}
|
||
|
||
private IEnumerator ApplyHealOverTimeCoroutine(GameObject target, float totalAmount, float duration, float tickInterval, GameObject source)
|
||
{
|
||
if (target == null) yield break;
|
||
var comp = target.GetComponent<ICombatant>();
|
||
if (comp == null)
|
||
{
|
||
Debug.LogWarning($"[EffectSystem] Target {target.name} does not implement ICombatant -> cannot apply HOT");
|
||
yield break;
|
||
}
|
||
|
||
if (duration <= 0f)
|
||
{
|
||
comp.ReceiveHeal(totalAmount, source);
|
||
// Record stats
|
||
if (source != null)
|
||
{
|
||
var ally = source.GetComponent<AllyCombatant>();
|
||
if (ally != null && teamUIController.Instance != null) teamUIController.Instance.RecordHeal(ally.slotIndex, totalAmount);
|
||
}
|
||
yield break;
|
||
}
|
||
|
||
int ticks = Mathf.Max(1, Mathf.CeilToInt(duration / tickInterval));
|
||
float perTick = totalAmount / ticks;
|
||
float elapsed = 0f;
|
||
while (elapsed < duration)
|
||
{
|
||
if (target == null) yield break;
|
||
comp.ReceiveHeal(perTick, source);
|
||
// Record stats
|
||
if (source != null)
|
||
{
|
||
var ally = source.GetComponent<AllyCombatant>();
|
||
if (ally != null && teamUIController.Instance != null) teamUIController.Instance.RecordHeal(ally.slotIndex, perTick);
|
||
}
|
||
yield return new WaitForSeconds(tickInterval);
|
||
elapsed += tickInterval;
|
||
}
|
||
}
|
||
|
||
private IEnumerator ApplyIncreaseManaOverTimeCoroutine(GameObject target, float totalAmount, float duration, float tickInterval, GameObject source)
|
||
{
|
||
if (target == null) yield break;
|
||
var comp = target.GetComponent<ICombatant>();
|
||
// If target implements a method to add mana, call it; otherwise try to find AllyCombatant component
|
||
var ally = target.GetComponent<AllyCombatant>();
|
||
if (ally == null && comp == null)
|
||
{
|
||
Debug.LogWarning($"[EffectSystem] Target {target.name} cannot receive mana -> no AllyCombatant or ICombatant found");
|
||
yield break;
|
||
}
|
||
|
||
if (duration <= 0f)
|
||
{
|
||
// immediate add totalAmount to mana if ally present
|
||
if (ally != null)
|
||
{
|
||
ally.ModifyMana(Mathf.CeilToInt(totalAmount), true, true);
|
||
// Record stats
|
||
if (source != null)
|
||
{
|
||
var sourceAlly = source.GetComponent<AllyCombatant>();
|
||
if (sourceAlly != null && teamUIController.Instance != null) teamUIController.Instance.RecordMana(sourceAlly.slotIndex, totalAmount);
|
||
}
|
||
}
|
||
yield break;
|
||
}
|
||
|
||
int ticks = Mathf.Max(1, Mathf.CeilToInt(duration / tickInterval));
|
||
float perTick = totalAmount / ticks;
|
||
float elapsed = 0f;
|
||
while (elapsed < duration)
|
||
{
|
||
if (target == null) yield break;
|
||
if (ally != null)
|
||
{
|
||
ally.ModifyMana(Mathf.CeilToInt(perTick), true, true);
|
||
// Record stats
|
||
if (source != null)
|
||
{
|
||
var sourceAlly = source.GetComponent<AllyCombatant>();
|
||
if (sourceAlly != null && teamUIController.Instance != null) teamUIController.Instance.RecordMana(sourceAlly.slotIndex, perTick);
|
||
}
|
||
}
|
||
yield return new WaitForSeconds(tickInterval);
|
||
elapsed += tickInterval;
|
||
}
|
||
}
|
||
|
||
private IEnumerator ApplyTimedBuffCoroutine(GameObject target, Buff buff, GameObject source)
|
||
{
|
||
if (target == null) yield break;
|
||
var comp = target.GetComponent<ICombatant>();
|
||
if (comp == null)
|
||
{
|
||
Debug.LogWarning($"[EffectSystem] Target {target.name} does not implement ICombatant -> cannot apply Buff");
|
||
yield break;
|
||
}
|
||
|
||
comp.ApplyBuff(buff, source);
|
||
yield return new WaitForSeconds(buff.duration);
|
||
comp.RemoveBuff(buff.buffId);
|
||
}
|
||
|
||
private IEnumerator ApplyTimedDebuffCoroutine(GameObject target, Buff debuff, GameObject source)
|
||
{
|
||
// For now debuff is same as buff but may be handled differently by ICombatant implementation
|
||
if (target == null) yield break;
|
||
var comp = target.GetComponent<ICombatant>();
|
||
if (comp == null)
|
||
{
|
||
Debug.LogWarning($"[EffectSystem] Target {target.name} does not implement ICombatant -> cannot apply Debuff");
|
||
yield break;
|
||
}
|
||
|
||
comp.ApplyBuff(debuff, source);
|
||
yield return new WaitForSeconds(debuff.duration);
|
||
comp.RemoveBuff(debuff.buffId);
|
||
}
|
||
|
||
// Coroutine to apply temporary scoreEfficiency change and revert after duration
|
||
private IEnumerator ApplyTemporaryScoreEfficiencyChangeCoroutine(AllyCombatant ally, float delta, float duration)
|
||
{
|
||
if (ally == null) yield break;
|
||
ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency + delta);
|
||
yield return new WaitForSeconds(duration);
|
||
ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency - delta);
|
||
}
|
||
|
||
// Coroutine to apply temporary damageResistance change and revert after duration
|
||
private IEnumerator ApplyTemporaryDamageResistanceChangeCoroutine(GameObject target, float delta, float duration)
|
||
{
|
||
if (target == null) yield break;
|
||
// support both ally and enemy
|
||
var ally = target.GetComponent<AllyCombatant>();
|
||
if (ally != null)
|
||
{
|
||
float before = ally.damageResistance;
|
||
ally.damageResistance = Mathf.Clamp01(ally.damageResistance + delta);
|
||
yield return new WaitForSeconds(duration);
|
||
// revert by subtracting delta, clamp
|
||
ally.damageResistance = Mathf.Clamp01(ally.damageResistance - delta);
|
||
yield break;
|
||
}
|
||
|
||
var enemy = target.GetComponent<EnemyCombatant>();
|
||
if (enemy != null)
|
||
{
|
||
float beforeE = enemy.damageResistance;
|
||
enemy.damageResistance = Mathf.Clamp01(enemy.damageResistance + delta);
|
||
yield return new WaitForSeconds(duration);
|
||
enemy.damageResistance = Mathf.Clamp01(enemy.damageResistance - delta);
|
||
yield break;
|
||
}
|
||
}
|
||
|
||
// Coroutine to apply temporary attack change for AllyCombatant
|
||
private IEnumerator ApplyTemporaryAttackChangeCoroutine(AllyCombatant ally, int delta, float duration)
|
||
{
|
||
if (ally == null) yield break;
|
||
ally.attack = Mathf.Max(0, ally.attack + delta);
|
||
yield return new WaitForSeconds(duration);
|
||
ally.attack = Mathf.Max(0, ally.attack - delta);
|
||
}
|
||
|
||
// Coroutine to apply temporary attack change for EnemyCombatant
|
||
private IEnumerator ApplyTemporaryAttackChangeCoroutine(EnemyCombatant enemy, int delta, float duration)
|
||
{
|
||
if (enemy == null) yield break;
|
||
enemy.attack = Mathf.Max(0, enemy.attack + delta);
|
||
yield return new WaitForSeconds(duration);
|
||
enemy.attack = Mathf.Max(0, enemy.attack - delta);
|
||
}
|
||
#endregion
|
||
}
|