using System; using DanroJump.Audio; using DanroJump.Hardware.Com; using DanroJump.Prizes; 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 const int DefaultAdditionalLives = 1; private const float HeightMetersPerScorePoint = 10f; private static readonly TimeSpan GameOverOverlayReturnDelay = TimeSpan.FromSeconds(8f); private readonly IPlayerController player; private readonly IReadOnlyServiceSettings serviceSettings; private readonly ISceneFlowService sceneFlow; private readonly IPrizeFlowService prizeFlow; private readonly IComCreditWallet creditWallet; private readonly IGameAudioService audioService; private readonly int startingLives; private int baseScore; private float savedRecordHeight; public string SessionId { get; private set; } = CreateSessionId(); /// /// Итоговые очки текущего забега: игровые начисления плюс бонус за максимальную высоту. /// public int Score => baseScore + HeightScore; /// /// Очки, начисленные за максимальную высоту текущего забега. /// public int HeightScore => Mathf.FloorToInt(MaxRunHeight / HeightMetersPerScorePoint); /// /// Монеты, собранные в текущем забеге. /// 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 int ContinueGameCost => ResolveContinueCost(); public int ContinueGameLives => ResolveAdditionalLives(); public bool CanContinueGame => State == GameplaySessionState.GameOver && IsContinueEnabled() && HasContinueBalance(ContinueGameCost); public event Action Changed; /// /// Создает сессию и подписывается на события смерти/respawn игрока. /// public GameplaySession( IPlayerController player, [Zenject.InjectOptional] IReadOnlyServiceSettings serviceSettings = null, [Zenject.InjectOptional] ISceneFlowService sceneFlow = null, [Zenject.InjectOptional] IPrizeFlowService prizeFlow = null, [Zenject.InjectOptional] IComCreditWallet creditWallet = null, [Zenject.InjectOptional] IGameAudioService audioService = null) { this.player = player; this.serviceSettings = serviceSettings; this.sceneFlow = sceneFlow; this.prizeFlow = prizeFlow; this.creditWallet = creditWallet; this.audioService = audioService; 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; } baseScore += amount; Changed?.Invoke(); } /// /// Добавляет монеты в текущий забег. /// public void AddCoins(int amount) { if (amount <= 0) { return; } Coins += amount; Changed?.Invoke(); } /// /// Полностью сбрасывает забег, сохранив рекорд при необходимости. /// public void ResetSession() { SaveRecordIfNeeded(); SessionId = CreateSessionId(); baseScore = 0; Coins = 0; DeathCount = 0; RemainingLives = startingLives; State = GameplaySessionState.Running; player?.ResetRun(); Changed?.Invoke(); } public bool TryContinueGame() { if (!CanContinueGame) { return false; } if (!TrySpendContinueBalance(ContinueGameCost)) { Changed?.Invoke(); return false; } audioService?.Play(GameAudioEvent.ContinueAccepted); sceneFlow?.CancelGameOverReturn(); RemainingLives = ContinueGameLives; State = GameplaySessionState.Respawning; if (player != null) { player.AllowRespawn(); player.Respawn(); } else { State = GameplaySessionState.Running; } Changed?.Invoke(); return true; } /// /// Сохраняет рекорд и отписывается от событий игрока. /// public void Dispose() { SaveRecordIfNeeded(); if (player == null) { return; } player.Died -= HandlePlayerDied; player.Respawned -= HandlePlayerRespawned; } /// /// Обновляет состояние сессии после смерти игрока. /// private void HandlePlayerDied(IPlayerController _) { audioService?.Play(GameAudioEvent.PlayerDeath); DeathCount++; RemainingLives = Mathf.Max(0, RemainingLives - 1); State = RemainingLives > 0 ? GameplaySessionState.Respawning : GameplaySessionState.GameOver; if (State == GameplaySessionState.GameOver) { audioService?.Play(GameAudioEvent.GameOver); audioService?.PlayMusic(GameMusicCue.GameOver); player?.BlockPendingRespawn(); var prizeResult = prizeFlow?.StartPrizeFlow(this) ?? default; if (!prizeResult.HasPrize) { StartGameOverReturn(GameOverOverlayReturnDelay); } } else { player?.ScheduleRespawn(player.DeathRespawnDelay); } SaveRecordIfNeeded(); Changed?.Invoke(); } /// /// Возвращает сессию в Running после respawn, если игра еще не закончена. /// private void HandlePlayerRespawned(IPlayerController _) { if (State != GameplaySessionState.GameOver) { State = GameplaySessionState.Running; audioService?.Play(GameAudioEvent.PlayerRespawn); audioService?.PlayMusic(GameMusicCue.Gameplay); } Changed?.Invoke(); } /// /// Читает стартовое количество жизней из сервисных настроек. /// private int ResolveStartingLives() { if (serviceSettings == null) { return DefaultStartingLives; } var configuredLives = serviceSettings.GetInt(ServiceSettingsKeys.StartingLives); return Mathf.Max(1, configuredLives); } private int ResolveAdditionalLives() { if (serviceSettings == null) { return DefaultAdditionalLives; } return Mathf.Max(1, serviceSettings.GetInt(ServiceSettingsKeys.AdditionalLives)); } private int ResolveContinueCost() { return serviceSettings != null ? Mathf.Max(0, serviceSettings.GetInt(ServiceSettingsKeys.ContinueGameCost)) : 0; } public bool IsContinueEnabled() { return serviceSettings != null && serviceSettings.GetBool(ServiceSettingsKeys.CanContinueGame); } private bool HasContinueBalance(int cost) { return IsFreePlay() || cost <= 0 || creditWallet != null && creditWallet.Credits >= cost; } private bool TrySpendContinueBalance(int cost) { if (IsFreePlay() || cost <= 0) { return true; } return creditWallet != null && creditWallet.TrySpendCredits(cost); } private bool IsFreePlay() { return serviceSettings != null && serviceSettings.GetBool(ServiceSettingsKeys.FreePlay); } /// /// Сохраняет лучший результат по высоте в PlayerPrefs. /// private void SaveRecordIfNeeded() { if (MaxRunHeight <= savedRecordHeight) { return; } savedRecordHeight = MaxRunHeight; PlayerPrefs.SetFloat(RecordHeightPrefsKey, savedRecordHeight); PlayerPrefs.Save(); } /// /// Запускает возврат в главное меню после финальной смерти. /// private void StartGameOverReturn(TimeSpan delay) { if (sceneFlow == null) { Debug.LogWarning($"{nameof(GameplaySession)} cannot return to main menu because {nameof(ISceneFlowService)} is not bound."); return; } sceneFlow.ReturnToMainMenuAfterGameOver(delay); } private static string CreateSessionId() { return Guid.NewGuid().ToString("N"); } } /// /// Состояние игрового забега. /// public enum GameplaySessionState { Waiting = 0, Running = 1, Respawning = 2, GameOver = 3, } }