360 lines
12 KiB
C#
360 lines
12 KiB
C#
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<KeyCode, Queue<Note>> judgeQueues = new Dictionary<KeyCode, Queue<Note>>();
|
|
private Dictionary<string, bool> startJudgedNotes = new Dictionary<string, bool>();
|
|
private Dictionary<string, bool> releasedNotes = new Dictionary<string, bool>();
|
|
private Dictionary<string, float> noteEndTimes = new Dictionary<string, float>();
|
|
private Dictionary<string, int> middlePassedCounts = new Dictionary<string, int>();
|
|
|
|
// 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<string, HoldJudgeState> holdStates = new Dictionary<string, HoldJudgeState>();
|
|
|
|
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<settlementController>() ?? settlement_go.GetComponentInChildren<settlementController>(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().");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to invoke the AllNotesJudged event and call the public hook.
|
|
/// Call this when you detect the last note was judged.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set the total number of notes in the current beatmap. Remaining notes will be initialized
|
|
/// to this value. Call before playback/spawning starts.
|
|
/// </summary>
|
|
public void SetTotalNotes(int total)
|
|
{
|
|
totalNotes = Mathf.Max(0, total);
|
|
remainingNotes = totalNotes;
|
|
settlementTriggeredThisChart = false;
|
|
if (IsDebugEnabled) Debug.Log($"[JudgeManager] SetTotalNotes: total={totalNotes}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notify that one note has been finally judged (hit or miss). When remaining reaches zero,
|
|
/// TriggerAllNotesJudged will be invoked.
|
|
/// </summary>
|
|
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<Note>();
|
|
|
|
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<Note> newQueue = new Queue<Note>();
|
|
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<string, float> 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}");
|
|
}
|
|
}
|