using UnityEngine; using UnityEngine.UI; using UnityEngine.SceneManagement; using System.Collections; using DG.Tweening; public class SongButton : MonoBehaviour { private static readonly bool VerboseLogs = false; private const string SongSelectedIndexPrefsPrefix = "song_selected_"; private const string SongSelectedIdPrefsPrefix = "song_selected_id_"; private const string SongSelectedDifficultyPrefsPrefix = "song_selected_difficulty_"; [Header("thisSong_so")] public ScriptableObject thisSong_so; [Header("Inspector")] public Text songNameText; public Text multiNameText; public Text joinTime_and_dlcName_Text; public Text songOverallDescriptionText; public Text highestScoreText; public Text highestScoreDifficultyText; public Text difficultyIDText; public Text songIDText; public Image buttonImage; public Image scoreLevelImage; [Header("Difficulty Level Images")] public Image lvl_img_01; public Image lvl_img_02; public Image lvl_img_03; [Header("Inspector")] public Image profileImage; public int song_songSerialID; [Header("select mark")] public Image selected_boarder; public Image fade_image; [Header("Rank Configuration")] public RankConfig rankConfig; [Header("Fade Image Settings")] public float minAlpha = 0f; public float maxAlpha = 1f; public float blinkDuration = 0.5f; private static SongButton currentSelected; private Coroutine fadeCoroutine; private Coroutine blinkCoroutine; public bool IsSelectedForQuickEnter() { return selected_boarder != null && selected_boarder.color.a > 0.5f; } public SongData GetAssignedSongData() { return thisSong_so as SongData; } public int GetSongSerialId() { return song_songSerialID; } public static string BuildSongSelectedIndexPrefsKey(string dlcKey) { string safeDlcKey = string.IsNullOrEmpty(dlcKey) ? "default_0" : dlcKey; return SongSelectedIndexPrefsPrefix + safeDlcKey; } public static string BuildSongSelectedIdPrefsKey(string dlcKey) { string safeDlcKey = string.IsNullOrEmpty(dlcKey) ? "default_0" : dlcKey; return SongSelectedIdPrefsPrefix + safeDlcKey; } public static string BuildSongSelectedDifficultyPrefsKey(int songId) { return SongSelectedDifficultyPrefsPrefix + Mathf.Max(0, songId); } public void Start() { if (selected_boarder != null) { Color c = selected_boarder.color; c.a = 0f; selected_boarder.color = c; } if (fade_image != null) { Color c = fade_image.color; c.a = 0f; fade_image.color = c; } } private void OnDisable() { StopVisualCoroutines(); } private void OnDestroy() { StopVisualCoroutines(); if (currentSelected == this) { currentSelected = null; } } private void StopVisualCoroutines() { if (fadeCoroutine != null) { StopCoroutine(fadeCoroutine); fadeCoroutine = null; } if (blinkCoroutine != null) { StopCoroutine(blinkCoroutine); blinkCoroutine = null; } } public void SetButtonData(string songName, int highestScore, int difficultyID, Sprite songSprite, int songID) { SongData sd = thisSong_so as SongData; if (sd != null) sd.EnsurePersistentDataLoaded(); if (SongDataLibrary.Instance != null && SongDataLibrary.Instance.IsLoaded) sd = SongDataLibrary.Instance.GetSongDataByID(songID) ?? sd; if (sd != null) { Sprite resolvedIllustration = sd.GetResolvedIllustration(); Sprite resolvedProfile = sd.GetResolvedCardProfileImage(); if (songNameText != null) songNameText.text = sd.songName; if (multiNameText != null) multiNameText.text = LocalizationService.GetFormat("song.button.artist_info", sd.artistName, sd.painter, sd.level_creator); if (joinTime_and_dlcName_Text != null) { string join = string.IsNullOrEmpty(sd.thisLevel_addDateString) ? "Unknown" : sd.thisLevel_addDateString; string dlc = string.IsNullOrEmpty(sd.belongsTo_whichDLC) ? "Unknown DLC" : sd.belongsTo_whichDLC; joinTime_and_dlcName_Text.text = LocalizationService.GetFormat("song.button.release_info", join, dlc); } if (songOverallDescriptionText != null) { songOverallDescriptionText.text = string.IsNullOrEmpty(sd.thisLevel_overallDescription) ? "" : sd.thisLevel_overallDescription; } RefreshCurrentDifficultyDisplay(sd, difficultyID); if (buttonImage != null) buttonImage.sprite = resolvedIllustration != null ? resolvedIllustration : songSprite; if (profileImage != null) { profileImage.sprite = resolvedProfile; if (resolvedProfile != null) { profileImage.color = new Color(profileImage.color.r, profileImage.color.g, profileImage.color.b, 1f); profileImage.enabled = true; } else { profileImage.enabled = false; } } song_songSerialID = songID; if (songIDText != null) songIDText.text = LocalizationService.GetFormat("song.button.song_id", song_songSerialID); } else { if (songNameText != null) songNameText.text = songName; if (highestScoreText != null) highestScoreText.text = LocalizationService.GetFormat("song.button.highest_score", highestScore); if (difficultyIDText != null) difficultyIDText.text = difficultyID.ToString(); if (buttonImage != null) buttonImage.sprite = songSprite; song_songSerialID = songID; if (songIDText != null) songIDText.text = LocalizationService.GetFormat("song.button.song_id", song_songSerialID); } } public void RefreshCurrentDifficultyDisplay() { SongData sd = null; if (SongDataLibrary.Instance != null && SongDataLibrary.Instance.IsLoaded) { sd = SongDataLibrary.Instance.GetSongDataByID(song_songSerialID); } if (sd == null) { sd = thisSong_so as SongData; } RefreshCurrentDifficultyDisplay(sd, 0); } public static void RefreshAllButtonsForCurrentDifficulty() { SongButton[] buttons = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); for (int i = 0; i < buttons.Length; i++) { if (buttons[i] == null) continue; buttons[i].RefreshCurrentDifficultyDisplay(); } } private void RefreshCurrentDifficultyDisplay(SongData sd, int fallbackDifficulty) { if (sd == null) { if (highestScoreText != null) highestScoreText.text = "0"; if (highestScoreDifficultyText != null) highestScoreDifficultyText.text = ""; if (difficultyIDText != null) difficultyIDText.text = fallbackDifficulty.ToString(); ApplyScoreRankSprite(0); ClearDifficultyRankImages(); return; } sd.EnsurePersistentDataLoaded(); int currentDifficulty = ResolveCurrentDisplayDifficulty(sd, fallbackDifficulty); ChartFileEntry entry = FindChartEntry(sd, currentDifficulty); float difficultyLevel = entry != null ? entry.difficultyLEVEL : currentDifficulty; int totalForDifficulty = GetTotalRecordForDifficulty(sd, currentDifficulty, entry); string difficultyName = GetDifficultyCode(currentDifficulty); if (highestScoreText != null) highestScoreText.text = totalForDifficulty.ToString(); if (highestScoreDifficultyText != null) highestScoreDifficultyText.text = "(" + difficultyName + ")"; if (difficultyIDText != null) { string difficultyLevelText = difficultyLevel.ToString("0.#"); difficultyIDText.text = string.IsNullOrEmpty(difficultyName) ? difficultyLevelText : difficultyLevelText + " (" + difficultyName + ")"; } ApplyScoreRankSprite(totalForDifficulty); ClearDifficultyRankImages(); } private int ResolveCurrentDisplayDifficulty(SongData sd, int fallbackDifficulty) { if (sd == null) { return Mathf.Clamp(fallbackDifficulty, 0, 3); } int difficulty = Mathf.Clamp(sd.thisLevel_selectedDifficultyID, 0, 3); string difficultyKey = BuildSongSelectedDifficultyPrefsKey(sd.songID); if (PlayerPrefs.HasKey(difficultyKey)) { difficulty = Mathf.Clamp(PlayerPrefs.GetInt(difficultyKey, difficulty), 0, 3); } if (FindChartEntry(sd, difficulty) != null || sd.GetResolvedChartFile(difficulty) != null) { return difficulty; } int fallback = Mathf.Clamp(fallbackDifficulty, 0, 3); if (FindChartEntry(sd, fallback) != null || sd.GetResolvedChartFile(fallback) != null) { return fallback; } return GetFirstAvailableDifficulty(sd); } private string GetDifficultyCode(int difficulty) { switch (difficulty) { case 0: return "EZ"; case 1: return "HD"; case 2: return "IN"; case 3: return "IM"; default: return difficulty.ToString(); } } private ChartFileEntry FindChartEntry(SongData sd, int difficulty) { if (sd == null || sd.chartFiles == null) { return null; } for (int i = 0; i < sd.chartFiles.Count; i++) { ChartFileEntry entry = sd.chartFiles[i]; if (entry != null && entry.difficulty == difficulty) { return entry; } } return null; } private int GetTotalRecordForDifficulty(SongData sd, int difficulty, ChartFileEntry entry) { int total = entry != null ? entry.totalPersonalRecordForThisDifficulty : 0; if (total <= 0 && entry != null) { total = entry.chartPersonalRecordForThisDifficulty + entry.idolPersonalRecordForThisDifficulty; } if (total <= 0 && sd != null) { total = sd.GetPersonalRecord(difficulty) + sd.GetIdolRecord(difficulty); } return Mathf.Max(0, total); } private void ApplyScoreRankSprite(int score) { if (scoreLevelImage == null) { return; } if (rankConfig != null) { scoreLevelImage.sprite = rankConfig.GetRankSprite(score); scoreLevelImage.enabled = scoreLevelImage.sprite != null; } else { scoreLevelImage.enabled = false; Debug.LogWarning("SongButton: rankConfig is not assigned!"); } } private void ClearDifficultyRankImages() { Image[] rankImages = { lvl_img_01, lvl_img_02, lvl_img_03 }; for (int i = 0; i < rankImages.Length; i++) { if (rankImages[i] == null) continue; rankImages[i].enabled = false; } } public void OnSongButtonClick() { SongData selectedSong = null; if (SongDataLibrary.Instance != null && SongDataLibrary.Instance.IsLoaded) { selectedSong = SongDataLibrary.Instance.GetSongDataByID(song_songSerialID); } if (selectedSong == null) { selectedSong = thisSong_so as SongData; } if (selectedSong != null) { selectedSong.EnsurePersistentDataLoaded(); selectedSong.GetAbsoluteHighestScore(out int bestTotal, out int bestDiff, out string bestDiffName); selectedSong.thisLevel_selectedDifficultyID = ResolvePreferredDifficulty(selectedSong, bestDiff); if (currentSelected != null && currentSelected != this) { currentSelected.FadeOutSelectedBorder(); } currentSelected = this; FadeInSelectedBorder(); SongDataHolder.SelectedSongData = selectedSong; RefreshCurrentDifficultyDisplay(selectedSong, selectedSong.thisLevel_selectedDifficultyID); var selectedInfo = SceneObjectLookupCache.FindAny(); if (selectedInfo != null) { selectedInfo.UpdateSelectedSongDisplay(); } // Save per-DLC selected song index to PlayerPrefs string dlcKey = SongSelectUI.CurrentDlcKey; if (string.IsNullOrEmpty(dlcKey)) dlcKey = "default_0"; string prefsKey = BuildSongSelectedIndexPrefsKey(dlcKey); int index = GetIndexAmongSongButtons(); PlayerPrefs.SetInt(prefsKey, index); PlayerPrefs.SetInt(BuildSongSelectedIdPrefsKey(dlcKey), selectedSong.songID); PlayerPrefs.SetInt(BuildSongSelectedDifficultyPrefsKey(selectedSong.songID), selectedSong.thisLevel_selectedDifficultyID); PlayerPrefs.Save(); if (VerboseLogs) Debug.Log($"SongButton: saved selected song index {index} for key {prefsKey}"); } else { Debug.LogError("未找到对应的 SongData, songID: " + song_songSerialID); } } private int ResolvePreferredDifficulty(SongData selectedSong, int bestDiff) { if (selectedSong == null) { return 0; } int songId = selectedSong.songID; int globalDifficulty = SongSelectUI.GetSelectedDifficulty(); if (IsDifficultyAvailable(selectedSong, globalDifficulty)) { return globalDifficulty; } string difficultyKey = BuildSongSelectedDifficultyPrefsKey(songId); if (PlayerPrefs.HasKey(difficultyKey)) { int savedDifficulty = PlayerPrefs.GetInt(difficultyKey, selectedSong.thisLevel_selectedDifficultyID); if (IsDifficultyAvailable(selectedSong, savedDifficulty)) { return savedDifficulty; } } if (IsDifficultyAvailable(selectedSong, selectedSong.thisLevel_selectedDifficultyID)) { return selectedSong.thisLevel_selectedDifficultyID; } if (bestDiff >= 0 && IsDifficultyAvailable(selectedSong, bestDiff)) { return bestDiff; } return GetFirstAvailableDifficulty(selectedSong); } private bool IsDifficultyAvailable(SongData song, int difficulty) { if (song == null || difficulty < 0) { return false; } return song.GetResolvedChartFile(difficulty) != null; } private int GetFirstAvailableDifficulty(SongData song) { if (song == null) { return 0; } if (song.chartFiles != null) { for (int i = 0; i < song.chartFiles.Count; i++) { ChartFileEntry entry = song.chartFiles[i]; if (entry == null) { continue; } if (song.GetResolvedChartFile(entry.difficulty) != null) { return Mathf.Max(0, entry.difficulty); } } } for (int difficulty = 0; difficulty <= 3; difficulty++) { if (song.GetResolvedChartFile(difficulty) != null) { return difficulty; } } return 0; } private int GetIndexAmongSongButtons() { // Use parent traversal to find only SongButton siblings, to avoid mismatch when other UI children exist. var parent = transform.parent; if (parent == null) return 0; int idx = 0; for (int i = 0; i < parent.childCount; i++) { var child = parent.GetChild(i); var sb = child != null ? child.GetComponent() : null; if (sb == null) continue; if (sb == this) return idx; idx++; } return 0; } private void FadeInSelectedBorder() { if (selected_boarder != null) { if (fadeCoroutine != null) StopCoroutine(fadeCoroutine); fadeCoroutine = StartCoroutine(FadeImage(selected_boarder, selected_boarder.color.a, 1f, 0.3f)); } if (fade_image != null) { if (blinkCoroutine != null) StopCoroutine(blinkCoroutine); blinkCoroutine = StartCoroutine(BlinkFadeImage()); } } private void FadeOutSelectedBorder() { if (selected_boarder != null) { if (fadeCoroutine != null) StopCoroutine(fadeCoroutine); fadeCoroutine = StartCoroutine(FadeImage(selected_boarder, selected_boarder.color.a, 0f, 0.3f)); } if (fade_image != null) { if (blinkCoroutine != null) StopCoroutine(blinkCoroutine); fadeCoroutine = StartCoroutine(FadeImage(fade_image, fade_image.color.a, 0f, 0.3f)); } } private IEnumerator FadeImage(Image img, float startAlpha, float endAlpha, float duration) { if (img == null) yield break; float time = 0f; Color color = img.color; while (time < duration) { time += Time.deltaTime; float alpha = Mathf.Lerp(startAlpha, endAlpha, time / duration); color.a = alpha; img.color = color; yield return null; } color.a = endAlpha; img.color = color; } private IEnumerator BlinkFadeImage() { if (fade_image == null) yield break; Color color = fade_image.color; while (true) { float time = 0f; while (time < blinkDuration / 2f) { time += Time.deltaTime; float alpha = Mathf.Lerp(minAlpha, maxAlpha, time / (blinkDuration / 2f)); color.a = alpha; fade_image.color = color; yield return null; } time = 0f; while (time < blinkDuration / 2f) { time += Time.deltaTime; float alpha = Mathf.Lerp(maxAlpha, minAlpha, time / (blinkDuration / 2f)); color.a = alpha; fade_image.color = color; yield return null; } } } }