Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- доработан 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'ы и добавлены сопутствующие проектные артефакты, которые сейчас используются обновлённой конфигурацией проекта
348 lines
13 KiB
C#
348 lines
13 KiB
C#
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;
|
||
}
|
||
}
|
||
|