using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class JudgeManager : MonoBehaviour { [Header("settlement go")] public GameObject settlement_go; public settlementController sc; public NoteSpawner ns; [Header("Settlement Delay")] [SerializeField] private float settlementEnterDelaySeconds = 1.5f; private bool _cachedIsDebugEnabled; private void UpdateDebugCache() { _cachedIsDebugEnabled = (_enableDebugLogs || GameConfig.verboseLogs); } [Header("Debug Settings")] [SerializeField] private bool _enableDebugLogs = false; public bool EnableDebugLogs => _enableDebugLogs; public static bool IsDebugEnabled => (Instance != null && Instance._cachedIsDebugEnabled); [Header("Global Note Settings")] [Tooltip("Global material applied to hold notes when they are judged.")] public Material globalJudgedMaterial; [Tooltip("Whether to enable material switching after a hold note is judged.")] public bool enableJudgedMaterial = false; public static JudgeManager Instance { get; private set; } // total/remaining notes tracking for end-of-song detection private int totalNotes = 0; private int remainingNotes = 0; private bool settlementTriggeredThisChart = false; // Documentation text normalized. private Dictionary> judgeQueues = new Dictionary>(); private Dictionary startJudgedNotes = new Dictionary(); private Dictionary releasedNotes = new Dictionary(); private Dictionary noteEndTimes = new Dictionary(); private Dictionary middlePassedCounts = new Dictionary(); // Documentation text normalized. private class HoldJudgeState { public bool startResolved; // Documentation text normalized. public bool endResolved; // Documentation text normalized. public bool skillTriggered; // Documentation text normalized. } private Dictionary holdStates = new Dictionary(); private HoldJudgeState GetHoldState(string noteID) { if (!holdStates.TryGetValue(noteID, out var state)) { state = new HoldJudgeState(); holdStates[noteID] = state; } return state; } // Public event and hook invoked when the entire beatmap's last note has been judged. // The method is intentionally empty so you can override or call it via the singleton. public event Action AllNotesJudged; /// /// Public empty hook called when all notes have been judged. Implement your end-of-song logic here /// or subscribe to the AllNotesJudged event. This method is intentionally left empty. /// public void OnAllNotesJudged() { if (settlementTriggeredThisChart) { if (IsDebugEnabled) Debug.Log("[JudgeManager] OnAllNotesJudged ignored: settlement already triggered for this chart."); return; } settlementTriggeredThisChart = true; if (settlement_go == null) { if (IsDebugEnabled) Debug.LogError("结算页面不存在!"); return; } // Enter settlement after a real-time delay to leave a short idle window after chart end. StartCoroutine(InvokeSettlementUpdateNextFrame()); } private IEnumerator InvokeSettlementUpdateNextFrame() { // Keep settlement lead-in fixed at 1.5s for consistent pacing. float delay = 1.5f; if (delay > 0f) { yield return new WaitForSecondsRealtime(delay); } // Activate settlement UI so components run their lifecycle. try { settlement_go.SetActive(true); } catch (Exception ex) { if (IsDebugEnabled) Debug.LogWarning($"[JudgeManager] Failed to activate settlement_go: {ex}"); } // Try to resolve settlementController reference if missing. if (sc == null && settlement_go != null) { sc = settlement_go.GetComponent() ?? settlement_go.GetComponentInChildren(true); } // Wait one frame to allow Awake/Start/OnEnable. yield return null; if (sc != null) { try { sc.startSettlement_uiUpdate(); } catch (Exception ex) { if (IsDebugEnabled) Debug.LogWarning($"[JudgeManager] Exception calling startSettlement_uiUpdate: {ex}"); } } else { if (IsDebugEnabled) Debug.LogWarning("[JudgeManager] settlementController (sc) not found on settlement_go; cannot call startSettlement_uiUpdate()."); } } /// /// Helper to invoke the AllNotesJudged event and call the public hook. /// Call this when you detect the last note was judged. /// public void TriggerAllNotesJudged() { try { AllNotesJudged?.Invoke(); } catch (Exception ex) { if (IsDebugEnabled) Debug.LogWarning($"[JudgeManager] Exception while invoking AllNotesJudged event: {ex}"); } try { OnAllNotesJudged(); } catch (Exception ex) { if (IsDebugEnabled) Debug.LogWarning($"[JudgeManager] Exception in OnAllNotesJudged hook: {ex}"); } } private void Awake() { if (Instance == null) Instance = this; else Destroy(gameObject); UpdateDebugCache(); if (settlement_go != null) settlement_go.SetActive(false); } /// /// Set the total number of notes in the current beatmap. Remaining notes will be initialized /// to this value. Call before playback/spawning starts. /// public void SetTotalNotes(int total) { totalNotes = Mathf.Max(0, total); remainingNotes = totalNotes; settlementTriggeredThisChart = false; if (IsDebugEnabled) Debug.Log($"[JudgeManager] SetTotalNotes: total={totalNotes}"); } /// /// Notify that one note has been finally judged (hit or miss). When remaining reaches zero, /// TriggerAllNotesJudged will be invoked. /// public void NotifyNoteJudged() { if (remainingNotes <= 0) return; remainingNotes = Mathf.Max(0, remainingNotes - 1); if (IsDebugEnabled) Debug.Log($"[JudgeManager] NotifyNoteJudged: remaining={remainingNotes}"); if (remainingNotes == 0) { if (IsDebugEnabled) Debug.Log("[JudgeManager] All notes judged -> Triggering AllNotesJudged"); TriggerAllNotesJudged(); } } // Documentation text normalized. public bool TryResolveStart(string noteID) { var s = GetHoldState(noteID); if (s.startResolved) { if (IsDebugEnabled) Debug.LogWarning($"[JudgeManager] TryResolveStart: already resolved start for {noteID}"); return false; } s.startResolved = true; if (IsDebugEnabled) Debug.Log($"[JudgeManager] TryResolveStart: start resolved for {noteID} at time={GameplayClock.NowSongTime:F3}"); return true; } public bool TryResolveEnd(string noteID) { var s = GetHoldState(noteID); if (s.endResolved) { if (IsDebugEnabled) Debug.LogWarning($"[JudgeManager] TryResolveEnd: already resolved end for {noteID}"); return false; } s.endResolved = true; if (IsDebugEnabled) Debug.Log($"[JudgeManager] TryResolveEnd: end resolved for {noteID} at time={GameplayClock.NowSongTime:F3}"); return true; } public bool TryTriggerSkill(string noteID) { var s = GetHoldState(noteID); if (s.skillTriggered) { if (IsDebugEnabled) Debug.LogWarning($"[JudgeManager] TryTriggerSkill: already triggered for {noteID}"); return false; } s.skillTriggered = true; if (IsDebugEnabled) Debug.Log($"[JudgeManager] TryTriggerSkill: skill triggered for {noteID} at time={GameplayClock.NowSongTime:F3}"); return true; } public void ClearHoldState(string noteID) { if (holdStates.ContainsKey(noteID)) { holdStates.Remove(noteID); if (IsDebugEnabled) Debug.Log($"[JudgeManager] ClearHoldState: cleared state for {noteID}"); } } // Documentation text normalized. public void RegisterScheduledEndTime(string noteID, float endTime) { noteEndTimes[noteID] = endTime; if (IsDebugEnabled) Debug.Log($"[JudgeManager] RegisterScheduledEndTime: {noteID} -> {endTime:F3}"); } public float GetScheduledEndTime(string noteID) { return noteEndTimes.TryGetValue(noteID, out var endTime) ? endTime : 0f; } public void RegisterStartJudged(string noteID, bool state) { startJudgedNotes[noteID] = state; if (IsDebugEnabled) Debug.Log($"[JudgeManager] RegisterStartJudged: {noteID} = {state} at time={GameplayClock.NowSongTime:F3}"); } public bool IsStartJudged(string noteID) { return startJudgedNotes.TryGetValue(noteID, out var state) && state; } public void RegisterNoteReleased(string noteID, bool state) { releasedNotes[noteID] = state; if (IsDebugEnabled) Debug.Log($"[JudgeManager] RegisterNoteReleased: {noteID} = {state}"); } public bool HasNoteReleased(string noteID) { return releasedNotes.TryGetValue(noteID, out var state) && state; } public void RegisterNote(KeyCode key, Note note) { if (!judgeQueues.ContainsKey(key)) judgeQueues[key] = new Queue(); if (!judgeQueues[key].Contains(note)) judgeQueues[key].Enqueue(note); if (IsDebugEnabled) Debug.Log($"[JudgeManager] RegisterNote: key={key} note={note.name} time={GameplayClock.NowSongTime:F3} queueSize={judgeQueues[key].Count}"); } public void UnregisterNote(KeyCode key, Note note) { if (!judgeQueues.ContainsKey(key)) return; Queue newQueue = new Queue(); while (judgeQueues[key].Count > 0) { Note n = judgeQueues[key].Dequeue(); if (n != note) newQueue.Enqueue(n); else if (!n.IsJudged()) n.JudgeMiss(); } judgeQueues[key] = newQueue; if (IsDebugEnabled) Debug.Log($"[JudgeManager] UnregisterNote: key={key} removed {note.name}, newQueueSize={judgeQueues[key].Count}"); } public void JudgeEarliestNote(KeyCode key) { if (judgeQueues.ContainsKey(key) && judgeQueues[key].Count > 0) { Note note = judgeQueues[key].Dequeue(); if (note != null && !note.IsJudged()) { if (IsDebugEnabled) Debug.Log($"[JudgeManager] JudgeEarliestNote: judging {note.name} for key={key} at time={GameplayClock.NowSongTime:F3}"); note.Judge(); } } } public void RegisterMiddlePassed(string noteID) { if (middlePassedCounts.TryGetValue(noteID, out int count)) { middlePassedCounts[noteID] = count + 1; } else { middlePassedCounts[noteID] = 1; } if (IsDebugEnabled) Debug.Log($"[JudgeManager] Middle passed for {noteID}, total={middlePassedCounts[noteID]}"); } // Event for syncing alpha across segments of a hold note public event Action OnHoldAlphaSync; public void SyncHoldAlpha(string noteID, float alpha) { OnHoldAlphaSync?.Invoke(noteID, alpha); } public int GetMiddlePassedCount(string noteID) { return middlePassedCounts.TryGetValue(noteID, out var count) ? count : 0; } public void ClearNoteRecord(string noteID) { startJudgedNotes.Remove(noteID); releasedNotes.Remove(noteID); noteEndTimes.Remove(noteID); middlePassedCounts.Remove(noteID); ClearHoldState(noteID); if (IsDebugEnabled) Debug.Log($"[JudgeManager] ClearNoteRecord: cleared records for {noteID}"); } }