299 lines
6.8 KiB
C#
299 lines
6.8 KiB
C#
using System;
|
|
using UnityEngine;
|
|
|
|
public sealed class PlayerEconomyLedger : MonoBehaviour
|
|
{
|
|
public static PlayerEconomyLedger Instance { get; private set; }
|
|
|
|
public event Action<int> OnCoinsChanged;
|
|
public event Action<int> 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<PlayerEconomyLedger>();
|
|
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();
|
|
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);
|
|
}
|
|
}
|
|
}
|