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 { /// /// Отдельная модалка настройки глобальной конфигурации витрины. /// Позволяет менять выбранную ячейку и порог очков для нее. /// public class PrizeStandSettingsModal : Modal { [Header("UI")] [SerializeField] private TMP_Text titleText; [SerializeField] private TMP_Text connectionStatusLabel; [SerializeField] private TMP_Text boxCountLabel; [SerializeField] private TMP_Text selectedBoxLabel; [SerializeField] private TMP_Text thresholdLabel; [SerializeField] private TMP_Text detailsText; [Header("Rows")] [SerializeField] private RectTransform selectedBoxRow; [SerializeField] private RectTransform thresholdRow; [Header("Pointer")] [SerializeField] private UIRowPointer rowPointer; [Header("Close")] [SerializeField] private Button closeModalButton; [Header("Navigation")] [SerializeField] private InputActionReference navigateAction; [SerializeField] private float deadZone = 0.5f; [Header("Limits")] [SerializeField] private int defaultBoxesCount = 5; private const int RowSelectedBox = 0; private const int RowThreshold = 1; private const int RowClose = 2; private const int RowCount = 3; private const int ThresholdStep = 1; private const int MaxStepMultiplier = 10; private const float HoldResetTimeout = 0.25f; private const float InitialHorizontalRepeatDelay = 0.25f; private const float ThresholdAccelerationTime = 0.8f; private const float MinimumHorizontalRepeatInterval = 0.03f; private PrizeStandSettings _settings; private int _selectedBoxIndex; private int _activeRow; private bool _axisHeldY; private Selectable _pendingFocusTarget; private int _pendingFocusFrames; private Selectable _selectedBoxRowSelectable; private Selectable _thresholdRowSelectable; private PrizeStandRowMoveHandler _selectedBoxMoveHandler; private PrizeStandRowMoveHandler _thresholdMoveHandler; public event Action OnApplied; protected override void OnEnable() { base.OnEnable(); if (navigateAction != null && navigateAction.action != null) { navigateAction.action.Enable(); } } protected override void OnDisable() { _axisHeldY = false; ResetHorizontalHold(); _pendingFocusTarget = null; _pendingFocusFrames = 0; base.OnDisable(); } protected override void OnReturnedToTop() { _axisHeldY = false; ResetHorizontalHold(); _pendingFocusTarget = null; _pendingFocusFrames = 0; RefreshRowHighlights(); } public override void Open() { PrepareForEditing(); base.Open(); RefreshRowHighlights(); } public void OpenForEditing() { Open(); } private void PrepareForEditing() { PrizeSystemManager manager = PrizeSystemManager.Instance; _settings = manager != null ? CopyPrizeStandSettings(manager.GetPrizeStandSettings()) : CreateDefaultSettings(); EnsureSettingsInitialized(); AutoResolveRows(); AutoResolveValueLabels(); EnsureRowSelectables(); _selectedBoxIndex = 0; _activeRow = RowSelectedBox; RefreshUI(); RefreshRowHighlights(); } public override void Close() { Save(); base.Close(); } private void Update() { if (!_isOpen) { return; } ProcessNavigationInput(); } private void LateUpdate() { if (!_isOpen) { return; } ApplyPendingFocus(); } private void ProcessNavigationInput() { if (navigateAction == null || navigateAction.action == null) { _axisHeldY = false; return; } Vector2 value = navigateAction.action.ReadValue(); if (Mathf.Abs(value.y) >= deadZone) { if (!_axisHeldY) { _axisHeldY = true; MoveRow(value.y > 0f ? -1 : 1); } } else { _axisHeldY = false; } } private void MoveRow(int delta) { _activeRow = (_activeRow + delta + RowCount) % RowCount; ResetHorizontalHold(); RefreshRowHighlights(); } private void AdjustSelectedBox(int direction) { EnsureSettingsInitialized(); int boxCount = _settings.boxes.Count; if (boxCount == 0) { return; } _selectedBoxIndex = (_selectedBoxIndex + direction) % boxCount; if (_selectedBoxIndex < 0) { _selectedBoxIndex += boxCount; } RefreshUI(); } internal void HandleRowMove(int rowIndex, int direction, int multiplier) { if (!_isOpen) { return; } switch (rowIndex) { case RowSelectedBox: AdjustSelectedBox(direction); break; case RowThreshold: AdjustThreshold(direction, multiplier); break; } } private void AdjustThreshold(int direction, int multiplier) { PrizeStandBoxConfig box = GetSelectedBox(); if (box == null) { return; } box.requiredScore = Mathf.Max(0, box.requiredScore + direction * ThresholdStep * Mathf.Max(1, multiplier)); RefreshUI(); } private void Save() { PrizeSystemManager manager = PrizeSystemManager.Instance; if (manager == null) { Debug.LogError("[PrizeStandSettingsModal] PrizeSystemManager недоступен."); return; } EnsureSettingsInitialized(); NormalizeBoxes(); manager.SetPrizeStandSettings(_settings); OnApplied?.Invoke(); } private void RefreshUI() { EnsureSettingsInitialized(); PrizeStandConnectionInfo connectionInfo = PrizeSystemManager.Instance != null ? PrizeSystemManager.Instance.GetPrizeStandConnectionInfo() : new PrizeStandConnectionInfo(false, 0); PrizeStandBoxConfig selectedBox = GetSelectedBox(); if (titleText != null) { titleText.text = "Настройки витрины"; } if (connectionStatusLabel != null) { connectionStatusLabel.text = connectionInfo.IsConnected ? "Витрина подключена" : "Витрина не подключена"; } if (boxCountLabel != null) { boxCountLabel.text = _settings.boxes.Count.ToString(); } if (selectedBoxLabel != null) { selectedBoxLabel.text = selectedBox != null ? selectedBox.boxNumber.ToString() : "-"; } if (thresholdLabel != null) { thresholdLabel.text = selectedBox != null ? selectedBox.requiredScore.ToString() : "-"; } if (detailsText != null) detailsText.text = string.Empty; } private void RefreshRowHighlights() { Selectable selectableToFocus = null; if (_activeRow == RowClose) { if (rowPointer != null) rowPointer.Hide(); selectableToFocus = closeModalButton; } else { RectTransform target = _activeRow switch { RowSelectedBox => selectedBoxRow, RowThreshold => thresholdRow, _ => selectedBoxRow }; selectableToFocus = _activeRow switch { RowSelectedBox => _selectedBoxRowSelectable, RowThreshold => _thresholdRowSelectable, _ => _selectedBoxRowSelectable }; if (rowPointer != null && target != null) { rowPointer.SetTarget(target); } } FocusSelectable(selectableToFocus); } private void AutoResolveRows() { if (selectedBoxRow == null && selectedBoxLabel != null) { selectedBoxRow = selectedBoxLabel.transform.parent as RectTransform; } if (thresholdRow == null && thresholdLabel != null) { thresholdRow = thresholdLabel.transform.parent as RectTransform; } } private void AutoResolveValueLabels() { selectedBoxLabel = ResolveValueLabel(selectedBoxRow, selectedBoxLabel); thresholdLabel = ResolveValueLabel(thresholdRow, thresholdLabel); } private static TMP_Text ResolveValueLabel(RectTransform row, TMP_Text current) { if (row == null) { return current; } TMP_Text[] texts = row.GetComponentsInChildren(true); TMP_Text best = null; float bestScore = float.MaxValue; for (int i = 0; i < texts.Length; i++) { TMP_Text text = texts[i]; if (text == null) { continue; } RectTransform textRect = text.rectTransform; if (textRect == null || !textRect.IsChildOf(row)) { continue; } Vector3 localCenter = row.InverseTransformPoint(textRect.TransformPoint(textRect.rect.center)); float score = Mathf.Abs(localCenter.x) + Mathf.Abs(localCenter.y) * 0.25f; if (best == null || score < bestScore) { best = text; bestScore = score; } } if (best != null) { return best; } return current; } private void EnsureRowSelectables() { _selectedBoxRowSelectable = EnsureRowSelectable(selectedBoxRow); _thresholdRowSelectable = EnsureRowSelectable(thresholdRow); _selectedBoxMoveHandler = EnsureRowMoveHandler(selectedBoxRow, RowSelectedBox); _thresholdMoveHandler = EnsureRowMoveHandler(thresholdRow, RowThreshold); } 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 PrizeStandRowMoveHandler EnsureRowMoveHandler(RectTransform row, int rowIndex) { if (row == null) { return null; } PrizeStandRowMoveHandler handler = row.GetComponent(); if (handler == null) { handler = row.gameObject.AddComponent(); } handler.Initialize(this, rowIndex); return handler; } 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 void EnsureSettingsInitialized() { _settings ??= CreateDefaultSettings(); _settings.boxes ??= new List(); if (_settings.boxes.Count == 0) { for (int i = 1; i <= defaultBoxesCount; i++) { _settings.boxes.Add(new PrizeStandBoxConfig { boxNumber = i, requiredScore = 0 }); } } NormalizeBoxes(); } private void ResetHorizontalHold() { _selectedBoxMoveHandler?.ResetHold(); _thresholdMoveHandler?.ResetHold(); } private void NormalizeBoxes() { _settings.boxes.Sort((a, b) => a.boxNumber.CompareTo(b.boxNumber)); for (int i = 0; i < _settings.boxes.Count; i++) { _settings.boxes[i] ??= new PrizeStandBoxConfig(); _settings.boxes[i].boxNumber = i + 1; _settings.boxes[i].requiredScore = Mathf.Max(0, _settings.boxes[i].requiredScore); } } private PrizeStandBoxConfig GetSelectedBox() { EnsureSettingsInitialized(); if (_settings.boxes.Count == 0) { return null; } _selectedBoxIndex = Mathf.Clamp(_selectedBoxIndex, 0, _settings.boxes.Count - 1); return _settings.boxes[_selectedBoxIndex]; } private PrizeStandSettings CreateDefaultSettings() { PrizeStandSettings settings = new PrizeStandSettings(); for (int i = 1; i <= defaultBoxesCount; i++) { settings.boxes.Add(new PrizeStandBoxConfig { boxNumber = i, requiredScore = 0 }); } return settings; } 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; } } [DisallowMultipleComponent] public sealed class PrizeStandRowMoveHandler : MonoBehaviour, IMoveHandler, IDeselectHandler { private const int MaxStepMultiplier = 10; private const int MultiplierStartCount = 6; private const int MultiplierMaxCount = 30; private const float HoldResetTimeout = 0.25f; private PrizeStandSettingsModal _owner; private int _rowIndex; private int _holdDirection; private int _holdCount; private float _lastMoveTime; public void Initialize(PrizeStandSettingsModal owner, int rowIndex) { _owner = owner; _rowIndex = rowIndex; ResetHold(); } public void OnMove(AxisEventData eventData) { if (_owner == null) { return; } switch (eventData.moveDir) { case MoveDirection.Left: ApplyMove(-1); eventData.Use(); break; case MoveDirection.Right: ApplyMove(1); eventData.Use(); break; case MoveDirection.Up: case MoveDirection.Down: ResetHold(); break; } } public void OnDeselect(BaseEventData eventData) { ResetHold(); } private void OnDisable() { ResetHold(); } public void ResetHold() { _holdDirection = 0; _holdCount = 0; _lastMoveTime = 0f; } private void ApplyMove(int direction) { float now = Time.unscaledTime; if (_holdDirection != direction || (now - _lastMoveTime) > HoldResetTimeout) { _holdDirection = direction; _holdCount = 0; } _lastMoveTime = now; _holdCount++; int multiplier = 1; if (_holdCount > MultiplierStartCount) { float progress = Mathf.Clamp01( (float)(_holdCount - MultiplierStartCount) / Mathf.Max(1, MultiplierMaxCount - MultiplierStartCount)); multiplier = Mathf.Max(1, Mathf.RoundToInt(Mathf.Lerp(1f, MaxStepMultiplier, progress))); } _owner.HandleRowMove(_rowIndex, direction, multiplier); } } }