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 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; } /// /// Records a timing offset in milliseconds. /// Positive = Early, Negative = Late. /// 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(); 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(); 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(); 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() { // Try to resolve missing refs (GameObject.Find doesn't return inactive objects). 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(); } } 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(true); if (slotTmpFallback[slotIndex] == null) { slotLegacyFallback[slotIndex] = parent.GetComponentInChildren(true); } slotFallbackResolved[slotIndex] = true; } /// /// 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. /// 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 try { string redPmText = red_pmScore_sum.ToString(); string greenPmText = green_pmScore_sum.ToString(); string yellowPmText = yellow_pmScore_sum.ToString(); string purplePmText = purple_pmScore_sum.ToString(); string bluePmText = blue_pmScore_sum.ToString(); string allPmText = allSum_pmScore.ToString(); string redIdolText = red_idolScore_sum.ToString(); string greenIdolText = green_idolScore_sum.ToString(); string yellowIdolText = yellow_idolScore_sum.ToString(); string purpleIdolText = purple_idolScore_sum.ToString(); string blueIdolText = blue_idolScore_sum.ToString(); string allIdolText = allSum_idolScore.ToString(); if (ui.red_pmScore_sum != null) ui.red_pmScore_sum.text = redPmText; if (ui.green_pmScore_sum != null) ui.green_pmScore_sum.text = greenPmText; if (ui.yellow_pmScore_sum != null) ui.yellow_pmScore_sum.text = yellowPmText; if (ui.purple_pmScore_sum != null) ui.purple_pmScore_sum.text = purplePmText; if (ui.blue_pmScore_sum != null) ui.blue_pmScore_sum.text = bluePmText; if (ui.allSum_pmScore != null) ui.allSum_pmScore.text = allPmText; if (ui.red_idolScore_sum != null) ui.red_idolScore_sum.text = redIdolText; if (ui.green_idolScore_sum != null) ui.green_idolScore_sum.text = greenIdolText; if (ui.yellow_idolScore_sum != null) ui.yellow_idolScore_sum.text = yellowIdolText; if (ui.purple_idolScore_sum != null) ui.purple_idolScore_sum.text = purpleIdolText; if (ui.blue_idolScore_sum != null) ui.blue_idolScore_sum.text = blueIdolText; if (ui.allSum_idolScore != null) ui.allSum_idolScore.text = allIdolText; } 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); } /// /// 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. /// 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(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(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) { var tmpComp = go.GetComponent(); if (tmpComp != null) { tmpComp.text = totalScore.ToString(); if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote total to TMP: {totalScore} -> {tmpComp.gameObject.name}"); } else { var legacyComp = go.GetComponent(); if (legacyComp != null) { legacyComp.text = totalScore.ToString(); if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote total to legacy Text: {totalScore} -> {legacyComp.gameObject.name}"); } else { ui.currentTotalScore.text = totalScore.ToString(); // 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(); } }