Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- добавлена новая система призов: PrizeBase, PrizeType, PrizeDatabase, LevelPrizeConfig и реализации для hopper, QR-кода, отсутствия приза и стойки с призом - добавлена конфигурация призов уровней через `prizes.json` и связанная логика загрузки/обработки в менеджерах игры - расширен UI главного меню и настроек уровней: добавлены LevelSettingsModal, LevelPrizeSettingsModal, QRCodeModal и PrizeStandModal - обновлена навигация модальных окон и связанный UI-код: ModalManager, MainMenu, RulesModal, PauseMenu, PrizeErrorDisplay, UIRowPointer и WinBannerController - доработаны настройки и генерация UI-элементов: обновлены SettingsDatabase, SettingsUIGenerator, SettingUIItem, ValueSelectorRow и editor drawer для настроек - обновлены и добавлены UI-prefab'ы для кнопок, строк настроек, панели групп и кнопки настроек уровня - обновлены сцены `MainMenu` и `Level UI` под новый флоу настройки уровней, выбора призов и отображения модалок - добавлены файлы лаунчера в `StreamingAssets`, включая `RRLauncher.exe`, служебные meta-файлы и документацию - улучшено логирование через FileLogger и удален устаревший код лицензирования - удалены больше неиспользуемые файлы: `Licensing.cs`, `SteppedSlider.cs` и recovery-сцены
415 lines
15 KiB
C#
415 lines
15 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.EventSystems;
|
||
using UnityEngine.UI;
|
||
|
||
namespace RR.Common.UI
|
||
{
|
||
/// <summary>
|
||
/// Центральный менеджер модальных окон.
|
||
/// Управляет стеком открытых модальных окон и фокусом между ними.
|
||
/// </summary>
|
||
public class ModalManager : MonoBehaviour
|
||
{
|
||
private static ModalManager _instance;
|
||
private static bool _applicationQuitting;
|
||
|
||
/// <summary>
|
||
/// Singleton-инстанс менеджера модальных окон.
|
||
/// Создаётся автоматически при первом обращении.
|
||
/// </summary>
|
||
public static ModalManager Instance
|
||
{
|
||
get
|
||
{
|
||
if (_applicationQuitting) return null;
|
||
if (_instance) return _instance;
|
||
|
||
// Ищем существующий в сцене
|
||
_instance = FindAnyObjectByType<ModalManager>();
|
||
if (_instance) return _instance;
|
||
|
||
// Создаём новый
|
||
GameObject go = new GameObject("[ModalManager]");
|
||
_instance = go.AddComponent<ModalManager>();
|
||
DontDestroyOnLoad(go);
|
||
return _instance;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Информация о состоянии UI до открытия модального окна.
|
||
/// </summary>
|
||
private class ModalStackEntry
|
||
{
|
||
public Modal Modal;
|
||
public GameObject PreviousSelected;
|
||
public List<CanvasGroup> DisabledGroups = new();
|
||
}
|
||
|
||
private readonly Stack<ModalStackEntry> _stack = new();
|
||
|
||
/// <summary>
|
||
/// Событие, вызываемое при открытии любого модального окна.
|
||
/// </summary>
|
||
public event Action<Modal> OnModalOpened;
|
||
|
||
/// <summary>
|
||
/// Событие, вызываемое при закрытии любого модального окна.
|
||
/// </summary>
|
||
public event Action<Modal> OnModalClosed;
|
||
|
||
/// <summary>
|
||
/// Текущее активное модальное окно (на вершине стека).
|
||
/// </summary>
|
||
public Modal CurrentModal => _stack.Count > 0 ? _stack.Peek().Modal : null;
|
||
|
||
/// <summary>
|
||
/// Есть ли открытые модальные окна.
|
||
/// </summary>
|
||
public bool HasOpenModals => _stack.Count > 0;
|
||
|
||
/// <summary>
|
||
/// Количество открытых модальных окон.
|
||
/// </summary>
|
||
public int OpenModalCount => _stack.Count;
|
||
|
||
private void Awake()
|
||
{
|
||
if (_instance && _instance != this)
|
||
{
|
||
Destroy(gameObject);
|
||
return;
|
||
}
|
||
|
||
_instance = this;
|
||
DontDestroyOnLoad(gameObject);
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
if (_instance == this)
|
||
{
|
||
_instance = null;
|
||
}
|
||
}
|
||
|
||
private void OnApplicationQuit()
|
||
{
|
||
_applicationQuitting = true;
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
/// <summary>
|
||
/// Сбрасываем статический флаг при выходе из Play Mode в редакторе,
|
||
/// иначе при повторном запуске Instance всегда будет null.
|
||
/// </summary>
|
||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||
private static void ResetStatics()
|
||
{
|
||
_applicationQuitting = false;
|
||
_instance = null;
|
||
}
|
||
#endif
|
||
|
||
private void Update()
|
||
{
|
||
// Следим за тем, чтобы фокус оставался внутри текущего модального окна
|
||
if (_stack.Count == 0) return;
|
||
|
||
ModalStackEntry top = _stack.Peek();
|
||
if (!top.Modal || !top.Modal.gameObject.activeInHierarchy) return;
|
||
if (!top.Modal.EnforceFocus) return;
|
||
|
||
EnsureFocusInModal(top.Modal);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Регистрирует модальное окно в стеке (вызывается из Modal.OnEnable).
|
||
/// </summary>
|
||
internal void Push(Modal modal)
|
||
{
|
||
if (modal == null) return;
|
||
|
||
// Проверяем, не зарегистрировано ли уже это окно
|
||
foreach (ModalStackEntry entry in _stack)
|
||
{
|
||
if (entry.Modal == modal) return;
|
||
}
|
||
|
||
ModalStackEntry newEntry = new ModalStackEntry
|
||
{
|
||
Modal = modal,
|
||
PreviousSelected = EventSystem.current ? EventSystem.current.currentSelectedGameObject : null
|
||
};
|
||
|
||
// Блокируем интерактивность всего, что не принадлежит этому модальному окну
|
||
if (modal.BlockBackground)
|
||
{
|
||
BlockBackgroundInteraction(modal, newEntry);
|
||
}
|
||
|
||
_stack.Push(newEntry);
|
||
|
||
// Устанавливаем фокус на первый элемент модального окна
|
||
SetInitialFocus(modal);
|
||
|
||
OnModalOpened?.Invoke(modal);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Удаляет модальное окно из стека (вызывается из Modal.OnDisable).
|
||
/// </summary>
|
||
internal void Pop(Modal modal)
|
||
{
|
||
if (modal == null) return;
|
||
if (_stack.Count == 0) return;
|
||
|
||
// Ищем и удаляем модальное окно из стека
|
||
Stack<ModalStackEntry> temp = new Stack<ModalStackEntry>();
|
||
ModalStackEntry foundEntry = null;
|
||
|
||
while (_stack.Count > 0)
|
||
{
|
||
ModalStackEntry entry = _stack.Pop();
|
||
if (entry.Modal == modal)
|
||
{
|
||
foundEntry = entry;
|
||
break;
|
||
}
|
||
temp.Push(entry);
|
||
}
|
||
|
||
// Возвращаем остальные в стек
|
||
while (temp.Count > 0)
|
||
{
|
||
_stack.Push(temp.Pop());
|
||
}
|
||
|
||
if (foundEntry == null) return;
|
||
|
||
// Восстанавливаем интерактивность заблокированных групп
|
||
RestoreBackgroundInteraction(foundEntry);
|
||
|
||
// Восстанавливаем фокус
|
||
RestoreFocus(foundEntry);
|
||
|
||
OnModalClosed?.Invoke(modal);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Закрывает все открытые модальные окна.
|
||
/// </summary>
|
||
public void CloseAll()
|
||
{
|
||
while (_stack.Count > 0)
|
||
{
|
||
ModalStackEntry entry = _stack.Peek();
|
||
if (entry.Modal != null)
|
||
{
|
||
entry.Modal.Close();
|
||
}
|
||
else
|
||
{
|
||
_stack.Pop();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Закрывает текущее (верхнее) модальное окно.
|
||
/// </summary>
|
||
public void CloseCurrent()
|
||
{
|
||
if (_stack.Count == 0) return;
|
||
|
||
Modal current = _stack.Peek().Modal;
|
||
if (current != null)
|
||
{
|
||
current.Close();
|
||
}
|
||
}
|
||
|
||
private void BlockBackgroundInteraction(Modal modal, ModalStackEntry entry)
|
||
{
|
||
// Находим все CanvasGroup, которые не принадлежат модальному окну
|
||
CanvasGroup[] allGroups = FindObjectsByType<CanvasGroup>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||
|
||
foreach (CanvasGroup group in allGroups)
|
||
{
|
||
if (!group || !group.interactable) continue;
|
||
if (IsUnderModal(group.transform, modal)) continue;
|
||
|
||
// Не блокируем родительские CanvasGroup модального окна
|
||
if (IsParentOfModal(group.transform, modal)) continue;
|
||
|
||
// Не блокируем CanvasGroup, если модальное окно является его потомком
|
||
if (modal.transform.IsChildOf(group.transform)) continue;
|
||
|
||
// Проверяем, не принадлежит ли эта группа другому модальному окну в стеке
|
||
bool belongsToOtherModal = false;
|
||
foreach (ModalStackEntry stackEntry in _stack)
|
||
{
|
||
if (stackEntry.Modal && IsUnderModal(group.transform, stackEntry.Modal))
|
||
{
|
||
belongsToOtherModal = true;
|
||
break;
|
||
}
|
||
}
|
||
if (belongsToOtherModal) continue;
|
||
|
||
group.interactable = false;
|
||
entry.DisabledGroups.Add(group);
|
||
}
|
||
|
||
// Если у модального окна нет CanvasGroup-родителя, блокируем все Selectable напрямую
|
||
if (entry.DisabledGroups.Count == 0)
|
||
{
|
||
BlockSelectablesOutsideModal(modal, entry);
|
||
}
|
||
}
|
||
|
||
private void BlockSelectablesOutsideModal(Modal modal, ModalStackEntry entry)
|
||
{
|
||
Selectable[] all = FindObjectsByType<Selectable>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||
|
||
foreach (Selectable s in all)
|
||
{
|
||
if (!s || !s.interactable) continue;
|
||
if (IsUnderModal(s.transform, modal)) continue;
|
||
|
||
// Добавляем временную CanvasGroup для блокировки
|
||
CanvasGroup cg = s.GetComponent<CanvasGroup>();
|
||
if (!cg)
|
||
{
|
||
cg = s.gameObject.AddComponent<CanvasGroup>();
|
||
}
|
||
|
||
if (cg.interactable)
|
||
{
|
||
cg.interactable = false;
|
||
entry.DisabledGroups.Add(cg);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void RestoreBackgroundInteraction(ModalStackEntry entry)
|
||
{
|
||
foreach (CanvasGroup group in entry.DisabledGroups)
|
||
{
|
||
if (group)
|
||
{
|
||
group.interactable = true;
|
||
}
|
||
}
|
||
entry.DisabledGroups.Clear();
|
||
}
|
||
|
||
private void SetInitialFocus(Modal modal)
|
||
{
|
||
if (!EventSystem.current) return;
|
||
|
||
GameObject toSelect = null;
|
||
|
||
if (modal.FirstSelected && modal.FirstSelected.gameObject.activeInHierarchy)
|
||
{
|
||
toSelect = modal.FirstSelected.gameObject;
|
||
}
|
||
else
|
||
{
|
||
Selectable any = modal.GetComponentInChildren<Selectable>(includeInactive: false);
|
||
if (any) toSelect = any.gameObject;
|
||
}
|
||
|
||
if (!toSelect) return;
|
||
|
||
// Сбрасываем и устанавливаем фокус
|
||
EventSystem.current.SetSelectedGameObject(null);
|
||
EventSystem.current.SetSelectedGameObject(toSelect);
|
||
}
|
||
|
||
private void RestoreFocus(ModalStackEntry entry)
|
||
{
|
||
if (!EventSystem.current) return;
|
||
|
||
// Если есть другие модальные окна в стеке, фокусируемся на верхнем
|
||
if (_stack.Count > 0)
|
||
{
|
||
Modal topModal = _stack.Peek().Modal;
|
||
if (topModal != null)
|
||
{
|
||
SetInitialFocus(topModal);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Иначе восстанавливаем предыдущий фокус
|
||
GameObject toSelect = null;
|
||
|
||
if (entry.PreviousSelected && entry.PreviousSelected.activeInHierarchy)
|
||
{
|
||
Selectable selectable = entry.PreviousSelected.GetComponent<Selectable>();
|
||
if (selectable && selectable.interactable)
|
||
{
|
||
toSelect = entry.PreviousSelected;
|
||
}
|
||
}
|
||
|
||
// Если предыдущий элемент недоступен, ищем любой доступный
|
||
if (!toSelect)
|
||
{
|
||
Selectable anySelectable = FindAnyObjectByType<Selectable>(FindObjectsInactive.Exclude);
|
||
if (anySelectable && anySelectable.interactable)
|
||
{
|
||
toSelect = anySelectable.gameObject;
|
||
}
|
||
}
|
||
|
||
EventSystem.current.SetSelectedGameObject(toSelect);
|
||
}
|
||
|
||
private void EnsureFocusInModal(Modal modal)
|
||
{
|
||
if (!EventSystem.current) return;
|
||
|
||
GameObject current = EventSystem.current.currentSelectedGameObject;
|
||
|
||
// Если текущий выделенный элемент находится внутри модального окна, всё ок
|
||
if (current && IsUnderModal(current.transform, modal))
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Иначе возвращаем фокус внутрь модального окна
|
||
SetInitialFocus(modal);
|
||
}
|
||
|
||
private bool IsUnderModal(Transform t, Modal modal)
|
||
{
|
||
if (!modal) return false;
|
||
Transform root = modal.transform;
|
||
for (Transform cur = t; cur; cur = cur.parent)
|
||
{
|
||
if (cur == root) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Проверяет, является ли указанный Transform родителем модального окна.
|
||
/// </summary>
|
||
private bool IsParentOfModal(Transform t, Modal modal)
|
||
{
|
||
if (!modal || !t) return false;
|
||
Transform modalTransform = modal.transform;
|
||
for (Transform cur = modalTransform.parent; cur; cur = cur.parent)
|
||
{
|
||
if (cur == t) return true;
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|