328 lines
11 KiB
C#
328 lines
11 KiB
C#
|
using UnityEngine;
|
||
|
using System.Collections.Generic;
|
||
|
using Firebase.Firestore;
|
||
|
using Firebase.Auth;
|
||
|
using System.Threading.Tasks;
|
||
|
using System;
|
||
|
using System.Collections;
|
||
|
|
||
|
namespace BulletHellTemplate
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// Manages reward data saving and local tracking (PlayerPrefs).
|
||
|
/// This object uses DontDestroyOnLoad to persist across scenes.
|
||
|
/// </summary>
|
||
|
public class RewardManagerPopup : MonoBehaviour
|
||
|
{
|
||
|
public static RewardManagerPopup Singleton;
|
||
|
|
||
|
// PlayerPrefs keys
|
||
|
private const string CompletedMapKey = "CompletedMap_";
|
||
|
private const string ClaimedRewardKey = "ClaimedReward_";
|
||
|
|
||
|
private FirebaseFirestore _db;
|
||
|
private FirebaseAuth _auth;
|
||
|
|
||
|
private void Awake()
|
||
|
{
|
||
|
if (Singleton == null)
|
||
|
{
|
||
|
Singleton = this;
|
||
|
DontDestroyOnLoad(gameObject);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Destroy(gameObject);
|
||
|
}
|
||
|
StartWaitAndLoadPendingMapRewards();
|
||
|
}
|
||
|
|
||
|
public void StartWaitAndLoadPendingMapRewards()
|
||
|
{
|
||
|
StartCoroutine(WaitAndLoadPendingMapRewards());
|
||
|
}
|
||
|
|
||
|
public IEnumerator WaitAndLoadPendingMapRewards()
|
||
|
{
|
||
|
while (!BackendManager.Singleton.CheckInitialized())
|
||
|
{
|
||
|
yield return null;
|
||
|
}
|
||
|
StartSyncRewardsFromServer();
|
||
|
}
|
||
|
|
||
|
|
||
|
public async void StartSyncRewardsFromServer()
|
||
|
{
|
||
|
await SyncRewardsFromServer();
|
||
|
}
|
||
|
|
||
|
|
||
|
/// <summary>
|
||
|
/// Called when a map is completed successfully. Saves that completion
|
||
|
/// both locally (PlayerPrefs) and to Firestore, unless it was already claimed.
|
||
|
/// After saving, it waits for the UIRewardManagerPopup to exist,
|
||
|
/// then shows any newly pending rewards.
|
||
|
/// </summary>
|
||
|
/// <param name="mapId">Identifier of the map completed.</param>
|
||
|
public async void TrySaveCompletedMap(int mapId)
|
||
|
{
|
||
|
_db = FirebaseManager.Singleton.firestore;
|
||
|
_auth = FirebaseManager.Singleton.auth;
|
||
|
if (_auth == null)
|
||
|
{
|
||
|
Debug.LogError("Authentication instance (_auth) is null. Cannot save map completion.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var user = _auth.CurrentUser;
|
||
|
if (user == null)
|
||
|
{
|
||
|
Debug.LogError("User not authenticated. Cannot save map completion.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Local map data check
|
||
|
var mapData = GetMapDataById(mapId);
|
||
|
if (mapData == null)
|
||
|
{
|
||
|
Debug.LogError($"Map data not found for mapId: {mapId}");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// If there's no valid reward to save, we skip.
|
||
|
if (!ShouldSaveMapCompletion(mapData))
|
||
|
{
|
||
|
Debug.Log($"Map completion should not be saved for mapId: {mapId}");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// If the reward is already claimed locally, skip.
|
||
|
if (IsRewardClaimed(mapId))
|
||
|
{
|
||
|
Debug.Log($"Reward for map {mapId} already claimed.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
try
|
||
|
{
|
||
|
// Check Firestore if there's already a doc for that map
|
||
|
var docRef = _db.Collection("Players")
|
||
|
.Document(user.UserId)
|
||
|
.Collection("rewardsToRedeem")
|
||
|
.Document(mapId.ToString());
|
||
|
|
||
|
var snapshot = await docRef.GetSnapshotAsync();
|
||
|
if (!snapshot.Exists)
|
||
|
{
|
||
|
// If not in Firestore, create doc
|
||
|
await SaveMapCompletionFirestore(mapId, user.UserId);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Debug.Log($"Map {mapId} already has pending reward in Firestore.");
|
||
|
}
|
||
|
|
||
|
// Mark locally as completed, so we know there's a pending reward.
|
||
|
MarkMapCompleted(mapId);
|
||
|
|
||
|
// After finishing the save, wait for UI to show
|
||
|
StartCoroutine(WaitAndShowRewards());
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError($"Error in TrySaveCompletedMap: {e.Message}");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Coroutine that waits until UIRewardManagerPopup.Singleton is not null,
|
||
|
/// then calls ShowPendingRewards() to display the local pending data.
|
||
|
/// </summary>
|
||
|
private IEnumerator WaitAndShowRewards()
|
||
|
{
|
||
|
while (UIRewardManagerPopup.Singleton == null)
|
||
|
{
|
||
|
yield return null;
|
||
|
}
|
||
|
|
||
|
yield return null;
|
||
|
|
||
|
UIRewardManagerPopup.Singleton.ShowPendingRewards();
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// NEW FUNCTION:
|
||
|
/// Loads all 'rewardsToRedeem' from Firestore and updates local PlayerPrefs accordingly.
|
||
|
/// - If 'claimed' == false, marks that map as completed locally (pending).
|
||
|
/// - If 'claimed' == true, marks that map as claimed locally (no longer pending).
|
||
|
/// This is typically called once after BackendManager.CheckInitialized() is true,
|
||
|
/// so the user has the latest data offline.
|
||
|
/// </summary>
|
||
|
public async Task SyncRewardsFromServer()
|
||
|
{
|
||
|
_db = FirebaseManager.Singleton.firestore;
|
||
|
_auth = FirebaseManager.Singleton.auth;
|
||
|
if (_auth == null)
|
||
|
{
|
||
|
Debug.LogError("Auth is null in SyncRewardsFromServer.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var user = _auth.CurrentUser;
|
||
|
if (user == null)
|
||
|
{
|
||
|
Debug.LogError("User is not authenticated in SyncRewardsFromServer.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
try
|
||
|
{
|
||
|
var docs = await _db.Collection("Players")
|
||
|
.Document(user.UserId)
|
||
|
.Collection("rewardsToRedeem")
|
||
|
.GetSnapshotAsync();
|
||
|
|
||
|
Debug.Log($"SyncRewardsFromServer found {docs.Count} docs in rewardsToRedeem.");
|
||
|
|
||
|
foreach (var doc in docs.Documents)
|
||
|
{
|
||
|
bool claimed = doc.ContainsField("claimed") && doc.GetValue<bool>("claimed");
|
||
|
|
||
|
int mapId = int.Parse(doc.Id);
|
||
|
|
||
|
if (!claimed)
|
||
|
{
|
||
|
MarkMapCompleted(mapId);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
MarkRewardClaimed(mapId);
|
||
|
}
|
||
|
}
|
||
|
StartCoroutine(WaitAndShowRewards());
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError($"SyncRewardsFromServer error: {e.Message}");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/// <summary>
|
||
|
/// Writes the map completion data in Firestore, setting 'claimed' to false by default.
|
||
|
/// </summary>
|
||
|
private async Task SaveMapCompletionFirestore(int mapId, string userId)
|
||
|
{
|
||
|
var data = new Dictionary<string, object>
|
||
|
{
|
||
|
{ "mapId", mapId },
|
||
|
{ "claimed", false },
|
||
|
{ "timestamp", FieldValue.ServerTimestamp }
|
||
|
};
|
||
|
|
||
|
var docRef = _db.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("rewardsToRedeem")
|
||
|
.Document(mapId.ToString());
|
||
|
|
||
|
await docRef.SetAsync(data);
|
||
|
Debug.Log($"Map {mapId} saved successfully in Firestore.");
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Finds the MapInfoData object by ID from GameInstance.
|
||
|
/// </summary>
|
||
|
private MapInfoData GetMapDataById(int mapId)
|
||
|
{
|
||
|
if (GameInstance.Singleton == null ||
|
||
|
GameInstance.Singleton.mapInfoData == null ||
|
||
|
GameInstance.Singleton.mapInfoData.Length == 0)
|
||
|
{
|
||
|
Debug.LogError("GameInstance or mapInfoData not initialized.");
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
return Array.Find(GameInstance.Singleton.mapInfoData, m => m != null && m.mapId == mapId);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Determines if a map's first-time reward should be saved (local + Firestore).
|
||
|
/// </summary>
|
||
|
private bool ShouldSaveMapCompletion(MapInfoData mapData)
|
||
|
{
|
||
|
if (mapData == null) return false;
|
||
|
|
||
|
// Must have first-time reward enabled and either currency or special item set
|
||
|
return mapData.isRewardOnCompleteFirstTime &&
|
||
|
(mapData.WinMapRewards.Count > 0 || mapData.rewardType != MapRewardType.None);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Marks a map as completed in PlayerPrefs (meaning there's a pending reward if not claimed).
|
||
|
/// </summary>
|
||
|
public void MarkMapCompleted(int mapId)
|
||
|
{
|
||
|
if (!IsMapCompleted(mapId))
|
||
|
{
|
||
|
PlayerPrefs.SetInt(CompletedMapKey + mapId, 1);
|
||
|
PlayerPrefs.Save();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Checks if a given map is marked completed (pending reward) in PlayerPrefs.
|
||
|
/// </summary>
|
||
|
public bool IsMapCompleted(int mapId)
|
||
|
{
|
||
|
return PlayerPrefs.GetInt(CompletedMapKey + mapId, 0) == 1;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Marks a map reward as claimed in PlayerPrefs (no longer pending).
|
||
|
/// </summary>
|
||
|
public void MarkRewardClaimed(int mapId)
|
||
|
{
|
||
|
PlayerPrefs.SetInt(ClaimedRewardKey + mapId, 1);
|
||
|
PlayerPrefs.Save();
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Checks if a reward was already claimed locally.
|
||
|
/// </summary>
|
||
|
public bool IsRewardClaimed(int mapId)
|
||
|
{
|
||
|
return PlayerPrefs.GetInt(ClaimedRewardKey + mapId, 0) == 1;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Returns all unclaimed maps by checking local PlayerPrefs (completed but not claimed).
|
||
|
/// This method does NOT call Firestore; it uses offline data only.
|
||
|
/// </summary>
|
||
|
public List<int> GetLocalUnclaimedMaps()
|
||
|
{
|
||
|
List<int> result = new List<int>();
|
||
|
if (GameInstance.Singleton == null || GameInstance.Singleton.mapInfoData == null)
|
||
|
{
|
||
|
Debug.LogWarning("No map data in GameInstance.Singleton.");
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
foreach (var map in GameInstance.Singleton.mapInfoData)
|
||
|
{
|
||
|
if (map == null) continue;
|
||
|
|
||
|
int mapId = map.mapId;
|
||
|
bool completed = IsMapCompleted(mapId);
|
||
|
bool claimed = IsRewardClaimed(mapId);
|
||
|
|
||
|
if (completed && !claimed)
|
||
|
{
|
||
|
result.Add(mapId);
|
||
|
}
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
}
|
||
|
}
|