using System; using UnityEngine; public sealed class PlayerEconomyLedger : MonoBehaviour { public static PlayerEconomyLedger Instance { get; private set; } public event Action OnCoinsChanged; public event Action OnMaterialChanged; private PlayerEconomyPayload payload; private bool initialized; private bool loadedFromSave; private bool hasAnyRecoverableLocalState; private Player_SO boundPlayerData; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void Bootstrap() { EnsureInstance(); } public static PlayerEconomyLedger EnsureInstance() { if (Instance != null) { return Instance; } var host = new GameObject("__runtime_wallet_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(); TryAttachDefaultPlayerData(); } private void OnApplicationPause(bool pauseStatus) { if (pauseStatus) { SaveNow(); } } private void OnApplicationQuit() { SaveNow(); } public void InitializeIfNeeded() { if (initialized) { return; } PlayerEconomyPayload loadedPayload; loadedFromSave = PlayerEconomyStorage.TryLoad(out loadedPayload); hasAnyRecoverableLocalState = loadedFromSave || PlayerEconomyStorage.HasAnyRecoverableState(); payload = loadedPayload; if (payload == null) { payload = PlayerEconomyStorage.CreateDefaultPayload(); } initialized = true; if (loadedFromSave) { SaveNow(); } } public void AttachPlayerData(Player_SO playerData) { if (playerData == null) { return; } InitializeIfNeeded(); boundPlayerData = playerData; if (!loadedFromSave && !hasAnyRecoverableLocalState) { // 首次启动且无任何可恢复存档:用硬编码默认值(0)建立初始存档, // 绝不读取 Player_SO 的烘焙值——该资产可能被打包时的测试数据污染, // 直接采用会把玩家进度“种”成 999988 之类的测试数值。 payload = PlayerEconomyStorage.CreateDefaultPayload(); SyncToPlayerData(); SaveNow(); loadedFromSave = true; hasAnyRecoverableLocalState = true; NotifyEconomyChanged(); return; } SyncToPlayerData(); GlobalAchievementService.EnsureInstance().ReportCurrentCoins(payload.coins); NotifyEconomyChanged(); } public int GetCoins() { InitializeIfNeeded(); return payload.coins; } public int GetMaterial() { InitializeIfNeeded(); return payload.material; } public bool HasEnoughCoins(long amount) { if (amount <= 0) { return true; } InitializeIfNeeded(); return payload.coins >= amount; } public bool HasEnoughMaterial(long amount) { if (amount <= 0) { return true; } InitializeIfNeeded(); return payload.material >= amount; } public void AddCoins(int amount) { if (amount <= 0) { return; } InitializeIfNeeded(); long next = (long)payload.coins + amount; payload.coins = next > int.MaxValue ? int.MaxValue : (int)next; GlobalAchievementService.EnsureInstance().ReportCoinsEarned(amount); SaveNow(); } public void AddMaterial(int amount) { if (amount <= 0) { return; } InitializeIfNeeded(); long next = (long)payload.material + amount; payload.material = next > int.MaxValue ? int.MaxValue : (int)next; SaveNow(); } public bool TrySpendCoins(int amount) { if (amount <= 0) { return true; } InitializeIfNeeded(); if (payload.coins < amount) { return false; } payload.coins -= amount; SaveNow(); DailyTaskEventHub.ReportSpendCoins(amount); int refundAmount = PlayerSkillService.GetCoinRefundAmount(amount); if (refundAmount > 0) { AddCoins(refundAmount); } return true; } public bool TrySpendMaterial(int amount) { if (amount <= 0) { return true; } InitializeIfNeeded(); if (payload.material < amount) { return false; } payload.material -= amount; SaveNow(); int refundAmount = PlayerSkillService.GetMaterialRefundAmount(amount); if (refundAmount > 0) { AddMaterial(refundAmount); } return true; } public void SetCoins(int value) { InitializeIfNeeded(); payload.coins = Mathf.Max(0, value); SaveNow(); } public void SetMaterial(int value) { InitializeIfNeeded(); payload.material = Mathf.Max(0, value); SaveNow(); } public void SaveNow() { InitializeIfNeededForSave(); if (!loadedFromSave && hasAnyRecoverableLocalState) { Debug.LogWarning("[PlayerEconomyLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older economy data."); return; } PlayerEconomyStorage.TrySave(payload); SyncToPlayerData(); GlobalAchievementService.EnsureInstance().ReportCurrentCoins(payload.coins); NotifyEconomyChanged(); } private void InitializeIfNeededForSave() { if (!initialized) { InitializeIfNeeded(); } if (payload == null) { payload = PlayerEconomyStorage.CreateDefaultPayload(); } } private void SyncToPlayerData() { if (boundPlayerData == null || payload == null) { return; } boundPlayerData.SetCoins(payload.coins); boundPlayerData.SetMaterial(payload.material); } private void NotifyEconomyChanged() { if (payload == null) { return; } if (OnCoinsChanged != null) { OnCoinsChanged(payload.coins); } if (OnMaterialChanged != null) { OnMaterialChanged(payload.material); } } private void TryAttachDefaultPlayerData() { Player_SO player = RuntimeResourcesCache.LoadDefaultPlayerSo(); if (player != null) { AttachPlayerData(player); } } }