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-сцены
359 lines
13 KiB
C#
359 lines
13 KiB
C#
using System;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.EventSystems;
|
||
using UnityEngine.InputSystem;
|
||
using UnityEngine.Serialization;
|
||
using UnityEngine.UI;
|
||
using RR.Common.Managers;
|
||
using RR.Common.Prizes;
|
||
|
||
namespace RR.Common.UI
|
||
{
|
||
/// <summary>
|
||
/// Модальное окно настройки приза за конкретный уровень.
|
||
/// Полностью управляется с клавиатуры/геймпада через Input System.
|
||
/// Навигация: вверх/вниз — переключение строк (зацикленно), влево/вправо — изменение значения.
|
||
/// Настройки сохраняются автоматически при закрытии окна.
|
||
/// </summary>
|
||
public class LevelPrizeSettingsModal : Modal
|
||
{
|
||
[Header("UI — Общее")]
|
||
[SerializeField] private TMP_Text titleText;
|
||
[SerializeField] private TMP_Text prizeTypeLabel;
|
||
|
||
[Header("UI — Параметры хоппера")]
|
||
[SerializeField] private GameObject hopperParamsGroup;
|
||
[SerializeField] private TMP_Text hopperCapsuleCountLabel;
|
||
|
||
[Header("UI — Параметры витрины")]
|
||
[FormerlySerializedAs("showcaseParamsGroup")]
|
||
[SerializeField] private GameObject prizeStandParamsGroup;
|
||
[FormerlySerializedAs("showcaseBoxCountLabel")]
|
||
[SerializeField] private TMP_Text prizeStandBoxCountLabel;
|
||
|
||
[Header("Строки (RectTransform)")]
|
||
[Tooltip("RectTransform строки выбора типа приза.")]
|
||
[SerializeField] private RectTransform prizeTypeRow;
|
||
|
||
[Header("Указатель")]
|
||
[Tooltip("Компонент UIRowPointer для визуальной стрелки-указателя (опционально).")]
|
||
[SerializeField] private UIRowPointer rowPointer;
|
||
|
||
[Header("Кнопка закрытия")]
|
||
[Tooltip("Кнопка закрытия окна. При фокусе на ней указатель прячется, но Submit срабатывает.")]
|
||
[SerializeField] private Button closeModalButton;
|
||
|
||
[Header("Навигация (Input System)")]
|
||
[Tooltip("InputAction навигации (Navigate, тип Value/Vector2)")]
|
||
[SerializeField] private InputActionReference navigateAction;
|
||
|
||
[Tooltip("Порог срабатывания по осям (0..1)")]
|
||
[SerializeField] private float deadZone = 0.5f;
|
||
|
||
[Header("Лимиты")]
|
||
[SerializeField] private int maxCapsules = 10;
|
||
[SerializeField] private int maxBoxes = 48;
|
||
|
||
// ── Строки ──
|
||
private const int ROW_TYPE = 0;
|
||
private const int ROW_PARAM = 1;
|
||
private const int ROW_CLOSE = 2;
|
||
private const int ROW_COUNT = 3;
|
||
|
||
// ── Текущее состояние ──
|
||
private string _levelName;
|
||
private LevelPrizeConfig _config;
|
||
|
||
private static readonly PrizeType[] PrizeTypes =
|
||
(PrizeType[])Enum.GetValues(typeof(PrizeType));
|
||
|
||
private int _currentTypeIndex;
|
||
private int _activeRow;
|
||
private bool _axisHeldX;
|
||
private bool _axisHeldY;
|
||
|
||
/// <summary>
|
||
/// Вызывается после сохранения настроек (при закрытии окна). Параметр — имя уровня.
|
||
/// </summary>
|
||
public event Action<string> OnApplied;
|
||
|
||
// ────────────── Lifecycle ──────────────
|
||
|
||
protected override void OnEnable()
|
||
{
|
||
base.OnEnable();
|
||
|
||
if (navigateAction != null && navigateAction.action != null)
|
||
{
|
||
navigateAction.action.Enable();
|
||
navigateAction.action.performed += OnNavigatePerformed;
|
||
navigateAction.action.canceled += OnNavigateCanceled;
|
||
}
|
||
}
|
||
|
||
protected override void OnDisable()
|
||
{
|
||
if (navigateAction != null && navigateAction.action != null)
|
||
{
|
||
navigateAction.action.performed -= OnNavigatePerformed;
|
||
navigateAction.action.canceled -= OnNavigateCanceled;
|
||
}
|
||
|
||
_axisHeldX = false;
|
||
_axisHeldY = false;
|
||
base.OnDisable();
|
||
}
|
||
|
||
// ────────────── Ввод ──────────────
|
||
|
||
private void OnNavigatePerformed(InputAction.CallbackContext ctx)
|
||
{
|
||
if (!_isOpen) return;
|
||
|
||
Vector2 v = ctx.ReadValue<Vector2>();
|
||
|
||
// ── Вертикаль: переключение строк ──
|
||
if (Mathf.Abs(v.y) >= deadZone)
|
||
{
|
||
if (!_axisHeldY)
|
||
{
|
||
_axisHeldY = true;
|
||
MoveRow(v.y > 0f ? -1 : 1);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_axisHeldY = false;
|
||
}
|
||
|
||
// ── Горизонталь: изменение значения ──
|
||
if (Mathf.Abs(v.x) >= deadZone)
|
||
{
|
||
if (!_axisHeldX)
|
||
{
|
||
_axisHeldX = true;
|
||
HandleLeftRight(v.x > 0f ? 1 : -1);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_axisHeldX = false;
|
||
}
|
||
}
|
||
|
||
private void OnNavigateCanceled(InputAction.CallbackContext ctx)
|
||
{
|
||
_axisHeldX = false;
|
||
_axisHeldY = false;
|
||
}
|
||
|
||
// ────────────── Навигация строк ──────────────
|
||
|
||
private void MoveRow(int delta)
|
||
{
|
||
int next = _activeRow;
|
||
for (int i = 0; i < ROW_COUNT; i++)
|
||
{
|
||
next = (next + delta + ROW_COUNT) % ROW_COUNT;
|
||
if (IsRowVisible(next))
|
||
{
|
||
_activeRow = next;
|
||
break;
|
||
}
|
||
}
|
||
RefreshRowHighlights();
|
||
}
|
||
|
||
private bool IsRowVisible(int row)
|
||
{
|
||
if (row == ROW_PARAM)
|
||
return _config.type == PrizeType.Hopper || _config.type == PrizeType.PrizeStand;
|
||
return true; // ROW_TYPE и ROW_CLOSE видны всегда
|
||
}
|
||
|
||
private void HandleLeftRight(int direction)
|
||
{
|
||
switch (_activeRow)
|
||
{
|
||
case ROW_TYPE:
|
||
CyclePrizeType(direction);
|
||
break;
|
||
|
||
case ROW_PARAM:
|
||
if (_config.type == PrizeType.Hopper)
|
||
AdjustCapsules(direction);
|
||
else if (_config.type == PrizeType.PrizeStand)
|
||
AdjustBoxes(direction);
|
||
break;
|
||
|
||
// ROW_CLOSE — лево/право ничего не делает
|
||
}
|
||
}
|
||
|
||
// ────────────── Публичный API ──────────────
|
||
|
||
/// <summary>
|
||
/// Открывает окно настройки приза для указанного уровня.
|
||
/// </summary>
|
||
public void OpenForLevel(string levelName)
|
||
{
|
||
_levelName = levelName;
|
||
|
||
PrizeDatabase db = PrizeSystemManager.Instance?.Database;
|
||
_config = db != null
|
||
? CopyConfig(db.GetConfig(levelName))
|
||
: new LevelPrizeConfig();
|
||
|
||
_currentTypeIndex = 0;
|
||
for (int i = 0; i < PrizeTypes.Length; i++)
|
||
{
|
||
if (PrizeTypes[i] == _config.type)
|
||
{
|
||
_currentTypeIndex = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
_activeRow = ROW_TYPE;
|
||
RefreshUI();
|
||
RefreshRowHighlights();
|
||
Open();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Закрывает окно и сохраняет настройки.
|
||
/// </summary>
|
||
public override void Close()
|
||
{
|
||
Save();
|
||
base.Close();
|
||
}
|
||
|
||
// ────────────── Внутреннее ──────────────
|
||
|
||
private void CyclePrizeType(int direction)
|
||
{
|
||
_currentTypeIndex = (_currentTypeIndex + direction + PrizeTypes.Length) % PrizeTypes.Length;
|
||
_config.type = PrizeTypes[_currentTypeIndex];
|
||
|
||
// Если строка параметров стала невидимой — перескочить
|
||
if (_activeRow == ROW_PARAM && !IsRowVisible(ROW_PARAM))
|
||
_activeRow = ROW_TYPE;
|
||
|
||
RefreshUI();
|
||
RefreshRowHighlights();
|
||
}
|
||
|
||
private void AdjustCapsules(int delta)
|
||
{
|
||
_config.hopperCapsuleCount = Mathf.Clamp(_config.hopperCapsuleCount + delta, 1, maxCapsules);
|
||
RefreshUI();
|
||
}
|
||
|
||
private void AdjustBoxes(int delta)
|
||
{
|
||
_config.prizeStandBoxCount = Mathf.Clamp(_config.prizeStandBoxCount + delta, 1, maxBoxes);
|
||
RefreshUI();
|
||
}
|
||
|
||
private void Save()
|
||
{
|
||
if (string.IsNullOrEmpty(_levelName) || _config == null) return;
|
||
|
||
PrizeDatabase db = PrizeSystemManager.Instance?.Database;
|
||
if (db == null)
|
||
{
|
||
Debug.LogError("[LevelPrizeSettingsModal] PrizeDatabase недоступна.");
|
||
return;
|
||
}
|
||
|
||
db.SetConfig(_levelName, _config);
|
||
Debug.Log($"[LevelPrizeSettingsModal] Уровень «{_levelName}» → приз «{_config.type}» сохранён.");
|
||
|
||
OnApplied?.Invoke(_levelName);
|
||
}
|
||
|
||
// ────────────── UI ──────────────
|
||
|
||
private void RefreshUI()
|
||
{
|
||
if (titleText)
|
||
titleText.text = $"Приз за уровень {_levelName}";
|
||
|
||
if (prizeTypeLabel)
|
||
prizeTypeLabel.text = GetPrizeTypeName(_config.type);
|
||
|
||
bool isHopper = _config.type == PrizeType.Hopper;
|
||
bool isPrizeStand = _config.type == PrizeType.PrizeStand;
|
||
|
||
if (hopperParamsGroup) hopperParamsGroup.SetActive(isHopper);
|
||
if (prizeStandParamsGroup) prizeStandParamsGroup.SetActive(isPrizeStand);
|
||
|
||
if (hopperCapsuleCountLabel)
|
||
hopperCapsuleCountLabel.text = _config.hopperCapsuleCount.ToString();
|
||
|
||
if (prizeStandBoxCountLabel)
|
||
prizeStandBoxCountLabel.text = _config.prizeStandBoxCount.ToString();
|
||
}
|
||
|
||
private void RefreshRowHighlights()
|
||
{
|
||
if (_activeRow == ROW_CLOSE)
|
||
{
|
||
// Прячем указатель, фокус переходит на кнопку закрытия
|
||
if (rowPointer) rowPointer.Hide();
|
||
|
||
if (closeModalButton && EventSystem.current)
|
||
EventSystem.current.SetSelectedGameObject(closeModalButton.gameObject);
|
||
}
|
||
else
|
||
{
|
||
|
||
if (rowPointer)
|
||
{
|
||
RectTransform target = GetActiveRowTransform();
|
||
if (target) rowPointer.SetTarget(target);
|
||
}
|
||
}
|
||
}
|
||
|
||
private RectTransform GetActiveRowTransform()
|
||
{
|
||
switch (_activeRow)
|
||
{
|
||
case ROW_TYPE:
|
||
return prizeTypeRow;
|
||
|
||
case ROW_PARAM:
|
||
if (_config.type == PrizeType.Hopper && hopperParamsGroup)
|
||
return hopperParamsGroup.transform as RectTransform;
|
||
if (_config.type == PrizeType.PrizeStand && prizeStandParamsGroup)
|
||
return prizeStandParamsGroup.transform as RectTransform;
|
||
return prizeTypeRow;
|
||
|
||
default:
|
||
return prizeTypeRow;
|
||
}
|
||
}
|
||
|
||
private static string GetPrizeTypeName(PrizeType type) => type switch
|
||
{
|
||
PrizeType.None => "Нет приза",
|
||
PrizeType.QRCode => "QR-код",
|
||
PrizeType.Hopper => "Хоппер",
|
||
PrizeType.PrizeStand => "Витрина",
|
||
_ => type.ToString()
|
||
};
|
||
|
||
private static LevelPrizeConfig CopyConfig(LevelPrizeConfig src) => new()
|
||
{
|
||
type = src.type,
|
||
hopperCapsuleCount = src.hopperCapsuleCount,
|
||
hopperDelayBetweenCapsules = src.hopperDelayBetweenCapsules,
|
||
prizeStandBoxCount = src.prizeStandBoxCount
|
||
};
|
||
}
|
||
}
|
||
|