using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif public sealed class AllyHeroDeployLedger : MonoBehaviour { public static AllyHeroDeployLedger Instance { get; private set; } public event System.Action OnHeroGrowthChanged; private readonly Dictionary currentExpByHeroId = new Dictionary(); private readonly Dictionary unlockedTierByHeroId = new Dictionary(); private readonly Dictionary autoBreakthroughEnabledByHeroId = new Dictionary(); private readonly Dictionary deployCountsByHeroId = new Dictionary(); private readonly Dictionary finishCountsByHeroId = new Dictionary(); private readonly Dictionary mvpCountsByHeroId = new Dictionary(); private readonly Dictionary joinDateUtcTicksByHeroId = new Dictionary(); private bool initialized; private bool loadedFromSave; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void Bootstrap() { EnsureInstance(); } public static AllyHeroDeployLedger EnsureInstance() { if (Instance != null) { Instance.ResetToDefaultsIfSaveMissing(); return Instance; } GameObject host = new GameObject("__runtime_ally_deploy_bridge"); host.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector; DontDestroyOnLoad(host); Instance = host.AddComponent(); return Instance; } private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); InitializeIfNeeded(); ResetToDefaultsIfSaveMissing(); } private void OnApplicationPause(bool pauseStatus) { if (pauseStatus) { SaveNow(); } } private void OnApplicationQuit() { SaveNow(); } public void InitializeIfNeeded() { if (initialized) { return; } AllyHeroDeployLedgerPayload payload; loadedFromSave = AllyHeroDeployLedgerStorage.TryLoad(out payload); RebuildFromPayload(payload); initialized = true; if (!loadedFromSave) { ResetGrowthStateToDefaults(); } SyncAllMirrorFlags(); SaveNow(); } public void ResetToDefaultsIfSaveMissing() { if (!initialized) { return; } if (AllyHeroDeployLedgerStorage.HasExistingSaveFile()) { return; } ResetGrowthStateToDefaults(); SyncAllMirrorFlags(); SaveNow(); } public int GetDeployCount(int heroId) { InitializeIfNeeded(); int count; return deployCountsByHeroId.TryGetValue(heroId, out count) ? count : 0; } public int GetFinishCount(int heroId) { InitializeIfNeeded(); int count; return finishCountsByHeroId.TryGetValue(heroId, out count) ? count : 0; } public int GetMvpCount(int heroId) { InitializeIfNeeded(); int count; return mvpCountsByHeroId.TryGetValue(heroId, out count) ? count : 0; } public long GetJoinDateUtcTicks(int heroId) { InitializeIfNeeded(); long ticks; return joinDateUtcTicksByHeroId.TryGetValue(heroId, out ticks) ? ticks : 0L; } public int GetCurrentExp(int heroId) { InitializeIfNeeded(); int count; return currentExpByHeroId.TryGetValue(heroId, out count) ? Mathf.Max(0, count) : 0; } public int GetUnlockedTierIndex(int heroId) { InitializeIfNeeded(); int value; return unlockedTierByHeroId.TryGetValue(heroId, out value) ? Mathf.Clamp(value, 0, 3) : 0; } public bool IsAutoBreakthroughEnabled(int heroId) { InitializeIfNeeded(); bool value; return autoBreakthroughEnabledByHeroId.TryGetValue(heroId, out value) && value; } public void SetCurrentExp(AllyHero_SO hero, int value) { if (hero == null || hero.ally_heroID <= 0) { return; } InitializeIfNeeded(); int safeValue = Mathf.Max(0, value); currentExpByHeroId[hero.ally_heroID] = safeValue; hero.ally_currentEXP = safeValue; MarkDirty(hero); SaveNow(); if (OnHeroGrowthChanged != null) { OnHeroGrowthChanged(hero.ally_heroID); } } public void SetUnlockedTierIndex(AllyHero_SO hero, int value) { if (hero == null || hero.ally_heroID <= 0) { return; } InitializeIfNeeded(); int safeValue = Mathf.Clamp(value, 0, 3); unlockedTierByHeroId[hero.ally_heroID] = safeValue; hero.ally_growthUnlockedTierIndex = safeValue; hero.ally_currentEXP = GetCurrentExp(hero.ally_heroID); MarkDirty(hero); SaveNow(); if (OnHeroGrowthChanged != null) { OnHeroGrowthChanged(hero.ally_heroID); } } public void SetAutoBreakthroughEnabled(AllyHero_SO hero, bool value) { if (hero == null || hero.ally_heroID <= 0) { return; } InitializeIfNeeded(); autoBreakthroughEnabledByHeroId[hero.ally_heroID] = value; hero.ally_autoBreakthroughEnabled = value; MarkDirty(hero); SaveNow(); if (OnHeroGrowthChanged != null) { OnHeroGrowthChanged(hero.ally_heroID); } } public void IncrementDeployCount(AllyHero_SO hero, int amount = 1) { if (hero == null || hero.ally_heroID <= 0 || amount <= 0) { return; } InitializeIfNeeded(); int current = GetDeployCount(hero.ally_heroID); long next = (long)current + amount; deployCountsByHeroId[hero.ally_heroID] = next > int.MaxValue ? int.MaxValue : (int)next; hero.ally_battleDeployCount = deployCountsByHeroId[hero.ally_heroID]; MarkDirty(hero); SaveNow(); } public void IncrementFinishCount(AllyHero_SO hero, int amount = 1) { if (hero == null || hero.ally_heroID <= 0 || amount <= 0) { return; } InitializeIfNeeded(); int current = GetFinishCount(hero.ally_heroID); long next = (long)current + amount; finishCountsByHeroId[hero.ally_heroID] = next > int.MaxValue ? int.MaxValue : (int)next; hero.ally_finishCount = finishCountsByHeroId[hero.ally_heroID]; MarkDirty(hero); SaveNow(); } public void IncrementMvpCount(AllyHero_SO hero, int amount = 1) { if (hero == null || hero.ally_heroID <= 0 || amount <= 0) { return; } InitializeIfNeeded(); int current = GetMvpCount(hero.ally_heroID); long next = (long)current + amount; mvpCountsByHeroId[hero.ally_heroID] = next > int.MaxValue ? int.MaxValue : (int)next; hero.ally_mvpCount = mvpCountsByHeroId[hero.ally_heroID]; MarkDirty(hero); SaveNow(); } public void SetJoinDateIfMissing(AllyHero_SO hero, System.DateTime? utcTime = null) { if (hero == null || hero.ally_heroID <= 0) { return; } InitializeIfNeeded(); if (GetJoinDateUtcTicks(hero.ally_heroID) > 0L) { hero.ally_joinDateUtcTicks = GetJoinDateUtcTicks(hero.ally_heroID); return; } long ticks = (utcTime ?? System.DateTime.UtcNow).Ticks; if (ticks <= 0L) { return; } joinDateUtcTicksByHeroId[hero.ally_heroID] = ticks; hero.ally_joinDateUtcTicks = ticks; MarkDirty(hero); SaveNow(); } public void SaveNow() { if (!initialized) { return; } SyncAllMirrorFlags(); AllyHeroDeployLedgerStorage.TrySave(BuildPayload()); } public void ResetGrowthProgressOnly() { InitializeIfNeeded(); currentExpByHeroId.Clear(); unlockedTierByHeroId.Clear(); autoBreakthroughEnabledByHeroId.Clear(); ResetGrowthStateToDefaults(); ClearAllPendingDebtKeys(); SyncAllMirrorFlags(); SaveNow(); } private void RebuildFromPayload(AllyHeroDeployLedgerPayload payload) { currentExpByHeroId.Clear(); unlockedTierByHeroId.Clear(); autoBreakthroughEnabledByHeroId.Clear(); deployCountsByHeroId.Clear(); finishCountsByHeroId.Clear(); mvpCountsByHeroId.Clear(); joinDateUtcTicksByHeroId.Clear(); if (payload == null || payload.entries == null) { return; } Dictionary heroById = BuildHeroLookup(); for (int i = 0; i < payload.entries.Count; i++) { AllyHeroDeployEntry entry = payload.entries[i]; if (entry == null || entry.heroId <= 0) { continue; } int currentExp = Mathf.Max(0, entry.currentExp); int unlockedTier = Mathf.Clamp(entry.unlockedTierIndex, 0, 3); AllyHero_SO hero; if (heroById.TryGetValue(entry.heroId, out hero)) { NormalizeGrowthState(hero, ref currentExp, ref unlockedTier); } currentExpByHeroId[entry.heroId] = currentExp; unlockedTierByHeroId[entry.heroId] = unlockedTier; autoBreakthroughEnabledByHeroId[entry.heroId] = entry.autoBreakthroughEnabled; deployCountsByHeroId[entry.heroId] = Mathf.Max(0, entry.deployCount); finishCountsByHeroId[entry.heroId] = Mathf.Max(0, entry.finishCount); mvpCountsByHeroId[entry.heroId] = Mathf.Max(0, entry.mvpCount); if (entry.joinDateUtcTicks > 0L) { joinDateUtcTicksByHeroId[entry.heroId] = entry.joinDateUtcTicks; } } } private void ResetGrowthStateToDefaults() { currentExpByHeroId.Clear(); unlockedTierByHeroId.Clear(); autoBreakthroughEnabledByHeroId.Clear(); AllyHero_SO[] heroes = Resources.LoadAll(string.Empty); for (int i = 0; i < heroes.Length; i++) { AllyHero_SO hero = heroes[i]; if (hero == null || hero.ally_heroID <= 0) { continue; } currentExpByHeroId[hero.ally_heroID] = 0; unlockedTierByHeroId[hero.ally_heroID] = 0; autoBreakthroughEnabledByHeroId[hero.ally_heroID] = false; if (hero.ally_currentEXP != 0) { hero.ally_currentEXP = 0; MarkDirty(hero); } if (hero.ally_growthUnlockedTierIndex != 0) { hero.ally_growthUnlockedTierIndex = 0; MarkDirty(hero); } if (hero.ally_autoBreakthroughEnabled) { hero.ally_autoBreakthroughEnabled = false; MarkDirty(hero); } } } private void SyncAllMirrorFlags() { AllyHero_SO[] heroes = Resources.LoadAll(string.Empty); for (int i = 0; i < heroes.Length; i++) { AllyHero_SO hero = heroes[i]; if (hero == null || hero.ally_heroID <= 0) { continue; } int count; if (!deployCountsByHeroId.TryGetValue(hero.ally_heroID, out count)) { count = 0; } int currentExp; if (!currentExpByHeroId.TryGetValue(hero.ally_heroID, out currentExp)) { currentExp = 0; currentExpByHeroId[hero.ally_heroID] = currentExp; } int unlockedTier; if (!unlockedTierByHeroId.TryGetValue(hero.ally_heroID, out unlockedTier)) { unlockedTier = 0; unlockedTierByHeroId[hero.ally_heroID] = unlockedTier; } bool autoBreakthroughEnabled; if (!autoBreakthroughEnabledByHeroId.TryGetValue(hero.ally_heroID, out autoBreakthroughEnabled)) { autoBreakthroughEnabled = false; autoBreakthroughEnabledByHeroId[hero.ally_heroID] = autoBreakthroughEnabled; } NormalizeGrowthState(hero, ref currentExp, ref unlockedTier); currentExpByHeroId[hero.ally_heroID] = currentExp; unlockedTierByHeroId[hero.ally_heroID] = unlockedTier; int finishCount; if (!finishCountsByHeroId.TryGetValue(hero.ally_heroID, out finishCount)) { finishCount = 0; } int mvpCount; if (!mvpCountsByHeroId.TryGetValue(hero.ally_heroID, out mvpCount)) { mvpCount = 0; } long joinTicks; if (!joinDateUtcTicksByHeroId.TryGetValue(hero.ally_heroID, out joinTicks)) { joinTicks = 0L; } bool changed = false; if (hero.ally_currentEXP != currentExp) { hero.ally_currentEXP = currentExp; changed = true; } if (hero.ally_growthUnlockedTierIndex != unlockedTier) { hero.ally_growthUnlockedTierIndex = unlockedTier; changed = true; } if (hero.ally_autoBreakthroughEnabled != autoBreakthroughEnabled) { hero.ally_autoBreakthroughEnabled = autoBreakthroughEnabled; changed = true; } if (hero.ally_battleDeployCount != count) { hero.ally_battleDeployCount = count; changed = true; } if (hero.ally_finishCount != finishCount) { hero.ally_finishCount = finishCount; changed = true; } if (hero.ally_mvpCount != mvpCount) { hero.ally_mvpCount = mvpCount; changed = true; } if (hero.ally_joinDateUtcTicks != joinTicks) { hero.ally_joinDateUtcTicks = joinTicks; changed = true; } if (changed) { MarkDirty(hero); } } } private static Dictionary BuildHeroLookup() { Dictionary heroById = new Dictionary(); AllyHero_SO[] heroes = Resources.LoadAll(string.Empty); for (int i = 0; i < heroes.Length; i++) { AllyHero_SO hero = heroes[i]; if (hero == null || hero.ally_heroID <= 0 || heroById.ContainsKey(hero.ally_heroID)) { continue; } heroById.Add(hero.ally_heroID, hero); } return heroById; } private static void NormalizeGrowthState(AllyHero_SO hero, ref int currentExp, ref int unlockedTier) { currentExp = Mathf.Max(0, currentExp); unlockedTier = Mathf.Clamp(unlockedTier, 0, 3); if (hero == null || hero.levelStats == null || hero.levelStats.Count == 0) { if (currentExp <= 0) { unlockedTier = 0; } return; } List levels = new List(); for (int i = 0; i < hero.levelStats.Count; i++) { if (hero.levelStats[i] != null) { levels.Add(hero.levelStats[i]); } } if (levels.Count == 0) { if (currentExp <= 0) { unlockedTier = 0; } return; } levels.Sort((left, right) => left.requiredEXP.CompareTo(right.requiredEXP)); int reachableTier = 0; for (int i = 0; i < levels.Count; i++) { if (currentExp >= Mathf.Max(0, levels[i].requiredEXP)) { reachableTier = i; } } unlockedTier = Mathf.Min(unlockedTier, reachableTier); } private static void ClearAllPendingDebtKeys() { AllyHero_SO[] heroes = Resources.LoadAll(string.Empty); for (int i = 0; i < heroes.Length; i++) { AllyHero_SO hero = heroes[i]; if (hero == null || hero.ally_heroID <= 0) { continue; } PlayerPrefs.DeleteKey("idol_upgrade_pending_debt_" + hero.ally_heroID); } PlayerPrefs.Save(); } private AllyHeroDeployLedgerPayload BuildPayload() { AllyHeroDeployLedgerPayload payload = AllyHeroDeployLedgerStorage.CreateDefaultPayload(); HashSet allHeroIds = new HashSet(); foreach (KeyValuePair pair in currentExpByHeroId) { allHeroIds.Add(pair.Key); } foreach (KeyValuePair pair in unlockedTierByHeroId) { allHeroIds.Add(pair.Key); } foreach (KeyValuePair pair in autoBreakthroughEnabledByHeroId) { allHeroIds.Add(pair.Key); } foreach (KeyValuePair pair in deployCountsByHeroId) { allHeroIds.Add(pair.Key); } foreach (KeyValuePair pair in finishCountsByHeroId) { allHeroIds.Add(pair.Key); } foreach (KeyValuePair pair in mvpCountsByHeroId) { allHeroIds.Add(pair.Key); } foreach (KeyValuePair pair in joinDateUtcTicksByHeroId) { allHeroIds.Add(pair.Key); } foreach (int heroId in allHeroIds) { payload.entries.Add(new AllyHeroDeployEntry { heroId = heroId, currentExp = Mathf.Max(0, GetCurrentExp(heroId)), unlockedTierIndex = Mathf.Clamp(GetUnlockedTierIndex(heroId), 0, 3), autoBreakthroughEnabled = IsAutoBreakthroughEnabled(heroId), deployCount = Mathf.Max(0, GetDeployCount(heroId)), finishCount = Mathf.Max(0, GetFinishCount(heroId)), mvpCount = Mathf.Max(0, GetMvpCount(heroId)), joinDateUtcTicks = GetJoinDateUtcTicks(heroId) }); } return payload; } private static void MarkDirty(AllyHero_SO hero) { #if UNITY_EDITOR if (!Application.isPlaying && hero != null) { EditorUtility.SetDirty(hero); } #endif } }