using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; namespace BulletHellTemplate { public class UIRankingMenu : MonoBehaviour { [Header("Prefabs and Containers")] [Tooltip("Prefab for each ranking entry.")] public RankingEntry rankingEntryPrefab; [Tooltip("Container for ranking entries.")] public Transform container; [Tooltip("Container for displaying the favorite character.")] public Transform characterFavouriteContainer; [Header("Selected Player UI Elements")] [Tooltip("Text displaying the selected player's nickname.")] public TextMeshProUGUI selectedPlayerNickname; [Tooltip("Text displaying the selected player's rank.")] public TextMeshProUGUI selectedPlayerRank; [Tooltip("Image displaying the selected player's icon.")] public Image selectedPlayerIcon; [Tooltip("Image displaying the selected player's frame.")] public Image selectedPlayerFrame; [Header("Other UI Elements")] [Tooltip("Text displaying the current player's rank.")] public TextMeshProUGUI myRank; public TextMeshProUGUI localPlayerName; public Image localPlayerIcon; public Image localPlayerFrame; [Tooltip("Button to refresh the rankings.")] public Button refreshButton; [Tooltip("Image used to display cooldown on the refresh button.")] public Image cooldownImage; [Header("UI Translates Messages")] public string yourRank = "Your Rank: {0}"; public NameTranslatedByLanguage[] yourRankTranslated; public string rankNotAvaliable = "Rank not available"; public NameTranslatedByLanguage[] rankNotAvaliableTranslated; public string rank = "Rank:"; public NameTranslatedByLanguage[] rankTranslated; [Header("Events")] [Tooltip("Event invoked when the menu is opened.")] public UnityEvent OnOpenMenu; [Tooltip("Event invoked when the menu is closed.")] public UnityEvent OnCloseMenu; public static UIRankingMenu Singleton; private string currentLang; private int localRanking; private const float RANK_FETCH_MIN_INTERVAL = 60f; private bool isFetchingRankings = false; private const string PREF_RANK_LAST_FETCH_UNIX = "rank_last_fetch_unix"; // PlayerPrefs private static List> s_cachedTopPlayers = new(); private static int s_cachedLocalRank = -1; private static long s_lastFetchUnix = -1; // seconds Unix /// /// List of all instantiated ranking entries. /// private List rankingEntries = new List(); private void Awake() { if (Singleton == null) { Singleton = this; } else { Destroy(gameObject); } } private static long NowUnix() => DateTimeOffset.UtcNow.ToUnixTimeSeconds(); /// /// Called when the object becomes enabled and active. /// Selects the first ranking entry once everything is loaded. /// private void OnEnable() { OnOpenMenu.Invoke(); currentLang = LanguageManager.LanguageManager.Instance.GetCurrentLanguage(); LoadLocalPlayerInfo(); bool hadCache = BuildUIFromCache(); LoadRankings(force: !hadCache); if (rankingEntries.Count > 0) { OnRankingEntrySelected(rankingEntries[0]); } } private void OnDisable() { OnCloseMenu.Invoke(); } public void OnClickRefreshRankings() { LoadRankings(force: false); ResetCooldown(); } /// /// Resets the cooldown for the refresh button. /// public void ResetCooldown() { StartCoroutine(StartCooldown()); } /// /// Loads the rankings and displays the top players, sorted by score in descending order from memory cache. /// private bool BuildUIFromCache() { if (s_cachedTopPlayers == null || s_cachedTopPlayers.Count == 0) return false; foreach (Transform child in container) Destroy(child.gameObject); rankingEntries.Clear(); var sorted = s_cachedTopPlayers .Select(pd => { long score = 0; if (pd.TryGetValue("score", out var sObj)) long.TryParse(sObj?.ToString(), out score); string playerName = pd.TryGetValue("PlayerName", out var nObj) ? nObj?.ToString() ?? "Unknown" : "Unknown"; return new { PlayerData = pd, Score = score, PlayerName = playerName }; }) .OrderByDescending(p => p.Score) .Take(20) .ToList(); for (int i = 0; i < sorted.Count; i++) { var p = sorted[i]; var pd = p.PlayerData; RankingEntry entry = Instantiate(rankingEntryPrefab, container); string scoreStr = p.Score.ToString(); string iconId = pd.TryGetValue("PlayerIcon", out var icObj) ? icObj?.ToString() ?? "defaultIconId" : "defaultIconId"; string frameId = pd.TryGetValue("PlayerFrame", out var frObj) ? frObj?.ToString() ?? "defaultFrameId" : "defaultFrameId"; int favChar = pd.TryGetValue("PlayerCharacterFavourite", out var favObj) && int.TryParse(favObj?.ToString(), out var favParsed) ? favParsed : 0; entry.SetRankingInfo( _playerName: p.PlayerName, _score: scoreStr, rankingPosition: i + 1, playerIconId: GetPlayerIcon(iconId), playerFrameId: GetPlayerFrame(frameId), playerCharacterFavourite: favChar ); rankingEntries.Add(entry); } localRanking = (s_cachedLocalRank > 0) ? s_cachedLocalRank : localRanking; UpdateMyRankLabel(); if (rankingEntries.Count > 0) OnRankingEntrySelected(rankingEntries[0]); return true; } /// /// Loads the rankings and displays the top players, sorted by score in descending order. /// public async void LoadRankings(bool force = false) { long lastUnix = LoadLastFetchUnix(); long nowUnix = NowUnix(); long elapsed = (lastUnix >= 0) ? (nowUnix - lastUnix) : long.MaxValue; if (!force && elapsed < (long)RANK_FETCH_MIN_INTERVAL) { long remain = (long)RANK_FETCH_MIN_INTERVAL - elapsed; Debug.Log($"[Ranking] Refresh throttled. Try again in {remain}s."); return; } if (isFetchingRankings) { Debug.Log("[Ranking] Refresh already in progress."); return; } isFetchingRankings = true; ResetCooldown(); List> topPlayers; try { topPlayers = await BackendManager.Service.GetTopPlayersAsync(); } catch (Exception ex) { Debug.LogWarning($"[Ranking] Fetch failed: {ex.Message}"); isFetchingRankings = false; return; } s_cachedTopPlayers = topPlayers ?? new(); s_cachedLocalRank = await BackendManager.Service.GetPlayerRankAsync(); SaveLastFetchUnix(nowUnix); BuildUIFromCache(); isFetchingRankings = false; } public void LoadLocalPlayerInfo() { string _playerName = PlayerSave.GetPlayerName(); string _playerIcon = PlayerSave.GetPlayerIcon(); string _playerFrame = PlayerSave.GetPlayerFrame(); localPlayerName.text = _playerName; localPlayerIcon.sprite = GetPlayerIcon(_playerIcon); localPlayerFrame.sprite = GetPlayerFrame(_playerFrame); UpdateMyRankLabel(); } private void UpdateMyRankLabel() { string _yourRankTranslated = GetTranslatedString(yourRankTranslated, yourRank, currentLang); string _rankNotEvaliableTranslated = GetTranslatedString(rankNotAvaliableTranslated, rankNotAvaliable, currentLang); myRank.text = (localRanking > 0) ? string.Format(_yourRankTranslated, localRanking) : _rankNotEvaliableTranslated; } /// /// Handles the selection of a ranking entry. /// Activates the selected image and updates selected player variables. /// /// The ranking entry that was selected. public void OnRankingEntrySelected(RankingEntry selectedEntry) { // Deactivate 'selected' image on all entries foreach (var entry in rankingEntries) { entry.SetSelected(false); } // Activate 'selected' image on the selected entry selectedEntry.SetSelected(true); // Update selected player variables selectedPlayerNickname.text = selectedEntry.playerName.text; string stringRankTranslated = GetTranslatedString(rankTranslated, rank, currentLang); selectedPlayerRank.text = $"{stringRankTranslated} {selectedEntry.ranking.text}"; selectedPlayerIcon.sprite = selectedEntry.playerIcon.sprite; selectedPlayerFrame.sprite = selectedEntry.playerFrame.sprite; // Display the favorite character DisplayFavouriteCharacter(selectedEntry.characterFavourite); } /// /// Instantiates the favorite character in the specified containerPassItems. /// /// The ID of the character to instantiate. public void DisplayFavouriteCharacter(int characterId) { // Clear existing favorite character display foreach (Transform child in characterFavouriteContainer) { Destroy(child.gameObject); } // Find the character passEntryPrefab by ID and instantiate it CharacterData characterPrefab = null; foreach (var character in GameInstance.Singleton.characterData) { if (character.characterId == characterId) { characterPrefab = character; break; } } if (characterPrefab != null) { Instantiate(characterPrefab.characterModel, characterFavouriteContainer); } else { Debug.LogWarning("Character with ID " + characterId + " not found."); } if (DragRotateAndHit.Singleton != null) { DragRotateAndHit.Singleton.UpdateCharacter(); } } private static long LoadLastFetchUnix() { if (s_lastFetchUnix >= 0) return s_lastFetchUnix; if (SecurePrefs.HasKey(PREF_RANK_LAST_FETCH_UNIX)) { var str = SecurePrefs.GetDecryptedString(PREF_RANK_LAST_FETCH_UNIX, "-1"); long.TryParse(str, out s_lastFetchUnix); } return s_lastFetchUnix; } private static void SaveLastFetchUnix(long unix) { s_lastFetchUnix = unix; PlayerPrefs.SetString(PREF_RANK_LAST_FETCH_UNIX, unix.ToString()); PlayerPrefs.Save(); } public Sprite GetPlayerIcon(string iconId) { foreach (var icon in GameInstance.Singleton.iconItems) { if (icon.iconId == iconId) { return icon.icon; ; } } return null; } public Sprite GetPlayerFrame(string frameId) { foreach (var frame in GameInstance.Singleton.frameItems) { if (frame.frameId == frameId) { return frame.icon; ; } } return null; } private string GetTranslatedString(NameTranslatedByLanguage[] translations, string fallback, string currentLang) { if (translations != null) { foreach (var trans in translations) { if (!string.IsNullOrEmpty(trans.LanguageId) && trans.LanguageId.Equals(currentLang) && !string.IsNullOrEmpty(trans.Translate)) { return trans.Translate; } } } return fallback; } /// /// Starts the cooldown for the refresh button and updates the UI. /// /// private IEnumerator StartCooldown() { refreshButton.interactable = false; float start = Time.unscaledTime; const float UI_COOLDOWN = 5f; while (Time.unscaledTime - start < UI_COOLDOWN) { float t = (Time.unscaledTime - start) / UI_COOLDOWN; cooldownImage.fillAmount = 1f - t; yield return null; } cooldownImage.fillAmount = 0f; refreshButton.interactable = true; } } }