备份,准备做双端
This commit is contained in:
@@ -55,10 +55,18 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
public int attack = 0;
|
||||
|
||||
// Keep an unbuffed attack baseline so temporary Buff.attackMultiplier can be applied/reverted correctly.
|
||||
// True (permanent) attack baseline. Only permanent changes touch this: initial stats,
|
||||
// level-ups, LevelRule AttackDelta, and duration<=0 skills. Timed skill buffs must NOT
|
||||
// bake into this — they live in _attackModifiers so percentages stay computed off the
|
||||
// unbuffed base and re-triggering a timed buff refreshes instead of compounding.
|
||||
private int _attackBaseUnbuffed = 0;
|
||||
private bool _attackBaseInitialized = false;
|
||||
|
||||
// Additive attack modifiers keyed by source (skillId). Each source contributes exactly one
|
||||
// entry; re-applying the same source replaces its value (idempotent), so "+10% from A" and
|
||||
// "+20% from B" sum to +30% of base rather than compounding multiplicatively per note hit.
|
||||
private readonly Dictionary<string, int> _attackModifiers = new Dictionary<string, int>();
|
||||
|
||||
// scoring fields
|
||||
[Header("Scoring")]
|
||||
[Tooltip("Base score value for a perfect hit on this track")]
|
||||
@@ -122,6 +130,11 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
private Coroutine skillIconAutoFadeCoroutine;
|
||||
private float lastSkillIconPushTime = -1f;
|
||||
|
||||
// Pool of removed-skill-icon GameObjects reused by PlaySkillIconExitAnim, so each icon
|
||||
// eviction reuses an instance instead of new GameObject(...) + Destroy every time.
|
||||
private readonly Stack<GameObject> _removedSkillIconPool = new Stack<GameObject>();
|
||||
private readonly List<GameObject> _removedSkillIconAll = new List<GameObject>();
|
||||
|
||||
// Track skills that are currently active because HP percent condition is satisfied.
|
||||
private HashSet<string> _activeHpPercentSkills = new HashSet<string>();
|
||||
|
||||
@@ -914,10 +927,11 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
if (!Application.isPlaying) return;
|
||||
|
||||
// Render removed icon under the slot parent to avoid disturbing slot layout positions.
|
||||
var go = new GameObject("RemovedSkillIcon", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
||||
var go = AcquireRemovedSkillIcon();
|
||||
RectTransform hostRt = hostSlot.rectTransform;
|
||||
RectTransform parentRt = hostRt != null ? hostRt.parent as RectTransform : null;
|
||||
go.transform.SetParent(parentRt != null ? parentRt : hostSlot.transform, false);
|
||||
go.SetActive(true);
|
||||
|
||||
var rt = go.GetComponent<RectTransform>();
|
||||
if (hostRt != null)
|
||||
@@ -947,7 +961,32 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
if (gameObject.activeInHierarchy)
|
||||
StartCoroutine(SkillIconExitCoroutine(go, rt, img, removedColor, hostSlot));
|
||||
else
|
||||
Destroy(go);
|
||||
ReleaseRemovedSkillIcon(go);
|
||||
}
|
||||
|
||||
private GameObject AcquireRemovedSkillIcon()
|
||||
{
|
||||
GameObject go = null;
|
||||
while (_removedSkillIconPool.Count > 0)
|
||||
{
|
||||
go = _removedSkillIconPool.Pop();
|
||||
if (go != null) break;
|
||||
go = null;
|
||||
}
|
||||
if (go == null)
|
||||
{
|
||||
go = new GameObject("RemovedSkillIcon", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
||||
_removedSkillIconAll.Add(go);
|
||||
}
|
||||
return go;
|
||||
}
|
||||
|
||||
private void ReleaseRemovedSkillIcon(GameObject go)
|
||||
{
|
||||
if (go == null) return;
|
||||
go.SetActive(false);
|
||||
go.transform.SetParent(transform, false);
|
||||
_removedSkillIconPool.Push(go);
|
||||
}
|
||||
|
||||
private IEnumerator SkillIconExitCoroutine(GameObject go, RectTransform rt, Image img, Color baseColor, Image hostSlot)
|
||||
@@ -995,7 +1034,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (go != null) Destroy(go);
|
||||
ReleaseRemovedSkillIcon(go);
|
||||
}
|
||||
|
||||
private void InitializeStatsFromData()
|
||||
@@ -1228,13 +1267,23 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
ScoreManager.Instance?.RecalculateTotal();
|
||||
}
|
||||
|
||||
// 性能:记录上次写入的 "cur/max" 字符串及其目标 Text 对象。仅当数值或目标对象变化时才写 .text,
|
||||
// 避免每次判定对同一 Text 重复赋相同字符串触发 TMP 网格重建。目标对象变化(换局/UI 重建)必写。
|
||||
private string _lastScoreUiText;
|
||||
private UnityEngine.Object _lastScoreUiTarget;
|
||||
|
||||
private void UpdateScoreUI()
|
||||
{
|
||||
string value = $"{currentScore}/{maxTrackScore}";
|
||||
if (currentScoreText != null)
|
||||
{
|
||||
currentScoreText.text = value;
|
||||
LogVerbose($"[AllyCombatant] Updated slot {slotIndex+1} score UI -> {value} (target {currentScoreText.gameObject.name})");
|
||||
if (!ReferenceEquals(_lastScoreUiTarget, currentScoreText) || _lastScoreUiText != value)
|
||||
{
|
||||
currentScoreText.text = value;
|
||||
_lastScoreUiTarget = currentScoreText;
|
||||
_lastScoreUiText = value;
|
||||
LogVerbose($"[AllyCombatant] Updated slot {slotIndex+1} score UI -> {value} (target {currentScoreText.gameObject.name})");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1242,6 +1291,10 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
var ui = teamUIController.Instance;
|
||||
if (ui != null)
|
||||
{
|
||||
// 性能:fallback 走固定的 teammateXX 字段(slotIndex 固定,目标稳定),值未变则跳过整段写入(等效)。
|
||||
if (_lastScoreUiText == value) return;
|
||||
_lastScoreUiText = value;
|
||||
_lastScoreUiTarget = null;
|
||||
switch (slotIndex)
|
||||
{
|
||||
case 0:
|
||||
@@ -1590,8 +1643,13 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
EnsureAttackBaseInitialized();
|
||||
float mult = GetTotalAttackMultiplier();
|
||||
|
||||
int modifierSum = 0;
|
||||
foreach (var kv in _attackModifiers) modifierSum += kv.Value;
|
||||
|
||||
int old = attack;
|
||||
attack = Mathf.Max(0, Mathf.RoundToInt(_attackBaseUnbuffed * mult));
|
||||
// Percentage buffs are pre-summed into _attackModifiers off the base, so the final value is
|
||||
// (base + Σ sourceModifiers) with the legacy multiplicative Buff path (mult, normally 1) on top.
|
||||
attack = Mathf.Max(0, Mathf.RoundToInt(_attackBaseUnbuffed * mult) + modifierSum);
|
||||
if (old != attack)
|
||||
{
|
||||
EvaluateAttackZeroTriggers();
|
||||
@@ -1600,6 +1658,33 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
}
|
||||
}
|
||||
|
||||
// Permanent base attack, unaffected by timed buffs. Skills that compute a percentage of
|
||||
// attack should read this so the percentage is always relative to the unbuffed value.
|
||||
public int GetBaseAttack()
|
||||
{
|
||||
EnsureAttackBaseInitialized();
|
||||
return _attackBaseUnbuffed;
|
||||
}
|
||||
|
||||
// Set/clear an additive attack modifier contributed by a single source (keyed by skillId).
|
||||
// Re-applying the same source replaces its prior contribution instead of stacking, which is
|
||||
// what keeps repeated OnNoteHit percentage buffs from compounding every note.
|
||||
public void SetAttackModifier(string sourceKey, int delta)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sourceKey)) { ModifyAttack(delta); return; }
|
||||
EnsureAttackBaseInitialized();
|
||||
if (delta == 0) _attackModifiers.Remove(sourceKey);
|
||||
else _attackModifiers[sourceKey] = delta;
|
||||
RecalculateAttackFromBuffs();
|
||||
}
|
||||
|
||||
public void ClearAttackModifier(string sourceKey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sourceKey)) return;
|
||||
if (_attackModifiers.Remove(sourceKey)) RecalculateAttackFromBuffs();
|
||||
}
|
||||
|
||||
// Permanent base change (level stats, LevelRule AttackDelta, duration<=0 skills).
|
||||
public void ModifyAttack(int delta)
|
||||
{
|
||||
EnsureAttackBaseInitialized();
|
||||
@@ -2590,8 +2675,19 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
state.iconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(redirector, PlayerBudeffIconType.dmg_redirect_toSelf, 0f, duration);
|
||||
}
|
||||
|
||||
// Frame guard so the global redirect list is swept once per frame, not once per ally
|
||||
// (this is static/global state; running it 5× per frame was pure redundant work).
|
||||
private static int s_lastRedirectCleanupFrame = -1;
|
||||
|
||||
private static void CleanupGlobalRedirects()
|
||||
{
|
||||
int frame = Time.frameCount;
|
||||
if (frame == s_lastRedirectCleanupFrame) return;
|
||||
s_lastRedirectCleanupFrame = frame;
|
||||
|
||||
// Nothing registered -> nothing to sweep.
|
||||
if (s_nextDamageRedirectStates.Count == 0) return;
|
||||
|
||||
for (int i = s_nextDamageRedirectStates.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var state = s_nextDamageRedirectStates[i];
|
||||
|
||||
Reference in New Issue
Block a user