using UnityEngine; using System.Collections.Generic; using Firebase.Firestore; using Firebase.Auth; using System.Threading.Tasks; using System; using System.Collections; namespace BulletHellTemplate { /// /// Manages reward data saving and local tracking (PlayerPrefs). /// This object uses DontDestroyOnLoad to persist across scenes. /// 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(); } /// /// 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. /// /// Identifier of the map completed. 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}"); } } /// /// Coroutine that waits until UIRewardManagerPopup.Singleton is not null, /// then calls ShowPendingRewards() to display the local pending data. /// private IEnumerator WaitAndShowRewards() { while (UIRewardManagerPopup.Singleton == null) { yield return null; } yield return null; UIRewardManagerPopup.Singleton.ShowPendingRewards(); } /// /// 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. /// 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("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}"); } } /// /// Writes the map completion data in Firestore, setting 'claimed' to false by default. /// private async Task SaveMapCompletionFirestore(int mapId, string userId) { var data = new Dictionary { { "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."); } /// /// Finds the MapInfoData object by ID from GameInstance. /// 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); } /// /// Determines if a map's first-time reward should be saved (local + Firestore). /// 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); } /// /// Marks a map as completed in PlayerPrefs (meaning there's a pending reward if not claimed). /// public void MarkMapCompleted(int mapId) { if (!IsMapCompleted(mapId)) { PlayerPrefs.SetInt(CompletedMapKey + mapId, 1); PlayerPrefs.Save(); } } /// /// Checks if a given map is marked completed (pending reward) in PlayerPrefs. /// public bool IsMapCompleted(int mapId) { return PlayerPrefs.GetInt(CompletedMapKey + mapId, 0) == 1; } /// /// Marks a map reward as claimed in PlayerPrefs (no longer pending). /// public void MarkRewardClaimed(int mapId) { PlayerPrefs.SetInt(ClaimedRewardKey + mapId, 1); PlayerPrefs.Save(); } /// /// Checks if a reward was already claimed locally. /// public bool IsRewardClaimed(int mapId) { return PlayerPrefs.GetInt(ClaimedRewardKey + mapId, 0) == 1; } /// /// Returns all unclaimed maps by checking local PlayerPrefs (completed but not claimed). /// This method does NOT call Firestore; it uses offline data only. /// public List GetLocalUnclaimedMaps() { List result = new List(); 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; } } }