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