488 lines
19 KiB
C#
488 lines
19 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)
|
|
{
|
|
List<GameObject> targets = ResolveTargets(selector, source, specificTarget);
|
|
|
|
if (targets == null || targets.Count == 0)
|
|
{
|
|
Debug.LogWarning($"[EffectSystem] No targets resolved for selector={selector}");
|
|
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 (targets.Count > 0)
|
|
ApplyInstantDamage(targets[0], 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;
|
|
|
|
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>();
|
|
|
|
switch (selector)
|
|
{
|
|
case Selector.Self:
|
|
if (source != null) list.Add(source);
|
|
break;
|
|
|
|
case Selector.AllAllies:
|
|
list.AddRange(FindAllAllies());
|
|
break;
|
|
|
|
case Selector.AllAlliesExceptSelf:
|
|
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)
|
|
{
|
|
var ui = teamUIController.Instance;
|
|
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);
|
|
return uniq;
|
|
}
|
|
|
|
private IEnumerable<GameObject> FindAllAllies()
|
|
{
|
|
List<GameObject> allies = new List<GameObject>();
|
|
// try names ally_01..ally_05
|
|
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.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 void ApplyInstantDamage(GameObject target, float amount, GameObject source)
|
|
{
|
|
if (target == null) return;
|
|
var comp = target.GetComponent<ICombatant>();
|
|
if (comp != null)
|
|
{
|
|
comp.ReceiveDamage(amount, source);
|
|
}
|
|
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);
|
|
}
|
|
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)
|
|
{
|
|
comp.ReceiveDamage(totalAmount, source);
|
|
yield break;
|
|
}
|
|
|
|
int ticks = Mathf.Max(1, Mathf.CeilToInt(duration / tickInterval));
|
|
float perTick = totalAmount / ticks;
|
|
float elapsed = 0f;
|
|
while (elapsed < duration)
|
|
{
|
|
comp.ReceiveDamage(perTick, source);
|
|
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);
|
|
yield break;
|
|
}
|
|
|
|
int ticks = Mathf.Max(1, Mathf.CeilToInt(duration / tickInterval));
|
|
float perTick = totalAmount / ticks;
|
|
float elapsed = 0f;
|
|
while (elapsed < duration)
|
|
{
|
|
comp.ReceiveHeal(perTick, source);
|
|
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);
|
|
yield break;
|
|
}
|
|
|
|
int ticks = Mathf.Max(1, Mathf.CeilToInt(duration / tickInterval));
|
|
float perTick = totalAmount / ticks;
|
|
float elapsed = 0f;
|
|
while (elapsed < duration)
|
|
{
|
|
if (ally != null) ally.ModifyMana(Mathf.CeilToInt(perTick), true);
|
|
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);
|
|
}
|
|
#endregion
|
|
}
|