Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- доработан 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'ы и добавлены сопутствующие проектные артефакты, которые сейчас используются обновлённой конфигурацией проекта
303 lines
10 KiB
C#
303 lines
10 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.EventSystems;
|
||
using UnityEngine.InputSystem;
|
||
using UnityEngine.UI;
|
||
|
||
using RR.Common.Controls;
|
||
using RR.Common.Managers;
|
||
using RR.Game2D.Core.Managers;
|
||
using ControlsBoxStatus = RR.Common.Controls.BoxStatus;
|
||
|
||
namespace RR.Common.UI
|
||
{
|
||
/// <summary>
|
||
/// Модальное окно витрины с выбором ячейки/полки для выдачи приза.
|
||
/// Отображает сетку кнопок-ячеек. Навигация — через Input System (Navigate).
|
||
/// </summary>
|
||
public class PrizeStandModal : Modal
|
||
{
|
||
/// <summary>Текущий экземпляр на сцене. Регистрируется автоматически.</summary>
|
||
public static PrizeStandModal Current { get; private set; }
|
||
|
||
[Header("Контент")]
|
||
[Tooltip("Контейнер для кнопок ячеек (например, GridLayoutGroup).")]
|
||
[SerializeField] private Transform boxButtonsContainer;
|
||
|
||
[Tooltip("Префаб кнопки ячейки. Должен содержать Button и TMP_Text.")]
|
||
[SerializeField] private GameObject boxButtonPrefab;
|
||
|
||
[Header("UI")]
|
||
[Tooltip("Текст-заголовок окна (опционально).")]
|
||
[SerializeField] private TMP_Text titleText;
|
||
|
||
[Tooltip("Текст заголовка по умолчанию.")]
|
||
[SerializeField] private string defaultTitle = "Выберите ячейку";
|
||
[Tooltip("Текст с количеством очков игрока.")]
|
||
[SerializeField] private TMP_Text scoreText;
|
||
[Tooltip("Формат текста очков. Используйте {0} для подстановки значения.")]
|
||
[SerializeField] private string scoreTextFormat = "Очки за игру: {0}";
|
||
|
||
[Header("Навигация")]
|
||
[Tooltip("InputAction для навигации (Navigate из Input System).")]
|
||
[SerializeField] private InputActionReference navigateAction;
|
||
|
||
/// <summary>
|
||
/// Вызывается при выборе ячейки. Параметр — номер ячейки (1-based).
|
||
/// Если игрок закрыл окно без выбора, вызывается с 0.
|
||
/// </summary>
|
||
public event Action<int> OnBoxSelected;
|
||
|
||
private readonly List<GameObject> _createdButtons = new();
|
||
private bool _selectionMade;
|
||
private PrizeStandController _standController;
|
||
private int _lastHighlightedBoxNumber;
|
||
|
||
// ────────────── Lifecycle ──────────────
|
||
|
||
protected override void Awake()
|
||
{
|
||
Current = this;
|
||
base.Awake();
|
||
|
||
// При закрытии без выбора — уведомляем с 0
|
||
OnClosed += HandleClosedWithoutSelection;
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
if (Current == this) Current = null;
|
||
OnClosed -= HandleClosedWithoutSelection;
|
||
}
|
||
|
||
// ────────────── Публичный API ──────────────
|
||
|
||
/// <summary>
|
||
/// Показывает окно с указанным количеством ячеек.
|
||
/// </summary>
|
||
/// <param name="boxCount">Количество ячеек для отображения.</param>
|
||
public void Show(
|
||
IReadOnlyList<PrizeStandBoxAvailability> boxes,
|
||
bool isConnected,
|
||
int connectedBoxesCount,
|
||
float gameTime,
|
||
int finalScore,
|
||
bool showFinalScore)
|
||
{
|
||
_selectionMade = false;
|
||
_lastHighlightedBoxNumber = 0;
|
||
_standController = isConnected ? ComSystemManager.GetPrizeStandController() : null;
|
||
|
||
BuildButtons(boxes);
|
||
|
||
if (titleText != null)
|
||
{
|
||
titleText.text = defaultTitle;
|
||
}
|
||
|
||
if (scoreText != null)
|
||
{
|
||
scoreText.text = BuildScoreText(finalScore);
|
||
}
|
||
|
||
Open();
|
||
GameManager.Instance?.SetPause(true);
|
||
}
|
||
|
||
// ────────────── Внутреннее ──────────────
|
||
|
||
private void BuildButtons(IReadOnlyList<PrizeStandBoxAvailability> boxes)
|
||
{
|
||
ClearButtons();
|
||
|
||
if (boxButtonsContainer == null || boxButtonPrefab == null)
|
||
{
|
||
Debug.LogError("[PrizeStandModal] Контейнер или префаб кнопки не назначены.");
|
||
return;
|
||
}
|
||
|
||
if (boxes == null)
|
||
return;
|
||
|
||
for (int i = 0; i < boxes.Count; i++)
|
||
{
|
||
PrizeStandBoxAvailability box = boxes[i];
|
||
GameObject btnObj = Instantiate(boxButtonPrefab, boxButtonsContainer);
|
||
btnObj.name = $"BoxButton_{box.BoxNumber}";
|
||
|
||
PrizeStandButtonView buttonView = btnObj.GetComponent<PrizeStandButtonView>();
|
||
if (buttonView == null)
|
||
{
|
||
buttonView = btnObj.AddComponent<PrizeStandButtonView>();
|
||
}
|
||
|
||
buttonView.Bind(box.BoxNumber, box.requiredScore);
|
||
|
||
// Настраиваем кнопку
|
||
Button btn = btnObj.GetComponent<Button>();
|
||
if (btn != null)
|
||
{
|
||
int boxNumber = box.BoxNumber;
|
||
btn.interactable = box.IsAvailable;
|
||
btn.onClick.AddListener(() => OnBoxButtonClicked(boxNumber));
|
||
|
||
PrizeStandButtonSelectionRelay relay = btnObj.GetComponent<PrizeStandButtonSelectionRelay>();
|
||
if (relay == null)
|
||
{
|
||
relay = btnObj.AddComponent<PrizeStandButtonSelectionRelay>();
|
||
}
|
||
|
||
relay.Initialize(boxNumber, OnBoxButtonFocused);
|
||
}
|
||
|
||
_createdButtons.Add(btnObj);
|
||
}
|
||
|
||
// Устанавливаем первый элемент для фокуса
|
||
for (int i = 0; i < _createdButtons.Count; i++)
|
||
{
|
||
Button firstBtn = _createdButtons[i].GetComponent<Button>();
|
||
if (firstBtn != null && firstBtn.interactable)
|
||
{
|
||
SetFirstSelected(firstBtn);
|
||
OnBoxButtonFocused(boxes[i].BoxNumber);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void OnBoxButtonClicked(int boxNumber)
|
||
{
|
||
_selectionMade = true;
|
||
|
||
Debug.Log($"[PrizeStandModal] Выбрана ячейка #{boxNumber}.");
|
||
|
||
Close();
|
||
GameManager.Instance?.SetPause(false);
|
||
|
||
OnBoxSelected?.Invoke(boxNumber);
|
||
}
|
||
|
||
private void HandleClosedWithoutSelection()
|
||
{
|
||
if (!_selectionMade)
|
||
{
|
||
Debug.Log("[PrizeStandModal] Окно закрыто без выбора.");
|
||
GameManager.Instance?.SetPause(false);
|
||
OnBoxSelected?.Invoke(0);
|
||
}
|
||
}
|
||
|
||
private void ClearButtons()
|
||
{
|
||
foreach (GameObject obj in _createdButtons)
|
||
{
|
||
if (obj != null) Destroy(obj);
|
||
}
|
||
_createdButtons.Clear();
|
||
}
|
||
|
||
private void OnBoxButtonFocused(int boxNumber)
|
||
{
|
||
if (_standController == null || boxNumber <= 0 || _lastHighlightedBoxNumber == boxNumber)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (_standController.HighlightBoxLighting(boxNumber))
|
||
{
|
||
_lastHighlightedBoxNumber = boxNumber;
|
||
}
|
||
}
|
||
|
||
private void RestoreIdleLighting()
|
||
{
|
||
if (_standController == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_standController.SetIdleLighting();
|
||
_lastHighlightedBoxNumber = 0;
|
||
}
|
||
|
||
protected override void OnDisable()
|
||
{
|
||
base.OnDisable();
|
||
RestoreIdleLighting();
|
||
ClearButtons();
|
||
if (scoreText != null)
|
||
scoreText.text = string.Empty;
|
||
GameManager.Instance?.SetPause(false);
|
||
_standController = null;
|
||
}
|
||
|
||
private string BuildScoreText(int finalScore)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(scoreTextFormat))
|
||
{
|
||
return finalScore.ToString();
|
||
}
|
||
|
||
try
|
||
{
|
||
return string.Format(scoreTextFormat, finalScore);
|
||
}
|
||
catch (FormatException)
|
||
{
|
||
return $"{scoreTextFormat} {finalScore}";
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
[DisallowMultipleComponent]
|
||
public sealed class PrizeStandButtonSelectionRelay : MonoBehaviour, ISelectHandler, IPointerEnterHandler
|
||
{
|
||
private Action<int> _onFocused;
|
||
private int _boxNumber;
|
||
|
||
public void Initialize(int boxNumber, Action<int> onFocused)
|
||
{
|
||
_boxNumber = boxNumber;
|
||
_onFocused = onFocused;
|
||
}
|
||
|
||
public void OnSelect(BaseEventData eventData)
|
||
{
|
||
_onFocused?.Invoke(_boxNumber);
|
||
}
|
||
|
||
public void OnPointerEnter(PointerEventData eventData)
|
||
{
|
||
_onFocused?.Invoke(_boxNumber);
|
||
}
|
||
}
|
||
|
||
public readonly struct PrizeStandBoxAvailability
|
||
{
|
||
public readonly int BoxNumber;
|
||
public readonly int requiredScore;
|
||
public readonly bool IsUnlocked;
|
||
public readonly bool IsConnected;
|
||
public readonly ControlsBoxStatus Status;
|
||
public bool IsReadyToOpen => PrizeStandController.IsReadyToOpenStatus(Status);
|
||
public bool IsAvailable => IsUnlocked && IsConnected && IsReadyToOpen;
|
||
public bool IsOpened => Status == ControlsBoxStatus.Open || Status == ControlsBoxStatus.OPENED;
|
||
public bool IsEmpty => Status == ControlsBoxStatus.Empty;
|
||
public bool HasError => Status == ControlsBoxStatus.OPEN_ERROR || Status == ControlsBoxStatus.Error || Status == ControlsBoxStatus.ParsingError;
|
||
|
||
public PrizeStandBoxAvailability(int boxNumber, int requiredScore, bool isUnlocked, bool isConnected, ControlsBoxStatus status)
|
||
{
|
||
BoxNumber = boxNumber;
|
||
this.requiredScore = requiredScore;
|
||
IsUnlocked = isUnlocked;
|
||
IsConnected = isConnected;
|
||
Status = status;
|
||
}
|
||
}
|
||
}
|
||
|