397 lines
14 KiB
C#

using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections.Generic;
using System;
using Firebase.Auth;
using System.Threading.Tasks;
namespace BulletHellTemplate
{
/// <summary>
/// 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.
/// </summary>
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<MapInfoData> _pendingRewards = new List<MapInfoData>();
private string currentLang;
private void Awake()
{
if (Singleton == null)
{
Singleton = this;
}
else
{
Destroy(gameObject);
}
}
/// <summary>
/// Called when the object is first loaded. We can set current language here.
/// Typically, we do not immediately show rewards. Wait for ShowPendingRewards() call.
/// </summary>
private void Start()
{
currentLang = LanguageManager.LanguageManager.Instance.GetCurrentLanguage();
rewardPopupMenu.SetActive(false);
}
/// <summary>
/// Public method to show all pending rewards from local data (PlayerPrefs).
/// Called by RewardManagerPopup after saving map completion, or whenever needed.
/// </summary>
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();
}
/// <summary>
/// Checks if mapData is valid for displaying a reward in the UI.
/// </summary>
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);
}
/// <summary>
/// Retrieves the MapInfoData by ID from the GameInstance.
/// </summary>
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);
}
/// <summary>
/// Creates UI entries for each pending map's rewards.
/// </summary>
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);
}
/// <summary>
/// Creates a UI entry for the special reward type (icon, frame, character, inventory).
/// </summary>
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;
}
}
/// <summary>
/// Generates one RewardPopupEntry in the UI with a specific icon and label (amount).
/// </summary>
private void CreateEntry(Sprite icon, string amount)
{
var entry = Instantiate(rewardEntryPrefab, entryContainer);
entry.Setup(icon, amount);
}
/// <summary>
/// Clears existing entries to rebuild the UI.
/// </summary>
private void ClearEntries()
{
foreach (Transform child in entryContainer)
{
Destroy(child.gameObject);
}
}
/// <summary>
/// Claims all pending rewards in the UI, marking them as claimed locally (and optionally on Firestore).
/// Then refreshes the display.
/// </summary>
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();
}
/// <summary>
/// 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.
/// </summary>
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}");
}
}
/// <summary>
/// Applies currency / exp / special item to the player's account.
/// </summary>
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);
}
/// <summary>
/// Creates a temporary BattlePassItem for the special reward (icon/frame/character/inventory).
/// </summary>
private BattlePassItem CreateBattlePassItem(MapInfoData mapData)
{
var item = ScriptableObject.CreateInstance<BattlePassItem>();
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;
}
/// <summary>
/// Displays an error message in the UI for a limited time.
/// </summary>
private void ShowError(string message)
{
errorText.text = message;
CancelInvoke(nameof(ClearError));
Invoke(nameof(ClearError), errorDisplayTime);
}
/// <summary>
/// Clears the displayed error message.
/// </summary>
private void ClearError()
{
errorText.text = string.Empty;
}
/// <summary>
/// Retrieves a translated string or returns the fallback if not found.
/// </summary>
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;
}
}
}