using System.IO; using UnityEngine; using UnityEngine.UI; using System.Collections; using UnityEngine.Networking; using UnityEngine.SceneManagement; using UnityEngineInternal; using Bansonic; using GameServer.Client; public class GameManager : MonoBehaviour { public BeatmapManager beatmapManager; // Beatmap manager public NoteSpawner noteSpawner; // Note spawner public AudioSource musicSource; // Music audio source public readyLetsGo readyLetsGo; [Header("paths")] // Paths for external test-mode resources public string JSONPath; public string audioPath; public string bgPicPath; [Header("canvas")] public GameObject startCanvas; public CanvasGroup cg_startCanvas; public GameObject extraStartObject; private CanvasGroup extraStartCG; public float canvasFadeTime; private Coroutine fade_canvasGroup; public CanvasGroup settleCG; [Header("Start Canvas Anim")] [SerializeField] private bool enableStartCanvasDetailedAnim = true; [SerializeField] private float startCanvasBtmFadeIn = 0.28f; [SerializeField] private float startCanvasBtmFadeOut = 0.22f; [SerializeField] private int startCanvasFlashCount = 2; [SerializeField] private float startCanvasFlashUnit = 0.055f; [SerializeField] private float startCanvasImageFadeIn = 0.2f; [SerializeField] private float startCanvasMoveInDuration = 0.3f; [SerializeField] private float startCanvasMoveOutDuration = 0.22f; [SerializeField] private float startCanvasMoveStagger = 0.06f; [SerializeField] private float startCanvasMoveOffsetY = 80f; [SerializeField] private float startCanvasPanelExitOffsetY = 120f; [SerializeField] private float startCanvasEntryGap = 0.04f; [Header("buttons")] public Button startGame; public Button backtoSelectingPage; [Header("sprite renderer")] public SpriteRenderer backgroundRenderer; // NEW: UI Text to display banflag content from pressStart [Header("Debug / Test")] public Text banflagTextUI; // new: display current song name on UI when assigning background public Text songnameText; [Header("UI Overlays")] public UnityEngine.UI.Image blackMaskImage; // assign in inspector: full-screen black overlay [Header("Playback")] [Tooltip("Delay in seconds after unpausing (press Space) before starting audio and spawning. Configure in inspector.")] public float playbackStartDelay = 3f; [Tooltip("Delay in seconds before starting the black mask fade out. Configure in inspector.")] public float blackMaskFadeStartDelay = 1.5f; private bool pauseSubscribed = false; private bool musicWasPlayingBeforePause = false; // tracks whether actual music playback (the real game playback) has started public bool PlaybackStarted { get; private set; } = false; // Flag set by UI button to request start private bool startRequested = false; // Flag to control whether Escape can trigger return (disabled once startRequested is true) private bool allowEscapeReturn = true; private bool onGameStartSkillTriggered = false; // --- Statistics Tracking --- private float sessionStartTime; private SongData currentSong; private bool timeRecorded = false; private Coroutine startCanvasIntroRoutine; private bool startCanvasIntroCompleted = false; private bool startCanvasExitStarted = false; private RectTransform startupCanvasRoot; private RectTransform startup_btm; private RectTransform startup_boarder; private RectTransform startup_image; private RectTransform startup_text; private RectTransform startup_start; private RectTransform startup_escape; private RectTransform startup_tips; private RectTransform startup_uiBack; private CanvasGroup startup_btmCg; private CanvasGroup startup_boarderCg; private CanvasGroup startup_imageCg; private CanvasGroup startup_textCg; private CanvasGroup startup_startCg; private CanvasGroup startup_escapeCg; private CanvasGroup startup_tipsCg; private CanvasGroup startup_uiBackCg; private Vector2 startup_boarderBasePos; private Vector2 startup_imageBasePos; private Vector2 startup_textBasePos; private Vector2 startup_startBasePos; private Vector2 startup_escapeBasePos; private void SubscribeToPauseManager() { if (pauseSubscribed) return; // prefer the singleton, but try to find in scene if null var pm = PauseManager.Instance ?? SceneObjectLookupCache.FindAny(); if (pm != null) { pm.OnPauseStateChanged += HandlePauseStateChanged; pauseSubscribed = true; } } private void UnsubscribeFromPauseManager() { if (!pauseSubscribed) return; var pm = PauseManager.Instance ?? SceneObjectLookupCache.FindAny(); if (pm != null) { pm.OnPauseStateChanged -= HandlePauseStateChanged; } pauseSubscribed = false; } private void OnEnable() { SubscribeToPauseManager(); } private void OnDisable() { UnsubscribeFromPauseManager(); } private void HandlePauseStateChanged(bool isPaused) { if (musicSource != null) { try { if (isPaused) { // remember whether music was playing and pause it to stop timeline advancing musicWasPlayingBeforePause = musicSource.isPlaying; if (musicWasPlayingBeforePause) { musicSource.Pause(); } // Also pause Time.timeScale so playback delay coroutines are also paused Time.timeScale = 0f; } else { // Resume Time.timeScale first Time.timeScale = 1f; // resume if it was playing before pause if (musicWasPlayingBeforePause) { try { musicSource.UnPause(); } catch { } } // reset flag musicWasPlayingBeforePause = false; } } catch { } } } /// /// Fade out the black mask image over duration seconds (unscaled), then disable its raycast target so UI passes through. /// public IEnumerator FadeOutBlackMask(float duration) { if (blackMaskImage == null) yield break; // ensure image active if (!blackMaskImage.gameObject.activeSelf) blackMaskImage.gameObject.SetActive(true); float t = 0f; Color c = blackMaskImage.color; float startA = c.a; while (t < duration) { t += Time.unscaledDeltaTime; float frac = Mathf.Clamp01(t / Mathf.Max(0.0001f, duration)); c.a = Mathf.Lerp(startA, 0f, frac); blackMaskImage.color = c; yield return null; } // ensure fully transparent c.a = 0f; blackMaskImage.color = c; // disable raycast so underlying UI can receive input try { blackMaskImage.raycastTarget = false; } catch { } // optionally deactivate the overlay to save draw calls blackMaskImage.gameObject.SetActive(false); } private Coroutine playbackDelayCoroutine; private void BeginGameplayClock() { GameplayClock.Reset(); GameplayClock.StartChart(0f); } // Public helper to reliably unmute and start music playback, respecting delay. public void PlayMusicWithDelay(float delaySeconds) { // reset flag until actual play occurs PlaybackStarted = false; // Stop any previous delay coroutine if (playbackDelayCoroutine != null) { StopCoroutine(playbackDelayCoroutine); playbackDelayCoroutine = null; } if (musicSource == null) return; // Documentation text normalized. try { musicSource.Stop(); musicSource.time = 0f; } catch { } try { // Documentation text normalized. musicSource.mute = false; Debug.Log("GameManager.PlayMusicWithDelay: musicSource unmuted"); } catch { } try { float startTime = 0f; if (delaySeconds < 0f) startTime = -delaySeconds; if (delaySeconds > 0f) { playbackDelayCoroutine = StartCoroutine(DelayAndPlayMusic(delaySeconds)); } else { float clampedStartTime = ClampAudioStartTime(startTime); musicSource.time = clampedStartTime; musicSource.Play(); PlaybackStarted = true; Debug.Log("GameManager.PlayMusicWithDelay: Music playback started immediately (no delay)"); } } catch (System.Exception ex) { Debug.LogWarning($"PlayMusicWithDelay failed: {ex}"); try { float clampedStartTime = ClampAudioStartTime(delaySeconds < 0f ? -delaySeconds : 0f); musicSource.time = clampedStartTime; musicSource.Play(); PlaybackStarted = true; } catch { } } } private float ClampAudioStartTime(float startTime) { if (musicSource == null || musicSource.clip == null) return Mathf.Max(0f, startTime); return Mathf.Clamp(startTime, 0f, musicSource.clip.length); } private IEnumerator DelayAndPlayMusic(float delaySeconds) { Debug.Log($"GameManager.DelayAndPlayMusic: waiting {delaySeconds} seconds before playing (respects pause state)"); // Wait using unscaled real time so this delay is not blocked when Time.timeScale==0 float elapsed = 0f; while (elapsed < delaySeconds) { elapsed += Time.unscaledDeltaTime; yield return null; } // After delay, play the music if (musicSource != null) { try { // Documentation text normalized. musicSource.mute = false; musicSource.time = 0f; musicSource.Play(); PlaybackStarted = true; Debug.Log($"GameManager.DelayAndPlayMusic: music playback started after {delaySeconds}s delay, mute={musicSource.mute}"); } catch (System.Exception ex) { Debug.LogWarning($"DelayAndPlayMusic failed to play: {ex}"); } } playbackDelayCoroutine = null; } void Start() { Debug.Log("GameManager Start() initialized"); // Move first-hit one-time setup cost into scene load instead of the first judged note. StartCoroutine(PrewarmGameplayFirstHitPath()); // --- Initialize Statistics --- currentSong = BeatmapManager.pendingSongData ?? SongDataHolder.SelectedSongData; if (currentSong != null && !GameConfig.autoPlayEnabled) { currentSong.game_enterTimes++; sessionStartTime = Time.realtimeSinceStartup; Debug.Log($"[GameManager] {currentSong.songName} launch count incremented to {currentSong.game_enterTimes}"); } else if (currentSong != null && GameConfig.autoPlayEnabled) { sessionStartTime = Time.realtimeSinceStartup; Debug.Log($"[GameManager] {currentSong.songName} autoplay enabled, skipping launch count increment"); } else { Debug.LogWarning("[GameManager] No SongData found for statistics tracking"); } // Start a new per-level skill timeline log (overwrites previous level log). string skillLogSessionName = currentSong != null ? currentSong.songName : "UnknownSong"; GameplaySkillLogger.BeginSession(skillLogSessionName); Debug.Log($"[GameManager] Skill timeline log reset: {GameplaySkillLogger.LogFilePath}"); // Ensure start canvas and its CanvasGroup are active and visible at scene start if (startCanvas != null && !startCanvas.activeSelf) startCanvas.SetActive(true); if (startCanvas != null && startCanvas.transform.localScale.sqrMagnitude < 0.0001f) startCanvas.transform.localScale = Vector3.one; if (cg_startCanvas != null) { cg_startCanvas.alpha = 1f; cg_startCanvas.interactable = true; cg_startCanvas.blocksRaycasts = true; } // Initialize extra start object and its CanvasGroup if (extraStartObject != null) { extraStartCG = extraStartObject.GetComponent(); } startCanvasExitStarted = false; startCanvasIntroCompleted = false; InitializeStartCanvasAnimRefs(); PrepareStartCanvasEntryState(); StartStartCanvasIntro(); // Try to apply assigned SongData (if any) to background and UI after scene load StartCoroutine(WaitAndApplyAssignedSong(2f)); if (beatmapManager == null) Debug.LogError("beatmapManager is null"); if (noteSpawner == null) Debug.LogError("noteSpawner is null"); if (musicSource == null) Debug.LogError("musicSource is null"); // Subscribe to NoteSpawner.AllNotesSpawned to trigger action when spawning completes if (noteSpawner != null) { noteSpawner.AllNotesSpawned += OnAllNotesSpawned; } // Wire up UI buttons if assigned if (startGame != null) { // ensure we don't register duplicate listeners try { startGame.onClick.RemoveListener(RequestStart); } catch { } startGame.onClick.AddListener(RequestStart); } if (backtoSelectingPage != null) { try { backtoSelectingPage.onClick.RemoveListener(BackToSelectingPage); } catch { } backtoSelectingPage.onClick.AddListener(BackToSelectingPage); } // preload audio fully and start muted playback to warm up audio decoding if (musicSource != null && musicSource.clip != null) { musicSource.loop = false; musicSource.playOnAwake = false; try { // Warm up decoding by playing briefly muted so the audio system decodes the clip. musicSource.mute = true; musicSource.Play(); Debug.Log("GameManager: warmed up audio playback (muted)"); // Documentation text normalized. // Wait a frame to ensure audio system processes the Play command StartCoroutine(StopWarmupAfterFrame()); } catch (System.Exception ex) { Debug.LogWarning("GameManager: failed to warm up audio: " + ex.Message); } settleCG.alpha = 0; } // display banflag content if available if (banflagTextUI != null && !string.IsNullOrEmpty(pressStart.banflagContent)) { banflagTextUI.text = pressStart.banflagContent; Debug.Log("Displayed banflag content from pressStart into banflagTextUI."); } // choose startup path if (GameConfig.testMode) { JSONPath = pressStart.testmode_beatmapJSON_path; audioPath = pressStart.testmode_audioFile_path; bgPicPath = pressStart.testmode_bgPicFile_path; Debug.Log("Test mode active: using external paths to preload assets"); if (banflagTextUI != null && !string.IsNullOrEmpty(pressStart.banflagContent)) { banflagTextUI.text = pressStart.banflagContent; } StartCoroutine(HandleTestModeStartup()); return; } else { StartCoroutine(HandleNormalModeStartup()); } // Ensure black mask blocks input and is fully opaque at scene start (will fade out after loading). if (blackMaskImage != null) { try { Color cc = blackMaskImage.color; cc.a = 1f; blackMaskImage.color = cc; blackMaskImage.gameObject.SetActive(true); blackMaskImage.raycastTarget = true; // start automatic fade from alpha=1 to 0 over 1 second after Start, with delay StartCoroutine(DelayedFadeOutBlackMask(blackMaskFadeStartDelay, 1f)); } catch { } } } /// /// Helper coroutine to stop audio warmup after one frame and unmute the source /// private IEnumerator StopWarmupAfterFrame() { yield return null; // wait one frame for audio system to process Play command if (musicSource != null) { try { musicSource.Stop(); musicSource.mute = false; // Documentation text normalized. musicSource.time = 0f; Debug.Log("GameManager: warmup completed, audio stopped and unmuted"); } catch (System.Exception ex) { Debug.LogWarning("GameManager: failed to stop warmup: " + ex.Message); } } } private IEnumerator PrewarmGameplayFirstHitPath() { yield return null; try { Note.PrewarmRuntimeCaches(); HoldNote.PrewarmRuntimeCaches(); var judgeFx = Animation_GenerateJudgementSituationPrefab.Instance ?? SceneObjectLookupCache.FindAny(); judgeFx?.PrewarmJudgePrefabs(5); var trackHitFx = TrackJudgeHitEffectController.Instance ?? SceneObjectLookupCache.FindAny(); trackHitFx?.PrewarmTrackParticles(10); var fxEvent = effectEventController.Instance ?? SceneObjectLookupCache.FindAny(); if (fxEvent != null && fxEvent.isActiveAndEnabled) { // Cache target and pre-create SmoothShake via OnEnable/Awake path already present. } var globalFx = globalNoteEffect.Instance ?? SceneObjectLookupCache.FindAny(); globalFx?.PrewarmRuntime(); } catch (System.Exception ex) { Debug.LogWarning($"[GameManager] Gameplay prewarm failed: {ex}"); } } private void Update() { // Detect Space or yellow note key to request start (only before gameplay starts) if (!startRequested) { KeyCode yellowKey = KeyBindingManager.GetKeyForColor("yellow"); // Check Space key if (Input.GetKeyDown(KeyCode.Space)) { RequestStart(); } // Check yellow track key from key binding manager else if (yellowKey != KeyCode.None && Input.GetKeyDown(yellowKey)) { if (yellowKey == KeyCode.Space) return; else RequestStart(); } // Check Escape key to trigger return to selecting page (only before gameplay starts and allowed) if (allowEscapeReturn && Input.GetKeyDown(KeyCode.Escape)) { BackToSelectingPage(); } } // Once startRequested is true, Escape is disabled and gameplay begins countdown } private void OnDestroy() { RecordTotalPlayTime(); // Unsubscribe from NoteSpawner.AllNotesSpawned to avoid memory leaks if (noteSpawner != null) { noteSpawner.AllNotesSpawned -= OnAllNotesSpawned; } // remove UI listeners if (startGame != null) { startGame.onClick.RemoveListener(RequestStart); } if (backtoSelectingPage != null) { backtoSelectingPage.onClick.RemoveListener(BackToSelectingPage); } // stop canvas fade coroutine if running if (fade_canvasGroup != null) { StopCoroutine(fade_canvasGroup); fade_canvasGroup = null; } if (startCanvasIntroRoutine != null) { StopCoroutine(startCanvasIntroRoutine); startCanvasIntroRoutine = null; } } // Start the fade coroutine for the start canvas group (safe to call multiple times) private void StartFadeStartCanvas() { if (cg_startCanvas == null || startCanvas == null) return; if (startCanvasExitStarted) return; startCanvasExitStarted = true; if (startCanvasIntroRoutine != null) { StopCoroutine(startCanvasIntroRoutine); startCanvasIntroRoutine = null; } SetStartButtonsInteractable(false); if (fade_canvasGroup != null) StopCoroutine(fade_canvasGroup); fade_canvasGroup = StartCoroutine(FadeStartCanvasCoroutine(canvasFadeTime)); } private IEnumerator FadeStartCanvasCoroutine(float duration) { if (cg_startCanvas == null || startCanvas == null) yield break; if (enableStartCanvasDetailedAnim) { ApplyStartCanvasEntryFinalState(); // text / START / escape: sequential move down + fade out yield return StartCoroutine(AnimateMoveAndFade(startup_text, startup_textCg, startup_textBasePos, startup_textBasePos + new Vector2(0f, -startCanvasMoveOffsetY), 1f, 0f, startCanvasMoveOutDuration, false)); yield return WaitRealtime(startCanvasMoveStagger); yield return StartCoroutine(AnimateMoveAndFade(startup_start, startup_startCg, startup_startBasePos, startup_startBasePos + new Vector2(0f, -startCanvasMoveOffsetY), 1f, 0f, startCanvasMoveOutDuration, false)); yield return WaitRealtime(startCanvasMoveStagger); yield return StartCoroutine(AnimateMoveAndFade(startup_escape, startup_escapeCg, startup_escapeBasePos, startup_escapeBasePos + new Vector2(0f, -startCanvasMoveOffsetY), 1f, 0f, startCanvasMoveOutDuration, false)); // TIPS / UIback: flicker out Coroutine tipsOut = null; Coroutine uiBackOut = null; if (startup_tipsCg != null) tipsOut = StartCoroutine(FlashOut(startup_tipsCg, startCanvasFlashCount, startCanvasFlashUnit)); if (startup_uiBackCg != null) uiBackOut = StartCoroutine(FlashOut(startup_uiBackCg, startCanvasFlashCount, startCanvasFlashUnit)); // boarder + Image: wait for text/START/escape, then move down + fade Coroutine boarderOut = StartCoroutine(AnimateMoveAndFade(startup_boarder, startup_boarderCg, startup_boarderBasePos, startup_boarderBasePos + new Vector2(0f, -startCanvasPanelExitOffsetY), 1f, 0f, startCanvasMoveOutDuration, false)); Coroutine imageOut = StartCoroutine(AnimateMoveAndFade(startup_image, startup_imageCg, startup_imageBasePos, startup_imageBasePos + new Vector2(0f, -startCanvasPanelExitOffsetY), 1f, 0f, startCanvasMoveOutDuration, false)); if (boarderOut != null || imageOut != null) yield return WaitRealtime(startCanvasMoveOutDuration + 0.01f); if (tipsOut != null || uiBackOut != null) yield return WaitRealtime(startCanvasFlashUnit * Mathf.Max(2, startCanvasFlashCount * 2) + 0.01f); // btm fade out at last yield return StartCoroutine(FadeCanvasGroupAlpha(startup_btmCg, 1f, 0f, startCanvasBtmFadeOut, false)); cg_startCanvas.alpha = 0f; cg_startCanvas.interactable = false; cg_startCanvas.blocksRaycasts = false; startCanvas.SetActive(false); if (extraStartCG != null) { extraStartCG.alpha = 0f; extraStartCG.interactable = false; extraStartCG.blocksRaycasts = false; if (extraStartObject != null) extraStartObject.SetActive(false); } fade_canvasGroup = null; yield break; } float elapsed = 0f; float startA = cg_startCanvas.alpha; while (elapsed < duration) { elapsed += Time.unscaledDeltaTime; float frac = Mathf.Clamp01(elapsed / Mathf.Max(0.0001f, duration)); float currentAlpha = Mathf.Lerp(startA, 0f, frac); cg_startCanvas.alpha = currentAlpha; if (extraStartCG != null) { extraStartCG.alpha = currentAlpha; // Sync raycast state with alpha bool visible = currentAlpha > 0.001f; extraStartCG.interactable = visible; extraStartCG.blocksRaycasts = visible; } yield return null; } cg_startCanvas.alpha = 0f; cg_startCanvas.interactable = false; cg_startCanvas.blocksRaycasts = false; startCanvas.SetActive(false); if (extraStartCG != null) { extraStartCG.alpha = 0f; extraStartCG.interactable = false; extraStartCG.blocksRaycasts = false; if (extraStartObject != null) extraStartObject.SetActive(false); } fade_canvasGroup = null; } private void StartStartCanvasIntro() { if (!enableStartCanvasDetailedAnim) { ApplyStartCanvasEntryFinalState(); startCanvasIntroCompleted = true; SetStartButtonsInteractable(true); return; } if (startCanvasIntroRoutine != null) { StopCoroutine(startCanvasIntroRoutine); startCanvasIntroRoutine = null; } startCanvasIntroRoutine = StartCoroutine(StartCanvasIntroCoroutine()); } private IEnumerator StartCanvasIntroCoroutine() { startCanvasIntroCompleted = false; SetStartButtonsInteractable(false); yield return StartCoroutine(FadeCanvasGroupAlpha(startup_btmCg, 0f, 1f, startCanvasBtmFadeIn, false)); float flashDuration = startCanvasFlashUnit * Mathf.Max(2, startCanvasFlashCount * 2); Coroutine boarderIn = null; Coroutine tipsIn = null; Coroutine uiBackIn = null; if (startup_boarderCg != null) boarderIn = StartCoroutine(FlashIn(startup_boarderCg, startCanvasFlashCount, startCanvasFlashUnit, false)); if (startup_tipsCg != null) tipsIn = StartCoroutine(FlashIn(startup_tipsCg, startCanvasFlashCount, startCanvasFlashUnit, false)); if (startup_uiBackCg != null) uiBackIn = StartCoroutine(FlashIn(startup_uiBackCg, startCanvasFlashCount, startCanvasFlashUnit, false)); if (boarderIn != null || tipsIn != null || uiBackIn != null) yield return WaitRealtime(flashDuration + 0.01f); yield return WaitRealtime(startCanvasEntryGap); yield return StartCoroutine(FadeCanvasGroupAlpha(startup_imageCg, 0f, 1f, startCanvasImageFadeIn, false)); Vector2 textFrom = startup_textBasePos + new Vector2(0f, -startCanvasMoveOffsetY); Vector2 startFrom = startup_startBasePos + new Vector2(0f, -startCanvasMoveOffsetY); Vector2 escapeFrom = startup_escapeBasePos + new Vector2(0f, -startCanvasMoveOffsetY); yield return StartCoroutine(AnimateMoveAndFade(startup_text, startup_textCg, textFrom, startup_textBasePos, 0f, 1f, startCanvasMoveInDuration, false)); yield return WaitRealtime(startCanvasMoveStagger); yield return StartCoroutine(AnimateMoveAndFade(startup_start, startup_startCg, startFrom, startup_startBasePos, 0f, 1f, startCanvasMoveInDuration, true)); yield return WaitRealtime(startCanvasMoveStagger); yield return StartCoroutine(AnimateMoveAndFade(startup_escape, startup_escapeCg, escapeFrom, startup_escapeBasePos, 0f, 1f, startCanvasMoveInDuration, true)); startCanvasIntroCompleted = true; SetStartButtonsInteractable(true); startCanvasIntroRoutine = null; } private void InitializeStartCanvasAnimRefs() { startupCanvasRoot = null; startup_btm = null; startup_boarder = null; startup_image = null; startup_text = null; startup_start = null; startup_escape = null; startup_tips = null; startup_uiBack = null; startup_btmCg = null; startup_boarderCg = null; startup_imageCg = null; startup_textCg = null; startup_startCg = null; startup_escapeCg = null; startup_tipsCg = null; startup_uiBackCg = null; if (startCanvas == null) return; Transform root = startCanvas.transform; startupCanvasRoot = FindRectByName(root, "startupCanvas"); if (startupCanvasRoot == null) startupCanvasRoot = root as RectTransform; if (startupCanvasRoot == null) return; startup_btm = FindDirectChildRectByName(startupCanvasRoot, "btm"); startup_boarder = FindDirectChildRectByName(startupCanvasRoot, "boarder"); startup_image = FindDirectChildRectByName(startupCanvasRoot, "Image"); startup_text = FindDirectChildRectByName(startupCanvasRoot, "text"); startup_start = FindDirectChildRectByName(startupCanvasRoot, "START"); startup_escape = FindDirectChildRectByName(startupCanvasRoot, "escape"); startup_tips = FindDirectChildRectByName(startupCanvasRoot, "TIPS"); startup_uiBack = FindDirectChildRectByName(startupCanvasRoot, "UIback"); if (startup_tips == null) startup_tips = FindRectByName(startupCanvasRoot, "TIPS"); if (startup_uiBack == null) startup_uiBack = FindRectByName(startupCanvasRoot, "UIback"); startup_btmCg = GetOrAddCanvasGroup(startup_btm); startup_boarderCg = GetOrAddCanvasGroup(startup_boarder); startup_imageCg = GetOrAddCanvasGroup(startup_image); startup_textCg = GetOrAddCanvasGroup(startup_text); startup_startCg = GetOrAddCanvasGroup(startup_start); startup_escapeCg = GetOrAddCanvasGroup(startup_escape); startup_tipsCg = GetOrAddCanvasGroup(startup_tips); startup_uiBackCg = GetOrAddCanvasGroup(startup_uiBack); if (startup_boarder != null) startup_boarderBasePos = startup_boarder.anchoredPosition; if (startup_image != null) startup_imageBasePos = startup_image.anchoredPosition; if (startup_text != null) startup_textBasePos = startup_text.anchoredPosition; if (startup_start != null) startup_startBasePos = startup_start.anchoredPosition; if (startup_escape != null) startup_escapeBasePos = startup_escape.anchoredPosition; } private void PrepareStartCanvasEntryState() { if (cg_startCanvas == null || startCanvas == null) return; if (!startCanvas.activeSelf) startCanvas.SetActive(true); cg_startCanvas.alpha = 1f; cg_startCanvas.interactable = true; cg_startCanvas.blocksRaycasts = true; if (extraStartCG != null) { if (extraStartObject != null && !extraStartObject.activeSelf) extraStartObject.SetActive(true); extraStartCG.alpha = 1f; extraStartCG.interactable = true; extraStartCG.blocksRaycasts = true; } if (!enableStartCanvasDetailedAnim) { ApplyStartCanvasEntryFinalState(); return; } SetCanvasGroupAlpha(startup_btmCg, 0f, false); SetCanvasGroupAlpha(startup_boarderCg, 0f, false); SetCanvasGroupAlpha(startup_imageCg, 0f, false); SetCanvasGroupAlpha(startup_textCg, 0f, false); SetCanvasGroupAlpha(startup_startCg, 0f, true); SetCanvasGroupAlpha(startup_escapeCg, 0f, true); SetCanvasGroupAlpha(startup_tipsCg, 0f, false); SetCanvasGroupAlpha(startup_uiBackCg, 0f, false); if (startup_boarder != null) startup_boarder.anchoredPosition = startup_boarderBasePos; if (startup_image != null) startup_image.anchoredPosition = startup_imageBasePos; if (startup_text != null) startup_text.anchoredPosition = startup_textBasePos + new Vector2(0f, -startCanvasMoveOffsetY); if (startup_start != null) startup_start.anchoredPosition = startup_startBasePos + new Vector2(0f, -startCanvasMoveOffsetY); if (startup_escape != null) startup_escape.anchoredPosition = startup_escapeBasePos + new Vector2(0f, -startCanvasMoveOffsetY); } private void ApplyStartCanvasEntryFinalState() { if (startup_boarder != null) startup_boarder.anchoredPosition = startup_boarderBasePos; if (startup_image != null) startup_image.anchoredPosition = startup_imageBasePos; if (startup_text != null) startup_text.anchoredPosition = startup_textBasePos; if (startup_start != null) startup_start.anchoredPosition = startup_startBasePos; if (startup_escape != null) startup_escape.anchoredPosition = startup_escapeBasePos; SetCanvasGroupAlpha(startup_btmCg, 1f, false); SetCanvasGroupAlpha(startup_boarderCg, 1f, false); SetCanvasGroupAlpha(startup_imageCg, 1f, false); SetCanvasGroupAlpha(startup_textCg, 1f, false); SetCanvasGroupAlpha(startup_startCg, 1f, true); SetCanvasGroupAlpha(startup_escapeCg, 1f, true); SetCanvasGroupAlpha(startup_tipsCg, 1f, false); SetCanvasGroupAlpha(startup_uiBackCg, 1f, false); } private void SetStartButtonsInteractable(bool interactable) { if (startGame != null) startGame.interactable = interactable; if (backtoSelectingPage != null) backtoSelectingPage.interactable = interactable; } private IEnumerator AnimateMoveAndFade(RectTransform rt, CanvasGroup cg, Vector2 from, Vector2 to, float fromAlpha, float toAlpha, float duration, bool allowRaycastWhenVisible) { if (rt == null || cg == null) yield break; float dur = Mathf.Max(0.01f, duration); float elapsed = 0f; rt.anchoredPosition = from; SetCanvasGroupAlpha(cg, fromAlpha, allowRaycastWhenVisible); while (elapsed < dur) { elapsed += Time.unscaledDeltaTime; float t = EaseOutCubic(Mathf.Clamp01(elapsed / dur)); rt.anchoredPosition = Vector2.LerpUnclamped(from, to, t); float a = Mathf.LerpUnclamped(fromAlpha, toAlpha, t); SetCanvasGroupAlpha(cg, a, allowRaycastWhenVisible); yield return null; } rt.anchoredPosition = to; SetCanvasGroupAlpha(cg, toAlpha, allowRaycastWhenVisible); } private IEnumerator FadeCanvasGroupAlpha(CanvasGroup cg, float fromAlpha, float toAlpha, float duration, bool allowRaycastWhenVisible) { if (cg == null) yield break; float dur = Mathf.Max(0.01f, duration); float elapsed = 0f; SetCanvasGroupAlpha(cg, fromAlpha, allowRaycastWhenVisible); while (elapsed < dur) { elapsed += Time.unscaledDeltaTime; float t = EaseOutCubic(Mathf.Clamp01(elapsed / dur)); float a = Mathf.LerpUnclamped(fromAlpha, toAlpha, t); SetCanvasGroupAlpha(cg, a, allowRaycastWhenVisible); yield return null; } SetCanvasGroupAlpha(cg, toAlpha, allowRaycastWhenVisible); } private IEnumerator FlashIn(CanvasGroup cg, int flashes, float unitDuration, bool allowRaycastWhenVisible) { if (cg == null) yield break; int count = Mathf.Max(1, flashes); float step = Mathf.Max(0.01f, unitDuration); SetCanvasGroupAlpha(cg, 0f, allowRaycastWhenVisible); for (int i = 0; i < count; i++) { SetCanvasGroupAlpha(cg, 1f, allowRaycastWhenVisible); yield return WaitRealtime(step); if (i < count - 1) { SetCanvasGroupAlpha(cg, 0f, allowRaycastWhenVisible); yield return WaitRealtime(step); } } SetCanvasGroupAlpha(cg, 1f, allowRaycastWhenVisible); } private IEnumerator FlashOut(CanvasGroup cg, int flashes, float unitDuration) { if (cg == null) yield break; int count = Mathf.Max(1, flashes); float step = Mathf.Max(0.01f, unitDuration); SetCanvasGroupAlpha(cg, 1f, false); for (int i = 0; i < count; i++) { SetCanvasGroupAlpha(cg, 0f, false); yield return WaitRealtime(step); if (i < count - 1) { SetCanvasGroupAlpha(cg, 1f, false); yield return WaitRealtime(step); } } SetCanvasGroupAlpha(cg, 0f, false); } private IEnumerator WaitRealtime(float duration) { float elapsed = 0f; float dur = Mathf.Max(0f, duration); while (elapsed < dur) { elapsed += Time.unscaledDeltaTime; yield return null; } } private static float EaseOutCubic(float t) { float inv = 1f - t; return 1f - inv * inv * inv; } private static CanvasGroup GetOrAddCanvasGroup(RectTransform rt) { if (rt == null) return null; CanvasGroup cg = rt.GetComponent(); if (cg == null) cg = rt.gameObject.AddComponent(); return cg; } private static void SetCanvasGroupAlpha(CanvasGroup cg, float alpha, bool allowRaycastWhenVisible) { if (cg == null) return; float a = Mathf.Clamp01(alpha); cg.alpha = a; bool visible = a > 0.001f; cg.interactable = visible && allowRaycastWhenVisible; cg.blocksRaycasts = visible && allowRaycastWhenVisible; } private static RectTransform FindDirectChildRectByName(Transform root, string exactName) { if (root == null || string.IsNullOrEmpty(exactName)) return null; for (int i = 0; i < root.childCount; i++) { Transform c = root.GetChild(i); if (c == null) continue; if (string.Equals(c.name, exactName, System.StringComparison.OrdinalIgnoreCase)) return c as RectTransform; } return null; } private static RectTransform FindRectByName(Transform root, string exactName) { Transform t = FindDescendantByName(root, exactName); return t as RectTransform; } private static Transform FindDescendantByName(Transform root, string exactName) { if (root == null || string.IsNullOrEmpty(exactName)) return null; Transform[] all = root.GetComponentsInChildren(true); for (int i = 0; i < all.Length; i++) { Transform t = all[i]; if (t == null) continue; if (string.Equals(t.name, exactName, System.StringComparison.OrdinalIgnoreCase)) return t; } return null; } private void OnAllNotesSpawned() { // All notes have been spawned - trigger settlement decision logic Debug.Log("[GameManager] All notes have been spawned. Initiating settlement countdown."); if (noteSpawner != null) { noteSpawner.StartSettlementRoutine(); } else { Debug.LogWarning("[GameManager] OnAllNotesSpawned: noteSpawner is null, cannot start settlement routine"); } } // Called by UI button to request the start countdown public void RequestStart() { // Guard: prevent duplicate RequestStart execution if (startRequested) { Debug.LogWarning("[GameManager] RequestStart already called, ignoring duplicate request"); return; } Debug.Log("[GameManager] RequestStart executed"); startRequested = true; allowEscapeReturn = false; // Disable Escape immediately when start is requested // Play ready sequence if available (null-check to avoid NullReferenceException) if (readyLetsGo != null) { try { readyLetsGo.PlaySequence(); Debug.Log("[GameManager] readyLetsGo.PlaySequence() triggered"); } catch (System.Exception ex) { Debug.LogError($"[GameManager] Error: {ex}"); } } else { Debug.LogWarning("[GameManager] readyLetsGo is null"); } } private IEnumerator HandleTestModeStartup() { // Preload audio from audioPath (absolute) if (!string.IsNullOrEmpty(audioPath)) { if (!File.Exists(audioPath)) { Debug.LogError($"TestMode audio file not found: {audioPath}"); } else { // Use UnityWebRequestMultimedia instead of WWW which is deprecated and can be unreliable for file:// audio string url = "file://" + audioPath; // determine audio type from extension for best compatibility AudioType audioType = AudioType.UNKNOWN; string ext = Path.GetExtension(audioPath)?.ToLowerInvariant(); if (!string.IsNullOrEmpty(ext)) { switch (ext) { case ".wav": audioType = AudioType.WAV; break; case ".ogg": audioType = AudioType.OGGVORBIS; break; case ".mp3": audioType = AudioType.MPEG; break; case ".aif": case ".aiff": audioType = AudioType.AIFF; break; default: audioType = AudioType.UNKNOWN; break; } } using (var uwr = UnityWebRequestMultimedia.GetAudioClip(url, audioType)) { yield return uwr.SendWebRequest(); #if UNITY_2020_1_OR_NEWER if (uwr.result == UnityWebRequest.Result.ConnectionError || uwr.result == UnityWebRequest.Result.ProtocolError) #else if (uwr.isNetworkError || uwr.isHttpError) #endif { Debug.LogError($"TestMode audio load failed: {uwr.error}"); } else { AudioClip clip = DownloadHandlerAudioClip.GetContent(uwr); if (clip == null) { Debug.LogError("Failed to obtain AudioClip from downloaded data"); } else { // wait for audio data to be loaded if necessary float waitStart = Time.realtimeSinceStartup; while (clip.loadState == AudioDataLoadState.Loading && Time.realtimeSinceStartup - waitStart < 5f) { yield return null; } if (clip.loadState != AudioDataLoadState.Loaded) { Debug.LogWarning($"AudioClip loadState is {clip.loadState}. It may still stream or be incomplete."); } if (musicSource == null) { Debug.LogError("musicSource is null - cannot assign TestMode audio"); } else { clip.name = Path.GetFileName(audioPath); musicSource.clip = clip; musicSource.loop = false; musicSource.playOnAwake = false; // Warm up decoding by playing briefly muted so the audio system decodes the clip. musicSource.mute = true; musicSource.Play(); // allow a few frames for audio system to start yield return null; Debug.Log("TestMode audio loaded into AudioSource"); // Documentation text normalized. StartCoroutine(StopWarmupAfterFrame()); // After loading audio, enable overlay and pause via PauseManager var pm = PauseManager.Instance ?? SceneObjectLookupCache.FindAny(); if (pm != null) { pm.Pause(true); Debug.Log("PauseManager.Pause(true) invoked after TestMode audio load"); } } } } } } } else { Debug.LogWarning("TestMode audioPath not provided"); } // Preload background image into sprite renderer if (backgroundRenderer != null) { if (!string.IsNullOrEmpty(bgPicPath)) { if (!File.Exists(bgPicPath)) { Debug.LogError($"TestMode background image not found: {bgPicPath}"); } else { byte[] imgBytes = null; try { imgBytes = File.ReadAllBytes(bgPicPath); } catch (System.Exception ex) { Debug.LogError($"Failed to read background image file: {ex}"); } if (imgBytes != null) { Texture2D tex = new Texture2D(2, 2); if (tex.LoadImage(imgBytes)) { Sprite s = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f)); backgroundRenderer.sprite = s; // ensure SpriteRenderer alpha is fully opaque Color color = backgroundRenderer.color; color.a = 1.0f; backgroundRenderer.color = color; Debug.Log("Background sprite assigned (ensure your SpriteRenderer size fits scene)"); // Set song name text if available if (songnameText != null) { songnameText.text = Path.GetFileNameWithoutExtension(bgPicPath); } } else { Debug.LogError($"Failed to create Texture2D from bytes: {bgPicPath}"); } } } } else { Debug.LogWarning("TestMode bgPicPath not provided"); } } else { Debug.LogWarning("backgroundRenderer not assigned; skipping background preload"); } // Read JSON from JSONPath and parse via BeatmapManager.ParseJsonOnly if (!string.IsNullOrEmpty(JSONPath)) { if (!File.Exists(JSONPath)) { Debug.LogError($"TestMode beatmap JSON not found: {JSONPath}"); yield break; // abort } string jsonContent = null; try { jsonContent = File.ReadAllText(JSONPath); } catch (System.Exception ex) { Debug.LogError($"Failed to read TestMode JSON file: {ex}"); yield break; } bool parsed = beatmapManager.ParseJsonOnly(jsonContent); if (!parsed) { Debug.LogError("ParseJsonOnly failed; aborting test mode startup"); yield break; } // Do not start spawning yet. We will start after unpausing. Debug.Log("Beatmap parsed (deferred start). Ready to unpause."); } else { Debug.LogError("TestMode JSONPath not provided"); yield break; } // Pause the system until Space pressed // Ensure we are subscribed before invoking pause so we track audio state SubscribeToPauseManager(); PauseManager.Instance?.Pause(true); UpdateStatusOnConsole("Paused: press Space to start playback"); // Determine yellow track key from key binding manager KeyCode yellowKey = KeyBindingManager.GetKeyForColor("yellow"); // Wait for start request: Space, yellow key, or UI button while (!startRequested && !Input.GetKeyDown(KeyCode.Space) && (yellowKey == KeyCode.None || !Input.GetKeyDown(yellowKey))) { yield return null; } // Clear request flag startRequested = false; // Kick off UI canvas fade-out when start requested StartFadeStartCanvas(); // Wait for readyLetsGo sequence if playing if (readyLetsGo != null && readyLetsGo.IsSequencePlaying) { while (readyLetsGo.IsSequencePlaying) { yield return null; } } gNotice.recommendation.display(LocalizationService.Get("gameplay.notice.game_start", "游戏开始!")); // Resume using PauseManager PauseManager.Instance?.Pause(false); BeginGameplayClock(); // Start spawning using the parsed beatmap if (beatmapManager.beatmap != null) { beatmapManager.LoadBeatmap(beatmapManager.beatmap); TriggerOnGameStartSkills(); } else { Debug.LogError("Beatmap missing after parsing"); yield break; } // If audio clip was loaded into musicSource.clip, play now with delay from parsed globalDelaySeconds float delaySeconds = beatmapManager != null ? beatmapManager.globalDelaySeconds : 0f; PlayMusicWithDelay(delaySeconds); UpdateStatusOnConsole("Playback started"); } private IEnumerator HandleNormalModeStartup() { // If a Beatmap was already provided by previous scene (pendingSongData path), skip loading default JSON if (beatmapManager != null && beatmapManager.beatmap != null) { Debug.Log("HandleNormalModeStartup: existing beatmap found on BeatmapManager; skipping default JSON load."); // Pause the system until Space pressed SubscribeToPauseManager(); PauseManager.Instance?.Pause(true); UpdateStatusOnConsole("Paused: press Space to start playback"); // Determine yellow track key from key binding manager KeyCode yellowKey = KeyBindingManager.GetKeyForColor("yellow"); // Wait for start request: Space, yellow key, or UI button while (!startRequested && !Input.GetKeyDown(KeyCode.Space) && (yellowKey == KeyCode.None || !Input.GetKeyDown(yellowKey))) { yield return null; } // Clear request flag startRequested = false; // Kick off UI canvas fade-out when start requested StartFadeStartCanvas(); // Wait for readyLetsGo sequence if playing if (readyLetsGo != null && readyLetsGo.IsSequencePlaying) { while (readyLetsGo.IsSequencePlaying) { yield return null; } } gNotice.recommendation.display(LocalizationService.Get("gameplay.notice.game_start", "游戏开始!")); // Resume PauseManager.Instance?.Pause(false); BeginGameplayClock(); // Start spawning using the existing parsed beatmap if (beatmapManager.beatmap != null) { noteSpawner.LoadBeatmap(beatmapManager.beatmap); TriggerOnGameStartSkills(); } else { Debug.LogError("HandleNormalModeStartup: beatmap was expected but is null"); yield break; } // Play background music if assigned if (musicSource != null && musicSource.clip != null) { float delay = beatmapManager != null ? beatmapManager.globalDelaySeconds : 0f; PlayMusicWithDelay(delay); } else { // try load via parsedMusicFile if available if (beatmapManager != null && !string.IsNullOrEmpty(beatmapManager.parsedMusicFile)) { AudioClip musicClip = Resources.Load(beatmapManager.parsedMusicFile); if (musicClip != null) { musicSource.clip = musicClip; // ensure we don't accidentally autoplay when assigning clips musicSource.playOnAwake = false; musicSource.Stop(); float delay = beatmapManager.globalDelaySeconds; PlayMusicWithDelay(delay); } } } UpdateStatusOnConsole("Playback started"); yield break; } // Original default demo load commented out; manual load only when needed string beatmapFilePath = Path.Combine(Application.streamingAssetsPath, "Emilia_demo.json"); if (!File.Exists(beatmapFilePath)) { Debug.LogError($"Beatmap file missing: {beatmapFilePath}"); yield break; } string json = File.ReadAllText(beatmapFilePath); bool parsed = beatmapManager.ParseJsonOnly(json); if (!parsed) { Debug.LogError("ParseJsonOnly failed"); yield break; } // Pause system SubscribeToPauseManager(); PauseManager.Instance?.Pause(true); UpdateStatusOnConsole("Paused: press Space to start playback"); // Determine yellow track key from key binding manager KeyCode yellowKey2 = KeyBindingManager.GetKeyForColor("yellow"); // Wait for start request: Space, yellow key, or UI button while (!startRequested && !Input.GetKeyDown(KeyCode.Space) && (yellowKey2 == KeyCode.None || !Input.GetKeyDown(yellowKey2))) { yield return null; } // Clear request flag startRequested = false; // Kick off UI canvas fade-out when start requested StartFadeStartCanvas(); // Wait for readyLetsGo sequence if playing if (readyLetsGo != null && readyLetsGo.IsSequencePlaying) { while (readyLetsGo.IsSequencePlaying) { yield return null; } } gNotice.recommendation.display(LocalizationService.Get("gameplay.notice.game_start", "游戏开始!")); // Resume PauseManager.Instance?.Pause(false); BeginGameplayClock(); // Start spawning if (beatmapManager.beatmap != null) { noteSpawner.LoadBeatmap(beatmapManager.beatmap); TriggerOnGameStartSkills(); } else { Debug.LogError("beatmap missing"); yield break; } // Load and play audio if (!string.IsNullOrEmpty(beatmapManager.parsedMusicFile)) { AudioClip musicClip = Resources.Load(beatmapManager.parsedMusicFile); if (musicClip == null) { Debug.LogError($"Failed to load parsed music file: {beatmapManager.parsedMusicFile}"); } else { musicSource.clip = musicClip; musicSource.playOnAwake = false; musicSource.Stop(); float delay = beatmapManager.globalDelaySeconds; PlayMusicWithDelay(delay); } } else { Debug.LogError("parsed musicFile empty"); } UpdateStatusOnConsole("Playback started"); } private void UpdateStatusOnConsole(string message) { Debug.Log(message); } /// /// Delay then fade out the black mask image. Waits for delaySeconds, then fades out over fadeDuration seconds (unscaled). /// private IEnumerator DelayedFadeOutBlackMask(float delaySeconds, float fadeDuration) { // Wait for the specified delay float elapsed = 0f; while (elapsed < delaySeconds) { elapsed += Time.unscaledDeltaTime; yield return null; } // After delay, start the fade out yield return StartCoroutine(FadeOutBlackMask(fadeDuration)); } private void TriggerOnGameStartSkills() { if (onGameStartSkillTriggered) return; onGameStartSkillTriggered = true; StartCoroutine(TriggerOnGameStartNextFrame()); } private IEnumerator TriggerOnGameStartNextFrame() { yield return null; var builder = SceneObjectLookupCache.FindAny(); if (builder == null) { Debug.LogWarning("[GameManager] TriggerOnGameStartSkills: SkillBuilder not found."); yield break; } int guard = 0; while (guard < 240) { if (teamUIController.Instance == null || SkillBuilder.Instance == null) { guard++; yield return null; continue; } bool ready = true; for (int i = 0; i < 5; i++) { if (builder.GetAllyHeroSOBySlot(i) == null) { ready = false; break; } } if (ready) break; guard++; yield return null; } builder.TriggerOnGameStart(); } // Called when the back button is pressed; fades to black then loads selection scene private void BackToSelectingPage() { RecordTotalPlayTime(); // restore time scale to normal so UI animations driven by unscaled or scaled time behave correctly try { Time.timeScale = 1f; } catch { } string targetScene = "selectYourSongFirst"; ArenaRoomService arenaRoomService = ArenaRoomService.Instance; if (arenaRoomService != null && arenaRoomService.IsInRoom) { arenaRoomService.MarkLocalGameplayExitedEarly(); targetScene = arenaRoomService.RoomSceneName; } // start coroutine to fade mask then load if (blackMaskImage == null) { StartCoroutine(LoadSceneAsync(targetScene)); return; } StartCoroutine(FadeToBlackAndLoad(targetScene, 0.25f)); } private IEnumerator LoadSceneAsync(string sceneName) { if (gTransition.LoadScene(sceneName, LoadSceneMode.Single)) { while (gTransition.IsBusy) { yield return null; } yield break; } AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName); while (asyncLoad != null && !asyncLoad.isDone) { yield return null; } } private IEnumerator FadeToBlackAndLoad(string sceneName, float duration) { RecordTotalPlayTime(); if (gTransition.LoadScene(sceneName, LoadSceneMode.Single, duration, duration)) { while (gTransition.IsBusy) { yield return null; } yield break; } if (blackMaskImage == null) { AsyncOperation asyncOp = SceneManager.LoadSceneAsync(sceneName); while (asyncOp != null && !asyncOp.isDone) yield return null; yield break; } // ensure image active and start from current alpha if (!blackMaskImage.gameObject.activeSelf) blackMaskImage.gameObject.SetActive(true); float startA = blackMaskImage.color.a; float elapsed = 0f; // enable raycast target while fading to block input try { blackMaskImage.raycastTarget = true; } catch {} while (elapsed < duration) { elapsed += Time.unscaledDeltaTime; float frac = Mathf.Clamp01(elapsed / Mathf.Max(0.0001f, duration)); Color c = blackMaskImage.color; c.a = Mathf.Lerp(startA, 1f, frac); blackMaskImage.color = c; yield return null; } // ensure fully opaque Color fc = blackMaskImage.color; fc.a = 1f; blackMaskImage.color = fc; // load scene AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName); while (asyncLoad != null && !asyncLoad.isDone) { yield return null; } } private IEnumerator WaitAndApplyAssignedSong(float timeoutSeconds) { float waited = 0f; while (waited < timeoutSeconds) { if (beatmapManager != null && beatmapManager.assignedSongData != null) { ApplyAssignedSongToUI(beatmapManager.assignedSongData); yield break; } waited += Time.unscaledDeltaTime; yield return null; } // also consider pendingSongData static as fallback if (BeatmapManager.pendingSongData != null) { ApplyAssignedSongToUI(BeatmapManager.pendingSongData); } } private void ApplyAssignedSongToUI(SongData sd) { if (sd == null) return; // set background sprite if present Sprite resolvedBackground = sd.GetResolvedFullscreenSongPicture(); if (backgroundRenderer != null && resolvedBackground != null) { backgroundRenderer.sprite = resolvedBackground; Color color = backgroundRenderer.color; color.a = 1f; backgroundRenderer.color = color; } // set song name text if available if (songnameText != null) { songnameText.text = sd.songName; } } /// /// Records the elapsed play time since the session started and adds it to the song's total play time. /// Can be called multiple times, but only records once per session. /// public float GetPendingSessionDurationSeconds() { if (timeRecorded || currentSong == null || GameConfig.autoPlayEnabled) { return 0f; } return Mathf.Max(0f, Time.realtimeSinceStartup - sessionStartTime); } public void RecordTotalPlayTime() { if (timeRecorded || currentSong == null || GameConfig.autoPlayEnabled) return; float elapsed = Time.realtimeSinceStartup - sessionStartTime; currentSong.time_totalPlayingTime += elapsed; timeRecorded = true; Debug.Log($"[GameManager] Recorded {elapsed:F2}s to total play time for {currentSong.songName}. New total: {currentSong.time_totalPlayingTime:F2}s"); } }