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
231 lines
8.0 KiB
C#
231 lines
8.0 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.InputSystem;
|
||
using UnityEngine.UI;
|
||
|
||
using RR.Game2D.Core.Managers;
|
||
|
||
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;
|
||
|
||
// ────────────── 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;
|
||
|
||
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}";
|
||
|
||
// Настраиваем текст
|
||
TMP_Text label = btnObj.GetComponentInChildren<TMP_Text>();
|
||
if (label != null)
|
||
{
|
||
label.text = box.IsConnected
|
||
? $"{box.BoxNumber}\n{box.requiredScore}"
|
||
: $"{box.BoxNumber}\nOFF";
|
||
}
|
||
|
||
// Настраиваем кнопку
|
||
Button btn = btnObj.GetComponent<Button>();
|
||
if (btn != null)
|
||
{
|
||
int boxNumber = box.BoxNumber;
|
||
btn.interactable = box.IsAvailable;
|
||
btn.onClick.AddListener(() => OnBoxButtonClicked(boxNumber));
|
||
}
|
||
|
||
_createdButtons.Add(btnObj);
|
||
}
|
||
|
||
// Устанавливаем первый элемент для фокуса
|
||
for (int i = 0; i < _createdButtons.Count; i++)
|
||
{
|
||
Button firstBtn = _createdButtons[i].GetComponent<Button>();
|
||
if (firstBtn != null && firstBtn.interactable)
|
||
{
|
||
SetFirstSelected(firstBtn);
|
||
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();
|
||
}
|
||
|
||
protected override void OnDisable()
|
||
{
|
||
base.OnDisable();
|
||
ClearButtons();
|
||
if (scoreText != null)
|
||
scoreText.text = string.Empty;
|
||
GameManager.Instance?.SetPause(false);
|
||
}
|
||
|
||
private string BuildScoreText(int finalScore)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(scoreTextFormat))
|
||
{
|
||
return finalScore.ToString();
|
||
}
|
||
|
||
try
|
||
{
|
||
return string.Format(scoreTextFormat, finalScore);
|
||
}
|
||
catch (FormatException)
|
||
{
|
||
return $"{scoreTextFormat} {finalScore}";
|
||
}
|
||
}
|
||
}
|
||
|
||
public readonly struct PrizeStandBoxAvailability
|
||
{
|
||
public readonly int BoxNumber;
|
||
public readonly int requiredScore;
|
||
public readonly bool IsUnlocked;
|
||
public readonly bool IsConnected;
|
||
public bool IsAvailable => IsUnlocked && IsConnected;
|
||
|
||
public PrizeStandBoxAvailability(int boxNumber, int requiredScore, bool isUnlocked, bool isConnected)
|
||
{
|
||
BoxNumber = boxNumber;
|
||
this.requiredScore = requiredScore;
|
||
IsUnlocked = isUnlocked;
|
||
IsConnected = isConnected;
|
||
}
|
||
}
|
||
}
|
||
|