编队系统大更新 基本快搞好了 准备做敌人和分数
This commit is contained in:
@@ -0,0 +1,938 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// SkillBuilder: 集中定义可快速调用的技能函数。
|
||||
/// - 每个技能为一个独立的 public 方法,接受必要参数(施法者 source, 目标 specificTarget 等)
|
||||
/// - 内部调用 EffectSystem.Instance.ApplyEffect(...),统一使用 Selector 与 EffectType
|
||||
/// - 提供常用参数(amount, duration, tickInterval)并对参数做简单校验
|
||||
///
|
||||
/// 用法示例:
|
||||
/// SkillBuilder.Instance.ExecuteSkill("Fireball", EffectType.DamageSingleEnemy, 120f, Selector.CurrentEnemies, caster, target);
|
||||
/// SkillBuilder.Instance.ApplyScoreMultiplier(caster, Selector.AllAllies, 1.5f, 5f); // 5 秒内队伍得分乘 1.5
|
||||
/// </summary>
|
||||
public class SkillBuilder : MonoBehaviour
|
||||
{
|
||||
public static SkillBuilder Instance { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null) Instance = this;
|
||||
else Destroy(gameObject);
|
||||
}
|
||||
|
||||
// ----------------------------- 基础参数模板 -----------------------------
|
||||
public float defaultDamage = 100f;
|
||||
public float defaultHeal = 80f;
|
||||
public float defaultDuration = 5f;
|
||||
public float defaultTickInterval = 1f;
|
||||
|
||||
[Header("Shared note-hit auto-skill settings")]
|
||||
// These defaults remain as a global fallback but the preferred source is per-level fields in AllyHero_SO
|
||||
[Tooltip("(Fallback) Mana gained on Good judge if SO level value not present")] public int manaGainGood = 1;
|
||||
[Tooltip("(Fallback) Mana gained on Great judge if SO level value not present")] public int manaGainGreat = 2;
|
||||
[Tooltip("(Fallback) Mana gained on Perfect judge if SO level value not present")] public int manaGainPerfect = 3;
|
||||
[Tooltip("(Fallback) Mana gained on Miss judge if SO level value not present")] public int manaGainOnMiss = 5;
|
||||
|
||||
[Tooltip("(Fallback) Damage multiplier applied to ally base attack for Good hits if SO level value not present")] public float damageMultiplierGood = 0.5f;
|
||||
[Tooltip("(Fallback) Damage multiplier applied to ally base attack for Great hits if SO level value not present")] public float damageMultiplierGreat = 0.75f;
|
||||
[Tooltip("(Fallback) Damage multiplier applied to ally base attack for Perfect hits if SO level value not present")] public float damageMultiplierPerfect = 1f;
|
||||
|
||||
[Tooltip("(Fallback) Base HP loss on Miss will be (missHpLossBase * (1 - damageResistance)) if SO level value not present")] public float missHpLossBase = 10f;
|
||||
|
||||
// Placeholder: if you want to route damage through EffectSystem later, use this.
|
||||
public void DealDamageFromAllyToEnemies(int slotIndex, float amount)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] DealDamageFromAllyToEnemies (placeholder): slot={slotIndex} amount={amount}");
|
||||
// Future: integrate with EffectSystem/Enemy system here. Example (commented):
|
||||
// var caster = GetAllyObjectBySlot(slotIndex);
|
||||
// global::EffectSystem.Instance?.ApplyEffect(Selector.CurrentEnemies, EffectType.DamageSingleEnemy, amount, 0f, caster, null);
|
||||
}
|
||||
|
||||
private void ApplySharedOnNoteHit(int trackIndex, string judgeResult)
|
||||
{
|
||||
if (trackIndex < 0) return;
|
||||
var allyGO = GetAllyObjectBySlot(trackIndex);
|
||||
if (allyGO == null) return;
|
||||
var ally = allyGO.GetComponent<AllyCombatant>();
|
||||
var so = GetAllyHeroSOBySlot(trackIndex);
|
||||
// try to obtain per-level params from SO if present
|
||||
AllyHero_SO.AllyLevelInfo levelInfo = so != null ? (so.GetEffectiveLevelForCurrentEXP()) : null;
|
||||
|
||||
int manaGain = 0;
|
||||
float damageMult = 0f;
|
||||
if (levelInfo != null)
|
||||
{
|
||||
switch (judgeResult)
|
||||
{
|
||||
case "Perfect": manaGain = levelInfo.manaGainPerfect; damageMult = levelInfo.damageMultiplierPerfect; break;
|
||||
case "Great": manaGain = levelInfo.manaGainGreat; damageMult = levelInfo.damageMultiplierGreat; break;
|
||||
case "Good": manaGain = levelInfo.manaGainGood; damageMult = levelInfo.damageMultiplierGood; break;
|
||||
default: manaGain = levelInfo.manaGainOnMiss; break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (judgeResult)
|
||||
{
|
||||
case "Perfect": manaGain = manaGainPerfect; damageMult = damageMultiplierPerfect; break;
|
||||
case "Great": manaGain = manaGainGreat; damageMult = damageMultiplierGreat; break;
|
||||
case "Good": manaGain = manaGainGood; damageMult = damageMultiplierGood; break;
|
||||
default: manaGain = manaGainOnMiss; break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ally != null && manaGain != 0)
|
||||
{
|
||||
ally.ModifyMana(manaGain, true);
|
||||
Debug.Log($"[SkillBuilder] Applied mana gain {manaGain} to slot {trackIndex} due to judge {judgeResult}");
|
||||
}
|
||||
|
||||
if (judgeResult == "Miss")
|
||||
{
|
||||
if (ally != null)
|
||||
{
|
||||
float missBase = (levelInfo != null) ? levelInfo.missHpLossBase : missHpLossBase;
|
||||
// apply damage reduction by damageResistance: final loss = missBase * (1 - damageResistance)
|
||||
float loss = missBase * (1f - ally.damageResistance);
|
||||
ally.ModifyHP(-Mathf.CeilToInt(loss), true);
|
||||
Debug.Log($"[SkillBuilder] Applied miss HP loss {loss} to slot {trackIndex} (damageResistance={ally.damageResistance})");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (damageMult > 0f)
|
||||
{
|
||||
int baseAtk = GetAllyBaseAttack(so);
|
||||
// damage should scale with (1 - damageResistance) of enemies when applied; here we pass raw amount = baseAtk * damageMult
|
||||
float dmg = baseAtk * damageMult;
|
||||
DealDamageFromAllyToEnemies(trackIndex, dmg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------- 通用技能执行接口 -----------------------------
|
||||
// 通用执行:技能名, 效果类型, 数值, 目标选择器, 施法者, 可选具体目标, 持续时间和 tick
|
||||
public void ExecuteSkill(string skillName, EffectType effectType, float amount, Selector selector, GameObject caster, GameObject specificTarget = null, float duration = 0f, float tickInterval = 1f)
|
||||
{
|
||||
if (global::EffectSystem.Instance == null)
|
||||
{
|
||||
Debug.LogWarning($"ExecuteSkill failed: EffectSystem.Instance is null");
|
||||
return;
|
||||
}
|
||||
// basic logging
|
||||
Debug.Log($"[SkillBuilder] ExecuteSkill: {skillName} type={effectType} amount={amount} selector={selector} caster={(caster?caster.name:"null")} target={(specificTarget?specificTarget.name:"null")} duration={duration}");
|
||||
|
||||
// call EffectSystem using GameObject API (keeps compatibility with existing EffectSystem)
|
||||
global::EffectSystem.Instance.ApplyEffect(selector, effectType, amount, duration, caster, specificTarget, tickInterval);
|
||||
}
|
||||
|
||||
// New overload: execute by slot index (no GameObject required externally)
|
||||
public void ExecuteSkillBySlot(string skillName, EffectType effectType, float amount, Selector selector, int casterSlotIndex = -1, int specificTargetSlot = -1, float duration = 0f, float tickInterval = 1f)
|
||||
{
|
||||
GameObject casterGO = casterSlotIndex >= 0 ? GetAllyObjectBySlot(casterSlotIndex) : null;
|
||||
GameObject targetGO = specificTargetSlot >= 0 ? GetAllyObjectBySlot(specificTargetSlot) : null;
|
||||
ExecuteSkill(skillName, effectType, amount, selector, casterGO, targetGO, duration, tickInterval);
|
||||
}
|
||||
|
||||
// ----------------------------- 目标解析(供本脚本使用,和 EffectSystem 的解析一致) -----------------------------
|
||||
// 返回符合 selector 的 GameObject 列表,方便技能脚本自行操作(例如直接给 ICombatant.Buff)
|
||||
public List<GameObject> ResolveTargetsLocal(Selector selector, GameObject source = null, GameObject specificTarget = null)
|
||||
{
|
||||
List<GameObject> list = new List<GameObject>();
|
||||
switch (selector)
|
||||
{
|
||||
case Selector.Self:
|
||||
if (source != null) list.Add(source);
|
||||
break;
|
||||
case Selector.AllAllies:
|
||||
for (int i = 1; i <= 5; i++) { var go = GameObject.Find($"ally_0{i}"); if (go != null) list.Add(go); }
|
||||
try { var tagged = GameObject.FindGameObjectsWithTag("Ally"); foreach (var g in tagged) if (!list.Contains(g)) list.Add(g); } catch { }
|
||||
break;
|
||||
case Selector.AllAlliesExceptSelf:
|
||||
for (int i = 1; i <= 5; i++) { var go = GameObject.Find($"ally_0{i}"); if (go != null) list.Add(go); }
|
||||
try { var tagged = GameObject.FindGameObjectsWithTag("Ally"); foreach (var g in tagged) if (!list.Contains(g)) list.Add(g); } catch { }
|
||||
if (source != null) list.RemoveAll(g => g == null || g == source);
|
||||
break;
|
||||
case Selector.AdjacentAllies:
|
||||
if (source == null) break;
|
||||
int slotIndex = -1;
|
||||
var ac = source.GetComponent<AllyCombatant>();
|
||||
if (ac != null) slotIndex = ac.slotIndex;
|
||||
else
|
||||
{
|
||||
var n = source.name;
|
||||
for (int i = 1; i <= 5; i++) if (n.Contains($"ally_0{i}")) { slotIndex = i - 1; break; }
|
||||
}
|
||||
|
||||
var ui = teamUIController.Instance;
|
||||
// Additional attempt: if source isn't AllyCombatant and name parse failed, try to match source to UI slots (or child of slot object)
|
||||
if (slotIndex < 0 && ui != null)
|
||||
{
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var slotObj = ui.GetAllyObjectBySlot(i);
|
||||
if (slotObj == null) continue;
|
||||
if (slotObj == source) { slotIndex = i; break; }
|
||||
// if source is a child of the slot object
|
||||
if (source.transform.IsChildOf(slotObj.transform)) { slotIndex = i; break; }
|
||||
}
|
||||
}
|
||||
|
||||
if (slotIndex >= 0)
|
||||
{
|
||||
if (ui != null)
|
||||
{
|
||||
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); }
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int left = slotIndex - 1, 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("[SkillBuilder] ResolveTargetsLocal: could not determine slot index for AdjacentAllies for source=" + (source?source.name:"null"));
|
||||
}
|
||||
break;
|
||||
case Selector.CurrentEnemies:
|
||||
if (specificTarget != null) list.Add(specificTarget);
|
||||
else { try { var tagged = GameObject.FindGameObjectsWithTag("Enemy"); foreach (var g in tagged) if (!list.Contains(g)) list.Add(g); } catch { var single = GameObject.Find("thisEnemy"); if (single != null) list.Add(single); } }
|
||||
break;
|
||||
case Selector.AllEntities:
|
||||
for (int i = 1; i <= 5; i++) { var go = GameObject.Find($"ally_0{i}"); if (go != null) list.Add(go); }
|
||||
try { var tagged = GameObject.FindGameObjectsWithTag("Ally"); foreach (var g in tagged) if (!list.Contains(g)) list.Add(g); } catch { }
|
||||
try { var taggedE = GameObject.FindGameObjectsWithTag("Enemy"); foreach (var g in taggedE) if (!list.Contains(g)) list.Add(g); } catch { var single = GameObject.Find("thisEnemy"); if (single != null && !list.Contains(single)) list.Add(single); }
|
||||
break;
|
||||
}
|
||||
list.RemoveAll(x => x == null);
|
||||
var uniq = new List<GameObject>();
|
||||
foreach (var g in list) if (!uniq.Contains(g)) uniq.Add(g);
|
||||
return uniq;
|
||||
}
|
||||
|
||||
// ----------------------------- 分数影响 API -----------------------------
|
||||
// 直接修改全局总分(立即生效)
|
||||
public void ModifyTotalScore(int delta)
|
||||
{
|
||||
if (ScoreManager.Instance == null) { Debug.LogWarning("ModifyTotalScore: ScoreManager.Instance == null"); return; }
|
||||
// adjust total and attempt to update UI
|
||||
ScoreManager.Instance.totalScore += delta;
|
||||
// try to force UI update via ScoreManager.RecalculateTotal() is not appropriate because it recalculates from allies
|
||||
// so instead update displayed total directly if teamUIController present
|
||||
if (teamUIController.Instance != null && teamUIController.Instance.currentTotalScore != null)
|
||||
{
|
||||
teamUIController.Instance.currentTotalScore.text = ScoreManager.Instance.totalScore.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// 修改单个友军的分数(即时)
|
||||
public void ModifySingleAllyScoreDirect(GameObject allyObject, int delta)
|
||||
{
|
||||
if (allyObject == null) return;
|
||||
var ally = allyObject.GetComponent<AllyCombatant>();
|
||||
if (ally != null)
|
||||
{
|
||||
ally.AddScoreDirect(delta);
|
||||
}
|
||||
}
|
||||
|
||||
// Slot-based: modify single ally by slot index (no GameObject required)
|
||||
public void ModifySingleAllyScoreDirectBySlot(int slotIndex, int delta)
|
||||
{
|
||||
var go = GetAllyObjectBySlot(slotIndex);
|
||||
if (go == null) return;
|
||||
var ally = go.GetComponent<AllyCombatant>();
|
||||
if (ally != null) ally.AddScoreDirect(delta);
|
||||
}
|
||||
|
||||
// 对一组目标应用一次性的分数奖励/惩罚(按集合应用)
|
||||
public void ModifyGroupScoreDirect(Selector selector, GameObject caster, int delta)
|
||||
{
|
||||
var targets = ResolveTargetsLocal(selector, caster);
|
||||
foreach (var t in targets)
|
||||
{
|
||||
var ally = t.GetComponent<AllyCombatant>();
|
||||
if (ally != null) ally.AddScoreDirect(delta);
|
||||
}
|
||||
}
|
||||
|
||||
// 临时提高(或降低)目标的分数倍率,使用 EffectSystem.ScoreMultiplier(推荐)
|
||||
public void ApplyScoreMultiplier(Selector selector, GameObject caster, float multiplier, float duration)
|
||||
{
|
||||
if (multiplier <= 0f) { Debug.LogWarning("ApplyScoreMultiplier: invalid multiplier"); return; }
|
||||
global::EffectSystem.Instance.ApplyEffect(selector, EffectType.ScoreMultiplier, multiplier, duration, caster, null);
|
||||
}
|
||||
|
||||
// Slot-based helper: apply score multiplier to a single slot
|
||||
public void ApplyScoreMultiplierToSlot(int slotIndex, float multiplier, float duration)
|
||||
{
|
||||
if (multiplier <= 0f) { Debug.LogWarning("ApplyScoreMultiplierToSlot: invalid multiplier"); return; }
|
||||
var go = GetAllyObjectBySlot(slotIndex);
|
||||
if (go == null) { Debug.LogWarning($"ApplyScoreMultiplierToSlot: no ally object for slot {slotIndex}"); return; }
|
||||
global::EffectSystem.Instance.ApplyEffect(Selector.Self, EffectType.ScoreMultiplier, multiplier, duration, go, null);
|
||||
}
|
||||
|
||||
// Slot-based helper: add flat score to a single slot (immediate)
|
||||
public void AddScoreToSlot(int slotIndex, int delta)
|
||||
{
|
||||
var go = GetAllyObjectBySlot(slotIndex);
|
||||
if (go == null) { Debug.LogWarning($"AddScoreToSlot: no ally object for slot {slotIndex}"); return; }
|
||||
global::EffectSystem.Instance.ApplyEffect(Selector.Self, EffectType.AddScore, delta, 0f, go, null);
|
||||
}
|
||||
|
||||
private IEnumerator ApplyBuffToTargetsCoroutine(Buff buff, List<GameObject> targets, GameObject source)
|
||||
{
|
||||
if (buff == null) yield break;
|
||||
foreach (var t in targets)
|
||||
{
|
||||
if (t == null) continue;
|
||||
var comp = t.GetComponent<ICombatant>();
|
||||
if (comp != null) comp.ApplyBuff(buff, source);
|
||||
else Debug.LogWarning($"ApplyBuffToTargets: target {t.name} has no ICombatant");
|
||||
}
|
||||
if (buff.duration > 0f)
|
||||
yield return new WaitForSeconds(buff.duration);
|
||||
else
|
||||
yield break;
|
||||
|
||||
foreach (var t in targets)
|
||||
{
|
||||
if (t == null) continue;
|
||||
var comp = t.GetComponent<ICombatant>();
|
||||
if (comp != null) comp.RemoveBuff(buff.buffId);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------- 现有便捷技能示例 -----------------------------
|
||||
public void DealSingleEnemyDamage(GameObject caster, GameObject enemyTarget, float amount)
|
||||
{
|
||||
if (enemyTarget == null) { Debug.LogWarning("DealSingleEnemyDamage: enemyTarget == null"); return; }
|
||||
ExecuteSkill("DealSingleEnemyDamage", EffectType.DamageSingleEnemy, amount, Selector.CurrentEnemies, caster, enemyTarget);
|
||||
}
|
||||
|
||||
public void ApplyDamageOverTimeToEnemies(GameObject caster, Selector selector, float totalAmount, float duration, float tickInterval = 1f, GameObject specificTarget = null)
|
||||
{
|
||||
if (duration <= 0f) duration = defaultDuration;
|
||||
if (tickInterval <= 0f) tickInterval = defaultTickInterval;
|
||||
ExecuteSkill("DamageOverTime", EffectType.DamageOverTimeEnemy, totalAmount, selector, caster, specificTarget, duration, tickInterval);
|
||||
}
|
||||
|
||||
public void HealSelf(GameObject caster, float amount)
|
||||
{
|
||||
ExecuteSkill("HealSelf", EffectType.HealSingleSelf, amount, Selector.Self, caster, caster);
|
||||
}
|
||||
|
||||
public void HealSelfOverTime(GameObject caster, float totalAmount, float duration, float tickInterval = 1f)
|
||||
{
|
||||
ExecuteSkill("HealSelfOverTime", EffectType.HealOverTimeSelf, totalAmount, Selector.Self, caster, caster, duration, tickInterval);
|
||||
}
|
||||
|
||||
public void HealGroupSingle(GameObject caster, float amount, bool includeSelf = true)
|
||||
{
|
||||
var sel = includeSelf ? Selector.AllAllies : Selector.AllAlliesExceptSelf;
|
||||
ExecuteSkill("HealGroupSingle", EffectType.HealGroupSingle, amount, sel, caster);
|
||||
}
|
||||
|
||||
public void HealGroupOverTime(GameObject caster, float totalAmount, float duration, float tickInterval = 1f, bool includeSelf = true)
|
||||
{
|
||||
var sel = includeSelf ? Selector.AllAllies : Selector.AllAlliesExceptSelf;
|
||||
ExecuteSkill("HealGroupOverTime", EffectType.HealGroupOverTime, totalAmount, sel, caster, null, duration, tickInterval);
|
||||
}
|
||||
|
||||
public void IncreaseManaOverTime(GameObject caster, Selector selector, float totalAmount, float duration, float tickInterval = 1f, GameObject specificTarget = null)
|
||||
{
|
||||
ExecuteSkill("IncreaseManaOverTime", EffectType.IncreaseManaOverTime, totalAmount, selector, caster, specificTarget, duration, tickInterval);
|
||||
}
|
||||
|
||||
public void ReduceEnemyHealOverTime(GameObject caster, Selector selector, float duration, GameObject specificTarget = null)
|
||||
{
|
||||
ExecuteSkill("ReduceEnemyHealOverTime", EffectType.ReduceEnemyHealOverTime, 0f, selector, caster, specificTarget, duration);
|
||||
}
|
||||
|
||||
public void HealAdjacentAllies(GameObject caster, float amount)
|
||||
{
|
||||
ExecuteSkill("HealAdjacentAllies", EffectType.HealGroupSingle, amount, Selector.AdjacentAllies, caster);
|
||||
}
|
||||
|
||||
public void IncreaseManaAdjacentAllies(GameObject caster, float totalAmount, float duration, float tickInterval = 1f)
|
||||
{
|
||||
ExecuteSkill("IncreaseManaAdjacentAllies", EffectType.IncreaseManaOverTime, totalAmount, Selector.AdjacentAllies, caster, null, duration, tickInterval);
|
||||
}
|
||||
|
||||
public void Hero_A_Ultimate(GameObject caster, GameObject enemyTarget)
|
||||
{
|
||||
DealSingleEnemyDamage(caster, enemyTarget, defaultDamage * 2f);
|
||||
HealAdjacentAllies(caster, defaultHeal * 0.5f);
|
||||
}
|
||||
|
||||
public void MassHeal(GameObject caster)
|
||||
{
|
||||
HealGroupOverTime(caster, defaultHeal * 5f, 4f, 1f, true);
|
||||
}
|
||||
|
||||
// ----------------------------- 基于 SO 的数值计算 helpers -----------------------------
|
||||
// 获取某个槽位的 ally GameObject(0-based slot index)
|
||||
public GameObject GetAllyObjectBySlot(int slotIndex)
|
||||
{
|
||||
var ui = teamUIController.Instance;
|
||||
if (ui != null) return ui.GetAllyObjectBySlot(slotIndex);
|
||||
var named = GameObject.Find($"ally_0{slotIndex + 1}");
|
||||
return named;
|
||||
}
|
||||
|
||||
// 根据槽位解析 AllyHero_SO(运行时从 Resources 中查找 ally_heroID)
|
||||
public AllyHero_SO GetAllyHeroSOBySlot(int slotIndex)
|
||||
{
|
||||
if (teamUIController.Instance == null || teamUIController.Instance.allySlotIds == null) return null;
|
||||
if (slotIndex < 0 || slotIndex >= teamUIController.Instance.allySlotIds.Count) return null;
|
||||
int id = teamUIController.Instance.allySlotIds[slotIndex];
|
||||
if (id <= 0) return null;
|
||||
|
||||
var all = Resources.LoadAll<AllyHero_SO>("");
|
||||
foreach (var a in all)
|
||||
{
|
||||
if (a != null && a.ally_heroID == id) return a;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 根据当前经验选择生效的等级信息(返回 null 表示未找到)
|
||||
private AllyHero_SO.AllyLevelInfo GetEffectiveLevelInfo(AllyHero_SO so)
|
||||
{
|
||||
if (so == null || so.levelStats == null || so.levelStats.Count == 0) return null;
|
||||
// choose the highest requiredEXP that is <= ally_currentEXP
|
||||
AllyHero_SO.AllyLevelInfo best = null;
|
||||
int currentExp = so.ally_currentEXP;
|
||||
foreach (var lvl in so.levelStats)
|
||||
{
|
||||
if (best == null)
|
||||
{
|
||||
if (currentExp >= lvl.requiredEXP) best = lvl;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (currentExp >= lvl.requiredEXP && lvl.requiredEXP >= best.requiredEXP) best = lvl;
|
||||
}
|
||||
}
|
||||
// if no level matched (all requiredEXP > currentExp), fallback to lowest level (index 0)
|
||||
if (best == null) return so.levelStats[0];
|
||||
return best;
|
||||
}
|
||||
|
||||
// 取得英雄基础攻击力(使用当前经验匹配的等级的 attack),若没有则返回 0
|
||||
public int GetAllyBaseAttack(AllyHero_SO so)
|
||||
{
|
||||
if (so == null) return 0;
|
||||
var eff = GetEffectiveLevelInfo(so);
|
||||
if (eff != null) return eff.attack;
|
||||
if (so.levelStats != null && so.levelStats.Count > 0) return so.levelStats[0].attack;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 统一计算技能造成数值:当 inputValue == -1 时使用 skillStatic + allyAttack,
|
||||
// 否则直接使用 inputValue(按传入的 EffectType 做必要转换)
|
||||
public float ComputeSkillValue(GameObject caster, int slotIndex, float inputValue, int skillStatic)
|
||||
{
|
||||
if (inputValue != -1f) return inputValue;
|
||||
var so = GetAllyHeroSOBySlot(slotIndex);
|
||||
int allyAtk = GetAllyBaseAttack(so);
|
||||
return skillStatic + allyAtk;
|
||||
}
|
||||
|
||||
// ----------------------------- 槽位技能通用接口 -----------------------------
|
||||
// 通常技能在被槽位的角色释放时调用此函数:
|
||||
// slotIndex: 0-based 槽位索引;skillId: 自定义技能标识;effectType,value,selector,duration等参数同前述约定
|
||||
public void AllySlotSkill(int slotIndex, string skillId, EffectType effectType, float value, Selector selector, float duration = 0f, GameObject specificTarget = null, float tickInterval = 1f)
|
||||
{
|
||||
GameObject caster = GetAllyObjectBySlot(slotIndex);
|
||||
if (caster == null)
|
||||
{
|
||||
Debug.LogWarning($"AllySlotSkill: no caster found for slot {slotIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
// determine skill-specific static values (示例:skill01 的固定伤害为 50)
|
||||
int skillStaticDamage = 0;
|
||||
switch (skillId)
|
||||
{
|
||||
case "skill01": skillStaticDamage = 50; break;
|
||||
case "skill_heal_small": skillStaticDamage = 30; break;
|
||||
// 在此添加更多技能静态值
|
||||
default: skillStaticDamage = 0; break;
|
||||
}
|
||||
|
||||
float finalValue = ComputeSkillValue(caster, slotIndex, value, skillStaticDamage);
|
||||
|
||||
// 特殊处理:若 effectType 是单次伤害但我们希望对所有敌人使用 CurrentEnemies,selector 参数已由调用方传入
|
||||
ExecuteSkill($"slot{slotIndex + 1}_{skillId}", effectType, finalValue, selector, caster, specificTarget, duration, tickInterval);
|
||||
}
|
||||
|
||||
// New: invoke a SkillDefinition directly (respects staticValue and SO attack when value == -1)
|
||||
public void UseSkillDefinition(SkillDefinition def, int slotIndex, float inputValue = -1f, GameObject specificTarget = null)
|
||||
{
|
||||
if (def == null) { Debug.LogWarning("UseSkillDefinition: null def"); return; }
|
||||
GameObject caster = GetAllyObjectBySlot(slotIndex);
|
||||
if (caster == null)
|
||||
{
|
||||
Debug.LogWarning($"UseSkillDefinition: no caster GameObject found for slot {slotIndex}. Proceeding with null caster.");
|
||||
}
|
||||
|
||||
float amount = 0f;
|
||||
// If caller provided explicit inputValue use it
|
||||
if (inputValue != -1f)
|
||||
{
|
||||
amount = inputValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Build variables from SO and caster; formula can be a plain number too
|
||||
var so = GetAllyHeroSOBySlot(slotIndex);
|
||||
var vars = new Dictionary<string, float>();
|
||||
vars["slot"] = slotIndex;
|
||||
vars["attack"] = GetAllyBaseAttack(so);
|
||||
if (so != null && so.levelStats != null && so.levelStats.Count > 0)
|
||||
{
|
||||
var eff = GetEffectiveLevelInfo(so);
|
||||
var lvl = eff ?? so.levelStats[0];
|
||||
vars["maxHP"] = lvl.maxHP;
|
||||
vars["maxMana"] = lvl.maxMana;
|
||||
vars["damageResistance"] = lvl.damageResistance;
|
||||
vars["scoreEfficiency"] = lvl.scoreEfficiency;
|
||||
vars["attack"] = lvl.attack; // ensure attack from SO levelStats is available
|
||||
}
|
||||
else
|
||||
{
|
||||
vars["maxHP"] = 0f;
|
||||
vars["maxMana"] = 0f;
|
||||
vars["damageResistance"] = 0f;
|
||||
vars["scoreEfficiency"] = 1f;
|
||||
}
|
||||
if (so != null) vars["ally_currentEXP"] = so.ally_currentEXP;
|
||||
else vars["ally_currentEXP"] = 0f;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(def.formula))
|
||||
{
|
||||
Debug.LogWarning($"UseSkillDefinition: skill {def.skillId} has empty formula, defaulting to 0");
|
||||
amount = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!SkillDefinition.TryEvaluateFormula(def.formula, vars, out float fresult))
|
||||
{
|
||||
Debug.LogWarning($"UseSkillDefinition: formula evaluation failed for skill {def.skillId}, defaulting to 0");
|
||||
amount = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
amount = fresult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert formula result (interpreted as per-tick when skill is sustained) to total amount
|
||||
// amount now represents the formula result (per-tick for sustained effects, instant for single-instance)
|
||||
float amountPerTick = amount;
|
||||
float amountTotal = amountPerTick;
|
||||
if (!def.IsSingleInstance)
|
||||
{
|
||||
// compute ticks based on def duration and effective tick interval
|
||||
float tickInterval = def.GetEffectiveTickInterval();
|
||||
int ticks = Mathf.Max(1, Mathf.CeilToInt(def.defaultDuration / tickInterval));
|
||||
amountTotal = amountPerTick * ticks;
|
||||
}
|
||||
|
||||
// If this skill is intended to be triggered by note hits, treat it as a one-time application
|
||||
// (do not multiply by duration ticks). This prevents OnNoteHit triggers from unexpectedly
|
||||
// applying sustained totals (e.g. 5s * perTick) when the designer expects a single effect.
|
||||
if (def.triggerCondition == SkillDefinition.SkillTrigger.OnNoteHit)
|
||||
{
|
||||
amountTotal = amountPerTick;
|
||||
}
|
||||
|
||||
if (def.operateDirectly)
|
||||
{
|
||||
// Resolve targets as GameObjects but operate on their AllyCombatant / ICombatant data directly
|
||||
List<GameObject> targets = ResolveTargetsLocal(def.defaultSelector, caster, specificTarget);
|
||||
if (targets == null || targets.Count == 0)
|
||||
{
|
||||
Debug.LogWarning($"UseSkillDefinition: no targets for direct operation skill {def.skillId}");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var t in targets)
|
||||
{
|
||||
if (t == null) continue;
|
||||
var ally = t.GetComponent<AllyCombatant>();
|
||||
var ic = t.GetComponent<ICombatant>();
|
||||
|
||||
switch (def.effectType)
|
||||
{
|
||||
case EffectType.DamageSingleEnemy:
|
||||
// treat as damage to an enemy; if target has AllyCombatant, apply negative HP
|
||||
if (ally != null) ally.ModifyHP(-Mathf.CeilToInt(amountTotal), true);
|
||||
else if (ic != null) ic.ReceiveDamage(amountTotal, caster);
|
||||
break;
|
||||
case EffectType.DamageOverTimeEnemy:
|
||||
// sustained: start coroutine to apply per-tick damage rather than instant total
|
||||
if (def.defaultDuration <= 0f)
|
||||
{
|
||||
if (ally != null) ally.ModifyHP(-Mathf.CeilToInt(amountPerTick), true);
|
||||
else if (ic != null) ic.ReceiveDamage(amountPerTick, caster);
|
||||
}
|
||||
else
|
||||
{
|
||||
StartCoroutine(ApplyDamageOverTimeDirect(t, amountTotal, def.defaultDuration, def.GetEffectiveTickInterval(), caster));
|
||||
}
|
||||
break;
|
||||
case EffectType.HealSingleSelf:
|
||||
case EffectType.HealGroupSingle:
|
||||
if (ally != null) ally.ModifyHP(Mathf.CeilToInt(amountTotal), true);
|
||||
else if (ic != null) ic.ReceiveHeal(amountTotal, caster);
|
||||
break;
|
||||
case EffectType.HealOverTimeSelf:
|
||||
case EffectType.HealGroupOverTime:
|
||||
if (def.defaultDuration <= 0f)
|
||||
{
|
||||
if (ally != null) ally.ModifyHP(Mathf.CeilToInt(amountPerTick), true);
|
||||
else ic?.ReceiveHeal(amountPerTick, caster);
|
||||
}
|
||||
else
|
||||
{
|
||||
StartCoroutine(ApplyHealOverTimeDirect(t, amountTotal, def.defaultDuration, def.GetEffectiveTickInterval(), caster));
|
||||
}
|
||||
break;
|
||||
case EffectType.IncreaseManaOverTime:
|
||||
if (ally != null)
|
||||
{
|
||||
if (def.defaultDuration <= 0f)
|
||||
{
|
||||
// single instance - amountPerTick equals immediate amount
|
||||
ally.ModifyMana(Mathf.CeilToInt(amountPerTick), true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// for sustained, pass total amount (amountTotal) to coroutine which expects total
|
||||
StartCoroutine(ApplyIncreaseManaOverTimeDirect(ally, amountTotal, def.defaultDuration, def.GetEffectiveTickInterval()));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EffectType.ReduceEnemyHealOverTime:
|
||||
var deb = new Buff { buffId = System.Guid.NewGuid().ToString(), duration = def.defaultDuration, healReceivedMultiplier = 0.5f };
|
||||
if (ic != null) ic.ApplyBuff(deb, caster);
|
||||
break;
|
||||
case EffectType.BuffDuration:
|
||||
case EffectType.DebuffDuration:
|
||||
var b = new Buff { buffId = System.Guid.NewGuid().ToString(), duration = def.defaultDuration };
|
||||
if (def.effectType == EffectType.BuffDuration) b.scoreMultiplier = 1.5f;
|
||||
if (ic != null) ic.ApplyBuff(b, caster);
|
||||
break;
|
||||
default:
|
||||
// non-direct path handled below, but keep compatibility
|
||||
ExecuteSkill(def.displayName ?? def.skillId, def.effectType, amountTotal, def.defaultSelector, caster, specificTarget, def.defaultDuration, def.defaultTickInterval);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// For non-direct path, EffectSystem expects 'amount' to be total amount for sustained effects
|
||||
ExecuteSkill(def.displayName ?? def.skillId, def.effectType, amountTotal, def.defaultSelector, caster, specificTarget, def.defaultDuration, def.defaultTickInterval);
|
||||
}
|
||||
}
|
||||
|
||||
// Use the selected skill index from an AllyHero_SO for a given slot (calls UseSkillDefinition)
|
||||
public void UseSelectedSkillForSlot(int slotIndex, int selectedSkillIndex, float inputValue = -1f, GameObject specificTarget = null)
|
||||
{
|
||||
var so = GetAllyHeroSOBySlot(slotIndex);
|
||||
if (so == null || so.availableSkills == null || selectedSkillIndex < 0 || selectedSkillIndex >= so.availableSkills.Length)
|
||||
{
|
||||
Debug.LogWarning($"UseSelectedSkillForSlot: invalid selection for slot {slotIndex}");
|
||||
return;
|
||||
}
|
||||
UseSkillDefinition(so.availableSkills[selectedSkillIndex], slotIndex, inputValue, specificTarget);
|
||||
}
|
||||
|
||||
// Use the primary skill configured in the AllyHero_SO for this slot (primarySkillIndex dropdown)
|
||||
public void UsePrimarySkillForSlot(int slotIndex, float inputValue = -1f, GameObject specificTarget = null)
|
||||
{
|
||||
var so = GetAllyHeroSOBySlot(slotIndex);
|
||||
if (so == null)
|
||||
{
|
||||
Debug.LogWarning($"UsePrimarySkillForSlot: no SO for slot {slotIndex}");
|
||||
return;
|
||||
}
|
||||
int idx = so.primarySkillIndex;
|
||||
if (idx < 0) { Debug.LogWarning($"UsePrimarySkillForSlot: primarySkillIndex not set for slot {slotIndex}"); return; }
|
||||
UseSelectedSkillForSlot(slotIndex, idx, inputValue, specificTarget);
|
||||
}
|
||||
|
||||
// Called at game start to trigger any allies whose primary skill is set to trigger on game start
|
||||
public void TriggerOnGameStart()
|
||||
{
|
||||
if (teamUIController.Instance == null || teamUIController.Instance.allySlotIds == null) return;
|
||||
int slots = teamUIController.Instance.allySlotIds.Count;
|
||||
for (int i = 0; i < slots; i++)
|
||||
{
|
||||
var so = GetAllyHeroSOBySlot(i);
|
||||
if (so == null) continue;
|
||||
var def = so.GetPrimarySkill();
|
||||
if (def == null) continue;
|
||||
if (def.triggerCondition == SkillDefinition.SkillTrigger.OnGameStart)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] TriggerOnGameStart: casting primary skill {def.skillId} for slot {i}");
|
||||
UsePrimarySkillForSlot(i, -1f, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, float> _lastOnNoteHitTriggerTime = new Dictionary<string, float>();
|
||||
|
||||
// Called when a note on a particular track is hit (judgeResult e.g. "Perfect"/"Great"/"Good").
|
||||
// Will cast the primary skill for the ally in that track if its primary skill is configured to trigger on note hit.
|
||||
// noteType indicates whether the hit event came from a Tap or Hold (tail)
|
||||
public bool NotifyNoteHit(int trackIndex, string judgeResult, SkillDefinition.NoteTypeTrigger noteType = SkillDefinition.NoteTypeTrigger.Tap)
|
||||
{
|
||||
// Always apply shared per-note behavior (mana gain / miss penalty / reserved damage) regardless of SO-defined primary skill
|
||||
try { ApplySharedOnNoteHit(trackIndex, judgeResult); } catch (System.Exception ex) { Debug.LogError($"[SkillBuilder] ApplySharedOnNoteHit threw: {ex}"); }
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit RECEIVED: track={trackIndex} judge={judgeResult} noteType={noteType}");
|
||||
if (trackIndex < 0) { Debug.Log("[SkillBuilder] NotifyNoteHit: invalid trackIndex"); return false; }
|
||||
var so = GetAllyHeroSOBySlot(trackIndex);
|
||||
if (so == null)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit: no SO found for slot {trackIndex}, attempting fallback resolution...");
|
||||
// Dump current UI slot ids for diagnostics
|
||||
if (teamUIController.Instance != null && teamUIController.Instance.allySlotIds != null)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] teamUIController.allySlotIds = [{string.Join(",", teamUIController.Instance.allySlotIds)}]");
|
||||
}
|
||||
// Try to find ally GameObject named by convention
|
||||
var foundGO = GameObject.Find($"ally_0{trackIndex + 1}");
|
||||
if (foundGO != null)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] Found GameObject by name ally_0{trackIndex + 1}: {foundGO.name}");
|
||||
// try to map to UI slot
|
||||
var ui = teamUIController.Instance;
|
||||
if (ui != null && ui.allySlotIds != null)
|
||||
{
|
||||
for (int i = 0; i < ui.allySlotIds.Count; i++)
|
||||
{
|
||||
var slotObj = ui.GetAllyObjectBySlot(i);
|
||||
if (slotObj == null) continue;
|
||||
if (slotObj == foundGO || foundGO.transform.IsChildOf(slotObj.transform))
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] Resolved fallback slot {i} for GameObject {foundGO.name}");
|
||||
trackIndex = i; // override
|
||||
so = GetAllyHeroSOBySlot(trackIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (so == null)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit: still no SO for resolved slot {trackIndex}, aborting NotifyNoteHit");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
var def = so.GetPrimarySkill();
|
||||
if (def == null) { Debug.Log($"[SkillBuilder] NotifyNoteHit: no primary skill for slot {trackIndex}"); return false; }
|
||||
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit: skill={def.skillId} noteTriggerType={def.noteTriggerType} onNoteHitMinThreshold={def.onNoteHitMinThreshold} cooldown={def.onNoteHitCooldown}");
|
||||
|
||||
// Treat Hold events as Tap-equivalent by default to improve compatibility: many skills expect Tap/Either
|
||||
var effectiveNoteType = noteType;
|
||||
if (noteType == SkillDefinition.NoteTypeTrigger.Hold)
|
||||
{
|
||||
// Only convert to Tap-equivalent for checking; keep original in logs
|
||||
effectiveNoteType = SkillDefinition.NoteTypeTrigger.Tap;
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit: treating incoming Hold event as Tap-equivalent for skill checks (track={trackIndex})");
|
||||
}
|
||||
|
||||
// Respect note type (Tap/Hold/Either)
|
||||
switch (def.noteTriggerType)
|
||||
{
|
||||
case SkillDefinition.NoteTypeTrigger.Tap:
|
||||
// accept Tap (and treat Hold as Tap-equivalent)
|
||||
if (effectiveNoteType != SkillDefinition.NoteTypeTrigger.Tap)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {def.skillId} requires Tap but event is {noteType}, rejecting");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case SkillDefinition.NoteTypeTrigger.Hold:
|
||||
if (noteType != SkillDefinition.NoteTypeTrigger.Hold)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {def.skillId} requires Hold but event is {noteType}, rejecting");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case SkillDefinition.NoteTypeTrigger.Either:
|
||||
// accept both
|
||||
break;
|
||||
}
|
||||
|
||||
// Map judgeResult to numeric quality
|
||||
int quality = JudgeQualityFromString(judgeResult);
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit: judgeResult='{judgeResult}' mappedQuality={quality}");
|
||||
// Special case: if threshold == Miss, only trigger on Miss (quality == 0)
|
||||
if (def.onNoteHitMinThreshold == SkillDefinition.NoteTriggerThreshold.Miss)
|
||||
{
|
||||
if (quality != 0)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {def.skillId} is Miss-only but quality={quality}, rejecting");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int required = (int)def.onNoteHitMinThreshold; // Good=1, Great=2, Perfect=3
|
||||
if (quality < required)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {def.skillId} requires quality>={required} but got {quality}, rejecting");
|
||||
// not high enough
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// cooldown: per slot+skill
|
||||
string key = $"{trackIndex}:{def.skillId}";
|
||||
float now = Time.time;
|
||||
if (def.onNoteHitCooldown > 0f)
|
||||
{
|
||||
if (_lastOnNoteHitTriggerTime.TryGetValue(key, out float last))
|
||||
{
|
||||
if (now - last < def.onNoteHitCooldown)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {def.skillId} on cooldown for slot {trackIndex}");
|
||||
// still cooling down
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Passed checks -> invoke primary skill for slot
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit: triggering primary skill {def.skillId} for slot {trackIndex} due to note hit {judgeResult} (noteType={noteType})");
|
||||
UsePrimarySkillForSlot(trackIndex, -1f, null);
|
||||
_lastOnNoteHitTriggerTime[key] = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
private int JudgeQualityFromString(string judge)
|
||||
{
|
||||
switch (judge)
|
||||
{
|
||||
case "Perfect": return 3;
|
||||
case "Great": return 2;
|
||||
case "Good": return 1;
|
||||
default: return 0; // Miss or unknown
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Explanation of caster and enemyTarget:
|
||||
- GameObject caster: the GameObject that performs (casts) the skill. For ally skills this should be the in-scene ally GameObject (usually name ally_01..ally_05) and should have AllyCombatant / ICombatant components. SkillBuilder uses slotIndex -> GetAllyObjectBySlot to resolve this.
|
||||
- GameObject specificTarget / enemyTarget: when a skill targets a specific unit (single enemy or ally), pass that GameObject here. If null, the selected Selector determines the set of targets (e.g. CurrentEnemies without specificTarget will resolve all enemies). Use Enemy GameObjects found by tag 'Enemy' or name 'thisEnemy', or keep a reference to the spawned enemy instance you want.
|
||||
|
||||
How to assign/choose:
|
||||
- Caster: when you call from gameplay code, pass the caster GameObject (or call UseSelectedSkillForSlot with slot index so SkillBuilder resolves caster for you).
|
||||
- EnemyTarget: if your skill should hit a specific enemy instance, supply that enemy's GameObject (for example from Enemy spawn manager or from collision detection). If you want to apply to all enemies, pass null and use Selector.CurrentEnemies.
|
||||
*/
|
||||
|
||||
// ----------------------------- 示例:为轨道3 封装一个快捷函数(用户示例) -----------------------------
|
||||
// 轨道3(索引2) 的技能1,遵循用户所需签名:_ally03skill01(EffectType, value, Selector, duration)
|
||||
public void _ally03skill01(EffectType effectType, float value, Selector selector, float duration = 0f, GameObject specificTarget = null)
|
||||
{
|
||||
AllySlotSkill(2, "skill01", effectType, value, selector, duration, specificTarget);
|
||||
}
|
||||
|
||||
// Coroutine helper for IncreaseManaOverTime
|
||||
private IEnumerator ApplyIncreaseManaOverTimeDirect(AllyCombatant ally, float totalAmount, float duration, float tickInterval)
|
||||
{
|
||||
if (ally == null) yield break;
|
||||
if (duration <= 0f || tickInterval <= 0f)
|
||||
{
|
||||
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)
|
||||
{
|
||||
ally.ModifyMana(Mathf.CeilToInt(perTick), true);
|
||||
yield return new WaitForSeconds(tickInterval);
|
||||
elapsed += tickInterval;
|
||||
}
|
||||
}
|
||||
|
||||
// Coroutine helpers for direct DoT/HoT on GameObject targets
|
||||
private IEnumerator ApplyDamageOverTimeDirect(GameObject target, float totalAmount, float duration, float tickInterval, GameObject source)
|
||||
{
|
||||
if (target == null) yield break;
|
||||
var ic = target.GetComponent<ICombatant>();
|
||||
var ally = target.GetComponent<AllyCombatant>();
|
||||
if (ally == null && ic == null)
|
||||
{
|
||||
Debug.LogWarning($"ApplyDamageOverTimeDirect: target {target.name} cannot receive damage");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (duration <= 0f || tickInterval <= 0f)
|
||||
{
|
||||
if (ally != null) ally.ModifyHP(-Mathf.CeilToInt(totalAmount), true);
|
||||
else ic?.ReceiveDamage(totalAmount, source);
|
||||
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.ModifyHP(-Mathf.CeilToInt(perTick), true);
|
||||
else ic?.ReceiveDamage(perTick, source);
|
||||
yield return new WaitForSeconds(tickInterval);
|
||||
elapsed += tickInterval;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator ApplyHealOverTimeDirect(GameObject target, float totalAmount, float duration, float tickInterval, GameObject source)
|
||||
{
|
||||
if (target == null) yield break;
|
||||
var ic = target.GetComponent<ICombatant>();
|
||||
var ally = target.GetComponent<AllyCombatant>();
|
||||
if (ally == null && ic == null)
|
||||
{
|
||||
Debug.LogWarning($"ApplyHealOverTimeDirect: target {target.name} cannot receive heal");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (duration <= 0f || tickInterval <= 0f)
|
||||
{
|
||||
if (ally != null) ally.ModifyHP(Mathf.CeilToInt(totalAmount), true);
|
||||
else ic?.ReceiveHeal(totalAmount, source);
|
||||
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.ModifyHP(Mathf.CeilToInt(perTick), true);
|
||||
else ic?.ReceiveHeal(perTick, source);
|
||||
yield return new WaitForSeconds(tickInterval);
|
||||
elapsed += tickInterval;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user