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 { /// /// Модальное окно настройки приза за конкретный уровень. /// Полностью управляется с клавиатуры/геймпада через Input System. /// Навигация: вверх/вниз — переключение строк (зацикленно), влево/вправо — изменение значения. /// Настройки сохраняются автоматически при закрытии окна. /// 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; /// /// Вызывается после сохранения настроек (при закрытии окна). Параметр — имя уровня. /// public event Action 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(); // ── Вертикаль: переключение строк ── 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 ────────────── /// /// Открывает окно настройки приза для указанного уровня. /// 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(); } /// /// Закрывает окно и сохраняет настройки. /// 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 }; } }