Подробные изменения: - Добавлены и доработаны модальные окна главного меню, сервисные настройки, шаблоны UI Toolkit и единый scene-flow сервис для переходов между сценами. - Расширена gameplay-архитектура: сессия забега, HUD, спавн контента платформ, collectible-предметы, оборудование, хрупкие и движущиеся платформы. - Добавлена база противников: типы спавна, патрулирование, процедурный спавнер, правила размещения через ScriptableObject-сложности и убийство врагов прыжком сверху. - Исправлены смерть и respawn игрока, финальный GameOver, отскоки от врагов, пружин и boost-предметов, а также проход сквозь врагов при усиленном подъеме. - Проведен рефакторинг взаимодействия систем: общий landing context, единые правила player-contact, NonLazy gameplay session, fail-fast installer и централизованный выбор точек спавна. - Обновлены сцены, prefab-ассеты, difficulty stages, ресурсы главного меню, настройки проекта, roadmap и аппаратная COM-интеграция. - Добавлены ассеты летучей мыши, врагов, collectibles, TextMesh Pro, ProjectContext и gameplay UI. Проверки: - dotnet build .\\Assembly-CSharp.csproj --no-restore - Unity validation ключевых gameplay/UI/scene-flow скриптов - Unity Console после force refresh без ошибок
247 lines
8.2 KiB
C#
247 lines
8.2 KiB
C#
using System;
|
||
using DanroJump.SceneFlow;
|
||
using DanroJump.Settings;
|
||
using UnityEngine;
|
||
|
||
namespace DanroJump.Gameplay
|
||
{
|
||
/// <summary>
|
||
/// Хранит состояние текущего забега: счет, монеты, жизни, высоту и рекорд.
|
||
/// </summary>
|
||
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;
|
||
|
||
/// <summary>
|
||
/// Очки, набранные в текущем забеге.
|
||
/// </summary>
|
||
public int Score { get; private set; }
|
||
|
||
/// <summary>
|
||
/// Монеты, собранные в текущем забеге.
|
||
/// </summary>
|
||
public int Coins { get; private set; }
|
||
|
||
/// <summary>
|
||
/// Количество смертей в текущем забеге.
|
||
/// </summary>
|
||
public int DeathCount { get; private set; }
|
||
|
||
/// <summary>
|
||
/// Оставшиеся жизни до завершения забега.
|
||
/// </summary>
|
||
public int RemainingLives { get; private set; }
|
||
|
||
/// <summary>
|
||
/// Текущее состояние gameplay-сессии.
|
||
/// </summary>
|
||
public GameplaySessionState State { get; private set; }
|
||
|
||
/// <summary>
|
||
/// Фактическая текущая высота игрока над стартом.
|
||
/// </summary>
|
||
public float CurrentHeight => player != null ? Mathf.Max(0f, player.transform.position.y) : 0f;
|
||
|
||
/// <summary>
|
||
/// Максимальная высота, достигнутая в текущем забеге.
|
||
/// </summary>
|
||
public float MaxRunHeight => player != null ? Mathf.Max(0f, player.HighestY) : 0f;
|
||
|
||
/// <summary>
|
||
/// Лучшая высота текущего забега.
|
||
/// </summary>
|
||
public float BestHeight => MaxRunHeight;
|
||
|
||
/// <summary>
|
||
/// Абсолютный рекорд с учетом сохраненного значения и текущего забега.
|
||
/// </summary>
|
||
public float RecordHeight => Mathf.Max(savedRecordHeight, MaxRunHeight);
|
||
|
||
/// <summary>
|
||
/// Показывает, находится ли игрок в состоянии смерти или завершенного забега.
|
||
/// </summary>
|
||
public bool IsPlayerDead => State == GameplaySessionState.Respawning || State == GameplaySessionState.GameOver;
|
||
|
||
public event Action Changed;
|
||
|
||
/// <summary>
|
||
/// Создает сессию и подписывается на события смерти/respawn игрока.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Добавляет очки в текущий забег.
|
||
/// </summary>
|
||
public void AddScore(int amount)
|
||
{
|
||
if (amount <= 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Score += amount;
|
||
Changed?.Invoke();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Добавляет монеты в текущий забег.
|
||
/// </summary>
|
||
public void AddCoins(int amount)
|
||
{
|
||
if (amount <= 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Coins += amount;
|
||
Changed?.Invoke();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Полностью сбрасывает забег, сохранив рекорд при необходимости.
|
||
/// </summary>
|
||
public void ResetSession()
|
||
{
|
||
SaveRecordIfNeeded();
|
||
Score = 0;
|
||
Coins = 0;
|
||
DeathCount = 0;
|
||
RemainingLives = startingLives;
|
||
State = GameplaySessionState.Running;
|
||
player?.ResetRun();
|
||
Changed?.Invoke();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Сохраняет рекорд и отписывается от событий игрока.
|
||
/// </summary>
|
||
public void Dispose()
|
||
{
|
||
SaveRecordIfNeeded();
|
||
|
||
if (player == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
player.Died -= HandlePlayerDied;
|
||
player.Respawned -= HandlePlayerRespawned;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Обновляет состояние сессии после смерти игрока.
|
||
/// </summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Возвращает сессию в Running после respawn, если игра еще не закончена.
|
||
/// </summary>
|
||
private void HandlePlayerRespawned(JumpPlayerController _)
|
||
{
|
||
if (State != GameplaySessionState.GameOver)
|
||
{
|
||
State = GameplaySessionState.Running;
|
||
}
|
||
|
||
Changed?.Invoke();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Читает стартовое количество жизней из сервисных настроек.
|
||
/// </summary>
|
||
private int ResolveStartingLives()
|
||
{
|
||
if (serviceSettings == null)
|
||
{
|
||
return DefaultStartingLives;
|
||
}
|
||
|
||
var configuredLives = serviceSettings.GetInt(ServiceSettingsKeys.StartingLives);
|
||
return Mathf.Max(1, configuredLives);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Сохраняет лучший результат по высоте в PlayerPrefs.
|
||
/// </summary>
|
||
private void SaveRecordIfNeeded()
|
||
{
|
||
if (MaxRunHeight <= savedRecordHeight)
|
||
{
|
||
return;
|
||
}
|
||
|
||
savedRecordHeight = MaxRunHeight;
|
||
PlayerPrefs.SetFloat(RecordHeightPrefsKey, savedRecordHeight);
|
||
PlayerPrefs.Save();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Запускает возврат в главное меню после финальной смерти.
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Состояние игрового забега.
|
||
/// </summary>
|
||
public enum GameplaySessionState
|
||
{
|
||
Waiting = 0,
|
||
Running = 1,
|
||
Respawning = 2,
|
||
GameOver = 3,
|
||
}
|
||
}
|