Files
2026-07-25 08:31:10 +08:00

1935 lines
67 KiB
C#

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using System.Collections;
using System;
using DG.Tweening;
using Bansonic;
public class selected_songInfo : MonoBehaviour
{
private static readonly bool VerboseLogs = false;
private const string LastEnteredSongIdPrefsKey = "last_entered_song_id";
[Header("ranking")]
public Button rankingButton;
public rankingList s_rl;
[Header("Music Controls")]
public Button playpausebutton;
public Button stopbutton;
public Image playPauseStateImage;
public Text currentTime;
public Text maxtime;
public Slider playbarSlider;
public Sprite playSprite;
public Sprite pauseSprite;
public AudioSource preListenAS;
private bool isDraggingSlider = false;
public Image t_songCoverImage;
public Image btm_songCoverImage;
[Header("Inspector")]
public Text songNameText;
public Text currentDifficulty;
public Text sumScore_thisDifficulty;
public Text total_gameTime;
[Header("Current Difficulty Rank")]
public Image currentDifficultyRankImage;
public RankConfig currentDifficultyRankConfig;
private int maxDifficulty = 22;
public Image c_DifficultyImage;
public Image c_imageMask;
[Header("Inspector")]
public Color _0to5d5;
public Color _5d5to11;
public Color _11to16d5;
public Color _16d5to22;
public Color _equal22;
[Header("autoplay button")]
public Button autoplayButton;
public Sprite ap_enabled;
public Sprite ap_disabled;
[Header("Inspector")]
public List<Button> difficultyButtons = new List<Button>();
[Header("Inspector")]
public List<Text> difficultyTexts = new List<Text>();
[Header("Difficulty Text Colors")]
[SerializeField] private Color difficultySelectedTextColor = Color.white;
[SerializeField] private Color difficultyUnselectedTextColor = Color.black;
[Header("enter detail page")]
public Button enterDetailPageButton;
[Header("quick enter game play")]
public Button quickEnter_gamePlay;
[Header("Inspector")]
public Sprite buttons_bgImage;
[Header("black mask")]
public Image blackMaskImage;
[Header("Quick Enter Prompt UI")]
[SerializeField] GameObject mustSelectYourIdolRoot;
[SerializeField] GameObject noChoiceRoot;
[SerializeField] GameObject choiceLessThanFiveRoot;
[SerializeField] Button promptContinueButton;
[SerializeField] Button promptConsiderButton;
[SerializeField] Button promptDoNotShowAgainButton;
[SerializeField] Button promptOkayButton;
[SerializeField] float promptFlashOnTime = 0.04f;
[SerializeField] float promptFlashOffTime = 0.03f;
[SerializeField] int promptFlashCount = 2;
[SerializeField] Color doNotShowOffButtonColor = Color.white;
[SerializeField] Color doNotShowOnButtonColor = new Color(0.227f, 0.227f, 0.227f, 1f);
[SerializeField] Color doNotShowOffTextColor = new Color(0.227f, 0.227f, 0.227f, 1f);
[SerializeField] Color doNotShowOnTextColor = Color.white;
private Coroutine _quickEnterPromptRoutine;
private bool _promptAwaitingChoice = false;
private bool _doNotShowAgainPrompt = false;
private bool _doNotShowPrefLoaded = false;
private Image autoplayButtonImage;
private SpriteState cachedAutoplaySpriteState;
private bool hasCachedAutoplaySpriteState;
private const string QuickEnterPromptSkipPrefKey = "quickenter_team_warning_skip";
private enum QuickEnterPromptDecision
{
None = 0,
Continue = 1,
Consider = 2
}
private QuickEnterPromptDecision _promptDecision = QuickEnterPromptDecision.None;
// store the original sprites so we don't overwrite the designer's setup
private List<Sprite> originalButtonSprites = new List<Sprite>();
[Header("Switch Animations")]
[SerializeField] bool play_SongSwitch_Anim = true;
[SerializeField] RectTransform songInfo_Root;
[SerializeField] float songSwitch_Time = 0.22f;
[SerializeField] float songSwitch_Move = 30f;
[SerializeField] Ease songSwitch_Ease = Ease.OutCubic;
[SerializeField] bool play_DifficultySwitch_Anim = true;
[SerializeField] float diffSwitch_Punch = 0.06f;
[SerializeField] float diffSwitch_Punch_Time = 0.18f;
[SerializeField] float diffSwitch_Text_Fade = 0.08f;
[SerializeField] bool switch_Use_Unscaled = true;
private CanvasGroup songInfo_Canvas;
private Vector2 songInfo_BasePos;
private bool songInfo_BaseCached;
private int lastSongId = -1;
[Header("Difficulty ID Roll")]
[SerializeField] string difficultyIdName = "difficultyID";
[SerializeField] string difficultyIdGroundName = "difficultyIDGround";
[SerializeField] float difficultyIdStepDelayFast = 0.03f;
[SerializeField] float difficultyIdStepDelaySlow = 0.11f;
[SerializeField] bool difficultyIdUseUnscaledTime = true;
private readonly List<Text> difficultyIdDisplayTexts = new List<Text>();
private Coroutine difficultyIdRollCoroutine;
private int difficultyIdDisplayed = int.MinValue;
private static void LogVerbose(string message)
{
if (GameConfig.verboseLogs) Debug.LogWarning(message);
}
private void ResolveDifficultyIdDisplayTexts()
{
difficultyIdDisplayTexts.Clear();
Text[] allTexts = Resources.FindObjectsOfTypeAll<Text>();
for (int i = 0; i < allTexts.Length; i++)
{
Text txt = allTexts[i];
if (txt == null) continue;
if (txt.gameObject == null || txt.gameObject.scene != gameObject.scene) continue;
bool nameMatch = string.Equals(txt.name, difficultyIdName, StringComparison.OrdinalIgnoreCase) ||
string.Equals(txt.name, difficultyIdGroundName, StringComparison.OrdinalIgnoreCase);
if (!nameMatch) continue;
txt.raycastTarget = false;
difficultyIdDisplayTexts.Add(txt);
}
}
private void ApplyDifficultyIdDisplayValue(int value)
{
if (difficultyIdDisplayTexts.Count == 0)
ResolveDifficultyIdDisplayTexts();
string text = value.ToString();
for (int i = 0; i < difficultyIdDisplayTexts.Count; i++)
{
Text t = difficultyIdDisplayTexts[i];
if (t == null) continue;
t.text = text;
t.raycastTarget = false;
}
}
private void UpdateDifficultyIdDisplayAnimated(float difficultyLevel, bool instant)
{
int target = Mathf.RoundToInt(difficultyLevel);
if (target < 0) target = 0;
if (instant || difficultyIdDisplayed == int.MinValue)
{
difficultyIdDisplayed = target;
ApplyDifficultyIdDisplayValue(target);
return;
}
if (target == difficultyIdDisplayed)
{
ApplyDifficultyIdDisplayValue(target);
return;
}
if (difficultyIdRollCoroutine != null)
{
StopCoroutine(difficultyIdRollCoroutine);
difficultyIdRollCoroutine = null;
}
difficultyIdRollCoroutine = StartCoroutine(RollDifficultyIdDisplay(difficultyIdDisplayed, target));
}
private IEnumerator RollDifficultyIdDisplay(int from, int target)
{
int distance = Mathf.Abs(target - from);
if (distance == 0)
{
ApplyDifficultyIdDisplayValue(target);
difficultyIdDisplayed = target;
difficultyIdRollCoroutine = null;
yield break;
}
int dir = target > from ? 1 : -1;
int current = from;
for (int i = 1; i <= distance; i++)
{
current += dir;
difficultyIdDisplayed = current;
ApplyDifficultyIdDisplayValue(current);
float progress = distance <= 1 ? 1f : (float)i / distance;
float eased = progress * progress; // fast at start, slower near final value
float delay = Mathf.Lerp(difficultyIdStepDelayFast, difficultyIdStepDelaySlow, eased);
if (difficultyIdUseUnscaledTime)
yield return new WaitForSecondsRealtime(Mathf.Max(0.005f, delay));
else
yield return new WaitForSeconds(Mathf.Max(0.005f, delay));
}
difficultyIdDisplayed = target;
ApplyDifficultyIdDisplayValue(target);
difficultyIdRollCoroutine = null;
}
private int GetTeamMemberCount()
{
int id1 = PlayerPrefs.GetInt("selected_heroSlot01_heroID", 0);
int id2 = PlayerPrefs.GetInt("selected_heroSlot02_heroID", 0);
int id3 = PlayerPrefs.GetInt("selected_heroSlot03_heroID", 0);
int id4 = PlayerPrefs.GetInt("selected_heroSlot04_heroID", 0);
int id5 = PlayerPrefs.GetInt("selected_heroSlot05_heroID", 0);
int count = 0;
if (id1 != 0) count++;
if (id2 != 0) count++;
if (id3 != 0) count++;
if (id4 != 0) count++;
if (id5 != 0) count++;
return count;
}
void Awake()
{
LogVerbose("selected_songInfo.Awake called");
}
void OnEnable()
{
LogVerbose("selected_songInfo.OnEnable called");
GameConfig.LoadPrefs();
EnsurePromptReferences();
LoadDoNotShowAgainPreference();
SetPromptRootVisible(false);
ApplyDoNotShowAgainVisual();
ResolveDifficultyIdDisplayTexts();
InitializeMusicControls();
BindAutoplayButton();
RefreshAutoplayButtonVisual();
// register listeners here to ensure binding even if Start wasn't called yet
if (enterDetailPageButton != null)
{
enterDetailPageButton.onClick.RemoveListener(OnEnterDetailPageClicked);
enterDetailPageButton.onClick.AddListener(OnEnterDetailPageClicked);
LogVerbose("enterDetailPageButton listener added");
}
else
{
LogVerbose("enterDetailPageButton is null in OnEnable");
}
if (quickEnter_gamePlay != null)
{
quickEnter_gamePlay.onClick.RemoveListener(OnQuickEnterClicked);
quickEnter_gamePlay.onClick.AddListener(OnQuickEnterClicked);
LogVerbose("quickEnter_gamePlay listener added");
}
else
{
LogVerbose("quickEnter_gamePlay is null in OnEnable");
}
if (rankingButton != null)
{
rankingButton.onClick.RemoveListener(OnRankingButtonClicked);
rankingButton.onClick.AddListener(OnRankingButtonClicked);
LogVerbose("rankingButton listener added");
}
else
{
LogVerbose("rankingButton is null in OnEnable");
}
BindPromptButtons();
}
void OnDisable()
{
LogVerbose("selected_songInfo.OnDisable called");
UnbindMusicControls();
UnbindAutoplayButton();
if (enterDetailPageButton != null)
{
enterDetailPageButton.onClick.RemoveListener(OnEnterDetailPageClicked);
}
if (quickEnter_gamePlay != null)
{
quickEnter_gamePlay.onClick.RemoveListener(OnQuickEnterClicked);
}
if (rankingButton != null)
{
rankingButton.onClick.RemoveListener(OnRankingButtonClicked);
}
UnbindPromptButtons();
_promptAwaitingChoice = false;
_promptDecision = QuickEnterPromptDecision.None;
if (_quickEnterPromptRoutine != null)
{
StopCoroutine(_quickEnterPromptRoutine);
_quickEnterPromptRoutine = null;
}
if (difficultyIdRollCoroutine != null)
{
StopCoroutine(difficultyIdRollCoroutine);
difficultyIdRollCoroutine = null;
}
KillTweens();
}
void OnDestroy()
{
LogVerbose("selected_songInfo.OnDestroy called");
// ensure we remove sceneLoaded subscription if still present
SceneManager.sceneLoaded -= OnSceneLoadedAfterQuickEnter;
KillTweens();
}
private void KillTweens()
{
if (songInfo_Root != null)
{
songInfo_Root.DOKill();
}
if (songInfo_Canvas != null)
{
songInfo_Canvas.DOKill();
}
if (currentDifficulty != null)
{
currentDifficulty.DOKill();
}
if (sumScore_thisDifficulty != null)
{
sumScore_thisDifficulty.DOKill();
}
if (c_DifficultyImage != null)
{
c_DifficultyImage.DOKill();
}
if (c_imageMask != null)
{
c_imageMask.DOKill();
}
if (blackMaskImage != null)
{
blackMaskImage.DOKill();
}
AudioSource globalMusicSource = GetGlobalMusicSource();
if (globalMusicSource != null)
{
globalMusicSource.DOKill();
}
}
void Start()
{
LogVerbose("selected_songInfo.Start called");
EnsurePromptReferences();
LoadDoNotShowAgainPreference();
SetPromptRootVisible(false);
ApplyDoNotShowAgainVisual();
ResolveDifficultyIdDisplayTexts();
GameConfig.LoadPrefs();
GameConfig.SetAutoPlayEnabled(false);
CacheAutoplayButtonImage();
RefreshAutoplayButtonVisual();
// ensure black mask is transparent and inactive at start
if (blackMaskImage != null)
{
var c = blackMaskImage.color;
c.a = 0f;
blackMaskImage.color = c;
blackMaskImage.gameObject.SetActive(false);
}
// capture original sprites for difficulty buttons before we make any changes
originalButtonSprites.Clear();
if (difficultyButtons != null)
{
foreach (var btn in difficultyButtons)
{
if (btn == null)
{
originalButtonSprites.Add(null);
continue;
}
var img = btn.GetComponent<Image>();
originalButtonSprites.Add(img != null ? img.sprite : null);
}
}
// attach click listeners to difficulty buttons (map list index 0..n-1 -> difficulty 0..n-1)
if (difficultyButtons != null)
{
for (int i = 0; i < difficultyButtons.Count; i++)
{
var btn = difficultyButtons[i];
if (btn == null) continue;
int idx = i; // capture
btn.onClick.RemoveAllListeners();
btn.onClick.AddListener(() => OnDifficultyButtonClicked(idx));
}
}
UpdateSelectedSongDisplay();
// Diagnostic: confirm Start ran and key refs
LogVerbose($"selected_songInfo.Start: quickEnter_gamePlay is {(quickEnter_gamePlay == null ? "NULL" : "ASSIGNED")}, enterDetail assigned={(enterDetailPageButton == null ? "NULL" : "ASSIGNED")}, blackMask assigned={(blackMaskImage == null ? "NULL" : "ASSIGNED")}");
LogVerbose($"selected_songInfo.Start: SongDataHolder.SelectedSongData is {(SongDataHolder.SelectedSongData == null ? "NULL" : SongDataHolder.SelectedSongData.songName)}");
}
private void BindAutoplayButton()
{
if (autoplayButton == null)
{
return;
}
CacheAutoplayButtonImage();
autoplayButton.onClick.RemoveListener(OnAutoplayButtonClicked);
autoplayButton.onClick.AddListener(OnAutoplayButtonClicked);
}
private void UnbindAutoplayButton()
{
if (autoplayButton == null)
{
return;
}
autoplayButton.onClick.RemoveListener(OnAutoplayButtonClicked);
}
private void CacheAutoplayButtonImage()
{
if (autoplayButton == null)
{
autoplayButtonImage = null;
return;
}
if (autoplayButtonImage == null)
{
autoplayButtonImage = autoplayButton.GetComponent<Image>();
if (autoplayButtonImage == null)
{
autoplayButtonImage = autoplayButton.targetGraphic as Image;
}
}
if (!hasCachedAutoplaySpriteState)
{
cachedAutoplaySpriteState = autoplayButton.spriteState;
hasCachedAutoplaySpriteState = true;
}
}
private void OnAutoplayButtonClicked()
{
bool next = !GameConfig.autoPlayEnabled;
GameConfig.SetAutoPlayEnabled(next);
RefreshAutoplayButtonVisual();
}
private void RefreshAutoplayButtonVisual()
{
CacheAutoplayButtonImage();
if (autoplayButtonImage == null)
{
return;
}
Sprite targetSprite = GameConfig.autoPlayEnabled ? ap_enabled : ap_disabled;
if (targetSprite == null)
{
return;
}
autoplayButtonImage.sprite = targetSprite;
autoplayButtonImage.overrideSprite = targetSprite;
autoplayButtonImage.SetAllDirty();
if (autoplayButton != null)
{
SpriteState state = hasCachedAutoplaySpriteState ? cachedAutoplaySpriteState : autoplayButton.spriteState;
state.highlightedSprite = targetSprite;
state.pressedSprite = targetSprite;
state.selectedSprite = targetSprite;
autoplayButton.spriteState = state;
if (autoplayButton.targetGraphic is Image targetGraphicImage && targetGraphicImage != autoplayButtonImage)
{
targetGraphicImage.sprite = targetSprite;
targetGraphicImage.overrideSprite = targetSprite;
targetGraphicImage.SetAllDirty();
}
}
}
private SongData GetCurrentSelectedSong()
{
if (SongDataHolder.SelectedSongData != null)
{
return SongDataHolder.SelectedSongData;
}
LogVerbose("GetCurrentSelectedSong: SongDataHolder.SelectedSongData is null");
return null;
}
public void OnRankingButtonClicked()
{
SongData song = GetCurrentSelectedSong();
if (song == null)
{
Debug.LogWarning("[selected_songInfo] Ranking button clicked, but no song is currently selected.");
return;
}
if (s_rl == null)
{
Debug.LogWarning("[selected_songInfo] Ranking button clicked, but rankingList reference is missing.");
return;
}
s_rl.Open(song.songID.ToString(), song.songName);
}
void Update()
{
UpdateMusicPlaybackProgress();
if (Input.GetKeyDown(KeyCode.F1))
{
Debug.LogWarning("selected_songInfo.DebugTrigger: F1 pressed - dumping diagnostic info");
Debug.LogWarning($"GameObject activeInHierarchy={gameObject.activeInHierarchy}, enabled={enabled}");
Debug.LogWarning($"quickEnter_gamePlay assigned={(quickEnter_gamePlay == null ? "NULL" : "ASSIGNED")}");
Debug.LogWarning($"enterDetailPageButton assigned={(enterDetailPageButton == null ? "NULL" : "ASSIGNED")}");
Debug.LogWarning($"blackMaskImage assigned={(blackMaskImage == null ? "NULL" : "ASSIGNED")}");
Debug.LogWarning($"SongDataHolder.SelectedSongData={(SongDataHolder.SelectedSongData == null ? "NULL" : SongDataHolder.SelectedSongData.songName)}");
var allButtons = UnityEngine.Object.FindObjectsByType<SongButton>(FindObjectsInactive.Exclude, FindObjectsSortMode.InstanceID);
Debug.LogWarning($"Found {allButtons.Length} SongButton instances in scene");
for (int i = 0; i < allButtons.Length; i++)
{
var sb = allButtons[i];
if (sb == null) continue;
Debug.LogWarning($" [{i}] name={sb.name} active={sb.gameObject.activeInHierarchy}");
}
}
}
private void InitializeMusicControls()
{
if (playpausebutton != null)
{
playpausebutton.onClick.RemoveListener(TogglePlayPause);
playpausebutton.onClick.AddListener(TogglePlayPause);
}
if (stopbutton != null)
{
stopbutton.onClick.RemoveListener(StopPlayback);
stopbutton.onClick.AddListener(StopPlayback);
}
if (playbarSlider != null)
{
playbarSlider.onValueChanged.RemoveListener(OnSliderValueChanged);
playbarSlider.onValueChanged.AddListener(OnSliderValueChanged);
}
}
private void UnbindMusicControls()
{
if (playpausebutton != null)
{
playpausebutton.onClick.RemoveListener(TogglePlayPause);
}
if (stopbutton != null)
{
stopbutton.onClick.RemoveListener(StopPlayback);
}
if (playbarSlider != null)
{
playbarSlider.onValueChanged.RemoveListener(OnSliderValueChanged);
}
}
private void TogglePlayPause()
{
if (preListenAS == null) return;
if (preListenAS.isPlaying)
{
preListenAS.Pause();
UpdatePlayPauseButtonSprite(false);
// 试听暂停,恢复全局音乐
FadeGlobalMusic(true);
}
else
{
if (preListenAS.clip != null)
{
preListenAS.Play();
UpdatePlayPauseButtonSprite(true);
// 试听开始,淡出全局音乐
FadeGlobalMusic(false);
}
else
{
// 鲁棒性处理:显示错误提示
gNotice.error.display(LocalizationService.Get("song.selected.no_preview", "暂无预览文件"));
}
}
}
private void StopPlayback()
{
if (preListenAS == null) return;
preListenAS.Stop();
preListenAS.time = 0;
UpdatePlayPauseButtonSprite(false);
UpdateMusicUI(0, preListenAS.clip != null ? preListenAS.clip.length : 0);
// 试听停止,恢复全局音乐
FadeGlobalMusic(true);
}
private void FadeGlobalMusic(bool fadeIn)
{
AudioSource globalMusicSource = GetGlobalMusicSource();
if (globalMusicSource != null)
{
float targetVolume = fadeIn ? 1f : 0f;
globalMusicSource.DOFade(targetVolume, 0.5f).SetUpdate(true);
}
}
private static AudioSource GetGlobalMusicSource()
{
BgmPlaybackManager bgm = BgmPlaybackManager.Instance;
if (bgm != null && bgm.audioSource != null)
{
return bgm.audioSource;
}
Music_Mgr musicManager = SceneObjectLookupCache.FindAny<Music_Mgr>();
return musicManager != null ? musicManager.AudioSource : null;
}
private void OnSliderValueChanged(float value)
{
if (preListenAS == null || preListenAS.clip == null) return;
// 只有当用户手动拖动时才改变播放时间
// 这里可以通过 Input.GetMouseButton(0) 简单判断是否在拖拽
if (Input.GetMouseButton(0))
{
float targetTime = value * preListenAS.clip.length;
preListenAS.time = targetTime;
}
}
private void UpdateMusicPlaybackProgress()
{
if (preListenAS == null || preListenAS.clip == null) return;
// 检查是否自然播放结束
if (!preListenAS.isPlaying && preListenAS.time >= preListenAS.clip.length * 0.99f)
{
StopPlayback();
return;
}
if (!preListenAS.isPlaying) return;
// 如果用户没在拖动,则自动更新进度条
if (!Input.GetMouseButton(0))
{
UpdateMusicUI(preListenAS.time, preListenAS.clip.length);
}
}
private void UpdateMusicUI(float currentTimeVal, float maxTimeVal)
{
if (playbarSlider != null && maxTimeVal > 0)
{
playbarSlider.value = currentTimeVal / maxTimeVal;
}
if (currentTime != null)
{
currentTime.text = FormatTime(currentTimeVal);
}
if (maxtime != null)
{
maxtime.text = FormatTime(maxTimeVal);
}
}
private void UpdatePlayPauseButtonSprite(bool isPlaying)
{
if (playPauseStateImage == null) return;
if (isPlaying)
{
if (pauseSprite != null) playPauseStateImage.sprite = pauseSprite;
}
else
{
if (playSprite != null) playPauseStateImage.sprite = playSprite;
}
}
private string FormatTime(float seconds)
{
int min = Mathf.FloorToInt(seconds / 60);
int sec = Mathf.FloorToInt(seconds % 60);
return string.Format("{0:00}:{1:00}", min, sec);
}
public void UpdateSelectedSongDisplay()
{
if (SongDataHolder.SelectedSongData != null)
{
var song = SongDataHolder.SelectedSongData;
Sprite topCover = song.GetResolvedRightTopImage();
Sprite bottomCover = song.GetResolvedRightBottomImage();
AudioClip previewAudio = song.GetResolvedPreviewAudio();
if (t_songCoverImage != null && topCover != null)
{
t_songCoverImage.sprite = topCover;
t_songCoverImage.enabled = true;
}
else
{
t_songCoverImage.enabled = false;
}
if (btm_songCoverImage != null && bottomCover != null)
{
btm_songCoverImage.sprite = bottomCover;
btm_songCoverImage.enabled = true;
}
else
{
btm_songCoverImage.enabled = false;
}
if (songNameText != null)
{
songNameText.text = song.songName;
}
// 更新预览音频
if (preListenAS != null)
{
preListenAS.Stop();
preListenAS.clip = previewAudio;
preListenAS.time = 0;
UpdatePlayPauseButtonSprite(false);
UpdateMusicUI(0, preListenAS.clip != null ? preListenAS.clip.length : 0);
}
// set difficulty button backgrounds according to currently selected difficulty in the song SO
SetDifficultyButtonBackground(song.thisLevel_selectedDifficultyID);
// Update currentDifficulty text and colored difficulty bar
float difficultyLevel = GetDifficultyLevel(song, song.thisLevel_selectedDifficultyID);
if (currentDifficulty != null)
{
currentDifficulty.text = difficultyLevel.ToString("0.0");
}
UpdateDifficultyIdDisplayAnimated(difficultyLevel, false);
if (c_DifficultyImage != null)
{
// fill amount is ratio of difficultyLevel / maxDifficulty
float fill = Mathf.Clamp01(difficultyLevel / (float)maxDifficulty);
c_DifficultyImage.fillAmount = fill;
}
int totalForDifficulty = GetCurrentDifficultyTotalRecord(song);
// Update sumScore_thisDifficulty based on currently selected difficulty
if (sumScore_thisDifficulty != null)
{
sumScore_thisDifficulty.text = totalForDifficulty.ToString();
}
UpdateCurrentDifficultyRankImage(totalForDifficulty);
// Update total_gameTime once from SongData.game_enterTimes (no dynamic change on difficulty switch)
if (total_gameTime != null)
{
int enterTimes = song.game_enterTimes;
total_gameTime.text = enterTimes.ToString();
}
TryPlaySongSwitchAnim(song.songID);
}
else
{
// Clear display if no song selected
if (t_songCoverImage != null)
{
t_songCoverImage.sprite = null;
t_songCoverImage.enabled = false;
}
if (btm_songCoverImage != null)
{
btm_songCoverImage.sprite = null;
btm_songCoverImage.enabled = false;
}
if (songNameText != null)
{
songNameText.text = LocalizationService.Get("song.selected.no_song", "未选择歌曲");
}
// restore original difficulty button backgrounds and text colors
SetDifficultyButtonBackground(-1);
if (sumScore_thisDifficulty != null) sumScore_thisDifficulty.text = "0";
UpdateCurrentDifficultyRankImage(0);
if (total_gameTime != null) total_gameTime.text = "0";
UpdateDifficultyIdDisplayAnimated(0f, true);
lastSongId = -1;
}
}
private int GetCurrentDifficultyTotalRecord(SongData song)
{
if (song == null)
{
return 0;
}
song.EnsurePersistentDataLoaded();
int sel = song.thisLevel_selectedDifficultyID;
int totalForDifficulty = 0;
if (song.chartFiles != null)
{
foreach (var entry in song.chartFiles)
{
if (entry == null || entry.difficulty != sel)
{
continue;
}
if (entry.totalPersonalRecordForThisDifficulty > 0)
{
totalForDifficulty = entry.totalPersonalRecordForThisDifficulty;
}
else
{
totalForDifficulty = entry.chartPersonalRecordForThisDifficulty + entry.idolPersonalRecordForThisDifficulty;
}
break;
}
}
if (totalForDifficulty <= 0)
{
int p = song.GetPersonalRecord(sel);
int i = song.GetIdolRecord(sel);
totalForDifficulty = p + i;
}
return Mathf.Max(0, totalForDifficulty);
}
private void UpdateCurrentDifficultyRankImage(int score)
{
if (currentDifficultyRankImage == null)
{
return;
}
if (currentDifficultyRankConfig == null)
{
currentDifficultyRankImage.enabled = false;
return;
}
Sprite rankSprite = currentDifficultyRankConfig.GetRankSprite(score);
currentDifficultyRankImage.sprite = rankSprite;
currentDifficultyRankImage.enabled = rankSprite != null;
}
private void EnsureOriginalSpritesCount()
{
if (difficultyButtons == null) return;
while (originalButtonSprites.Count < difficultyButtons.Count)
{
var btn = difficultyButtons[originalButtonSprites.Count];
if (btn == null) originalButtonSprites.Add(null);
else
{
var img = btn.GetComponent<Image>();
originalButtonSprites.Add(img != null ? img.sprite : null);
}
}
}
private void SetDifficultyButtonBackground(int selectedDifficulty)
{
if (difficultyButtons == null || difficultyButtons.Count == 0) return;
EnsureOriginalSpritesCount();
for (int i = 0; i < difficultyButtons.Count; i++)
{
var btn = difficultyButtons[i];
if (btn == null) continue;
var img = btn.GetComponent<Image>();
if (img == null) continue;
if (i == selectedDifficulty)
{
// apply highlight sprite to selected button
if (buttons_bgImage != null)
{
img.sprite = buttons_bgImage;
img.color = Color.white;
}
// set corresponding text (if exists) to white
if (difficultyTexts != null && i < difficultyTexts.Count && difficultyTexts[i] != null)
{
difficultyTexts[i].color = difficultySelectedTextColor;
}
}
else
{
// restore original sprite if we recorded one; otherwise leave as-is
if (i < originalButtonSprites.Count)
{
img.sprite = originalButtonSprites[i];
}
// set corresponding text (if exists) to black
if (difficultyTexts != null && i < difficultyTexts.Count && difficultyTexts[i] != null)
{
difficultyTexts[i].color = difficultyUnselectedTextColor;
}
}
}
}
private void OnDifficultyButtonClicked(int index)
{
// map button index 0..3 to song SO difficulty 0..3
if (SongDataHolder.SelectedSongData != null)
{
int selectedDifficulty = Mathf.Clamp(index, 0, 3);
SongSelectUI.SetSelectedDifficulty(selectedDifficulty);
SongDataHolder.SelectedSongData.thisLevel_selectedDifficultyID = selectedDifficulty;
PersistSelectedSongDifficulty(SongDataHolder.SelectedSongData);
SongSelectUI.ApplySelectedDifficultyToLoadedSongButtons();
// immediately update UI to reflect new selection
SetDifficultyButtonBackground(SongDataHolder.SelectedSongData.thisLevel_selectedDifficultyID);
// refresh displayed song info so currentDifficulty / difficulty bar update to new difficulty
UpdateSelectedSongDisplay();
SongButton.RefreshAllButtonsForCurrentDifficulty();
PlayDifficultySwitchAnim(index);
}
else
{
Debug.LogWarning("No song selected - difficulty button click ignored.");
}
}
private void PersistSelectedSongDifficulty(SongData song)
{
if (song == null)
{
return;
}
int difficulty = Mathf.Clamp(song.thisLevel_selectedDifficultyID, 0, 3);
PlayerPrefs.SetInt(SongButton.BuildSongSelectedDifficultyPrefsKey(song.songID), difficulty);
PlayerPrefs.Save();
}
private void EnsureSongInfoRoot()
{
if (songInfo_Root == null)
{
songInfo_Root = transform as RectTransform;
}
if (songInfo_Root == null) return;
if (songInfo_Canvas == null || songInfo_Canvas.gameObject != songInfo_Root.gameObject)
{
songInfo_Canvas = songInfo_Root.GetComponent<CanvasGroup>();
if (songInfo_Canvas == null)
{
songInfo_Canvas = songInfo_Root.gameObject.AddComponent<CanvasGroup>();
}
}
}
private void TryPlaySongSwitchAnim(int songId)
{
if (!play_SongSwitch_Anim)
{
lastSongId = songId;
return;
}
if (lastSongId == songId)
{
return;
}
lastSongId = songId;
EnsureSongInfoRoot();
if (songInfo_Root == null || songInfo_Canvas == null)
{
return;
}
songInfo_BasePos = songInfo_Root.anchoredPosition;
songInfo_BaseCached = true;
songInfo_Root.DOKill();
songInfo_Canvas.DOKill();
bool canMove = !UI_OrderedEntryAnimator.IsLayoutSensitive(songInfo_Root);
if (canMove)
{
songInfo_Root.anchoredPosition = songInfo_BasePos + new Vector2(-songSwitch_Move, 0f);
}
songInfo_Canvas.alpha = 0f;
Sequence seq = DOTween.Sequence().SetUpdate(switch_Use_Unscaled);
if (canMove)
{
seq.Join(songInfo_Root.DOAnchorPos(songInfo_BasePos, songSwitch_Time).SetEase(songSwitch_Ease));
}
seq.Join(songInfo_Canvas.DOFade(1f, songSwitch_Time));
}
private void PlayDifficultySwitchAnim(int index)
{
if (!play_DifficultySwitch_Anim)
{
return;
}
if (difficultyButtons != null && index >= 0 && index < difficultyButtons.Count)
{
var btn = difficultyButtons[index];
if (btn != null)
{
Transform t = btn.transform;
t.DOKill();
t.localScale = Vector3.one;
if (!UI_OrderedEntryAnimator.IsLayoutSensitive(btn.transform as RectTransform))
{
t.DOPunchScale(Vector3.one * diffSwitch_Punch, diffSwitch_Punch_Time, 6, 0.7f)
.SetUpdate(switch_Use_Unscaled);
}
}
}
FlashGraphic(currentDifficulty);
FlashGraphic(sumScore_thisDifficulty);
if (c_DifficultyImage != null)
{
FlashGraphic(c_DifficultyImage);
}
}
private void FlashGraphic(Graphic g)
{
if (g == null) return;
g.DOKill();
Color baseColor = g.color;
float baseAlpha = baseColor.a <= 0f ? 1f : baseColor.a;
baseColor.a = baseAlpha;
g.color = baseColor;
Sequence seq = DOTween.Sequence().SetUpdate(switch_Use_Unscaled);
seq.Append(g.DOFade(Mathf.Clamp01(baseAlpha * 0.35f), diffSwitch_Text_Fade));
seq.Append(g.DOFade(baseAlpha, diffSwitch_Text_Fade));
}
public void OnEnterDetailPageClicked()
{
if (SongDataHolder.SelectedSongData != null)
{
StartCoroutine(LoadSceneAsync("Songs_Select"));
}
else
{
Debug.LogWarning("No song selected, cannot enter detail page.");
}
}
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 void LoadDoNotShowAgainPreference()
{
if (_doNotShowPrefLoaded) return;
_doNotShowAgainPrompt = PlayerPrefs.GetInt(QuickEnterPromptSkipPrefKey, 0) == 1;
_doNotShowPrefLoaded = true;
}
private void SetDoNotShowAgainPreference(bool enabled)
{
_doNotShowAgainPrompt = enabled;
_doNotShowPrefLoaded = true;
PlayerPrefs.SetInt(QuickEnterPromptSkipPrefKey, enabled ? 1 : 0);
PlayerPrefs.Save();
ApplyDoNotShowAgainVisual();
}
private void EnsurePromptReferences()
{
if (mustSelectYourIdolRoot == null)
{
mustSelectYourIdolRoot = FindSceneObjectByExactName("\uFF01mustselectyouridol");
if (mustSelectYourIdolRoot == null)
mustSelectYourIdolRoot = FindSceneObjectByContainsName("mustselectyouridol");
}
if (mustSelectYourIdolRoot != null)
{
if (noChoiceRoot == null)
noChoiceRoot = FindChildByName(mustSelectYourIdolRoot.transform, "No choice");
if (choiceLessThanFiveRoot == null)
choiceLessThanFiveRoot = FindChildByName(mustSelectYourIdolRoot.transform, "choice < 5");
if (promptContinueButton == null)
promptContinueButton = FindButtonByName(mustSelectYourIdolRoot.transform, "Continue");
if (promptConsiderButton == null)
promptConsiderButton = FindButtonByName(mustSelectYourIdolRoot.transform, "Consider");
if (promptDoNotShowAgainButton == null)
promptDoNotShowAgainButton = FindButtonByName(mustSelectYourIdolRoot.transform, "Do not show again");
if (promptOkayButton == null)
promptOkayButton = FindButtonByName(mustSelectYourIdolRoot.transform, "okay");
}
}
private void BindPromptButtons()
{
EnsurePromptReferences();
if (promptContinueButton != null)
{
promptContinueButton.onClick.RemoveListener(OnPromptContinueClicked);
promptContinueButton.onClick.AddListener(OnPromptContinueClicked);
}
if (promptConsiderButton != null)
{
promptConsiderButton.onClick.RemoveListener(OnPromptConsiderClicked);
promptConsiderButton.onClick.AddListener(OnPromptConsiderClicked);
}
if (promptDoNotShowAgainButton != null)
{
promptDoNotShowAgainButton.onClick.RemoveListener(OnPromptDoNotShowAgainClicked);
promptDoNotShowAgainButton.onClick.AddListener(OnPromptDoNotShowAgainClicked);
}
}
private void UnbindPromptButtons()
{
if (promptContinueButton != null)
promptContinueButton.onClick.RemoveListener(OnPromptContinueClicked);
if (promptConsiderButton != null)
promptConsiderButton.onClick.RemoveListener(OnPromptConsiderClicked);
if (promptDoNotShowAgainButton != null)
promptDoNotShowAgainButton.onClick.RemoveListener(OnPromptDoNotShowAgainClicked);
}
private void OnPromptContinueClicked()
{
if (!_promptAwaitingChoice) return;
_promptDecision = QuickEnterPromptDecision.Continue;
}
private void OnPromptConsiderClicked()
{
if (!_promptAwaitingChoice) return;
_promptDecision = QuickEnterPromptDecision.Consider;
}
private void OnPromptDoNotShowAgainClicked()
{
LoadDoNotShowAgainPreference();
SetDoNotShowAgainPreference(!_doNotShowAgainPrompt);
}
private void ApplyDoNotShowAgainVisual()
{
if (promptDoNotShowAgainButton == null) return;
Graphic g = promptDoNotShowAgainButton.targetGraphic;
if (g != null)
{
g.color = _doNotShowAgainPrompt ? doNotShowOnButtonColor : doNotShowOffButtonColor;
}
Text[] texts = promptDoNotShowAgainButton.GetComponentsInChildren<Text>(true);
Color textColor = _doNotShowAgainPrompt ? doNotShowOnTextColor : doNotShowOffTextColor;
for (int i = 0; i < texts.Length; i++)
{
if (texts[i] != null)
texts[i].color = textColor;
}
}
private void SetPromptRootVisible(bool visible)
{
if (mustSelectYourIdolRoot == null) return;
mustSelectYourIdolRoot.SetActive(visible);
if (!visible)
{
SetPromptCaseState(showNoChoice: false, showChoiceLessThanFive: false);
SetPromptButtonsVisible(false);
}
}
private void SetPromptCaseState(bool showNoChoice, bool showChoiceLessThanFive)
{
if (noChoiceRoot != null) noChoiceRoot.SetActive(showNoChoice);
if (choiceLessThanFiveRoot != null) choiceLessThanFiveRoot.SetActive(showChoiceLessThanFive);
}
private void SetPromptButtonsVisible(bool visible)
{
if (promptContinueButton != null) promptContinueButton.gameObject.SetActive(visible);
if (promptConsiderButton != null) promptConsiderButton.gameObject.SetActive(visible);
if (promptDoNotShowAgainButton != null) promptDoNotShowAgainButton.gameObject.SetActive(visible);
if (promptOkayButton != null) promptOkayButton.gameObject.SetActive(false);
}
private IEnumerator FlashShowObject(GameObject go)
{
if (go == null) yield break;
go.SetActive(true);
CanvasGroup cg = go.GetComponent<CanvasGroup>();
if (cg == null) cg = go.AddComponent<CanvasGroup>();
int flashes = Mathf.Max(1, promptFlashCount);
// Force high-speed flashing even if inspector values are larger.
float onTime = Mathf.Clamp(promptFlashOnTime, 0.01f, 0.05f);
float offTime = Mathf.Clamp(promptFlashOffTime, 0.01f, 0.04f);
cg.alpha = 0f;
for (int i = 0; i < flashes; i++)
{
cg.alpha = 1f;
yield return new WaitForSecondsRealtime(onTime);
if (i < flashes - 1)
{
cg.alpha = 0f;
yield return new WaitForSecondsRealtime(offTime);
}
}
cg.alpha = 1f;
}
private GameObject FindSceneObjectByExactName(string objectName)
{
if (string.IsNullOrEmpty(objectName)) return null;
Transform[] all = Resources.FindObjectsOfTypeAll<Transform>();
for (int i = 0; i < all.Length; i++)
{
Transform t = all[i];
if (t == null || !t.gameObject.scene.IsValid()) continue;
if (string.Equals(t.name, objectName, StringComparison.OrdinalIgnoreCase))
return t.gameObject;
}
return null;
}
private GameObject FindSceneObjectByContainsName(string token)
{
if (string.IsNullOrEmpty(token)) return null;
Transform[] all = Resources.FindObjectsOfTypeAll<Transform>();
for (int i = 0; i < all.Length; i++)
{
Transform t = all[i];
if (t == null || !t.gameObject.scene.IsValid()) continue;
if (t.name.IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0)
return t.gameObject;
}
return null;
}
private GameObject FindChildByName(Transform root, string childName)
{
if (root == null || string.IsNullOrEmpty(childName)) return null;
// Prefer direct children first.
for (int i = 0; i < root.childCount; i++)
{
Transform c = root.GetChild(i);
if (c != null && string.Equals(c.name, childName, StringComparison.OrdinalIgnoreCase))
return c.gameObject;
}
// Fallback: any descendant.
Transform[] all = root.GetComponentsInChildren<Transform>(true);
for (int i = 0; i < all.Length; i++)
{
Transform c = all[i];
if (c != null && string.Equals(c.name, childName, StringComparison.OrdinalIgnoreCase))
return c.gameObject;
}
return null;
}
private Button FindButtonByName(Transform root, string buttonName)
{
if (root == null || string.IsNullOrEmpty(buttonName)) return null;
Button[] btns = root.GetComponentsInChildren<Button>(true);
for (int i = 0; i < btns.Length; i++)
{
Button b = btns[i];
if (b != null && string.Equals(b.gameObject.name, buttonName, StringComparison.OrdinalIgnoreCase))
return b;
}
return null;
}
private void OnQuickEnterClicked()
{
/* 暂时注释编队不足时的弹出逻辑
int teamCount = GetTeamMemberCount();
bool needTeamWarning = teamCount < 5;
if (needTeamWarning)
{
LoadDoNotShowAgainPreference();
if (!_doNotShowAgainPrompt)
{
ShowQuickEnterPromptForTeam(teamCount);
return;
}
}
*/
BeginQuickEnterInternal();
}
private void BeginQuickEnterInternal()
{
// use SongData from holder or SongButton; ensure selection exists
SongData sd = FindSongDataFromSelectedButton();
if (sd == null) sd = SongDataHolder.SelectedSongData;
if (sd == null)
{
LogVerbose("No song selected - cannot quick enter gameplay.");
return;
}
// Validate that the selected difficulty has a chart file before proceeding
var chart = sd.GetResolvedChartFile(sd.thisLevel_selectedDifficultyID);
if (chart == null)
{
Debug.LogError($"QuickEnter aborted: Song '{sd.songName}' has no chart file for difficulty {sd.thisLevel_selectedDifficultyID}.");
gNotice.error.display(LocalizationService.Get("song.selected.difficulty_not_found", "未找到此难度的关卡"));
return;
}
LogVerbose($"QuickEnter clicked, selected song: {sd.songName} id={sd.songID} difficulty={sd.thisLevel_selectedDifficultyID}");
// pass selection to BeatmapManager pending fields so newly loaded scene picks it up
BeatmapManager.SetPendingSong(sd, sd.thisLevel_selectedDifficultyID);
// keep selected in holder to make it accessible in new scene
SongDataHolder.SelectedSongData = sd;
PlayerPrefs.SetInt(LastEnteredSongIdPrefsKey, sd.songID);
PlayerPrefs.Save();
// start fade and load sequence
StartCoroutine(QuickEnterSequence());
}
private void ShowQuickEnterPromptForTeam(int teamCount)
{
EnsurePromptReferences();
LoadDoNotShowAgainPreference();
ApplyDoNotShowAgainVisual();
if (mustSelectYourIdolRoot == null)
{
// If UI is missing, fallback to direct quick enter to avoid dead-end.
BeginQuickEnterInternal();
return;
}
if (_quickEnterPromptRoutine != null)
{
StopCoroutine(_quickEnterPromptRoutine);
_quickEnterPromptRoutine = null;
}
_quickEnterPromptRoutine = StartCoroutine(QuickEnterPromptRoutine(teamCount));
}
private IEnumerator QuickEnterPromptRoutine(int teamCount)
{
_promptAwaitingChoice = false;
_promptDecision = QuickEnterPromptDecision.None;
SetPromptRootVisible(true);
SetPromptCaseState(showNoChoice: false, showChoiceLessThanFive: false);
SetPromptButtonsVisible(false);
ApplyDoNotShowAgainVisual();
yield return FlashShowObject(mustSelectYourIdolRoot);
bool isTeamEmpty = teamCount <= 0;
if (isTeamEmpty)
{
SetPromptCaseState(showNoChoice: true, showChoiceLessThanFive: false);
yield return FlashShowObject(noChoiceRoot);
}
else
{
SetPromptCaseState(showNoChoice: false, showChoiceLessThanFive: true);
yield return FlashShowObject(choiceLessThanFiveRoot);
}
// Buttons are displayed directly without flashing.
SetPromptButtonsVisible(true);
_promptAwaitingChoice = true;
while (_promptDecision == QuickEnterPromptDecision.None)
yield return null;
_promptAwaitingChoice = false;
QuickEnterPromptDecision decision = _promptDecision;
_promptDecision = QuickEnterPromptDecision.None;
SetPromptRootVisible(false);
_quickEnterPromptRoutine = null;
if (decision == QuickEnterPromptDecision.Continue)
{
BeginQuickEnterInternal();
}
}
private IEnumerator QuickEnterSequence()
{
LogVerbose("QuickEnterSequence started: beginning fade");
// Fade black mask in over 0.25 seconds
float duration = 0.25f;
if (blackMaskImage != null)
{
// if mask is inactive, ensure its alpha is zero before activating
if (!blackMaskImage.gameObject.activeSelf)
{
Color cc = blackMaskImage.color;
cc.a = 0f;
blackMaskImage.color = cc;
blackMaskImage.gameObject.SetActive(true);
}
else
{
// active: explicitly set alpha to 0 to start fade from transparent
Color cc = blackMaskImage.color;
cc.a = 0f;
blackMaskImage.color = cc;
}
float t = 0f;
Color c = blackMaskImage.color;
while (t < duration)
{
t += Time.deltaTime;
float alpha = Mathf.Clamp01(t / duration);
c.a = alpha;
blackMaskImage.color = c;
yield return null;
}
c.a = 1f;
blackMaskImage.color = c;
}
LogVerbose("Fade complete, loading gameplay scene...");
// selected_songInfo is scene UI and must not survive scene switching.
// Gameplay startup is already handled by BeatmapManager/GameManager via pendingSongData.
if (gTransition.LoadScene("gamePlay_gamePlay", LoadSceneMode.Single))
{
while (gTransition.IsBusy)
{
yield return null;
}
yield break;
}
var asyncOp = SceneManager.LoadSceneAsync("gamePlay_gamePlay");
if (asyncOp != null)
{
while (!asyncOp.isDone)
{
yield return null;
}
}
else
{
if (!gTransition.LoadScene("gamePlay_gamePlay", LoadSceneMode.Single))
{
SceneManager.LoadScene("gamePlay_gamePlay");
}
}
}
private SongData FindSongDataFromSelectedButton()
{
var allButtons = UnityEngine.Object.FindObjectsByType<SongButton>(FindObjectsInactive.Exclude, FindObjectsSortMode.InstanceID);
LogVerbose($"FindSongDataFromSelectedButton: found {allButtons.Length} SongButton instances");
foreach (var sb in allButtons)
{
if (sb == null) continue;
try
{
Image img = sb.selected_boarder;
float alpha = img != null ? img.color.a : -1f;
LogVerbose($"SongButton id field value for '{sb.name}': selected_boarder alpha={alpha}");
if (sb.IsSelectedForQuickEnter())
{
SongData assignedSong = sb.GetAssignedSongData();
if (assignedSong != null)
{
LogVerbose($"Selected SongButton has thisSong_so assigned: {assignedSong.songName}");
return assignedSong;
}
int id = sb.GetSongSerialId();
LogVerbose($"Selected SongButton id = {id}");
var sd = SongDataLibrary.Instance != null ? SongDataLibrary.Instance.GetSongDataByID(id) : null;
if (sd != null)
{
LogVerbose($"Found SongData by id: {sd.songName}");
return sd;
}
}
}
catch (System.Exception ex)
{
LogVerbose("Exception while inspecting SongButton: " + ex.Message);
}
}
LogVerbose("FindSongDataFromSelectedButton: no selected SongButton found");
return null;
}
private void OnSceneLoadedAfterQuickEnter(Scene scene, LoadSceneMode mode)
{
// If this object has been destroyed, unsubscribe and bail out safely
if (!this)
{
SceneManager.sceneLoaded -= OnSceneLoadedAfterQuickEnter;
return;
}
// only handle target gameplay scene
if (scene.name != "gamePlay_gamePlay")
{
// unsubscribe anyway
SceneManager.sceneLoaded -= OnSceneLoadedAfterQuickEnter;
return;
}
// If the black mask reference exists but the GameObject is disabled, ensure its alpha is 0
if (blackMaskImage != null && !blackMaskImage.gameObject.activeSelf)
{
var cc = blackMaskImage.color;
cc.a = 0f;
blackMaskImage.color = cc;
}
// perform setup: load chart JSON from selected SongData's selected difficulty and set audio
SongData selected = null;
// Prefer SongButton's assigned SO if available
selected = FindSongDataFromSelectedButton();
// Fallback to SongDataHolder
if (selected == null) selected = SongDataHolder.SelectedSongData;
if (selected == null)
{
LogVerbose("OnSceneLoadedAfterQuickEnter: SelectedSongData is null");
SceneManager.sceneLoaded -= OnSceneLoadedAfterQuickEnter;
// allow this object to be destroyed now
Destroy(gameObject);
return;
}
// find managers in the new scene
var beatmapManager = SceneObjectLookupCache.FindAny<BeatmapManager>();
var gameManager = SceneObjectLookupCache.FindAny<GameManager>();
LogVerbose($"selected song: {selected.songName} (id {selected.songID}), difficulty {selected.thisLevel_selectedDifficultyID}");
// get chart TextAsset from SongData for current selected difficulty
TextAsset chartAsset = selected.GetResolvedChartFile(selected.thisLevel_selectedDifficultyID);
if (chartAsset == null)
{
LogVerbose($"Chart file for difficulty {selected.thisLevel_selectedDifficultyID} not found on SongData {selected.songName}");
}
else
{
LogVerbose($"Chart asset found, size={chartAsset.text?.Length ?? 0} chars");
}
// parse beatmap only and assign music, but DO NOT start spawning until player starts
if (beatmapManager != null && chartAsset != null)
{
LogVerbose("Parsing chart JSON via BeatmapManager.ParseJsonOnly");
bool parsed = false;
try
{
parsed = beatmapManager.ParseJsonOnly(chartAsset.text);
}
catch (System.Exception ex)
{
LogVerbose("Exception during ParseJsonOnly: " + ex.Message + "\n" + ex.StackTrace);
}
LogVerbose($"ParseJsonOnly returned {parsed}");
if (!parsed)
{
LogVerbose("Failed to parse chart JSON from SongData chart file.");
}
else
{
LogVerbose($"Beatmap parsed: beatmap is {(beatmapManager.beatmap != null ? "NOT null" : "null")}, parsedMusicFile={beatmapManager.parsedMusicFile}, globalDelaySeconds={beatmapManager.globalDelaySeconds}");
}
}
else
{
if (beatmapManager == null) LogVerbose("BeatmapManager not found in gameplay scene");
}
// assign audio clip to GameManager.musicSource if available
if (gameManager != null)
{
AudioClip resolvedAudio = selected.GetResolvedAudioFile();
if (resolvedAudio != null && gameManager.musicSource != null)
{
gameManager.musicSource.clip = resolvedAudio;
gameManager.musicSource.loop = false;
// make sure AudioSource won't auto-play due to inspector settings
gameManager.musicSource.playOnAwake = false;
// warm up audio by playing muted so decoding happens before unpause
try
{
gameManager.musicSource.mute = true;
gameManager.musicSource.Play();
if (VerboseLogs) Debug.Log("Warmed audio playback (muted) after assigning SongData.audioFile");
}
catch (System.Exception ex)
{
if (VerboseLogs) Debug.Log("Failed to Play() for warmup: " + ex.Message);
}
LogVerbose("Assigned SongData.audioFile to GameManager.musicSource.clip (playOnAwake disabled)");
// Request PauseManager-driven pause (centralized)
RequestPauseManagerPause();
}
else
{
// try to load by parsedMusicFile similar to pressStart normal mode
if (gameManager.musicSource != null && beatmapManager != null && !string.IsNullOrEmpty(beatmapManager.parsedMusicFile))
{
LogVerbose($"Attempting Resources.Load for audio: {beatmapManager.parsedMusicFile}");
var ac = Resources.Load<AudioClip>(beatmapManager.parsedMusicFile);
if (ac != null)
{
gameManager.musicSource.clip = ac;
gameManager.musicSource.playOnAwake = false;
// warm up audio by playing muted
try
{
gameManager.musicSource.mute = true;
gameManager.musicSource.Play();
LogVerbose("Warmed audio playback (muted) after Resources.Load");
}
catch (System.Exception ex)
{
LogVerbose("Failed to Play() for warmup after Resources.Load: " + ex.Message);
}
LogVerbose("Loaded audio via Resources.Load(parsedMusicFile) and disabled playOnAwake");
RequestPauseManagerPause();
}
else
{
LogVerbose($"Resources.Load failed for '{beatmapManager.parsedMusicFile}'");
}
}
else
{
LogVerbose("No audio assigned: selected.audioFile null and parsedMusicFile empty or musicSource missing");
}
}
}
// Ensure PauseManager pause is requested; the RequestPauseManagerPause call above will have already
// attempted to pause the game if audio was set. If not yet requested, call it now to reuse PauseManager.
RequestPauseManagerPause();
// Start coroutine to wait for player input (Space) to begin playback and spawning
// Use a safe host for coroutine in case this MonoBehaviour is destroyed unexpectedly
var pauseMgr = PauseManager.Instance != null ? PauseManager.Instance : SceneObjectLookupCache.FindAny<PauseManager>();
MonoBehaviour coroutineHost = (pauseMgr as MonoBehaviour) ?? (gameManager as MonoBehaviour) ?? (beatmapManager as MonoBehaviour) ?? this;
if (coroutineHost != null)
{
LogVerbose("Starting WaitForPlayerStartAndBegin coroutine on host: " + coroutineHost.name);
coroutineHost.StartCoroutine(WaitForPlayerStartAndBegin(beatmapManager, gameManager));
}
else
{
LogVerbose("No valid coroutine host found to start WaitForPlayerStartAndBegin.");
}
// unsubscribe from sceneLoaded
SceneManager.sceneLoaded -= OnSceneLoadedAfterQuickEnter;
}
private IEnumerator WaitForPlayerStartAndBegin(BeatmapManager beatmapManager, GameManager gameManager)
{
LogVerbose("Waiting for player to press Space to start playback...");
// Wait until player presses Space
while (!Input.GetKeyDown(KeyCode.Space))
{
yield return null;
}
LogVerbose("Player pressed Space. Preparing to start playback.");
// wait for NotePool prewarm to complete (block until finished to avoid hitch)
if (NotePool.Instance != null)
{
LogVerbose("Waiting for NotePool prewarm to finish (blocking)...");
while (!NotePool.Instance.IsPrewarmed)
{
yield return null;
}
LogVerbose("NotePool prewarm completed.");
}
// ensure audio has been warmed (attempt play muted if not already playing)
if (gameManager != null && gameManager.musicSource != null && gameManager.musicSource.clip != null && !gameManager.musicSource.isPlaying)
{
try
{
gameManager.musicSource.mute = true;
gameManager.musicSource.Play();
}
catch { }
}
// Prewarm particle systems via AnimationController to avoid first-play hitch
var anim = AnimationController.Global != null ? AnimationController.Global : SceneObjectLookupCache.FindAny<AnimationController>();
if (anim != null)
{
LogVerbose("Starting AnimationController particle prewarm (will wait until complete)...");
// yield until the prewarm completes
yield return StartCoroutine(anim.PrewarmParticlesRoutine(2));
LogVerbose("AnimationController particle prewarm complete.");
}
// Unpause via PauseManager
var pauseMgr = PauseManager.Instance != null ? PauseManager.Instance : SceneObjectLookupCache.FindAny<PauseManager>();
if (pauseMgr != null) pauseMgr.Pause(false);
// small buffer to avoid hitching immediately after unpause: wait configured delay from GameManager
// Use scaled WaitForSeconds so PauseManager (Escape) will pause the countdown
float unpauseBuffer = 3f;
if (gameManager != null) unpauseBuffer = Mathf.Max(0f, gameManager.playbackStartDelay);
yield return new WaitForSeconds(unpauseBuffer);
// Start spawning notes
if (beatmapManager != null && beatmapManager.beatmap != null)
{
LogVerbose("Calling beatmapManager.LoadBeatmap");
beatmapManager.LoadBeatmap(beatmapManager.beatmap);
}
else
{
LogVerbose("Cannot start beatmap: beatmapManager or beatmap is null.");
}
// fade out black mask in gameManager if available
if (gameManager != null)
{
gameManager.StartCoroutine(gameManager.FadeOutBlackMask(0.25f));
}
// Play audio using GameManager.musicSource with delay
if (gameManager != null && gameManager.musicSource != null)
{
float delay = beatmapManager != null ? beatmapManager.globalDelaySeconds : 0f;
if (gameManager.musicSource.clip != null)
{
LogVerbose($"Starting audio playback with delay {delay}");
// Use GameManager helper to ensure unmute and start playback reliably
try { gameManager.PlayMusicWithDelay(delay); }
catch (System.Exception ex) { LogVerbose("Failed to start music via GameManager.PlayMusicWithDelay: " + ex); }
}
else
{
LogVerbose("GameManager.musicSource.clip is null; skipping audio playback.");
}
}
// done with this helper object - allow it to be destroyed
Destroy(gameObject);
yield break;
}
// Debug helper: press F1 at runtime to print diagnostic state to Console (warnings)
// Removed duplicate Update() here - consolidated into L365
// Helper to centralize PauseManager pause request so selected_songInfo reuses PauseManager implementation
private bool pauseRequested = false;
private void RequestPauseManagerPause()
{
if (pauseRequested) return;
var pm = PauseManager.Instance != null ? PauseManager.Instance : SceneObjectLookupCache.FindAny<PauseManager>();
if (pm != null)
{
pm.Pause(true);
LogVerbose("selected_songInfo: PauseManager.Pause(true) requested (centralized)");
pauseRequested = true;
}
else
{
LogVerbose("selected_songInfo: PauseManager not found when requesting pause");
}
}
// Helper to get difficulty level (float) from SongData for a given difficulty id
private float GetDifficultyLevel(SongData song, int difficultyId)
{
if (song == null) return 0f;
if (song.chartFiles != null)
{
foreach (var entry in song.chartFiles)
{
if (entry != null && entry.difficulty == difficultyId)
{
return entry.difficultyLEVEL;
}
}
}
// fallback: try dictionary if available
if (song.difficultyNumberMap != null && song.difficultyNumberMap.ContainsKey(difficultyId))
{
return (float)song.difficultyNumberMap[difficultyId];
}
return 0f;
}
private string FormatSeconds(int totalSeconds)
{
if (totalSeconds <= 0) return "0:00";
int hours = totalSeconds / 3600;
int minutes = (totalSeconds % 3600) / 60;
int seconds = totalSeconds % 60;
if (hours > 0)
return string.Format("{0}:{1:D2}:{2:D2}", hours, minutes, seconds);
return string.Format("{0}:{1:D2}", minutes, seconds);
}
}