1628 lines
65 KiB
C#
1628 lines
65 KiB
C#
|
using System.Collections.Generic;
|
||
|
using System.Threading.Tasks;
|
||
|
using UnityEngine;
|
||
|
using Firebase;
|
||
|
using Firebase.Auth;
|
||
|
using Firebase.Firestore;
|
||
|
using System;
|
||
|
using System.Linq;
|
||
|
using static BulletHellTemplate.PlayerSave;
|
||
|
|
||
|
namespace BulletHellTemplate
|
||
|
{
|
||
|
public class FirebaseManager : MonoBehaviour
|
||
|
{
|
||
|
[Header("Component responsible for loading and saving user data")]
|
||
|
public FirebaseAuth auth;
|
||
|
public FirebaseFirestore firestore;
|
||
|
public static FirebaseManager Singleton;
|
||
|
|
||
|
[Header("Daily Quest Reset Configuration")]
|
||
|
public int resetHour = 0;
|
||
|
|
||
|
private void Awake()
|
||
|
{
|
||
|
if (Singleton == null)
|
||
|
{
|
||
|
Singleton = this;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Destroy(gameObject);
|
||
|
return;
|
||
|
}
|
||
|
DontDestroyOnLoad(gameObject);
|
||
|
|
||
|
}
|
||
|
public void Start()
|
||
|
{
|
||
|
StartInitializeFirebase();
|
||
|
}
|
||
|
public void StartInitializeFirebase()
|
||
|
{
|
||
|
InitializeFirebase();
|
||
|
}
|
||
|
|
||
|
private async void InitializeFirebase()
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
var dependencyStatus = await FirebaseApp.CheckAndFixDependenciesAsync();
|
||
|
if (dependencyStatus == DependencyStatus.Available)
|
||
|
{
|
||
|
auth = FirebaseAuth.DefaultInstance;
|
||
|
firestore = FirebaseFirestore.DefaultInstance;
|
||
|
|
||
|
FirebaseAuthManager.Singleton.InitializeAuthBackend(auth, firestore);
|
||
|
FirebaseSave.Singleton.InitializeFirebase(auth.CurrentUser, firestore);
|
||
|
|
||
|
Debug.Log("Firebase initialized successfully.");
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Debug.LogError("Could not resolve all Firebase dependencies: " + dependencyStatus);
|
||
|
}
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError("Failed to initialize Firebase: " + e.Message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads and synchronizes all required player data from Firestore in a single method.
|
||
|
/// If the player's document does not exist, it will create default data once.
|
||
|
/// This approach removes repeated checks and reduces extra snapshot calls.
|
||
|
/// </summary>
|
||
|
/// <param name="userId">The authenticated user's ID.</param>
|
||
|
/// <returns>A result message describing the outcome.</returns>
|
||
|
public async Task<string> LoadAndSyncPlayerData(string userId)
|
||
|
{
|
||
|
// Single checks at the beginning
|
||
|
if (firestore == null)
|
||
|
{
|
||
|
return "Failed to initialize Firestore.";
|
||
|
}
|
||
|
|
||
|
if (!MonetizationManager.Singleton.IsInitialized())
|
||
|
{
|
||
|
return "MonetizationManager is not initialized.";
|
||
|
}
|
||
|
|
||
|
if (auth == null || auth.CurrentUser == null)
|
||
|
{
|
||
|
return "No user is currently logged in.";
|
||
|
}
|
||
|
|
||
|
PlayerPrefs.DeleteAll();
|
||
|
PlayerPrefs.Save();
|
||
|
|
||
|
try
|
||
|
{
|
||
|
// 1) Check or create the main document
|
||
|
DocumentReference docRef = firestore.Collection("Players").Document(userId);
|
||
|
DocumentSnapshot snapshot = await docRef.GetSnapshotAsync();
|
||
|
|
||
|
if (!snapshot.Exists)
|
||
|
{
|
||
|
await CreateDefaultPlayerData(docRef);
|
||
|
snapshot = await docRef.GetSnapshotAsync();
|
||
|
if (!snapshot.Exists)
|
||
|
{
|
||
|
return "Failed to create default player data.";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 2) Parse minimal fields in the main document
|
||
|
Dictionary<string, object> playerData = snapshot.ToDictionary();
|
||
|
if (playerData.TryGetValue("PlayerName", out object pName) && pName != null)
|
||
|
{
|
||
|
PlayerSave.SetPlayerName(pName.ToString());
|
||
|
}
|
||
|
if (playerData.TryGetValue("PlayerIcon", out object pIcon) && pIcon != null)
|
||
|
{
|
||
|
PlayerSave.SetPlayerIcon(pIcon.ToString());
|
||
|
}
|
||
|
if (playerData.TryGetValue("PlayerFrame", out object pFrame) && pFrame != null)
|
||
|
{
|
||
|
PlayerSave.SetPlayerFrame(pFrame.ToString());
|
||
|
}
|
||
|
if (playerData.TryGetValue("AccountLevel", out object aLevelObj) && aLevelObj != null)
|
||
|
{
|
||
|
// Convert the object to int properly, handling Firestore's usual 'long' storage
|
||
|
int levelValue = Convert.ToInt32(aLevelObj);
|
||
|
PlayerSave.SetAccountLevel(levelValue);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// If the field doesn't exist or is null, set a default
|
||
|
PlayerSave.SetAccountLevel(1);
|
||
|
}
|
||
|
|
||
|
if (playerData.TryGetValue("AccountCurrentExp", out object aExpObj) && aExpObj != null)
|
||
|
{
|
||
|
int expValue = Convert.ToInt32(aExpObj);
|
||
|
PlayerSave.SetAccountCurrentExp(expValue);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
PlayerSave.SetAccountCurrentExp(0);
|
||
|
}
|
||
|
|
||
|
// 3) Run all other sub-loads in parallel to speed up the process
|
||
|
List<Task> loadTasks = new List<Task>
|
||
|
{
|
||
|
LoadAndSyncCurrencies(userId),
|
||
|
LoadPurchasedItemsAsync(userId),
|
||
|
LoadSelectedCharacter(userId),
|
||
|
LoadPlayerCharacterFavouriteAsync(userId),
|
||
|
LoadAndSyncAllCharacterUpgradesAsync(userId),
|
||
|
LoadUnlockedMapsAsync(userId),
|
||
|
ResetDailyQuestsIfNeeded(userId),
|
||
|
LoadQuestsAsync(userId),
|
||
|
LoadUsedCouponsAsync(userId),
|
||
|
CheckForSeasonEndAsync(userId),
|
||
|
LoadBattlePassProgressAsync(),
|
||
|
LoadClaimedRewardsFromFirebase(userId),
|
||
|
InitializeInventoryAsync(),
|
||
|
LoadAndSyncAllCharacterBasicInfoAsync(),
|
||
|
LoadCharacterSlotsAsync(),
|
||
|
LoadAndSyncAllCharacterUnlockedSkinsAsync(),
|
||
|
|
||
|
};
|
||
|
|
||
|
await Task.WhenAll(loadTasks);
|
||
|
await LoadRewardsDataAsync();
|
||
|
BackendManager.Singleton.SetInitialized();
|
||
|
return "Player data loaded successfully.";
|
||
|
}
|
||
|
catch (Exception ex)
|
||
|
{
|
||
|
Debug.LogError($"Failed to load and synchronize player data: {ex.Message}");
|
||
|
return $"Failed to load player data: {ex.Message}";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Creates a default player document if none exists.
|
||
|
/// </summary>
|
||
|
private async Task CreateDefaultPlayerData(DocumentReference docRef)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
string playerName = "GUEST-" + UnityEngine.Random.Range(100000, 999999);
|
||
|
string defaultIconId = GameInstance.Singleton.iconItems[0].iconId;
|
||
|
string defaultFrameId = GameInstance.Singleton.frameItems[0].frameId;
|
||
|
|
||
|
Dictionary<string, object> defaultData = new Dictionary<string, object>
|
||
|
{
|
||
|
{ "PlayerName", playerName },
|
||
|
{ "PlayerIcon", defaultIconId },
|
||
|
{ "PlayerFrame", defaultFrameId }
|
||
|
};
|
||
|
|
||
|
await docRef.SetAsync(defaultData);
|
||
|
Debug.Log($"Created default player data for user document: {docRef.Id}");
|
||
|
}
|
||
|
catch (Exception ex)
|
||
|
{
|
||
|
Debug.LogError($"Failed to create default player data: {ex.Message}");
|
||
|
throw;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads and synchronizes the player's currencies (parallel-ready, no repeated checks).
|
||
|
/// </summary>
|
||
|
private async Task LoadAndSyncCurrencies(string userId)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
CollectionReference currenciesCollection = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("Currencies");
|
||
|
|
||
|
foreach (Currency currency in MonetizationManager.Singleton.currencies)
|
||
|
{
|
||
|
DocumentReference currencyDoc = currenciesCollection.Document(currency.coinID);
|
||
|
DocumentSnapshot snapshot = await currencyDoc.GetSnapshotAsync();
|
||
|
|
||
|
if (snapshot.Exists)
|
||
|
{
|
||
|
int amount = snapshot.GetValue<int>("amount");
|
||
|
MonetizationManager.SetCurrency(currency.coinID, amount);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Dictionary<string, object> currencyData = new Dictionary<string, object>
|
||
|
{
|
||
|
{ "initialAmount", currency.initialAmount },
|
||
|
{ "amount", currency.initialAmount }
|
||
|
};
|
||
|
await currencyDoc.SetAsync(currencyData);
|
||
|
MonetizationManager.SetCurrency(currency.coinID, currency.initialAmount);
|
||
|
}
|
||
|
}
|
||
|
Debug.Log("Currencies loaded successfully.");
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError($"Failed to load currencies: {e.Message}");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads the selected character from the main document, parallel-ready.
|
||
|
/// </summary>
|
||
|
private async Task LoadSelectedCharacter(string userId)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
DocumentReference docRef = firestore.Collection("Players").Document(userId);
|
||
|
DocumentSnapshot snapshot = await docRef.GetSnapshotAsync();
|
||
|
|
||
|
if (snapshot.Exists)
|
||
|
{
|
||
|
int selectedCharacter = 0;
|
||
|
if (snapshot.ContainsField("selectedCharacter"))
|
||
|
{
|
||
|
selectedCharacter = snapshot.GetValue<int>("selectedCharacter");
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
await docRef.UpdateAsync("selectedCharacter", 0);
|
||
|
}
|
||
|
PlayerSave.SetSelectedCharacter(selectedCharacter);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Dictionary<string, object> data = new Dictionary<string, object> { { "selectedCharacter", 0 } };
|
||
|
await docRef.SetAsync(data);
|
||
|
PlayerSave.SetSelectedCharacter(0);
|
||
|
}
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError($"Failed to load selected character: {e.Message}");
|
||
|
}
|
||
|
}
|
||
|
// <summary>
|
||
|
/// Loads purchased items without using 'dynamic'. Each subdocument is handled with a specific typed method.
|
||
|
/// </summary>
|
||
|
private async Task LoadPurchasedItemsAsync(string userId)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
CollectionReference docRef = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("PurchasedItems");
|
||
|
|
||
|
// Default lists
|
||
|
var defaultCharacterList = new PurchasedCharacterList(new List<PurchasedCharacter>());
|
||
|
var defaultIconList = new PurchasedIconList(new List<PurchasedIcon>());
|
||
|
var defaultFrameList = new PurchasedFrameList(new List<PurchasedFrame>());
|
||
|
var defaultShopItemList = new PurchasedShopItemList(new List<PurchasedShopItem>());
|
||
|
|
||
|
// Call each "ensure + load" method separately
|
||
|
Task<PurchasedCharacterList> taskCharacters = EnsureCharactersDocExistsAndLoad(docRef.Document("Characters"), "Characters", defaultCharacterList);
|
||
|
Task<PurchasedIconList> taskIcons = EnsureIconsDocExistsAndLoad(docRef.Document("Icons"), "Icons", defaultIconList);
|
||
|
Task<PurchasedFrameList> taskFrames = EnsureFramesDocExistsAndLoad(docRef.Document("Frames"), "Frames", defaultFrameList);
|
||
|
Task<PurchasedShopItemList> taskShopItems = EnsureShopItemsDocExistsAndLoad(docRef.Document("ShopItems"), "ShopItems", defaultShopItemList);
|
||
|
|
||
|
// Wait for all to complete
|
||
|
await Task.WhenAll(taskCharacters, taskIcons, taskFrames, taskShopItems);
|
||
|
|
||
|
// Retrieve the final typed results
|
||
|
PurchasedCharacterList finalCharacters = taskCharacters.Result;
|
||
|
PurchasedIconList finalIcons = taskIcons.Result;
|
||
|
PurchasedFrameList finalFrames = taskFrames.Result;
|
||
|
PurchasedShopItemList finalShopItems = taskShopItems.Result;
|
||
|
PurchasedInventoryItemList finalItems = new PurchasedInventoryItemList(new List<PurchasedInventoryItem>());
|
||
|
|
||
|
// Merge results into MonetizationManager
|
||
|
MonetizationManager.Singleton.UpdatePurchasedItems(
|
||
|
finalCharacters.purchasedCharacters,
|
||
|
finalIcons.purchasedIcons,
|
||
|
finalFrames.purchasedFrames,
|
||
|
finalShopItems.purchasedShopItems,
|
||
|
finalItems.purchasedInventoryItems
|
||
|
);
|
||
|
}
|
||
|
catch (Exception ex)
|
||
|
{
|
||
|
Debug.LogError("Error loading purchased items: " + ex.Message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Ensures the 'Characters' subdocument exists, creates it if not, and returns a PurchasedCharacterList.
|
||
|
/// </summary>
|
||
|
private async Task<PurchasedCharacterList> EnsureCharactersDocExistsAndLoad(
|
||
|
DocumentReference docRef,
|
||
|
string fieldName,
|
||
|
PurchasedCharacterList defaultValue)
|
||
|
{
|
||
|
var snapshot = await docRef.GetSnapshotAsync();
|
||
|
if (!snapshot.Exists)
|
||
|
{
|
||
|
// Create a new document with an empty or default list
|
||
|
await docRef.SetAsync(new Dictionary<string, object>
|
||
|
{
|
||
|
{ fieldName, defaultValue.purchasedCharacters } // use the .purchasedCharacters list
|
||
|
});
|
||
|
return defaultValue;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Load existing data
|
||
|
var listData = snapshot.GetValue<List<object>>(fieldName);
|
||
|
var typed = ConvertToList<PurchasedCharacter>(listData);
|
||
|
return new PurchasedCharacterList(typed);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Ensures the 'Icons' subdocument exists, creates it if not, and returns a PurchasedIconList.
|
||
|
/// </summary>
|
||
|
private async Task<PurchasedIconList> EnsureIconsDocExistsAndLoad(
|
||
|
DocumentReference docRef,
|
||
|
string fieldName,
|
||
|
PurchasedIconList defaultValue)
|
||
|
{
|
||
|
var snapshot = await docRef.GetSnapshotAsync();
|
||
|
if (!snapshot.Exists)
|
||
|
{
|
||
|
await docRef.SetAsync(new Dictionary<string, object>
|
||
|
{
|
||
|
{ fieldName, defaultValue.purchasedIcons }
|
||
|
});
|
||
|
return defaultValue;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
var listData = snapshot.GetValue<List<object>>(fieldName);
|
||
|
var typed = ConvertToList<PurchasedIcon>(listData);
|
||
|
return new PurchasedIconList(typed);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Ensures the 'Frames' subdocument exists, creates it if not, and returns a PurchasedFrameList.
|
||
|
/// </summary>
|
||
|
private async Task<PurchasedFrameList> EnsureFramesDocExistsAndLoad(
|
||
|
DocumentReference docRef,
|
||
|
string fieldName,
|
||
|
PurchasedFrameList defaultValue)
|
||
|
{
|
||
|
var snapshot = await docRef.GetSnapshotAsync();
|
||
|
if (!snapshot.Exists)
|
||
|
{
|
||
|
await docRef.SetAsync(new Dictionary<string, object>
|
||
|
{
|
||
|
{ fieldName, defaultValue.purchasedFrames }
|
||
|
});
|
||
|
return defaultValue;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
var listData = snapshot.GetValue<List<object>>(fieldName);
|
||
|
var typed = ConvertToList<PurchasedFrame>(listData);
|
||
|
return new PurchasedFrameList(typed);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Ensures the 'ShopItems' subdocument exists, creates it if not, and returns a PurchasedShopItemList.
|
||
|
/// </summary>
|
||
|
private async Task<PurchasedShopItemList> EnsureShopItemsDocExistsAndLoad(
|
||
|
DocumentReference docRef,
|
||
|
string fieldName,
|
||
|
PurchasedShopItemList defaultValue)
|
||
|
{
|
||
|
var snapshot = await docRef.GetSnapshotAsync();
|
||
|
if (!snapshot.Exists)
|
||
|
{
|
||
|
await docRef.SetAsync(new Dictionary<string, object>
|
||
|
{
|
||
|
{ fieldName, defaultValue.purchasedShopItems }
|
||
|
});
|
||
|
return defaultValue;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
var listData = snapshot.GetValue<List<object>>(fieldName);
|
||
|
var typed = ConvertToList<PurchasedShopItem>(listData);
|
||
|
return new PurchasedShopItemList(typed);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads a single character's upgrades and stores them locally.
|
||
|
/// </summary>
|
||
|
private async Task LoadCharacterUpgradesAsync(string userId, int characterId)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
DocumentReference upgradesDocRef = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("Characters")
|
||
|
.Document(characterId.ToString())
|
||
|
.Collection("Upgrades")
|
||
|
.Document("Stats");
|
||
|
|
||
|
DocumentSnapshot snapshot = await upgradesDocRef.GetSnapshotAsync();
|
||
|
if (snapshot.Exists)
|
||
|
{
|
||
|
Dictionary<string, object> upgradesData = snapshot.ToDictionary();
|
||
|
foreach (var entry in upgradesData)
|
||
|
{
|
||
|
if (Enum.TryParse(entry.Key, out StatType statType))
|
||
|
{
|
||
|
int level = Convert.ToInt32(entry.Value);
|
||
|
PlayerSave.SaveCharacterUpgradeLevel(characterId, statType, level);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// If no upgrades, save local defaults as 0
|
||
|
foreach (StatType statType in Enum.GetValues(typeof(StatType)))
|
||
|
{
|
||
|
PlayerSave.SaveInitialCharacterUpgradeLevel(characterId, statType);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError($"Failed to load character upgrades for {characterId}: {e.Message}");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads and saves all character upgrades in parallel for each unlocked character.
|
||
|
/// </summary>
|
||
|
private async Task LoadAndSyncAllCharacterUpgradesAsync(string userId)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
List<Task> upgradeTasks = new List<Task>();
|
||
|
foreach (CharacterData character in GameInstance.Singleton.characterData)
|
||
|
{
|
||
|
if (character.CheckUnlocked || MonetizationManager.Singleton.IsCharacterPurchased(character.characterId.ToString()))
|
||
|
{
|
||
|
upgradeTasks.Add(LoadCharacterUpgradesAsync(userId, character.characterId));
|
||
|
}
|
||
|
}
|
||
|
await Task.WhenAll(upgradeTasks);
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError("Failed to load all character upgrades: " + e.Message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads the unlocked skins for a specific character from Firebase Firestore and saves them locally.
|
||
|
/// </summary>
|
||
|
/// <param name="userId">The user's ID.</param>
|
||
|
/// <param name="characterId">The ID of the character.</param>
|
||
|
public async Task LoadCharacterUnlockedSkinsAsync(string userId, int characterId)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
DocumentReference skinsDocRef = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("Characters")
|
||
|
.Document(characterId.ToString())
|
||
|
.Collection("UnlockedSkins")
|
||
|
.Document("Skins");
|
||
|
|
||
|
DocumentSnapshot snapshot = await skinsDocRef.GetSnapshotAsync();
|
||
|
if (snapshot.Exists)
|
||
|
{
|
||
|
Dictionary<string, object> skinsData = snapshot.ToDictionary();
|
||
|
if (skinsData.ContainsKey("skins"))
|
||
|
{
|
||
|
List<int> unlockedSkins = new List<int>();
|
||
|
// Firestore returns numeric array elements as long.
|
||
|
if (skinsData["skins"] is IList<object> skinsList)
|
||
|
{
|
||
|
foreach (var skin in skinsList)
|
||
|
{
|
||
|
if (skin is long l)
|
||
|
{
|
||
|
unlockedSkins.Add((int)l);
|
||
|
}
|
||
|
else if (skin is int i)
|
||
|
{
|
||
|
unlockedSkins.Add(i);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
// Save unlocked skins locally using PlayerSave.
|
||
|
PlayerSave.SaveCharacterUnlockedSkins(characterId, unlockedSkins);
|
||
|
Debug.Log($"Unlocked skins for character {characterId} loaded and saved locally.");
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Debug.LogWarning($"No 'skins' field found for character {characterId}.");
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Debug.LogWarning($"No unlocked skins document found for character {characterId}.");
|
||
|
}
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError($"Failed to load unlocked skins for character {characterId}: {e.Message}");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads and synchronizes unlocked skins for all unlocked or purchased characters.
|
||
|
/// </summary>
|
||
|
public async Task LoadAndSyncAllCharacterUnlockedSkinsAsync()
|
||
|
{
|
||
|
if (auth.CurrentUser == null)
|
||
|
{
|
||
|
Debug.LogError("No user is logged in.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
try
|
||
|
{
|
||
|
List<Task> tasks = new List<Task>();
|
||
|
foreach (CharacterData character in GameInstance.Singleton.characterData)
|
||
|
{
|
||
|
if (character.CheckUnlocked || MonetizationManager.Singleton.IsCharacterPurchased(character.characterId.ToString()))
|
||
|
{
|
||
|
tasks.Add(LoadCharacterUnlockedSkinsAsync(auth.CurrentUser.UserId, character.characterId));
|
||
|
}
|
||
|
}
|
||
|
await Task.WhenAll(tasks);
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError("Failed to load all character unlocked skins: " + e.Message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads unlocked maps, clearing local data first.
|
||
|
/// </summary>
|
||
|
private async Task LoadUnlockedMapsAsync(string userId)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
DocumentReference mapsDocRef = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("Progress")
|
||
|
.Document("UnlockedMaps");
|
||
|
|
||
|
DocumentSnapshot snapshot = await mapsDocRef.GetSnapshotAsync();
|
||
|
if (snapshot.Exists)
|
||
|
{
|
||
|
Dictionary<string, object> mapsData = snapshot.ToDictionary();
|
||
|
List<int> unlockedMapIds = new List<int>();
|
||
|
foreach (var entry in mapsData)
|
||
|
{
|
||
|
if (int.TryParse(entry.Key, out int mapId))
|
||
|
{
|
||
|
unlockedMapIds.Add(mapId);
|
||
|
}
|
||
|
}
|
||
|
PlayerSave.SetUnlockedMaps(unlockedMapIds);
|
||
|
}
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError("Failed to load unlocked maps: " + e.Message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads quests from the "Quests" document and saves them locally.
|
||
|
/// </summary>
|
||
|
private async Task LoadQuestsAsync(string userId)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
DocumentReference questsDocRef = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("Progress")
|
||
|
.Document("Quests");
|
||
|
|
||
|
DocumentSnapshot snapshot = await questsDocRef.GetSnapshotAsync();
|
||
|
if (snapshot.Exists)
|
||
|
{
|
||
|
Dictionary<string, object> questsData = snapshot.ToDictionary();
|
||
|
foreach (var entry in questsData)
|
||
|
{
|
||
|
if (entry.Key.StartsWith("Complete"))
|
||
|
{
|
||
|
string[] parts = entry.Key.Split(' ');
|
||
|
if (parts.Length == 2 && int.TryParse(parts[1], out int questId))
|
||
|
{
|
||
|
PlayerSave.SaveQuestCompletion(questId);
|
||
|
}
|
||
|
}
|
||
|
else if (int.TryParse(entry.Key, out int questId))
|
||
|
{
|
||
|
int progress = Convert.ToInt32(entry.Value);
|
||
|
PlayerSave.SaveQuestProgress(questId, progress);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError("Failed to load quests: " + e.Message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Checks if daily quests need resetting based on server timestamp.
|
||
|
/// </summary>
|
||
|
private async Task ResetDailyQuestsIfNeeded(string userId)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
DocumentReference serverTimeRef = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("ServerTime")
|
||
|
.Document("CurrentTime");
|
||
|
|
||
|
await serverTimeRef.SetAsync(new Dictionary<string, object> { { "timestamp", FieldValue.ServerTimestamp } });
|
||
|
DocumentSnapshot serverTimeSnapshot = await serverTimeRef.GetSnapshotAsync();
|
||
|
Timestamp serverTimestamp = serverTimeSnapshot.GetValue<Timestamp>("timestamp");
|
||
|
|
||
|
DateTime serverDateTime = serverTimestamp.ToDateTime();
|
||
|
DateTime resetTimeToday = serverDateTime.Date.AddHours(resetHour);
|
||
|
|
||
|
await ResetDailyQuestProgressInFirebase(userId, serverDateTime, resetTimeToday);
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError("Failed to reset daily quests: " + e.Message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private async Task ResetDailyQuestProgressInFirebase(string userId, DateTime serverDateTime, DateTime resetTimeToday)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
DocumentReference questsDocRef = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("Progress")
|
||
|
.Document("Quests");
|
||
|
|
||
|
DocumentSnapshot snapshot = await questsDocRef.GetSnapshotAsync();
|
||
|
if (snapshot.Exists)
|
||
|
{
|
||
|
Dictionary<string, object> questsData = snapshot.ToDictionary();
|
||
|
foreach (var entry in questsData)
|
||
|
{
|
||
|
if (entry.Key.StartsWith("Complete "))
|
||
|
{
|
||
|
string[] parts = entry.Key.Split(' ');
|
||
|
if (parts.Length == 2 && int.TryParse(parts[1], out int questId))
|
||
|
{
|
||
|
QuestItem questItem = GameInstance.Singleton.questData.FirstOrDefault(q => q.questId == questId);
|
||
|
if (questItem != null && questItem.questType == QuestType.Daily)
|
||
|
{
|
||
|
string completionTimeKey = $"Complete {questId}_Timestamp";
|
||
|
if (questsData.TryGetValue(completionTimeKey, out object timestampValue) && timestampValue is Timestamp completionTimestamp)
|
||
|
{
|
||
|
DateTime completionTime = completionTimestamp.ToDateTime();
|
||
|
if (completionTime < resetTimeToday)
|
||
|
{
|
||
|
await questsDocRef.UpdateAsync(new Dictionary<string, object> { { questId.ToString(), 0 } });
|
||
|
await questsDocRef.UpdateAsync(new Dictionary<string, object> { { "Complete " + questId, FieldValue.Delete } });
|
||
|
await questsDocRef.UpdateAsync(new Dictionary<string, object> { { "Complete " + questId + "_Timestamp", FieldValue.Delete } });
|
||
|
|
||
|
PlayerPrefs.DeleteKey(completionTimeKey);
|
||
|
Debug.Log($"Quest {questId} has been reset.");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError("Failed to reset daily quest progress: " + e.Message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads both Daily Rewards and New Player Rewards data from Firebase,
|
||
|
/// retrieves the server time, and saves it locally in PlayerSave.
|
||
|
/// If the documents do not exist, they are created with default values.
|
||
|
/// </summary>
|
||
|
public async Task LoadRewardsDataAsync()
|
||
|
{
|
||
|
FirebaseUser user = auth.CurrentUser;
|
||
|
if (user == null)
|
||
|
{
|
||
|
Debug.LogError("Cannot load rewards data. No authenticated user found.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
DateTime serverDateTime = await FetchServerTimeAsync(user.UserId);
|
||
|
if (serverDateTime == DateTime.MinValue)
|
||
|
{
|
||
|
serverDateTime = DateTime.Now;
|
||
|
}
|
||
|
|
||
|
DailyRewardsData dailyData = await LoadDailyRewardsFromFirebase(user.UserId, serverDateTime);
|
||
|
NewPlayerRewardsData newPlayerData = await LoadNewPlayerRewardsFromFirebase(user.UserId);
|
||
|
|
||
|
PlayerSave.SetDailyRewardsLocal(dailyData);
|
||
|
PlayerSave.SetNewPlayerRewardsLocal(newPlayerData);
|
||
|
|
||
|
DateTime resetTimeToday = serverDateTime.Date.AddHours(resetHour);
|
||
|
DateTime nextReset = (serverDateTime >= resetTimeToday)
|
||
|
? resetTimeToday.AddDays(1)
|
||
|
: resetTimeToday;
|
||
|
|
||
|
PlayerSave.SetNextDailyReset(nextReset);
|
||
|
Debug.Log($"Daily & New Player rewards loaded. Next reset: {nextReset}");
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Fetches the current server time from Firestore and returns it as a DateTime.
|
||
|
/// </summary>
|
||
|
private async Task<DateTime> FetchServerTimeAsync(string userId)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
DocumentReference serverTimeRef = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("ServerTime")
|
||
|
.Document("CurrentTime");
|
||
|
|
||
|
await serverTimeRef.SetAsync(new Dictionary<string, object>
|
||
|
{
|
||
|
{ "timestamp", FieldValue.ServerTimestamp }
|
||
|
});
|
||
|
|
||
|
DocumentSnapshot snapshot = await serverTimeRef.GetSnapshotAsync();
|
||
|
Timestamp serverTimestamp = snapshot.GetValue<Timestamp>("timestamp");
|
||
|
return serverTimestamp.ToDateTime();
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError($"Failed to fetch server time: {e.Message}");
|
||
|
return DateTime.MinValue;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads the daily reward data from Firebase and returns a structured object.
|
||
|
/// If the document does not exist, creates a new one with the current server date.
|
||
|
/// </summary>
|
||
|
private async Task<DailyRewardsData> LoadDailyRewardsFromFirebase(string userId, DateTime serverDateTime)
|
||
|
{
|
||
|
DailyRewardsData result = new DailyRewardsData();
|
||
|
|
||
|
try
|
||
|
{
|
||
|
CollectionReference dailyRewardsCollection = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("DailyRewards");
|
||
|
|
||
|
DocumentReference docRef = dailyRewardsCollection.Document("RewardData");
|
||
|
DocumentSnapshot snapshot = await docRef.GetSnapshotAsync();
|
||
|
|
||
|
if (snapshot.Exists)
|
||
|
{
|
||
|
Dictionary<string, object> data = snapshot.ToDictionary();
|
||
|
|
||
|
if (data.ContainsKey("firstClaimDate"))
|
||
|
{
|
||
|
Timestamp ts = (Timestamp)data["firstClaimDate"];
|
||
|
result.firstClaimDate = ts.ToDateTime();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
result.firstClaimDate = serverDateTime.Date;
|
||
|
}
|
||
|
|
||
|
if (data.ContainsKey("claimedRewards"))
|
||
|
{
|
||
|
result.claimedRewards = new List<int>();
|
||
|
foreach (var obj in (List<object>)data["claimedRewards"])
|
||
|
{
|
||
|
result.claimedRewards.Add(Convert.ToInt32(obj));
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
result.claimedRewards = new List<int>();
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
result.firstClaimDate = serverDateTime.Date;
|
||
|
result.claimedRewards = new List<int>();
|
||
|
|
||
|
Dictionary<string, object> defaultData = new Dictionary<string, object>
|
||
|
{
|
||
|
{ "firstClaimDate", Timestamp.FromDateTime(serverDateTime.Date.ToUniversalTime()) },
|
||
|
{ "claimedRewards", new List<int>() }
|
||
|
};
|
||
|
|
||
|
await docRef.SetAsync(defaultData);
|
||
|
Debug.Log("Created new DailyRewards document with default data.");
|
||
|
}
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError($"Error loading daily rewards: {e.Message}");
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads the new player reward data from Firebase and returns a structured object.
|
||
|
/// </summary>
|
||
|
private async Task<NewPlayerRewardsData> LoadNewPlayerRewardsFromFirebase(string userId)
|
||
|
{
|
||
|
NewPlayerRewardsData result = new NewPlayerRewardsData();
|
||
|
|
||
|
try
|
||
|
{
|
||
|
DocumentReference docRef = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("NewPlayerRewards")
|
||
|
.Document("RewardData");
|
||
|
|
||
|
DocumentSnapshot snapshot = await docRef.GetSnapshotAsync();
|
||
|
if (snapshot.Exists)
|
||
|
{
|
||
|
Dictionary<string, object> data = snapshot.ToDictionary();
|
||
|
|
||
|
if (data.ContainsKey("accountCreationDate"))
|
||
|
{
|
||
|
Timestamp ts = (Timestamp)data["accountCreationDate"];
|
||
|
result.accountCreationDate = ts.ToDateTime();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
result.accountCreationDate = DateTime.Now.Date;
|
||
|
}
|
||
|
|
||
|
if (data.ContainsKey("claimedRewards"))
|
||
|
{
|
||
|
result.claimedRewards = new List<int>();
|
||
|
foreach (var obj in (List<object>)data["claimedRewards"])
|
||
|
{
|
||
|
result.claimedRewards.Add(Convert.ToInt32(obj));
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
result.claimedRewards = new List<int>();
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
result.accountCreationDate = DateTime.Now.Date;
|
||
|
result.claimedRewards = new List<int>();
|
||
|
}
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError($"Error loading new player rewards: {e.Message}");
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads the player's used coupons.
|
||
|
/// </summary>
|
||
|
private async Task LoadUsedCouponsAsync(string userId)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
DocumentReference couponsDocRef = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("Progress")
|
||
|
.Document("UsedCoupons");
|
||
|
|
||
|
DocumentSnapshot snapshot = await couponsDocRef.GetSnapshotAsync();
|
||
|
if (snapshot.Exists)
|
||
|
{
|
||
|
Dictionary<string, object> couponsData = snapshot.ToDictionary();
|
||
|
List<string> usedCoupons = new List<string>();
|
||
|
|
||
|
foreach (var entry in couponsData)
|
||
|
{
|
||
|
usedCoupons.Add(entry.Key);
|
||
|
}
|
||
|
PlayerSave.SaveUsedCoupons(usedCoupons);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
PlayerSave.SaveUsedCoupons(new List<string>());
|
||
|
}
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError($"Failed to load used coupons: {e.Message}");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Retrieves the top 30 players with the highest scores.
|
||
|
/// </summary>
|
||
|
/// <returns>A list of dictionaries containing player data.</returns>
|
||
|
public async Task<List<Dictionary<string, object>>> GetTopPlayersAsync()
|
||
|
{
|
||
|
List<Dictionary<string, object>> topPlayers = new List<Dictionary<string, object>>();
|
||
|
|
||
|
try
|
||
|
{
|
||
|
|
||
|
CollectionReference playersRef = firestore.Collection("Players");
|
||
|
|
||
|
Query query = playersRef.OrderByDescending("score").Limit(30);
|
||
|
QuerySnapshot querySnapshot = await query.GetSnapshotAsync();
|
||
|
|
||
|
foreach (DocumentSnapshot document in querySnapshot.Documents)
|
||
|
{
|
||
|
topPlayers.Add(document.ToDictionary());
|
||
|
}
|
||
|
|
||
|
Debug.Log("Top players retrieved successfully.");
|
||
|
}
|
||
|
catch (System.Exception e)
|
||
|
{
|
||
|
Debug.LogError("Error retrieving top players: " + e.Message);
|
||
|
}
|
||
|
|
||
|
return topPlayers;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads the player's favorite character from the main document, parallel-ready.
|
||
|
/// </summary>
|
||
|
private async Task LoadPlayerCharacterFavouriteAsync(string userId)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
DocumentReference docRef = firestore.Collection("Players").Document(userId);
|
||
|
DocumentSnapshot snapshot = await docRef.GetSnapshotAsync();
|
||
|
|
||
|
if (snapshot.Exists && snapshot.ContainsField("PlayerCharacterFavourite"))
|
||
|
{
|
||
|
int favouriteCharacter = snapshot.GetValue<int>("PlayerCharacterFavourite");
|
||
|
PlayerSave.SetFavouriteCharacter(favouriteCharacter);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
await docRef.UpdateAsync("PlayerCharacterFavourite", 0);
|
||
|
PlayerSave.SetFavouriteCharacter(0);
|
||
|
}
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError($"Failed to load favourite character: {e.Message}");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Retrieves the player's current rank based on their score.
|
||
|
/// </summary>
|
||
|
/// <returns>The player's rank as an integer.</returns>
|
||
|
public async Task<int> GetPlayerRankAsync()
|
||
|
{
|
||
|
FirebaseUser user = auth.CurrentUser;
|
||
|
if (user == null)
|
||
|
{
|
||
|
Debug.LogError("No user is logged in.");
|
||
|
return -1; // Return an invalid rank if no user is logged in
|
||
|
}
|
||
|
|
||
|
try
|
||
|
{
|
||
|
// Retrieve all players ordered by score
|
||
|
QuerySnapshot allPlayersSnapshot = await firestore.Collection("Players")
|
||
|
.OrderByDescending("score")
|
||
|
.GetSnapshotAsync();
|
||
|
|
||
|
int rank = 1;
|
||
|
|
||
|
// Iterate through the players and find the rank of the current player
|
||
|
foreach (DocumentSnapshot document in allPlayersSnapshot.Documents)
|
||
|
{
|
||
|
if (document.Id == user.UserId)
|
||
|
{
|
||
|
return rank;
|
||
|
}
|
||
|
rank++;
|
||
|
}
|
||
|
|
||
|
// If the player's ID was not found
|
||
|
return -1;
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError("Failed to get player rank: " + e.Message);
|
||
|
return -1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads the Battle Pass progress from Firebase and checks if the player's season is up-to-date.
|
||
|
/// If the player's season data is missing, it will save the current season from the global settings.
|
||
|
/// </summary>
|
||
|
public async Task LoadBattlePassProgressAsync()
|
||
|
{
|
||
|
FirebaseUser user = auth.CurrentUser;
|
||
|
if (user == null)
|
||
|
{
|
||
|
Debug.LogError("No user is logged in.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
try
|
||
|
{
|
||
|
// Reference to the Battle Pass progress and season documents
|
||
|
DocumentReference battlePassDocRef = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(user.UserId)
|
||
|
.Collection("Progress")
|
||
|
.Document("BattlePass");
|
||
|
|
||
|
DocumentReference seasonDocRef = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(user.UserId)
|
||
|
.Collection("Progress")
|
||
|
.Document("Season");
|
||
|
|
||
|
// Get snapshots of the Battle Pass progress and season data
|
||
|
DocumentSnapshot battlePassSnapshot = await battlePassDocRef.GetSnapshotAsync();
|
||
|
DocumentSnapshot seasonSnapshot = await seasonDocRef.GetSnapshotAsync();
|
||
|
|
||
|
if (battlePassSnapshot.Exists)
|
||
|
{
|
||
|
// Load Battle Pass XP, Level, and Unlock Status
|
||
|
int currentXP = battlePassSnapshot.ContainsField("CurrentXP") ? battlePassSnapshot.GetValue<int>("CurrentXP") : 0;
|
||
|
int currentLevel = battlePassSnapshot.ContainsField("CurrentLevel") ? battlePassSnapshot.GetValue<int>("CurrentLevel") : 1;
|
||
|
bool isUnlocked = battlePassSnapshot.ContainsField("IsUnlocked") && battlePassSnapshot.GetValue<bool>("IsUnlocked");
|
||
|
|
||
|
// Set the Battle Pass manager values
|
||
|
BattlePassManager.Singleton.currentXP = currentXP;
|
||
|
BattlePassManager.Singleton.currentLevel = currentLevel;
|
||
|
BattlePassManager.Singleton.xpForNextLevel = BattlePassManager.Singleton.CalculateXPForNextLevel(currentLevel);
|
||
|
|
||
|
// Season management
|
||
|
int currentSeason;
|
||
|
if (!seasonSnapshot.Exists || !seasonSnapshot.ContainsField("CurrentSeason"))
|
||
|
{
|
||
|
currentSeason = await GetCurrentSeasonAsync();
|
||
|
await seasonDocRef.SetAsync(new Dictionary<string, object> { { "CurrentSeason", currentSeason } });
|
||
|
Debug.Log($"Player's current season saved: {currentSeason}");
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
currentSeason = seasonSnapshot.GetValue<int>("CurrentSeason");
|
||
|
}
|
||
|
|
||
|
// If the Battle Pass is unlocked, store that information locally
|
||
|
if (isUnlocked)
|
||
|
{
|
||
|
PlayerPrefs.SetInt(BattlePassManager.Singleton.playerPrefsPassUnlockedKey, 1);
|
||
|
}
|
||
|
|
||
|
// Save XP and Level to PlayerPrefs
|
||
|
PlayerPrefs.SetInt(BattlePassManager.Singleton.playerPrefsXPKey, currentXP);
|
||
|
PlayerPrefs.SetInt(BattlePassManager.Singleton.playerPrefsLevelKey, currentLevel);
|
||
|
|
||
|
// Set the current season in the BattlePassManager
|
||
|
BattlePassManager.SetPlayerBattlePassSeason(currentSeason);
|
||
|
|
||
|
// Save PlayerPrefs changes
|
||
|
PlayerPrefs.Save();
|
||
|
|
||
|
Debug.Log("Battle Pass progress loaded from Firebase successfully.");
|
||
|
}
|
||
|
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError("Failed to load Battle Pass progress: " + e.Message);
|
||
|
}
|
||
|
}
|
||
|
/// <summary>
|
||
|
/// Loads claimed rewards for the player's Battle Pass.
|
||
|
/// </summary>
|
||
|
private async Task LoadClaimedRewardsFromFirebase(string userId)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
DocumentReference rewardsDocRef = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("Progress")
|
||
|
.Document("BattlePassRewards");
|
||
|
|
||
|
DocumentSnapshot snapshot = await rewardsDocRef.GetSnapshotAsync();
|
||
|
if (snapshot.Exists)
|
||
|
{
|
||
|
Dictionary<string, object> rewardData = snapshot.ToDictionary();
|
||
|
foreach (KeyValuePair<string, object> entry in rewardData)
|
||
|
{
|
||
|
string rewardId = entry.Key;
|
||
|
bool isClaimed = (bool)entry.Value;
|
||
|
if (isClaimed)
|
||
|
{
|
||
|
BattlePassManager.Singleton.MarkRewardAsClaimed(rewardId);
|
||
|
}
|
||
|
}
|
||
|
PlayerPrefs.Save();
|
||
|
}
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError($"Failed to load claimed rewards: {e.Message}");
|
||
|
}
|
||
|
}
|
||
|
/// <summary>
|
||
|
/// Resets Battle Pass progress in Firebase if you want to call it automatically.
|
||
|
/// </summary>
|
||
|
public async Task ResetBattlePassProgressAsync()
|
||
|
{
|
||
|
string userId = auth.CurrentUser.UserId;
|
||
|
try
|
||
|
{
|
||
|
DocumentReference battlePassDocRef = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("Progress")
|
||
|
.Document("BattlePass");
|
||
|
|
||
|
await battlePassDocRef.DeleteAsync();
|
||
|
|
||
|
DocumentReference battlePassRewardsDocRef = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("Progress")
|
||
|
.Document("BattlePassRewards");
|
||
|
|
||
|
await battlePassRewardsDocRef.DeleteAsync();
|
||
|
|
||
|
int currentSeason = await GetCurrentSeasonAsync();
|
||
|
DocumentReference seasonDocRef = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("Progress")
|
||
|
.Document("Season");
|
||
|
|
||
|
await seasonDocRef.SetAsync(new Dictionary<string, object> { { "CurrentSeason", currentSeason } });
|
||
|
BattlePassManager.SetPlayerBattlePassSeason(currentSeason);
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError($"Failed to reset Battle Pass: {e.Message}");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/// <summary>
|
||
|
/// Retrieves the current Battle Pass season from the Firestore database.
|
||
|
/// If the season document or field is missing, it returns a default value of 1.
|
||
|
/// </summary>
|
||
|
/// <returns>The current Battle Pass season as an integer.</returns>
|
||
|
public async Task<int> GetCurrentSeasonAsync()
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
// Reference to the document that contains the current season information
|
||
|
DocumentReference seasonDocRef = firestore.Collection("BattlePass").Document("SeasonInfo");
|
||
|
DocumentSnapshot snapshot = await seasonDocRef.GetSnapshotAsync();
|
||
|
|
||
|
if (snapshot.Exists)
|
||
|
{
|
||
|
// Try to retrieve the value of the 'Season' field
|
||
|
if (snapshot.TryGetValue<int>("Season", out int currentSeason))
|
||
|
{
|
||
|
Debug.Log($"Current Season from Firestore: {currentSeason}");
|
||
|
return currentSeason;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Debug.LogWarning("Season field is missing in Firestore document.");
|
||
|
return 1; // Default value if the 'Season' field is missing
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Debug.LogWarning("Season document does not exist in Firestore.");
|
||
|
return 1; // Default value if the document does not exist
|
||
|
}
|
||
|
}
|
||
|
catch (System.Exception e)
|
||
|
{
|
||
|
Debug.LogError($"Failed to get current season: {e.Message}");
|
||
|
return 1; // Default value in case of an error
|
||
|
}
|
||
|
}
|
||
|
/// <summary>
|
||
|
/// Checks if the season has ended (parallel call if you want).
|
||
|
/// </summary>
|
||
|
private async Task<bool> CheckForSeasonEndAsync(string userId)
|
||
|
{
|
||
|
// You can remove user checks, as we did them in the main method
|
||
|
try
|
||
|
{
|
||
|
DocumentReference seasonDocRef = firestore
|
||
|
.Collection("BattlePass")
|
||
|
.Document("SeasonInfo");
|
||
|
|
||
|
DocumentSnapshot snapshot = await seasonDocRef.GetSnapshotAsync();
|
||
|
if (snapshot.Exists && snapshot.ContainsField("StartSeason"))
|
||
|
{
|
||
|
Timestamp startSeasonTimestamp = snapshot.GetValue<Timestamp>("StartSeason");
|
||
|
DateTime startSeasonDate = startSeasonTimestamp.ToDateTime();
|
||
|
int seasonLengthInDays = BattlePassManager.Singleton.SeasonLengthInDays;
|
||
|
DateTime seasonEndDate = startSeasonDate.AddDays(seasonLengthInDays);
|
||
|
|
||
|
BattlePassManager.Singleton.SaveRemainingSeasonTimeLocally(startSeasonDate);
|
||
|
|
||
|
if (DateTime.UtcNow >= seasonEndDate)
|
||
|
{
|
||
|
Debug.Log("The current season has ended.");
|
||
|
BattlePassManager.SetPlayerBattlePassSeasonEnded();
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError($"Failed to check for season end: {e.Message}");
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads basic character info (skin, level, experience, mastery) from Firestore
|
||
|
/// and applies to local PlayerSave.
|
||
|
/// </summary>
|
||
|
/// <param name="characterId">ID of the character.</param>
|
||
|
public async Task LoadCharacterBasicInfoAsync(int characterId)
|
||
|
{
|
||
|
if (auth.CurrentUser == null)
|
||
|
{
|
||
|
Debug.LogError("No user is logged in.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
try
|
||
|
{
|
||
|
string userId = auth.CurrentUser.UserId;
|
||
|
DocumentReference charDocRef = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("Characters")
|
||
|
.Document(characterId.ToString());
|
||
|
|
||
|
DocumentSnapshot snapshot = await charDocRef.GetSnapshotAsync();
|
||
|
if (!snapshot.Exists)
|
||
|
{
|
||
|
Debug.LogWarning($"No character info found in Firestore for CharacterId {characterId}.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Apply to local PlayerSave if fields exist
|
||
|
if (snapshot.ContainsField("CharacterSelectedSkin"))
|
||
|
{
|
||
|
int skinIndex = snapshot.GetValue<int>("CharacterSelectedSkin");
|
||
|
PlayerSave.SetCharacterSkin(characterId, skinIndex);
|
||
|
}
|
||
|
if (snapshot.ContainsField("CharacterLevel"))
|
||
|
{
|
||
|
int level = snapshot.GetValue<int>("CharacterLevel");
|
||
|
PlayerSave.SetCharacterLevel(characterId, level);
|
||
|
}
|
||
|
if (snapshot.ContainsField("CharacterCurrentExp"))
|
||
|
{
|
||
|
int currentExp = snapshot.GetValue<int>("CharacterCurrentExp");
|
||
|
PlayerSave.SetCharacterCurrentExp(characterId, currentExp);
|
||
|
}
|
||
|
if (snapshot.ContainsField("CharacterMasteryLevel"))
|
||
|
{
|
||
|
int masteryLevel = snapshot.GetValue<int>("CharacterMasteryLevel");
|
||
|
PlayerSave.SetCharacterMasteryLevel(characterId, masteryLevel);
|
||
|
}
|
||
|
if (snapshot.ContainsField("CharacterCurrentMasteryExp"))
|
||
|
{
|
||
|
int masteryExp = snapshot.GetValue<int>("CharacterCurrentMasteryExp");
|
||
|
PlayerSave.SetCharacterCurrentMasteryExp(characterId, masteryExp);
|
||
|
}
|
||
|
|
||
|
Debug.Log($"Character basic info loaded for CharacterId {characterId}.");
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError($"Failed to load character basic info for {characterId}: {e.Message}");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public async Task LoadAndSyncAllCharacterBasicInfoAsync()
|
||
|
{
|
||
|
if (auth.CurrentUser == null)
|
||
|
{
|
||
|
Debug.LogError("No user is logged in.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
try
|
||
|
{
|
||
|
List<Task> tasks = new List<Task>();
|
||
|
foreach (CharacterData character in GameInstance.Singleton.characterData)
|
||
|
{
|
||
|
if (character.CheckUnlocked || MonetizationManager.Singleton.IsCharacterPurchased(character.characterId.ToString()))
|
||
|
{
|
||
|
tasks.Add(LoadCharacterBasicInfoAsync(character.characterId));
|
||
|
}
|
||
|
}
|
||
|
await Task.WhenAll(tasks);
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError("Failed to load all character info: " + e.Message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads the item documents for each known slot of each character in GameInstance.Singleton.characterData.
|
||
|
/// For each 'slotName' in charData.itemSlots and charData.runeSlots,
|
||
|
/// fetches the subcollection path: /Players/{userId}/Characters/{charId}/{slotName}.
|
||
|
/// Then loads all docs found (usually only 1 doc, if you equip 1 item por slot).
|
||
|
/// </summary>
|
||
|
public async Task LoadCharacterSlotsAsync()
|
||
|
{
|
||
|
if (auth.CurrentUser == null)
|
||
|
{
|
||
|
Debug.LogError("No user is logged in.");
|
||
|
return;
|
||
|
}
|
||
|
if (GameInstance.Singleton == null || GameInstance.Singleton.characterData == null)
|
||
|
{
|
||
|
Debug.LogError("GameInstance or characterData is null. Cannot load character slots.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
string userId = auth.CurrentUser.UserId;
|
||
|
|
||
|
try
|
||
|
{
|
||
|
foreach (var charData in GameInstance.Singleton.characterData)
|
||
|
{
|
||
|
int charId = charData.characterId;
|
||
|
if (charData.itemSlots != null)
|
||
|
{
|
||
|
foreach (string slotName in charData.itemSlots)
|
||
|
{
|
||
|
await LoadSlotDocsForCharacter(userId, charId, slotName);
|
||
|
}
|
||
|
}
|
||
|
if (charData.runeSlots != null)
|
||
|
{
|
||
|
foreach (string slotName in charData.runeSlots)
|
||
|
{
|
||
|
await LoadSlotDocsForCharacter(userId, charId, slotName);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Debug.Log($"[LoadCharacterSlotsAsync] Finished loading slots for Character {charId}.");
|
||
|
}
|
||
|
|
||
|
Debug.Log("[LoadCharacterSlotsAsync] Finished loading slots for ALL characters.");
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError($"[LoadCharacterSlotsAsync] Failed: {e.Message}");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads *all* documents in the slot subcollection of a single character.
|
||
|
/// Path: /Players/{userId}/Characters/{charId}/{slotName}
|
||
|
/// Each doc is typically a single item (uniqueItemGuid).
|
||
|
/// </summary>
|
||
|
private async Task LoadSlotDocsForCharacter(string userId, int charId, string slotName)
|
||
|
{
|
||
|
CollectionReference slotCol = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("Characters")
|
||
|
.Document(charId.ToString())
|
||
|
.Collection(slotName);
|
||
|
|
||
|
QuerySnapshot snapshot = await slotCol.GetSnapshotAsync();
|
||
|
foreach (DocumentSnapshot doc in snapshot.Documents)
|
||
|
{
|
||
|
if (!doc.Exists)
|
||
|
continue;
|
||
|
|
||
|
string uniqueGuid = doc.Id;
|
||
|
string itemId = doc.ContainsField("ItemId") ? doc.GetValue<string>("ItemId") : "";
|
||
|
int itemLevel = doc.ContainsField("ItemLevel") ? doc.GetValue<int>("ItemLevel") : 1;
|
||
|
|
||
|
PlayerSave.SetCharacterSlotItem(charId, slotName, uniqueGuid);
|
||
|
|
||
|
PlayerPrefs.SetInt($"{charId}_{slotName}_level", itemLevel);
|
||
|
|
||
|
Debug.Log($"[LoadSlotDocsForCharacter] Loaded item '{itemId}' (GUID={uniqueGuid}) at slot '{slotName}', level={itemLevel}, for Char {charId}.");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads all purchased items from Firestore into local MonetizationManager,
|
||
|
/// then checks each InventoryItem in GameInstance. If 'isUnlocked' is true and
|
||
|
/// no item with the same itemId exists, purchases it to create a new GUID.
|
||
|
/// </summary>
|
||
|
public async Task InitializeInventoryAsync()
|
||
|
{
|
||
|
if (auth.CurrentUser == null)
|
||
|
{
|
||
|
Debug.LogError("No user is logged in.");
|
||
|
return;
|
||
|
}
|
||
|
List<PurchasedInventoryItem> remotePurchasedItems = await LoadAllPurchasedInventoryItemsAsync();
|
||
|
var existingCharacters = MonetizationManager.Singleton.GetPurchasedCharacters();
|
||
|
var existingIcons = MonetizationManager.Singleton.GetPurchasedIcons();
|
||
|
var existingFrames = MonetizationManager.Singleton.GetPurchasedFrames();
|
||
|
var existingShopItems = MonetizationManager.Singleton.GetPurchasedShopItems();
|
||
|
|
||
|
MonetizationManager.Singleton.UpdatePurchasedItems(
|
||
|
existingCharacters,
|
||
|
existingIcons,
|
||
|
existingFrames,
|
||
|
existingShopItems,
|
||
|
remotePurchasedItems
|
||
|
);
|
||
|
bool needSaveItem = false;
|
||
|
foreach (var soItem in GameInstance.Singleton.inventoryItems)
|
||
|
{
|
||
|
if (soItem.isUnlocked)
|
||
|
{
|
||
|
bool alreadyOwned = MonetizationManager.Singleton
|
||
|
.GetPurchasedInventoryItems()
|
||
|
.Exists(pi => pi.itemId == soItem.itemId);
|
||
|
|
||
|
if (!alreadyOwned)
|
||
|
{
|
||
|
MonetizationManager.Singleton.PurchaseInventoryItemNoImmediateSave(soItem.itemId);
|
||
|
Debug.Log($"Created new purchased item for {soItem.itemId} (isUnlocked).");
|
||
|
needSaveItem = true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
MonetizationManager.Singleton.SavePurchasedItems(needSaveItem);
|
||
|
}
|
||
|
|
||
|
|
||
|
/// <summary>
|
||
|
/// Loads all purchased inventory items from Firestore into a List, ensuring that the parent document exists.
|
||
|
/// </summary>
|
||
|
public async Task<List<PurchasedInventoryItem>> LoadAllPurchasedInventoryItemsAsync()
|
||
|
{
|
||
|
List<PurchasedInventoryItem> result = new List<PurchasedInventoryItem>();
|
||
|
|
||
|
try
|
||
|
{
|
||
|
string userId = auth.CurrentUser.UserId;
|
||
|
// Reference to the "Items" document under PurchasedItems
|
||
|
DocumentReference itemsDocRef = firestore
|
||
|
.Collection("Players")
|
||
|
.Document(userId)
|
||
|
.Collection("PurchasedItems")
|
||
|
.Document("Items");
|
||
|
|
||
|
// Check if the "Items" document exists; if not, create it.
|
||
|
DocumentSnapshot itemsDocSnapshot = await itemsDocRef.GetSnapshotAsync();
|
||
|
if (!itemsDocSnapshot.Exists)
|
||
|
{
|
||
|
await itemsDocRef.SetAsync(new Dictionary<string, object>());
|
||
|
Debug.Log("Created the 'Items' document as it did not exist.");
|
||
|
}
|
||
|
|
||
|
// Get the collection reference for the "List" subcollection
|
||
|
CollectionReference itemsColRef = itemsDocRef.Collection("List");
|
||
|
|
||
|
QuerySnapshot snapshot = await itemsColRef.GetSnapshotAsync();
|
||
|
foreach (DocumentSnapshot doc in snapshot.Documents)
|
||
|
{
|
||
|
if (!doc.Exists)
|
||
|
continue;
|
||
|
|
||
|
string uniqueGuid = doc.Id;
|
||
|
string itemId = doc.ContainsField("itemId") ? doc.GetValue<string>("itemId") : "";
|
||
|
int itemLevel = doc.ContainsField("itemLevel") ? doc.GetValue<int>("itemLevel") : 0;
|
||
|
|
||
|
Dictionary<int, int> upgrades = new Dictionary<int, int>();
|
||
|
if (doc.ContainsField("itemUpgrades"))
|
||
|
{
|
||
|
Dictionary<string, object> upgradesData = doc.GetValue<Dictionary<string, object>>("itemUpgrades");
|
||
|
foreach (var kvp in upgradesData)
|
||
|
{
|
||
|
if (int.TryParse(kvp.Key, out int upgradeIndex))
|
||
|
{
|
||
|
int upgradeValue = Convert.ToInt32(kvp.Value);
|
||
|
upgrades[upgradeIndex] = upgradeValue;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
PurchasedInventoryItem pi = new PurchasedInventoryItem
|
||
|
{
|
||
|
uniqueItemGuid = uniqueGuid,
|
||
|
itemId = itemId,
|
||
|
itemLevel = itemLevel,
|
||
|
upgrades = upgrades
|
||
|
};
|
||
|
result.Add(pi);
|
||
|
}
|
||
|
|
||
|
Debug.Log($"Loaded {result.Count} purchased inventory items from Firestore.");
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Debug.LogError($"Failed to load purchased inventory items: {e.Message}");
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
|
||
|
/// <summary>
|
||
|
/// Converts a list of generic objects to a list of a specific type using JsonUtility.
|
||
|
/// </summary>
|
||
|
private List<T> ConvertToList<T>(List<object> data)
|
||
|
{
|
||
|
var list = new List<T>();
|
||
|
if (data == null) return list;
|
||
|
|
||
|
foreach (var item in data)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
list.Add(JsonUtility.FromJson<T>(item.ToString()));
|
||
|
}
|
||
|
catch (Exception ex)
|
||
|
{
|
||
|
Debug.LogError($"Error converting item: {ex.Message}");
|
||
|
}
|
||
|
}
|
||
|
return list;
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
}
|
||
|
}
|