超大量的更新 修复很多问题,gameplay特效初步
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -41,7 +41,11 @@ public enum EffectType
|
||||
DecreaseDamageResistance,
|
||||
IncreaseAttack,
|
||||
DecreaseAttack,
|
||||
RedirectNextDamageToSelf
|
||||
RedirectNextDamageToSelf,
|
||||
// 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
|
||||
}
|
||||
|
||||
public interface ICombatant
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
// EnemyCombatant �ǵ������ϵ��˵�����ʱ���֣����е��˹���ͬһ�� GameObject����˳��dz���
|
||||
// ʵ�� ICombatant���� EffectSystem/SkillBuilder ����
|
||||
// Documentation text normalized.
|
||||
// Documentation text normalized.
|
||||
public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
{
|
||||
public EnemyData_SO sourceData;
|
||||
@@ -16,6 +16,10 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
public float damageResistance = 0f;
|
||||
public int attack = 0;
|
||||
|
||||
// Keep an unbuffed attack baseline so temporary Buff.attackMultiplier can be applied/reverted correctly.
|
||||
private int _attackBaseUnbuffed = 0;
|
||||
private bool _attackBaseInitialized = false;
|
||||
|
||||
private List<Buff> activeBuffs = new List<Buff>();
|
||||
|
||||
// Events to notify manager/UI
|
||||
@@ -30,22 +34,28 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
gameObject.name = "thisEnemy";
|
||||
}
|
||||
|
||||
// ��ʼ��/����Ϊָ���� EnemyData_SO
|
||||
// Documentation text normalized.
|
||||
public void InitializeFromSO(EnemyData_SO so)
|
||||
{
|
||||
// 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;
|
||||
|
||||
sourceData = so;
|
||||
if (so != null)
|
||||
{
|
||||
maxHP = Mathf.Max(1, so.enemy_maxHP);
|
||||
damageResistance = so.enemy_damageResistance;
|
||||
attack = so.enemy_baseAttack;
|
||||
SetAttack(so.enemy_baseAttack);
|
||||
maxMana = Mathf.Max(0, so.GetEffectiveMaxMana());
|
||||
}
|
||||
else
|
||||
{
|
||||
maxHP = 100;
|
||||
damageResistance = 0f;
|
||||
attack = 0;
|
||||
SetAttack(0);
|
||||
maxMana = 0;
|
||||
}
|
||||
|
||||
@@ -67,7 +77,7 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
TryTriggerPrimarySkill(SkillDefinition.SkillTrigger.OnEnemyRevive);
|
||||
}
|
||||
|
||||
// ���Դ��� primary skill������������ƥ��ʱ���� SkillBuilder
|
||||
// Documentation text normalized.
|
||||
private void TryTriggerPrimarySkill(SkillDefinition.SkillTrigger when)
|
||||
{
|
||||
if (sourceData == null) return;
|
||||
@@ -93,7 +103,15 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
}
|
||||
|
||||
float totalAmount = perTick;
|
||||
if (!def.IsSingleInstance)
|
||||
bool scaleByTicks =
|
||||
!def.IsSingleInstance &&
|
||||
(def.effectType == EffectType.DamageOverTimeEnemy ||
|
||||
def.effectType == EffectType.DamageOverTimeAlly ||
|
||||
def.effectType == EffectType.HealOverTimeEnemy ||
|
||||
def.effectType == EffectType.HealOverTimeSelf ||
|
||||
def.effectType == EffectType.HealGroupOverTime ||
|
||||
def.effectType == EffectType.IncreaseManaOverTime);
|
||||
if (scaleByTicks)
|
||||
{
|
||||
float tickInterval = def.GetEffectiveTickInterval();
|
||||
int ticks = Mathf.Max(1, Mathf.CeilToInt(def.defaultDuration / tickInterval));
|
||||
@@ -132,7 +150,8 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
public void ReceiveHeal(float amount, GameObject source)
|
||||
{
|
||||
if (isDead) return;
|
||||
int delta = Mathf.CeilToInt(amount);
|
||||
float mult = GetTotalHealReceivedMultiplier();
|
||||
int delta = Mathf.CeilToInt(amount * mult);
|
||||
currentHP = Mathf.Clamp(currentHP + delta, 0, maxHP);
|
||||
}
|
||||
|
||||
@@ -140,8 +159,8 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
{
|
||||
if (buff == null) return;
|
||||
activeBuffs.Add(buff);
|
||||
// Simple apply: only modify healReceivedMultiplier or attackMultiplier as example
|
||||
if (buff.attackMultiplier != 1f) attack = Mathf.RoundToInt(attack * buff.attackMultiplier);
|
||||
// Apply reversible multipliers. attackMultiplier is applied via baseline+recalc to ensure it can be reverted.
|
||||
if (buff.attackMultiplier != 1f) RecalculateAttackFromBuffs();
|
||||
}
|
||||
|
||||
public void RemoveBuff(string buffId)
|
||||
@@ -150,9 +169,62 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
if (b != null)
|
||||
{
|
||||
activeBuffs.Remove(b);
|
||||
if (b.attackMultiplier != 1f) RecalculateAttackFromBuffs();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureAttackBaseInitialized()
|
||||
{
|
||||
if (_attackBaseInitialized) return;
|
||||
_attackBaseInitialized = true;
|
||||
_attackBaseUnbuffed = Mathf.Max(0, attack);
|
||||
}
|
||||
|
||||
private float GetTotalAttackMultiplier()
|
||||
{
|
||||
float mult = 1f;
|
||||
for (int i = 0; i < activeBuffs.Count; i++)
|
||||
{
|
||||
var b = activeBuffs[i];
|
||||
if (b == null) continue;
|
||||
mult *= b.attackMultiplier;
|
||||
}
|
||||
return mult;
|
||||
}
|
||||
|
||||
private float GetTotalHealReceivedMultiplier()
|
||||
{
|
||||
float mult = 1f;
|
||||
for (int i = 0; i < activeBuffs.Count; i++)
|
||||
{
|
||||
var b = activeBuffs[i];
|
||||
if (b == null) continue;
|
||||
mult *= b.healReceivedMultiplier;
|
||||
}
|
||||
return mult;
|
||||
}
|
||||
|
||||
private void RecalculateAttackFromBuffs()
|
||||
{
|
||||
EnsureAttackBaseInitialized();
|
||||
float mult = GetTotalAttackMultiplier();
|
||||
attack = Mathf.Max(0, Mathf.RoundToInt(_attackBaseUnbuffed * mult));
|
||||
}
|
||||
|
||||
public void ModifyAttack(int delta)
|
||||
{
|
||||
EnsureAttackBaseInitialized();
|
||||
_attackBaseUnbuffed = Mathf.Max(0, _attackBaseUnbuffed + delta);
|
||||
RecalculateAttackFromBuffs();
|
||||
}
|
||||
|
||||
public void SetAttack(int value)
|
||||
{
|
||||
EnsureAttackBaseInitialized();
|
||||
_attackBaseUnbuffed = Mathf.Max(0, value);
|
||||
RecalculateAttackFromBuffs();
|
||||
}
|
||||
|
||||
public void ModifyHP(int delta)
|
||||
{
|
||||
if (isDead && delta > 0) return;
|
||||
@@ -189,6 +261,17 @@ 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.
|
||||
isDead = true;
|
||||
|
||||
// 播放 KO 特效
|
||||
if (GfxController.Instance != null)
|
||||
{
|
||||
GfxController.Instance.PlayKOFX(this.gameObject);
|
||||
}
|
||||
|
||||
// Notify SkillBuilder so allies can react to each enemy death BEFORE listeners that may reuse this Enemy instance
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
@@ -262,9 +345,6 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
Debug.LogError($"[EnemyCombatant] Error while checking all-enemies-defeated: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
// Finally mark dead so further damage/heal is ignored
|
||||
isDead = true;
|
||||
}
|
||||
|
||||
// Utility: expose a read-only status
|
||||
|
||||
@@ -8,6 +8,16 @@ public class ScoreManager : MonoBehaviour
|
||||
|
||||
public int totalScore = 0;
|
||||
|
||||
// Gameplay UI: per-track chart progress keys under bgSPRITE (Scale.y: 0%->0, 100%->2).
|
||||
private readonly Transform[] _idolscoreKeys = new Transform[5];
|
||||
private readonly Vector3[] _idolscoreKeyBaseScales = new Vector3[5];
|
||||
private readonly bool[] _idolscoreKeyBaseScaleCached = new bool[5];
|
||||
private readonly float[] _idolscoreDisplayY = new float[5];
|
||||
private readonly float[] _idolscoreTargetY = new float[5];
|
||||
private readonly float[] _idolscoreLastTargetY = new float[5];
|
||||
private readonly float[] _idolscorePulseAmplitude = new float[5];
|
||||
private readonly float[] _idolscorePulseSeed = new float[5];
|
||||
|
||||
// per-track pm score sums (red, green, yellow, purple, blue)
|
||||
public int red_pmScore_sum = 0;
|
||||
public int green_pmScore_sum = 0;
|
||||
@@ -42,6 +52,10 @@ public class ScoreManager : MonoBehaviour
|
||||
private readonly TextMeshProUGUI[] slotTmpFallback = new TextMeshProUGUI[5];
|
||||
private readonly Text[] slotLegacyFallback = new Text[5];
|
||||
private readonly bool[] slotFallbackResolved = new bool[5];
|
||||
private JudgeManager hookedJudgeManager;
|
||||
private bool perfectBonusHooked = false;
|
||||
private int perfectClearBonusPm = 0;
|
||||
private bool perfectClearBonusApplied = false;
|
||||
|
||||
[Header("Judgement Statistics")]
|
||||
public int countPerfect = 0;
|
||||
@@ -64,6 +78,8 @@ public class ScoreManager : MonoBehaviour
|
||||
public void ResetStatistics()
|
||||
{
|
||||
countPerfect = countGreat = countGood = countMiss = 0;
|
||||
perfectClearBonusPm = 0;
|
||||
perfectClearBonusApplied = false;
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
@@ -100,10 +116,212 @@ public class ScoreManager : MonoBehaviour
|
||||
|
||||
for (int i = 0; i < pmScoreSums.Length; i++) pmScoreSums[i] = 0;
|
||||
for (int i = 0; i < idolScoreSums.Length; i++) idolScoreSums[i] = 0;
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
_idolscoreDisplayY[i] = 0f;
|
||||
_idolscoreTargetY[i] = 0f;
|
||||
_idolscoreLastTargetY[i] = 0f;
|
||||
_idolscorePulseAmplitude[i] = 0f;
|
||||
_idolscorePulseSeed[i] = UnityEngine.Random.Range(0.01f, 999f);
|
||||
}
|
||||
|
||||
ResetStatistics();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
TryHookPerfectBonusEvent();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
UnhookPerfectBonusEvent();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
UnhookPerfectBonusEvent();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Ensure the progress keys start at 0.
|
||||
UpdateIdolscoreKeyScales();
|
||||
TryHookPerfectBonusEvent();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
EnsureIdolscoreKeys();
|
||||
|
||||
float dt = Time.unscaledDeltaTime;
|
||||
if (dt <= 0f) return;
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
Transform t = _idolscoreKeys[i];
|
||||
if (t == null) continue;
|
||||
|
||||
Vector3 baseScale = _idolscoreKeyBaseScaleCached[i] ? _idolscoreKeyBaseScales[i] : t.localScale;
|
||||
float targetY = Mathf.Max(0f, _idolscoreTargetY[i]);
|
||||
|
||||
// Fast rise, slow settle.
|
||||
float smooth = 1f - Mathf.Exp(-7.5f * dt);
|
||||
_idolscoreDisplayY[i] = Mathf.Lerp(_idolscoreDisplayY[i], targetY, smooth);
|
||||
|
||||
// Random growth wobble when target updates.
|
||||
_idolscorePulseAmplitude[i] = Mathf.Lerp(_idolscorePulseAmplitude[i], 0f, 4.8f * dt);
|
||||
float noise = (Mathf.PerlinNoise(_idolscorePulseSeed[i], Time.unscaledTime * 7f) - 0.5f) * 2f;
|
||||
float y = Mathf.Max(0f, _idolscoreDisplayY[i] + noise * _idolscorePulseAmplitude[i]);
|
||||
|
||||
t.localScale = new Vector3(baseScale.x, y, baseScale.z);
|
||||
}
|
||||
}
|
||||
|
||||
private void TryHookPerfectBonusEvent()
|
||||
{
|
||||
if (!Application.isPlaying) return;
|
||||
|
||||
if (perfectBonusHooked)
|
||||
{
|
||||
if (hookedJudgeManager != null) return;
|
||||
// previous JudgeManager got destroyed across scene switch
|
||||
perfectBonusHooked = false;
|
||||
}
|
||||
|
||||
var jm = JudgeManager.Instance ?? UnityEngine.Object.FindAnyObjectByType<JudgeManager>();
|
||||
if (jm == null) return;
|
||||
|
||||
jm.AllNotesJudged -= OnAllNotesJudgedForPerfectBonus;
|
||||
jm.AllNotesJudged += OnAllNotesJudgedForPerfectBonus;
|
||||
hookedJudgeManager = jm;
|
||||
perfectBonusHooked = true;
|
||||
}
|
||||
|
||||
private void UnhookPerfectBonusEvent()
|
||||
{
|
||||
if (!perfectBonusHooked) return;
|
||||
if (hookedJudgeManager != null)
|
||||
{
|
||||
hookedJudgeManager.AllNotesJudged -= OnAllNotesJudgedForPerfectBonus;
|
||||
}
|
||||
hookedJudgeManager = null;
|
||||
perfectBonusHooked = false;
|
||||
}
|
||||
|
||||
private int GetCurrentChartLogicalNoteCount()
|
||||
{
|
||||
var bmm = UnityEngine.Object.FindAnyObjectByType<BeatmapManager>();
|
||||
if (bmm != null)
|
||||
{
|
||||
if (bmm.beatmap != null && bmm.beatmap.notes != null && bmm.beatmap.notes.Length > 0)
|
||||
return bmm.beatmap.notes.Length;
|
||||
if (bmm.parsedNoteAmount > 0)
|
||||
return bmm.parsedNoteAmount;
|
||||
}
|
||||
|
||||
// Fallback to runtime judged count if beatmap reference is unavailable.
|
||||
return countPerfect + countGreat + countGood + countMiss;
|
||||
}
|
||||
|
||||
private int GetCurrentChartLeftoverScore()
|
||||
{
|
||||
var bmm = UnityEngine.Object.FindAnyObjectByType<BeatmapManager>();
|
||||
if (bmm == null) return 0;
|
||||
return Mathf.Max(0, bmm.leftoverScore);
|
||||
}
|
||||
|
||||
private void OnAllNotesJudgedForPerfectBonus()
|
||||
{
|
||||
if (perfectClearBonusApplied) return;
|
||||
|
||||
int noteCount = Mathf.Max(0, GetCurrentChartLogicalNoteCount());
|
||||
bool allPerfect = noteCount > 0 &&
|
||||
countGreat == 0 &&
|
||||
countGood == 0 &&
|
||||
countMiss == 0 &&
|
||||
countPerfect >= noteCount;
|
||||
|
||||
if (allPerfect)
|
||||
{
|
||||
perfectClearBonusPm = GetCurrentChartLeftoverScore();
|
||||
}
|
||||
else
|
||||
{
|
||||
perfectClearBonusPm = 0;
|
||||
}
|
||||
perfectClearBonusApplied = true;
|
||||
|
||||
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)
|
||||
{
|
||||
if (ui.allSum_pmScore != null) ui.allSum_pmScore.text = allSum_pmScore.ToString();
|
||||
if (ui.allSum_idolScore != null) ui.allSum_idolScore.text = allSum_idolScore.ToString();
|
||||
}
|
||||
|
||||
RecalculateTotal();
|
||||
}
|
||||
|
||||
private void EnsureIdolscoreKeys()
|
||||
{
|
||||
// Try to resolve missing refs (GameObject.Find doesn't return inactive objects).
|
||||
if (_idolscoreKeys[0] == null) _idolscoreKeys[0] = GameObject.Find("idolscoreKey_red")?.transform;
|
||||
if (_idolscoreKeys[1] == null) _idolscoreKeys[1] = GameObject.Find("idolscoreKey_green")?.transform;
|
||||
if (_idolscoreKeys[2] == null) _idolscoreKeys[2] = GameObject.Find("idolscoreKey_yellow")?.transform;
|
||||
if (_idolscoreKeys[3] == null) _idolscoreKeys[3] = GameObject.Find("idolscoreKey_purple")?.transform;
|
||||
if (_idolscoreKeys[4] == null) _idolscoreKeys[4] = GameObject.Find("idolscoreKey_blue")?.transform;
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var t = _idolscoreKeys[i];
|
||||
if (t == null) continue;
|
||||
if (_idolscoreKeyBaseScaleCached[i]) continue;
|
||||
_idolscoreKeyBaseScales[i] = t.localScale;
|
||||
_idolscoreKeyBaseScaleCached[i] = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateIdolscoreKeyScales()
|
||||
{
|
||||
EnsureIdolscoreKeys();
|
||||
EnsureAllyCache();
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var t = _idolscoreKeys[i];
|
||||
if (t == null) continue;
|
||||
|
||||
int maxScore = 0;
|
||||
var ally = allyCombatants[i];
|
||||
if (ally != null) maxScore = Mathf.Max(0, ally.maxTrackScore);
|
||||
|
||||
int curScore = pmScoreSums[i];
|
||||
float ratio = maxScore > 0 ? (float)curScore / maxScore : 0f;
|
||||
ratio = Mathf.Clamp01(ratio);
|
||||
|
||||
float targetY = ratio * 2f;
|
||||
_idolscoreTargetY[i] = targetY;
|
||||
|
||||
if (Mathf.Abs(_idolscoreLastTargetY[i] - targetY) > 0.0001f)
|
||||
{
|
||||
_idolscoreLastTargetY[i] = targetY;
|
||||
_idolscorePulseAmplitude[i] = UnityEngine.Random.Range(0.08f, 0.24f);
|
||||
}
|
||||
|
||||
if (targetY <= 0.001f)
|
||||
_idolscoreDisplayY[i] = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureAllyCache()
|
||||
{
|
||||
int frame = Time.frameCount;
|
||||
@@ -147,6 +365,7 @@ public class ScoreManager : MonoBehaviour
|
||||
/// </summary>
|
||||
public void AddPmScoreForTrack(int trackIndex, int pmDelta, float scoreEfficiency)
|
||||
{
|
||||
TryHookPerfectBonusEvent();
|
||||
if (trackIndex < 0 || trackIndex >= pmScoreSums.Length) return;
|
||||
if (pmDelta == 0) return;
|
||||
pmScoreSums[trackIndex] += pmDelta;
|
||||
@@ -178,7 +397,8 @@ public class ScoreManager : MonoBehaviour
|
||||
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];
|
||||
allSum_pmScore = (int)Mathf.Min((float)aggPm, (float)int.MaxValue - 1f);
|
||||
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);
|
||||
|
||||
if (JudgeManager.IsDebugEnabled)
|
||||
@@ -215,6 +435,9 @@ public class ScoreManager : MonoBehaviour
|
||||
|
||||
// Ensure total is recalculated when pm/idol aggregates change so totalScore reflects pm + idol
|
||||
RecalculateTotal();
|
||||
|
||||
// Update per-track progress keys (bgSPRITE/idolscoreKey_*).
|
||||
UpdateIdolscoreKeyScales();
|
||||
}
|
||||
|
||||
public void RecalculateTotal()
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
using System.Collections;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// SkillBuilder: ���ж���ɿ��ٵ��õļ��ܺ�����?
|
||||
/// - ÿ������Ϊһ�������� public ���������ܱ�Ҫ������ʩ���� source, Ŀ�� specificTarget �ȣ�
|
||||
/// - �ڲ����� EffectSystem.Instance.ApplyEffect(...)��ͳһʹ�� Selector �� EffectType
|
||||
/// - �ṩ���ò�����amount, duration, tickInterval�����Բ�������У��
|
||||
/// Documentation text normalized.
|
||||
/// Documentation text normalized.
|
||||
/// Documentation text normalized.
|
||||
/// Documentation text normalized.
|
||||
///
|
||||
/// �÷�ʾ��:
|
||||
/// Documentation text normalized.
|
||||
/// SkillBuilder.Instance.ExecuteSkill("Fireball", EffectType.DamageSingleEnemy, 120f, Selector.CurrentEnemies, caster, target);
|
||||
/// SkillBuilder.Instance.ApplyScoreMultiplier(caster, Selector.AllAllies, 1.5f, 5f); // 5 ���ڶ���÷ֳ�?1.5
|
||||
/// Documentation text normalized.
|
||||
/// </summary>
|
||||
public class SkillBuilder : MonoBehaviour
|
||||
{
|
||||
public static SkillBuilder Instance { get; private set; }
|
||||
|
||||
// Dedup UI feed so one small-skill (group) that uses multiple SkillDefinitions only prints once per frame.
|
||||
private static readonly Dictionary<long, int> s_lastSkillFeedFrameByKey = new Dictionary<long, int>(128);
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null) Instance = this;
|
||||
@@ -38,9 +41,13 @@ public class SkillBuilder : MonoBehaviour
|
||||
{
|
||||
Debug.LogWarning($"[SkillBuilder] PrewarmAllyHeroSOIndex failed: {ex}");
|
||||
}
|
||||
|
||||
// Reset in-game skill trigger feed each play session.
|
||||
try { SkillTriggerFeedUI.Clear(); } catch { }
|
||||
try { s_lastSkillFeedFrameByKey.Clear(); } catch { }
|
||||
}
|
||||
|
||||
// ----------------------------- ��������� -----------------------------
|
||||
// Documentation text normalized.
|
||||
public float defaultDamage = 100f;
|
||||
public float defaultHeal = 80f;
|
||||
public float defaultDuration = 5f;
|
||||
@@ -67,9 +74,46 @@ public class SkillBuilder : MonoBehaviour
|
||||
// Reusable buffers to reduce allocations during gameplay
|
||||
private readonly Dictionary<string, float> _varsBuffer = new Dictionary<string, float>(16);
|
||||
private readonly List<string> _tmpNoteIdRemoval = new List<string>(16);
|
||||
private readonly Dictionary<string, float> _lastSkillTriggerTime = new Dictionary<string, float>(256);
|
||||
private readonly Dictionary<string, Coroutine> _refreshOnlyTimedEffectCoroutines = new Dictionary<string, Coroutine>(32);
|
||||
private readonly Dictionary<string, RefreshOnlyTimedState> _refreshOnlyTimedEffectStates = new Dictionary<string, RefreshOnlyTimedState>(32);
|
||||
private readonly Dictionary<string, Coroutine> _refreshOnlyOverTimeCoroutines = new Dictionary<string, Coroutine>(16);
|
||||
private int _cachedAlliesFrame = -1;
|
||||
private readonly List<GameObject> _cachedAllies = new List<GameObject>(8);
|
||||
|
||||
private static readonly HashSet<string> s_refreshOnlyTimedSkillIds = new HashSet<string>
|
||||
{
|
||||
// Lock
|
||||
"44305_2", // Lock_TurnTables (attack up, timed)
|
||||
"44306_2", // Lock_Mine (score efficiency up, timed)
|
||||
"44308", // Lock_Accomplice (damage resistance up, timed)
|
||||
|
||||
// MocaiLi
|
||||
"44202", // HoldLine (damage resistance up, timed)
|
||||
"44203", // LockOn (enemy damage resistance down, timed)
|
||||
"44204", // Heartbeat (enemy attack down, timed)
|
||||
"44208", // BloodInspire (score efficiency up, timed)
|
||||
|
||||
// Winnie
|
||||
"44403", // Inspiration (score efficiency up, timed)
|
||||
"44404", // EasyTask (score efficiency up, timed)
|
||||
"44408", // ContributeCreate (max HP up, timed)
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> s_refreshOnlyOverTimeSkillIds = new HashSet<string>
|
||||
{
|
||||
// MocaiLi LastHope pair
|
||||
"44210_1", // Heal over time
|
||||
"44210_2", // Mana over time (can be negative)
|
||||
};
|
||||
|
||||
private sealed class RefreshOnlyTimedState
|
||||
{
|
||||
public EffectType effectType;
|
||||
public float floatDelta;
|
||||
public int intDelta;
|
||||
}
|
||||
|
||||
private void PrewarmAllyHeroSOIndex()
|
||||
{
|
||||
if (_allAllyHeroSOs != null && _allAllyHeroSOs.Length > 0) return;
|
||||
@@ -116,6 +160,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
|
||||
var so = GetAllyHeroSOBySlot(trackIndex);
|
||||
// try to obtain per-level params from SO if present
|
||||
AllyHero_SO.AllyLevelInfo levelInfo = so != null ? (so.GetEffectiveLevelForCurrentEXP()) : null;
|
||||
@@ -153,18 +198,43 @@ public class SkillBuilder : MonoBehaviour
|
||||
{
|
||||
if (ally != null)
|
||||
{
|
||||
// 如果所有敌人都已被消灭,Miss 不再扣血
|
||||
if (global::EffectSystem.Instance != null && global::EffectSystem.Instance.AreAllEnemiesDead())
|
||||
{
|
||||
LogVerbose($"[SkillBuilder] All enemies dead -> Skip miss HP loss for slot {trackIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
LogVerbose($"[SkillBuilder] Applied miss HP loss {loss} to slot {trackIndex} (damageResistance={ally.damageResistance})");
|
||||
|
||||
// 播放敌人攻击特效,并将伤害逻辑延迟到特效到达时执行
|
||||
bool effectStarted = false;
|
||||
if (GfxController.Instance != null)
|
||||
{
|
||||
effectStarted = GfxController.Instance.PlayEnemyAttackOnMiss(trackIndex, missBase, ally);
|
||||
}
|
||||
|
||||
if (!effectStarted)
|
||||
{
|
||||
// 如果特效启动失败或 GfxController 不存在,则立即扣血作为保底,并触发闪红
|
||||
ally.ReceiveDamage(missBase, null);
|
||||
if (teamUIController.Instance != null)
|
||||
{
|
||||
teamUIController.Instance.TriggerAllyHurtFlash(trackIndex);
|
||||
}
|
||||
LogVerbose($"[SkillBuilder] Gfx failed or missing. Applied immediate miss HP loss for slot {trackIndex}");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogVerbose($"[SkillBuilder] Miss occurred for slot {trackIndex}. Damage {missBase} queued via GfxController.");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (damageMult > 0f)
|
||||
{
|
||||
int baseAtk = GetAllyBaseAttack(so);
|
||||
int baseAtk = ally != null ? ally.attack : 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);
|
||||
@@ -172,8 +242,8 @@ public class SkillBuilder : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------- ͨ�ü���ִ�нӿ� -----------------------------
|
||||
// ͨ��ִ�У�������, Ч������, ��ֵ, Ŀ��ѡ����, ʩ����, ��ѡ����Ŀ��, ����ʱ���?tick
|
||||
// Documentation text normalized.
|
||||
// Documentation text normalized.
|
||||
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)
|
||||
@@ -246,8 +316,8 @@ public class SkillBuilder : MonoBehaviour
|
||||
|
||||
return _cachedAllies;
|
||||
}
|
||||
// ----------------------------- Ŀ������������ű�ʹ�ã���?EffectSystem �Ľ���һ�£� -----------------------------
|
||||
// ���ط��� selector �� GameObject �б������㼼�ܽű����в���������ֱ�Ӹ� ICombatant.Buff��
|
||||
// Documentation text normalized.
|
||||
// Documentation text normalized.
|
||||
public List<GameObject> ResolveTargetsLocal(Selector selector, GameObject source = null, GameObject specificTarget = null)
|
||||
{
|
||||
List<GameObject> list = new List<GameObject>();
|
||||
@@ -342,8 +412,8 @@ public class SkillBuilder : MonoBehaviour
|
||||
return uniq;
|
||||
}
|
||||
|
||||
// ----------------------------- ����Ӱ�� API -----------------------------
|
||||
// ֱ����ȫ���ܷ֣�������Ч��
|
||||
// Documentation text normalized.
|
||||
// Documentation text normalized.
|
||||
public void ModifyTotalScore(int delta)
|
||||
{
|
||||
if (ScoreManager.Instance == null) { Debug.LogWarning("ModifyTotalScore: ScoreManager.Instance is null"); return; }
|
||||
@@ -357,7 +427,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// �ĵ����Ѿ��ķ�������ʱ��
|
||||
// Documentation text normalized.
|
||||
public void ModifySingleAllyScoreDirect(GameObject allyObject, int delta)
|
||||
{
|
||||
if (allyObject == null) return;
|
||||
@@ -377,7 +447,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
if (ally != null) ally.AddScoreDirect(delta);
|
||||
}
|
||||
|
||||
// ��һ��Ŀ��Ӧ��һ���Եķ�������/�ͷ���������Ӧ�ã�
|
||||
// Documentation text normalized.
|
||||
public void ModifyGroupScoreDirect(Selector selector, GameObject caster, int delta)
|
||||
{
|
||||
var targets = ResolveTargetsLocal(selector, caster);
|
||||
@@ -388,7 +458,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// ��ʱ��ߣ��ͣ�Ŀ��ķ������ʣ�ʹ�� EffectSystem.ScoreMultiplier���Ƽ���
|
||||
// Documentation text normalized.
|
||||
public void ApplyScoreMultiplier(Selector selector, GameObject caster, float multiplier, float duration)
|
||||
{
|
||||
if (multiplier <= 0f) { Debug.LogWarning("ApplyScoreMultiplier: invalid multiplier"); return; }
|
||||
@@ -435,7 +505,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------- ���б�ݼ���ʾ��?-----------------------------
|
||||
// Documentation text normalized.
|
||||
public void DealSingleEnemyDamage(GameObject caster, GameObject enemyTarget, float amount)
|
||||
{
|
||||
if (enemyTarget == null) { Debug.LogWarning("DealSingleEnemyDamage: enemyTarget == null"); return; }
|
||||
@@ -510,7 +580,9 @@ public class SkillBuilder : MonoBehaviour
|
||||
|
||||
public void ReduceEnemyHealOverTime(GameObject caster, Selector selector, float duration, GameObject specificTarget = null)
|
||||
{
|
||||
ExecuteSkill("ReduceEnemyHealOverTime", EffectType.ReduceEnemyHealOverTime, 0f, selector, caster, specificTarget, duration);
|
||||
// Default to 50% heal reduction for the duration. Designers can use SkillDefinition.effectType directly
|
||||
// with a custom formula/amount if they need other ratios.
|
||||
ExecuteSkill("ReduceEnemyHealOverTime", EffectType.ReduceEnemyHealOverTime, 0.5f, selector, caster, specificTarget, duration);
|
||||
}
|
||||
|
||||
public void HealAdjacentAllies(GameObject caster, float amount)
|
||||
@@ -534,8 +606,8 @@ public class SkillBuilder : MonoBehaviour
|
||||
HealGroupOverTime(caster, defaultHeal * 5f, 4f, 1f, true);
|
||||
}
|
||||
|
||||
// ----------------------------- ���� SO ����ֵ���� helpers -----------------------------
|
||||
// ��ȡij����λ�� ally GameObject��0-based slot index��
|
||||
// Documentation text normalized.
|
||||
// Documentation text normalized.
|
||||
public GameObject GetAllyObjectBySlot(int slotIndex)
|
||||
{
|
||||
var ui = teamUIController.Instance;
|
||||
@@ -544,17 +616,24 @@ public class SkillBuilder : MonoBehaviour
|
||||
return named;
|
||||
}
|
||||
|
||||
// ���ݲ�λ���� AllyHero_SO������ʱ�� Resources �в��� ally_heroID��
|
||||
// Documentation text normalized.
|
||||
public AllyHero_SO GetAllyHeroSOBySlot(int slotIndex)
|
||||
{
|
||||
// NOTE: slot index -> hero ID mapping can change between runs/scenes (PlayerPrefs load timing),
|
||||
// so the cache must be validated against the current allySlotIds before returning.
|
||||
var ui = teamUIController.Instance;
|
||||
if (ui == null || ui.allySlotIds == null) return null;
|
||||
if (slotIndex < 0 || slotIndex >= ui.allySlotIds.Count) return null;
|
||||
int id = ui.allySlotIds[slotIndex];
|
||||
if (id <= 0) return null;
|
||||
|
||||
// slot-based cache (depends on team selection)
|
||||
if (_allyHeroSoBySlotCache != null && _allyHeroSoBySlotCache.TryGetValue(slotIndex, out var cached) && cached != null)
|
||||
return cached;
|
||||
|
||||
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;
|
||||
{
|
||||
if (cached.ally_heroID == id) return cached;
|
||||
// stale entry -> remove so we can rebuild below
|
||||
_allyHeroSoBySlotCache.Remove(slotIndex);
|
||||
}
|
||||
|
||||
// Ensure index is built
|
||||
if (_allAllyHeroSOs == null || _allAllyHeroSOs.Length == 0) PrewarmAllyHeroSOIndex();
|
||||
@@ -582,7 +661,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
return result;
|
||||
}
|
||||
|
||||
// ���ݵ�ǰ����ѡ����Ч�ĵȼ���Ϣ������ null ��ʾδ�ҵ���
|
||||
// Documentation text normalized.
|
||||
private AllyHero_SO.AllyLevelInfo GetEffectiveLevelInfo(AllyHero_SO so)
|
||||
{
|
||||
if (so == null || so.levelStats == null || so.levelStats.Count == 0) return null;
|
||||
@@ -605,7 +684,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
return best;
|
||||
}
|
||||
|
||||
// ȡ��Ӣ�ۻ�����������ʹ�õ�ǰ����ƥ��ĵȼ���?attack������û���� 0
|
||||
// Documentation text normalized.
|
||||
public int GetAllyBaseAttack(AllyHero_SO so)
|
||||
{
|
||||
if (so == null) return 0;
|
||||
@@ -615,19 +694,20 @@ public class SkillBuilder : MonoBehaviour
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ͳһ���㼼�������ֵ����?inputValue == -1 ʱʹ�� skillStatic + allyAttack��
|
||||
// ����ֱ��ʹ�� inputValue���������?EffectType ����Ҫת����
|
||||
// Documentation text normalized.
|
||||
// Documentation text normalized.
|
||||
public float ComputeSkillValue(GameObject caster, int slotIndex, float inputValue, int skillStatic)
|
||||
{
|
||||
if (inputValue != -1f) return inputValue;
|
||||
var so = GetAllyHeroSOBySlot(slotIndex);
|
||||
int allyAtk = GetAllyBaseAttack(so);
|
||||
var ally = caster != null ? caster.GetComponent<AllyCombatant>() : null;
|
||||
int allyAtk = ally != null ? ally.attack : GetAllyBaseAttack(so);
|
||||
return skillStatic + allyAtk;
|
||||
}
|
||||
|
||||
// ----------------------------- ��λ����ͨ�ýӿ� -----------------------------
|
||||
// ͨ�������ڱ���λ�Ľ�ɫ�ͷ�ʱ���ô˺�����
|
||||
// slotIndex: 0-based ��λ������skillId: �Զ��弼�ܱ�ʶ��effectType,value,selector,duration�Ȳ���ͬǰ��Լ��
|
||||
// Documentation text normalized.
|
||||
// Documentation text normalized.
|
||||
// Documentation text normalized.
|
||||
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);
|
||||
@@ -637,26 +717,347 @@ public class SkillBuilder : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
// determine skill-specific static values (ʾ����skill01 �Ĺ̶��˺�Ϊ 50)
|
||||
// Documentation text normalized.
|
||||
int skillStaticDamage = 0;
|
||||
switch (skillId)
|
||||
{
|
||||
case "skill01": skillStaticDamage = 50; break;
|
||||
case "skill_heal_small": skillStaticDamage = 30; break;
|
||||
// �ڴ����Ӹ��༼�ܾ�ֵ̬
|
||||
// Documentation text normalized.
|
||||
default: skillStaticDamage = 0; break;
|
||||
}
|
||||
|
||||
float finalValue = ComputeSkillValue(caster, slotIndex, value, skillStaticDamage);
|
||||
|
||||
// ��������� effectType �ǵ����˺�������ϣ�������е���ʹ�� CurrentEnemies��selector �������ɵ��÷�����
|
||||
// Documentation text normalized.
|
||||
ExecuteSkill($"slot{slotIndex + 1}_{skillId}", effectType, finalValue, selector, caster, specificTarget, duration, tickInterval);
|
||||
}
|
||||
|
||||
private static bool IsRefreshOnlyTimedSkill(SkillDefinition def)
|
||||
{
|
||||
if (def == null || string.IsNullOrWhiteSpace(def.skillId)) return false;
|
||||
return s_refreshOnlyTimedSkillIds.Contains(def.skillId);
|
||||
}
|
||||
|
||||
private static bool IsRefreshOnlyOverTimeSkill(SkillDefinition def)
|
||||
{
|
||||
if (def == null || string.IsNullOrWhiteSpace(def.skillId)) return false;
|
||||
return s_refreshOnlyOverTimeSkillIds.Contains(def.skillId);
|
||||
}
|
||||
|
||||
private static string BuildRefreshOnlyEffectKey(SkillDefinition def, GameObject target)
|
||||
{
|
||||
if (def == null || target == null || string.IsNullOrWhiteSpace(def.skillId)) return null;
|
||||
return def.skillId + ":" + target.GetInstanceID();
|
||||
}
|
||||
|
||||
private bool TryApplyRefreshOnlyTimedEffect(SkillDefinition def, GameObject target, float amount, float duration)
|
||||
{
|
||||
if (!IsRefreshOnlyTimedSkill(def)) return false;
|
||||
if (target == null || duration <= 0f) return false;
|
||||
|
||||
string key = BuildRefreshOnlyEffectKey(def, target);
|
||||
if (string.IsNullOrEmpty(key)) return false;
|
||||
|
||||
if (_refreshOnlyTimedEffectStates.TryGetValue(key, out var existing))
|
||||
{
|
||||
RevertRefreshOnlyTimedEffect(target, existing);
|
||||
if (_refreshOnlyTimedEffectCoroutines.TryGetValue(key, out var running) && running != null)
|
||||
StopCoroutine(running);
|
||||
}
|
||||
|
||||
if (!TryApplyRefreshOnlyTimedEffectNow(target, def.effectType, amount, out var applied))
|
||||
{
|
||||
_refreshOnlyTimedEffectStates.Remove(key);
|
||||
_refreshOnlyTimedEffectCoroutines.Remove(key);
|
||||
return false;
|
||||
}
|
||||
|
||||
_refreshOnlyTimedEffectStates[key] = applied;
|
||||
_refreshOnlyTimedEffectCoroutines[key] = StartCoroutine(RemoveRefreshOnlyTimedEffectAfterDuration(key, target, duration));
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEnumerator RemoveRefreshOnlyTimedEffectAfterDuration(string key, GameObject target, float duration)
|
||||
{
|
||||
yield return new WaitForSeconds(duration);
|
||||
|
||||
if (_refreshOnlyTimedEffectStates.TryGetValue(key, out var state))
|
||||
{
|
||||
RevertRefreshOnlyTimedEffect(target, state);
|
||||
_refreshOnlyTimedEffectStates.Remove(key);
|
||||
}
|
||||
|
||||
_refreshOnlyTimedEffectCoroutines.Remove(key);
|
||||
}
|
||||
|
||||
private bool TryApplyRefreshOnlyTimedEffectNow(GameObject target, EffectType effectType, float amount, out RefreshOnlyTimedState state)
|
||||
{
|
||||
state = null;
|
||||
var ally = target != null ? target.GetComponent<AllyCombatant>() : null;
|
||||
var enemy = target != null ? target.GetComponent<EnemyCombatant>() : null;
|
||||
if (ally == null && enemy == null) return false;
|
||||
|
||||
switch (effectType)
|
||||
{
|
||||
case EffectType.IncreaseScoreEfficiency:
|
||||
case EffectType.DecreaseScoreEfficiency:
|
||||
if (ally == null) return false;
|
||||
float scoreDelta = effectType == EffectType.IncreaseScoreEfficiency ? amount : -amount;
|
||||
ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency + scoreDelta);
|
||||
state = new RefreshOnlyTimedState { effectType = effectType, floatDelta = scoreDelta };
|
||||
return true;
|
||||
|
||||
case EffectType.IncreaseDamageResistance:
|
||||
case EffectType.DecreaseDamageResistance:
|
||||
float resistDelta = effectType == EffectType.IncreaseDamageResistance ? amount : -amount;
|
||||
if (ally != null) ally.damageResistance = Mathf.Clamp01(ally.damageResistance + resistDelta);
|
||||
else enemy.damageResistance = Mathf.Clamp01(enemy.damageResistance + resistDelta);
|
||||
state = new RefreshOnlyTimedState { effectType = effectType, floatDelta = resistDelta };
|
||||
return true;
|
||||
|
||||
case EffectType.IncreaseAttack:
|
||||
case EffectType.DecreaseAttack:
|
||||
int atkDelta = Mathf.CeilToInt(Mathf.Abs(amount));
|
||||
if (effectType == EffectType.DecreaseAttack) atkDelta = -atkDelta;
|
||||
if (ally != null) ally.ModifyAttack(atkDelta);
|
||||
else enemy.ModifyAttack(atkDelta);
|
||||
state = new RefreshOnlyTimedState { effectType = effectType, intDelta = atkDelta };
|
||||
return true;
|
||||
|
||||
case EffectType.IncreaseMaxHP:
|
||||
case EffectType.DecreaseMaxHP:
|
||||
int hpDelta = Mathf.CeilToInt(Mathf.Abs(amount));
|
||||
if (effectType == EffectType.DecreaseMaxHP) hpDelta = -hpDelta;
|
||||
if (ally != null) ally.SetMaxHP(Mathf.Max(1, ally.maxHP + hpDelta), false);
|
||||
else enemy.SetMaxHP(Mathf.Max(1, enemy.maxHP + hpDelta), false);
|
||||
state = new RefreshOnlyTimedState { effectType = effectType, intDelta = hpDelta };
|
||||
return true;
|
||||
|
||||
case EffectType.IncreaseMaxMana:
|
||||
case EffectType.DecreaseMaxMana:
|
||||
int manaDelta = Mathf.CeilToInt(Mathf.Abs(amount));
|
||||
if (effectType == EffectType.DecreaseMaxMana) manaDelta = -manaDelta;
|
||||
if (ally != null) ally.SetMaxMana(Mathf.Max(1, ally.maxMana + manaDelta), false);
|
||||
else enemy.SetMaxMana(Mathf.Max(1, enemy.maxMana + manaDelta), false);
|
||||
state = new RefreshOnlyTimedState { effectType = effectType, intDelta = manaDelta };
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RevertRefreshOnlyTimedEffect(GameObject target, RefreshOnlyTimedState state)
|
||||
{
|
||||
if (target == null || state == null) return;
|
||||
var ally = target.GetComponent<AllyCombatant>();
|
||||
var enemy = target.GetComponent<EnemyCombatant>();
|
||||
if (ally == null && enemy == null) return;
|
||||
|
||||
switch (state.effectType)
|
||||
{
|
||||
case EffectType.IncreaseScoreEfficiency:
|
||||
case EffectType.DecreaseScoreEfficiency:
|
||||
if (ally != null)
|
||||
ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency - state.floatDelta);
|
||||
break;
|
||||
|
||||
case EffectType.IncreaseDamageResistance:
|
||||
case EffectType.DecreaseDamageResistance:
|
||||
if (ally != null) ally.damageResistance = Mathf.Clamp01(ally.damageResistance - state.floatDelta);
|
||||
else enemy.damageResistance = Mathf.Clamp01(enemy.damageResistance - state.floatDelta);
|
||||
break;
|
||||
|
||||
case EffectType.IncreaseAttack:
|
||||
case EffectType.DecreaseAttack:
|
||||
if (ally != null) ally.ModifyAttack(-state.intDelta);
|
||||
else enemy.ModifyAttack(-state.intDelta);
|
||||
break;
|
||||
|
||||
case EffectType.IncreaseMaxHP:
|
||||
case EffectType.DecreaseMaxHP:
|
||||
if (ally != null) ally.SetMaxHP(Mathf.Max(1, ally.maxHP - state.intDelta), false);
|
||||
else enemy.SetMaxHP(Mathf.Max(1, enemy.maxHP - state.intDelta), false);
|
||||
break;
|
||||
|
||||
case EffectType.IncreaseMaxMana:
|
||||
case EffectType.DecreaseMaxMana:
|
||||
if (ally != null) ally.SetMaxMana(Mathf.Max(1, ally.maxMana - state.intDelta), false);
|
||||
else enemy.SetMaxMana(Mathf.Max(1, enemy.maxMana - state.intDelta), false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryApplyRefreshOnlyOverTimeEffect(SkillDefinition def, GameObject target, float totalAmount, GameObject source)
|
||||
{
|
||||
if (!IsRefreshOnlyOverTimeSkill(def)) return false;
|
||||
if (target == null || def.defaultDuration <= 0f) return false;
|
||||
if (def.effectType != EffectType.HealGroupOverTime && def.effectType != EffectType.IncreaseManaOverTime) return false;
|
||||
|
||||
string key = BuildRefreshOnlyEffectKey(def, target);
|
||||
if (string.IsNullOrEmpty(key)) return false;
|
||||
|
||||
if (_refreshOnlyOverTimeCoroutines.TryGetValue(key, out var running) && running != null)
|
||||
StopCoroutine(running);
|
||||
|
||||
_refreshOnlyOverTimeCoroutines[key] = StartCoroutine(ApplyRefreshOnlyOverTimeEffectCoroutine(key, def, target, totalAmount, source));
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEnumerator ApplyRefreshOnlyOverTimeEffectCoroutine(string key, SkillDefinition def, GameObject target, float totalAmount, GameObject source)
|
||||
{
|
||||
float duration = Mathf.Max(0f, def.defaultDuration);
|
||||
float tickInterval = def.GetEffectiveTickInterval();
|
||||
if (tickInterval <= 0f) tickInterval = 1f;
|
||||
|
||||
if (duration <= 0f)
|
||||
{
|
||||
ApplyRefreshOnlyOverTimeTick(def.effectType, target, totalAmount, source);
|
||||
_refreshOnlyOverTimeCoroutines.Remove(key);
|
||||
yield break;
|
||||
}
|
||||
|
||||
int ticks = Mathf.Max(1, Mathf.CeilToInt(duration / tickInterval));
|
||||
float perTick = totalAmount / ticks;
|
||||
float elapsed = 0f;
|
||||
while (elapsed < duration)
|
||||
{
|
||||
if (target == null) break;
|
||||
if (!ApplyRefreshOnlyOverTimeTick(def.effectType, target, perTick, source)) break;
|
||||
|
||||
yield return new WaitForSeconds(tickInterval);
|
||||
elapsed += tickInterval;
|
||||
}
|
||||
|
||||
_refreshOnlyOverTimeCoroutines.Remove(key);
|
||||
}
|
||||
|
||||
private bool ApplyRefreshOnlyOverTimeTick(EffectType effectType, GameObject target, float amount, GameObject source)
|
||||
{
|
||||
if (target == null) return false;
|
||||
|
||||
switch (effectType)
|
||||
{
|
||||
case EffectType.HealGroupOverTime:
|
||||
case EffectType.HealOverTimeSelf:
|
||||
case EffectType.HealOverTimeEnemy:
|
||||
var comp = target.GetComponent<ICombatant>();
|
||||
if (comp == null) return false;
|
||||
comp.ReceiveHeal(amount, source);
|
||||
if (source != null)
|
||||
{
|
||||
var sourceAlly = source.GetComponent<AllyCombatant>();
|
||||
if (sourceAlly != null && teamUIController.Instance != null)
|
||||
teamUIController.Instance.RecordHeal(sourceAlly.slotIndex, amount);
|
||||
}
|
||||
return true;
|
||||
|
||||
case EffectType.IncreaseManaOverTime:
|
||||
var ally = target.GetComponent<AllyCombatant>();
|
||||
if (ally == null) return false;
|
||||
ally.ModifyMana(Mathf.CeilToInt(amount), true, true);
|
||||
if (amount > 0f && source != null)
|
||||
{
|
||||
var sourceAlly = source.GetComponent<AllyCombatant>();
|
||||
if (sourceAlly != null && teamUIController.Instance != null)
|
||||
teamUIController.Instance.RecordMana(sourceAlly.slotIndex, amount);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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; }
|
||||
|
||||
// Resolve group/icon metadata for the ally HUD skill icon queue.
|
||||
try
|
||||
{
|
||||
var heroSoForName = GetAllyHeroSOBySlot(slotIndex);
|
||||
|
||||
// Documentation text normalized.
|
||||
int groupId = 0;
|
||||
string smallSkillName = null;
|
||||
Sprite smallSkillIcon = null;
|
||||
if (heroSoForName != null && heroSoForName.skillGroups != null)
|
||||
{
|
||||
foreach (var g in heroSoForName.skillGroups)
|
||||
{
|
||||
if (g == null || g.skills == null) continue;
|
||||
foreach (var sd in g.skills)
|
||||
{
|
||||
if (sd == null) continue;
|
||||
if (sd == def)
|
||||
{
|
||||
groupId = g.skillGroupID;
|
||||
if (!string.IsNullOrWhiteSpace(g.groupName)) smallSkillName = g.groupName;
|
||||
smallSkillIcon = g.groupIcon;
|
||||
goto ResolvedGroup;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: match by skillId when the SkillDefinition instance isn't the same reference.
|
||||
if (!string.IsNullOrWhiteSpace(def.skillId))
|
||||
{
|
||||
foreach (var g in heroSoForName.skillGroups)
|
||||
{
|
||||
if (g == null || g.skills == null) continue;
|
||||
foreach (var sd in g.skills)
|
||||
{
|
||||
if (sd == null) continue;
|
||||
if (!string.IsNullOrWhiteSpace(sd.skillId) && sd.skillId == def.skillId)
|
||||
{
|
||||
groupId = g.skillGroupID;
|
||||
if (!string.IsNullOrWhiteSpace(g.groupName)) smallSkillName = g.groupName;
|
||||
smallSkillIcon = g.groupIcon;
|
||||
goto ResolvedGroup;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ResolvedGroup:
|
||||
|
||||
if (string.IsNullOrWhiteSpace(smallSkillName))
|
||||
{
|
||||
// Fallback: best-effort readable name.
|
||||
if (!string.IsNullOrWhiteSpace(def.summaryInfo)) smallSkillName = def.summaryInfo;
|
||||
else if (!string.IsNullOrWhiteSpace(def.displayName)) smallSkillName = def.displayName;
|
||||
else smallSkillName = !string.IsNullOrWhiteSpace(def.skillId) ? def.skillId : def.name;
|
||||
}
|
||||
|
||||
// Dedup: avoid printing multiple times for the same group in a single frame.
|
||||
int frame = Time.frameCount;
|
||||
int keySlot = slotIndex < 0 ? 0 : slotIndex;
|
||||
long key;
|
||||
if (groupId != 0)
|
||||
key = ((long)keySlot << 32) | (uint)groupId;
|
||||
else
|
||||
key = ((long)keySlot << 32) | (uint)(smallSkillName.GetHashCode());
|
||||
|
||||
int lastFrame;
|
||||
if (!s_lastSkillFeedFrameByKey.TryGetValue(key, out lastFrame) || lastFrame != frame)
|
||||
{
|
||||
s_lastSkillFeedFrameByKey[key] = frame;
|
||||
// Ally HUD skill icon feed (newest first, max 3).
|
||||
if (smallSkillIcon != null && slotIndex >= 0 && slotIndex < 5)
|
||||
{
|
||||
try
|
||||
{
|
||||
var allyGo = GetAllyObjectBySlot(slotIndex);
|
||||
var ally = allyGo != null ? allyGo.GetComponent<AllyCombatant>() : null;
|
||||
if (ally != null) ally.PushSkillIcon(smallSkillIcon);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { /* never break gameplay because of optional UI */ }
|
||||
|
||||
GameObject caster = GetAllyObjectBySlot(slotIndex);
|
||||
if (caster == null)
|
||||
{
|
||||
@@ -708,26 +1109,46 @@ public class SkillBuilder : MonoBehaviour
|
||||
var vars = _varsBuffer;
|
||||
vars.Clear();
|
||||
vars["slot"] = slotIndex;
|
||||
vars["attack"] = GetAllyBaseAttack(so);
|
||||
if (so != null && so.levelStats != null && so.levelStats.Count > 0)
|
||||
|
||||
// Prefer runtime values so buffs/debuffs and max stat changes affect formulas.
|
||||
if (casterAlly != null)
|
||||
{
|
||||
vars["attack"] = casterAlly.attack;
|
||||
vars["maxHP"] = casterAlly.maxHP;
|
||||
vars["maxMana"] = casterAlly.maxMana;
|
||||
vars["damageResistance"] = casterAlly.damageResistance;
|
||||
vars["scoreEfficiency"] = casterAlly.scoreEfficiency;
|
||||
}
|
||||
else if (so != null && so.levelStats != null && so.levelStats.Count > 0)
|
||||
{
|
||||
var eff = GetEffectiveLevelInfo(so);
|
||||
var lvl = eff ?? so.levelStats[0];
|
||||
vars["attack"] = lvl.attack;
|
||||
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["attack"] = GetAllyBaseAttack(so);
|
||||
vars["maxHP"] = 0f;
|
||||
vars["maxMana"] = 0f;
|
||||
vars["damageResistance"] = 0f;
|
||||
vars["scoreEfficiency"] = 1f;
|
||||
}
|
||||
|
||||
// Expose level variables (base level from SO).
|
||||
if (so != null && so.levelStats != null && so.levelStats.Count > 0)
|
||||
{
|
||||
var eff = GetEffectiveLevelInfo(so);
|
||||
var lvl = eff ?? so.levelStats[0];
|
||||
vars["level"] = lvl.levelID;
|
||||
vars["levelID"] = lvl.levelID;
|
||||
vars["currentLevel"] = lvl.levelID;
|
||||
}
|
||||
else
|
||||
{
|
||||
vars["maxHP"] = 0f;
|
||||
vars["maxMana"] = 0f;
|
||||
vars["damageResistance"] = 0f;
|
||||
vars["scoreEfficiency"] = 1f;
|
||||
vars["level"] = 0f;
|
||||
vars["levelID"] = 0f;
|
||||
vars["currentLevel"] = 0f;
|
||||
@@ -762,7 +1183,19 @@ public class SkillBuilder : MonoBehaviour
|
||||
// amount now represents the formula result (per-tick for sustained effects, instant for single-instance)
|
||||
float amountPerTick = amount;
|
||||
float amountTotal = amountPerTick;
|
||||
if (!def.IsSingleInstance)
|
||||
|
||||
// Only some effect types treat formula as "per tick". Others (buffs/debuffs/stat changes) should use the
|
||||
// raw formula value even if designers leave a non-zero tickInterval by accident.
|
||||
bool scaleByTicks =
|
||||
!def.IsSingleInstance &&
|
||||
(def.effectType == EffectType.DamageOverTimeEnemy ||
|
||||
def.effectType == EffectType.DamageOverTimeAlly ||
|
||||
def.effectType == EffectType.HealOverTimeEnemy ||
|
||||
def.effectType == EffectType.HealOverTimeSelf ||
|
||||
def.effectType == EffectType.HealGroupOverTime ||
|
||||
def.effectType == EffectType.IncreaseManaOverTime);
|
||||
|
||||
if (scaleByTicks)
|
||||
{
|
||||
// compute ticks based on def duration and effective tick interval
|
||||
float tickInterval = def.GetEffectiveTickInterval();
|
||||
@@ -778,6 +1211,20 @@ public class SkillBuilder : MonoBehaviour
|
||||
amountTotal = amountPerTick;
|
||||
}
|
||||
|
||||
// Repeat-window override (stateful): if this skill is triggered again within the window,
|
||||
// override the computed amount with repeatValue. Used by effects like "300 score, but 500 if retriggered within 5s".
|
||||
if (def.repeatWindowSeconds > 0f && def.repeatValue != 0f)
|
||||
{
|
||||
string sid = !string.IsNullOrWhiteSpace(def.skillId) ? def.skillId : def.name;
|
||||
string repeatKey = $"{slotIndex}:{sid}";
|
||||
float now = Time.time;
|
||||
if (_lastSkillTriggerTime.TryGetValue(repeatKey, out float last) && (now - last) <= def.repeatWindowSeconds)
|
||||
{
|
||||
amountTotal = def.repeatValue;
|
||||
}
|
||||
_lastSkillTriggerTime[repeatKey] = now;
|
||||
}
|
||||
|
||||
if (def.operateDirectly)
|
||||
{
|
||||
// Resolve targets as GameObjects but operate on their AllyCombatant / ICombatant data directly
|
||||
@@ -821,14 +1268,14 @@ public class SkillBuilder : MonoBehaviour
|
||||
break;
|
||||
case EffectType.HealSingleSelf:
|
||||
case EffectType.HealGroupSingle:
|
||||
if (ally != null) ally.ModifyHP(Mathf.CeilToInt(amountTotal), true);
|
||||
if (ally != null) ally.ReceiveHeal(amountTotal, caster);
|
||||
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);
|
||||
if (ally != null) ally.ReceiveHeal(amountPerTick, caster);
|
||||
else ic?.ReceiveHeal(amountPerTick, caster);
|
||||
}
|
||||
else
|
||||
@@ -852,45 +1299,108 @@ public class SkillBuilder : MonoBehaviour
|
||||
}
|
||||
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);
|
||||
if (ic != null)
|
||||
{
|
||||
float reduction = Mathf.Clamp01(amountPerTick);
|
||||
var deb = new Buff
|
||||
{
|
||||
buffId = System.Guid.NewGuid().ToString(),
|
||||
duration = def.defaultDuration,
|
||||
description = "ReduceHeal",
|
||||
healReceivedMultiplier = 1f - reduction
|
||||
};
|
||||
ic.ApplyBuff(deb, caster);
|
||||
StartCoroutine(RemoveBuffAfterDuration(t, deb.buffId, deb.duration));
|
||||
}
|
||||
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);
|
||||
if (ic != null)
|
||||
{
|
||||
// Generic timed buff/debuff: use amount as score multiplier (1.5 = +50%, 0.5 = -50%).
|
||||
float mult = amountPerTick;
|
||||
if (mult <= 0f) mult = 1f;
|
||||
var b = new Buff
|
||||
{
|
||||
buffId = System.Guid.NewGuid().ToString(),
|
||||
duration = def.defaultDuration,
|
||||
scoreMultiplier = mult
|
||||
};
|
||||
ic.ApplyBuff(b, caster);
|
||||
StartCoroutine(RemoveBuffAfterDuration(t, b.buffId, b.duration));
|
||||
}
|
||||
break;
|
||||
// ���� effect type ����
|
||||
// Documentation text normalized.
|
||||
case EffectType.IncreaseMaxHP:
|
||||
if (ally != null) ally.SetMaxHP(ally.maxHP + Mathf.CeilToInt(amountTotal), false);
|
||||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy)) enemy.maxHP += Mathf.CeilToInt(amountTotal);
|
||||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy)) enemy.SetMaxHP(enemy.maxHP + Mathf.CeilToInt(amountTotal), false);
|
||||
break;
|
||||
case EffectType.DecreaseMaxHP:
|
||||
if (ally != null) ally.SetMaxHP(Mathf.Max(1, ally.maxHP - Mathf.CeilToInt(amountTotal)), false);
|
||||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy2)) enemy2.maxHP = Mathf.Max(1, enemy2.maxHP - Mathf.CeilToInt(amountTotal));
|
||||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy2)) enemy2.SetMaxHP(Mathf.Max(1, enemy2.maxHP - Mathf.CeilToInt(amountTotal)), false);
|
||||
break;
|
||||
case EffectType.IncreaseMaxMana:
|
||||
if (ally != null) ally.SetMaxMana(ally.maxMana + Mathf.CeilToInt(amountTotal), false);
|
||||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy3)) enemy3.maxMana += Mathf.CeilToInt(amountTotal);
|
||||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy3)) enemy3.SetMaxMana(enemy3.maxMana + Mathf.CeilToInt(amountTotal), false);
|
||||
break;
|
||||
case EffectType.DecreaseMaxMana:
|
||||
if (ally != null) ally.SetMaxMana(Mathf.Max(1, ally.maxMana - Mathf.CeilToInt(amountTotal)), false);
|
||||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy4)) enemy4.maxMana = Mathf.Max(1, enemy4.maxMana - Mathf.CeilToInt(amountTotal));
|
||||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy4)) enemy4.SetMaxMana(Mathf.Max(1, enemy4.maxMana - Mathf.CeilToInt(amountTotal)), false);
|
||||
break;
|
||||
case EffectType.IncreaseScoreEfficiency:
|
||||
if (ally != null) ally.scoreEfficiency += amountTotal;
|
||||
if (ally != null)
|
||||
{
|
||||
if (def.defaultDuration <= 0f)
|
||||
{
|
||||
ally.scoreEfficiency += amountTotal;
|
||||
}
|
||||
else if (!TryApplyRefreshOnlyTimedEffect(def, t, amountTotal, def.defaultDuration))
|
||||
{
|
||||
StartCoroutine(ApplyTemporaryScoreEfficiencyChangeDirect(ally, amountTotal, def.defaultDuration));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EffectType.DecreaseScoreEfficiency:
|
||||
if (ally != null) ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency - amountTotal);
|
||||
if (ally != null)
|
||||
{
|
||||
if (def.defaultDuration <= 0f) ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency - amountTotal);
|
||||
else if (!TryApplyRefreshOnlyTimedEffect(def, t, amountTotal, def.defaultDuration))
|
||||
StartCoroutine(ApplyTemporaryScoreEfficiencyChangeDirect(ally, -amountTotal, def.defaultDuration));
|
||||
}
|
||||
break;
|
||||
case EffectType.IncreaseAttack:
|
||||
if (ally != null) ally.ModifyAttack(Mathf.CeilToInt(amountTotal));
|
||||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy5)) enemy5.attack += Mathf.CeilToInt(amountTotal);
|
||||
{
|
||||
int delta = Mathf.CeilToInt(amountTotal);
|
||||
if (ally != null)
|
||||
{
|
||||
if (def.defaultDuration <= 0f) ally.ModifyAttack(delta);
|
||||
else if (!TryApplyRefreshOnlyTimedEffect(def, t, amountTotal, def.defaultDuration))
|
||||
StartCoroutine(ApplyTemporaryAttackChangeDirect(ally, delta, def.defaultDuration));
|
||||
}
|
||||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy5))
|
||||
{
|
||||
if (def.defaultDuration <= 0f) enemy5.ModifyAttack(delta);
|
||||
else if (!TryApplyRefreshOnlyTimedEffect(def, t, amountTotal, def.defaultDuration))
|
||||
StartCoroutine(ApplyTemporaryAttackChangeDirect(enemy5, delta, def.defaultDuration));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EffectType.DecreaseAttack:
|
||||
if (ally != null) ally.ModifyAttack(-Mathf.CeilToInt(amountTotal));
|
||||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy6)) enemy6.attack = Mathf.Max(0, enemy6.attack - Mathf.CeilToInt(amountTotal));
|
||||
{
|
||||
int delta = Mathf.CeilToInt(amountTotal);
|
||||
if (ally != null)
|
||||
{
|
||||
if (def.defaultDuration <= 0f) ally.ModifyAttack(-delta);
|
||||
else if (!TryApplyRefreshOnlyTimedEffect(def, t, amountTotal, def.defaultDuration))
|
||||
StartCoroutine(ApplyTemporaryAttackChangeDirect(ally, -delta, def.defaultDuration));
|
||||
}
|
||||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy6))
|
||||
{
|
||||
if (def.defaultDuration <= 0f) enemy6.ModifyAttack(-delta);
|
||||
else if (!TryApplyRefreshOnlyTimedEffect(def, t, amountTotal, def.defaultDuration))
|
||||
StartCoroutine(ApplyTemporaryAttackChangeDirect(enemy6, -delta, def.defaultDuration));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EffectType.RedirectNextDamageToSelf:
|
||||
if (ally != null) AllyCombatant.ActivateNextDamageRedirect(ally, def.defaultDuration);
|
||||
@@ -908,6 +1418,26 @@ public class SkillBuilder : MonoBehaviour
|
||||
{
|
||||
Debug.Log($"<color=#00FFFF>[SkillDebug]</color> <color=#FFD700>ID: {def.skillId}</color> | <color=#00FF00>Trigger: {def.triggerCondition}</color> | <color=#FFA500>Target: (Resolving via EffectSystem)</color> | <color=#EE82EE>Effect: {def.effectType}</color> | <color=#FF4500>Value: {amountTotal:F2}</color>");
|
||||
}
|
||||
|
||||
if (def.defaultDuration > 0f)
|
||||
{
|
||||
var resolvedTargets = ResolveTargetsLocal(def.defaultSelector, caster, specificTarget);
|
||||
bool appliedRefreshOnly = false;
|
||||
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);
|
||||
if (handled) appliedRefreshOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
// For listed skills: do not stack when retriggered, only refresh duration.
|
||||
if (appliedRefreshOnly) return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
@@ -1249,6 +1779,19 @@ public class SkillBuilder : MonoBehaviour
|
||||
try { ApplySharedOnNoteHit(trackIndex, judgeResult); } catch (System.Exception ex) { Debug.LogError($"[SkillBuilder] ApplySharedOnNoteHit threw: {ex}"); }
|
||||
}
|
||||
|
||||
// Dead allies should not trigger any note-hit skills.
|
||||
try
|
||||
{
|
||||
var allyGO = GetAllyObjectBySlot(trackIndex);
|
||||
var ally = allyGO != null ? allyGO.GetComponent<AllyCombatant>() : null;
|
||||
if (ally != null && ally.IsDead)
|
||||
{
|
||||
LogVerbose($"[SkillBuilder] NotifyNoteHit: slot {trackIndex} is dead -> ignore skill triggers.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (trackIndex < 0) { LogVerbose("[SkillBuilder] NotifyNoteHit: invalid trackIndex"); return false; }
|
||||
var so = GetAllyHeroSOBySlot(trackIndex);
|
||||
if (so == null)
|
||||
@@ -1405,8 +1948,8 @@ public class SkillBuilder : MonoBehaviour
|
||||
- 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.
|
||||
*/
|
||||
|
||||
// ----------------------------- ʾ����Ϊ���? ��װһ����ݺ������û�ʾ����?-----------------------------
|
||||
// ���?(����2) �ļ���1����ѭ�û�����ǩ����_ally03skill01(EffectType, value, Selector, duration)
|
||||
// Documentation text normalized.
|
||||
// Documentation text normalized.
|
||||
public void _ally03skill01(EffectType effectType, float value, Selector selector, float duration = 0f, GameObject specificTarget = null)
|
||||
{
|
||||
AllySlotSkill(2, "skill01", effectType, value, selector, duration, specificTarget);
|
||||
@@ -1432,6 +1975,42 @@ public class SkillBuilder : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator RemoveBuffAfterDuration(GameObject target, string buffId, float duration)
|
||||
{
|
||||
if (duration <= 0f) yield break;
|
||||
yield return new WaitForSeconds(duration);
|
||||
if (target == null) yield break;
|
||||
var ic = target.GetComponent<ICombatant>();
|
||||
ic?.RemoveBuff(buffId);
|
||||
}
|
||||
|
||||
private IEnumerator ApplyTemporaryScoreEfficiencyChangeDirect(AllyCombatant ally, float delta, float duration)
|
||||
{
|
||||
if (ally == null) yield break;
|
||||
ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency + delta);
|
||||
yield return new WaitForSeconds(duration);
|
||||
if (ally == null) yield break;
|
||||
ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency - delta);
|
||||
}
|
||||
|
||||
private IEnumerator ApplyTemporaryAttackChangeDirect(AllyCombatant ally, int delta, float duration)
|
||||
{
|
||||
if (ally == null) yield break;
|
||||
ally.ModifyAttack(delta);
|
||||
yield return new WaitForSeconds(duration);
|
||||
if (ally == null) yield break;
|
||||
ally.ModifyAttack(-delta);
|
||||
}
|
||||
|
||||
private IEnumerator ApplyTemporaryAttackChangeDirect(EnemyCombatant enemy, int delta, float duration)
|
||||
{
|
||||
if (enemy == null) yield break;
|
||||
enemy.ModifyAttack(delta);
|
||||
yield return new WaitForSeconds(duration);
|
||||
if (enemy == null) yield break;
|
||||
enemy.ModifyAttack(-delta);
|
||||
}
|
||||
|
||||
// Coroutine helpers for direct DoT/HoT on GameObject targets
|
||||
private IEnumerator ApplyDamageOverTimeDirect(GameObject target, float totalAmount, float duration, float tickInterval, GameObject source)
|
||||
{
|
||||
@@ -1476,7 +2055,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
|
||||
if (duration <= 0f || tickInterval <= 0f)
|
||||
{
|
||||
if (ally != null) ally.ModifyHP(Mathf.CeilToInt(totalAmount), true);
|
||||
if (ally != null) ally.ReceiveHeal(totalAmount, source);
|
||||
else ic?.ReceiveHeal(totalAmount, source);
|
||||
yield break;
|
||||
}
|
||||
@@ -1486,7 +2065,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
float elapsed = 0f;
|
||||
while (elapsed < duration)
|
||||
{
|
||||
if (ally != null) ally.ModifyHP(Mathf.CeilToInt(perTick), true);
|
||||
if (ally != null) ally.ReceiveHeal(perTick, source);
|
||||
else ic?.ReceiveHeal(perTick, source);
|
||||
yield return new WaitForSeconds(tickInterval);
|
||||
elapsed += tickInterval;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
@@ -16,90 +16,114 @@ public class SkillDefinition : ScriptableObject
|
||||
OnNoteHit,
|
||||
OnManaFull,
|
||||
OnAttackZero,
|
||||
OnEnemyRevive, // �����������˵dz�ʱ����
|
||||
OnEnemyDead, // ����������������ʱ����
|
||||
// �����������е��˱�����ʱ����
|
||||
OnEnemyRevive, // Documentation text normalized.
|
||||
OnEnemyDead, // Documentation text normalized.
|
||||
// Documentation text normalized.
|
||||
OnAllEnemiesDefeated,
|
||||
// ����������λ�ָ�����ֵʱ���������ƣ�
|
||||
// Documentation text normalized.
|
||||
OnHPHealed,
|
||||
// ����������λʧȥ����ֵʱ����������/���˺���
|
||||
// Documentation text normalized.
|
||||
OnHPLost,
|
||||
// ����������λ��÷���ʱ����
|
||||
// Documentation text normalized.
|
||||
OnManaGained,
|
||||
// ����������λʧȥ����ʱ����
|
||||
// Documentation text normalized.
|
||||
OnManaLost,
|
||||
// ������������ֵ����ij��ֵʱ���������ٷֱȣ�0-1��
|
||||
// Documentation text normalized.
|
||||
OnHPAbovePercent,
|
||||
// ������������ֵ����ij��ֵʱ���������ٷֱȣ�0-1��
|
||||
OnHPBelowPercent
|
||||
// Documentation text normalized.
|
||||
OnHPBelowPercent,
|
||||
// New: attack threshold trigger (attack < attackTriggerValue)
|
||||
OnAttackBelowValue,
|
||||
// New: triggers once when this ally is defeated (HP reaches 0 from above).
|
||||
OnSelfDefeated
|
||||
}
|
||||
|
||||
[Header("��ʶ (Identity)")]
|
||||
[Tooltip("���ܵ��ڲ�Ψһ ID�����ڴ�����ʶ��˼���")]
|
||||
[Header("Inspector")]
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
public string skillId;
|
||||
|
||||
[Tooltip("������ UI ����ʾ������")]
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
public string displayName;
|
||||
|
||||
[Tooltip("���ע��/ժҪ���������ʱ�ο�����Ӱ������")]
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
[TextArea(1,2)]
|
||||
public string summaryInfo;
|
||||
|
||||
[Header("�� (Effect)")]
|
||||
[Tooltip("���ܵ�Ч�����ͣ����缴ʱ�˺��������˺������ơ�����/�����")]
|
||||
[Header("Inspector")]
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
public EffectType effectType = EffectType.DamageSingleEnemy;
|
||||
|
||||
[Tooltip("Ĭ��Ŀ��ѡ��������������Ĭ�����õ�Ŀ�귶Χ������/�����Ѿ�/�з��ȣ�")]
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
public Selector defaultSelector = Selector.CurrentEnemies;
|
||||
|
||||
[Tooltip("��ѡ�� FromFormula ʱʹ�õı���ʽ�ַ�����֧��ʹ�ñ�����attack, maxHP, currentHP, maxMana, currentMana, damageResistance, scoreEfficiency, ally_currentEXP, currentScore(�� idolScore), slot, level/currentLevel/levelID��ʾ��: 'attack * 1.5 + maxHP * 0.1 + 20'�����ֻ��дһ�����֣����� '120'�������ͬ��ָ���̶���ֵ��")]
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
[TextArea(2,4)]
|
||||
public string formula = "";
|
||||
|
||||
[Tooltip("Ĭ�ϳ���ʱ�䣨�룩�����ڳ���Ч���� Buff �ij���ʱ������Ϊ 0 ����Ϊһ����/��ʱ���ܣ����ᰴ tick �ַ���")]
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
public float defaultDuration = 0f;
|
||||
|
||||
[Tooltip("Ĭ�ϴ�������룩�����ڳ���Ч����ÿ�δ������������ÿ���Ѫ������Ϊ 0 ����Ϊһ����/��ʱ����")]
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
public float defaultTickInterval = 1f;
|
||||
|
||||
[Header("��־ (Flags)")]
|
||||
[Tooltip("�Ƿ����ָ������Ŀ�꣨true ��ʾ�˼���ͨ����Ҫ���� specificTarget��")]
|
||||
[Header("Inspector")]
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
public bool requiresSpecificTarget = false; // if true, skill usually targets a specific GameObject
|
||||
|
||||
[Tooltip("�Ƿ�ֱ��������Ŀ������ݣ�true ��ʾ SkillBuilder ��ֱ�ӵ��� ICombatant/AllyCombatant������ͨ�� EffectSystem �ַ���")]
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
public bool operateDirectly = false; // if true, SkillBuilder will operate on ICombatant/AllyCombatant fields directly and update UI, instead of calling EffectSystem
|
||||
|
||||
[Tooltip("���ܵ�������ע���������ʱ��д˵��")]
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
[TextArea(8,4)]
|
||||
public string description;
|
||||
|
||||
[Header("�������� (Trigger)")]
|
||||
[Tooltip("���ܴ���������������Ϸʱ����������ʱ������ֵ��ʱ��")]
|
||||
[Header("Inspector")]
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
public SkillTrigger triggerCondition = SkillTrigger.None;
|
||||
|
||||
[Tooltip("���� triggerCondition == OnNoteHit ʱ��Ч���ﵽ���ж������ʱ��������ѡ Miss/Good/Great/Perfect�����ر�˵����ѡ�� Miss ��ʾ�������ж�Ϊ Miss ʱ�������������ж���Good/Great/Perfect�����ᴥ������")]
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
public NoteTriggerThreshold onNoteHitMinThreshold = NoteTriggerThreshold.Good;
|
||||
|
||||
[Tooltip("���� triggerCondition == OnNoteHit ʱ��Ч��ָ���˼����� Tap�������/Hold������β�Σ��� Either�����⣩��������ѡ�� Hold����ֻ�г�����β�λᴥ������ѡ�� Tap������̰�������Either �����߾��ɣ��� Hold �Կɱ���ʽ���ã���")]
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
public NoteTypeTrigger noteTriggerType = NoteTypeTrigger.Either;
|
||||
|
||||
[Tooltip("���� triggerCondition == OnNoteHit ʱ��Ч�����������ȴʱ�䣨�룩������ȴʱ�����ٴ������ж����ᴥ������")]
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
public float onNoteHitCooldown = 0f;
|
||||
|
||||
[Tooltip("���� triggerCondition Ϊ HP �ٷֱ����ʱ��Ч������ֵ��ֵ��0-1�������� 0.3 ��ʾ 30%")]
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
public float hpTriggerPercent = 0.5f;
|
||||
|
||||
[Tooltip("Optional: for HP percent triggers, evaluate this formula (0..1) to get the threshold instead of hpTriggerPercent. Useful for per-level thresholds like 90/80/75/70.")]
|
||||
public string hpTriggerPercentFormula = "";
|
||||
|
||||
[Tooltip("For OnAttackBelowValue: triggers when attack < attackTriggerValue.")]
|
||||
public int attackTriggerValue = 0;
|
||||
|
||||
[Tooltip("Optional: if > 0 and repeatValue != 0, when the skill triggers again within this window, the computed amount is overridden by repeatValue.")]
|
||||
public float repeatWindowSeconds = 0f;
|
||||
public float repeatValue = 0f;
|
||||
|
||||
[Header("技能专属")]
|
||||
[Tooltip("技能专属")]
|
||||
public specificBelongsTo specificBelongType = specificBelongsTo.None;
|
||||
public enum specificBelongsTo
|
||||
{
|
||||
None,
|
||||
Unique,
|
||||
Special
|
||||
}
|
||||
|
||||
// Minimum judge quality required to trigger on note hit
|
||||
public enum NoteTriggerThreshold
|
||||
{
|
||||
// ������Ϊ Miss ʱ�����ܽ����ж�Ϊ Miss��quality == 0��ʱ����
|
||||
// Documentation text normalized.
|
||||
Miss = 0,
|
||||
Good = 1,
|
||||
Great = 2,
|
||||
Perfect = 3
|
||||
}
|
||||
|
||||
// ������ָ������Ӧ�� Tap/ Hold/ Either ����
|
||||
// Documentation text normalized.
|
||||
public enum NoteTypeTrigger
|
||||
{
|
||||
Tap,
|
||||
@@ -107,10 +131,10 @@ public class SkillDefinition : ScriptableObject
|
||||
Either
|
||||
}
|
||||
|
||||
// ����ʱ�������ԣ��� defaultDuration �� defaultTickInterval Ϊ 0 ʱ������һ���Լ�ʱ����
|
||||
// Documentation text normalized.
|
||||
public bool IsSingleInstance => defaultDuration <= 0f || defaultTickInterval <= 0f;
|
||||
|
||||
// ����ʵ�����ڷַ����ļ������Ϊһ���Լ����� 0��
|
||||
// Documentation text normalized.
|
||||
public float GetEffectiveTickInterval()
|
||||
{
|
||||
return IsSingleInstance ? 0f : Mathf.Max(0.0001f, defaultTickInterval);
|
||||
@@ -118,13 +142,15 @@ public class SkillDefinition : ScriptableObject
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
// ��֤��ֵΪ�Ǹ�
|
||||
// Documentation text normalized.
|
||||
if (defaultDuration < 0f) defaultDuration = 0f;
|
||||
if (defaultTickInterval < 0f) defaultTickInterval = 0f;
|
||||
if (onNoteHitCooldown < 0f) onNoteHitCooldown = 0f;
|
||||
// clamp hpTriggerPercent
|
||||
if (hpTriggerPercent < 0f) hpTriggerPercent = 0f;
|
||||
if (hpTriggerPercent > 1f) hpTriggerPercent = 1f;
|
||||
if (attackTriggerValue < 0) attackTriggerValue = 0;
|
||||
if (repeatWindowSeconds < 0f) repeatWindowSeconds = 0f;
|
||||
}
|
||||
|
||||
// --- Simple expression evaluator used when valueMode == FromFormula ---
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine;
|
||||
|
||||
[System.Serializable]
|
||||
public class SkillGroup
|
||||
@@ -9,10 +9,10 @@ public class SkillGroup
|
||||
[Tooltip("Optional human-readable name for the group (for designers).")]
|
||||
public string groupName = "";
|
||||
|
||||
// 新增:栏目编号,用于分组和排序
|
||||
// Documentation text normalized.
|
||||
public int columnID = 0;
|
||||
|
||||
// 新增:栏目名字,用于UI显示
|
||||
// Documentation text normalized.
|
||||
public string columnName = "";
|
||||
|
||||
// Variable-length list of SkillDefinitions so one group ID can map to multiple skills
|
||||
|
||||
Reference in New Issue
Block a user