289 lines
9.6 KiB
C#
289 lines
9.6 KiB
C#
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using TMPro;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using Cysharp.Threading.Tasks;
|
|
|
|
namespace BulletHellTemplate
|
|
{
|
|
/// <summary>
|
|
/// Popup that displays pending "first completion" map rewards and lets the player redeem them.
|
|
///
|
|
/// Relies on RewardManagerPopup for pending + claimed state.
|
|
/// When the player clicks Redeem, we call the authoritative server endpoint
|
|
/// (via BackendManager.Service.ApplyMapRewardsAsync); on success we mark claimed locally
|
|
/// and update UI/economy using the server echo already applied in the backend service extension.
|
|
/// </summary>
|
|
public sealed class UIRewardManagerPopup : MonoBehaviour
|
|
{
|
|
public static UIRewardManagerPopup Singleton;
|
|
|
|
[Header("UI Components")]
|
|
[Tooltip("Root popup GameObject.")]
|
|
public GameObject rewardPopupMenu;
|
|
|
|
[Tooltip("Prefab used for each reward entry row.")]
|
|
public RewardPopupEntry rewardEntryPrefab;
|
|
|
|
[Tooltip("Parent container that holds spawned reward entry rows.")]
|
|
public Transform entryContainer;
|
|
|
|
[Tooltip("Button that redeems all pending map rewards.")]
|
|
public Button redeemButton;
|
|
|
|
[Tooltip("Text field used to display temporary error messages.")]
|
|
public TextMeshProUGUI errorText;
|
|
|
|
[Header("Icons for EXP types")]
|
|
[Tooltip("Icon representing Account EXP.")]
|
|
public Sprite ExpAccountIcon;
|
|
[Tooltip("Icon representing Mastery EXP.")]
|
|
public Sprite ExpMasteryIcon;
|
|
[Tooltip("Icon representing Character EXP.")]
|
|
public Sprite ExpCharacterIcon;
|
|
|
|
[Header("Settings")]
|
|
[Tooltip("Seconds to display transient error messages.")]
|
|
public float errorDisplayTime = 1.5f;
|
|
|
|
[Tooltip("Fallback error prefix if translation unavailable.")]
|
|
public string errorMessage = "Error loading rewards:";
|
|
|
|
[Tooltip("Translated versions of the fallback error message.")]
|
|
public NameTranslatedByLanguage[] errorMessageTranslated;
|
|
|
|
private readonly List<MapInfoData> _pendingRewards = new();
|
|
private string _currentLang;
|
|
|
|
private void Awake()
|
|
{
|
|
if (Singleton == null)
|
|
{
|
|
Singleton = this;
|
|
}
|
|
else
|
|
{
|
|
Destroy(gameObject);
|
|
}
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
_currentLang = LanguageManager.LanguageManager.Instance.GetCurrentLanguage();
|
|
if (rewardPopupMenu != null)
|
|
rewardPopupMenu.SetActive(false);
|
|
|
|
ShowPendingRewards();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Query pending maps from RewardManagerPopup and rebuild the popup UI.
|
|
/// </summary>
|
|
public void ShowPendingRewards()
|
|
{
|
|
_pendingRewards.Clear();
|
|
|
|
var unclaimed = RewardManagerPopup.Singleton?.GetLocalUnclaimedMaps();
|
|
if (unclaimed == null || unclaimed.Count == 0)
|
|
{
|
|
HidePopup();
|
|
return;
|
|
}
|
|
|
|
foreach (var mapId in unclaimed)
|
|
{
|
|
var mapData = GetMapDataById(mapId);
|
|
if (IsValidRewardMap(mapData))
|
|
_pendingRewards.Add(mapData);
|
|
}
|
|
|
|
if (_pendingRewards.Count == 0)
|
|
{
|
|
HidePopup();
|
|
return;
|
|
}
|
|
|
|
ShowPopup();
|
|
PopulateRewardEntries();
|
|
}
|
|
|
|
private void ShowPopup()
|
|
{
|
|
if (rewardPopupMenu != null)
|
|
rewardPopupMenu.SetActive(true);
|
|
}
|
|
|
|
private void HidePopup()
|
|
{
|
|
if (rewardPopupMenu != null)
|
|
rewardPopupMenu.SetActive(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds the UI rows for all pending rewards.
|
|
/// </summary>
|
|
private void PopulateRewardEntries()
|
|
{
|
|
ClearEntries();
|
|
|
|
foreach (var mapData in _pendingRewards)
|
|
{
|
|
// Currency / EXP rewards
|
|
if (mapData.WinMapRewards != null)
|
|
{
|
|
foreach (var r in mapData.WinMapRewards)
|
|
{
|
|
if (r.currency != null)
|
|
CreateEntry(r.currency.icon, r.amount.ToString());
|
|
|
|
if (r.accountExp > 0)
|
|
CreateEntry(ExpAccountIcon, r.accountExp.ToString());
|
|
|
|
if (r.characterExp > 0)
|
|
CreateEntry(ExpCharacterIcon, r.characterExp.ToString());
|
|
|
|
if (r.characterMasteryAmount > 0)
|
|
CreateEntry(ExpMasteryIcon, r.characterMasteryAmount.ToString());
|
|
}
|
|
}
|
|
|
|
// Special reward
|
|
if (mapData.rewardType != MapRewardType.None)
|
|
CreateSpecialRewardEntry(mapData);
|
|
}
|
|
|
|
if (redeemButton != null)
|
|
{
|
|
redeemButton.onClick.RemoveAllListeners();
|
|
redeemButton.onClick.AddListener(() => ClaimAllRewards().Forget());
|
|
}
|
|
}
|
|
|
|
private void ClearEntries()
|
|
{
|
|
if (entryContainer == null) return;
|
|
foreach (Transform child in entryContainer)
|
|
Destroy(child.gameObject);
|
|
}
|
|
|
|
private void CreateEntry(Sprite icon, string amount)
|
|
{
|
|
if (rewardEntryPrefab == null || entryContainer == null) return;
|
|
var entry = Instantiate(rewardEntryPrefab, entryContainer);
|
|
entry.Setup(icon, amount);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Redeems all pending map rewards sequentially (await each to avoid request burst).
|
|
/// </summary>
|
|
private async UniTaskVoid ClaimAllRewards()
|
|
{
|
|
if (redeemButton != null)
|
|
redeemButton.interactable = false;
|
|
|
|
foreach (var mapData in _pendingRewards)
|
|
await ClaimSingleMapRewardAsync(mapData);
|
|
|
|
// refresh after claims
|
|
HidePopup();
|
|
if (redeemButton != null)
|
|
redeemButton.interactable = true;
|
|
|
|
UIMainMenu.Singleton?.LoadPlayerInfo();
|
|
UIMainMenu.Singleton?.LoadCharacterInfo();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Claim a single map reward from the server; mark local on success.
|
|
/// </summary>
|
|
private async UniTask ClaimSingleMapRewardAsync(MapInfoData mapData)
|
|
{
|
|
if (mapData == null) return;
|
|
|
|
int characterId = PlayerSave.GetSelectedCharacter();
|
|
|
|
var result = await BackendManager.Service.ApplyMapRewardsAsync(
|
|
new MapCompletionRewardData
|
|
{
|
|
mapId = mapData.mapId,
|
|
characterId = characterId
|
|
});
|
|
|
|
if (!result.Success)
|
|
{
|
|
Debug.LogWarning($"[UIRewardManagerPopup] Map {mapData.mapId} claim failed (code:{result.Reason}).");
|
|
ShowError($"Reward {mapData.mapId} failed ({result.Reason})");
|
|
return;
|
|
}
|
|
|
|
RewardManagerPopup.Singleton?.MarkRewardClaimed(mapData.mapId);
|
|
}
|
|
|
|
private bool IsValidRewardMap(MapInfoData mapData)
|
|
{
|
|
if (mapData == null) return false;
|
|
bool hasList = mapData.WinMapRewards != null && mapData.WinMapRewards.Count > 0;
|
|
bool hasSpecial = mapData.rewardType != MapRewardType.None;
|
|
return mapData.isRewardOnCompleteFirstTime && (hasList || hasSpecial);
|
|
}
|
|
|
|
private MapInfoData GetMapDataById(int mapId)
|
|
{
|
|
var maps = GameInstance.Singleton?.mapInfoData;
|
|
if (maps == null) return null;
|
|
return Array.Find(maps, m => m != null && m.mapId == mapId);
|
|
}
|
|
|
|
private void ShowError(string message)
|
|
{
|
|
if (errorText == null) return;
|
|
errorText.text = message;
|
|
CancelInvoke(nameof(ClearError));
|
|
Invoke(nameof(ClearError), errorDisplayTime);
|
|
}
|
|
|
|
private void ClearError()
|
|
{
|
|
if (errorText != null)
|
|
errorText.text = string.Empty;
|
|
}
|
|
|
|
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, StringComparison.OrdinalIgnoreCase) &&
|
|
!string.IsNullOrEmpty(trans.Translate))
|
|
{
|
|
return trans.Translate;
|
|
}
|
|
}
|
|
}
|
|
return fallback;
|
|
}
|
|
}
|
|
}
|