using UnityEngine; using UnityEngine.UI; using TMPro; using System.Collections.Generic; using System; using Firebase.Auth; using System.Threading.Tasks; namespace BulletHellTemplate { /// /// Manages the UI popup for any pending map rewards. Relies on local data from RewardManagerPopup. /// No DontDestroyOnLoad here, so it will be recreated in the main menu scene. /// public class UIRewardManagerPopup : MonoBehaviour { public static UIRewardManagerPopup Singleton; [Header("UI Components")] [Tooltip("Main reward popup menu GameObject")] public GameObject rewardPopupMenu; [Tooltip("Prefab for individual reward entries")] public RewardPopupEntry rewardEntryPrefab; [Tooltip("Container for reward entries")] public Transform entryContainer; [Tooltip("Button to claim all rewards")] public Button redeemButton; [Tooltip("Text component for error messages")] public TextMeshProUGUI errorText; [Header("Icons for Experience Types")] [Tooltip("Icon to represent Account EXP in reward entries")] public Sprite ExpAccountIcon; [Tooltip("Icon to represent Mastery EXP in reward entries")] public Sprite ExpMasteryIcon; [Tooltip("Icon to represent Character EXP in reward entries")] public Sprite ExpCharacterIcon; [Header("Settings")] [Tooltip("Time to display error messages in seconds")] public float errorDisplayTime = 1.5f; [Tooltip("Fallback error message in case translations are not found")] public string errorMessage = "Error loading rewards:"; [Tooltip("Translated versions of the fallback error message")] public NameTranslatedByLanguage[] errorMessageTranslated; private List _pendingRewards = new List(); private string currentLang; private void Awake() { if (Singleton == null) { Singleton = this; } else { Destroy(gameObject); } } /// /// Called when the object is first loaded. We can set current language here. /// Typically, we do not immediately show rewards. Wait for ShowPendingRewards() call. /// private void Start() { currentLang = LanguageManager.LanguageManager.Instance.GetCurrentLanguage(); rewardPopupMenu.SetActive(false); } /// /// Public method to show all pending rewards from local data (PlayerPrefs). /// Called by RewardManagerPopup after saving map completion, or whenever needed. /// public void ShowPendingRewards() { // Clear old list _pendingRewards.Clear(); // Retrieve local unclaimed maps var unclaimedMapIds = RewardManagerPopup.Singleton.GetLocalUnclaimedMaps(); // Build a list of MapInfoData for each unclaimed map foreach (var mapId in unclaimedMapIds) { MapInfoData mapData = GetMapDataById(mapId); if (IsValidRewardMap(mapData)) { _pendingRewards.Add(mapData); } } // If no pending rewards, hide popup if (_pendingRewards.Count == 0) { rewardPopupMenu.SetActive(false); return; } // Otherwise, display them rewardPopupMenu.SetActive(true); PopulateRewardEntries(); } /// /// Checks if mapData is valid for displaying a reward in the UI. /// private bool IsValidRewardMap(MapInfoData mapData) { if (mapData == null) return false; bool hasCurrencyOrExp = mapData.WinMapRewards.Count > 0; bool hasSpecial = mapData.rewardType != MapRewardType.None; return mapData.isRewardOnCompleteFirstTime && (hasCurrencyOrExp || hasSpecial); } /// /// Retrieves the MapInfoData by ID from the GameInstance. /// private MapInfoData GetMapDataById(int mapId) { if (GameInstance.Singleton == null || GameInstance.Singleton.mapInfoData == null) return null; return Array.Find(GameInstance.Singleton.mapInfoData, m => m != null && m.mapId == mapId); } /// /// Creates UI entries for each pending map's rewards. /// private void PopulateRewardEntries() { ClearEntries(); foreach (var mapData in _pendingRewards) { // Show currency / exp rewards foreach (var reward in mapData.WinMapRewards) { if (reward.currency != null) { CreateEntry(reward.currency.icon, reward.amount.ToString()); } if (reward.accountExp > 0) { CreateEntry(ExpAccountIcon, reward.accountExp.ToString()); } if (reward.characterExp > 0) { CreateEntry(ExpCharacterIcon, reward.characterExp.ToString()); } if (reward.characterMasteryAmount > 0) { CreateEntry(ExpMasteryIcon, reward.characterMasteryAmount.ToString()); } } // Show special item if assigned if (mapData.rewardType != MapRewardType.None) { CreateSpecialRewardEntry(mapData); } } redeemButton.onClick.RemoveAllListeners(); redeemButton.onClick.AddListener(ClaimAllRewards); } /// /// Creates a UI entry for the special reward type (icon, frame, character, inventory). /// private void CreateSpecialRewardEntry(MapInfoData mapData) { switch (mapData.rewardType) { case MapRewardType.Icon when mapData.iconItem != null: CreateEntry(mapData.iconItem.icon, "1"); break; case MapRewardType.Frame when mapData.frameItem != null: CreateEntry(mapData.frameItem.icon, "1"); break; case MapRewardType.Character when mapData.characterData != null: CreateEntry(mapData.characterData.icon, "1"); break; case MapRewardType.InventoryItem when mapData.inventoryItem != null: CreateEntry(mapData.inventoryItem.itemIcon, "1"); break; default: break; } } /// /// Generates one RewardPopupEntry in the UI with a specific icon and label (amount). /// private void CreateEntry(Sprite icon, string amount) { var entry = Instantiate(rewardEntryPrefab, entryContainer); entry.Setup(icon, amount); } /// /// Clears existing entries to rebuild the UI. /// private void ClearEntries() { foreach (Transform child in entryContainer) { Destroy(child.gameObject); } } /// /// Claims all pending rewards in the UI, marking them as claimed locally (and optionally on Firestore). /// Then refreshes the display. /// private async void ClaimAllRewards() { redeemButton.interactable = false; foreach (var mapData in _pendingRewards) { try { RewardManagerPopup.Singleton.MarkRewardClaimed(mapData.mapId); await FinalizeRewardClaimOnServer(mapData.mapId); ApplyRewards(mapData); } catch (Exception innerException) { Debug.LogWarning($"Reward {mapData.mapId} failed to claim: {innerException.Message}"); ShowError($"Reward {mapData.mapId} failed: {innerException.Message}"); continue; } } rewardPopupMenu.SetActive(false); redeemButton.interactable = true; UIMainMenu.Singleton.LoadPlayerInfo(); UIMainMenu.Singleton.LoadCharacterInfo(); } /// /// Sets the Firestore 'claimed' field to true, so if we re-sync in the future, /// it doesn't show up as unclaimed. This is optional, depending on your flow. /// private async Task FinalizeRewardClaimOnServer(int mapId) { try { // Only do this if user is logged in var user = FirebaseManager.Singleton.auth.CurrentUser; if (user == null) return; var db = FirebaseManager.Singleton.firestore; if (db == null) return; var docRef = db.Collection("Players") .Document(user.UserId) .Collection("rewardsToRedeem") .Document(mapId.ToString()); await docRef.UpdateAsync("claimed", true); } catch (Exception e) { // Not fatal for local usage, but you can log Debug.LogWarning($"Could not finalize claim on server: {e.Message}"); } } /// /// Applies currency / exp / special item to the player's account. /// private void ApplyRewards(MapInfoData mapData) { // Currency / EXP foreach (var reward in mapData.WinMapRewards) { if (reward.currency != null) { int current = MonetizationManager.GetCurrency(reward.currency.coinID); MonetizationManager.SetCurrency(reward.currency.coinID, current + reward.amount); } if (reward.accountExp > 0) { GameManager.Singleton.AddAccountExp(reward.accountExp); } if (reward.characterExp > 0) { int currentCharacterId = PlayerSave.GetSelectedCharacter(); bool characterExpAdded = GameManager.Singleton.AddCharacterExp(currentCharacterId, reward.characterExp); if (characterExpAdded) { GameManager.Singleton.SaveCharacterBasicInfo(currentCharacterId); } } if (reward.characterMasteryAmount > 0) { int currentCharacterId = PlayerSave.GetSelectedCharacter(); bool masteryExpAdded = GameManager.Singleton.AddCharacterMasteryExp(currentCharacterId, reward.characterMasteryAmount); if (masteryExpAdded) { GameManager.Singleton.SaveCharacterBasicInfo(currentCharacterId); } } } // Special item var tempItem = CreateBattlePassItem(mapData); MonetizationManager.Singleton.BattlePassUnlockItem(tempItem); } /// /// Creates a temporary BattlePassItem for the special reward (icon/frame/character/inventory). /// private BattlePassItem CreateBattlePassItem(MapInfoData mapData) { var item = ScriptableObject.CreateInstance(); item.itemTitle = $"{mapData.mapName} Completion Reward"; item.itemDescription = "First time completion reward"; item.itemIcon = mapData.mapPreviewImage; switch (mapData.rewardType) { case MapRewardType.Icon: item.rewardType = BattlePassItem.RewardType.IconReward; item.iconReward = mapData.iconItem; break; case MapRewardType.Frame: item.rewardType = BattlePassItem.RewardType.FrameReward; item.frameReward = mapData.frameItem; break; case MapRewardType.Character: item.rewardType = BattlePassItem.RewardType.CharacterReward; item.characterData = new[] { mapData.characterData }; break; case MapRewardType.InventoryItem: item.rewardType = BattlePassItem.RewardType.InventoryItemReward; item.inventoryItems = new[] { mapData.inventoryItem }; break; } return item; } /// /// Displays an error message in the UI for a limited time. /// private void ShowError(string message) { errorText.text = message; CancelInvoke(nameof(ClearError)); Invoke(nameof(ClearError), errorDisplayTime); } /// /// Clears the displayed error message. /// private void ClearError() { errorText.text = string.Empty; } /// /// Retrieves a translated string or returns the fallback if not found. /// public 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; } } }