Danro/Assets/Scripts/Settings/UI/ValueSelectorRow.cs
Danil Iarosh cb2e83a596
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat(ui, prizes, spawners, editor): expand prize flow and add spawning/editor tooling
- доработан flow призовой витрины и связанного UI:
  добавлены `PrizeStandButtonView` и `RulesPrizeListPage`, обновлены
  `PrizeStandController`, `PrizeStandPrize`, `PrizeStandModal`,
  `PrizeStandSettingsModal`, `LevelPrizeSettingsModal`, `QRCodeSettingsModal`,
  `BalanceDisplay`, `ButtonTextFocusColor` и связанные prefab'ы,
  чтобы улучшить настройку, навигацию, отображение кнопок и prize flow
- расширена система настроек и сохранения значений:
  `OptionPicker` получил отдельный формат `ValueSelector`,
  обновлены `SettingsDatabase`, `SettingEntryDrawer`, `SettingUIItem`,
  `SettingsUIGenerator`, `ValueSelectorRow` и `MainSettingsDatabase.asset`,
  а сохранение option picker-значений, включая `gameplay.gameDifficulty`,
  исправлено и синхронизировано с остальными настройками
- добавлены универсальные спавнеры сущностей:
  реализованы `EntitySpawner` и `EntitySpawnerManager` с поддержкой
  весов префабов, точек спавна, заполнения по проценту, ограничений
  на количество и автоспавна при инициализации
- доработана логика игрового спавна и сложности:
  процент инициализационного спавна может зависеть от настройки
  `gameplay.gameDifficulty` с градациями 25/50/75/100,
  в одном цикле массового спавна повторно не используется та же точка,
  а gizmo спавнера теперь показывает только сами точки без линий
- упрощена работа в редакторе Unity и добавлены editor-инструменты:
  в `GameManager` лицензия пропускается при запуске в `UNITY_EDITOR`,
  а `HierarchySelectionCounter`, `HierarchySelectionCountOverlay`
  и `HierarchyBulkRenamerWindow` добавляют подсчет выделения,
  realtime overlay в `SceneView` и массовое переименование объектов
  по правилам нумерации из Unity Project Settings
- синхронизированы сцены, ресурсы и UI-ассеты под новый flow:
  обновлены `MainMenu`, `Level UI`, уровни `1-1` и `1-2`,
  строки настроек, кнопки, TMP/font assets, изображения UI,
  `prizes.json` и recovery-данные после правок интерфейса и логики
- очищены и переразложены часть внешних и служебных ресурсов:
  удалён пакет `Lava_Flowing_Shader`, переименованы/заменены
  отдельные UI-prefab'ы и добавлены сопутствующие проектные артефакты,
  которые сейчас используются обновлённой конфигурацией проекта
2026-04-28 14:05:05 +03:00

348 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace RR.Common.Settings.UI
{
/// <summary>
/// Компонент-Selectable для элемента настроек с выбором значения стрелками влево/вправо.
/// Поддерживает как int, так и float значения.
/// При зажатии кнопки скорость изменения плавно увеличивается.
/// Автоповтор целиком управляется Event System (inputActionsPerSecond).
/// Работает на одном префабе вместе с SettingUIItem.
/// </summary>
public class ValueSelectorRow : Selectable, IMoveHandler
{
private enum SelectorMode
{
Int,
Float,
OptionPicker
}
[Header("References")]
[SerializeField] private TMP_Text valueText;
private SettingEntry _entry;
private SelectorMode _mode;
private int _currentInt;
private float _currentFloat;
private int _intStep = 1;
private float _floatStep = 0.1f;
private int _currentOptionIndex;
// ───────────────────── Ускорение при зажатии ─────────────────────
/// <summary>Максимальный множитель шага при длительном зажатии.</summary>
private const int MaxStepMultiplier = 10;
/// <summary>Сколько вызовов OnMove до начала роста множителя.</summary>
private const int MultiplierStartCount = 6;
/// <summary>Сколько вызовов OnMove до достижения максимального множителя.</summary>
private const int MultiplierMaxCount = 30;
/// <summary>Если между двумя OnMove прошло больше этого времени — сброс множителя.</summary>
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;
_mode = SelectorMode.Int;
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;
_mode = SelectorMode.Float;
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();
}
// ───────────────────── Option Picker Setup ─────────────────────
public void SetupOptionPicker(SettingEntry entry, int currentIndex)
{
if (entry == null)
{
Debug.LogError("[ValueSelectorRow] Cannot setup option picker with null entry!");
return;
}
_entry = entry;
_mode = SelectorMode.OptionPicker;
int maxIndex = entry.options != null && entry.options.Count > 0
? entry.options.Count - 1
: 0;
_currentOptionIndex = Mathf.Clamp(currentIndex, 0, maxIndex);
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 (_mode == SelectorMode.Float)
{
float newValue = Mathf.Max(_entry.minFloat, _currentFloat - _floatStep * multiplier);
if (!Mathf.Approximately(newValue, _currentFloat))
SetValue(newValue);
}
else if (_mode == SelectorMode.OptionPicker)
{
int newValue = Mathf.Max(0, _currentOptionIndex - multiplier);
if (newValue != _currentOptionIndex)
SetValue(newValue);
}
else
{
int newValue = Mathf.Max(_entry.minInt, _currentInt - _intStep * multiplier);
if (newValue != _currentInt)
SetValue(newValue);
}
}
private void IncreaseValue(int multiplier)
{
if (_mode == SelectorMode.Float)
{
float newValue = Mathf.Min(_entry.maxFloat, _currentFloat + _floatStep * multiplier);
if (!Mathf.Approximately(newValue, _currentFloat))
SetValue(newValue);
}
else if (_mode == SelectorMode.OptionPicker)
{
int maxIndex = _entry.options != null && _entry.options.Count > 0
? _entry.options.Count - 1
: 0;
int newValue = Mathf.Min(maxIndex, _currentOptionIndex + multiplier);
if (newValue != _currentOptionIndex)
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 (_mode == SelectorMode.Float)
{
if (_entry != null && _entry.displayAsPercentage)
valueText.text = (_currentFloat * 100f).ToString("0") + "%";
else
valueText.text = _currentFloat.ToString("0.##");
}
else if (_mode == SelectorMode.OptionPicker)
{
if (_entry != null && _entry.options != null &&
_currentOptionIndex >= 0 && _currentOptionIndex < _entry.options.Count)
valueText.text = _entry.options[_currentOptionIndex];
else
valueText.text = string.Empty;
}
else
{
valueText.text = _currentInt.ToString();
}
}
// ───────────────────── Public API (int) ─────────────────────
public void SetValue(int value)
{
if (_entry == null) return;
if (_mode == SelectorMode.OptionPicker)
{
int maxIndex = _entry.options != null && _entry.options.Count > 0
? _entry.options.Count - 1
: 0;
int clampedOption = Mathf.Clamp(value, 0, maxIndex);
if (clampedOption != _currentOptionIndex)
{
_currentOptionIndex = clampedOption;
UpdateUI();
var manager = SettingsManager.Instance;
if (manager != null)
manager.SetInt(_entry.key, _currentOptionIndex);
else
Debug.LogError("[ValueSelectorRow] SettingsManager.Instance is 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 => _mode == SelectorMode.Float;
public string Key => _entry?.key;
}
}