Files
bansonic_beta_main/Assets/scripts/Combat/ScoreManager.cs
T
2026-07-26 03:44:27 +08:00

920 lines
35 KiB
C#

using UnityEngine;
using TMPro;
using UnityEngine.UI;
using DG.Tweening;
public class ScoreManager : MonoBehaviour
{
public static ScoreManager Instance { get; private set; }
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;
public int yellow_pmScore_sum = 0;
public int purple_pmScore_sum = 0;
public int blue_pmScore_sum = 0;
// aggregate of the five track pm sums
public int allSum_pmScore = 0;
private int[] pmScoreSums = new int[5];
// per-track idol score sums (red, green, yellow, purple, blue)
public int red_idolScore_sum = 0;
public int green_idolScore_sum = 0;
public int yellow_idolScore_sum = 0;
public int purple_idolScore_sum = 0;
public int blue_idolScore_sum = 0;
// aggregate of the five track idol sums
public int allSum_idolScore = 0;
private int[] idolScoreSums = new int[5];
private readonly int[] tmpCurrents = new int[5];
private readonly int[] tmpMaxes = new int[5];
private readonly GameObject[] allyObjects = new GameObject[5];
private readonly AllyCombatant[] allyCombatants = new AllyCombatant[5];
private int lastAllyCacheFrame = -9999;
private readonly TextMeshProUGUI[] slotTmpFallback = new TextMeshProUGUI[5];
private readonly Text[] slotLegacyFallback = new Text[5];
private readonly bool[] slotFallbackResolved = new bool[5];
private float nextIdolscoreKeyLookupTime = 0f;
private const float IdolscoreKeyLookupInterval = 0.5f;
// 性能:缓存 currentTotalScore 的 TMP/Text 组件,避免每次判定 GetComponent。
private TextMeshProUGUI _cachedTotalTmp;
private Text _cachedTotalLegacy;
private GameObject _cachedTotalGo;
// 性能:记录上次写入 UI 的字符串,值未变则跳过 .text 赋值(避免 TMP 无谓网格重建)。
private string _lastTotalText;
// 性能:per-track pm/idol 合计上次写入值(12 项),值未变跳过 .text,避免每判定重写全部槽位触发网格重建。
// 索引:0-5 = pm(red,green,yellow,purple,blue,all),6-11 = idol(同序)。int.MinValue 表示尚未写入。
private readonly int[] _lastSumWritten = new int[12] {
int.MinValue, int.MinValue, int.MinValue, int.MinValue, int.MinValue, int.MinValue,
int.MinValue, int.MinValue, int.MinValue, int.MinValue, int.MinValue, int.MinValue
};
// 仅当数值变化时写 TMP.text(避免相同字符串触发无谓网格重建)。
private void SetSumTextIfChanged(TextMeshProUGUI field, int value, int cacheIndex)
{
if (field == null) return;
if (_lastSumWritten[cacheIndex] == value) return;
_lastSumWritten[cacheIndex] = value;
field.text = value.ToString();
}
private JudgeManager hookedJudgeManager;
private bool perfectBonusHooked = false;
private int perfectClearBonusPm = 0;
private bool perfectClearBonusApplied = false;
private int comboRecentPlusAmount = 0;
private int displayedRecentPlusAmount = 0;
private int lastKnownCombo = 0;
private float lastRecentPlusScoreTime = float.NegativeInfinity;
private Tween recentPlusAmountValueTween;
private const float RecentPlusResetDelaySeconds = 5f;
[Header("Judgement Statistics")]
public int countPerfect = 0;
public int countGreat = 0;
public int countGood = 0;
public int countMiss = 0;
[Header("Per-Track Judgement Statistics")]
public int[] trackPerfectCounts = new int[5];
public int[] trackGreatCounts = new int[5];
public int[] trackGoodCounts = new int[5];
public int[] trackMissCounts = new int[5];
[Header("Timing Statistics")]
public int countEarly = 0;
public int countLate = 0;
public float totalOffsetMs = 0f;
public int offsetCount = 0;
public void ResetStatistics()
{
countPerfect = countGreat = countGood = countMiss = 0;
perfectClearBonusPm = 0;
perfectClearBonusApplied = false;
for (int i = 0; i < 5; i++)
{
trackPerfectCounts[i] = 0;
trackGreatCounts[i] = 0;
trackGoodCounts[i] = 0;
trackMissCounts[i] = 0;
}
countEarly = countLate = 0;
totalOffsetMs = 0f;
offsetCount = 0;
}
/// <summary>
/// Records a timing offset in milliseconds.
/// Positive = Early, Negative = Late.
/// </summary>
public void RecordOffset(float offsetMs)
{
if (float.IsNaN(offsetMs)) return;
totalOffsetMs += offsetMs;
offsetCount++;
if (offsetMs > 0) countEarly++;
else if (offsetMs < 0) countLate++;
}
private void Awake()
{
if (Instance == null) Instance = this;
else Destroy(gameObject);
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();
comboRecentPlusAmount = 0;
displayedRecentPlusAmount = 0;
lastKnownCombo = 0;
lastRecentPlusScoreTime = float.NegativeInfinity;
}
private void OnEnable()
{
TryHookPerfectBonusEvent();
}
private void OnDisable()
{
UnhookPerfectBonusEvent();
}
private void OnDestroy()
{
KillRecentPlusAmountValueTween();
UnhookPerfectBonusEvent();
}
private void Start()
{
// Ensure the progress keys start at 0.
UpdateIdolscoreKeyScales();
TryHookPerfectBonusEvent();
RefreshAllScoreUi();
}
private void Update()
{
EnsureIdolscoreKeys();
UpdateRecentPlusAmountTimeout();
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 ?? SceneObjectLookupCache.FindAny<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 = SceneObjectLookupCache.FindAny<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 = SceneObjectLookupCache.FindAny<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();
GameplaySkillLogger.RecordScoreDelta(
"PerfectClearBonus",
-1,
perfectClearBonusPm,
perfectClearBonusPm,
0,
0,
float.NaN,
false,
-1,
-1,
allSum_pmScore,
allSum_idolScore,
totalScore);
}
private void EnsureIdolscoreKeys()
{
bool hasMissingKey = false;
for (int i = 0; i < _idolscoreKeys.Length; i++)
{
if (_idolscoreKeys[i] == null)
{
hasMissingKey = true;
break;
}
}
// Avoid scene lookup every frame when one of these optional key objects is absent.
if (hasMissingKey && Time.unscaledTime >= nextIdolscoreKeyLookupTime)
{
nextIdolscoreKeyLookupTime = Time.unscaledTime + IdolscoreKeyLookupInterval;
if (_idolscoreKeys[0] == null) _idolscoreKeys[0] = SceneObjectLookupCache.Find("idolscoreKey_red")?.transform;
if (_idolscoreKeys[1] == null) _idolscoreKeys[1] = SceneObjectLookupCache.Find("idolscoreKey_green")?.transform;
if (_idolscoreKeys[2] == null) _idolscoreKeys[2] = SceneObjectLookupCache.Find("idolscoreKey_yellow")?.transform;
if (_idolscoreKeys[3] == null) _idolscoreKeys[3] = SceneObjectLookupCache.Find("idolscoreKey_purple")?.transform;
if (_idolscoreKeys[4] == null) _idolscoreKeys[4] = SceneObjectLookupCache.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;
if (frame - lastAllyCacheFrame < 30) return;
lastAllyCacheFrame = frame;
for (int i = 0; i < 5; i++)
{
var current = allyCombatants[i];
if (current != null) continue;
var go = allyObjects[i];
if (go == null)
{
go = SceneObjectLookupCache.Find($"ally_0{i + 1}");
allyObjects[i] = go;
}
if (go != null)
allyCombatants[i] = go.GetComponent<AllyCombatant>();
}
}
private void ResolveSlotFallbackUi(int slotIndex, GameObject parent)
{
if (slotIndex < 0 || slotIndex >= 5) return;
if (slotFallbackResolved[slotIndex]) return;
if (parent == null) return;
slotTmpFallback[slotIndex] = parent.GetComponentInChildren<TextMeshProUGUI>(true);
if (slotTmpFallback[slotIndex] == null)
{
slotLegacyFallback[slotIndex] = parent.GetComponentInChildren<Text>(true);
}
slotFallbackResolved[slotIndex] = true;
}
/// <summary>
/// Add a per-note pm score delta to the specified track (0..4) and update named fields + aggregate.
/// Also calculates idol score as pmDelta * scoreEfficiency.
/// </summary>
public void AddPmScoreForTrack(int trackIndex, int pmDelta, float scoreEfficiency, bool isSkillRelated = false)
{
TryHookPerfectBonusEvent();
if (trackIndex < 0 || trackIndex >= pmScoreSums.Length) return;
if (pmDelta == 0) return;
int pmBefore = pmScoreSums[trackIndex];
pmScoreSums[trackIndex] += pmDelta;
// clamp at int.MaxValue-1 to avoid overflow
if (pmScoreSums[trackIndex] < 0) pmScoreSums[trackIndex] = 0;
if (pmScoreSums[trackIndex] > int.MaxValue - 1) pmScoreSums[trackIndex] = int.MaxValue - 1;
int pmActualDelta = pmScoreSums[trackIndex] - pmBefore;
int idolBefore = idolScoreSums[trackIndex];
int idolDelta = Mathf.FloorToInt(pmDelta * scoreEfficiency);
idolScoreSums[trackIndex] += idolDelta;
if (idolScoreSums[trackIndex] < 0) idolScoreSums[trackIndex] = 0;
if (idolScoreSums[trackIndex] > int.MaxValue - 1) idolScoreSums[trackIndex] = int.MaxValue - 1;
int idolActualDelta = idolScoreSums[trackIndex] - idolBefore;
// Spawn floating number popup for idol score change
if (idolActualDelta != 0)
{
var type = idolActualDelta > 0 ? iNumberPrefabController.InstantNumberType.IscorePlus : iNumberPrefabController.InstantNumberType.IscoreMinus;
iNumberPrefabController.SpawnForAllyStatic(trackIndex, type, idolActualDelta);
}
// update named fields for easy access
red_pmScore_sum = pmScoreSums[0];
green_pmScore_sum = pmScoreSums[1];
yellow_pmScore_sum = pmScoreSums[2];
purple_pmScore_sum = pmScoreSums[3];
blue_pmScore_sum = pmScoreSums[4];
red_idolScore_sum = idolScoreSums[0];
green_idolScore_sum = idolScoreSums[1];
yellow_idolScore_sum = idolScoreSums[2];
purple_idolScore_sum = idolScoreSums[3];
blue_idolScore_sum = idolScoreSums[4];
// recompute aggregates
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);
if (JudgeManager.IsDebugEnabled)
Debug.Log($"[ScoreManager] Added {pmDelta} to track {trackIndex} pm sum, idol delta {idolDelta}. New pm sums: R{red_pmScore_sum} G{green_pmScore_sum} Y{yellow_pmScore_sum} P{purple_pmScore_sum} B{blue_pmScore_sum} -> allSumPm={allSum_pmScore}. Idol sums: R{red_idolScore_sum} G{green_idolScore_sum} Y{yellow_idolScore_sum} P{purple_idolScore_sum} B{blue_idolScore_sum} -> allSumIdol={allSum_idolScore}");
UpdateRecentPlusAmountUi(pmActualDelta + idolActualDelta);
// Update UI in teamUIController if available (per-track pm sums + aggregate)
var ui = teamUIController.Instance;
if (ui != null)
{
// Legacy Text fields on teamUIController
// 性能:仅在数值变化时写(SetSumTextIfChanged),避免每判定重写 12 个槽位触发 TMP 网格重建。
try
{
SetSumTextIfChanged(ui.red_pmScore_sum, red_pmScore_sum, 0);
SetSumTextIfChanged(ui.green_pmScore_sum, green_pmScore_sum, 1);
SetSumTextIfChanged(ui.yellow_pmScore_sum, yellow_pmScore_sum, 2);
SetSumTextIfChanged(ui.purple_pmScore_sum, purple_pmScore_sum, 3);
SetSumTextIfChanged(ui.blue_pmScore_sum, blue_pmScore_sum, 4);
SetSumTextIfChanged(ui.allSum_pmScore, allSum_pmScore, 5);
SetSumTextIfChanged(ui.red_idolScore_sum, red_idolScore_sum, 6);
SetSumTextIfChanged(ui.green_idolScore_sum, green_idolScore_sum, 7);
SetSumTextIfChanged(ui.yellow_idolScore_sum, yellow_idolScore_sum, 8);
SetSumTextIfChanged(ui.purple_idolScore_sum, purple_idolScore_sum, 9);
SetSumTextIfChanged(ui.blue_idolScore_sum, blue_idolScore_sum, 10);
SetSumTextIfChanged(ui.allSum_idolScore, allSum_idolScore, 11);
}
catch (System.Exception ex)
{
if (JudgeManager.IsDebugEnabled) Debug.LogWarning($"[ScoreManager] Failed to write sums to teamUIController fields: {ex}");
}
}
// Optionally update any UI for pm sums here if teamUIController exposes fields (not implemented by default)
// Ensure total is recalculated when pm/idol aggregates change so totalScore reflects pm + idol
RecalculateTotal();
// Update per-track progress keys (bgSPRITE/idolscoreKey_*).
UpdateIdolscoreKeyScales();
GameplaySkillLogger.RecordScoreDelta(
"AddPmScoreForTrack",
trackIndex,
pmDelta,
pmActualDelta,
idolDelta,
idolActualDelta,
scoreEfficiency,
isSkillRelated,
pmScoreSums[trackIndex],
idolScoreSums[trackIndex],
allSum_pmScore,
allSum_idolScore,
totalScore);
}
/// <summary>
/// Add idol score directly to a track (0..4) without changing pm score.
/// Used by skills that grant/consume idol score independently from note judgement pm.
/// </summary>
public void AddIdolScoreForTrack(int trackIndex, int idolDelta)
{
TryHookPerfectBonusEvent();
if (trackIndex < 0 || trackIndex >= idolScoreSums.Length) return;
if (idolDelta == 0) return;
int idolBefore = idolScoreSums[trackIndex];
idolScoreSums[trackIndex] += idolDelta;
if (idolScoreSums[trackIndex] < 0) idolScoreSums[trackIndex] = 0;
if (idolScoreSums[trackIndex] > int.MaxValue - 1) idolScoreSums[trackIndex] = int.MaxValue - 1;
int actual = idolScoreSums[trackIndex] - idolBefore;
if (actual != 0)
{
var type = actual > 0 ? iNumberPrefabController.InstantNumberType.IscorePlus : iNumberPrefabController.InstantNumberType.IscoreMinus;
iNumberPrefabController.SpawnForAllyStatic(trackIndex, type, actual);
}
UpdateRecentPlusAmountUi(actual);
red_idolScore_sum = idolScoreSums[0];
green_idolScore_sum = idolScoreSums[1];
yellow_idolScore_sum = idolScoreSums[2];
purple_idolScore_sum = idolScoreSums[3];
blue_idolScore_sum = idolScoreSums[4];
long aggPm = 0;
long aggIdol = 0;
for (int i = 0; i < pmScoreSums.Length; i++) aggPm += pmScoreSums[i];
for (int i = 0; i < idolScoreSums.Length; i++) aggIdol += idolScoreSums[i];
long aggPmWithBonus = aggPm + (long)perfectClearBonusPm;
allSum_pmScore = (int)Mathf.Min((float)aggPmWithBonus, (float)int.MaxValue - 1f);
allSum_idolScore = (int)Mathf.Min((float)aggIdol, (float)int.MaxValue - 1f);
var ui = teamUIController.Instance;
if (ui != null)
{
try
{
if (ui.red_idolScore_sum != null) ui.red_idolScore_sum.text = red_idolScore_sum.ToString();
if (ui.green_idolScore_sum != null) ui.green_idolScore_sum.text = green_idolScore_sum.ToString();
if (ui.yellow_idolScore_sum != null) ui.yellow_idolScore_sum.text = yellow_idolScore_sum.ToString();
if (ui.purple_idolScore_sum != null) ui.purple_idolScore_sum.text = purple_idolScore_sum.ToString();
if (ui.blue_idolScore_sum != null) ui.blue_idolScore_sum.text = blue_idolScore_sum.ToString();
if (ui.allSum_idolScore != null) ui.allSum_idolScore.text = allSum_idolScore.ToString();
}
catch (System.Exception ex)
{
if (JudgeManager.IsDebugEnabled) Debug.LogWarning($"[ScoreManager] Failed to write idol sums to teamUIController fields: {ex}");
}
}
RecalculateTotal();
GameplaySkillLogger.RecordScoreDelta(
"AddIdolScoreForTrack",
trackIndex,
0,
0,
idolDelta,
actual,
float.NaN,
true,
pmScoreSums[trackIndex],
idolScoreSums[trackIndex],
allSum_pmScore,
allSum_idolScore,
totalScore);
}
public void RecalculateTotal()
{
EnsureAllyCache();
for (int i = 0; i < 5; i++)
{
var ally = allyCombatants[i];
if (ally != null)
{
tmpCurrents[i] = ally.currentScore;
tmpMaxes[i] = ally.maxTrackScore;
}
else
{
tmpCurrents[i] = 0;
tmpMaxes[i] = 0;
}
}
// totalScore should include both pm (per-note) and idol aggregates maintained by ScoreManager
long combined = (long)allSum_pmScore + (long)allSum_idolScore;
// clamp to int range
totalScore = (int)Mathf.Min((float)combined, (float)int.MaxValue - 1f);
// update UI if available
if (teamUIController.Instance != null)
{
var ui = teamUIController.Instance;
// helper lambda to set text on TMP or legacy Text under a parent fallback
void SetScoreText(TextMeshProUGUI assignedTmp, GameObject parent, int cur, int max, string slotName)
{
string value = cur + "/" + max;
if (assignedTmp != null)
{
assignedTmp.text = value;
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to assigned TMP for {slotName}: '{value}' -> {assignedTmp.gameObject.name}");
return;
}
if (parent != null)
{
int idx = slotName == "teammate01" ? 0 :
slotName == "teammate02" ? 1 :
slotName == "teammate03" ? 2 :
slotName == "teammate04" ? 3 :
slotName == "teammate05" ? 4 : -1;
if (idx >= 0)
{
ResolveSlotFallbackUi(idx, parent);
var tmp = slotTmpFallback[idx];
if (tmp != null)
{
tmp.text = value;
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to TMP in parent {parent.name} for {slotName}: '{value}' -> {tmp.gameObject.name}");
return;
}
var legacy = slotLegacyFallback[idx];
if (legacy != null)
{
legacy.text = value;
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to legacy Text in parent {parent.name} for {slotName}: '{value}' -> {legacy.gameObject.name}");
return;
}
}
else
{
var tmp = parent.GetComponentInChildren<TextMeshProUGUI>(true);
if (tmp != null)
{
tmp.text = value;
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to TMP in parent {parent.name} for {slotName}: '{value}' -> {tmp.gameObject.name}");
return;
}
var legacy = parent.GetComponentInChildren<Text>(true);
if (legacy != null)
{
legacy.text = value;
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to legacy Text in parent {parent.name} for {slotName}: '{value}' -> {legacy.gameObject.name}");
return;
}
}
}
if (JudgeManager.IsDebugEnabled) Debug.LogWarning($"[ScoreManager] Could not find UI to write score for {slotName}. assignedTmp is null and parent '{parent?.name}' has no TMP/Text children.");
}
// Update each slot: use the explicit TMP fields if set; otherwise try parent object fields
SetScoreText(ui.teammate01_current_scoreText, ui.objectFather_ally01, tmpCurrents[0], tmpMaxes[0], "teammate01");
SetScoreText(ui.teammate02_current_scoreText, ui.objectFather_ally02, tmpCurrents[1], tmpMaxes[1], "teammate02");
SetScoreText(ui.teammate03_current_scoreText, ui.objectFather_ally03, tmpCurrents[2], tmpMaxes[2], "teammate03");
SetScoreText(ui.teammate04_current_scoreText, ui.objectFather_ally04, tmpCurrents[3], tmpMaxes[3], "teammate04");
SetScoreText(ui.teammate05_current_scoreText, ui.objectFather_ally05, tmpCurrents[4], tmpMaxes[4], "teammate05");
// update total score (try TMP first, then legacy Text)
if (ui.currentTotalScore != null)
{
var go = ui.currentTotalScore.gameObject;
if (go != null)
{
// 性能:仅在目标物体变化时 GetComponent(缓存),避免每次判定重复反射查找。
if (_cachedTotalGo != go)
{
_cachedTotalGo = go;
_cachedTotalTmp = go.GetComponent<TextMeshProUGUI>();
_cachedTotalLegacy = _cachedTotalTmp == null ? go.GetComponent<Text>() : null;
_lastTotalText = null; // 目标物体变了(如新一局/换场景),强制写一次
}
string totalText = totalScore.ToString();
// 性能:值未变则跳过 .text 赋值,避免 TMP 无谓网格重建。
if (totalText != _lastTotalText)
{
_lastTotalText = totalText;
if (_cachedTotalTmp != null)
{
_cachedTotalTmp.text = totalText;
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote total to TMP: {totalScore} -> {_cachedTotalTmp.gameObject.name}");
}
else if (_cachedTotalLegacy != null)
{
_cachedTotalLegacy.text = totalText;
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote total to legacy Text: {totalScore} -> {_cachedTotalLegacy.gameObject.name}");
}
else
{
ui.currentTotalScore.text = totalText; // fallback
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote total to currentTotalScore field fallback: {totalScore}");
}
}
}
else
{
ui.currentTotalScore.text = totalScore.ToString();
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote total to currentTotalScore fallback (no go): {totalScore}");
}
}
else
{
if (JudgeManager.IsDebugEnabled) Debug.LogWarning("[ScoreManager] teamUIController.currentTotalScore is null; cannot display total score.");
}
if (ui.currentScoreRankImage != null && ui.currentScoreRankConfig != null)
{
ui.currentScoreRankImage.sprite = ui.currentScoreRankConfig.GetRankSprite(totalScore);
}
}
else
{
if (JudgeManager.IsDebugEnabled) Debug.LogWarning("[ScoreManager] teamUIController.Instance is null; cannot update UI.");
}
}
public void ApplyExternalTotalScoreDelta(int delta)
{
if (delta == 0)
{
return;
}
long next = (long)totalScore + delta;
if (next < 0) next = 0;
if (next > int.MaxValue - 1L) next = int.MaxValue - 1L;
totalScore = (int)next;
UpdateRecentPlusAmountUi(delta);
RefreshAllScoreUi();
}
private void UpdateRecentPlusAmountUi(int scoreDelta)
{
var ui = teamUIController.Instance;
if (ui == null || ui.recentPlusAmountText == null)
{
return;
}
int comboNow = ui.CurrentCombo;
bool comboBroken = comboNow <= 0;
bool comboRestarted = comboNow > 0 && lastKnownCombo <= 0;
if (comboBroken)
{
ResetRecentPlusAmountState(ui, true);
lastKnownCombo = comboNow;
return;
}
if (comboRestarted)
{
ResetRecentPlusAmountState(ui, false);
}
if (scoreDelta != 0)
{
comboRecentPlusAmount += scoreDelta;
lastRecentPlusScoreTime = Time.unscaledTime;
}
ui.ResetRecentPlusAmountVisual();
AnimateRecentPlusAmountText(ui, comboRecentPlusAmount);
lastKnownCombo = comboNow;
}
public void RefreshAllScoreUi()
{
var ui = teamUIController.Instance;
if (ui == null)
{
return;
}
if (ui.currentTotalScore != null)
{
ui.currentTotalScore.text = totalScore.ToString();
}
if (ui.currentScoreRankImage != null && ui.currentScoreRankConfig != null)
{
ui.currentScoreRankImage.sprite = ui.currentScoreRankConfig.GetRankSprite(totalScore);
}
}
private void AnimateRecentPlusAmountText(teamUIController ui, int targetValue)
{
if (ui == null || ui.recentPlusAmountText == null)
{
return;
}
KillRecentPlusAmountValueTween();
int startValue = displayedRecentPlusAmount;
if (startValue == targetValue)
{
ui.recentPlusAmountText.text = FormatSignedScoreDelta(targetValue);
return;
}
recentPlusAmountValueTween = DOTween
.To(() => startValue, value =>
{
startValue = value;
displayedRecentPlusAmount = value;
ui.recentPlusAmountText.text = FormatSignedScoreDelta(value);
}, targetValue, 0.2f)
.SetEase(Ease.OutQuad)
.SetUpdate(true)
.OnComplete(() =>
{
displayedRecentPlusAmount = targetValue;
if (ui.recentPlusAmountText != null)
{
ui.recentPlusAmountText.text = FormatSignedScoreDelta(targetValue);
}
recentPlusAmountValueTween = null;
});
}
private void KillRecentPlusAmountValueTween()
{
if (recentPlusAmountValueTween == null)
{
return;
}
recentPlusAmountValueTween.Kill();
recentPlusAmountValueTween = null;
}
private void UpdateRecentPlusAmountTimeout()
{
if (comboRecentPlusAmount == 0)
{
return;
}
if (Time.unscaledTime - lastRecentPlusScoreTime < RecentPlusResetDelaySeconds)
{
return;
}
ResetRecentPlusAmountState(teamUIController.Instance, false);
}
private void ResetRecentPlusAmountState(teamUIController ui, bool killTweenOnly)
{
comboRecentPlusAmount = 0;
displayedRecentPlusAmount = 0;
lastRecentPlusScoreTime = float.NegativeInfinity;
KillRecentPlusAmountValueTween();
if (ui == null || ui.recentPlusAmountText == null)
{
return;
}
ui.recentPlusAmountText.text = FormatSignedScoreDelta(0);
if (killTweenOnly)
{
return;
}
}
private static string FormatSignedScoreDelta(int value)
{
return value >= 0 ? $"+{value}" : value.ToString();
}
}