Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- завершен спринт 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
443 lines
16 KiB
C#
443 lines
16 KiB
C#
using UnityEngine;
|
||
using UnityEngine.InputSystem;
|
||
using UnityEngine.InputSystem.Controls;
|
||
using RR.Common.Controls;
|
||
using RR.Common.Settings;
|
||
using RR.Game2D.Core.Managers;
|
||
|
||
namespace RR.Common.Managers
|
||
{
|
||
/// <summary>
|
||
/// Менеджер отслеживания бездействия игрока.
|
||
/// При отсутствии ввода в течение заданного времени выкидывает игрока в главное меню.
|
||
/// </summary>
|
||
public class InactivityManager : MonoBehaviour
|
||
{
|
||
public static InactivityManager Instance { get; private set; }
|
||
|
||
[Header("Настройки бездействия")]
|
||
[SerializeField] private float inactivityTimeout = 300f; // 5 минут в секундах
|
||
[SerializeField] private bool enableInactivityCheck = true;
|
||
|
||
[Header("Отладка")]
|
||
[SerializeField] private bool showDebugInfo;
|
||
|
||
private float lastInputTime;
|
||
private bool isInMainMenu = true;
|
||
private bool isGameActive;
|
||
private InputSnapshot lastInputSnapshot;
|
||
private bool hasInputSnapshot;
|
||
private string lastComInputState = "11111111111111111111111111";
|
||
|
||
private void Awake()
|
||
{
|
||
if (Instance != null && Instance != this)
|
||
{
|
||
Destroy(gameObject);
|
||
return;
|
||
}
|
||
|
||
Instance = this;
|
||
DontDestroyOnLoad(gameObject);
|
||
|
||
Initialize();
|
||
}
|
||
|
||
private void Initialize()
|
||
{
|
||
lastInputTime = Time.unscaledTime;
|
||
|
||
// Подписываемся на все возможные источники ввода
|
||
SubscribeToInputEvents();
|
||
|
||
// Подписываемся на события смены сцены
|
||
UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded;
|
||
|
||
// Подписываемся на изменения настроек
|
||
var settingsManager = SettingsManager.Instance;
|
||
if (settingsManager != null)
|
||
{
|
||
settingsManager.OnSettingsLoaded += LoadSettingsValues;
|
||
settingsManager.OnSettingsSaved += LoadSettingsValues;
|
||
LoadSettingsValues();
|
||
}
|
||
|
||
Debug.Log("[InactivityManager] Инициализирован");
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
UnsubscribeFromInputEvents();
|
||
UnityEngine.SceneManagement.SceneManager.sceneLoaded -= OnSceneLoaded;
|
||
|
||
var settingsManager = SettingsManager.Instance;
|
||
if (settingsManager != null)
|
||
{
|
||
settingsManager.OnSettingsLoaded -= LoadSettingsValues;
|
||
settingsManager.OnSettingsSaved -= LoadSettingsValues;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Загружает значения из настроек
|
||
/// </summary>
|
||
private void LoadSettingsValues()
|
||
{
|
||
var settings = SettingsManager.Instance;
|
||
if (settings == null)
|
||
{
|
||
Debug.LogWarning("[InactivityManager] SettingsManager is null!");
|
||
return;
|
||
}
|
||
|
||
inactivityTimeout = settings.GetInt("gameplay.inactivityTimeout");
|
||
|
||
if (inactivityTimeout == 0)
|
||
{
|
||
Debug.Log("[InactivityManager] Отслеживание бездействия отключено (таймаут = 0)");
|
||
}
|
||
else
|
||
{
|
||
Debug.Log($"[InactivityManager] Таймаут бездействия: {inactivityTimeout}s");
|
||
}
|
||
}
|
||
|
||
private void SubscribeToInputEvents()
|
||
{
|
||
// Подписываемся на события ComSystemManager (кнопки автомата)
|
||
if (ComSystemManager.OnInputReceived != null)
|
||
{
|
||
ComSystemManager.OnInputReceived.AddListener(OnComInput);
|
||
}
|
||
|
||
// InputSystem.onAnyButtonPress - это событие, которое срабатывает каждый раз
|
||
// когда нажимается любая кнопка. Мы просто слушаем его в Update через CheckForAnyInput
|
||
}
|
||
|
||
private void UnsubscribeFromInputEvents()
|
||
{
|
||
if (ComSystemManager.OnInputReceived != null)
|
||
{
|
||
ComSystemManager.OnInputReceived.RemoveListener(OnComInput);
|
||
}
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
// Проверяем, изменилось ли состояние ввода в этом кадре
|
||
CheckForInputStateChanges();
|
||
|
||
// Проверяем таймер бездействия
|
||
if (enableInactivityCheck && inactivityTimeout > 0f && !isInMainMenu && isGameActive)
|
||
{
|
||
float timeSinceLastInput = Time.unscaledTime - lastInputTime;
|
||
|
||
if (showDebugInfo && Time.frameCount % 60 == 0)
|
||
{
|
||
Debug.Log($"[InactivityManager] Время бездействия: {timeSinceLastInput:F1}s / {inactivityTimeout}s");
|
||
}
|
||
|
||
if (timeSinceLastInput >= inactivityTimeout)
|
||
{
|
||
OnInactivityTimeout();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Проверяет, изменилось ли состояние ввода с прошлого кадра.
|
||
/// Зажатая кнопка без изменений считается бездействием.
|
||
/// </summary>
|
||
private void CheckForInputStateChanges()
|
||
{
|
||
if (isInMainMenu || !isGameActive)
|
||
return;
|
||
|
||
InputSnapshot currentSnapshot = BuildInputSnapshot();
|
||
if (!hasInputSnapshot)
|
||
{
|
||
lastInputSnapshot = currentSnapshot;
|
||
hasInputSnapshot = true;
|
||
return;
|
||
}
|
||
|
||
if (!currentSnapshot.Equals(lastInputSnapshot))
|
||
{
|
||
lastInputSnapshot = currentSnapshot;
|
||
ResetInactivityTimer();
|
||
}
|
||
}
|
||
|
||
private void OnComInput(string input)
|
||
{
|
||
lastComInputState = string.IsNullOrWhiteSpace(input)
|
||
? "11111111111111111111111111"
|
||
: input;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Сбрасывает таймер бездействия
|
||
/// </summary>
|
||
public void ResetInactivityTimer()
|
||
{
|
||
lastInputTime = Time.unscaledTime;
|
||
|
||
if (showDebugInfo)
|
||
{
|
||
Debug.Log("[InactivityManager] Таймер сброшен");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Вызывается при достижении таймаута бездействия
|
||
/// </summary>
|
||
private void OnInactivityTimeout()
|
||
{
|
||
Debug.Log("[InactivityManager] Таймаут бездействия! Возврат в главное меню...");
|
||
|
||
enableInactivityCheck = false; // Отключаем проверку до следующей игры
|
||
|
||
// Останавливаем игру
|
||
if (GameManager.Instance != null)
|
||
{
|
||
GameManager.Instance.SetPause(false);
|
||
GameManager.Instance.LoadScene("MainMenu", false);
|
||
}
|
||
|
||
ResetInactivityTimer();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Обработчик загрузки сцены
|
||
/// </summary>
|
||
private void OnSceneLoaded(UnityEngine.SceneManagement.Scene scene, UnityEngine.SceneManagement.LoadSceneMode mode)
|
||
{
|
||
isInMainMenu = scene.name == "MainMenu";
|
||
|
||
if (isInMainMenu)
|
||
{
|
||
enableInactivityCheck = false;
|
||
isGameActive = false;
|
||
hasInputSnapshot = false;
|
||
Debug.Log("[InactivityManager] Главное меню загружено - проверка отключена");
|
||
}
|
||
|
||
ResetInactivityTimer();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Включает проверку бездействия (вызывается при старте игры)
|
||
/// </summary>
|
||
public void EnableInactivityCheck()
|
||
{
|
||
if (inactivityTimeout <= 0f)
|
||
{
|
||
enableInactivityCheck = false;
|
||
isGameActive = false;
|
||
ResetInactivityTimer();
|
||
Debug.Log("[InactivityManager] Проверка бездействия не включена: таймаут = 0");
|
||
return;
|
||
}
|
||
|
||
enableInactivityCheck = true;
|
||
isGameActive = true;
|
||
lastComInputState = GetCurrentComInputState();
|
||
lastInputSnapshot = BuildInputSnapshot();
|
||
hasInputSnapshot = true;
|
||
ResetInactivityTimer();
|
||
Debug.Log("[InactivityManager] Проверка бездействия включена");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отключает проверку бездействия
|
||
/// </summary>
|
||
public void DisableInactivityCheck()
|
||
{
|
||
enableInactivityCheck = false;
|
||
isGameActive = false;
|
||
hasInputSnapshot = false;
|
||
Debug.Log("[InactivityManager] Проверка бездействия отключена");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Устанавливает время таймаута бездействия.
|
||
/// Если установлено 0, отслеживание бездействия отключается.
|
||
/// </summary>
|
||
public void SetInactivityTimeout(float timeoutSeconds)
|
||
{
|
||
inactivityTimeout = Mathf.Max(0f, timeoutSeconds); // 0 = отключено, минимум 0
|
||
|
||
if (inactivityTimeout == 0f)
|
||
{
|
||
enableInactivityCheck = false;
|
||
isGameActive = false;
|
||
hasInputSnapshot = false;
|
||
Debug.Log("[InactivityManager] Отслеживание бездействия отключено (таймаут = 0)");
|
||
}
|
||
else
|
||
{
|
||
Debug.Log($"[InactivityManager] Таймаут установлен: {inactivityTimeout}s");
|
||
}
|
||
}
|
||
|
||
private InputSnapshot BuildInputSnapshot()
|
||
{
|
||
return new InputSnapshot(
|
||
BuildKeyboardStateHash(),
|
||
BuildMouseStateHash(),
|
||
BuildGamepadStateHash(),
|
||
BuildComStateHash());
|
||
}
|
||
|
||
private static int BuildKeyboardStateHash()
|
||
{
|
||
Keyboard keyboard = Keyboard.current;
|
||
if (keyboard == null)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
int hash = 17;
|
||
foreach (KeyControl key in keyboard.allKeys)
|
||
{
|
||
if (key.isPressed)
|
||
{
|
||
hash = hash * 31 + (int)key.keyCode;
|
||
}
|
||
}
|
||
return hash;
|
||
}
|
||
|
||
private static int BuildMouseStateHash()
|
||
{
|
||
Mouse mouse = Mouse.current;
|
||
if (mouse == null)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
Vector2 position = mouse.position.ReadValue();
|
||
Vector2 scroll = mouse.scroll.ReadValue();
|
||
|
||
int hash = 17;
|
||
hash = hash * 31 + (mouse.leftButton.isPressed ? 1 : 0);
|
||
hash = hash * 31 + (mouse.rightButton.isPressed ? 1 : 0);
|
||
hash = hash * 31 + (mouse.middleButton.isPressed ? 1 : 0);
|
||
hash = hash * 31 + Mathf.RoundToInt(position.x);
|
||
hash = hash * 31 + Mathf.RoundToInt(position.y);
|
||
hash = hash * 31 + Mathf.RoundToInt(scroll.x);
|
||
hash = hash * 31 + Mathf.RoundToInt(scroll.y);
|
||
return hash;
|
||
}
|
||
|
||
private static int BuildGamepadStateHash()
|
||
{
|
||
Gamepad gamepad = Gamepad.current;
|
||
if (gamepad == null)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
int hash = 17;
|
||
foreach (InputControl control in gamepad.allControls)
|
||
{
|
||
if (control is ButtonControl button && button.isPressed)
|
||
{
|
||
hash = hash * 31 + control.name.GetHashCode();
|
||
}
|
||
}
|
||
|
||
Vector2 leftStick = QuantizeStick(gamepad.leftStick.ReadValue());
|
||
Vector2 rightStick = QuantizeStick(gamepad.rightStick.ReadValue());
|
||
float leftTrigger = QuantizeAxis(gamepad.leftTrigger.ReadValue());
|
||
float rightTrigger = QuantizeAxis(gamepad.rightTrigger.ReadValue());
|
||
|
||
hash = hash * 31 + Mathf.RoundToInt(leftStick.x * 10f);
|
||
hash = hash * 31 + Mathf.RoundToInt(leftStick.y * 10f);
|
||
hash = hash * 31 + Mathf.RoundToInt(rightStick.x * 10f);
|
||
hash = hash * 31 + Mathf.RoundToInt(rightStick.y * 10f);
|
||
hash = hash * 31 + Mathf.RoundToInt(leftTrigger * 10f);
|
||
hash = hash * 31 + Mathf.RoundToInt(rightTrigger * 10f);
|
||
return hash;
|
||
}
|
||
|
||
private int BuildComStateHash()
|
||
{
|
||
string currentState = GetCurrentComInputState();
|
||
if (string.IsNullOrWhiteSpace(currentState))
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
int hash = 17;
|
||
for (int i = 0; i < currentState.Length; i++)
|
||
{
|
||
hash = hash * 31 + currentState[i];
|
||
}
|
||
|
||
return hash;
|
||
}
|
||
|
||
private string GetCurrentComInputState()
|
||
{
|
||
string currentState = ComSystemManager.GetLastInputState();
|
||
if (!string.IsNullOrWhiteSpace(currentState))
|
||
{
|
||
lastComInputState = currentState;
|
||
}
|
||
|
||
return string.IsNullOrWhiteSpace(lastComInputState)
|
||
? "11111111111111111111111111"
|
||
: lastComInputState;
|
||
}
|
||
|
||
private static Vector2 QuantizeStick(Vector2 value)
|
||
{
|
||
return new Vector2(QuantizeAxis(value.x), QuantizeAxis(value.y));
|
||
}
|
||
|
||
private static float QuantizeAxis(float value)
|
||
{
|
||
if (Mathf.Abs(value) < 0.15f)
|
||
return 0f;
|
||
|
||
return Mathf.Round(value * 10f) / 10f;
|
||
}
|
||
|
||
private readonly struct InputSnapshot : System.IEquatable<InputSnapshot>
|
||
{
|
||
private readonly int _keyboardHash;
|
||
private readonly int _mouseHash;
|
||
private readonly int _gamepadHash;
|
||
private readonly int _comHash;
|
||
|
||
public InputSnapshot(int keyboardHash, int mouseHash, int gamepadHash, int comHash)
|
||
{
|
||
_keyboardHash = keyboardHash;
|
||
_mouseHash = mouseHash;
|
||
_gamepadHash = gamepadHash;
|
||
_comHash = comHash;
|
||
}
|
||
|
||
public bool Equals(InputSnapshot other)
|
||
{
|
||
return _keyboardHash == other._keyboardHash
|
||
&& _mouseHash == other._mouseHash
|
||
&& _gamepadHash == other._gamepadHash
|
||
&& _comHash == other._comHash;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получает текущее время таймаута
|
||
/// </summary>
|
||
public float GetInactivityTimeout() => inactivityTimeout;
|
||
|
||
/// <summary>
|
||
/// Получает время с момента последнего ввода
|
||
/// </summary>
|
||
public float GetTimeSinceLastInput() => Time.unscaledTime - lastInputTime;
|
||
}
|
||
}
|
||
|