using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace RR.Common.Settings.UI { /// /// Компонент-Selectable для элемента настроек с выбором значения стрелками влево/вправо. /// Поддерживает как int, так и float значения. /// При зажатии кнопки скорость изменения плавно увеличивается. /// Автоповтор целиком управляется Event System (inputActionsPerSecond). /// Работает на одном префабе вместе с SettingUIItem. /// public class ValueSelectorRow : Selectable, IMoveHandler { [Header("References")] [SerializeField] private TMP_Text valueText; private SettingEntry _entry; private bool _isFloat; private int _currentInt; private float _currentFloat; private int _intStep = 1; private float _floatStep = 0.1f; // ───────────────────── Ускорение при зажатии ───────────────────── /// Максимальный множитель шага при длительном зажатии. private const int MaxStepMultiplier = 10; /// Сколько вызовов OnMove до начала роста множителя. private const int MultiplierStartCount = 6; /// Сколько вызовов OnMove до достижения максимального множителя. private const int MultiplierMaxCount = 30; /// Если между двумя OnMove прошло больше этого времени — сброс множителя. private const float HoldResetTimeout = 0.25f; private int _holdDirection; // -1 = left, +1 = right, 0 = idle private int _holdCount; // Счётчик последовательных OnMove в одном направлении private float _lastMoveTime; // Time.unscaledTime последнего OnMove // ───────────────────── Int Setup ───────────────────── public void Setup(SettingEntry entry, int currentValue) { if (entry == null) { Debug.LogError("[ValueSelectorRow] Cannot setup with null entry!"); return; } _entry = entry; _isFloat = false; if (entry.intStep <= 0) { Debug.LogWarning($"[ValueSelectorRow] Invalid step {entry.intStep} for key {entry.key}, using 1"); _intStep = 1; } else { _intStep = entry.intStep; } _currentInt = Mathf.Clamp(currentValue, entry.minInt, entry.maxInt); UpdateUI(); } // ───────────────────── Float Setup ───────────────────── public void Setup(SettingEntry entry, float currentValue) { if (entry == null) { Debug.LogError("[ValueSelectorRow] Cannot setup with null entry!"); return; } _entry = entry; _isFloat = true; if (entry.floatStep <= 0f) { Debug.LogWarning($"[ValueSelectorRow] Invalid step {entry.floatStep} for key {entry.key}, using 0.1"); _floatStep = 0.1f; } else { _floatStep = entry.floatStep; } _currentFloat = Mathf.Clamp(currentValue, entry.minFloat, entry.maxFloat); UpdateUI(); } // ───────────────────── Navigation ───────────────────── public new void OnMove(AxisEventData eventData) { if (_entry == null || !IsInteractable()) { base.OnMove(eventData); 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(); base.OnMove(eventData); break; } } // ───────────────────── Move Logic ───────────────────── 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) / (MultiplierMaxCount - MultiplierStartCount)); multiplier = Mathf.Max(1, Mathf.RoundToInt(Mathf.Lerp(1f, MaxStepMultiplier, progress))); } if (direction > 0) IncreaseValue(multiplier); else DecreaseValue(multiplier); } private void ResetHold() { _holdDirection = 0; _holdCount = 0; } protected override void OnDisable() { base.OnDisable(); ResetHold(); } public override void OnDeselect(BaseEventData eventData) { base.OnDeselect(eventData); ResetHold(); } // ───────────────────── Value Changes ───────────────────── private void DecreaseValue(int multiplier) { if (_isFloat) { float newValue = Mathf.Max(_entry.minFloat, _currentFloat - _floatStep * multiplier); if (!Mathf.Approximately(newValue, _currentFloat)) SetValue(newValue); } else { int newValue = Mathf.Max(_entry.minInt, _currentInt - _intStep * multiplier); if (newValue != _currentInt) SetValue(newValue); } } private void IncreaseValue(int multiplier) { if (_isFloat) { float newValue = Mathf.Min(_entry.maxFloat, _currentFloat + _floatStep * multiplier); if (!Mathf.Approximately(newValue, _currentFloat)) SetValue(newValue); } else { int newValue = Mathf.Min(_entry.maxInt, _currentInt + _intStep * multiplier); if (newValue != _currentInt) SetValue(newValue); } } // ───────────────────── UI ───────────────────── private void UpdateUI() { if (valueText == null) return; if (_isFloat) { if (_entry != null && _entry.displayAsPercentage) valueText.text = (_currentFloat * 100f).ToString("0") + "%"; else valueText.text = _currentFloat.ToString("0.##"); } else { valueText.text = _currentInt.ToString(); } } // ───────────────────── Public API (int) ───────────────────── public void SetValue(int value) { if (_entry == null) return; int clamped = Mathf.Clamp(value, _entry.minInt, _entry.maxInt); if (clamped != _currentInt) { _currentInt = clamped; UpdateUI(); var manager = SettingsManager.Instance; if (manager != null) manager.SetInt(_entry.key, _currentInt); else Debug.LogError("[ValueSelectorRow] SettingsManager.Instance is null!"); } } // ───────────────────── Public API (float) ───────────────────── public void SetValue(float value) { if (_entry == null) return; float clamped = Mathf.Clamp(value, _entry.minFloat, _entry.maxFloat); if (!Mathf.Approximately(clamped, _currentFloat)) { _currentFloat = clamped; UpdateUI(); var manager = SettingsManager.Instance; if (manager != null) manager.SetFloat(_entry.key, _currentFloat); else Debug.LogError("[ValueSelectorRow] SettingsManager.Instance is null!"); } } public bool IsFloat => _isFloat; public string Key => _entry?.key; } }