Files
bansonic_beta_main/Assets/scripts/gamePlay_gameplay/GameManager.cs
T

1694 lines
63 KiB
C#

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<PauseManager>();
if (pm != null)
{
pm.OnPauseStateChanged += HandlePauseStateChanged;
pauseSubscribed = true;
}
}
private void UnsubscribeFromPauseManager()
{
if (!pauseSubscribed) return;
var pm = PauseManager.Instance ?? SceneObjectLookupCache.FindAny<PauseManager>();
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 { }
}
}
/// <summary>
/// Fade out the black mask image over duration seconds (unscaled), then disable its raycast target so UI passes through.
/// </summary>
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;
// 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<CanvasGroup>();
}
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 { }
}
}
/// <summary>
/// Helper coroutine to stop audio warmup after one frame and unmute the source
/// </summary>
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<Animation_GenerateJudgementSituationPrefab>();
judgeFx?.PrewarmJudgePrefabs(1);
var fxEvent = effectEventController.Instance ?? SceneObjectLookupCache.FindAny<effectEventController>();
if (fxEvent != null && fxEvent.isActiveAndEnabled)
{
// Cache target and pre-create SmoothShake via OnEnable/Awake path already present.
}
var globalFx = globalNoteEffect.Instance ?? SceneObjectLookupCache.FindAny<globalNoteEffect>();
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<CanvasGroup>();
if (cg == null) cg = rt.gameObject.AddComponent<CanvasGroup>();
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<Transform>(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<PauseManager>();
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);
// 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);
// 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<AudioClip>(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);
// ensure overlay shows and pause logic runs after loading the clip
var pm2 = PauseManager.Instance ?? SceneObjectLookupCache.FindAny<PauseManager>();
if (pm2 != null)
{
pm2.Pause(true);
Debug.Log("PauseManager.Pause(true) invoked after assigning musicClip in normal startup");
}
}
}
}
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);
// 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<AudioClip>(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);
var pm3 = PauseManager.Instance ?? SceneObjectLookupCache.FindAny<PauseManager>();
if (pm3 != null)
{
pm3.Pause(true);
Debug.Log("PauseManager.Pause(true) invoked after assigning parsedMusicFile clip");
}
}
}
else
{
Debug.LogError("parsed musicFile empty");
}
UpdateStatusOnConsole("Playback started");
}
private void UpdateStatusOnConsole(string message)
{
Debug.Log(message);
}
/// <summary>
/// Delay then fade out the black mask image. Waits for delaySeconds, then fades out over fadeDuration seconds (unscaled).
/// </summary>
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<SkillBuilder>();
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)
{
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName);
while (!asyncLoad.isDone)
{
yield return null;
}
}
private IEnumerator FadeToBlackAndLoad(string sceneName, float duration)
{
RecordTotalPlayTime();
if (blackMaskImage == null)
{
AsyncOperation asyncOp = SceneManager.LoadSceneAsync(sceneName);
while (!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.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
if (backgroundRenderer != null && sd.fullscreen_songPicture != null)
{
backgroundRenderer.sprite = sd.fullscreen_songPicture;
Color color = backgroundRenderer.color;
color.a = 1f;
backgroundRenderer.color = color;
}
// set song name text if available
if (songnameText != null)
{
songnameText.text = sd.songName;
}
}
/// <summary>
/// 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.
/// </summary>
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");
}
}