备份,准备做双端

This commit is contained in:
2026-07-30 23:15:58 +08:00
parent add675e45d
commit ec4ec53545
88 changed files with 4050 additions and 872 deletions
+103 -7
View File
@@ -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];