Danro/Assets/Scripts/UI/Modal/LevelPrizeSettingsModal.cs
Danil Iarosh a40b761c81
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat(prizes, ui): add configurable level prize flow and launcher assets
- добавлена новая система призов:
  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-сцены
2026-03-16 08:15:36 +03:00

359 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
};
}
}