using System; using DanroJump.SceneFlow; using DanroJump.Settings; using UnityEngine; namespace DanroJump.Gameplay { /// /// Хранит состояние текущего забега: счет, монеты, жизни, высоту и рекорд. /// public sealed class GameplaySession : IDisposable { private const string RecordHeightPrefsKey = "DanroJump.Gameplay.RecordHeight"; private const int DefaultStartingLives = 3; private static readonly TimeSpan GameOverReturnDelay = TimeSpan.FromSeconds(0.35f); private readonly JumpPlayerController player; private readonly IReadOnlyServiceSettings serviceSettings; private readonly ISceneFlowService sceneFlow; private readonly int startingLives; private float savedRecordHeight; /// /// Очки, набранные в текущем забеге. /// public int Score { get; private set; } /// /// Монеты, собранные в текущем забеге. /// public int Coins { get; private set; } /// /// Количество смертей в текущем забеге. /// public int DeathCount { get; private set; } /// /// Оставшиеся жизни до завершения забега. /// public int RemainingLives { get; private set; } /// /// Текущее состояние gameplay-сессии. /// public GameplaySessionState State { get; private set; } /// /// Фактическая текущая высота игрока над стартом. /// public float CurrentHeight => player != null ? Mathf.Max(0f, player.transform.position.y) : 0f; /// /// Максимальная высота, достигнутая в текущем забеге. /// public float MaxRunHeight => player != null ? Mathf.Max(0f, player.HighestY) : 0f; /// /// Лучшая высота текущего забега. /// public float BestHeight => MaxRunHeight; /// /// Абсолютный рекорд с учетом сохраненного значения и текущего забега. /// public float RecordHeight => Mathf.Max(savedRecordHeight, MaxRunHeight); /// /// Показывает, находится ли игрок в состоянии смерти или завершенного забега. /// public bool IsPlayerDead => State == GameplaySessionState.Respawning || State == GameplaySessionState.GameOver; public event Action Changed; /// /// Создает сессию и подписывается на события смерти/respawn игрока. /// public GameplaySession( JumpPlayerController player, [Zenject.InjectOptional] IReadOnlyServiceSettings serviceSettings = null, [Zenject.InjectOptional] ISceneFlowService sceneFlow = null) { this.player = player; this.serviceSettings = serviceSettings; this.sceneFlow = sceneFlow; startingLives = ResolveStartingLives(); RemainingLives = startingLives; State = GameplaySessionState.Running; savedRecordHeight = PlayerPrefs.GetFloat(RecordHeightPrefsKey, 0f); if (this.player != null) { this.player.Died += HandlePlayerDied; this.player.Respawned += HandlePlayerRespawned; } } /// /// Добавляет очки в текущий забег. /// public void AddScore(int amount) { if (amount <= 0) { return; } Score += amount; Changed?.Invoke(); } /// /// Добавляет монеты в текущий забег. /// public void AddCoins(int amount) { if (amount <= 0) { return; } Coins += amount; Changed?.Invoke(); } /// /// Полностью сбрасывает забег, сохранив рекорд при необходимости. /// public void ResetSession() { SaveRecordIfNeeded(); Score = 0; Coins = 0; DeathCount = 0; RemainingLives = startingLives; State = GameplaySessionState.Running; player?.ResetRun(); Changed?.Invoke(); } /// /// Сохраняет рекорд и отписывается от событий игрока. /// public void Dispose() { SaveRecordIfNeeded(); if (player == null) { return; } player.Died -= HandlePlayerDied; player.Respawned -= HandlePlayerRespawned; } /// /// Обновляет состояние сессии после смерти игрока. /// private void HandlePlayerDied(JumpPlayerController _) { DeathCount++; RemainingLives = Mathf.Max(0, RemainingLives - 1); State = RemainingLives > 0 ? GameplaySessionState.Respawning : GameplaySessionState.GameOver; if (State == GameplaySessionState.GameOver) { player?.BlockPendingRespawn(); StartGameOverReturn(); } else { player?.ScheduleRespawn(player.DeathRespawnDelay); } SaveRecordIfNeeded(); Changed?.Invoke(); } /// /// Возвращает сессию в Running после respawn, если игра еще не закончена. /// private void HandlePlayerRespawned(JumpPlayerController _) { if (State != GameplaySessionState.GameOver) { State = GameplaySessionState.Running; } Changed?.Invoke(); } /// /// Читает стартовое количество жизней из сервисных настроек. /// private int ResolveStartingLives() { if (serviceSettings == null) { return DefaultStartingLives; } var configuredLives = serviceSettings.GetInt(ServiceSettingsKeys.StartingLives); return Mathf.Max(1, configuredLives); } /// /// Сохраняет лучший результат по высоте в PlayerPrefs. /// private void SaveRecordIfNeeded() { if (MaxRunHeight <= savedRecordHeight) { return; } savedRecordHeight = MaxRunHeight; PlayerPrefs.SetFloat(RecordHeightPrefsKey, savedRecordHeight); PlayerPrefs.Save(); } /// /// Запускает возврат в главное меню после финальной смерти. /// private void StartGameOverReturn() { if (sceneFlow == null) { Debug.LogWarning($"{nameof(GameplaySession)} cannot return to main menu because {nameof(ISceneFlowService)} is not bound."); return; } sceneFlow.ReturnToMainMenuAfterGameOver(GameOverReturnDelay); } } /// /// Состояние игрового забега. /// public enum GameplaySessionState { Waiting = 0, Running = 1, Respawning = 2, GameOver = 3, } }