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 CurrentCombo { get; private set; } = 1;
///
/// Итоговые очки текущего забега: игровые начисления плюс бонус за максимальную высоту.
///
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;
this.player.LandedOnNormalPlatform += ResetCombo;
}
}
///
/// Увеличивает комбо-множитель.
///
public void IncrementCombo()
{
CurrentCombo++;
Changed?.Invoke();
}
///
/// Сбрасывает комбо-множитель в 1.
///
public void ResetCombo()
{
if (CurrentCombo != 1)
{
CurrentCombo = 1;
Changed?.Invoke();
}
}
///
/// Добавляет очки в текущий забег (с учетом комбо-множителя).
///
public void AddScore(int amount)
{
if (amount <= 0)
{
return;
}
baseScore += amount * CurrentCombo;
CurrentCombo++;
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;
CurrentCombo = 1;
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;
CurrentCombo = 1;
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;
player.LandedOnNormalPlatform -= ResetCombo;
}
///
/// Обновляет состояние сессии после смерти игрока.
///
private void HandlePlayerDied(IPlayerController _)
{
audioService?.Play(GameAudioEvent.PlayerDeath);
DeathCount++;
RemainingLives = Mathf.Max(0, RemainingLives - 1);
State = RemainingLives > 0 ? GameplaySessionState.Respawning : GameplaySessionState.GameOver;
CurrentCombo = 1;
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,
}
}