Исправлена бесконечная рекурсия (StackOverflowException) при синхронизации баланса с кредитами платы. Детали изменений: - В класс BalanceManager добавлен флаг-предохранитель _isSyncingCredits. - Обработчик OnImpulsePaid теперь игнорирует события начисления кредитов, вызванные синхронизацией. - В методе SyncCreditsWithBalance вызов ComSystemManager.SetCredits обернут в блок try-finally для безопасного управления флагом _isSyncingCredits.
342 lines
12 KiB
C#
342 lines
12 KiB
C#
using RR.Common.Controls;
|
||
using RR.Common.Settings;
|
||
using UnityEngine;
|
||
using UnityEngine.Events;
|
||
|
||
namespace RR.Common.Managers
|
||
{
|
||
/// <summary>
|
||
/// Менеджер игрового баланса. Управляет внутриигровой валютой,
|
||
/// которая начисляется за импульсы оплаты и списывается за начало/продолжение игры.
|
||
/// </summary>
|
||
public class BalanceManager : MonoBehaviour
|
||
{
|
||
public static BalanceManager Instance { get; private set; }
|
||
|
||
[Header("Текущий баланс")]
|
||
[SerializeField] private int currentBalance;
|
||
|
||
[Header("Настройки сохранения")]
|
||
[SerializeField] private bool saveBalanceBetweenSessions;
|
||
|
||
// События для уведомления UI и других систем
|
||
public UnityEvent<int> OnBalanceChanged;
|
||
public UnityEvent<int, int> OnBalanceAdded; // (добавлено, новый баланс)
|
||
public UnityEvent<int, int> OnBalanceSpent; // (потрачено, новый баланс)
|
||
|
||
/// <summary>
|
||
/// Текущий баланс игрока
|
||
/// </summary>
|
||
public int CurrentBalance => currentBalance;
|
||
|
||
private const string BALANCE_SAVE_KEY = "PlayerBalance";
|
||
|
||
// Кешированные значения из настроек
|
||
private int _impulseCost = 10;
|
||
private int _startGameCost = 10;
|
||
private int _continueGameCost = 5;
|
||
private bool _freeGame;
|
||
private bool _isSyncingCredits;
|
||
|
||
private void Awake()
|
||
{
|
||
if (Instance != null && Instance != this)
|
||
{
|
||
Destroy(gameObject);
|
||
return;
|
||
}
|
||
|
||
Instance = this;
|
||
DontDestroyOnLoad(gameObject);
|
||
|
||
Initialize();
|
||
}
|
||
|
||
private void Initialize()
|
||
{
|
||
// Инициализация событий
|
||
OnBalanceChanged ??= new UnityEvent<int>();
|
||
OnBalanceChanged ??= new UnityEvent<int>();
|
||
OnBalanceAdded ??= new UnityEvent<int, int>();
|
||
OnBalanceSpent ??= new UnityEvent<int, int>();
|
||
|
||
// Загрузка баланса из сохранений
|
||
if (saveBalanceBetweenSessions)
|
||
{
|
||
currentBalance = PlayerPrefs.GetInt(BALANCE_SAVE_KEY, 0);
|
||
}
|
||
else
|
||
{
|
||
currentBalance = 0;
|
||
PlayerPrefs.SetInt(BALANCE_SAVE_KEY, 0);
|
||
}
|
||
|
||
// Подписка на события настроек для кеширования значений
|
||
var settingsManager = SettingsManager.Instance;
|
||
if (settingsManager != null)
|
||
{
|
||
settingsManager.OnSettingsLoaded += CacheSettingsValues;
|
||
settingsManager.OnSettingsSaved += CacheSettingsValues;
|
||
CacheSettingsValues(); // Загрузка при инициализации
|
||
}
|
||
|
||
// Подписка на события оплаты
|
||
if (PaymentManager.Instance != null)
|
||
{
|
||
PaymentManager.Instance.OnCreditAdded.AddListener(OnImpulsePaid);
|
||
}
|
||
|
||
Debug.Log($"[BalanceManager] Инициализирован. Баланс: {currentBalance}");
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
if (PaymentManager.Instance != null)
|
||
{
|
||
PaymentManager.Instance.OnCreditAdded.RemoveListener(OnImpulsePaid);
|
||
}
|
||
|
||
var settingsManager = SettingsManager.Instance;
|
||
if (settingsManager != null)
|
||
{
|
||
settingsManager.OnSettingsLoaded -= CacheSettingsValues;
|
||
settingsManager.OnSettingsSaved -= CacheSettingsValues;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Кеширует значения из настроек
|
||
/// </summary>
|
||
private void CacheSettingsValues()
|
||
{
|
||
var settings = SettingsManager.Instance;
|
||
if (settings == null)
|
||
{
|
||
Debug.LogError("[BalanceManager] SettingsManager is null, cannot cache settings!");
|
||
return;
|
||
}
|
||
|
||
_impulseCost = settings.GetInt("payment.paymentImpulseCost");
|
||
_startGameCost = settings.GetInt("payment.startGameCost");
|
||
_continueGameCost = settings.GetInt("payment.continueGameCost");
|
||
_freeGame = settings.GetBool("payment.freeGame");
|
||
|
||
// Валидация значений
|
||
if (_impulseCost <= 0)
|
||
{
|
||
Debug.LogError($"[BalanceManager] Invalid payment.paymentImpulseCost: {_impulseCost}");
|
||
_impulseCost = 10;
|
||
}
|
||
if (_startGameCost <= 0)
|
||
{
|
||
Debug.LogError($"[BalanceManager] Invalid payment.startGameCost: {_startGameCost}");
|
||
_startGameCost = 10;
|
||
}
|
||
if (_continueGameCost <= 0)
|
||
{
|
||
Debug.LogError($"[BalanceManager] Invalid payment.continueGameCost: {_continueGameCost}");
|
||
_continueGameCost = 5;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Обработчик оплаты импульса - начисляет баланс
|
||
/// </summary>
|
||
private void OnImpulsePaid(int creditsCount)
|
||
{
|
||
if (_isSyncingCredits)
|
||
return;
|
||
|
||
// creditsCount - это количество добавленных кредитов (всегда 1 для одного импульса)
|
||
// Начисляем стоимость импульса
|
||
AddBalance(_impulseCost);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Добавляет баланс игроку
|
||
/// </summary>
|
||
/// <param name="amount">Количество добавляемого баланса</param>
|
||
public void AddBalance(int amount)
|
||
{
|
||
if (amount <= 0)
|
||
{
|
||
Debug.LogWarning($"[BalanceManager] Trying to add invalid amount: {amount}");
|
||
return;
|
||
}
|
||
|
||
currentBalance += amount;
|
||
|
||
// Сохранение если включено
|
||
if (saveBalanceBetweenSessions)
|
||
{
|
||
PlayerPrefs.SetInt(BALANCE_SAVE_KEY, currentBalance);
|
||
PlayerPrefs.Save();
|
||
}
|
||
|
||
// Синхронизация кредитов с балансом
|
||
SyncCreditsWithBalance();
|
||
|
||
// Уведомление подписчиков
|
||
OnBalanceAdded?.Invoke(amount, currentBalance);
|
||
OnBalanceChanged?.Invoke(currentBalance);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Проверяет, достаточно ли баланса для начала игры
|
||
/// </summary>
|
||
public bool CanStartGame()
|
||
{
|
||
// Проверка бесплатной игры
|
||
if (_freeGame)
|
||
return true;
|
||
|
||
return currentBalance >= _startGameCost;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Проверяет, достаточно ли баланса для продолжения игры
|
||
/// </summary>
|
||
public bool CanContinueGame()
|
||
{
|
||
// Проверка бесплатной игры
|
||
if (_freeGame)
|
||
return true;
|
||
|
||
return currentBalance >= _continueGameCost;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Списывает баланс за начало игры
|
||
/// </summary>
|
||
/// <returns>true если списание успешно</returns>
|
||
public bool SpendForStart()
|
||
{
|
||
// Проверка бесплатной игры
|
||
if (_freeGame)
|
||
return true;
|
||
|
||
return SpendBalance(_startGameCost, "начало игры");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Списывает баланс за продолжение игры
|
||
/// </summary>
|
||
/// <returns>true если списание успешно</returns>
|
||
public bool SpendForContinue()
|
||
{
|
||
// Проверка бесплатной игры
|
||
if (_freeGame)
|
||
return true;
|
||
|
||
return SpendBalance(_continueGameCost, "продолжение игры");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Списывает указанное количество баланса
|
||
/// </summary>
|
||
/// <param name="amount">Количество списываемого баланса</param>
|
||
/// <param name="reason">Причина списания (для логирования)</param>
|
||
/// <returns>true если списание успешно</returns>
|
||
public bool SpendBalance(int amount, string reason = "")
|
||
{
|
||
if (amount <= 0)
|
||
{
|
||
Debug.LogWarning($"[BalanceManager] Trying to spend invalid amount: {amount}");
|
||
return false;
|
||
}
|
||
|
||
if (currentBalance < amount)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
currentBalance -= amount;
|
||
|
||
// Сохранение если включено
|
||
if (saveBalanceBetweenSessions)
|
||
{
|
||
PlayerPrefs.SetInt(BALANCE_SAVE_KEY, currentBalance);
|
||
PlayerPrefs.Save();
|
||
}
|
||
|
||
// Синхронизация кредитов с балансом
|
||
SyncCreditsWithBalance();
|
||
|
||
// Уведомление подписчиков
|
||
OnBalanceSpent?.Invoke(amount, currentBalance);
|
||
OnBalanceChanged?.Invoke(currentBalance);
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Сбрасывает баланс до нуля
|
||
/// </summary>
|
||
public void ResetBalance()
|
||
{
|
||
currentBalance = 0;
|
||
|
||
if (saveBalanceBetweenSessions)
|
||
{
|
||
PlayerPrefs.SetInt(BALANCE_SAVE_KEY, 0);
|
||
PlayerPrefs.Save();
|
||
}
|
||
|
||
// Синхронизация кредитов с балансом
|
||
SyncCreditsWithBalance();
|
||
|
||
OnBalanceChanged?.Invoke(currentBalance);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Синхронизирует кредиты в ComSystemManager с текущим балансом
|
||
/// по формуле: кредиты = баланс / стоимость_импульса (округление вниз)
|
||
/// </summary>
|
||
private void SyncCreditsWithBalance()
|
||
{
|
||
if (_impulseCost <= 0)
|
||
{
|
||
Debug.LogError("[BalanceManager] Cannot sync credits - invalid impulse cost");
|
||
return;
|
||
}
|
||
|
||
int targetCredits = currentBalance / _impulseCost;
|
||
int currentCredits = ComSystemManager.GetCredits();
|
||
|
||
if (targetCredits == currentCredits)
|
||
return; // Кредиты уже синхронизированы
|
||
|
||
_isSyncingCredits = true;
|
||
try
|
||
{
|
||
// Устанавливаем кредиты напрямую через SetCredits
|
||
ComSystemManager.SetCredits(targetCredits);
|
||
}
|
||
finally
|
||
{
|
||
_isSyncingCredits = false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Устанавливает баланс напрямую (для тестирования/отладки)
|
||
/// </summary>
|
||
public void SetBalance(int amount)
|
||
{
|
||
currentBalance = Mathf.Max(0, amount);
|
||
|
||
if (saveBalanceBetweenSessions)
|
||
{
|
||
PlayerPrefs.SetInt(BALANCE_SAVE_KEY, currentBalance);
|
||
PlayerPrefs.Save();
|
||
}
|
||
|
||
// Синхронизация кредитов с балансом
|
||
SyncCreditsWithBalance();
|
||
|
||
OnBalanceChanged?.Invoke(currentBalance);
|
||
}
|
||
}
|
||
}
|
||
|