397 lines
14 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|