1728 lines
64 KiB
C#
1728 lines
64 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;
|
|
|
|
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<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(5);
|
|
|
|
var trackHitFx = TrackJudgeHitEffectController.Instance
|
|
?? SceneObjectLookupCache.FindAny<TrackJudgeHitEffectController>();
|
|
trackHitFx?.PrewarmTrackParticles(10);
|
|
|
|
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));
|
|
|
|
ApplyStartCanvasExitFinalState();
|
|
|
|
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;
|
|
}
|
|
|
|
ApplyStartCanvasExitFinalState();
|
|
|
|
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));
|
|
|
|
ApplyStartCanvasEntryFinalState();
|
|
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 ApplyStartCanvasExitFinalState()
|
|
{
|
|
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);
|
|
if (startup_boarder != null) startup_boarder.anchoredPosition = startup_boarderBasePos + new Vector2(0f, -startCanvasPanelExitOffsetY);
|
|
if (startup_image != null) startup_image.anchoredPosition = startup_imageBasePos + new Vector2(0f, -startCanvasPanelExitOffsetY);
|
|
|
|
SetCanvasGroupAlpha(startup_btmCg, 0f, false);
|
|
SetCanvasGroupAlpha(startup_boarderCg, 0f, false);
|
|
SetCanvasGroupAlpha(startup_imageCg, 0f, false);
|
|
SetCanvasGroupAlpha(startup_textCg, 0f, false);
|
|
SetCanvasGroupAlpha(startup_startCg, 0f, false);
|
|
SetCanvasGroupAlpha(startup_escapeCg, 0f, false);
|
|
SetCanvasGroupAlpha(startup_tipsCg, 0f, false);
|
|
SetCanvasGroupAlpha(startup_uiBackCg, 0f, false);
|
|
|
|
if (cg_startCanvas != null)
|
|
{
|
|
cg_startCanvas.alpha = 0f;
|
|
cg_startCanvas.interactable = false;
|
|
cg_startCanvas.blocksRaycasts = false;
|
|
}
|
|
|
|
if (startCanvas != null) startCanvas.SetActive(false);
|
|
|
|
if (extraStartCG != null)
|
|
{
|
|
extraStartCG.alpha = 0f;
|
|
extraStartCG.interactable = false;
|
|
extraStartCG.blocksRaycasts = false;
|
|
}
|
|
|
|
if (extraStartObject != null) extraStartObject.SetActive(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);
|
|
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<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);
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
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<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);
|
|
|
|
}
|
|
}
|
|
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)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <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");
|
|
}
|
|
}
|