超大量的更新 修复很多问题,gameplay特效初步

This commit is contained in:
FloatGaming
2026-02-14 23:46:20 +08:00
parent ef8eb67259
commit 3a7a0b4669
360 changed files with 85670 additions and 4144 deletions
+244 -46
View File
@@ -4,13 +4,13 @@ using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// EffectSystem Чͳһַӿڡ
/// - ѡΧSelectorĿϣԼȫѾԼѾѾϵˡˣ
/// - ЧͣEffectType˼ʱ/˺///ȡ?
/// Documentation text normalized.
/// Documentation text normalized.
/// Documentation text normalized.
///
/// ĿʵӦʵ ICombatant ӿԱ EffectSystem ܵͳһ˺//ȣûʵ֣־ѡ
/// Documentation text normalized.
///
/// ýűΪܣãֻ?ApplyEffect(...) ȷűá
/// Documentation text normalized.
/// </summary>
public class EffectSystem : MonoBehaviour
{
@@ -34,14 +34,14 @@ public class EffectSystem : MonoBehaviour
}
/// <summary>
/// ApplyEffect: ָѡΧĿӦЧ
/// - selector: Ŀѡ
/// - effectType: Чͣʱ/ȣ
/// - amount: ֵ˺Чǿȣ
/// - duration: ʱԳЧ Buff/Debuff ЧʱЧΪ 0
/// - source: ߣ Self/AlliesExceptSelf жϻ¼?
/// - specificTarget: selector Ϊ CurrentEnemies ָֻʱɴĿ?
/// - tickInterval: Ч tick Ĭ?1
/// Documentation text normalized.
/// Documentation text normalized.
/// Documentation text normalized.
/// Documentation text normalized.
/// Documentation text normalized.
/// Documentation text normalized.
/// Documentation text normalized.
/// Documentation text normalized.
/// </summary>
public void ApplyEffect(Selector selector, EffectType effectType, float amount, float duration = 0f, GameObject source = null, GameObject specificTarget = null, float tickInterval = 1f)
{
@@ -59,10 +59,42 @@ public class EffectSystem : MonoBehaviour
if (targets == null || targets.Count == 0)
{
Debug.LogWarning($"[EffectSystem] No targets resolved for selector={selector} specificTarget={(specificTarget ? specificTarget.name : "null")}");
LogVerbose($"[EffectSystem] ApplyEffect: no targets (selector={selector}, source={(source ? source.name : "null")}, specificTarget={(specificTarget ? specificTarget.name : "null")})");
return;
}
// --- Projectile Logic ---
// Trigger projectile if source exists and is targeting the opposite side
if (GfxController.Instance != null && source != null)
{
foreach (var t in targets)
{
if (t != null && t != source)
{
// Identify if this is an offensive targeting (Ally -> Enemy or Enemy -> Ally)
bool sourceIsAlly = source.name.StartsWith("ally_0");
bool sourceIsEnemy = source.name == "thisEnemy";
bool targetIsAlly = t.name.StartsWith("ally_0");
bool targetIsEnemy = t.name == "thisEnemy";
if ((sourceIsAlly && targetIsEnemy) || (sourceIsEnemy && targetIsAlly))
{
// Captured target for callback
GameObject targetToHit = t;
bool isEnemyTarget = targetIsEnemy;
GfxController.Instance.PlayProjectile(source, t, (finalPos) => {
// Only play hit FX when projectile arrives
if (targetToHit != null && GfxController.Instance != null)
{
GfxController.Instance.PlayHitFX(targetToHit, null, isEnemyTarget, finalPos);
}
});
}
}
}
}
switch (effectType)
{
case EffectType.DamageSingleEnemy:
@@ -126,14 +158,25 @@ public class EffectSystem : MonoBehaviour
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 };
// Generic timed buff: treat amount as a score multiplier (1.5 = +50%, 0.5 = -50%).
Buff buff = new Buff
{
buffId = Guid.NewGuid().ToString(),
duration = duration,
scoreMultiplier = amount > 0f ? amount : 1f
};
foreach (var t in targets)
StartCoroutine(ApplyTimedBuffCoroutine(t, buff, source));
break;
case EffectType.DebuffDuration:
Buff debuff = new Buff { buffId = Guid.NewGuid().ToString(), duration = duration };
// Generic timed debuff: treat amount as a score multiplier (usually < 1).
Buff debuff = new Buff
{
buffId = Guid.NewGuid().ToString(),
duration = duration,
scoreMultiplier = amount > 0f ? amount : 1f
};
foreach (var t in targets)
StartCoroutine(ApplyTimedDebuffCoroutine(t, debuff, source));
break;
@@ -144,8 +187,16 @@ public class EffectSystem : MonoBehaviour
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" };
// Debuff: reduce heals received (healReceivedMultiplier < 1).
// amount is interpreted as a reduction ratio in [0..1], so 0.2 means -20% heals, 1 means -100% heals.
float reduction = Mathf.Clamp01(amount);
Buff healReductionDebuff = new Buff
{
buffId = Guid.NewGuid().ToString(),
duration = duration,
description = "ReduceHeal",
healReceivedMultiplier = 1f - reduction
};
foreach (var t in targets)
StartCoroutine(ApplyTimedDebuffCoroutine(t, healReductionDebuff, source));
break;
@@ -191,11 +242,19 @@ public class EffectSystem : MonoBehaviour
var ally = t.GetComponent<AllyCombatant>();
if (ally != null)
{
ally.SetMaxHP(ally.maxHP + Mathf.CeilToInt(amount), false);
int delta = Mathf.CeilToInt(amount);
if (duration <= 0f)
ally.SetMaxHP(ally.maxHP + delta, false);
else
StartCoroutine(ApplyTemporaryMaxHPDeltaCoroutine(ally, delta, duration));
}
else if (t.TryGetComponent<EnemyCombatant>(out var enemy))
{
enemy.SetMaxHP(enemy.maxHP + Mathf.CeilToInt(amount), false);
int delta = Mathf.CeilToInt(amount);
if (duration <= 0f)
enemy.SetMaxHP(enemy.maxHP + delta, false);
else
StartCoroutine(ApplyTemporaryMaxHPDeltaCoroutine(enemy, delta, duration));
}
}
break;
@@ -207,11 +266,19 @@ public class EffectSystem : MonoBehaviour
var ally = t.GetComponent<AllyCombatant>();
if (ally != null)
{
ally.SetMaxHP(Mathf.Max(1, ally.maxHP - Mathf.CeilToInt(amount)), false);
int delta = Mathf.CeilToInt(amount);
if (duration <= 0f)
ally.SetMaxHP(Mathf.Max(1, ally.maxHP - delta), false);
else
StartCoroutine(ApplyTemporaryMaxHPDeltaCoroutine(ally, -delta, duration));
}
else if (t.TryGetComponent<EnemyCombatant>(out var enemy2))
{
enemy2.SetMaxHP(Mathf.Max(1, enemy2.maxHP - Mathf.CeilToInt(amount)), false);
int delta = Mathf.CeilToInt(amount);
if (duration <= 0f)
enemy2.SetMaxHP(Mathf.Max(1, enemy2.maxHP - delta), false);
else
StartCoroutine(ApplyTemporaryMaxHPDeltaCoroutine(enemy2, -delta, duration));
}
}
break;
@@ -223,11 +290,19 @@ public class EffectSystem : MonoBehaviour
var ally = t.GetComponent<AllyCombatant>();
if (ally != null)
{
ally.SetMaxMana(ally.maxMana + Mathf.CeilToInt(amount), false);
int delta = Mathf.CeilToInt(amount);
if (duration <= 0f)
ally.SetMaxMana(ally.maxMana + delta, false);
else
StartCoroutine(ApplyTemporaryMaxManaDeltaCoroutine(ally, delta, duration));
}
else if (t.TryGetComponent<EnemyCombatant>(out var enemy3))
{
enemy3.SetMaxMana(enemy3.maxMana + Mathf.CeilToInt(amount), false);
int delta = Mathf.CeilToInt(amount);
if (duration <= 0f)
enemy3.SetMaxMana(enemy3.maxMana + delta, false);
else
StartCoroutine(ApplyTemporaryMaxManaDeltaCoroutine(enemy3, delta, duration));
}
}
break;
@@ -239,11 +314,19 @@ public class EffectSystem : MonoBehaviour
var ally = t.GetComponent<AllyCombatant>();
if (ally != null)
{
ally.SetMaxMana(Mathf.Max(1, ally.maxMana - Mathf.CeilToInt(amount)), false);
int delta = Mathf.CeilToInt(amount);
if (duration <= 0f)
ally.SetMaxMana(Mathf.Max(1, ally.maxMana - delta), false);
else
StartCoroutine(ApplyTemporaryMaxManaDeltaCoroutine(ally, -delta, duration));
}
else if (t.TryGetComponent<EnemyCombatant>(out var enemy4))
{
enemy4.SetMaxMana(Mathf.Max(1, enemy4.maxMana - Mathf.CeilToInt(amount)), false);
int delta = Mathf.CeilToInt(amount);
if (duration <= 0f)
enemy4.SetMaxMana(Mathf.Max(1, enemy4.maxMana - delta), false);
else
StartCoroutine(ApplyTemporaryMaxManaDeltaCoroutine(enemy4, -delta, duration));
}
}
break;
@@ -310,7 +393,7 @@ public class EffectSystem : MonoBehaviour
{
int deltaE = Mathf.CeilToInt(amount);
if (duration <= 0f)
enemy5.attack += deltaE;
enemy5.ModifyAttack(deltaE);
else
StartCoroutine(ApplyTemporaryAttackChangeCoroutine(enemy5, deltaE, duration));
}
@@ -339,7 +422,7 @@ public class EffectSystem : MonoBehaviour
{
int deltaE = Mathf.CeilToInt(amount);
if (duration <= 0f)
enemy6.attack = Mathf.Max(0, enemy6.attack - deltaE);
enemy6.ModifyAttack(-deltaE);
else
StartCoroutine(ApplyTemporaryAttackChangeCoroutine(enemy6, -deltaE, duration));
}
@@ -430,6 +513,56 @@ public class EffectSystem : MonoBehaviour
}
break;
case EffectType.GrantExtraPerfect:
foreach (var t in targets)
{
if (t == null) continue;
var ally = t.GetComponent<AllyCombatant>();
if (ally == null) continue;
if (ally.IsDead) continue;
int slot = ally.slotIndex;
// Update judgement counts (for settlement UI).
var sm = ScoreManager.Instance;
if (sm != null)
{
sm.countPerfect += 1;
if (slot >= 0 && slot < sm.trackPerfectCounts.Length) sm.trackPerfectCounts[slot] += 1;
}
// Grant score as if a Perfect happened on this track.
try
{
int pmDelta = ally.AddScoreForJudge("Perfect");
float eff = ally.scoreEfficiency;
sm?.AddPmScoreForTrack(slot, pmDelta, eff);
}
catch { }
// Trigger note-hit hooks as a virtual Perfect event (mana gain, damage, and Perfect-triggered skills).
try
{
if (SkillBuilder.Instance != null)
{
string uid = $"virtualPerfect:{slot}:{Time.frameCount}:{Guid.NewGuid()}";
SkillBuilder.Instance.NotifyNoteHit(slot, "Perfect", SkillDefinition.NoteTypeTrigger.Tap, uid);
}
}
catch { }
}
break;
case EffectType.RedirectSelfDamageToAdjacent:
foreach (var t in targets)
{
if (t == null) continue;
var ally = t.GetComponent<AllyCombatant>();
if (ally == null) continue;
ally.ActivateSelfDamageRedirectToAdjacent(duration);
}
break;
default:
Debug.LogWarning($"[EffectSystem] Unhandled EffectType {effectType}");
break;
@@ -587,9 +720,21 @@ public class EffectSystem : MonoBehaviour
foreach (var g in list)
if (!uniq.Contains(g)) uniq.Add(g);
// Debug: log resolved targets for diagnosis
// Filter out dead combatants so they won't receive further effects (damage/score/etc.).
// This prevents "dead units still get hit / still gain score" edge cases.
uniq.RemoveAll(go =>
{
if (go == null) return true;
var ally = go.GetComponent<AllyCombatant>();
if (ally != null) return ally.IsDead;
var enemy = go.GetComponent<EnemyCombatant>();
if (enemy != null) return enemy.IsDead || enemy.currentHP <= 0;
return false;
});
// Debug: log resolved targets for diagnosis (guarded to avoid log spam in normal gameplay).
if (uniq.Count == 0)
Debug.LogWarning($"[EffectSystem] ResolveTargets -> selector={selector} returned 0 targets (source={(source ? source.name : "null")}, specificTarget={(specificTarget ? specificTarget.name : "null")})");
LogVerbose($"[EffectSystem] ResolveTargets -> selector={selector} returned 0 targets (source={(source ? source.name : "null")}, specificTarget={(specificTarget ? specificTarget.name : "null")})");
else
LogVerbose($"[EffectSystem] ResolveTargets -> selector={selector} resolved {uniq.Count} targets: {string.Join(",", uniq.ConvertAll(x => x != null ? x.name : "null"))}");
@@ -656,8 +801,14 @@ public class EffectSystem : MonoBehaviour
catch { }
// Also include a single named instance if present
var single = GameObject.Find("thisEnemy");
if (single != null && !_cachedEnemies.Contains(single)) _cachedEnemies.Add(single);
// Note: GameObject.Find only works on active objects.
// We already use FindObjectsByType with Include Inactive above, which is safer.
// If we really need to find "thisEnemy" by name even if inactive, we should do it differently.
try
{
var single = GameObject.Find("thisEnemy");
if (single != null && !_cachedEnemies.Contains(single)) _cachedEnemies.Add(single);
} catch { }
// Finally include any objects tagged "Enemy" (if tag exists)
try
@@ -672,7 +823,7 @@ public class EffectSystem : MonoBehaviour
}
#region Instant / OverTime Helpers
private bool AreAllEnemiesDead()
public bool AreAllEnemiesDead()
{
var enemies = FindAllEnemies();
foreach (var go in enemies)
@@ -691,8 +842,14 @@ public class EffectSystem : MonoBehaviour
{
if (target == null) return;
// Skip dead targets (prevents edge cases where something bypasses ResolveTargets filtering).
var targetAlly = target.GetComponent<AllyCombatant>();
if (targetAlly != null && targetAlly.IsDead) return;
var targetEnemy = target.GetComponent<EnemyCombatant>();
if (targetEnemy != null && (targetEnemy.IsDead || targetEnemy.currentHP <= 0)) return;
// If target is an enemy, check if all enemies are already dead
if (target.GetComponent<EnemyCombatant>() != null && AreAllEnemiesDead())
if (targetEnemy != null && AreAllEnemiesDead())
{
LogVerbose($"[EffectSystem] Skip ApplyInstantDamage to {target.name} because all enemies are dead.");
return;
@@ -714,7 +871,6 @@ public class EffectSystem : MonoBehaviour
}
// Record damage taken if target is an ally
var targetAlly = target.GetComponent<AllyCombatant>();
if (targetAlly != null && teamUIController.Instance != null)
{
teamUIController.Instance.RecordDamageTaken(targetAlly.slotIndex, amount);
@@ -760,10 +916,16 @@ public class EffectSystem : MonoBehaviour
yield break;
}
var targetAlly = target.GetComponent<AllyCombatant>();
var targetEnemy = target.GetComponent<EnemyCombatant>();
// Stop immediately if the target is already dead.
if (targetAlly != null && targetAlly.IsDead) yield break;
if (targetEnemy != null && (targetEnemy.IsDead || targetEnemy.currentHP <= 0)) yield break;
if (duration <= 0f)
{
// check death state for immediate damage
if (target.GetComponent<EnemyCombatant>() != null && AreAllEnemiesDead())
if (targetEnemy != null && AreAllEnemiesDead())
{
LogVerbose($"[EffectSystem] Skip DOT (immediate) to {target.name} because all enemies are dead.");
yield break;
@@ -771,13 +933,12 @@ public class EffectSystem : MonoBehaviour
comp.ReceiveDamage(totalAmount, source);
// Record stats
if (source != null && target.GetComponent<EnemyCombatant>() != null)
if (source != null && targetEnemy != null)
{
var ally = source.GetComponent<AllyCombatant>();
if (ally != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamage(ally.slotIndex, totalAmount);
}
// Record damage taken
var targetAlly = target.GetComponent<AllyCombatant>();
if (targetAlly != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamageTaken(targetAlly.slotIndex, totalAmount);
yield break;
}
@@ -790,7 +951,9 @@ public class EffectSystem : MonoBehaviour
if (target == null) yield break;
// check death state inside loop
if (target.GetComponent<EnemyCombatant>() != null && AreAllEnemiesDead())
if (targetAlly != null && targetAlly.IsDead) yield break;
if (targetEnemy != null && (targetEnemy.IsDead || targetEnemy.currentHP <= 0)) yield break;
if (targetEnemy != null && AreAllEnemiesDead())
{
LogVerbose($"[EffectSystem] Stopping DOT to {target.name} because all enemies are dead.");
yield break;
@@ -798,13 +961,12 @@ public class EffectSystem : MonoBehaviour
comp.ReceiveDamage(perTick, source);
// Record stats
if (source != null && target.GetComponent<EnemyCombatant>() != null)
if (source != null && targetEnemy != null)
{
var ally = source.GetComponent<AllyCombatant>();
if (ally != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamage(ally.slotIndex, perTick);
}
// Record damage taken
var targetAlly = target.GetComponent<AllyCombatant>();
if (targetAlly != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamageTaken(targetAlly.slotIndex, perTick);
yield return new WaitForSeconds(tickInterval);
elapsed += tickInterval;
@@ -870,7 +1032,7 @@ public class EffectSystem : MonoBehaviour
{
ally.ModifyMana(Mathf.CeilToInt(totalAmount), true, true);
// Record stats
if (source != null)
if (totalAmount > 0f && source != null)
{
var sourceAlly = source.GetComponent<AllyCombatant>();
if (sourceAlly != null && teamUIController.Instance != null) teamUIController.Instance.RecordMana(sourceAlly.slotIndex, totalAmount);
@@ -889,7 +1051,7 @@ public class EffectSystem : MonoBehaviour
{
ally.ModifyMana(Mathf.CeilToInt(perTick), true, true);
// Record stats
if (source != null)
if (perTick > 0f && source != null)
{
var sourceAlly = source.GetComponent<AllyCombatant>();
if (sourceAlly != null && teamUIController.Instance != null) teamUIController.Instance.RecordMana(sourceAlly.slotIndex, perTick);
@@ -980,9 +1142,45 @@ public class EffectSystem : MonoBehaviour
private IEnumerator ApplyTemporaryAttackChangeCoroutine(EnemyCombatant enemy, int delta, float duration)
{
if (enemy == null) yield break;
enemy.attack = Mathf.Max(0, enemy.attack + delta);
enemy.ModifyAttack(delta);
yield return new WaitForSeconds(duration);
enemy.attack = Mathf.Max(0, enemy.attack - delta);
enemy.ModifyAttack(-delta);
}
private IEnumerator ApplyTemporaryMaxHPDeltaCoroutine(AllyCombatant ally, int delta, float duration)
{
if (ally == null) yield break;
ally.SetMaxHP(Mathf.Max(1, ally.maxHP + delta), false);
yield return new WaitForSeconds(duration);
if (ally == null) yield break;
ally.SetMaxHP(Mathf.Max(1, ally.maxHP - delta), false);
}
private IEnumerator ApplyTemporaryMaxHPDeltaCoroutine(EnemyCombatant enemy, int delta, float duration)
{
if (enemy == null) yield break;
enemy.SetMaxHP(Mathf.Max(1, enemy.maxHP + delta), false);
yield return new WaitForSeconds(duration);
if (enemy == null) yield break;
enemy.SetMaxHP(Mathf.Max(1, enemy.maxHP - delta), false);
}
private IEnumerator ApplyTemporaryMaxManaDeltaCoroutine(AllyCombatant ally, int delta, float duration)
{
if (ally == null) yield break;
ally.SetMaxMana(Mathf.Max(1, ally.maxMana + delta), false);
yield return new WaitForSeconds(duration);
if (ally == null) yield break;
ally.SetMaxMana(Mathf.Max(1, ally.maxMana - delta), false);
}
private IEnumerator ApplyTemporaryMaxManaDeltaCoroutine(EnemyCombatant enemy, int delta, float duration)
{
if (enemy == null) yield break;
enemy.SetMaxMana(Mathf.Max(1, enemy.maxMana + delta), false);
yield return new WaitForSeconds(duration);
if (enemy == null) yield break;
enemy.SetMaxMana(Mathf.Max(1, enemy.maxMana - delta), false);
}
#endregion
}