Danro/Assets/Scripts/Behaviour/ScoreSystem.cs
Danil Iarosh de87b318b7
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat: add ScoreSystem, environmental collectibles, object pool and spawner
- Добавлен `ScoreSystem` — singleton-компонент, отслеживающий монеты, убитых врагов и экологические очки;
  финальный счёт вычисляется по формуле `Score = E × (P + 45C + 90K + 10K²) − 0.5T`
  с раздельными множителями для победы и проигрыша.
- Добавлен `EnvironmentalCollectibleBase` — абстрактный базовый класс для подбираемых экологических объектов:
  срабатывает по триггеру, поддерживает пулинг, при подборе регистрирует очки в `ScoreSystem`.
- Добавлен `LeafCollectible` — листик с синусоидальным горизонтальным дрейфом, вращением и детекцией земли.
- Добавлен `BatCollectible` — летучая мышь с синусоидальным вертикальным движением, ограниченным горизонтальным
  блужданием и случайными сменами направления; переворачивает спрайт по X при смене направления.
- Добавлен `EnvironmentalPool` — простой объектный пул по prefab-ключу с прогревом (`Warmup`) и возвратом объектов.
- Добавлен `EnvironmentalCollectibleSpawner` — взвешенный случайный спаунер, поддерживающий фиксированное количество
  активных объектов; поддерживает лимиты по типу, принудительный выбор одного prefab и область спауна через
  `BoxCollider2D` или ручной `Bounds`.
- В `CoinCollectible.TryPickup` добавлен вызов `ScoreSystem.Instance?.RegisterCoinCollected()`.
- В `EnemyController.Die` добавлен guard через флаг `_isDead` для предотвращения повторного срабатывания;
  добавлен вызов `ScoreSystem.Instance?.RegisterEnemyKilled()`.
- В `GameManager` добавлен вызов `ScoreSystem.Instance?.ResetStats()` при старте новой игры и при потере жизни.
2026-03-03 09:14:49 +03:00

152 lines
4.6 KiB
C#

using System;
using RR.Game2D.Core.Managers;
using UnityEngine;
namespace RR.Game2D.Behaviour
{
/// <summary>
/// Система очков:
/// хранит количество собранных монет и убитых врагов,
/// считает итоговый счет при завершении игры.
/// Формула: Score = E * (P + 45C + 90K + 10K^2) - 0.5T
/// </summary>
[DefaultExecutionOrder(-900)]
public class ScoreSystem : MonoBehaviour
{
public static ScoreSystem Instance { get; private set; }
[Header("Коэффициенты исхода")]
[SerializeField] private float winMultiplier = 1f;
[SerializeField] private float loseMultiplier = 0.6f;
public int CollectedCoins { get; private set; }
public int TotalCollectedCoins => CollectedCoins;
public int KilledEnemies { get; private set; }
public int EnvironmentalPoints { get; private set; }
public int FinalScore { get; private set; }
public bool IsFinalized { get; private set; }
public event Action<int, int> StatsChanged;
public event Action<int> EnvironmentalPointsChanged;
public event Action<int, float, bool> ScoreCalculated;
private bool _subscribed;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
if (Instance != null) return;
GameObject go = new(nameof(ScoreSystem));
go.AddComponent<ScoreSystem>();
DontDestroyOnLoad(go);
}
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
ResetStats();
}
private void Start()
{
TrySubscribe();
}
private void OnEnable()
{
TrySubscribe();
}
private void OnDisable()
{
Unsubscribe();
}
public void RegisterCoinCollected(int amount = 1)
{
if (amount <= 0 || IsFinalized) return;
// This is the cumulative coin count for score calculation.
// It is intentionally separate from GameManager.Coins, which can wrap at 100 to award an extra life.
CollectedCoins += amount;
StatsChanged?.Invoke(CollectedCoins, KilledEnemies);
}
public void RegisterEnemyKilled(int amount = 1)
{
if (amount <= 0 || IsFinalized) return;
KilledEnemies += amount;
StatsChanged?.Invoke(CollectedCoins, KilledEnemies);
}
public void RegisterEnvironmentalPoints(int amount = 1)
{
if (amount <= 0 || IsFinalized) return;
EnvironmentalPoints += amount;
EnvironmentalPointsChanged?.Invoke(EnvironmentalPoints);
}
public void ResetStats()
{
CollectedCoins = 0;
KilledEnemies = 0;
EnvironmentalPoints = 0;
FinalScore = 0;
IsFinalized = false;
StatsChanged?.Invoke(CollectedCoins, KilledEnemies);
EnvironmentalPointsChanged?.Invoke(EnvironmentalPoints);
}
private void TrySubscribe()
{
if (_subscribed || GameManager.Instance == null) return;
GameManager.Instance.GameWonEvent += OnGameWon;
GameManager.Instance.GameOverEvent += OnGameOver;
_subscribed = true;
}
private void Unsubscribe()
{
if (!_subscribed || GameManager.Instance == null) return;
GameManager.Instance.GameWonEvent -= OnGameWon;
GameManager.Instance.GameOverEvent -= OnGameOver;
_subscribed = false;
}
private void OnGameWon(float gameTime)
{
Calculate(gameTime, true);
}
private void OnGameOver(float gameTime)
{
Calculate(gameTime, false);
}
private void Calculate(float gameTime, bool isWin)
{
if (IsFinalized) return;
float e = isWin ? winMultiplier : loseMultiplier;
float baseScore = EnvironmentalPoints + 45f * CollectedCoins + 90f * KilledEnemies + 10f * KilledEnemies * KilledEnemies;
float rawScore = e * baseScore - 0.5f * gameTime;
FinalScore = Mathf.Max(0, Mathf.RoundToInt(rawScore));
IsFinalized = true;
ScoreCalculated?.Invoke(FinalScore, gameTime, isWin);
}
}
}