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'ы и добавлены сопутствующие проектные артефакты, которые сейчас используются обновлённой конфигурацией проекта
777 lines
24 KiB
C#
777 lines
24 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.EventSystems;
|
||
using UnityEngine.InputSystem;
|
||
using UnityEngine.UI;
|
||
using RR.Common.Controls;
|
||
using RR.Common.Managers;
|
||
using RR.Common.Prizes;
|
||
using ControlsBoxStatus = RR.Common.Controls.BoxStatus;
|
||
|
||
namespace RR.Common.UI
|
||
{
|
||
/// <summary>
|
||
/// Отдельная модалка настройки глобальной конфигурации витрины.
|
||
/// Позволяет менять выбранную ячейку и порог очков для нее.
|
||
/// </summary>
|
||
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;
|
||
private Dictionary<int, ControlsBoxStatus> _boxStatuses = new();
|
||
private bool _statusesLoading;
|
||
private int _statusRequestVersion;
|
||
|
||
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;
|
||
_statusRequestVersion++;
|
||
base.OnDisable();
|
||
}
|
||
|
||
protected override void OnReturnedToTop()
|
||
{
|
||
_axisHeldY = false;
|
||
ResetHorizontalHold();
|
||
_pendingFocusTarget = null;
|
||
_pendingFocusFrames = 0;
|
||
RefreshRowHighlights();
|
||
RequestStatusesRefresh();
|
||
}
|
||
|
||
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;
|
||
_boxStatuses.Clear();
|
||
_statusesLoading = false;
|
||
_statusRequestVersion = 0;
|
||
|
||
RefreshUI();
|
||
RefreshRowHighlights();
|
||
RequestStatusesRefresh();
|
||
}
|
||
|
||
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<Vector2>();
|
||
|
||
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();
|
||
RequestStatusesRefresh();
|
||
}
|
||
|
||
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 = BuildDetailsText(connectionInfo, selectedBox);
|
||
}
|
||
|
||
private string BuildDetailsText(PrizeStandConnectionInfo connectionInfo, PrizeStandBoxConfig selectedBox)
|
||
{
|
||
if (!connectionInfo.IsConnected)
|
||
{
|
||
return "Витрина не подключена. Доступна только локальная настройка порогов.";
|
||
}
|
||
|
||
int readyCount = 0;
|
||
int readyWithCardCount = 0;
|
||
int emptyCount = 0;
|
||
int openedCount = 0;
|
||
int errorCount = 0;
|
||
int unknownCount = 0;
|
||
|
||
foreach (KeyValuePair<int, ControlsBoxStatus> entry in _boxStatuses)
|
||
{
|
||
switch (entry.Value)
|
||
{
|
||
case ControlsBoxStatus.OK:
|
||
readyCount++;
|
||
break;
|
||
case ControlsBoxStatus.OKplusCard:
|
||
readyWithCardCount++;
|
||
break;
|
||
case ControlsBoxStatus.Empty:
|
||
emptyCount++;
|
||
break;
|
||
case ControlsBoxStatus.Open:
|
||
case ControlsBoxStatus.OPENED:
|
||
openedCount++;
|
||
break;
|
||
case ControlsBoxStatus.OPEN_ERROR:
|
||
case ControlsBoxStatus.Error:
|
||
case ControlsBoxStatus.ParsingError:
|
||
errorCount++;
|
||
break;
|
||
default:
|
||
unknownCount++;
|
||
break;
|
||
}
|
||
}
|
||
|
||
string selectedStatusText = selectedBox != null
|
||
? $"Статус ячейки {selectedBox.boxNumber}: {GetStatusDescription(GetSelectedBoxStatus(selectedBox.boxNumber))}."
|
||
: "Ячейка не выбрана.";
|
||
|
||
string summaryText = _statusesLoading
|
||
? "Статусы витрины обновляются..."
|
||
: $"Готово: {readyCount}, готово с картой: {readyWithCardCount}, пусто: {emptyCount}, открыто: {openedCount}, ошибок: {errorCount}, неизвестно: {unknownCount}.";
|
||
|
||
return $"{selectedStatusText}\n{summaryText}";
|
||
}
|
||
|
||
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<TMP_Text>(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<Selectable>();
|
||
if (selectable == null)
|
||
{
|
||
selectable = row.gameObject.AddComponent<Selectable>();
|
||
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<PrizeStandRowMoveHandler>();
|
||
if (handler == null)
|
||
{
|
||
handler = row.gameObject.AddComponent<PrizeStandRowMoveHandler>();
|
||
}
|
||
|
||
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<PrizeStandBoxConfig>();
|
||
|
||
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 ControlsBoxStatus GetSelectedBoxStatus(int boxNumber)
|
||
{
|
||
return _boxStatuses.TryGetValue(boxNumber, out ControlsBoxStatus status)
|
||
? status
|
||
: ControlsBoxStatus.NotFound;
|
||
}
|
||
|
||
private async void RequestStatusesRefresh()
|
||
{
|
||
PrizeStandController stand = ComSystemManager.GetPrizeStandController();
|
||
PrizeStandConnectionInfo connectionInfo = PrizeSystemManager.Instance != null
|
||
? PrizeSystemManager.Instance.GetPrizeStandConnectionInfo()
|
||
: new PrizeStandConnectionInfo(false, 0);
|
||
|
||
if (stand == null || !connectionInfo.IsConnected)
|
||
{
|
||
_boxStatuses.Clear();
|
||
_statusesLoading = false;
|
||
RefreshUI();
|
||
return;
|
||
}
|
||
|
||
int requestVersion = ++_statusRequestVersion;
|
||
_statusesLoading = true;
|
||
RefreshUI();
|
||
|
||
try
|
||
{
|
||
Dictionary<int, ControlsBoxStatus> statuses = await stand.GetStatusesAsync();
|
||
if (requestVersion != _statusRequestVersion || this == null || !gameObject)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_boxStatuses = statuses ?? new Dictionary<int, ControlsBoxStatus>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
if (requestVersion != _statusRequestVersion || this == null || !gameObject)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_boxStatuses.Clear();
|
||
Debug.LogWarning($"[PrizeStandSettingsModal] Не удалось обновить статусы витрины: {ex.Message}");
|
||
}
|
||
finally
|
||
{
|
||
if (requestVersion == _statusRequestVersion && this != null && gameObject)
|
||
{
|
||
_statusesLoading = false;
|
||
RefreshUI();
|
||
}
|
||
}
|
||
}
|
||
|
||
private static string GetStatusDescription(ControlsBoxStatus status)
|
||
{
|
||
return status switch
|
||
{
|
||
ControlsBoxStatus.OK => "готова к выдаче",
|
||
ControlsBoxStatus.OKplusCard => "готова к выдаче и работе по карте",
|
||
ControlsBoxStatus.Empty => "пустая",
|
||
ControlsBoxStatus.Open => "открыта",
|
||
ControlsBoxStatus.OPENED => "только что открывалась успешно",
|
||
ControlsBoxStatus.OPEN_ERROR => "ошибка открытия",
|
||
ControlsBoxStatus.Error => "ошибка ячейки",
|
||
ControlsBoxStatus.ParsingError => "ошибка чтения статуса",
|
||
ControlsBoxStatus.NotFound => "нет данных",
|
||
_ => status.ToString()
|
||
};
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
}
|