417 lines
14 KiB
C#
417 lines
14 KiB
C#
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<Dictionary<string, object>> s_cachedTopPlayers = new();
|
|
private static int s_cachedLocalRank = -1;
|
|
private static long s_lastFetchUnix = -1; // seconds Unix
|
|
|
|
/// <summary>
|
|
/// List of all instantiated ranking entries.
|
|
/// </summary>
|
|
private List<RankingEntry> rankingEntries = new List<RankingEntry>();
|
|
|
|
private void Awake()
|
|
{
|
|
if (Singleton == null)
|
|
{
|
|
Singleton = this;
|
|
}
|
|
else
|
|
{
|
|
Destroy(gameObject);
|
|
}
|
|
|
|
}
|
|
|
|
private static long NowUnix() => DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
|
|
/// <summary>
|
|
/// Called when the object becomes enabled and active.
|
|
/// Selects the first ranking entry once everything is loaded.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the cooldown for the refresh button.
|
|
/// </summary>
|
|
public void ResetCooldown()
|
|
{
|
|
StartCoroutine(StartCooldown());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the rankings and displays the top players, sorted by score in descending order from memory cache.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the rankings and displays the top players, sorted by score in descending order.
|
|
/// </summary>
|
|
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<Dictionary<string, object>> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the selection of a ranking entry.
|
|
/// Activates the selected image and updates selected player variables.
|
|
/// </summary>
|
|
/// <param name="selectedEntry">The ranking entry that was selected.</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Instantiates the favorite character in the specified containerPassItems.
|
|
/// </summary>
|
|
/// <param name="characterId">The ID of the character to instantiate.</param>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the cooldown for the refresh button and updates the UI.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
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;
|
|
}
|
|
|
|
}
|
|
}
|