修了很多bug和增加功能,优化不少问题

This commit is contained in:
FloatGaming
2026-02-28 06:59:22 +08:00
parent 508a40bba0
commit e11cc1da7a
345 changed files with 173803 additions and 69332 deletions
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -45,7 +45,9 @@ public enum EffectType
// Adds a virtual "Perfect" judgement to targeted ally tracks (counts + score + triggers note-hit hooks).
GrantExtraPerfect,
// While active, redirects damage that would be taken by this ally to an adjacent ally.
RedirectSelfDamageToAdjacent
RedirectSelfDamageToAdjacent,
// While active, any non-Miss note judgement on the ally's track is rewritten to Perfect.
RewriteNonMissToPerfect
}
public interface ICombatant
+197 -14
View File
@@ -46,8 +46,19 @@ public class EffectSystem : MonoBehaviour
/// 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)
public void ApplyEffect(Selector selector, EffectType effectType, float amount, float duration = 0f, GameObject source = null, GameObject specificTarget = null, float tickInterval = 1f, bool skipVFX = false)
{
// --- Prevent empty ally slots (maxHP == 0) from being a source of any effect ---
if (source != null)
{
var allySource = source.GetComponent<AllyCombatant>();
if (allySource != null && allySource.maxHP == 0)
{
LogVerbose($"[EffectSystem] ApplyEffect: source is empty ally {source.name}, skipping.");
return;
}
}
// 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;
@@ -66,9 +77,16 @@ public class EffectSystem : MonoBehaviour
return;
}
// For direct buff-like effects (not Buff object path), allow AllyCombatant redirect states
// to reroute the receiver before the effect is applied.
if (IsDirectRedirectableBuffEffectType(effectType))
{
targets = RedirectDirectBuffTargets(targets);
}
// --- Projectile Logic ---
// Trigger projectile if source exists and is targeting the opposite side
if (GfxController.Instance != null && source != null)
if (GfxController.Instance != null && source != null && !skipVFX)
{
foreach (var t in targets)
{
@@ -82,6 +100,42 @@ public class EffectSystem : MonoBehaviour
if ((sourceIsAlly && targetIsEnemy) || (sourceIsEnemy && targetIsAlly))
{
// --- Check if the specific ally slot is active before playing VFX ---
if (sourceIsAlly)
{
var ally = source.GetComponent<AllyCombatant>();
if (ally != null && teamUIController.Instance != null)
{
bool isActive = false;
switch (ally.slotIndex)
{
case 0: isActive = teamUIController.Instance.isAlly01_active; break;
case 1: isActive = teamUIController.Instance.isAlly02_active; break;
case 2: isActive = teamUIController.Instance.isAlly03_active; break;
case 3: isActive = teamUIController.Instance.isAlly04_active; break;
case 4: isActive = teamUIController.Instance.isAlly05_active; break;
}
if (!isActive) continue; // Skip VFX for inactive ally
}
}
else if (targetIsAlly)
{
var ally = t.GetComponent<AllyCombatant>();
if (ally != null && teamUIController.Instance != null)
{
bool isActive = false;
switch (ally.slotIndex)
{
case 0: isActive = teamUIController.Instance.isAlly01_active; break;
case 1: isActive = teamUIController.Instance.isAlly02_active; break;
case 2: isActive = teamUIController.Instance.isAlly03_active; break;
case 3: isActive = teamUIController.Instance.isAlly04_active; break;
case 4: isActive = teamUIController.Instance.isAlly05_active; break;
}
if (!isActive) continue; // Skip VFX for inactive ally
}
}
// Captured target for callback
GameObject targetToHit = t;
bool isEnemyTarget = targetIsEnemy;
@@ -688,6 +742,20 @@ public class EffectSystem : MonoBehaviour
}
break;
case EffectType.RewriteNonMissToPerfect:
foreach (var t in targets)
{
if (t == null) continue;
var ally = t.GetComponent<AllyCombatant>() ?? t.GetComponentInChildren<AllyCombatant>(true);
if (ally == null) continue;
if (ally.IsDead) continue;
// Prefer configured duration; fallback to amount when designers authored duration in formula.
float activeDuration = duration > 0f ? duration : amount;
ally.ActivateNonMissToPerfectRewrite(activeDuration);
}
break;
case EffectType.RedirectSelfDamageToAdjacent:
foreach (var t in targets)
{
@@ -855,13 +923,13 @@ public class EffectSystem : MonoBehaviour
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.).
// Filter out dead or empty 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;
if (ally != null) return ally.IsDead || ally.maxHP == 0; // Skip dead or empty allies
var enemy = go.GetComponent<EnemyCombatant>();
if (enemy != null) return enemy.IsDead || enemy.currentHP <= 0;
return false;
@@ -983,11 +1051,33 @@ public class EffectSystem : MonoBehaviour
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())
// If target is an enemy, check if all enemies are already dead or no allies are active
if (targetEnemy != null)
{
LogVerbose($"[EffectSystem] Skip ApplyInstantDamage to {target.name} because all enemies are dead.");
return;
if (AreAllEnemiesDead())
{
LogVerbose($"[EffectSystem] Skip ApplyInstantDamage to {target.name} because all enemies are dead.");
return;
}
if (teamUIController.Instance != null)
{
if (!teamUIController.Instance.IsAnyAllyActive())
{
LogVerbose($"[EffectSystem] Skip ApplyInstantDamage to {target.name} because no allies are active.");
return;
}
if (source != null)
{
var sourceAlly = source.GetComponent<AllyCombatant>();
if (sourceAlly != null && !teamUIController.Instance.IsAllySlotActive(sourceAlly.slotIndex))
{
LogVerbose($"[EffectSystem] Skip ApplyInstantDamage to {target.name} because source ally slot {sourceAlly.slotIndex} is not active.");
return;
}
}
}
}
var comp = target.GetComponent<ICombatant>();
@@ -1090,10 +1180,32 @@ public class EffectSystem : MonoBehaviour
if (duration <= 0f)
{
if (targetEnemy != null && AreAllEnemiesDead())
if (targetEnemy != null)
{
LogVerbose($"[EffectSystem] Skip DOT (immediate) to {target.name} because all enemies are dead.");
yield break;
if (AreAllEnemiesDead())
{
LogVerbose($"[EffectSystem] Skip DOT (immediate) to {target.name} because all enemies are dead.");
yield break;
}
if (teamUIController.Instance != null)
{
if (!teamUIController.Instance.IsAnyAllyActive())
{
LogVerbose($"[EffectSystem] Skip DOT (immediate) to {target.name} because no allies are active.");
yield break;
}
if (source != null)
{
var sourceAlly = source.GetComponent<AllyCombatant>();
if (sourceAlly != null && !teamUIController.Instance.IsAllySlotActive(sourceAlly.slotIndex))
{
LogVerbose($"[EffectSystem] Skip DOT (immediate) to {target.name} because source ally slot {sourceAlly.slotIndex} is not active.");
yield break;
}
}
}
}
comp.ReceiveDamage(totalAmount, source);
@@ -1115,10 +1227,32 @@ public class EffectSystem : MonoBehaviour
if (targetAlly != null && targetAlly.IsDead) yield break;
if (targetEnemy != null && (targetEnemy.IsDead || targetEnemy.currentHP <= 0)) yield break;
if (targetEnemy != null && AreAllEnemiesDead())
if (targetEnemy != null)
{
LogVerbose($"[EffectSystem] Stopping DOT to {target.name} because all enemies are dead.");
yield break;
if (AreAllEnemiesDead())
{
LogVerbose($"[EffectSystem] Stopping DOT to {target.name} because all enemies are dead.");
yield break;
}
if (teamUIController.Instance != null)
{
if (!teamUIController.Instance.IsAnyAllyActive())
{
LogVerbose($"[EffectSystem] Stopping DOT to {target.name} because no allies are active.");
yield break;
}
if (source != null)
{
var sourceAlly = source.GetComponent<AllyCombatant>();
if (sourceAlly != null && !teamUIController.Instance.IsAllySlotActive(sourceAlly.slotIndex))
{
LogVerbose($"[EffectSystem] Stopping DOT to {target.name} because source ally slot {sourceAlly.slotIndex} is not active.");
yield break;
}
}
}
}
int beforeHP = 0;
@@ -1554,6 +1688,55 @@ public class EffectSystem : MonoBehaviour
iBudeffPrefabController.Instance?.UnregisterEnemyTimedEffect(enemyInstanceId, iconId);
}
}
private static bool IsDirectRedirectableBuffEffectType(EffectType effectType)
{
switch (effectType)
{
case EffectType.IncreaseMaxHP:
case EffectType.DecreaseMaxHP:
case EffectType.IncreaseMaxMana:
case EffectType.DecreaseMaxMana:
case EffectType.IncreaseScoreEfficiency:
case EffectType.DecreaseScoreEfficiency:
case EffectType.IncreaseDamageResistance:
case EffectType.DecreaseDamageResistance:
case EffectType.IncreaseAttack:
case EffectType.DecreaseAttack:
case EffectType.RewriteNonMissToPerfect:
return true;
default:
return false;
}
}
private static List<GameObject> RedirectDirectBuffTargets(List<GameObject> targets)
{
if (targets == null || targets.Count == 0) return targets;
var redirectedTargets = new List<GameObject>(targets.Count);
for (int i = 0; i < targets.Count; i++)
{
var t = targets[i];
if (t == null)
{
redirectedTargets.Add(null);
continue;
}
var ally = t.GetComponent<AllyCombatant>() ?? t.GetComponentInChildren<AllyCombatant>(true);
if (ally != null && ally.TryRedirectIncomingGenericBuff(out var redirected) && redirected != null)
{
redirectedTargets.Add(redirected.gameObject);
}
else
{
redirectedTargets.Add(t);
}
}
return redirectedTargets;
}
#endregion
}
+49 -10
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using UnityEngine;
@@ -8,6 +8,9 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
{
public EnemyData_SO sourceData;
private int _currentDifficultyID;
private GameObject _lastAttacker;
// runtime stats
public int maxHP = 100;
public int currentHP = 100;
@@ -35,21 +38,24 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
}
// Documentation text normalized.
public void InitializeFromSO(EnemyData_SO so)
public void InitializeFromSO(EnemyData_SO so, int difficultyID)
{
// This enemy GameObject can be reused for multiple enemy entries in sequence.
// Clear any runtime buffs so they don't leak across enemy transitions.
activeBuffs.Clear();
_attackBaseInitialized = false;
_attackBaseUnbuffed = 0;
_lastAttacker = null;
_currentDifficultyID = difficultyID;
sourceData = so;
if (so != null)
{
maxHP = Mathf.Max(1, so.enemy_maxHP);
damageResistance = so.enemy_damageResistance;
SetAttack(so.enemy_baseAttack);
maxMana = Mathf.Max(0, so.GetEffectiveMaxMana());
var stats = so.GetStatsByDifficultyID(difficultyID);
maxHP = Mathf.Max(1, stats.enemy_maxHP);
damageResistance = stats.enemy_damageResistance;
SetAttack(stats.enemy_baseAttack);
maxMana = Mathf.Max(0, stats.enemy_maxMana);
}
else
{
@@ -131,6 +137,13 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
{
if (isDead) return;
// Check if any allies are active before receiving damage
if (teamUIController.Instance != null && !teamUIController.Instance.IsAnyAllyActive())
{
return;
}
_lastAttacker = source;
float effective = amount * (1f - damageResistance);
int delta = Mathf.CeilToInt(effective);
currentHP = Mathf.Clamp(currentHP - delta, 0, maxHP);
@@ -278,9 +291,27 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
{
if (isDead) return;
// Finally mark dead so further damage/heal is ignored.
// We set this EARLY because listeners (OnEnemyDied) might re-initialize this instance
// for the next enemy in sequence, which would set isDead = false.
// award score to last attacker
if (sourceData != null)
{
var stats = sourceData.GetStatsByDifficultyID(_currentDifficultyID);
int award = stats.scoreOnBeingDefeated;
if (award > 0)
{
var ally = _lastAttacker != null ? _lastAttacker.GetComponent<AllyCombatant>() : null;
if (ally != null)
{
ally.AddScoreDirect(award);
}
else
{
// If no specific attacker, try to find current primary track or just add to global
ScoreManager.Instance?.AddIdolScoreForTrack(0, award);
}
}
}
// Finally mark dead
isDead = true;
// 播放 KO 特效
@@ -318,11 +349,19 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
// notify listeners (UI/manager) that enemy died AFTER triggering skills so they operate on the current instance
OnEnemyDied?.Invoke(this);
// Check whether all enemies are now defeated. Use currentHP/IsDead to determine status.
// Check whether all enemies are now defeated.
// If teamUIController exists, it owns enemy-sequence completion and will fire this event exactly once.
// Fallback to legacy scene scan only when teamUIController is not present.
if (Application.isPlaying)
{
try
{
var ui = UnityEngine.Object.FindAnyObjectByType<teamUIController>();
if (ui != null)
{
return;
}
var all = UnityEngine.Object.FindObjectsByType<EnemyCombatant>(FindObjectsInactive.Include, FindObjectsSortMode.None);
bool anyAlive = false;
foreach (var e in all)
+50
View File
@@ -448,6 +448,56 @@ public class ScoreManager : MonoBehaviour
UpdateIdolscoreKeyScales();
}
/// <summary>
/// Add idol score directly to a track (0..4) without changing pm score.
/// Used by skills that grant/consume idol score independently from note judgement pm.
/// </summary>
public void AddIdolScoreForTrack(int trackIndex, int idolDelta)
{
TryHookPerfectBonusEvent();
if (trackIndex < 0 || trackIndex >= idolScoreSums.Length) return;
if (idolDelta == 0) return;
idolScoreSums[trackIndex] += idolDelta;
if (idolScoreSums[trackIndex] < 0) idolScoreSums[trackIndex] = 0;
if (idolScoreSums[trackIndex] > int.MaxValue - 1) idolScoreSums[trackIndex] = int.MaxValue - 1;
red_idolScore_sum = idolScoreSums[0];
green_idolScore_sum = idolScoreSums[1];
yellow_idolScore_sum = idolScoreSums[2];
purple_idolScore_sum = idolScoreSums[3];
blue_idolScore_sum = idolScoreSums[4];
long aggPm = 0;
long aggIdol = 0;
for (int i = 0; i < pmScoreSums.Length; i++) aggPm += pmScoreSums[i];
for (int i = 0; i < idolScoreSums.Length; i++) aggIdol += idolScoreSums[i];
long aggPmWithBonus = aggPm + (long)perfectClearBonusPm;
allSum_pmScore = (int)Mathf.Min((float)aggPmWithBonus, (float)int.MaxValue - 1f);
allSum_idolScore = (int)Mathf.Min((float)aggIdol, (float)int.MaxValue - 1f);
var ui = teamUIController.Instance;
if (ui != null)
{
try
{
if (ui.red_idolScore_sum != null) ui.red_idolScore_sum.text = red_idolScore_sum.ToString();
if (ui.green_idolScore_sum != null) ui.green_idolScore_sum.text = green_idolScore_sum.ToString();
if (ui.yellow_idolScore_sum != null) ui.yellow_idolScore_sum.text = yellow_idolScore_sum.ToString();
if (ui.purple_idolScore_sum != null) ui.purple_idolScore_sum.text = purple_idolScore_sum.ToString();
if (ui.blue_idolScore_sum != null) ui.blue_idolScore_sum.text = blue_idolScore_sum.ToString();
if (ui.allSum_idolScore != null) ui.allSum_idolScore.text = allSum_idolScore.ToString();
}
catch (System.Exception ex)
{
if (JudgeManager.IsDebugEnabled) Debug.LogWarning($"[ScoreManager] Failed to write idol sums to teamUIController fields: {ex}");
}
}
RecalculateTotal();
}
public void RecalculateTotal()
{
EnsureAllyCache();
+253 -26
View File
@@ -1,4 +1,4 @@
using System.Collections;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
@@ -163,7 +163,7 @@ public class SkillBuilder : MonoBehaviour
var allyGO = GetAllyObjectBySlot(trackIndex);
if (allyGO == null) return;
var ally = allyGO.GetComponent<AllyCombatant>();
if (ally != null && ally.IsDead) return; // dead allies should not gain mana / take miss damage / deal note-hit damage
if (ally != null && (ally.IsDead || ally.maxHP == 0)) return; // dead or empty allies should not gain mana / take miss damage / deal note-hit damage
var so = GetAllyHeroSOBySlot(trackIndex);
// try to obtain per-level params from SO if present
AllyHero_SO.AllyLevelInfo levelInfo = so != null ? (so.GetEffectiveLevelForCurrentEXP()) : null;
@@ -214,7 +214,35 @@ public class SkillBuilder : MonoBehaviour
else if (ally != null) mName = ally.name; // fallback to GameObject name
SkillTriggerFeedUI.PushMiss(mName);
float missBase = (levelInfo != null) ? levelInfo.missHpLossBase : missHpLossBase;
// --- New Miss Damage Formula ---
// Formula: EnemyAttack * DifficultyLevel * 0.03 * AllyMissHpLossBase * (1 - Resistance)
// Note: AllyCombatant.ReceiveDamage already applies (1 - Resistance), so we calculate the base here.
float enemyAtk = 0f;
var enemyGO = GameObject.Find("thisEnemy");
if (enemyGO != null)
{
var ec = enemyGO.GetComponent<EnemyCombatant>();
if (ec != null) enemyAtk = ec.attack;
}
float diffLevel = 1f;
if (BeatmapManager.Instance != null && BeatmapManager.Instance.assignedSongData != null)
{
int curDiff = BeatmapManager.Instance.assignedDifficulty;
if (BeatmapManager.Instance.assignedSongData.chartFiles != null)
{
var chart = BeatmapManager.Instance.assignedSongData.chartFiles.Find(c => c.difficulty == curDiff);
if (chart != null) diffLevel = chart.difficultyLEVEL;
}
}
float fixedMult = 0.03f;
float allyMissBase = (levelInfo != null) ? levelInfo.missHpLossBase : missHpLossBase;
float missBase = enemyAtk * diffLevel * fixedMult * allyMissBase;
LogVerbose($"[SkillBuilder] Miss Damage Calc: Atk({enemyAtk}) * DiffLvl({diffLevel}) * Mult({fixedMult}) * AllyBase({allyMissBase}) = {missBase}");
// 播放敌人攻击特效,并将伤害逻辑延迟到特效到达时执行
bool effectStarted = false;
@@ -779,6 +807,64 @@ public class SkillBuilder : MonoBehaviour
case EffectType.DecreaseAttack:
case EffectType.RedirectNextDamageToSelf:
case EffectType.RedirectSelfDamageToAdjacent:
case EffectType.RewriteNonMissToPerfect:
return true;
default:
return false;
}
}
// Buff redirection (交给我!/都给你!) should affect any "buff-like" effect that is represented
// by budeff icons or timed status changes, not only Buff objects.
private static bool IsRedirectableBudeffEvent(EffectType effectType)
{
switch (effectType)
{
case EffectType.DamageOverTimeEnemy:
case EffectType.DamageOverTimeAlly:
case EffectType.ReduceEnemyHealOverTime:
case EffectType.BuffDuration:
case EffectType.DebuffDuration:
case EffectType.IncreaseManaOverTime:
case EffectType.IncreaseMaxHP:
case EffectType.DecreaseMaxHP:
case EffectType.IncreaseMaxMana:
case EffectType.DecreaseMaxMana:
case EffectType.IncreaseScoreEfficiency:
case EffectType.DecreaseScoreEfficiency:
case EffectType.IncreaseDamageResistance:
case EffectType.DecreaseDamageResistance:
case EffectType.IncreaseAttack:
case EffectType.DecreaseAttack:
case EffectType.RedirectNextDamageToSelf:
case EffectType.RedirectSelfDamageToAdjacent:
case EffectType.RewriteNonMissToPerfect:
return true;
default:
return false;
}
}
// Subset for def.operateDirectly branch where effects are applied inside SkillBuilder switch.
// Do not include types that fall through to default ExecuteSkill, otherwise redirect would be consumed too early.
private static bool IsRedirectableDirectBranchEffect(EffectType effectType)
{
switch (effectType)
{
case EffectType.DamageOverTimeEnemy:
case EffectType.IncreaseManaOverTime:
case EffectType.ReduceEnemyHealOverTime:
case EffectType.BuffDuration:
case EffectType.DebuffDuration:
case EffectType.IncreaseMaxHP:
case EffectType.DecreaseMaxHP:
case EffectType.IncreaseMaxMana:
case EffectType.DecreaseMaxMana:
case EffectType.IncreaseScoreEfficiency:
case EffectType.DecreaseScoreEfficiency:
case EffectType.IncreaseAttack:
case EffectType.DecreaseAttack:
case EffectType.RedirectNextDamageToSelf:
return true;
default:
return false;
@@ -1372,14 +1458,23 @@ ResolvedGroup:
{
if (t == null) continue;
GameObject target = t;
var ally = target.GetComponent<AllyCombatant>();
if (ally != null && IsRedirectableDirectBranchEffect(def.effectType))
{
if (ally.TryRedirectIncomingGenericBuff(out var redirectedAlly) && redirectedAlly != null)
{
target = redirectedAlly.gameObject;
ally = redirectedAlly;
}
}
var ic = target.GetComponent<ICombatant>();
if (GameConfig.skillDebugMode)
{
Debug.Log($"<color=#00FFFF>[SkillDebug]</color> <color=#FFD700>ID: {def.skillId}</color> | <color=#00FF00>Trigger: {def.triggerCondition}</color> | <color=#FFA500>Target: {t.name}</color> | <color=#EE82EE>Effect: {def.effectType}</color> | <color=#FF4500>Value: {amountTotal:F2}</color>");
Debug.Log($"<color=#00FFFF>[SkillDebug]</color> <color=#FFD700>ID: {def.skillId}</color> | <color=#00FF00>Trigger: {def.triggerCondition}</color> | <color=#FFA500>Target: {target.name}</color> | <color=#EE82EE>Effect: {def.effectType}</color> | <color=#FF4500>Value: {amountTotal:F2}</color>");
}
var ally = t.GetComponent<AllyCombatant>();
var ic = t.GetComponent<ICombatant>();
if (ally != null && ShouldSpawnApprForEffect(def.effectType))
{
var apprSprite = iBudeffPrefabController.Instance != null ? iBudeffPrefabController.Instance.GetSpriteForEffect(def.effectType) : null;
@@ -1405,7 +1500,7 @@ ResolvedGroup:
string iconId = null;
int allySlotIndex = -1;
int enemyInstanceId = 0;
var enemyDot = t.GetComponent<EnemyCombatant>() ?? t.GetComponentInChildren<EnemyCombatant>(true);
var enemyDot = target.GetComponent<EnemyCombatant>() ?? target.GetComponentInChildren<EnemyCombatant>(true);
if (enemyDot != null)
{
int ticks = Mathf.Max(1, Mathf.CeilToInt(def.defaultDuration / def.GetEffectiveTickInterval()));
@@ -1420,7 +1515,7 @@ ResolvedGroup:
iconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_bleeding, perTick, def.defaultDuration);
allySlotIndex = ally.slotIndex;
}
StartCoroutine(ApplyDamageOverTimeDirect(t, amountTotal, def.defaultDuration, def.GetEffectiveTickInterval(), caster, iconId, allySlotIndex, enemyInstanceId));
StartCoroutine(ApplyDamageOverTimeDirect(target, amountTotal, def.defaultDuration, def.GetEffectiveTickInterval(), caster, iconId, allySlotIndex, enemyInstanceId));
}
break;
case EffectType.HealSingleSelf:
@@ -1437,7 +1532,7 @@ ResolvedGroup:
}
else
{
StartCoroutine(ApplyHealOverTimeDirect(t, amountTotal, def.defaultDuration, def.GetEffectiveTickInterval(), caster));
StartCoroutine(ApplyHealOverTimeDirect(target, amountTotal, def.defaultDuration, def.GetEffectiveTickInterval(), caster));
}
break;
case EffectType.IncreaseManaOverTime:
@@ -1467,7 +1562,7 @@ ResolvedGroup:
healReceivedMultiplier = 1f - reduction
};
ic.ApplyBuff(deb, caster);
StartCoroutine(RemoveBuffAfterDuration(t, deb.buffId, deb.duration));
StartCoroutine(RemoveBuffAfterDuration(target, deb.buffId, deb.duration));
}
break;
case EffectType.BuffDuration:
@@ -1484,7 +1579,7 @@ ResolvedGroup:
scoreMultiplier = mult
};
ic.ApplyBuff(b, caster);
StartCoroutine(RemoveBuffAfterDuration(t, b.buffId, b.duration));
StartCoroutine(RemoveBuffAfterDuration(target, b.buffId, b.duration));
}
break;
// Documentation text normalized.
@@ -1496,7 +1591,7 @@ ResolvedGroup:
if (d > 0) ally.SetCurrentHP(ally.currentHP + d, false);
iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_maxHP_up, d, 0f);
}
else if (t.TryGetComponent<EnemyCombatant>(out var enemy))
else if (target.TryGetComponent<EnemyCombatant>(out var enemy))
{
int d = Mathf.CeilToInt(amountTotal);
enemy.SetMaxHP(enemy.maxHP + d, false);
@@ -1511,7 +1606,7 @@ ResolvedGroup:
ally.SetMaxHP(Mathf.Max(1, ally.maxHP - d), false);
iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_maxHP_down, -d, 0f);
}
else if (t.TryGetComponent<EnemyCombatant>(out var enemy2))
else if (target.TryGetComponent<EnemyCombatant>(out var enemy2))
{
int d = Mathf.CeilToInt(amountTotal);
enemy2.SetMaxHP(Mathf.Max(1, enemy2.maxHP - d), false);
@@ -1525,7 +1620,7 @@ ResolvedGroup:
ally.SetMaxMana(ally.maxMana + d, false);
iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_maxMana_up, d, 0f);
}
else if (t.TryGetComponent<EnemyCombatant>(out var enemy3))
else if (target.TryGetComponent<EnemyCombatant>(out var enemy3))
{
int d = Mathf.CeilToInt(amountTotal);
enemy3.SetMaxMana(enemy3.maxMana + d, false);
@@ -1539,7 +1634,7 @@ ResolvedGroup:
ally.SetMaxMana(Mathf.Max(1, ally.maxMana - d), false);
iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_maxMana_down, -d, 0f);
}
else if (t.TryGetComponent<EnemyCombatant>(out var enemy4))
else if (target.TryGetComponent<EnemyCombatant>(out var enemy4))
{
int d = Mathf.CeilToInt(amountTotal);
enemy4.SetMaxMana(Mathf.Max(1, enemy4.maxMana - d), false);
@@ -1554,7 +1649,7 @@ ResolvedGroup:
ally.scoreEfficiency += amountTotal;
iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_scoreEfficiency_up, amountTotal, 0f);
}
else if (!TryApplyRefreshOnlyTimedEffect(def, t, amountTotal, def.defaultDuration))
else if (!TryApplyRefreshOnlyTimedEffect(def, target, amountTotal, def.defaultDuration))
{
var iconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_scoreEfficiency_up, amountTotal, def.defaultDuration);
StartCoroutine(ApplyTemporaryScoreEfficiencyChangeDirect(ally, amountTotal, def.defaultDuration, iconId));
@@ -1569,7 +1664,7 @@ ResolvedGroup:
ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency - amountTotal);
iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_scoreEfficiency_down, -amountTotal, 0f);
}
else if (!TryApplyRefreshOnlyTimedEffect(def, t, amountTotal, def.defaultDuration))
else if (!TryApplyRefreshOnlyTimedEffect(def, target, amountTotal, def.defaultDuration))
{
var iconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_scoreEfficiency_down, -amountTotal, def.defaultDuration);
StartCoroutine(ApplyTemporaryScoreEfficiencyChangeDirect(ally, -amountTotal, def.defaultDuration, iconId));
@@ -1586,20 +1681,20 @@ ResolvedGroup:
ally.ModifyAttack(delta);
iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_atk_up, delta, 0f);
}
else if (!TryApplyRefreshOnlyTimedEffect(def, t, amountTotal, def.defaultDuration))
else if (!TryApplyRefreshOnlyTimedEffect(def, target, amountTotal, def.defaultDuration))
{
var iconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_atk_up, delta, def.defaultDuration);
StartCoroutine(ApplyTemporaryAttackChangeDirect(ally, delta, def.defaultDuration, iconId));
}
}
else if (t.TryGetComponent<EnemyCombatant>(out var enemy5))
else if (target.TryGetComponent<EnemyCombatant>(out var enemy5))
{
if (def.defaultDuration <= 0f)
{
enemy5.ModifyAttack(delta);
iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemy5, PlayerBudeffIconType.ot_atk_up, delta, 0f);
}
else if (!TryApplyRefreshOnlyTimedEffect(def, t, amountTotal, def.defaultDuration))
else if (!TryApplyRefreshOnlyTimedEffect(def, target, amountTotal, def.defaultDuration))
{
var iconId = iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemy5, PlayerBudeffIconType.ot_atk_up, delta, def.defaultDuration);
StartCoroutine(ApplyTemporaryAttackChangeDirect(enemy5, delta, def.defaultDuration, iconId, enemy5.GetInstanceID()));
@@ -1617,20 +1712,20 @@ ResolvedGroup:
ally.ModifyAttack(-delta);
iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_atk_down, -delta, 0f);
}
else if (!TryApplyRefreshOnlyTimedEffect(def, t, amountTotal, def.defaultDuration))
else if (!TryApplyRefreshOnlyTimedEffect(def, target, amountTotal, def.defaultDuration))
{
var iconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_atk_down, -delta, def.defaultDuration);
StartCoroutine(ApplyTemporaryAttackChangeDirect(ally, -delta, def.defaultDuration, iconId));
}
}
else if (t.TryGetComponent<EnemyCombatant>(out var enemy6))
else if (target.TryGetComponent<EnemyCombatant>(out var enemy6))
{
if (def.defaultDuration <= 0f)
{
enemy6.ModifyAttack(-delta);
iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemy6, PlayerBudeffIconType.ot_atk_down, -delta, 0f);
}
else if (!TryApplyRefreshOnlyTimedEffect(def, t, amountTotal, def.defaultDuration))
else if (!TryApplyRefreshOnlyTimedEffect(def, target, amountTotal, def.defaultDuration))
{
var iconId = iBudeffPrefabController.Instance?.RegisterEnemyTimedEffect(enemy6, PlayerBudeffIconType.ot_atk_down, -delta, def.defaultDuration);
StartCoroutine(ApplyTemporaryAttackChangeDirect(enemy6, -delta, def.defaultDuration, iconId, enemy6.GetInstanceID()));
@@ -1677,13 +1772,27 @@ ResolvedGroup:
{
var resolvedTargets = ResolveTargetsLocal(def.defaultSelector, caster, specificTarget);
bool appliedRefreshOnly = false;
bool mayHandleLocally = IsRefreshOnlyTimedSkill(def) || IsRefreshOnlyOverTimeSkill(def);
if (resolvedTargets != null)
{
foreach (var t in resolvedTargets)
{
if (t == null) continue;
bool handled = TryApplyRefreshOnlyTimedEffect(def, t, amountTotal, def.defaultDuration);
if (!handled) handled = TryApplyRefreshOnlyOverTimeEffect(def, t, amountTotal, caster);
var target = t;
if (mayHandleLocally)
{
var ally = target.GetComponent<AllyCombatant>() ?? target.GetComponentInChildren<AllyCombatant>(true);
if (ally != null && IsRedirectableBudeffEvent(def.effectType))
{
if (ally.TryRedirectIncomingGenericBuff(out var redirectedAlly) && redirectedAlly != null)
{
target = redirectedAlly.gameObject;
}
}
}
bool handled = TryApplyRefreshOnlyTimedEffect(def, target, amountTotal, def.defaultDuration);
if (!handled) handled = TryApplyRefreshOnlyOverTimeEffect(def, target, amountTotal, caster);
if (handled) appliedRefreshOnly = true;
}
}
@@ -1992,6 +2101,89 @@ ResolvedGroup:
}
}
// Trigger ally skills when an adjacent ally releases a skill (mana-full cast path).
public void TriggerOnAdjacentAllySkillCast(int casterSlotIndex)
{
if (!Application.isPlaying) return;
if (casterSlotIndex < 0) return;
int slots = 5;
if (teamUIController.Instance != null && teamUIController.Instance.allySlotIds != null)
slots = teamUIController.Instance.allySlotIds.Count;
for (int i = 0; i < slots; i++)
{
if (i == casterSlotIndex) continue;
bool isAdjacent = false;
if (teamUIController.Instance != null)
{
int[] adj = teamUIController.Instance.GetAdjacentAllyIndices(i);
if (adj != null)
{
for (int j = 0; j < adj.Length; j++)
{
if (adj[j] == casterSlotIndex) { isAdjacent = true; break; }
}
}
}
else
{
isAdjacent = Mathf.Abs(i - casterSlotIndex) == 1;
}
if (!isAdjacent) continue;
var allyObj = GetAllyObjectBySlot(i);
var allyComp = allyObj != null ? allyObj.GetComponent<AllyCombatant>() : null;
if (allyComp != null && allyComp.IsDead) continue;
var so = GetAllyHeroSOBySlot(i);
if (so == null) continue;
if (so.equippedSkillGroupIDs != null && so.equippedSkillGroupIDs.Length > 0 && so.skillGroups != null)
{
foreach (int gid in so.equippedSkillGroupIDs)
{
if (gid == 0) continue;
SkillGroup group = null;
for (int k = 0; k < so.skillGroups.Length; k++)
{
var g = so.skillGroups[k];
if (g != null && g.skillGroupID == gid) { group = g; break; }
}
if (group == null || group.skills == null) continue;
foreach (var def in group.skills)
{
if (def == null) continue;
if (def.triggerCondition != SkillDefinition.SkillTrigger.OnAdjacentAllySkillCast) continue;
UseSkillDefinition(def, i, -1f, null);
}
}
continue;
}
var fallbackGroup = so.GetPrimarySkillGroup();
if (fallbackGroup != null && fallbackGroup.skills != null)
{
foreach (var def in fallbackGroup.skills)
{
if (def == null) continue;
if (def.triggerCondition != SkillDefinition.SkillTrigger.OnAdjacentAllySkillCast) continue;
UseSkillDefinition(def, i, -1f, null);
}
continue;
}
var primary = so.GetPrimarySkill();
if (primary != null && primary.triggerCondition == SkillDefinition.SkillTrigger.OnAdjacentAllySkillCast)
{
UsePrimarySkillForSlot(i, -1f, null);
}
}
}
private Dictionary<string, float> _lastOnNoteHitTriggerTime = new Dictionary<string, float>();
// track processed unique note IDs (e.g. long-hold note id) so shared effects (mana/hp) are applied only once
@@ -2306,6 +2498,23 @@ ResolvedGroup:
if (duration <= 0f || tickInterval <= 0f)
{
if (target.GetComponent<EnemyCombatant>() != null && teamUIController.Instance != null)
{
if (!teamUIController.Instance.IsAnyAllyActive())
{
yield break;
}
if (source != null)
{
var sourceAlly = source.GetComponent<AllyCombatant>();
if (sourceAlly != null && !teamUIController.Instance.IsAllySlotActive(sourceAlly.slotIndex))
{
yield break;
}
}
}
if (ally != null) ally.ModifyHP(-Mathf.CeilToInt(totalAmount), true);
else ic?.ReceiveDamage(totalAmount, source);
yield break;
@@ -2317,6 +2526,24 @@ ResolvedGroup:
while (elapsed < duration)
{
if (target == null) yield break;
if (target.GetComponent<EnemyCombatant>() != null && teamUIController.Instance != null)
{
if (!teamUIController.Instance.IsAnyAllyActive())
{
yield break;
}
if (source != null)
{
var sourceAlly = source.GetComponent<AllyCombatant>();
if (sourceAlly != null && !teamUIController.Instance.IsAllySlotActive(sourceAlly.slotIndex))
{
yield break;
}
}
}
if (ally != null) ally.ModifyHP(-Mathf.CeilToInt(perTick), true);
else ic?.ReceiveDamage(perTick, source);
yield return new WaitForSeconds(tickInterval);
+3 -1
View File
@@ -35,7 +35,9 @@ public class SkillDefinition : ScriptableObject
// New: attack threshold trigger (attack < attackTriggerValue)
OnAttackBelowValue,
// New: triggers once when this ally is defeated (HP reaches 0 from above).
OnSelfDefeated
OnSelfDefeated,
// New: triggers on this ally when an adjacent ally releases a skill (casts on mana full).
OnAdjacentAllySkillCast
}
[Header("Inspector")]