Danro/Assets/Scripts/Behaviour/ScoreSystem.cs
Danil Iarosh cb2e83a596
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat(ui, prizes, spawners, editor): expand prize flow and add spawning/editor tooling
- доработан flow призовой витрины и связанного UI:
  добавлены `PrizeStandButtonView` и `RulesPrizeListPage`, обновлены
  `PrizeStandController`, `PrizeStandPrize`, `PrizeStandModal`,
  `PrizeStandSettingsModal`, `LevelPrizeSettingsModal`, `QRCodeSettingsModal`,
  `BalanceDisplay`, `ButtonTextFocusColor` и связанные prefab'ы,
  чтобы улучшить настройку, навигацию, отображение кнопок и prize flow
- расширена система настроек и сохранения значений:
  `OptionPicker` получил отдельный формат `ValueSelector`,
  обновлены `SettingsDatabase`, `SettingEntryDrawer`, `SettingUIItem`,
  `SettingsUIGenerator`, `ValueSelectorRow` и `MainSettingsDatabase.asset`,
  а сохранение option picker-значений, включая `gameplay.gameDifficulty`,
  исправлено и синхронизировано с остальными настройками
- добавлены универсальные спавнеры сущностей:
  реализованы `EntitySpawner` и `EntitySpawnerManager` с поддержкой
  весов префабов, точек спавна, заполнения по проценту, ограничений
  на количество и автоспавна при инициализации
- доработана логика игрового спавна и сложности:
  процент инициализационного спавна может зависеть от настройки
  `gameplay.gameDifficulty` с градациями 25/50/75/100,
  в одном цикле массового спавна повторно не используется та же точка,
  а gizmo спавнера теперь показывает только сами точки без линий
- упрощена работа в редакторе Unity и добавлены editor-инструменты:
  в `GameManager` лицензия пропускается при запуске в `UNITY_EDITOR`,
  а `HierarchySelectionCounter`, `HierarchySelectionCountOverlay`
  и `HierarchyBulkRenamerWindow` добавляют подсчет выделения,
  realtime overlay в `SceneView` и массовое переименование объектов
  по правилам нумерации из Unity Project Settings
- синхронизированы сцены, ресурсы и UI-ассеты под новый flow:
  обновлены `MainMenu`, `Level UI`, уровни `1-1` и `1-2`,
  строки настроек, кнопки, TMP/font assets, изображения UI,
  `prizes.json` и recovery-данные после правок интерфейса и логики
- очищены и переразложены часть внешних и служебных ресурсов:
  удалён пакет `Lava_Flowing_Shader`, переименованы/заменены
  отдельные UI-prefab'ы и добавлены сопутствующие проектные артефакты,
  которые сейчас используются обновлённой конфигурацией проекта
2026-04-28 14:05:05 +03:00

213 lines
7.0 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;
[Header("Environmental Collectibles")]
[SerializeField] private int maxSharedEnvironmentalCollectibles = 50;
public int CollectedCoins { get; private set; }
public int TotalCollectedCoins => CollectedCoins;
public int KilledEnemies { get; private set; }
public int EnvironmentalPoints { get; private set; }
public int SharedEnvironmentalCollectiblesCollected { get; private set; }
public int CurrentScore { get; private set; }
public int FinalScore { get; private set; }
public bool IsFinalized { get; private set; }
public bool HasReachedSharedEnvironmentalCollectibleLimit =>
maxSharedEnvironmentalCollectibles > 0 && SharedEnvironmentalCollectiblesCollected >= maxSharedEnvironmentalCollectibles;
public event Action<int, int> StatsChanged;
public event Action<int> EnvironmentalPointsChanged;
public event Action<int, int> SharedEnvironmentalCollectiblesChanged;
public event Action<int> CurrentScoreChanged;
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 is used by gameplay/UI and life-award thresholds.
CollectedCoins += amount;
StatsChanged?.Invoke(CollectedCoins, KilledEnemies);
RefreshCurrentScore();
}
public void RegisterEnemyKilled(int amount = 1)
{
if (amount <= 0 || IsFinalized) return;
KilledEnemies += amount;
StatsChanged?.Invoke(CollectedCoins, KilledEnemies);
RefreshCurrentScore();
}
public void RegisterEnvironmentalPoints(int amount = 1)
{
if (amount <= 0 || IsFinalized) return;
EnvironmentalPoints += amount;
EnvironmentalPointsChanged?.Invoke(EnvironmentalPoints);
RefreshCurrentScore();
}
public void RegisterSharedEnvironmentalCollectibleCollected(int points = 1)
{
if (IsFinalized || HasReachedSharedEnvironmentalCollectibleLimit) return;
SharedEnvironmentalCollectiblesCollected++;
SharedEnvironmentalCollectiblesChanged?.Invoke(SharedEnvironmentalCollectiblesCollected, maxSharedEnvironmentalCollectibles);
if (points > 0)
{
EnvironmentalPoints += points;
EnvironmentalPointsChanged?.Invoke(EnvironmentalPoints);
}
RefreshCurrentScore();
}
public int CalculatePreviewScore(float gameTime, bool isWin)
{
float e = isWin ? winMultiplier : loseMultiplier;
float baseScore = EnvironmentalPoints + 45f * CollectedCoins + 90f * KilledEnemies + 10f * KilledEnemies * KilledEnemies;
float rawScore = e * baseScore - 0.5f * gameTime;
return Mathf.Max(0, Mathf.RoundToInt(rawScore));
}
public int GetDisplayScore(float gameTime, bool isWin = true)
{
if (IsFinalized)
{
return FinalScore;
}
return CalculatePreviewScore(gameTime, isWin);
}
public void ResetStats()
{
CollectedCoins = 0;
KilledEnemies = 0;
EnvironmentalPoints = 0;
SharedEnvironmentalCollectiblesCollected = 0;
CurrentScore = 0;
FinalScore = 0;
IsFinalized = false;
StatsChanged?.Invoke(CollectedCoins, KilledEnemies);
EnvironmentalPointsChanged?.Invoke(EnvironmentalPoints);
SharedEnvironmentalCollectiblesChanged?.Invoke(SharedEnvironmentalCollectiblesCollected, maxSharedEnvironmentalCollectibles);
CurrentScoreChanged?.Invoke(CurrentScore);
}
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;
FinalScore = CalculatePreviewScore(gameTime, isWin);
CurrentScore = FinalScore;
IsFinalized = true;
CurrentScoreChanged?.Invoke(CurrentScore);
ScoreCalculated?.Invoke(FinalScore, gameTime, isWin);
}
private void RefreshCurrentScore()
{
if (IsFinalized)
{
return;
}
float gameTime = GameManager.Instance != null ? GameManager.Instance.GetCurrentGameTime() : 0f;
CurrentScore = CalculatePreviewScore(gameTime, true);
CurrentScoreChanged?.Invoke(CurrentScore);
}
}
}