Danro/Assets/Scripts/Behaviour/ScoreSystem.cs
Danil Iarosh 78d7679e2b
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat(prizes, ui): complete Mar 24-Apr 2 sprint ahead of schedule
- завершен спринт 24 марта - 2 апреля с опережением плана и закрытием задач раньше срока
- расширена и стабилизирована система призов:
  добавлены и доработаны QR-tier конфиги, новые типы и модели данных,
  сохранение `qrCodes` в `prizes.json`, fallback-логика и синхронизация
  через `PrizeSystemManager`, `PrizeDatabase` и `LevelPrizeConfig`
- переработан flow настройки QR-призов:
  добавлены `QRCodeSettingsModal` и связанный UI-код,
  разнесены короткий текст приза и полный текст сообщения,
  добавлены шаблоны форматирования, корректная подстановка текста
  в QR и в UI-модалках, а также сохранение конфигурации между запусками
- доработан flow суперприза и текстовых настроек:
  добавлены fallback-правила показа, корректная обработка пустых строк
  и строк из одних пробелов, обновлены binders и показ текста в меню
- реализована и обновлена отдельная настройка витрины:
  добавлена `PrizeStandSettingsModal`, обновлена навигация строк,
  логика выбора ячейки и порогов очков, циклический выбор ячеек,
  ускорение изменения значений и исправления для keyboard/gamepad UX
- доработана игровая модалка витрины `PrizeStandModal`:
  добавлен вывод очков игрока, форматируемый текст счета,
  улучшено отображение ячеек и связанная логика выбора приза
- исправлен подсчет очков и интеграция с prize flow:
  `ScoreSystem` переведен на событийное обновление текущего счета,
  добавлены `CurrentScore`, `CurrentScoreChanged` и корректный display score,
  витрина и связанные UI-элементы теперь получают актуальные очки,
  а не нулевые или несвоевременные preview-значения
- переработаны модальные окна и навигация UI:
  улучшены `Modal`, `ModalManager`, `UIRowPointer`, `RulesModal`,
  `QRCodeModal`, `PrizeStandModal`, `LevelPrizeSettingsModal` и др.,
  чтобы фокус, указатель и submit работали консистентно без мыши
- обновлен win/prize UI:
  доработаны `WinBannerController`, `GameEndUIController`,
  `MainMenu`, `QRCodeUIController`, `TopPanelTextController` и настройки UI,
  улучшены тексты баннеров, отображение призов и итогового счета
- обновлены сцены и ассеты под новый flow:
  `MainMenu`, `Level UI`, prefab'ы строк настроек и кнопок,
  глобальные префабы, шрифты TMP, иконки, recovery-данные и ресурсы UI
- добавлены вспомогательные UI-компоненты и служебные скрипты:
  `ButtonTextFocusColor`, `SelectableDescriptionTarget`, `QRTier`
  и другие изменения, поддерживающие новый конфигурируемый prize UI
2026-04-01 12:47:17 +03:00

188 lines
5.8 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 CurrentScore { 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> 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 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;
CurrentScore = 0;
FinalScore = 0;
IsFinalized = false;
StatsChanged?.Invoke(CollectedCoins, KilledEnemies);
EnvironmentalPointsChanged?.Invoke(EnvironmentalPoints);
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);
}
}
}