using System; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.InputSystem; using UnityEngine.UI; using RR.Common.Managers; using RR.Common.Prizes; namespace RR.Common.UI { /// /// Модальное окно настройки приза за конкретный уровень. /// Для QR настраивает ранг на уровне, для витрины редактирует общую игровую конфигурацию. /// public class LevelPrizeSettingsModal : Modal { [Header("UI — Общее")] [SerializeField] private TMP_Text titleText; [SerializeField] private TMP_Text prizeTypeLabel; [SerializeField] private TMP_Text detailsText; [Header("UI — QR")] [SerializeField] private GameObject qrParamsGroup; [SerializeField] private TMP_Text qrTierLabel; [Header("UI — Параметры хоппера")] [SerializeField] private GameObject hopperParamsGroup; [SerializeField] private TMP_Text hopperCapsuleCountLabel; [Header("UI — Параметры витрины")] [SerializeField] private GameObject prizeStandParamsGroup; [SerializeField] private TMP_Text prizeStandStatusLabel; [SerializeField] private TMP_Text prizeStandBoxCountLabel; [SerializeField] private TMP_Text prizeStandSelectedBoxLabel; [SerializeField] private TMP_Text prizeStandThresholdLabel; [Header("Строки (RectTransform)")] [SerializeField] private RectTransform prizeTypeRow; [SerializeField] private RectTransform qrTierRow; [SerializeField] private RectTransform hopperParamRow; [SerializeField] private RectTransform prizeStandBoxCountRow; [SerializeField] private RectTransform prizeStandSelectedBoxRow; [SerializeField] private RectTransform prizeStandThresholdRow; [Header("Указатель")] [SerializeField] private UIRowPointer rowPointer; [Header("Кнопка закрытия")] [SerializeField] private Button closeModalButton; [Header("Навигация (Input System)")] [SerializeField] private InputActionReference navigateAction; [SerializeField] private float deadZone = 0.5f; [Header("QR Tier Colors")] [SerializeField] private Color bronzeTierColor = new Color32(205, 127, 50, 255); [SerializeField] private Color silverTierColor = new Color32(192, 192, 192, 255); [SerializeField] private Color goldTierColor = new Color32(255, 215, 0, 255); [Header("Лимиты")] [SerializeField] private int maxCapsules = 10; [SerializeField] private int maxBoxes = 48; [SerializeField] private int defaultPrizeStandBoxesCount = 5; [SerializeField] private int scoreStep = 50; private const int ROW_TYPE = 0; private const int ROW_QR_TIER = 1; private const int ROW_HOPPER_PARAM = 2; private const int ROW_STAND_BOX_COUNT = 3; private const int ROW_STAND_SELECTED_BOX = 4; private const int ROW_STAND_THRESHOLD = 5; private const int ROW_CLOSE = 6; private const int ROW_COUNT = 7; private static readonly PrizeType[] PrizeTypes = (PrizeType[])Enum.GetValues(typeof(PrizeType)); private static readonly QRTier[] QRTiers = { QRTier.Bronze, QRTier.Silver, QRTier.Gold }; private string _levelName; private LevelPrizeConfig _config; private PrizeStandSettings _prizeStandSettings; private int _currentTypeIndex; private int _currentTierIndex; private int _activeRow; private int _selectedPrizeStandBoxIndex; private bool _axisHeldX; private bool _axisHeldY; private Selectable _pendingFocusTarget; private int _pendingFocusFrames; private Selectable _prizeTypeRowSelectable; private Selectable _qrTierRowSelectable; private Selectable _hopperParamRowSelectable; private Selectable _prizeStandBoxCountRowSelectable; private Selectable _prizeStandSelectedBoxRowSelectable; private Selectable _prizeStandThresholdRowSelectable; public event Action OnApplied; 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; _pendingFocusTarget = null; _pendingFocusFrames = 0; base.OnDisable(); } protected override void OnReturnedToTop() { _axisHeldX = false; _axisHeldY = false; _pendingFocusTarget = null; _pendingFocusFrames = 0; RefreshRowHighlights(); } private void LateUpdate() { if (!_isOpen) { return; } ApplyPendingFocus(); } 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) { return row switch { ROW_QR_TIER => _config.type == PrizeType.QRCode, ROW_HOPPER_PARAM => _config.type == PrizeType.Hopper, ROW_STAND_BOX_COUNT => HasVisiblePrizeStandLevelSettings(), ROW_STAND_SELECTED_BOX => HasVisiblePrizeStandLevelSettings() && GetPrizeStandBoxes().Count > 0, ROW_STAND_THRESHOLD => HasVisiblePrizeStandLevelSettings() && GetPrizeStandBoxes().Count > 0, _ => true }; } private void HandleLeftRight(int direction) { switch (_activeRow) { case ROW_TYPE: CyclePrizeType(direction); break; case ROW_QR_TIER: AdjustQrTier(direction); break; case ROW_HOPPER_PARAM: AdjustCapsules(direction); break; case ROW_STAND_BOX_COUNT: AdjustPrizeStandBoxCount(direction); break; case ROW_STAND_SELECTED_BOX: AdjustSelectedPrizeStandBox(direction); break; case ROW_STAND_THRESHOLD: AdjustPrizeStandThreshold(direction); break; } } public void OpenForLevel(string levelName) { _levelName = levelName; PrizeDatabase db = PrizeSystemManager.Instance?.Database; _config = db != null ? CopyConfig(db.GetConfig(levelName)) : new LevelPrizeConfig(); _prizeStandSettings = db != null ? CopyPrizeStandSettings(db.GetPrizeStandSettings()) : CreateDefaultPrizeStandSettings(); EnsurePrizeStandSettingsInitialized(); _currentTypeIndex = Array.IndexOf(PrizeTypes, _config.type); if (_currentTypeIndex < 0) _currentTypeIndex = 0; _currentTierIndex = GetTierIndex(_config.qrTier); _selectedPrizeStandBoxIndex = Mathf.Clamp(_selectedPrizeStandBoxIndex, 0, Mathf.Max(0, GetPrizeStandBoxes().Count - 1)); _activeRow = ROW_TYPE; AutoResolveRows(); EnsureRowSelectables(); 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 (_config.type == PrizeType.QRCode && _config.qrTier == QRTier.None) _config.qrTier = QRTiers[0]; EnsurePrizeStandSettingsInitialized(); if (!IsRowVisible(_activeRow)) _activeRow = ROW_TYPE; RefreshUI(); RefreshRowHighlights(); } private void AdjustQrTier(int direction) { _currentTierIndex = (_currentTierIndex + direction + QRTiers.Length) % QRTiers.Length; _config.qrTier = QRTiers[_currentTierIndex]; RefreshUI(); } private void AdjustCapsules(int delta) { _config.hopperCapsuleCount = Mathf.Clamp(_config.hopperCapsuleCount + delta, 1, maxCapsules); RefreshUI(); } private void AdjustPrizeStandBoxCount(int delta) { List boxes = GetPrizeStandBoxes(); int currentCount = boxes.Count; int nextCount = Mathf.Clamp(currentCount + delta, 1, maxBoxes); if (nextCount == currentCount) return; if (nextCount > currentCount) { for (int i = currentCount + 1; i <= nextCount; i++) boxes.Add(new PrizeStandBoxConfig { boxNumber = i, requiredScore = 0 }); } else { boxes.RemoveRange(nextCount, currentCount - nextCount); } NormalizePrizeStandBoxes(); _selectedPrizeStandBoxIndex = Mathf.Clamp(_selectedPrizeStandBoxIndex, 0, boxes.Count - 1); RefreshUI(); RefreshRowHighlights(); } private void AdjustSelectedPrizeStandBox(int delta) { List boxes = GetPrizeStandBoxes(); if (boxes.Count == 0) return; _selectedPrizeStandBoxIndex = (_selectedPrizeStandBoxIndex + delta + boxes.Count) % boxes.Count; RefreshUI(); } private void AdjustPrizeStandThreshold(int direction) { PrizeStandBoxConfig selectedBox = GetSelectedPrizeStandBox(); if (selectedBox == null) return; selectedBox.requiredScore = Mathf.Max(0, selectedBox.requiredScore + direction * scoreStep); RefreshUI(); } private void Save() { if (string.IsNullOrEmpty(_levelName) || _config == null) return; PrizeDatabase db = PrizeSystemManager.Instance?.Database; PrizeSystemManager manager = PrizeSystemManager.Instance; if (db == null || manager == null) { Debug.LogError("[LevelPrizeSettingsModal] PrizeDatabase недоступна."); return; } db.SetConfig(_levelName, _config); Debug.Log($"[LevelPrizeSettingsModal] Уровень «{_levelName}» → приз «{_config.type}» сохранён."); OnApplied?.Invoke(_levelName); } private void RefreshUI() { EnsurePrizeStandSettingsInitialized(); if (titleText) titleText.text = $"Приз за уровень {_levelName}"; if (prizeTypeLabel) prizeTypeLabel.text = GetPrizeTypeName(_config.type); if (qrParamsGroup) qrParamsGroup.SetActive(_config.type == PrizeType.QRCode); if (hopperParamsGroup) hopperParamsGroup.SetActive(_config.type == PrizeType.Hopper); if (prizeStandParamsGroup) prizeStandParamsGroup.SetActive(HasVisiblePrizeStandLevelSettings()); if (qrTierLabel) { qrTierLabel.text = GetTierName(_config.qrTier); qrTierLabel.color = GetTierColor(_config.qrTier); } if (hopperCapsuleCountLabel) hopperCapsuleCountLabel.text = _config.hopperCapsuleCount.ToString(); PrizeStandConnectionInfo connectionInfo = PrizeSystemManager.Instance != null ? PrizeSystemManager.Instance.GetPrizeStandConnectionInfo() : new PrizeStandConnectionInfo(false, 0); List boxes = GetPrizeStandBoxes(); PrizeStandBoxConfig selectedBox = GetSelectedPrizeStandBox(); if (prizeStandStatusLabel) prizeStandStatusLabel.text = connectionInfo.IsConnected ? $"Подключена ({connectionInfo.ConnectedBoxesCount} ячеек)" : "Не подключена"; if (prizeStandBoxCountLabel) prizeStandBoxCountLabel.text = boxes.Count.ToString(); if (prizeStandSelectedBoxLabel) prizeStandSelectedBoxLabel.text = selectedBox != null ? selectedBox.boxNumber.ToString() : "-"; if (prizeStandThresholdLabel) prizeStandThresholdLabel.text = selectedBox != null ? selectedBox.requiredScore.ToString() : "-"; if (detailsText) detailsText.text = BuildDetailsText(connectionInfo, boxes, selectedBox); } private string BuildDetailsText(PrizeStandConnectionInfo connectionInfo, List boxes, PrizeStandBoxConfig selectedBox) { return _config.type switch { PrizeType.QRCode => $"Ранг QR-приза: {GetTierName(_config.qrTier)}", PrizeType.Hopper => $"Капсул за выдачу: {_config.hopperCapsuleCount}", PrizeType.PrizeStand => BuildPrizeStandDetails(connectionInfo, boxes, selectedBox), _ => "Для уровня приз не назначен." }; } private string BuildPrizeStandDetails(PrizeStandConnectionInfo connectionInfo, List boxes, PrizeStandBoxConfig selectedBox) { string connectionText = connectionInfo.IsConnected ? $"Витрина подключена, доступно на устройстве: {connectionInfo.ConnectedBoxesCount} ячеек." : "Витрина не подключена."; if (HasVisiblePrizeStandLevelSettings()) { string selectedText = selectedBox != null ? $"Выбрана ячейка {selectedBox.boxNumber}, порог: {selectedBox.requiredScore} очков." : "Ячейки не настроены."; return $"{connectionText}\nСконфигурировано ячеек: {boxes.Count}\n{selectedText}"; } return $"{connectionText}\nПараметры витрины настраиваются отдельно в глобальных настройках витрины."; } private void RefreshRowHighlights() { Selectable selectableToFocus = _activeRow switch { ROW_TYPE => _prizeTypeRowSelectable, ROW_QR_TIER => _qrTierRowSelectable, ROW_HOPPER_PARAM => _hopperParamRowSelectable, ROW_STAND_BOX_COUNT => _prizeStandBoxCountRowSelectable, ROW_STAND_SELECTED_BOX => _prizeStandSelectedBoxRowSelectable, ROW_STAND_THRESHOLD => _prizeStandThresholdRowSelectable, ROW_CLOSE => closeModalButton, _ => _prizeTypeRowSelectable }; if (_activeRow == ROW_CLOSE) { if (rowPointer) rowPointer.Hide(); } else { if (rowPointer) { RectTransform target = GetActiveRowTransform(); if (target) rowPointer.SetTarget(target); } } FocusSelectable(selectableToFocus); } private RectTransform GetActiveRowTransform() { return _activeRow switch { ROW_TYPE => prizeTypeRow, ROW_QR_TIER => qrTierRow != null ? qrTierRow : prizeTypeRow, ROW_HOPPER_PARAM => hopperParamRow != null ? hopperParamRow : prizeTypeRow, ROW_STAND_BOX_COUNT => prizeStandBoxCountRow, ROW_STAND_SELECTED_BOX => prizeStandSelectedBoxRow, ROW_STAND_THRESHOLD => prizeStandThresholdRow, _ => prizeTypeRow }; } private void AutoResolveRows() { if (prizeTypeRow == null && prizeTypeLabel != null) prizeTypeRow = prizeTypeLabel.transform.parent as RectTransform; if (qrTierRow == null && qrTierLabel != null) qrTierRow = qrTierLabel.transform.parent as RectTransform; if (hopperParamRow == null && hopperCapsuleCountLabel != null) hopperParamRow = hopperCapsuleCountLabel.transform.parent as RectTransform; if (prizeStandBoxCountRow == null && prizeStandBoxCountLabel != null) prizeStandBoxCountRow = prizeStandBoxCountLabel.transform.parent as RectTransform; if (prizeStandSelectedBoxRow == null && prizeStandSelectedBoxLabel != null) prizeStandSelectedBoxRow = prizeStandSelectedBoxLabel.transform.parent as RectTransform; if (prizeStandThresholdRow == null && prizeStandThresholdLabel != null) prizeStandThresholdRow = prizeStandThresholdLabel.transform.parent as RectTransform; } private void EnsureRowSelectables() { _prizeTypeRowSelectable = EnsureRowSelectable(prizeTypeRow); _qrTierRowSelectable = EnsureRowSelectable(qrTierRow); _hopperParamRowSelectable = EnsureRowSelectable(hopperParamRow); _prizeStandBoxCountRowSelectable = EnsureRowSelectable(prizeStandBoxCountRow); _prizeStandSelectedBoxRowSelectable = EnsureRowSelectable(prizeStandSelectedBoxRow); _prizeStandThresholdRowSelectable = EnsureRowSelectable(prizeStandThresholdRow); } private Selectable EnsureRowSelectable(RectTransform row) { if (row == null) { return null; } Selectable selectable = row.GetComponent(); if (selectable == null) { selectable = row.gameObject.AddComponent(); selectable.transition = Selectable.Transition.None; selectable.navigation = new Navigation { mode = Navigation.Mode.None }; } selectable.interactable = true; return selectable; } private void FocusSelectable(Selectable selectable) { if (selectable == null || !selectable.gameObject.activeInHierarchy) { return; } SetFirstSelected(selectable); _pendingFocusTarget = selectable; _pendingFocusFrames = 2; ApplyPendingFocus(); } private void ApplyPendingFocus() { if (_pendingFocusTarget == null || _pendingFocusFrames <= 0) { return; } if (EventSystem.current == null || !_pendingFocusTarget.gameObject.activeInHierarchy) { _pendingFocusTarget = null; _pendingFocusFrames = 0; return; } if (EventSystem.current.currentSelectedGameObject != _pendingFocusTarget.gameObject) { EventSystem.current.SetSelectedGameObject(null); EventSystem.current.SetSelectedGameObject(_pendingFocusTarget.gameObject); _pendingFocusTarget.Select(); } _pendingFocusFrames--; if (_pendingFocusFrames <= 0) { _pendingFocusTarget = null; } } private bool HasVisiblePrizeStandLevelSettings() { if (_config == null || _config.type != PrizeType.PrizeStand) { return false; } return prizeStandParamsGroup != null && (prizeStandBoxCountRow != null || prizeStandSelectedBoxRow != null || prizeStandThresholdRow != null); } private void EnsurePrizeStandSettingsInitialized() { _prizeStandSettings ??= CreateDefaultPrizeStandSettings(); _prizeStandSettings.boxes ??= new List(); if (_prizeStandSettings.boxes.Count == 0) { int initialCount = defaultPrizeStandBoxesCount; PrizeStandConnectionInfo connectionInfo = PrizeSystemManager.Instance != null ? PrizeSystemManager.Instance.GetPrizeStandConnectionInfo() : new PrizeStandConnectionInfo(false, 0); if (connectionInfo.IsConnected && connectionInfo.ConnectedBoxesCount > 0) initialCount = Mathf.Clamp(connectionInfo.ConnectedBoxesCount, 1, maxBoxes); for (int i = 1; i <= initialCount; i++) _prizeStandSettings.boxes.Add(new PrizeStandBoxConfig { boxNumber = i, requiredScore = 0 }); } NormalizePrizeStandBoxes(); } private void NormalizePrizeStandBoxes() { List boxes = _prizeStandSettings.boxes; boxes.Sort((a, b) => a.boxNumber.CompareTo(b.boxNumber)); for (int i = 0; i < boxes.Count; i++) { if (boxes[i] == null) boxes[i] = new PrizeStandBoxConfig(); boxes[i].boxNumber = i + 1; boxes[i].requiredScore = Mathf.Max(0, boxes[i].requiredScore); } } private List GetPrizeStandBoxes() { EnsurePrizeStandSettingsInitialized(); return _prizeStandSettings.boxes; } private PrizeStandBoxConfig GetSelectedPrizeStandBox() { List boxes = GetPrizeStandBoxes(); if (boxes.Count == 0) return null; _selectedPrizeStandBoxIndex = Mathf.Clamp(_selectedPrizeStandBoxIndex, 0, boxes.Count - 1); return boxes[_selectedPrizeStandBoxIndex]; } private static string GetPrizeTypeName(PrizeType type) { return type switch { PrizeType.None => "Нет приза", PrizeType.QRCode => "QR-код", PrizeType.Hopper => "Хоппер", PrizeType.PrizeStand => "Витрина", _ => type.ToString() }; } private static string GetTierName(QRTier tier) { return tier switch { QRTier.Bronze => "Бронзовый", QRTier.Silver => "Серебряный", QRTier.Gold => "Золотой", _ => "Не задан" }; } private Color GetTierColor(QRTier tier) { return tier switch { QRTier.Bronze => bronzeTierColor, QRTier.Silver => silverTierColor, QRTier.Gold => goldTierColor, _ => qrTierLabel != null ? qrTierLabel.color : Color.white }; } private static int GetTierIndex(QRTier tier) { int index = Array.IndexOf(QRTiers, tier); return index >= 0 ? index : 0; } private static LevelPrizeConfig CopyConfig(LevelPrizeConfig src) { return new LevelPrizeConfig { type = src.type, qrTier = src.qrTier, hopperCapsuleCount = src.hopperCapsuleCount, hopperDelayBetweenCapsules = src.hopperDelayBetweenCapsules }; } private static PrizeStandSettings CopyPrizeStandSettings(PrizeStandSettings src) { PrizeStandSettings copy = new PrizeStandSettings(); if (src?.boxes == null) return copy; for (int i = 0; i < src.boxes.Count; i++) { PrizeStandBoxConfig box = src.boxes[i]; if (box == null) continue; copy.boxes.Add(new PrizeStandBoxConfig { boxNumber = box.boxNumber, requiredScore = box.requiredScore }); } return copy; } private PrizeStandSettings CreateDefaultPrizeStandSettings() { PrizeStandSettings settings = new PrizeStandSettings(); for (int i = 1; i <= defaultPrizeStandBoxesCount; i++) { settings.boxes.Add(new PrizeStandBoxConfig { boxNumber = i, requiredScore = 0 }); } return settings; } } }