using System; using System.Collections; using UnityEngine; using UnityEngine.Networking; using UnityEngine.SceneManagement; using UnityEngine.UI; using UnityEngine.EventSystems; public class GlobalCombatSceneManager : MonoBehaviour { [SerializeField] private string homeSceneName = "HomeScene"; [SerializeField] private string walletSceneName = "WalletScene"; [SerializeField] private string loginSceneName = "LoginScene"; [SerializeField] private string globalCombatGameplaySceneName = "GameplayClean"; [SerializeField] private string getActiveEventUrl = "https://auth.ludokobo.com/global-combat/get-active-event.php"; [SerializeField] private string joinEventUrl = "https://auth.ludokobo.com/global-combat/join-event.php"; [SerializeField] private string getSessionUrl = "https://auth.ludokobo.com/global-combat/get-session.php"; [SerializeField] private string getMyResultUrl = "https://auth.ludokobo.com/global-combat/get-my-result.php"; [SerializeField] private string getLeaderboardUrl = "https://auth.ludokobo.com/global-combat/get-leaderboard.php"; [SerializeField] private string getHomeCardStateUrl = "https://auth.ludokobo.com/home/get-global-combat-home.php"; [SerializeField] private bool verboseLogs = false; private const string SceneName = "GlobalCombatScene"; private const string GlobalCombatStartOverlayUntilTicksKey = "LD_GLOBAL_COMBAT_START_OVERLAY_UNTIL_TICKS"; private const string GlobalCombatStartOverlayDurationKey = "LD_GLOBAL_COMBAT_START_OVERLAY_DURATION_SECONDS"; private const string GlobalCombatStartOverlayStartedTicksKey = "LD_GLOBAL_COMBAT_START_OVERLAY_STARTED_TICKS"; private bool isBusy; private string statusMessage = "Loading Global Combat..."; private string lastError = ""; private GlobalCombatActiveEventResponse activeEventResponse; private GlobalCombatJoinResponse latestJoinResponse; private GlobalCombatSessionResponse latestSessionResponse; private GlobalCombatGetMyResultResponse latestResultResponse; private GlobalCombatLeaderboardResponse latestLeaderboardResponse; private GlobalCombatHomeCardStateResponse latestHomeCardStateResponse; private string latestHomeCardButtonState = ""; private string latestHomeCardButtonLabel = ""; private Canvas runtimeCanvas; private Text titleLabel; private Text subtitleLabel; private Text statusLabel; private Text eventHeaderLabel; private Text eventInfoLabel; private GameObject eventHeroPanel; private Text walletHeaderLabel; private Text walletLabel; private Text sessionHeaderLabel; private Text sessionLabel; private Text resultHeaderLabel; private Text resultLabel; private Text leaderboardHeaderLabel; private Text leaderboardLabel; private GameObject leaderboardCard; private GameObject leaderboardDropdownPanel; private Text leaderboardDropdownTitleLabel; private Text leaderboardDropdownBodyLabel; private Button leaderboardDropdownCloseButton; private ScrollRect leaderboardDropdownScrollRect; private RectTransform leaderboardDropdownContentRect; private const string LeaderboardProfileRowPrefix = "LeaderboardProfileInfoRow_"; private bool leaderboardDropdownOpen; private bool resultModalOpen; private Text footerHintLabel; private GameObject resultModalPanel; private Text resultModalTitleLabel; private Text resultModalBodyLabel; private Button resultModalCloseButton; private Button resultModalPlayAgainButton; private Button refreshButton; private Button joinButton; private Button sessionButton; private Button resultButton; private Button leaderboardButton; private Button homeButton; private Button buyCoinsButton; private bool postRunModalDismissed; private bool pendingLeaderboardAutoOpen; private bool playRequestInProgress; private bool launchedFromHomeHandoff; private int handoffRetryEntryId; private string handoffRetryEntryRef = ""; private Coroutine autoStartTimeoutRoutine; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] private static void AutoInstall() { Scene activeScene = SceneManager.GetActiveScene(); if (!string.Equals(activeScene.name, SceneName, StringComparison.Ordinal)) return; if (FindFirstObjectByType() != null) return; GameObject go = new GameObject("GlobalCombatSceneManager"); go.AddComponent(); } private void Start() { BuildUiIfNeeded(); if (!HasValidSession()) { ForceLogoutToLogin(); return; } if (TryConsumeLaunchHandoff()) return; RefreshUi(); pendingLeaderboardAutoOpen = GlobalCombatSessionData.OpenLeaderboardOnReturn; if (pendingLeaderboardAutoOpen) GlobalCombatSessionData.SetOpenLeaderboardOnReturn(false); StartCoroutine(LoadActiveEventCoroutine()); } private bool TryConsumeLaunchHandoff() { if (!GlobalCombatLaunchHandoff.Has) return false; bool autoStart = GlobalCombatLaunchHandoff.AutoStart; bool skipLanding = GlobalCombatLaunchHandoff.SkipLanding; int entryId = GlobalCombatLaunchHandoff.EntryId; string entryRef = GlobalCombatLaunchHandoff.EntryRef; Debug.Log("[GlobalCombatScene] handoff autoStart=" + autoStart + " skipLanding=" + skipLanding); GlobalCombatLaunchHandoff.Clear(); if (!autoStart || !skipLanding || entryId <= 0) return false; launchedFromHomeHandoff = true; handoffRetryEntryId = entryId; handoffRetryEntryRef = entryRef ?? ""; GlobalCombatTransitionOverlay.Show("GLOBAL COMBAT", "Preparing Global Combat..."); if (runtimeCanvas != null) runtimeCanvas.gameObject.SetActive(false); StartCoroutine(AutoStartFromHandoffCoroutine()); return true; } private IEnumerator AutoStartFromHandoffCoroutine() { Debug.Log("[GlobalCombatStart] phase=Preparing Global Combat"); StartAutoStartWatchdog(15f); GlobalCombatTransitionOverlay.Show("GLOBAL COMBAT", "Preparing Global Combat..."); yield return null; yield return LoadSessionAndContinueCoroutine(); StopAutoStartWatchdog(); } private bool HasValidSession() { return AuthService.IsLoggedIn(); } private IEnumerator LoadActiveEventCoroutine() { if (isBusy) yield break; isBusy = true; statusMessage = "Loading active event..."; lastError = ""; using (UnityWebRequest request = UnityWebRequest.Get(getActiveEventUrl)) { PrepareAuthRequest(request); yield return request.SendWebRequest(); if (HandleUnauthorized(request)) yield break; string body = request.downloadHandler != null ? request.downloadHandler.text : ""; LogVerbose("GlobalCombat get-active-event HTTP " + request.responseCode + " => " + body); if (HasRequestError(request)) { Fail("Failed to load Global Combat event."); yield break; } activeEventResponse = ParseJson(body); if (activeEventResponse == null || !activeEventResponse.success) { Fail(activeEventResponse != null ? activeEventResponse.message : "Invalid Global Combat event response."); yield break; } if (activeEventResponse.@event == null) { statusMessage = "No active Global Combat event right now."; latestJoinResponse = null; latestSessionResponse = null; latestResultResponse = null; latestLeaderboardResponse = null; isBusy = false; RefreshUi(); yield break; } statusMessage = "Active event loaded."; } isBusy = false; RefreshUi(); // Keep this scene's main action button in sync with the HomeScene Global Combat card. StartCoroutine(LoadHomeCardStateCoroutine()); if (ShouldAutoRefreshPostRunData()) StartCoroutine(LoadPostRunDataCoroutine()); else if (pendingLeaderboardAutoOpen) StartCoroutine(OpenLeaderboardAfterLoadCoroutine()); else StartCoroutine(LoadMyResultCoroutine()); } private IEnumerator LoadHomeCardStateCoroutine() { if (string.IsNullOrWhiteSpace(getHomeCardStateUrl)) yield break; string url = getHomeCardStateUrl; string countryCode = FirstText(PlayerPrefs.GetString("country_code", ""), PlayerPrefs.GetString("selected_country_code", ""), PlayerPrefs.GetString("player_country_code", ""), "GM"); if (!url.Contains("?")) url += "?country_code=" + UnityWebRequest.EscapeURL(countryCode); using (UnityWebRequest request = UnityWebRequest.Get(url)) { PrepareAuthRequest(request); request.timeout = 12; yield return request.SendWebRequest(); if (HandleUnauthorized(request)) yield break; string body = request.downloadHandler != null ? request.downloadHandler.text : ""; LogVerbose("GlobalCombat home-card-state HTTP " + request.responseCode + " => " + body); if (HasRequestError(request)) { Debug.LogWarning("[GlobalCombatScene] Home card state refresh failed: " + request.error + " HTTP " + request.responseCode); yield break; } GlobalCombatHomeCardStateResponse parsed = ParseJson(body); if (parsed == null || !parsed.success) { Debug.LogWarning("[GlobalCombatScene] Home card state response invalid: " + (parsed != null ? parsed.message : body)); yield break; } latestHomeCardStateResponse = parsed; latestHomeCardButtonState = ResolveHomeCardButtonState(parsed); latestHomeCardButtonLabel = ResolveHomeCardButtonLabel(parsed, latestHomeCardButtonState); SyncHomeCardPlayableEntryToLocal(parsed); Debug.Log("[GlobalCombatScene] synced Home card state=" + latestHomeCardButtonState + " label=" + latestHomeCardButtonLabel); RefreshUi(); } } private IEnumerator JoinActiveEventCoroutine() { if (isBusy || playRequestInProgress) yield break; if (activeEventResponse == null || activeEventResponse.@event == null) { statusMessage = "No Global Combat event is ready yet. Refresh and try again."; RefreshUi(); yield break; } isBusy = true; playRequestInProgress = true; statusMessage = "Joining Global Combat..."; lastError = ""; GlobalCombatTransitionOverlay.Show("GLOBAL COMBAT", "Joining Global Combat..."); string json = "{\"event_id\":" + activeEventResponse.@event.event_id + "}"; using (UnityWebRequest request = new UnityWebRequest(joinEventUrl, UnityWebRequest.kHttpVerbPOST)) { byte[] payload = System.Text.Encoding.UTF8.GetBytes(json); request.uploadHandler = new UploadHandlerRaw(payload); request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); PrepareAuthRequest(request); yield return request.SendWebRequest(); if (HandleUnauthorized(request)) yield break; string body = request.downloadHandler != null ? request.downloadHandler.text : ""; LogVerbose("GlobalCombat join-event HTTP " + request.responseCode + " => " + body); latestJoinResponse = ParseJson(body); if (HasRequestError(request)) { Fail(latestJoinResponse != null && !string.IsNullOrWhiteSpace(latestJoinResponse.message) ? latestJoinResponse.message : "Failed to join Global Combat."); yield break; } if (latestJoinResponse == null || !latestJoinResponse.success || latestJoinResponse.entry == null) { Fail(latestJoinResponse != null ? latestJoinResponse.message : "Invalid join response."); playRequestInProgress = false; yield break; } GlobalCombatJoinEventData joinedEvent = latestJoinResponse.@event ?? BuildJoinEventDataFromActiveEvent(); GlobalCombatSessionData.MarkJoined(joinedEvent, latestJoinResponse.entry); latestHomeCardButtonState = "play"; latestHomeCardButtonLabel = "Play"; postRunModalDismissed = false; statusMessage = "Global Combat attempt joined."; GlobalCombatTransitionOverlay.UpdateMessage("Attempt joined. Loading run..."); } playRequestInProgress = false; isBusy = false; RefreshUi(); } private IEnumerator LoadSessionCoroutine() { int entryId = latestJoinResponse != null && latestJoinResponse.entry != null ? latestJoinResponse.entry.entry_id : (GlobalCombatSessionData.EntryId > 0 ? GlobalCombatSessionData.EntryId : handoffRetryEntryId); if (isBusy) yield break; if (entryId <= 0) { Fail("No Global Combat entry is ready. Join the event again."); yield break; } isBusy = true; statusMessage = "Loading Global Combat run..."; lastError = ""; Debug.Log("[GlobalCombatStart] phase=Creating solo run entry_id=" + entryId); GlobalCombatTransitionOverlay.Show("GLOBAL COMBAT", "Creating solo run..."); using (UnityWebRequest request = UnityWebRequest.Get(getSessionUrl + "?entry_id=" + entryId)) { PrepareAuthRequest(request); request.timeout = 12; GlobalCombatTransitionOverlay.UpdateMessage("Preparing solo run..."); yield return request.SendWebRequest(); if (HandleUnauthorized(request)) yield break; string body = request.downloadHandler != null ? request.downloadHandler.text : ""; LogVerbose("GlobalCombat get-session HTTP " + request.responseCode + " => " + body); if (HasRequestError(request)) { Debug.LogWarning("[GlobalCombatStart] get-session failed: " + request.error + " HTTP " + request.responseCode); Fail("Failed to load Global Combat run."); yield break; } latestSessionResponse = ParseJson(body); if (latestSessionResponse == null || !latestSessionResponse.success || latestSessionResponse.run == null) { Fail(latestSessionResponse != null ? latestSessionResponse.message : "Invalid session response."); yield break; } Debug.Log("[GlobalCombatStart] phase=Loading board"); GlobalCombatSessionData.Save(latestSessionResponse); GlobalCombatTransitionOverlay.UpdateMessage("Loading board..."); statusMessage = "Run ready. Entry #" + latestSessionResponse.entry.entry_no + " | Moves " + latestSessionResponse.run.moves_remaining + "/" + latestSessionResponse.run.moves_allowed; } isBusy = false; RefreshUi(); } private IEnumerator LoadMyResultCoroutine() { int eventId = activeEventResponse != null && activeEventResponse.@event != null ? activeEventResponse.@event.event_id : GlobalCombatSessionData.EventId; if (isBusy || eventId <= 0) yield break; isBusy = true; statusMessage = "Loading my results..."; lastError = ""; using (UnityWebRequest request = UnityWebRequest.Get(getMyResultUrl + "?event_id=" + eventId)) { PrepareAuthRequest(request); yield return request.SendWebRequest(); if (HandleUnauthorized(request)) yield break; string body = request.downloadHandler != null ? request.downloadHandler.text : ""; LogVerbose("GlobalCombat get-my-result HTTP " + request.responseCode + " => " + body); if (HasRequestError(request)) { Fail("Failed to load Global Combat result."); yield break; } latestResultResponse = ParseJson(body); if (latestResultResponse == null || !latestResultResponse.success) { Fail(latestResultResponse != null ? latestResultResponse.message : "Invalid result response."); yield break; } statusMessage = "Results loaded."; } isBusy = false; RefreshUi(); } private IEnumerator LoadLeaderboardCoroutine() { int eventId = activeEventResponse != null && activeEventResponse.@event != null ? activeEventResponse.@event.event_id : GlobalCombatSessionData.EventId; if (isBusy || eventId <= 0) yield break; isBusy = true; statusMessage = "Loading leaderboard..."; lastError = ""; using (UnityWebRequest request = UnityWebRequest.Get(getLeaderboardUrl + "?event_id=" + eventId + "&limit=10")) { PrepareAuthRequest(request); yield return request.SendWebRequest(); if (HandleUnauthorized(request)) yield break; string body = request.downloadHandler != null ? request.downloadHandler.text : ""; LogVerbose("GlobalCombat get-leaderboard HTTP " + request.responseCode + " => " + body); if (HasRequestError(request)) { Fail("Failed to load leaderboard."); yield break; } latestLeaderboardResponse = ParseJson(body); if (latestLeaderboardResponse == null || !latestLeaderboardResponse.success) { Fail(latestLeaderboardResponse != null ? latestLeaderboardResponse.message : "Invalid leaderboard response."); yield break; } statusMessage = "Leaderboard loaded."; } isBusy = false; RefreshUi(); } private void ToggleLeaderboardDropdown() { leaderboardDropdownOpen = !leaderboardDropdownOpen; EnsureLeaderboardDropdownUi(); if (leaderboardDropdownOpen) { Debug.Log("[GlobalCombatScene] Leaderboard dropdown opened"); if (!HasLoadedLeaderboardItems()) StartCoroutine(LoadLeaderboardCoroutine()); else RefreshUi(); } else { Debug.Log("[GlobalCombatScene] Leaderboard dropdown closed"); RefreshUi(); } } private void CloseLeaderboardDropdown() { leaderboardDropdownOpen = false; RefreshUi(); } private bool HasLoadedLeaderboardItems() { return latestLeaderboardResponse != null && latestLeaderboardResponse.leaderboard != null && latestLeaderboardResponse.leaderboard.Length > 0; } private void PlayNow() { if (isBusy || playRequestInProgress) return; string state = ResolveSceneMainButtonState(); Debug.Log("[GlobalCombatScene] SessionButton clicked state=" + state); if (state == "results" || state == "attempts_finished") { OpenFullResultPanel(); return; } if (state == "unavailable" || state == "ended" || state == "settled") { statusMessage = state == "unavailable" ? "Global Combat is unavailable right now." : "Tournament ended. Tap Results to view standings."; RefreshUi(); return; } StartCoroutine(PlayNowCoroutine()); } private IEnumerator PlayNowCoroutine() { SyncHomeCardPlayableEntryToLocal(latestHomeCardStateResponse); if (HasPendingPlayableAttempt()) { yield return LoadSessionAndContinueCoroutine(); yield break; } if (activeEventResponse == null || activeEventResponse.@event == null) { statusMessage = "Loading Global Combat event..."; RefreshUi(); yield return LoadActiveEventCoroutine(); } if (HasPendingPlayableAttempt()) { yield return LoadSessionAndContinueCoroutine(); yield break; } if (!CanJoinActiveEvent()) { statusMessage = activeEventResponse != null && activeEventResponse.@event != null ? "No playable Global Combat attempt is available right now." : "No Global Combat event is ready yet. Refresh and try again."; RefreshUi(); yield break; } Debug.Log("[GlobalCombatScene] Play Now clicked: joining and starting run."); yield return JoinActiveEventCoroutine(); if (HasPendingPlayableAttempt()) { yield return LoadSessionAndContinueCoroutine(); } } private void ContinueRun() { if (isBusy || playRequestInProgress) return; if (HasPendingPlayableAttempt()) { if (latestSessionResponse != null && latestSessionResponse.run != null) { LaunchGameplayScene(); return; } if (GlobalCombatSessionData.RunId > 0) { LaunchGameplayScene(); return; } StartCoroutine(LoadSessionAndContinueCoroutine()); return; } if (activeEventResponse == null) { statusMessage = "Loading Global Combat event. Please wait or tap Refresh."; } else { statusMessage = CanJoinActiveEvent() ? "Tap Play Now to join and start your Global Combat run." : "No playable Global Combat attempt is available right now."; } RefreshUi(); } private IEnumerator LoadSessionAndContinueCoroutine() { yield return LoadSessionCoroutine(); if (latestSessionResponse != null && latestSessionResponse.run != null) { LaunchGameplayScene(); yield break; } if (GlobalCombatSessionData.RunId > 0) LaunchGameplayScene(); } private void LaunchGameplayScene() { if (!CanLoadScene(globalCombatGameplaySceneName)) { Fail("Gameplay scene is not available: " + globalCombatGameplaySceneName); return; } RequestGlobalCombatStartOverlay(4f); Debug.Log("[GlobalCombatStart] phase=Starting run"); GlobalCombatTransitionOverlay.Show("GLOBAL COMBAT", "Starting run..."); GameplaySessionData.Clear(); GameplayResumeState.Clear(); PendingMatchResultState.Clear(); SceneManager.LoadScene(globalCombatGameplaySceneName); } private static void RequestGlobalCombatStartOverlay(float seconds) { float duration = Mathf.Max(1f, seconds); long untilTicks = DateTime.UtcNow.AddSeconds(duration + 15f).Ticks; PlayerPrefs.SetFloat(GlobalCombatStartOverlayDurationKey, duration); PlayerPrefs.DeleteKey(GlobalCombatStartOverlayStartedTicksKey); PlayerPrefs.SetString(GlobalCombatStartOverlayUntilTicksKey, untilTicks.ToString()); PlayerPrefs.Save(); } private bool CanLoadScene(string sceneName) { if (string.IsNullOrWhiteSpace(sceneName)) return false; int sceneCount = SceneManager.sceneCountInBuildSettings; for (int i = 0; i < sceneCount; i++) { string path = SceneUtility.GetScenePathByBuildIndex(i); string name = System.IO.Path.GetFileNameWithoutExtension(path); if (string.Equals(name, sceneName, StringComparison.Ordinal)) return true; } return false; } private bool ShouldAutoRefreshPostRunData() { if (!GlobalCombatSessionData.HasData) return false; string status = GlobalCombatSessionData.Status; return string.Equals(status, "submitted", StringComparison.OrdinalIgnoreCase) || string.Equals(status, "finished", StringComparison.OrdinalIgnoreCase); } private IEnumerator LoadPostRunDataCoroutine() { yield return null; yield return LoadMyResultCoroutine(); yield return LoadLeaderboardCoroutine(); } private IEnumerator OpenLeaderboardAfterLoadCoroutine() { yield return null; pendingLeaderboardAutoOpen = false; leaderboardDropdownOpen = true; EnsureLeaderboardDropdownUi(); yield return LoadLeaderboardCoroutine(); UpdateLeaderboardDropdownUi(); } private void BuildUiIfNeeded() { if (runtimeCanvas != null) return; Canvas existingCanvas = FindSceneComponentByObjectName("GlobalCombatCanvas"); if (existingCanvas != null && string.Equals(existingCanvas.gameObject.name, "GlobalCombatCanvas", StringComparison.Ordinal)) { runtimeCanvas = existingCanvas; titleLabel = FindText("Title"); subtitleLabel = FindText("Subtitle"); statusLabel = FindText("Status"); eventHeaderLabel = FindText("EventHeader"); eventInfoLabel = FindText("EventInfo"); walletHeaderLabel = FindText("WalletHeader"); walletLabel = FindText("Wallet"); sessionHeaderLabel = FindText("SessionHeader"); sessionLabel = FindText("Session"); resultHeaderLabel = FindText("ResultHeader"); resultLabel = FindText("Result"); leaderboardCard = FindPanel("LeaderboardCard"); leaderboardHeaderLabel = FindText("LeaderboardHeader"); leaderboardLabel = FindText("Leaderboard"); leaderboardDropdownPanel = FindPanel("LeaderboardDropdownPanel"); leaderboardDropdownTitleLabel = FindText("LeaderboardDropdownTitle"); GameObject existingLeaderboardContent = FindSceneObject("LeaderboardDropdownContent"); leaderboardDropdownContentRect = existingLeaderboardContent != null ? existingLeaderboardContent.GetComponent() : null; leaderboardDropdownBodyLabel = FindText("LeaderboardDropdownBody"); leaderboardDropdownCloseButton = FindButton("LeaderboardDropdownCloseButton"); leaderboardDropdownScrollRect = leaderboardDropdownPanel != null ? leaderboardDropdownPanel.GetComponentInChildren(true) : null; eventHeroPanel = FindPanel("02_EventHero") ?? FindPanel("EventHero") ?? FindPanel("EventCard"); EnsureResultInEventHeroUi(); ApplyLeaderboardFullWidthLayout(); footerHintLabel = FindText("FooterHint"); resultModalPanel = FindPanel("GlobalCombatResultModal"); resultModalTitleLabel = FindText("ResultModalTitle"); resultModalBodyLabel = FindText("ResultModalBody"); resultModalCloseButton = FindButton("ResultModalCloseButton"); resultModalPlayAgainButton = FindButton("ResultModalPlayAgainButton"); refreshButton = FindButton("RefreshButton"); joinButton = FindButton("JoinButton"); sessionButton = FindButton("SessionButton"); resultButton = FindButton("ResultButton"); leaderboardButton = FindButton("LeaderboardButton"); homeButton = FindButton("HomeButton"); buyCoinsButton = FindButton("BuyCoinsButton"); if (ShouldRebuildExistingCanvas()) { DestroyCanvas(runtimeCanvas.gameObject); runtimeCanvas = null; titleLabel = null; subtitleLabel = null; statusLabel = null; eventHeaderLabel = null; eventInfoLabel = null; eventHeroPanel = null; walletHeaderLabel = null; walletLabel = null; sessionHeaderLabel = null; sessionLabel = null; resultHeaderLabel = null; resultLabel = null; leaderboardCard = null; leaderboardHeaderLabel = null; leaderboardLabel = null; leaderboardDropdownPanel = null; leaderboardDropdownTitleLabel = null; leaderboardDropdownBodyLabel = null; leaderboardDropdownCloseButton = null; leaderboardDropdownScrollRect = null; leaderboardDropdownContentRect = null; footerHintLabel = null; resultModalPanel = null; resultModalTitleLabel = null; resultModalBodyLabel = null; resultModalCloseButton = null; resultModalPlayAgainButton = null; refreshButton = null; joinButton = null; sessionButton = null; resultButton = null; leaderboardButton = null; homeButton = null; buyCoinsButton = null; } else { if (refreshButton != null) refreshButton.onClick.RemoveAllListeners(); if (joinButton != null) joinButton.onClick.RemoveAllListeners(); if (sessionButton != null) sessionButton.onClick.RemoveAllListeners(); if (resultButton != null) resultButton.onClick.RemoveAllListeners(); if (leaderboardButton != null) leaderboardButton.onClick.RemoveAllListeners(); if (homeButton != null) homeButton.onClick.RemoveAllListeners(); if (buyCoinsButton != null) buyCoinsButton.onClick.RemoveAllListeners(); if (resultModalCloseButton != null) resultModalCloseButton.onClick.RemoveAllListeners(); if (resultModalPlayAgainButton != null) resultModalPlayAgainButton.onClick.RemoveAllListeners(); if (leaderboardDropdownCloseButton != null) leaderboardDropdownCloseButton.onClick.RemoveAllListeners(); if (refreshButton != null) refreshButton.onClick.AddListener(() => StartCoroutine(LoadActiveEventCoroutine())); if (joinButton != null) joinButton.onClick.RemoveAllListeners(); if (sessionButton != null) sessionButton.onClick.AddListener(PlayNow); if (resultButton != null) resultButton.onClick.AddListener(OpenFullResultPanel); if (leaderboardButton != null) leaderboardButton.onClick.AddListener(ToggleLeaderboardDropdown); if (homeButton != null) homeButton.onClick.AddListener(() => SceneManager.LoadScene(homeSceneName)); if (buyCoinsButton != null) buyCoinsButton.onClick.AddListener(() => SceneManager.LoadScene(walletSceneName)); if (resultModalCloseButton != null) resultModalCloseButton.onClick.AddListener(DismissPostRunModal); if (resultModalPlayAgainButton != null) resultModalPlayAgainButton.onClick.AddListener(PlayNow); if (leaderboardDropdownCloseButton != null) leaderboardDropdownCloseButton.onClick.AddListener(CloseLeaderboardDropdown); SetButtonLabel(refreshButton, "Refresh"); HideJoinAttemptButton(); ConfigureSinglePlayNowButton(); ConfigureResultAndLeaderboardButtons(); EnsureResultInEventHeroUi(); ApplyLeaderboardFullWidthLayout(); SetButtonLabel(sessionButton, "Play Now"); SetButtonLabel(resultButton, "Result"); SetButtonLabel(leaderboardButton, "Leaderboard"); SetButtonLabel(homeButton, "Back"); SetButtonLabel(buyCoinsButton, "Buy Dragon Coins"); SetButtonLabel(resultModalPlayAgainButton, string.Empty); EnsureLeaderboardDropdownUi(); UpdateLeaderboardDropdownUi(); return; } } EnsureEventSystem(); Font defaultFont = Resources.GetBuiltinResource("LegacyRuntime.ttf"); GameObject canvasGo = new GameObject("GlobalCombatCanvas", typeof(RectTransform)); runtimeCanvas = canvasGo.AddComponent(); runtimeCanvas.renderMode = RenderMode.ScreenSpaceOverlay; canvasGo.AddComponent().uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; canvasGo.GetComponent().referenceResolution = new Vector2(1080f, 1920f); canvasGo.GetComponent().screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight; canvasGo.GetComponent().matchWidthOrHeight = 0.5f; canvasGo.AddComponent(); GameObject background = CreatePanel("Background", canvasGo.transform, new Color(0.05f, 0.08f, 0.13f, 1f)); RectTransform backgroundRect = background.GetComponent(); backgroundRect.anchorMin = Vector2.zero; backgroundRect.anchorMax = Vector2.one; backgroundRect.offsetMin = Vector2.zero; backgroundRect.offsetMax = Vector2.zero; GameObject topGlow = CreatePanel("TopGlow", background.transform, new Color(0.18f, 0.43f, 0.74f, 0.24f)); SetRect(topGlow.GetComponent(), new Vector2(0f, 0.76f), new Vector2(1f, 1f)); GameObject card = CreatePanel("Card", background.transform, new Color(0.10f, 0.16f, 0.23f, 0.96f)); RectTransform cardRect = card.GetComponent(); cardRect.anchorMin = new Vector2(0.05f, 0.08f); cardRect.anchorMax = new Vector2(0.95f, 0.92f); cardRect.offsetMin = Vector2.zero; cardRect.offsetMax = Vector2.zero; GameObject accentBar = CreatePanel("AccentBar", card.transform, new Color(0.95f, 0.76f, 0.30f, 1f)); SetRect(accentBar.GetComponent(), new Vector2(0.05f, 0.955f), new Vector2(0.95f, 0.972f)); titleLabel = CreateText("Title", card.transform, defaultFont, 54, FontStyle.Bold, TextAnchor.MiddleLeft, new Color(0.96f, 0.91f, 0.64f)); SetRect(titleLabel.rectTransform, new Vector2(0.06f, 0.875f), new Vector2(0.94f, 0.945f)); titleLabel.text = "GLOBAL COMBAT"; AddTextShadow(titleLabel, new Color(0f, 0f, 0f, 0.35f)); subtitleLabel = CreateText("Subtitle", card.transform, defaultFont, 24, FontStyle.Normal, TextAnchor.MiddleLeft, new Color(0.77f, 0.86f, 0.94f)); SetRect(subtitleLabel.rectTransform, new Vector2(0.06f, 0.835f), new Vector2(0.94f, 0.875f)); subtitleLabel.text = "Cross-country Dragon Coin competition"; statusLabel = CreateText("Status", card.transform, defaultFont, 28, FontStyle.Bold, TextAnchor.MiddleLeft, Color.white); SetRect(statusLabel.rectTransform, new Vector2(0.06f, 0.775f), new Vector2(0.94f, 0.825f)); GameObject eventCard = CreatePanel("02_EventHero", card.transform, new Color(0.12f, 0.20f, 0.30f, 0.98f)); eventHeroPanel = eventCard; SetRect(eventCard.GetComponent(), new Vector2(0.05f, 0.47f), new Vector2(0.95f, 0.75f)); eventHeaderLabel = CreateText("EventHeader", eventCard.transform, defaultFont, 24, FontStyle.Bold, TextAnchor.MiddleLeft, new Color(1f, 0.88f, 0.55f)); SetRect(eventHeaderLabel.rectTransform, new Vector2(0.05f, 0.79f), new Vector2(0.95f, 0.97f)); eventHeaderLabel.text = "EVENT"; eventInfoLabel = CreateText("EventInfo", eventCard.transform, defaultFont, 28, FontStyle.Normal, TextAnchor.UpperLeft, new Color(0.85f, 0.91f, 0.97f)); SetRect(eventInfoLabel.rectTransform, new Vector2(0.05f, 0.35f), new Vector2(0.95f, 0.80f)); eventInfoLabel.horizontalOverflow = HorizontalWrapMode.Wrap; eventInfoLabel.verticalOverflow = VerticalWrapMode.Overflow; resultHeaderLabel = CreateText("ResultHeader", eventCard.transform, defaultFont, 22, FontStyle.Bold, TextAnchor.MiddleLeft, new Color(1f, 0.86f, 0.66f)); SetRect(resultHeaderLabel.rectTransform, new Vector2(0.05f, 0.22f), new Vector2(0.95f, 0.34f)); resultHeaderLabel.text = "MY RESULT"; resultLabel = CreateText("Result", eventCard.transform, defaultFont, 22, FontStyle.Bold, TextAnchor.UpperLeft, new Color(1f, 0.95f, 0.88f)); SetRect(resultLabel.rectTransform, new Vector2(0.05f, 0.06f), new Vector2(0.95f, 0.24f)); resultLabel.horizontalOverflow = HorizontalWrapMode.Wrap; resultLabel.verticalOverflow = VerticalWrapMode.Overflow; GameObject walletCard = CreatePanel("WalletCard", card.transform, new Color(0.09f, 0.24f, 0.21f, 0.98f)); SetRect(walletCard.GetComponent(), new Vector2(0.05f, 0.35f), new Vector2(0.95f, 0.44f)); walletHeaderLabel = CreateText("WalletHeader", walletCard.transform, defaultFont, 22, FontStyle.Bold, TextAnchor.MiddleLeft, new Color(0.76f, 0.98f, 0.88f)); SetRect(walletHeaderLabel.rectTransform, new Vector2(0.05f, 0.58f), new Vector2(0.95f, 0.96f)); walletHeaderLabel.text = "WALLET"; walletLabel = CreateText("Wallet", walletCard.transform, defaultFont, 24, FontStyle.Bold, TextAnchor.MiddleLeft, new Color(0.90f, 1f, 0.96f)); SetRect(walletLabel.rectTransform, new Vector2(0.05f, 0.06f), new Vector2(0.64f, 0.62f)); buyCoinsButton = CreateButton("BuyCoinsButton", walletCard.transform, defaultFont, "Buy Dragon Coins", new Color(0.84f, 0.52f, 0.17f)); SetRect(buyCoinsButton.GetComponent(), new Vector2(0.67f, 0.14f), new Vector2(0.95f, 0.70f)); buyCoinsButton.onClick.AddListener(() => SceneManager.LoadScene(walletSceneName)); GameObject sessionCard = CreatePanel("SessionCard", card.transform, new Color(0.20f, 0.15f, 0.31f, 0.98f)); SetRect(sessionCard.GetComponent(), new Vector2(0.05f, 0.18f), new Vector2(0.95f, 0.31f)); sessionHeaderLabel = CreateText("SessionHeader", sessionCard.transform, defaultFont, 22, FontStyle.Bold, TextAnchor.MiddleLeft, new Color(0.90f, 0.80f, 1f)); SetRect(sessionHeaderLabel.rectTransform, new Vector2(0.05f, 0.66f), new Vector2(0.95f, 0.96f)); sessionHeaderLabel.text = "RUN STATUS"; sessionLabel = CreateText("Session", sessionCard.transform, defaultFont, 24, FontStyle.Normal, TextAnchor.UpperLeft, Color.white); SetRect(sessionLabel.rectTransform, new Vector2(0.05f, 0.08f), new Vector2(0.95f, 0.70f)); sessionLabel.horizontalOverflow = HorizontalWrapMode.Wrap; sessionLabel.verticalOverflow = VerticalWrapMode.Overflow; leaderboardCard = CreatePanel("LeaderboardCard", card.transform, new Color(0.14f, 0.18f, 0.12f, 0.98f)); SetRect(leaderboardCard.GetComponent(), new Vector2(0.03f, 0.045f), new Vector2(0.97f, 0.165f)); leaderboardHeaderLabel = CreateText("LeaderboardHeader", leaderboardCard.transform, defaultFont, 22, FontStyle.Bold, TextAnchor.MiddleLeft, new Color(0.88f, 0.95f, 0.72f)); SetRect(leaderboardHeaderLabel.rectTransform, new Vector2(0.05f, 0.62f), new Vector2(0.95f, 0.96f)); leaderboardHeaderLabel.text = "TOP LEADERBOARD"; leaderboardLabel = CreateText("Leaderboard", leaderboardCard.transform, defaultFont, 20, FontStyle.Normal, TextAnchor.UpperLeft, new Color(0.93f, 0.98f, 0.90f)); SetRect(leaderboardLabel.rectTransform, new Vector2(0.05f, 0.06f), new Vector2(0.95f, 0.70f)); leaderboardLabel.horizontalOverflow = HorizontalWrapMode.Wrap; leaderboardLabel.verticalOverflow = VerticalWrapMode.Overflow; refreshButton = CreateButton("RefreshButton", background.transform, defaultFont, "Refresh", new Color(0.18f, 0.41f, 0.76f)); SetRect(refreshButton.GetComponent(), new Vector2(0.03f, 0.008f), new Vector2(0.31f, 0.052f)); refreshButton.onClick.AddListener(() => StartCoroutine(LoadActiveEventCoroutine())); joinButton = null; sessionButton = CreateButton("SessionButton", background.transform, defaultFont, "Play Now", new Color(0.57f, 0.33f, 0.77f)); SetRect(sessionButton.GetComponent(), new Vector2(0.35f, 0.008f), new Vector2(0.97f, 0.052f)); sessionButton.onClick.AddListener(PlayNow); resultButton = CreateButton("ResultButton", background.transform, defaultFont, string.Empty, new Color(0.84f, 0.52f, 0.17f)); SetRect(resultButton.GetComponent(), new Vector2(0.19f, 0.058f), new Vector2(0.47f, 0.102f)); resultButton.onClick.AddListener(OpenFullResultPanel); resultButton.gameObject.SetActive(false); leaderboardButton = CreateButton("LeaderboardButton", background.transform, defaultFont, "Leaderboard", new Color(0.37f, 0.52f, 0.17f)); SetRect(leaderboardButton.GetComponent(), new Vector2(0.03f, 0.058f), new Vector2(0.97f, 0.102f)); leaderboardButton.onClick.AddListener(ToggleLeaderboardDropdown); homeButton = CreateButton("HomeButton", background.transform, defaultFont, "Back", new Color(0.18f, 0.20f, 0.28f, 0.92f)); SetRect(homeButton.GetComponent(), new Vector2(0.04f, 0.94f), new Vector2(0.18f, 0.99f)); homeButton.onClick.AddListener(() => SceneManager.LoadScene(homeSceneName)); footerHintLabel = CreateText("FooterHint", background.transform, defaultFont, 20, FontStyle.Normal, TextAnchor.MiddleLeft, new Color(0.78f, 0.85f, 0.92f, 0.92f)); SetRect(footerHintLabel.rectTransform, new Vector2(0.05f, 0.108f), new Vector2(0.95f, 0.145f)); footerHintLabel.text = "Spend Dragon Coins to enter. Your highest score becomes your global rank."; resultModalPanel = CreatePanel("GlobalCombatResultModal", background.transform, new Color(0.05f, 0.08f, 0.13f, 0.97f)); SetRect(resultModalPanel.GetComponent(), new Vector2(0.04f, 0.12f), new Vector2(0.96f, 0.82f)); resultModalPanel.SetActive(false); GameObject resultModalAccent = CreatePanel("GlobalCombatResultModalAccent", resultModalPanel.transform, new Color(0.95f, 0.76f, 0.30f, 1f)); SetRect(resultModalAccent.GetComponent(), new Vector2(0.06f, 0.92f), new Vector2(0.94f, 0.95f)); resultModalTitleLabel = CreateText("ResultModalTitle", resultModalPanel.transform, defaultFont, 34, FontStyle.Bold, TextAnchor.MiddleLeft, new Color(1f, 0.95f, 0.88f)); SetRect(resultModalTitleLabel.rectTransform, new Vector2(0.08f, 0.78f), new Vector2(0.92f, 0.90f)); resultModalTitleLabel.text = "GLOBAL COMBAT RESULT"; resultModalBodyLabel = CreateText("ResultModalBody", resultModalPanel.transform, defaultFont, 21, FontStyle.Bold, TextAnchor.UpperLeft, new Color(0.90f, 0.95f, 1f)); SetRect(resultModalBodyLabel.rectTransform, new Vector2(0.06f, 0.18f), new Vector2(0.94f, 0.76f)); resultModalBodyLabel.horizontalOverflow = HorizontalWrapMode.Wrap; resultModalBodyLabel.verticalOverflow = VerticalWrapMode.Overflow; resultModalCloseButton = CreateButton("ResultModalCloseButton", resultModalPanel.transform, defaultFont, "Close", new Color(0.22f, 0.28f, 0.38f, 0.95f)); SetRect(resultModalCloseButton.GetComponent(), new Vector2(0.08f, 0.06f), new Vector2(0.42f, 0.16f)); resultModalCloseButton.onClick.AddListener(DismissPostRunModal); resultModalPlayAgainButton = CreateButton("ResultModalPlayAgainButton", resultModalPanel.transform, defaultFont, string.Empty, new Color(0.20f, 0.61f, 0.32f)); SetRect(resultModalPlayAgainButton.GetComponent(), new Vector2(0.48f, 0.06f), new Vector2(0.92f, 0.16f)); resultModalPlayAgainButton.onClick.AddListener(PlayNow); resultModalPlayAgainButton.gameObject.SetActive(false); ConfigureResultAndLeaderboardButtons(); EnsureResultInEventHeroUi(); ApplyLeaderboardFullWidthLayout(); RefreshUi(); } private void PrepareAuthRequest(UnityWebRequest request) { request.timeout = 20; request.downloadHandler ??= new DownloadHandlerBuffer(); request.SetRequestHeader("Authorization", "Bearer " + AuthService.GetAuthToken()); } private bool HandleUnauthorized(UnityWebRequest request) { if (request.responseCode != 401) return false; isBusy = false; playRequestInProgress = false; ForceLogoutToLogin(); return true; } private bool HasRequestError(UnityWebRequest request) { #if UNITY_2020_1_OR_NEWER return request.result != UnityWebRequest.Result.Success; #else return request.isNetworkError || request.isHttpError; #endif } private void Fail(string message) { lastError = message; statusMessage = message; isBusy = false; playRequestInProgress = false; StopAutoStartWatchdog(); if (ShouldShowStartRecovery()) { Debug.LogWarning("[GlobalCombatStart] failed: " + message); ShowStartRecovery(message); return; } GlobalCombatTransitionOverlay.Hide(); RefreshUi(); } private bool ShouldShowStartRecovery() { return launchedFromHomeHandoff || handoffRetryEntryId > 0 || (GlobalCombatSessionData.HasData && GlobalCombatSessionData.EntryId > 0); } private void ShowStartRecovery(string reason) { string safeReason = string.IsNullOrWhiteSpace(reason) ? "Global Combat could not start." : reason.Trim(); GlobalCombatTransitionOverlay.ShowRecovery( "Global Combat could not start", safeReason + "\nYour entry is safe. Please try again or return Home.", RetryStartFromRecovery, ReturnHomeFromRecovery, 10f); } private void RetryStartFromRecovery() { Debug.Log("[GlobalCombatStart] Try Again clicked"); isBusy = false; playRequestInProgress = false; latestSessionResponse = null; if (handoffRetryEntryId > 0 && GlobalCombatSessionData.EntryId <= 0) { // Preserve the entry id for get-session; no join-event call is made here, so no extra charge. GlobalCombatLaunchHandoff.Set(GlobalCombatSessionData.EventId, handoffRetryEntryId, handoffRetryEntryRef, true, true); } StartCoroutine(AutoStartFromHandoffCoroutine()); } private void ReturnHomeFromRecovery() { Debug.Log("[GlobalCombatStart] Go Home clicked"); isBusy = false; playRequestInProgress = false; StopAutoStartWatchdog(); GlobalCombatTransitionOverlay.MarkHomeNoticeAndLoadHome( "Global Combat not started", "Your entry is safe. You can try again from the Global Combat card.", homeSceneName); } private void StartAutoStartWatchdog(float seconds) { StopAutoStartWatchdog(); autoStartTimeoutRoutine = StartCoroutine(AutoStartWatchdogCoroutine(seconds)); } private void StopAutoStartWatchdog() { if (autoStartTimeoutRoutine != null) { StopCoroutine(autoStartTimeoutRoutine); autoStartTimeoutRoutine = null; } } private IEnumerator AutoStartWatchdogCoroutine(float seconds) { yield return new WaitForSecondsRealtime(Mathf.Max(5f, seconds)); autoStartTimeoutRoutine = null; if (latestSessionResponse == null || latestSessionResponse.run == null) { Debug.LogWarning("[GlobalCombatStart] timeout; showing recovery overlay"); isBusy = false; playRequestInProgress = false; ShowStartRecovery("Preparing Global Combat took too long."); } } private void ForceLogoutToLogin() { GlobalCombatTransitionOverlay.Hide(); GlobalCombatSessionData.Clear(); AuthService.ClearAuthState(); SceneManager.LoadScene(loginSceneName); } private void LogVerbose(string message) { if (verboseLogs) Debug.Log(message); } private static string ResolveFirstPlacePrizeLabel(GlobalCombatEventData e) { if (e == null) return "-"; string value = FirstText( e.proposed_first_place_prize, e.first_place_prize, e.first_prize, e.top_prize ); if (!string.IsNullOrWhiteSpace(value)) return value; string amount = FirstText(e.prize_pool_cash); string currency = FirstText(e.prize_currency_code); if (!string.IsNullOrWhiteSpace(amount) && !string.IsNullOrWhiteSpace(currency)) return amount + " " + currency; return string.IsNullOrWhiteSpace(amount) ? "-" : amount; } private static string FirstText(params string[] values) { if (values == null) return ""; for (int i = 0; i < values.Length; i++) { if (!string.IsNullOrWhiteSpace(values[i])) return values[i].Trim(); } return ""; } private static string NormalizeStatus(string value) { return string.IsNullOrWhiteSpace(value) ? "" : value.Trim().ToLowerInvariant(); } private void RefreshUi() { if (runtimeCanvas == null) return; bool hasPendingPlayableAttempt = HasPendingPlayableAttempt(); bool hasSubmittedRun = HasSubmittedRunState(); statusLabel.text = statusMessage; subtitleLabel.text = isBusy ? "Syncing with the global event..." : "Cross-country Dragon Coin competition"; if (activeEventResponse != null && activeEventResponse.@event != null) { GlobalCombatEventData e = activeEventResponse.@event; titleLabel.text = e.title.ToUpperInvariant(); eventInfoLabel.text = "Status\n" + e.status + "\n\n" + "Entry Fee\n" + e.entry_dragon_coins + " Dragon Coins\n\n" + "Attempts\n" + e.attempts_used + " used / " + e.attempts_left + " left\n\n" + "First Prize\n" + ResolveFirstPlacePrizeLabel(e) + "\n\n" + "Allowed Moves\n" + e.allowed_moves + "\n\n" + "Leaderboard\n" + e.leaderboard_rule + " | " + e.tie_break_rule; } else { titleLabel.text = "GLOBAL COMBAT"; eventInfoLabel.text = "No active event loaded.\n\nPull refresh to check whether a new competition is live."; } if (!string.IsNullOrWhiteSpace(lastError)) eventInfoLabel.text += "\n\nError: " + lastError; if (activeEventResponse != null && activeEventResponse.wallet != null) { walletLabel.text = "Dragon Coins " + activeEventResponse.wallet.dragon_coins + "\nWithdrawable " + activeEventResponse.wallet.withdrawable_balance; } else { walletLabel.text = "Wallet unavailable."; } if (latestSessionResponse != null && latestSessionResponse.run != null) { sessionLabel.text = "Run ready for Entry #" + latestSessionResponse.entry.entry_no + "\nRun ID " + latestSessionResponse.run.run_id + "\nMoves Remaining " + latestSessionResponse.run.moves_remaining + " / " + latestSessionResponse.run.moves_allowed; } else if (hasPendingPlayableAttempt && GlobalCombatSessionData.EntryId > 0) { sessionLabel.text = "Attempt #" + GlobalCombatSessionData.EntryNo + " is ready." + "\nTap Play Now to generate or open your challenge." + "\nYou must finish or submit this run before joining again."; } else if (GlobalCombatSessionData.RunId > 0) { sessionLabel.text = "Saved run available for Entry #" + GlobalCombatSessionData.EntryNo + "\nRun ID " + GlobalCombatSessionData.RunId + "\nMoves Remaining " + GlobalCombatSessionData.MovesRemaining + " / " + GlobalCombatSessionData.MovesAllowed; if (string.Equals(GlobalCombatSessionData.Status, "submitted", StringComparison.OrdinalIgnoreCase)) sessionLabel.text += "\nRun finished. Review your standing below or go back to join a fresh attempt."; } else if (latestJoinResponse != null && latestJoinResponse.entry != null) { sessionLabel.text = "Joined attempt #" + latestJoinResponse.entry.entry_no + "\nEntry ID " + latestJoinResponse.entry.entry_id + "\nTap Play Now to generate or resume your challenge."; } else { sessionLabel.text = "No active run yet.\nJoin an attempt to start your solo challenge."; } if (latestResultResponse != null && latestResultResponse.summary != null) { string rewardText = FirstText(latestResultResponse.summary.reward_label, latestResultResponse.summary.best_reward_amount, "0.00"); resultLabel.text = "Best Score " + latestResultResponse.summary.best_score + "\nBest Rank " + (latestResultResponse.summary.best_rank > 0 ? "#" + latestResultResponse.summary.best_rank : "Pending") + "\nWon " + rewardText + "\nStatus " + FirstText(latestResultResponse.summary.reward_status, "Pending"); } else { resultLabel.text = "Load your current standing and best score."; } if (latestLeaderboardResponse != null && latestLeaderboardResponse.leaderboard != null && latestLeaderboardResponse.leaderboard.Length > 0) { int count = Mathf.Min(3, latestLeaderboardResponse.leaderboard.Length); string leaderboardText = ""; string localUserId = AuthService.GetUserId(); for (int i = 0; i < count; i++) { GlobalCombatLeaderboardItem item = latestLeaderboardResponse.leaderboard[i]; if (item == null) continue; if (leaderboardText.Length > 0) leaderboardText += "\n"; bool isLocalUser = !string.IsNullOrWhiteSpace(localUserId) && item.user_id.ToString() == localUserId; string itemReward = FirstText(item.reward_label, item.local_reward_amount, item.reward_amount, ""); leaderboardText += (isLocalUser ? ">> YOU " : "") + "#" + item.rank + " " + FirstText(item.display_name, item.player_name, "Player") + " " + item.score + (!string.IsNullOrWhiteSpace(itemReward) ? " | Won " + itemReward : ""); } leaderboardLabel.text = leaderboardText; } else { leaderboardLabel.text = leaderboardDropdownOpen ? "Loading leaderboard..." : "Tap Leaderboard to open the current top players."; } UpdateLeaderboardDropdownUi(); bool canJoin = CanJoinActiveEvent(); bool canResumeRun = !isBusy && !playRequestInProgress && !hasSubmittedRun && hasPendingPlayableAttempt; bool canLoadResult = !isBusy && ((activeEventResponse != null && activeEventResponse.@event != null) || GlobalCombatSessionData.EventId > 0); string mainButtonState = ResolveSceneMainButtonState(); string mainButtonLabel = ResolveSceneMainButtonLabel(mainButtonState); bool mainButtonCanClick = CanClickSceneMainButton(mainButtonState); if (refreshButton != null) refreshButton.interactable = !isBusy && !playRequestInProgress; HideJoinAttemptButton(); ConfigureSinglePlayNowButton(); ConfigureResultAndLeaderboardButtons(); SetButtonLabel(resultButton, "Result"); if (sessionButton != null) sessionButton.interactable = mainButtonCanClick; if (resultButton != null) resultButton.interactable = canLoadResult; if (leaderboardButton != null) leaderboardButton.interactable = canLoadResult; if (homeButton != null) homeButton.interactable = !isBusy; if (buyCoinsButton != null) buyCoinsButton.interactable = !isBusy; SetButtonLabel(sessionButton, mainButtonLabel); footerHintLabel.text = BuildSceneMainButtonHint(mainButtonState, hasPendingPlayableAttempt, hasSubmittedRun, canJoin, canResumeRun); footerHintLabel.text += "\nScores should be submitted from gameplay, not from the lobby."; bool shouldShowPostRunModal = resultModalOpen && !postRunModalDismissed; if (resultModalPanel != null) { resultModalPanel.SetActive(shouldShowPostRunModal); if (shouldShowPostRunModal) resultModalPanel.transform.SetAsLastSibling(); } if (resultModalTitleLabel != null) resultModalTitleLabel.text = "GLOBAL COMBAT FULL RESULT"; if (resultModalBodyLabel != null) resultModalBodyLabel.text = BuildFullResultModalText(); if (resultModalPlayAgainButton != null) { bool showPlayAgain = shouldShowPostRunModal && CanClickSceneMainButton(ResolveSceneMainButtonState()) && ResolveSceneMainButtonState() != "results"; resultModalPlayAgainButton.gameObject.SetActive(showPlayAgain); resultModalPlayAgainButton.interactable = showPlayAgain; SetButtonLabel(resultModalPlayAgainButton, showPlayAgain ? ResolveSceneMainButtonLabel(ResolveSceneMainButtonState()) : string.Empty); } } private string ResolveSceneMainButtonState() { string homeState = NormalizeStatus(latestHomeCardButtonState); if (!string.IsNullOrWhiteSpace(homeState)) return homeState; if (HasPendingPlayableAttempt()) return "play"; if (HasSubmittedRunState()) { if (CanJoinActiveEvent()) return "play_again"; return "results"; } if (CanJoinActiveEvent()) return "join"; string eventStatus = activeEventResponse != null && activeEventResponse.@event != null ? NormalizeStatus(activeEventResponse.@event.status) : ""; if (eventStatus == "ended" || eventStatus == "settled") return "results"; return "unavailable"; } private string ResolveSceneMainButtonLabel(string state) { if (!string.IsNullOrWhiteSpace(latestHomeCardButtonLabel)) return latestHomeCardButtonLabel; switch (NormalizeStatus(state)) { case "join": return "Join"; case "joined": return "Joined"; case "play": return "Play"; case "play_again": return "Play Again"; case "attempts_finished": return "Results"; case "results": return "Results"; case "upcoming": return "Upcoming"; case "ended": return "Results"; case "settled": return "Results"; default: return "Unavailable"; } } private bool CanClickSceneMainButton(string state) { if (isBusy || playRequestInProgress) return false; switch (NormalizeStatus(state)) { case "join": case "play": case "play_again": case "results": case "attempts_finished": return true; default: return false; } } private string BuildSceneMainButtonHint(string state, bool hasPendingPlayableAttempt, bool hasSubmittedRun, bool canJoin, bool canResumeRun) { switch (NormalizeStatus(state)) { case "join": return "Tap Join to enter the Global Combat tournament."; case "play": return hasPendingPlayableAttempt || canResumeRun ? "Your current attempt is waiting. Tap Play to continue." : "Tap Play to start your Global Combat run."; case "play_again": return "Run finished. Tap Play Again to join a fresh Global Combat attempt."; case "results": case "attempts_finished": return "Tournament result is ready. Tap Results to see your full result, rank, reward, and leaderboard."; case "joined": return "You have joined this event. Play will become available when the run is ready."; case "upcoming": return "This Global Combat tournament is upcoming."; default: return "No playable Global Combat attempt is available right now. Refresh the event or review your current standing."; } } private string ResolveHomeCardButtonState(GlobalCombatHomeCardStateResponse data) { if (data != null && data.global_card != null && !string.IsNullOrWhiteSpace(data.global_card.button_state)) return NormalizeStatus(data.global_card.button_state); if (data != null && data.player_state != null && !string.IsNullOrWhiteSpace(data.player_state.button_state)) return NormalizeStatus(data.player_state.button_state); if (data != null && data.labels != null && !string.IsNullOrWhiteSpace(data.labels.button_state)) return NormalizeStatus(data.labels.button_state); GlobalCombatHomeCardEventData ev = data != null ? (data.@event != null ? data.@event : data.event_data_proxy) : null; string status = ev != null ? NormalizeStatus(ev.status) : ""; GlobalCombatHomeCardPlayerState player = data != null ? data.player_state : null; if (status == "live") { if (HomeHasPlayableEntry(player)) return "play"; if (player != null && player.max_entries_per_user > 0 && player.attempts_used >= player.max_entries_per_user) return "attempts_finished"; if (player != null && player.attempts_used > 0) return "play_again"; return "join"; } if (status == "ended" || status == "settled") return "results"; if (status == "upcoming") return "join"; return string.IsNullOrWhiteSpace(status) ? "unavailable" : status; } private string ResolveHomeCardButtonLabel(GlobalCombatHomeCardStateResponse data, string state) { if (data != null && data.global_card != null && !string.IsNullOrWhiteSpace(data.global_card.button_label)) return data.global_card.button_label; if (data != null && data.player_state != null && !string.IsNullOrWhiteSpace(data.player_state.button_label)) return data.player_state.button_label; if (data != null && data.labels != null && !string.IsNullOrWhiteSpace(data.labels.button_label)) return data.labels.button_label; switch (NormalizeStatus(state)) { case "join": return "Join"; case "joined": return "Joined"; case "play": return "Play"; case "play_again": return "Play Again"; case "attempts_finished": return "Results"; case "results": return "Results"; case "upcoming": return "Upcoming"; default: return "Unavailable"; } } private void SyncHomeCardPlayableEntryToLocal(GlobalCombatHomeCardStateResponse data) { if (data == null || data.player_state == null) return; GlobalCombatHomeCardPlayerState player = data.player_state; if (!HomeHasPlayableEntry(player) || player.entry_id <= 0) return; GlobalCombatHomeCardEventData ev = data.@event != null ? data.@event : data.event_data_proxy; int eventId = 0; string eventRef = ""; string title = ""; int allowedMoves = 0; if (ev != null) { eventId = ev.event_id > 0 ? ev.event_id : ev.id; eventRef = ev.event_ref; title = ev.title; allowedMoves = ev.allowed_moves; } GlobalCombatJoinEventData joinEvent = new GlobalCombatJoinEventData { event_id = eventId, event_ref = eventRef, title = title }; GlobalCombatJoinEntryData joinEntry = new GlobalCombatJoinEntryData { entry_id = player.entry_id, entry_ref = "", entry_no = 0, entry_dragon_coins = player.entry_dragon_coins > 0 ? player.entry_dragon_coins.ToString() : "", allowed_moves_snapshot = allowedMoves, status = string.IsNullOrWhiteSpace(player.entry_status) ? "active" : player.entry_status }; GlobalCombatSessionData.MarkJoined(joinEvent, joinEntry); } private static bool HomeHasPlayableEntry(GlobalCombatHomeCardPlayerState player) { if (player == null) return false; if (player.has_active_entry || player.can_play) { string status = NormalizeStatus(player.entry_status); return string.IsNullOrWhiteSpace(status) || status == "joined" || status == "started" || status == "active" || status == "live" || status == "running" || status == "in_progress"; } return false; } private bool HasSubmittedRunState() { if (string.Equals(GlobalCombatSessionData.Status, "submitted", StringComparison.OrdinalIgnoreCase) || string.Equals(GlobalCombatSessionData.Status, "finished", StringComparison.OrdinalIgnoreCase)) return true; if (latestSessionResponse != null && latestSessionResponse.entry != null && !string.IsNullOrWhiteSpace(latestSessionResponse.entry.status) && (string.Equals(latestSessionResponse.entry.status, "submitted", StringComparison.OrdinalIgnoreCase) || string.Equals(latestSessionResponse.entry.status, "finished", StringComparison.OrdinalIgnoreCase))) return true; return false; } private bool CanJoinActiveEvent() { return !isBusy && !playRequestInProgress && activeEventResponse != null && activeEventResponse.@event != null && !HasPendingPlayableAttempt(); } private GlobalCombatJoinEventData BuildJoinEventDataFromActiveEvent() { if (activeEventResponse == null || activeEventResponse.@event == null) return null; GlobalCombatEventData active = activeEventResponse.@event; return new GlobalCombatJoinEventData { event_id = active.event_id, event_ref = active.event_ref, title = active.title }; } private bool HasPendingPlayableAttempt() { if (HasSubmittedRunState()) return false; if (latestSessionResponse != null && latestSessionResponse.run != null) return true; if (GlobalCombatSessionData.EntryId > 0) { string status = (GlobalCombatSessionData.Status ?? "").Trim(); if (string.IsNullOrEmpty(status) || string.Equals(status, "joined", StringComparison.OrdinalIgnoreCase) || string.Equals(status, "started", StringComparison.OrdinalIgnoreCase) || string.Equals(status, "active", StringComparison.OrdinalIgnoreCase)) return true; } if (latestJoinResponse != null && latestJoinResponse.entry != null) { string status = (latestJoinResponse.entry.status ?? "").Trim(); if (string.IsNullOrEmpty(status) || string.Equals(status, "joined", StringComparison.OrdinalIgnoreCase) || string.Equals(status, "started", StringComparison.OrdinalIgnoreCase)) return true; } return false; } private void OpenFullResultPanel() { if (isBusy) return; StartCoroutine(OpenFullResultPanelCoroutine()); } private IEnumerator OpenFullResultPanelCoroutine() { resultModalOpen = true; postRunModalDismissed = false; if (resultModalTitleLabel != null) resultModalTitleLabel.text = "GLOBAL COMBAT FULL RESULT"; if (resultModalBodyLabel != null) resultModalBodyLabel.text = "Loading your result and leaderboard..."; if (resultModalPanel != null) { resultModalPanel.SetActive(true); resultModalPanel.transform.SetAsLastSibling(); } Debug.Log("[GlobalCombatScene] Result button clicked: loading full result."); yield return LoadMyResultCoroutine(); yield return LoadLeaderboardCoroutine(); resultModalOpen = true; postRunModalDismissed = false; if (resultModalPanel != null) { resultModalPanel.SetActive(true); resultModalPanel.transform.SetAsLastSibling(); } if (resultModalTitleLabel != null) resultModalTitleLabel.text = "GLOBAL COMBAT FULL RESULT"; if (resultModalBodyLabel != null) resultModalBodyLabel.text = BuildFullResultModalText(); RefreshUi(); } private string BuildFullResultModalText() { string title = FirstText( latestLeaderboardResponse != null && latestLeaderboardResponse.@event != null ? latestLeaderboardResponse.@event.title : "", activeEventResponse != null && activeEventResponse.@event != null ? activeEventResponse.@event.title : "", "Global Combat" ); string text = title.ToUpperInvariant(); if (latestResultResponse != null && latestResultResponse.summary != null) { GlobalCombatResultSummary summary = latestResultResponse.summary; text += "\n\nMY RESULT"; string rewardText = FirstText(summary.reward_label, summary.best_reward_amount, "0.00"); text += "\nBest Entry #" + summary.best_entry_no; text += "\nAttempts Used " + summary.attempts_used; text += "\nBest Score " + summary.best_score; text += "\nBest Rank " + (summary.best_rank > 0 ? "#" + summary.best_rank : "Pending"); text += "\nAmount Won " + rewardText; text += "\nStatus " + FirstText(summary.reward_status, "Pending"); if (!string.IsNullOrWhiteSpace(summary.wallet_txn_ref)) text += "\nTransaction " + summary.wallet_txn_ref; if (!string.IsNullOrWhiteSpace(summary.paid_to)) text += "\nPaid To " + summary.paid_to; } else { text += "\n\nMY RESULT"; text += "\nNo submitted result was returned yet."; } text += "\n\nLEADERBOARD"; if (latestLeaderboardResponse != null && latestLeaderboardResponse.leaderboard != null && latestLeaderboardResponse.leaderboard.Length > 0) { string localUserId = AuthService.GetUserId(); for (int i = 0; i < latestLeaderboardResponse.leaderboard.Length; i++) { GlobalCombatLeaderboardItem item = latestLeaderboardResponse.leaderboard[i]; if (item == null) continue; bool isLocalUser = !string.IsNullOrWhiteSpace(localUserId) && item.user_id.ToString() == localUserId; string itemReward = FirstText(item.reward_label, item.local_reward_amount, item.reward_amount, ""); text += "\n" + (isLocalUser ? "YOU " : "") + "#" + item.rank + " " + FirstText(item.display_name, item.player_name, "Player") + " Score " + item.score; if (!string.IsNullOrWhiteSpace(itemReward)) text += " | Won " + itemReward; if (!string.IsNullOrWhiteSpace(item.reward_status)) text += " | " + item.reward_status; if (!string.IsNullOrWhiteSpace(item.played_at)) text += " | " + item.played_at; } } else { text += "\nLeaderboard is not loaded yet or there are no submitted runs."; } string prizePool = ResolvePrizePoolLabelForResult(); if (!string.IsNullOrWhiteSpace(prizePool)) text += "\n\nPrize Pool " + prizePool; text += "\n\nTap Play Again when a new attempt is available."; return text; } private string FormatRewardAmount(string amount) { amount = FirstText(amount, "0"); string currency = FirstText( latestLeaderboardResponse != null && latestLeaderboardResponse.@event != null ? latestLeaderboardResponse.@event.prize_currency_code : "", activeEventResponse != null && activeEventResponse.@event != null ? activeEventResponse.@event.prize_currency_code : "" ); if (string.IsNullOrWhiteSpace(currency) || amount.IndexOf(currency, StringComparison.OrdinalIgnoreCase) >= 0) return amount; return amount + " " + currency; } private string ResolvePrizePoolLabelForResult() { string amount = FirstText( latestLeaderboardResponse != null && latestLeaderboardResponse.@event != null ? latestLeaderboardResponse.@event.prize_pool_cash : "", activeEventResponse != null && activeEventResponse.@event != null ? activeEventResponse.@event.prize_pool_cash : "" ); string currency = FirstText( latestLeaderboardResponse != null && latestLeaderboardResponse.@event != null ? latestLeaderboardResponse.@event.prize_currency_code : "", activeEventResponse != null && activeEventResponse.@event != null ? activeEventResponse.@event.prize_currency_code : "" ); if (string.IsNullOrWhiteSpace(amount)) return ""; return string.IsNullOrWhiteSpace(currency) ? amount : amount + " " + currency; } private string BuildPostRunModalText() { string text = "Your run is complete."; if (latestResultResponse != null && latestResultResponse.summary != null) { text += "\n\nBest Score " + latestResultResponse.summary.best_score; text += "\nBest Rank " + latestResultResponse.summary.best_rank; text += "\nReward " + latestResultResponse.summary.best_reward_amount; } if (latestLeaderboardResponse != null && latestLeaderboardResponse.leaderboard != null && latestLeaderboardResponse.leaderboard.Length > 0) { text += "\n\nLeaderboard"; int count = Mathf.Min(3, latestLeaderboardResponse.leaderboard.Length); for (int i = 0; i < count; i++) { GlobalCombatLeaderboardItem item = latestLeaderboardResponse.leaderboard[i]; if (item == null) continue; text += "\n#" + item.rank + " " + item.display_name + " " + item.score; } } text += "\n\nReturn to the event screen to join a fresh attempt when you are ready."; return text; } private void DismissPostRunModal() { resultModalOpen = false; postRunModalDismissed = true; if (resultModalPanel != null) resultModalPanel.SetActive(false); } private void EnsureLeaderboardDropdownUi() { if (runtimeCanvas == null) return; if (leaderboardDropdownPanel != null && leaderboardDropdownBodyLabel != null && leaderboardDropdownScrollRect != null && leaderboardDropdownContentRect != null) return; Font defaultFont = Resources.GetBuiltinResource("LegacyRuntime.ttf"); GameObject background = FindPanel("Background"); Transform parent = background != null ? background.transform : runtimeCanvas.transform; if (leaderboardDropdownPanel == null) { leaderboardDropdownPanel = CreatePanel("LeaderboardDropdownPanel", parent, new Color(0.055f, 0.085f, 0.055f, 0.985f)); SetRect(leaderboardDropdownPanel.GetComponent(), new Vector2(0.02f, 0.135f), new Vector2(0.98f, 0.72f)); leaderboardDropdownPanel.transform.SetAsLastSibling(); } if (leaderboardDropdownTitleLabel == null) { leaderboardDropdownTitleLabel = CreateText("LeaderboardDropdownTitle", leaderboardDropdownPanel.transform, defaultFont, 30, FontStyle.Bold, TextAnchor.MiddleLeft, new Color(0.96f, 0.92f, 0.58f)); SetRect(leaderboardDropdownTitleLabel.rectTransform, new Vector2(0.04f, 0.86f), new Vector2(0.82f, 0.97f)); leaderboardDropdownTitleLabel.text = "LIVE LEADERBOARD"; } if (leaderboardDropdownCloseButton == null) { leaderboardDropdownCloseButton = CreateButton("LeaderboardDropdownCloseButton", leaderboardDropdownPanel.transform, defaultFont, "X", new Color(0.25f, 0.18f, 0.16f, 0.98f)); SetRect(leaderboardDropdownCloseButton.GetComponent(), new Vector2(0.88f, 0.865f), new Vector2(0.97f, 0.965f)); leaderboardDropdownCloseButton.onClick.RemoveAllListeners(); leaderboardDropdownCloseButton.onClick.AddListener(CloseLeaderboardDropdown); } GameObject scrollGo = FindPanel("LeaderboardDropdownScroll"); if (scrollGo == null || scrollGo.transform.parent != leaderboardDropdownPanel.transform) { scrollGo = new GameObject("LeaderboardDropdownScroll", typeof(RectTransform)); scrollGo.transform.SetParent(leaderboardDropdownPanel.transform, false); SetRect(scrollGo.GetComponent(), new Vector2(0.035f, 0.07f), new Vector2(0.965f, 0.83f)); Image scrollBg = scrollGo.AddComponent(); scrollBg.color = new Color(0.02f, 0.035f, 0.03f, 0.55f); } leaderboardDropdownScrollRect = scrollGo.GetComponent(); if (leaderboardDropdownScrollRect == null) leaderboardDropdownScrollRect = scrollGo.AddComponent(); GameObject viewportGo = FindSceneObject("LeaderboardDropdownViewport"); if (viewportGo == null || viewportGo.transform.parent != scrollGo.transform) { viewportGo = new GameObject("LeaderboardDropdownViewport", typeof(RectTransform)); viewportGo.transform.SetParent(scrollGo.transform, false); RectTransform viewportRect = viewportGo.GetComponent(); viewportRect.anchorMin = Vector2.zero; viewportRect.anchorMax = Vector2.one; viewportRect.offsetMin = new Vector2(6f, 6f); viewportRect.offsetMax = new Vector2(-6f, -6f); Image viewportImage = viewportGo.AddComponent(); viewportImage.color = new Color(1f, 1f, 1f, 0.02f); Mask mask = viewportGo.AddComponent(); mask.showMaskGraphic = false; } GameObject contentGo = FindSceneObject("LeaderboardDropdownContent"); if (contentGo == null || contentGo.transform.parent != viewportGo.transform) { contentGo = new GameObject("LeaderboardDropdownContent", typeof(RectTransform)); contentGo.transform.SetParent(viewportGo.transform, false); RectTransform contentRect = contentGo.GetComponent(); contentRect.anchorMin = new Vector2(0f, 1f); contentRect.anchorMax = new Vector2(1f, 1f); contentRect.pivot = new Vector2(0.5f, 1f); contentRect.anchoredPosition = Vector2.zero; contentRect.sizeDelta = new Vector2(0f, 700f); } leaderboardDropdownContentRect = contentGo.GetComponent(); leaderboardDropdownBodyLabel = FindText("LeaderboardDropdownBody"); if (leaderboardDropdownBodyLabel == null || leaderboardDropdownBodyLabel.transform.parent != contentGo.transform) { leaderboardDropdownBodyLabel = CreateText("LeaderboardDropdownBody", contentGo.transform, defaultFont, 24, FontStyle.Bold, TextAnchor.UpperLeft, new Color(0.92f, 0.98f, 0.90f)); RectTransform bodyRect = leaderboardDropdownBodyLabel.rectTransform; bodyRect.anchorMin = Vector2.zero; bodyRect.anchorMax = Vector2.one; bodyRect.offsetMin = new Vector2(20f, 18f); bodyRect.offsetMax = new Vector2(-20f, -18f); leaderboardDropdownBodyLabel.horizontalOverflow = HorizontalWrapMode.Wrap; leaderboardDropdownBodyLabel.verticalOverflow = VerticalWrapMode.Overflow; } leaderboardDropdownScrollRect.viewport = viewportGo.GetComponent(); leaderboardDropdownScrollRect.content = contentGo.GetComponent(); leaderboardDropdownScrollRect.horizontal = false; leaderboardDropdownScrollRect.vertical = true; leaderboardDropdownScrollRect.movementType = ScrollRect.MovementType.Elastic; leaderboardDropdownScrollRect.inertia = true; leaderboardDropdownScrollRect.scrollSensitivity = 30f; ApplyLeaderboardFullWidthLayout(); leaderboardDropdownPanel.SetActive(leaderboardDropdownOpen); } private void UpdateLeaderboardDropdownUi() { if (leaderboardDropdownPanel == null) return; leaderboardDropdownPanel.transform.SetAsLastSibling(); leaderboardDropdownPanel.SetActive(leaderboardDropdownOpen); if (leaderboardButton != null) SetButtonLabel(leaderboardButton, leaderboardDropdownOpen ? "Hide Board" : "Leaderboard"); if (leaderboardDropdownTitleLabel != null) { string title = "LIVE LEADERBOARD"; if (latestLeaderboardResponse != null && latestLeaderboardResponse.@event != null && !string.IsNullOrWhiteSpace(latestLeaderboardResponse.@event.title)) title = latestLeaderboardResponse.@event.title.ToUpperInvariant(); leaderboardDropdownTitleLabel.text = title; } BuildLeaderboardProfileRows(); } private void BuildLeaderboardProfileRows() { RectTransform contentRect = leaderboardDropdownScrollRect != null ? leaderboardDropdownScrollRect.content : leaderboardDropdownContentRect; if (contentRect == null) return; leaderboardDropdownContentRect = contentRect; ClearLeaderboardProfileRows(contentRect); bool hasRows = latestLeaderboardResponse != null && latestLeaderboardResponse.leaderboard != null && latestLeaderboardResponse.leaderboard.Length > 0; if (!hasRows) { if (leaderboardDropdownBodyLabel != null) { leaderboardDropdownBodyLabel.gameObject.SetActive(true); leaderboardDropdownBodyLabel.text = BuildFullLeaderboardText(); RectTransform bodyRect = leaderboardDropdownBodyLabel.rectTransform; bodyRect.anchorMin = Vector2.zero; bodyRect.anchorMax = Vector2.one; bodyRect.offsetMin = new Vector2(22f, 18f); bodyRect.offsetMax = new Vector2(-22f, -18f); } contentRect.sizeDelta = new Vector2(contentRect.sizeDelta.x, 680f); contentRect.anchoredPosition = Vector2.zero; return; } if (leaderboardDropdownBodyLabel != null) leaderboardDropdownBodyLabel.gameObject.SetActive(false); Font defaultFont = Resources.GetBuiltinResource("LegacyRuntime.ttf"); string localUserId = AuthService.GetUserId(); float topPadding = 18f; float rowHeight = 124f; float gap = 14f; float contentHeight = topPadding; for (int i = 0; i < latestLeaderboardResponse.leaderboard.Length; i++) { GlobalCombatLeaderboardItem item = latestLeaderboardResponse.leaderboard[i]; if (item == null) continue; bool isLocalUser = !string.IsNullOrWhiteSpace(localUserId) && item.user_id.ToString() == localUserId; float y = -contentHeight; GameObject row = CreatePanel(LeaderboardProfileRowPrefix + i, contentRect, isLocalUser ? new Color(0.28f, 0.20f, 0.07f, 0.98f) : new Color(0.09f, 0.13f, 0.17f, 0.97f)); RectTransform rowRect = row.GetComponent(); rowRect.anchorMin = new Vector2(0f, 1f); rowRect.anchorMax = new Vector2(1f, 1f); rowRect.pivot = new Vector2(0.5f, 1f); rowRect.offsetMin = new Vector2(16f, 0f); rowRect.offsetMax = new Vector2(-16f, 0f); rowRect.sizeDelta = new Vector2(-32f, rowHeight); rowRect.anchoredPosition = new Vector2(0f, y); GameObject rankBadge = CreatePanel("RankBadge", row.transform, ResolveLeaderboardRankColor(item.rank, isLocalUser)); RectTransform rankRect = rankBadge.GetComponent(); rankRect.anchorMin = new Vector2(0.035f, 0.20f); rankRect.anchorMax = new Vector2(0.17f, 0.80f); rankRect.offsetMin = Vector2.zero; rankRect.offsetMax = Vector2.zero; Text rankText = CreateText("RankText", rankBadge.transform, defaultFont, 30, FontStyle.Bold, TextAnchor.MiddleCenter, Color.white); SetRect(rankText.rectTransform, Vector2.zero, Vector2.one); rankText.text = "#" + item.rank; string playerName = string.IsNullOrWhiteSpace(item.display_name) ? "Player" : item.display_name.Trim(); Text nameText = CreateText("PlayerName", row.transform, defaultFont, 28, FontStyle.Bold, TextAnchor.MiddleLeft, isLocalUser ? new Color(1f, 0.93f, 0.62f) : new Color(0.96f, 0.98f, 1f)); SetRect(nameText.rectTransform, new Vector2(0.20f, 0.52f), new Vector2(0.62f, 0.91f)); nameText.horizontalOverflow = HorizontalWrapMode.Wrap; nameText.text = isLocalUser ? playerName + " • YOU" : playerName; Text playerInfo = CreateText("PlayerInfo", row.transform, defaultFont, 19, FontStyle.Normal, TextAnchor.MiddleLeft, new Color(0.68f, 0.76f, 0.84f)); SetRect(playerInfo.rectTransform, new Vector2(0.20f, 0.19f), new Vector2(0.62f, 0.52f)); playerInfo.horizontalOverflow = HorizontalWrapMode.Wrap; playerInfo.text = "Player ID: " + item.user_id + "\nPlayed: " + FormatLeaderboardPlayedAt(item.played_at); CreateProfileInfoField(row.transform, defaultFont, "SCORE", item.score.ToString(), new Vector2(0.64f, 0.18f), new Vector2(0.80f, 0.84f)); CreateProfileInfoField(row.transform, defaultFont, "WON", FirstText(item.reward_label, item.local_reward_amount, item.reward_amount, "-"), new Vector2(0.81f, 0.18f), new Vector2(0.97f, 0.84f)); if (isLocalUser) { GameObject glow = CreatePanel("YouGlowLine", row.transform, new Color(1f, 0.78f, 0.22f, 1f)); RectTransform glowRect = glow.GetComponent(); glowRect.anchorMin = new Vector2(0f, 0f); glowRect.anchorMax = new Vector2(1f, 0.035f); glowRect.offsetMin = Vector2.zero; glowRect.offsetMax = Vector2.zero; } contentHeight += rowHeight + gap; } contentHeight = Mathf.Max(680f, contentHeight + 12f); contentRect.sizeDelta = new Vector2(contentRect.sizeDelta.x, contentHeight); contentRect.anchoredPosition = Vector2.zero; } private void ClearLeaderboardProfileRows(RectTransform contentRect) { if (contentRect == null) return; for (int i = contentRect.childCount - 1; i >= 0; i--) { Transform child = contentRect.GetChild(i); if (child == null) continue; if (!child.gameObject.name.StartsWith(LeaderboardProfileRowPrefix, StringComparison.Ordinal)) continue; if (Application.isPlaying) Destroy(child.gameObject); else DestroyImmediate(child.gameObject); } } private void CreateProfileInfoField(Transform parent, Font font, string label, string value, Vector2 min, Vector2 max) { GameObject field = CreatePanel("ProfileInfoField_" + label, parent, new Color(1f, 1f, 1f, 0.055f)); RectTransform fieldRect = field.GetComponent(); fieldRect.anchorMin = min; fieldRect.anchorMax = max; fieldRect.offsetMin = Vector2.zero; fieldRect.offsetMax = Vector2.zero; Text labelText = CreateText("Label", field.transform, font, 16, FontStyle.Bold, TextAnchor.MiddleCenter, new Color(0.70f, 0.78f, 0.84f)); SetRect(labelText.rectTransform, new Vector2(0.04f, 0.55f), new Vector2(0.96f, 0.94f)); labelText.text = label; Text valueText = CreateText("Value", field.transform, font, 24, FontStyle.Bold, TextAnchor.MiddleCenter, Color.white); SetRect(valueText.rectTransform, new Vector2(0.04f, 0.06f), new Vector2(0.96f, 0.60f)); valueText.resizeTextForBestFit = true; valueText.resizeTextMinSize = 14; valueText.resizeTextMaxSize = 24; valueText.text = string.IsNullOrWhiteSpace(value) ? "-" : value; } private Color ResolveLeaderboardRankColor(int rank, bool isLocalUser) { if (rank == 1) return new Color(0.95f, 0.66f, 0.18f, 1f); if (rank == 2) return new Color(0.60f, 0.65f, 0.72f, 1f); if (rank == 3) return new Color(0.70f, 0.42f, 0.20f, 1f); return isLocalUser ? new Color(0.90f, 0.62f, 0.18f, 1f) : new Color(0.22f, 0.36f, 0.52f, 1f); } private string FormatLeaderboardPlayedAt(string playedAt) { if (string.IsNullOrWhiteSpace(playedAt)) return "-"; DateTime dt; if (DateTime.TryParse(playedAt, out dt)) return dt.ToString("MMM dd, HH:mm"); return playedAt.Trim(); } private string BuildFullLeaderboardText() { if (isBusy && (latestLeaderboardResponse == null || latestLeaderboardResponse.leaderboard == null)) return "Loading leaderboard..."; if (latestLeaderboardResponse == null || latestLeaderboardResponse.leaderboard == null || latestLeaderboardResponse.leaderboard.Length == 0) { if (!string.IsNullOrWhiteSpace(lastError)) return "Leaderboard could not load.\n\n" + lastError + "\n\nTap Leaderboard again to retry."; return "No leaderboard loaded yet.\n\nTap Leaderboard to load the current top players."; } string localUserId = AuthService.GetUserId(); string text = "Rank Player Score Won\n"; text += "----------------------------------------------------------"; for (int i = 0; i < latestLeaderboardResponse.leaderboard.Length; i++) { GlobalCombatLeaderboardItem item = latestLeaderboardResponse.leaderboard[i]; if (item == null) continue; bool isLocalUser = !string.IsNullOrWhiteSpace(localUserId) && item.user_id.ToString() == localUserId; string name = string.IsNullOrWhiteSpace(item.display_name) ? "Player" : item.display_name.Trim(); if (name.Length > 24) name = name.Substring(0, 24) + "..."; string itemReward = FirstText(item.reward_label, item.local_reward_amount, item.reward_amount, "-"); text += "\n" + (isLocalUser ? ">> " : " ") + "#" + item.rank.ToString().PadRight(5) + name.PadRight(30) + item.score.ToString().PadRight(11) + itemReward; } return text; } [Serializable] private class GlobalCombatHomeCardStateResponse { public bool success; public string message; public bool has_event; public GlobalCombatHomeCardEventData @event; public GlobalCombatHomeCardEventData event_data_proxy; public GlobalCombatHomeCardLabels labels; public GlobalCombatHomeCardGlobalState global_card; public GlobalCombatHomeCardPlayerState player_state; } [Serializable] private class GlobalCombatHomeCardEventData { public int id; public int event_id; public string event_ref; public string title; public string status; public int allowed_moves; } [Serializable] private class GlobalCombatHomeCardLabels { public string button_state; public string button_label; } [Serializable] private class GlobalCombatHomeCardGlobalState { public string button_state; public string button_label; public bool can_join; public bool can_play; public bool is_joined; } [Serializable] private class GlobalCombatHomeCardPlayerState { public int user_id; public bool is_joined; public bool has_active_entry; public int entry_id; public string entry_status; public int attempts_used; public int max_entries_per_user; public bool can_join; public bool can_play; public string button_state; public string button_label; public int entry_dragon_coins; public int entry_dragon_coins_local; } private void EnsureEventSystem() { if (FindFirstObjectByType() != null) return; GameObject go = new GameObject("EventSystem"); go.AddComponent(); go.AddComponent(); } private GameObject CreatePanel(string name, Transform parent, Color color) { GameObject go = new GameObject(name, typeof(RectTransform)); go.transform.SetParent(parent, false); Image image = go.AddComponent(); image.color = color; RectTransform rect = go.GetComponent(); rect.anchorMin = new Vector2(0.5f, 0.5f); rect.anchorMax = new Vector2(0.5f, 0.5f); rect.sizeDelta = Vector2.zero; return go; } private Text CreateText(string name, Transform parent, Font font, int fontSize, FontStyle style, TextAnchor anchor, Color color) { GameObject go = new GameObject(name, typeof(RectTransform)); go.transform.SetParent(parent, false); Text text = go.AddComponent(); text.font = font; text.fontSize = fontSize; text.fontStyle = style; text.alignment = anchor; text.color = color; text.horizontalOverflow = HorizontalWrapMode.Overflow; text.verticalOverflow = VerticalWrapMode.Overflow; return text; } private void HideJoinAttemptButton() { if (joinButton == null) return; joinButton.onClick.RemoveAllListeners(); joinButton.interactable = false; joinButton.gameObject.SetActive(false); } private void ConfigureResultAndLeaderboardButtons() { if (resultButton != null) { resultButton.gameObject.SetActive(true); resultButton.onClick.RemoveListener(OpenFullResultPanel); resultButton.onClick.AddListener(OpenFullResultPanel); SetButtonLabel(resultButton, "Result"); RectTransform rt = resultButton.GetComponent(); if (rt != null) SetRect(rt, new Vector2(0.03f, 0.058f), new Vector2(0.32f, 0.102f)); } if (leaderboardButton != null) { leaderboardButton.gameObject.SetActive(true); RectTransform rt = leaderboardButton.GetComponent(); if (rt != null) SetRect(rt, new Vector2(0.35f, 0.058f), new Vector2(0.97f, 0.102f)); } } private void EnsureResultInEventHeroUi() { if (runtimeCanvas == null) return; eventHeroPanel = FindPanel("02_EventHero") ?? FindPanel("EventHero") ?? FindPanel("EventCard") ?? eventHeroPanel; if (eventHeroPanel == null) return; Font defaultFont = Resources.GetBuiltinResource("LegacyRuntime.ttf"); GameObject oldResultCard = FindPanel("ResultCard"); if (oldResultCard != null && oldResultCard != eventHeroPanel) oldResultCard.SetActive(false); if (eventInfoLabel != null && eventInfoLabel.transform.parent == eventHeroPanel.transform) SetRect(eventInfoLabel.rectTransform, new Vector2(0.05f, 0.35f), new Vector2(0.95f, 0.80f)); if (resultHeaderLabel == null || resultHeaderLabel.transform.parent != eventHeroPanel.transform) { resultHeaderLabel = CreateText("ResultHeader", eventHeroPanel.transform, defaultFont, 22, FontStyle.Bold, TextAnchor.MiddleLeft, new Color(1f, 0.86f, 0.66f)); resultHeaderLabel.text = "MY RESULT"; } if (resultLabel == null || resultLabel.transform.parent != eventHeroPanel.transform) { resultLabel = CreateText("Result", eventHeroPanel.transform, defaultFont, 22, FontStyle.Bold, TextAnchor.UpperLeft, new Color(1f, 0.95f, 0.88f)); resultLabel.horizontalOverflow = HorizontalWrapMode.Wrap; resultLabel.verticalOverflow = VerticalWrapMode.Overflow; } resultHeaderLabel.gameObject.SetActive(true); resultLabel.gameObject.SetActive(true); SetRect(resultHeaderLabel.rectTransform, new Vector2(0.05f, 0.22f), new Vector2(0.95f, 0.34f)); SetRect(resultLabel.rectTransform, new Vector2(0.05f, 0.06f), new Vector2(0.95f, 0.24f)); resultHeaderLabel.text = "MY RESULT"; } private void ApplyLeaderboardFullWidthLayout() { if (leaderboardCard != null) { RectTransform rt = leaderboardCard.GetComponent(); if (rt != null) SetRect(rt, new Vector2(0.03f, 0.045f), new Vector2(0.97f, 0.165f)); } if (leaderboardDropdownPanel != null) { RectTransform rt = leaderboardDropdownPanel.GetComponent(); if (rt != null) SetRect(rt, new Vector2(0.02f, 0.135f), new Vector2(0.98f, 0.72f)); } if (leaderboardDropdownTitleLabel != null) SetRect(leaderboardDropdownTitleLabel.rectTransform, new Vector2(0.04f, 0.86f), new Vector2(0.82f, 0.97f)); if (leaderboardDropdownCloseButton != null) { RectTransform rt = leaderboardDropdownCloseButton.GetComponent(); if (rt != null) SetRect(rt, new Vector2(0.88f, 0.865f), new Vector2(0.97f, 0.965f)); } GameObject scrollGo = FindPanel("LeaderboardDropdownScroll"); if (scrollGo != null) { RectTransform rt = scrollGo.GetComponent(); if (rt != null) SetRect(rt, new Vector2(0.035f, 0.07f), new Vector2(0.965f, 0.83f)); } } private void ConfigureSinglePlayNowButton() { if (sessionButton == null) return; RectTransform rt = sessionButton.GetComponent(); if (rt != null) SetRect(rt, new Vector2(0.35f, 0.008f), new Vector2(0.97f, 0.052f)); } private Text FindText(string name) { GameObject go = FindSceneObject(name); return go != null ? go.GetComponent() : null; } private Button FindButton(string name) { GameObject go = FindSceneObject(name); return go != null ? go.GetComponent