using UnityEngine;
using System;
using System.Collections;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using GameServer.Client;
using Bansonic;
#if UNITY_EDITOR
using UnityEditor;
#endif
///
/// Central gameplay pause manager.
/// - ESC / top-right Pause button can pause only in gameplay scene.
/// - Continue: hide pause panel, replay 3-2-1-Go, then resume chart + music.
/// - Replay: reload current gameplay scene (with current selected song/difficulty if available).
/// - Exit: return to selectYourSongFirst.
///
public class PauseManager : MonoBehaviour
{
public static PauseManager Instance { get; private set; }
[Header("Overlay")]
public GameObject overlayRoot;
[SerializeField] private float overlayFadeDuration = 0.2f;
[SerializeField] private bool preferPausePrefabOverWhiteScreen = true;
[SerializeField] private string legacyWhiteScreenRootName = "pauseWhiteScreen";
[Header("Gameplay Scope")]
[SerializeField] private bool onlyInGameplayScene = true;
[SerializeField] private string gameplaySceneName = "gamePlay_gamePlay";
[SerializeField] private bool requirePlaybackStarted = true;
[Header("Buttons")]
[SerializeField] private string pauseButtonScenePath = "artworks/NEW UI/zhuangshi (3)/Pause";
[Tooltip("手游暂停按钮:把场景里的暂停 Button 拖到这里,点击效果等同 ESC。\n" +
"留空则按 pauseButtonScenePath 或名为 \"Pause\" 的按钮自动查找。")]
[SerializeField] private Button pauseTriggerButton;
[SerializeField] private Button continueButton;
[SerializeField] private Button replayButton;
[SerializeField] private Button exitButton;
[SerializeField] private Button settingsButton;
[Header("Settings Prefab")]
[SerializeField] private GameObject settingsPrefab;
[Header("Pause Entry Animation")]
[SerializeField] private bool playPauseEntryAnimation = true;
[SerializeField] private float pauseItemEnterDuration = 0.18f;
[SerializeField] private float pauseItemEnterStagger = 0.016f;
public bool IsPaused { get; private set; } = false;
public event Action OnPauseStateChanged;
private CanvasGroup overlayCanvasGroup;
private RectTransform overlayPanelRoot;
private CanvasGroup overlayPanelCanvasGroup;
private Vector3 panelShownScale = Vector3.zero;
private static readonly Vector3 PanelHiddenScale = Vector3.zero;
private Coroutine overlayFadeCoroutine;
private Coroutine continueCoroutine;
private bool buttonsBound = false;
private float nextBindRetryTime = 0f;
private const string ExitSceneName = "selectYourSongFirst";
void Awake()
{
if (Instance == null) Instance = this;
else
{
Destroy(gameObject);
return;
}
}
void Start()
{
ResolveOverlayRootForPausePrefab();
EnsureOverlayCanvasGroup();
EnsureOverlayPanelRoot();
// Ensure CanvasGroup is initialized to 0 alpha, but keep interactable/blocksRaycasts true
if (overlayCanvasGroup != null)
{
overlayCanvasGroup.alpha = 0f;
overlayCanvasGroup.interactable = true;
overlayCanvasGroup.blocksRaycasts = true;
}
// Initialize buttons but DO NOT call SetOverlayVisibleImmediate(false) here
// because it would reset the flags we just set.
// Instead, we manually ensure the object is active if that's the desired initial state,
// or just rely on the alpha=0 to hide it.
// Assuming user wants the object active but transparent:
if (overlayRoot != null) overlayRoot.SetActive(true);
TryBindButtons(force: true);
}
void Update()
{
if (!buttonsBound && Time.unscaledTime >= nextBindRetryTime)
{
TryBindButtons(force: false);
}
if (Input.GetKeyDown(KeyCode.Delete))
{
#if UNITY_EDITOR
EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
return;
}
if (Input.GetKeyDown(KeyCode.Escape))
{
// User requested ESC as pause trigger in gameplay only.
if (CanPauseFromInput())
{
if (!IsPaused)
{
Pause(true);
}
}
}
// Keep existing Backspace quick-exit while paused.
if (IsPaused && Input.GetKeyDown(KeyCode.Backspace))
{
ResumeImmediately(true);
StartCoroutine(LoadSceneAsync("Main_main"));
}
}
public void Pause(bool pause)
{
if (pause)
{
if (IsPaused) return;
IsPaused = true;
Time.timeScale = 0f;
GameplayClock.Pause();
OnPauseStateChanged?.Invoke(true);
// Startup flows call Pause(true) before real gameplay starts.
// In that phase we keep time paused but do not show pause UI.
// Calculate allowOverlay WITHOUT checking IsPaused (since we just set it to true).
bool allowOverlay = true;
if (onlyInGameplayScene && !IsGameplaySceneActive()) allowOverlay = false;
else if (requirePlaybackStarted)
{
var gm = SceneObjectLookupCache.FindAny();
// If GM missing or playback not started, don't show overlay yet
if (gm != null && !gm.PlaybackStarted) allowOverlay = false;
}
if (allowOverlay)
{
ShowOverlayAnimated(true);
}
else
{
// Startup Pause State:
// Keep object active and blocking raycasts (per user request), but invisible.
if (overlayRoot != null) overlayRoot.SetActive(true);
if (overlayCanvasGroup != null)
{
overlayCanvasGroup.alpha = 0f;
overlayCanvasGroup.interactable = true;
overlayCanvasGroup.blocksRaycasts = true;
}
if (overlayPanelCanvasGroup != null)
{
overlayPanelCanvasGroup.alpha = 0f;
overlayPanelCanvasGroup.interactable = true;
overlayPanelCanvasGroup.blocksRaycasts = true;
}
if (overlayPanelRoot != null)
overlayPanelRoot.localScale = PanelHiddenScale;
}
}
else
{
if (!IsPaused) return;
IsPaused = false;
Time.timeScale = 1f;
GameplayClock.Resume();
// Re-anchor clock to actual music progress to correct any drift during pause
var gm = SceneObjectLookupCache.FindAny();
if (gm != null && gm.musicSource != null && gm.musicSource.isPlaying)
{
GameplayClock.ReanchorToMusicTime(gm.musicSource.time);
}
OnPauseStateChanged?.Invoke(false);
ShowOverlayAnimated(false);
}
}
private void ResumeImmediately(bool hideOverlay)
{
if (!IsPaused)
{
if (hideOverlay)
{
SetOverlayVisibleImmediate(false);
}
return;
}
IsPaused = false;
Time.timeScale = 1f;
GameplayClock.Resume();
// Re-anchor clock to actual music progress to correct any drift during pause
var gm = SceneObjectLookupCache.FindAny();
if (gm != null && gm.musicSource != null && gm.musicSource.isPlaying)
{
GameplayClock.ReanchorToMusicTime(gm.musicSource.time);
}
OnPauseStateChanged?.Invoke(false);
if (hideOverlay)
{
SetOverlayVisibleImmediate(false);
}
else
{
ShowOverlayAnimated(false);
}
}
public void TogglePause()
{
if (!CanPauseFromInput()) return;
if (!IsPaused) Pause(true);
}
public void ContinueFromPause()
{
if (!IsPaused) return;
if (continueCoroutine != null) return;
continueCoroutine = StartCoroutine(ContinueFromPauseCoroutine());
}
public void ReplayFromPause()
{
if (!IsPaused) return;
if (continueCoroutine != null)
{
StopCoroutine(continueCoroutine);
continueCoroutine = null;
}
StartCoroutine(ReplayFromPauseCoroutine());
}
public void ExitFromPause()
{
if (!IsPaused) return;
if (continueCoroutine != null)
{
StopCoroutine(continueCoroutine);
continueCoroutine = null;
}
StartCoroutine(ExitFromPauseCoroutine());
}
private bool IsGameplaySceneActive()
{
string sceneName = SceneManager.GetActiveScene().name ?? string.Empty;
if (string.Equals(sceneName, gameplaySceneName, StringComparison.OrdinalIgnoreCase)) return true;
if (sceneName.IndexOf("gameplay", StringComparison.OrdinalIgnoreCase) >= 0) return true;
return false;
}
private bool CanPauseFromInput()
{
if (onlyInGameplayScene && !IsGameplaySceneActive()) return false;
if (!requirePlaybackStarted) return true;
var gm = SceneObjectLookupCache.FindAny();
if (gm == null) return false;
// If already paused, allow input to unpause regardless of PlaybackStarted state
if (IsPaused) return true;
return gm.PlaybackStarted;
}
private void EnsureOverlayCanvasGroup()
{
if (overlayRoot == null) return;
// Priority: Use existing CanvasGroup if available (Index first)
if (!overlayRoot.TryGetComponent(out overlayCanvasGroup))
{
// Fallback: Create only if not found
overlayCanvasGroup = overlayRoot.AddComponent();
}
}
private void ResolveOverlayRootForPausePrefab()
{
// Priority: If manually assigned in Inspector, respect it.
if (overlayRoot != null) return;
if (!preferPausePrefabOverWhiteScreen) return;
// Prefer the real pause prefab root (contains Continue/Replay/Exit).
GameObject pauseRoot = FindPauseMenuRootInScene();
if (pauseRoot != null)
{
overlayRoot = pauseRoot;
return;
}
if (overlayRoot == null) return;
// Legacy setup fallback: PauseManager points to pauseWhiteScreen.
if (!string.Equals(overlayRoot.name, legacyWhiteScreenRootName, StringComparison.OrdinalIgnoreCase))
return;
GameObject candidate = FindPauseOverlayCandidate(overlayRoot.transform);
if (candidate == null)
candidate = FindPauseOverlayCandidate(null);
if (candidate != null && candidate != overlayRoot)
overlayRoot = candidate;
}
private GameObject FindPauseOverlayCandidate(Transform searchRoot)
{
Canvas[] canvases = searchRoot != null
? searchRoot.GetComponentsInChildren