using System; using System.IO; using RR.Common.Controls; using UnityEngine; using UnityEngine.Audio; using UnityEngine.SceneManagement; using RR.Common.Managers; using RR.Common.Settings; using RR.Common.Utility; using RR.Game2D.Behaviour; using RR.Game2D.Entities; namespace RR.Game2D.Core.Managers { [DefaultExecutionOrder(-1000)] public class GameManager : MonoBehaviour { // Singleton instance public static GameManager Instance { get; private set; } // Logger public static CompositeLogger Logger { get; private set; } // Game settings (новая система) public SettingsManager Settings { get; private set; } // Audio control for game public AudioManager Audio {get; private set;} // Payment control for game public PaymentManager Payment {get; private set;} // Balance control for game public BalanceManager Balance {get; private set;} // Inactivity control for game public InactivityManager Inactivity {get; private set;} // Plate control for game public ComWrapper Plate { get; private set; } // Light control for game public LightController Light => Plate != null ? Plate.Light : null; // Globals [Header("Globals")] [SerializeField] private string globalsPrefabResourcePath = "Prefabs/Global/Globals"; [SerializeField] private string AudioMixerResourcePath = "Audio/Master"; private GameObject globals; [Header("Пауза")] public bool IsPaused { get; private set; } [Header("Базовые настройки игры")] public int StartingLives { get; private set; } = 3; public int MaxLives { get; private set; } = 3; public int LivesIncrement { get; private set; } = 0; [Header("Информация для игры")] public int World { get; private set; } = 1; public int Stage { get; private set; } = 1; private int StagesByWorld { get; set; } = 3; public int Lives { get; set; } = 3; public int Coins { get; private set; } = 0; private float _gameStartTime; [Header("Игрок")] [SerializeField] private GameObject playerPrefab; [SerializeField] private string playerPrefabResourcePath = "Prefabs/Player_New"; // Путь в Resources для загрузки префаба игрока, если не назначен в инспекторе public GameObject PlayerObj { get; private set; } private bool _shouldSpawnPlayer; private void OnEnable() => SceneManager.sceneLoaded += OnSceneLoaded; private void OnDisable() => SceneManager.sceneLoaded -= OnSceneLoaded; // События менеджера public event Action PauseStateChanged; // Когда меняется состояние паузы public event Action PlayerSpawned; // Когда игрос спавнится на сцене public event Action CoinsChanged; // Когда меняется кол-во монет public event Action MaxLivesChanged; // Когда меняется максимальное кол-во жизней public event Action LivesChanged; // Когда меняется кол-во жизней public event Action GameOverEvent; // Когда игрок проигрывает public event Action GameWonEvent; // Когда игрок выигрывает #region Unity Lifetime // Singleton [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void Initialize() { if (Instance != null) return; GameObject go = new(nameof(GameManager)); Instance = go.AddComponent(); go.AddComponent(); DontDestroyOnLoad(go); } private void Awake() { // ---- Singleton ---- if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); // ------------------- // Initialize Logger if (Logger == null) { Logger = new CompositeLogger(); FileLogger fileLogger = new(Path.Combine(Directory.GetParent(Application.dataPath)?.ToString() ?? Application.dataPath, "game.log")); Logger.Initialize(fileLogger); } // Initialize Audio control AudioMixer audioMixer = Resources.Load(AudioMixerResourcePath); if (Audio == null) { Audio = gameObject.AddComponent(); Audio.Initialize(audioMixer); } // Initialize Global Scripts from prefab if (globals == null) { GameObject prefab = Resources.Load(globalsPrefabResourcePath); if (prefab != null) { Debug.Log($"[GameManager] Loading Globals prefab from {globalsPrefabResourcePath}"); globals = Instantiate(prefab); DontDestroyOnLoad(globals); Debug.Log($"[GameManager] Globals instantiated. SettingsManager.Instance = {(SettingsManager.Instance != null ? "exists" : "null")}"); } else { Debug.LogError($"[GameManager] Failed to load Globals prefab from {globalsPrefabResourcePath}"); } } // Initialize Settings Manager (новая система) from Globals if (Settings == null) Settings = globals.GetComponent(); if (Settings == null) Settings = SettingsManager.Instance; if (Settings != null) { Settings.OnSettingsLoaded += OnSettingsChanged; Settings.OnSettingsSaved += OnSettingsChanged; } // Initialize Payment control if (Payment == null) Payment = globals.GetComponent(); // Initialize Balance control if (Balance == null) Balance = globals.GetComponent(); if (Balance == null) Balance = BalanceManager.Instance; // Initialize Inactivity control if (Inactivity == null) Inactivity = gameObject.AddComponent(); if (Inactivity == null) Inactivity = InactivityManager.Instance; // Apply loaded settings (new system applies automatically in Awake) if (Plate == null) Plate = globals.GetComponent(); Payment.OnCreditAdded.AddListener(OnCreditAdded); _gameStartTime = Time.unscaledTime; MaxLives = StartingLives; MaxLivesChanged?.Invoke(MaxLives); if (SceneManager.GetActiveScene().name != "MainMenu") _shouldSpawnPlayer = true; } private void OnDestroy() { if (Instance == this) Instance = null; Plate.StopBlink(false); } private void OnApplicationQuit() { OnDestroy(); } /// /// Поиск префаба игрока. /// Если не найдет префаб из инспектора, ищет в "Resources" /// /// Префаб игрока private GameObject ResolvePlayerPrefab() { if (playerPrefab != null) return playerPrefab; GameObject loaded = Resources.Load(playerPrefabResourcePath); if (loaded == null) { Logger.LogError( gameObject.name, $"playerPrefab is null. Resources.Load('{playerPrefabResourcePath}') вернул null." + $" Ожидается файл: Assets/Resources/{playerPrefabResourcePath}.prefab (точное совпадение регистра)." ); } return loaded; } #endregion #region Payment Event Handlers private void OnCreditAdded(int newCount) { if (SceneManager.GetActiveScene().name == "MainMenu") { if (newCount > 1) { MaxLives += LivesIncrement; } } // Debug.Log($"Max Lives: {MaxLives}"); MaxLivesChanged?.Invoke(MaxLives); } #endregion #region Settings Event Handlers private void OnSettingsChanged() { if (Settings == null) return; // Получаем настройки из нового SettingsManager StartingLives = Settings.GetInt("gameplay.startLives"); LivesIncrement = Settings.GetInt("gameplay.additionalLives"); int currentCredits = Payment?.CreditsCount ?? 0; MaxLives = currentCredits > 1 ? StartingLives + (currentCredits - 1) * LivesIncrement : StartingLives; MaxLivesChanged?.Invoke(MaxLives); } #endregion #region Pause /// /// Метод паузы игры. /// Ставит на паузу все SFX и саму игру /// /// Ставить ли на паузу public void SetPause(bool pause) { if (IsPaused == pause) return; IsPaused = pause; Time.timeScale = pause ? 0f : 1f; Audio.SmoothPauseMusic(pause); PauseStateChanged?.Invoke(IsPaused); } // Можно заменить на другое поведение /// /// Метод переключателя паузы. /// public void TogglePause() => SetPause(!IsPaused); // -- #endregion #region Levels and Scene management /// /// Начало новой игры. /// Загружает первый уровень. /// Устанавливает жизни на 3, монеты на 0 /// public void NewGame() { Debug.Log("[GameManager] NewGame called"); // Проверка баланса перед началом игры if (Balance != null && !Balance.CanStartGame()) { Logger?.Log(gameObject.name, "Cannot start game - insufficient balance"); Debug.LogWarning("[GameManager] Cannot start - insufficient balance"); return; } int livesToGive = StartingLives; int balanceToSpend = 0; if (Balance != null) { int currentBalance = Balance.CurrentBalance; int impulseCost = Settings?.GetInt("payment.paymentImpulseCost") ?? 10; // Рассчитываем количество оплат int totalImpulses = currentBalance / impulseCost; // Первая оплата даёт базовые жизни // Каждая дополнительная оплата даёт LivesIncrement жизней if (totalImpulses > 0) { int additionalImpulses = totalImpulses - 1; int bonusLives = additionalImpulses * LivesIncrement; livesToGive = StartingLives + bonusLives; Debug.Log($"[GameManager] Balance: {currentBalance}, Impulses: {totalImpulses}, Base lives: {StartingLives}, Bonus lives: {bonusLives}, Total lives: {livesToGive}"); } // Списываем ВЕСЬ баланс balanceToSpend = currentBalance; if (!Balance.SpendBalance(balanceToSpend, "начало игры (полный баланс)")) { Logger?.Log(gameObject.name, "Failed to spend balance for game start"); Debug.LogError("[GameManager] Failed to spend balance!"); return; } Debug.Log($"[GameManager] Spent entire balance: {balanceToSpend}, Lives given: {livesToGive}"); } Lives = livesToGive; LivesChanged?.Invoke(Lives); Coins = 0; CoinsChanged?.Invoke(Coins); _gameStartTime = Time.time; Logger.Log(gameObject.name,$"Game start time = {_gameStartTime}"); if (Plate) Plate.StopBlink(); Light?.SetLight(LightMode.Rainbow, 99); // Включаем отслеживание бездействия if (Inactivity != null) Inactivity.EnableInactivityCheck(); LoadLevel(1, 1); } public void ContinueGame() { // Проверка баланса перед продолжением игры if (Balance != null && !Balance.CanContinueGame()) { Logger?.Log(gameObject.name, "Cannot continue game - insufficient balance"); return; } // Списание баланса за продолжение игры if (Balance != null && !Balance.SpendForContinue()) { Logger?.Log(gameObject.name, "Failed to spend balance for continue"); return; } Lives = StartingLives + 1; LivesChanged?.Invoke(Lives); Coins = 0; CoinsChanged?.Invoke(Coins); // Сбрасываем таймер бездействия if (Inactivity != null) Inactivity.ResetInactivityTimer(); RespawnPlayer(); } // Можно заменить на другое поведение /// /// Окончание игры. /// Загружает сцену главного меню /// private void GameOver() { float totalTime = Time.unscaledTime - _gameStartTime; // Отключаем отслеживание бездействия if (Inactivity != null) Inactivity.DisableInactivityCheck(); Light?.SetLight(LightMode.BlueWave, 30); GameOverEvent?.Invoke(totalTime); } // -- private void GameWin() { float totalTime = Time.unscaledTime - _gameStartTime; // Отключаем отслеживание бездействия if (Inactivity != null) Inactivity.DisableInactivityCheck(); Light?.SetLight(LightMode.FinalWave, 30); GameWonEvent?.Invoke(totalTime); } /// /// Метод переход на следующий уровень. /// Может загружать уровни и миры в прямой зависимости от /// Количества уровней на мир(StagesByWorld) /// public void NextLevel() { if (Stage >= StagesByWorld) { World++; Stage = 1; } else Stage++; LoadLevel(World, Stage); } /// /// Метод сброса уровня. /// При сбросе также сбрасывает монеты и отнимает жизни /// [Obsolete("Используйте RespawnPlayer() для респавна игрока без сброса уровня и жизней.")] public void ResetLevel() { Lives--; LivesChanged?.Invoke(Lives); Coins = 0; CoinsChanged?.Invoke(Coins); //print($"Lives: {lives}"); if (Lives > 0) LoadLevel(World, Stage); else GameOver(); } /// /// Загрузка уровня по данным. /// Загружает уровень по номерам мира и уровня /// /// Номер мира /// Номер уровня public void LoadLevel(int world, int stage) { this.World = world; this.Stage = stage; string sceneName = $"{world}-{stage}"; if (SceneInBuildSettingsExists(sceneName)) { LoadScene(sceneName); } else { // Можно заменить на другое поведение GameWin(); // -- } } /// /// Загрузка сцены по названию. /// Вызывает одноименный метод в . /// Может загружать сцены с UI и без. /// В сценах без UI игрок не спавнится /// /// Строковое название сцены /// Использовать ли UI public void LoadScene(string sceneName, bool useUI = true) { _shouldSpawnPlayer = true; SceneManager.LoadScene(sceneName); if (useUI) SceneManager.LoadScene("Level UI", LoadSceneMode.Additive); else { Light?.SetLight(LightMode.Off); Plate.Blink(); _shouldSpawnPlayer = false; } } /// /// Метод проверки сцены на существование в настройках билда /// /// Строковое название сцены /// private static bool SceneInBuildSettingsExists(string sceneName) { var count = SceneManager.sceneCountInBuildSettings; for (var i = 0; i < count; i++) { var path = SceneUtility.GetScenePathByBuildIndex(i); if (Path.GetFileNameWithoutExtension(path) == sceneName) return true; } return false; } /// /// Метод обработки события загрузки сцены. /// При загрузке сцены проверяет её название на наличие "-" (уровень формата "1-1") /// Если находит, спавнит игрока /// Также обновляет номера мира и уровня /// /// Сцена /// Режим загрузки сцены private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { string currentSceneName = SceneManager.GetActiveScene().name; if (!currentSceneName.Contains("-")) return; TrySpawnPlayerForActiveScene(); if (currentSceneName == $"{World}-{Stage}") return; string[] levelInfo = currentSceneName.Split("-"); World = Convert.ToInt32(levelInfo[0]); Stage = Convert.ToInt32(levelInfo[1]); } #endregion #region Player management /// /// Метод для спавна игрока. /// Ищет первый спавнпоинт, спавнит игрока на его позиции. /// Если на карте есть объект игрока, удаляет его и спавнит свой /// private void TrySpawnPlayerForActiveScene() { if (!_shouldSpawnPlayer) return; PlayerSpawn spawn = FindFirstObjectByType(); Vector3 pos = spawn ? spawn.transform.position : new Vector3(0, 1.3f, 0); Quaternion rot = spawn ? spawn.transform.rotation : Quaternion.identity; PlayerObj = GameObject.FindGameObjectWithTag("Player"); if (PlayerObj != null) Destroy(PlayerObj); GameObject prefab = ResolvePlayerPrefab(); if (prefab == null) return; PlayerObj = Instantiate(prefab, pos, rot); SceneManager.MoveGameObjectToScene(PlayerObj, SceneManager.GetActiveScene()); PlayerSpawned?.Invoke(PlayerObj); } /// /// Метод респавна игрока. /// Отнимает жизнь, если жизней не осталось — вызывает GameOver() /// Спавнит игрока на активном чекпоинте или на дефолтной позиции /// public void RespawnPlayer() { // отнимем жизнь (оставляем текущее поведение); монеты не сбрасываем Lives--; LivesChanged?.Invoke(Lives); if (Lives <= 0) { GameOver(); return; } // Определяем дефолтные позицию и направление из PlayerSpawn в сцене PlayerSpawn spawn = FindFirstObjectByType(); Vector3 defaultPos = spawn ? spawn.transform.position : new Vector3(0, 1.3f, 0); Vector2 defaultForward = spawn ? spawn.defaultForward : Vector2.right; // Если есть активный чекпоинт — берём из него; иначе дефолт Vector3 respawnPos = defaultPos; Vector2 respawnForward = defaultForward; if (CheckpointSystem.Instance != null) { respawnPos = CheckpointSystem.Instance.GetSpawnPosition(defaultPos); respawnForward = CheckpointSystem.Instance.GetSpawnForward(defaultForward); if (CheckpointSystem.Instance.Active != null) { respawnPos = CheckpointSystem.Instance.Active.GetSpawnPosition(); respawnForward = CheckpointSystem.Instance.Active.GetForward(); } } // Если потеряли ссылку — попробуем найти игрока в сцене if (PlayerObj == null) PlayerObj = GameObject.FindGameObjectWithTag("Player"); if (PlayerObj == null) { // Создадим нового игрока, если объект был уничтожен GameObject prefab = ResolvePlayerPrefab(); if (prefab == null) return; PlayerObj = Instantiate(prefab, respawnPos, Quaternion.identity); SceneManager.MoveGameObjectToScene(PlayerObj, SceneManager.GetActiveScene()); PlayerSpawned?.Invoke(PlayerObj); } else { // Остановим движение и телепортируем Rigidbody2D rb = PlayerObj.GetComponent(); if (rb != null) { rb.linearVelocity = Vector2.zero; rb.angularVelocity = 0f; } PlayerObj.transform.position = respawnPos; } // Повернём в нужную сторону (по X) if (PlayerObj == null) return; if (respawnForward.x != 0) { PlayerObj.transform.localEulerAngles = respawnForward.x >= 0 ? Vector3.zero : new Vector3(0, 180f, 0); } // Восстановим здоровье Health playerHealth = PlayerObj.GetComponent(); if (playerHealth != null) playerHealth.ResetHealth(); } #endregion #region Coins and Lives management /// /// Метод добавления внутриигровой монеты /// public void AddCoin(int count = 1) { Coins += count; // Можно заменить на другое поведение if (Coins >= 100) { Coins -= 100; AddLife(); } // -- CoinsChanged?.Invoke(Coins); } // Можно заменить на другое поведение /// /// Метод для добавления жизни. /// private void AddLife() { Lives++; LivesChanged?.Invoke(Lives); } // -- #endregion } }