Danro-Jump/Assets/Scripts/Runtime/UI/ArcadeInput/ArcadeStringInputState.cs
Danil Iarosh d39d160d92 Implement physical prize dispensing, service stats, and QR screen timeout
- Добавлен асинхронный процесс аппаратной выдачи физических призов (хоппер и витрина) в GameplayHudController.
- Реализована блокировка выдачи по 10-секундному таймауту датчика хоппера с гарантированной отправкой команды отключения двигателя '300'.
- Реализован автоматический таймаут неактивности на экране QR-кода с последующим переходом в главное меню.
- Расширен сервис статистики PrizeSessionStateService новыми долгосрочными счетчиками (сыгранные игры, очки, монеты, сбои связи COM, физ. выдачи и ошибки).
- Обновлен построитель сводки статистики в меню настроек для отображения новых счетчиков и списка последних 5 сессий.
- Написаны EditMode тесты для проверки инкремента счетчиков и запуска выдачи призов.
2026-06-23 10:36:13 +03:00

293 lines
8.7 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 System;
using System.Collections.Generic;
namespace DanroJump.UI.ArcadeInput
{
/// <summary>
/// Набор правил для ввода строки автоматным управлением: джойстик плюс одна кнопка.
/// </summary>
public readonly struct ArcadeStringInputProfile
{
public const string PhoneSymbols = " +0123456789()-";
public const string ShortTextSymbols = " 0123456789+-.,:/()ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя";
public const string TemplateSymbols = ShortTextSymbols + "{}_!?@#%&=*\"'";
public ArcadeStringInputProfile(string allowedSymbols, int maxLength)
{
AllowedSymbols = NormalizeSymbols(allowedSymbols);
MaxLength = Math.Max(1, maxLength);
}
public string AllowedSymbols { get; }
public int MaxLength { get; }
public static ArcadeStringInputProfile Phone(int maxLength = 18)
{
return new ArcadeStringInputProfile(PhoneSymbols, maxLength);
}
public static ArcadeStringInputProfile ShortText(int maxLength = 12)
{
return new ArcadeStringInputProfile(ShortTextSymbols, maxLength);
}
public static ArcadeStringInputProfile Text(int maxLength = 28)
{
return new ArcadeStringInputProfile(ShortTextSymbols, maxLength);
}
public static ArcadeStringInputProfile Template(int maxLength = 96)
{
return new ArcadeStringInputProfile(TemplateSymbols, maxLength);
}
private static string NormalizeSymbols(string symbols)
{
if (string.IsNullOrEmpty(symbols))
{
return " ";
}
var seen = new HashSet<char>();
var result = new List<char>(symbols.Length);
foreach (var symbol in symbols)
{
if (seen.Add(symbol))
{
result.Add(symbol);
}
}
return result.Count > 0 ? new string(result.ToArray()) : " ";
}
}
public enum ArcadeStringInputSelectionKind
{
Character,
Action
}
public enum ArcadeStringInputAction
{
Confirm,
Delete,
Cancel
}
public enum ArcadeStringInputSubmitResult
{
None,
Confirm,
Cancel
}
/// <summary>
/// Чистая модель посимвольного ввода для автоматов без физической клавиатуры.
/// </summary>
public sealed class ArcadeStringInputState
{
public const char BlankCharacter = ' ';
private static readonly ArcadeStringInputAction[] ActionOrder =
{
ArcadeStringInputAction.Confirm,
ArcadeStringInputAction.Delete,
ArcadeStringInputAction.Cancel
};
private readonly char[] slots;
private readonly char[] symbols;
private int selectedSlot;
private int selectedActionIndex;
private ArcadeStringInputSelectionKind selectionKind;
public ArcadeStringInputState(ArcadeStringInputProfile profile, string initialValue)
{
MaxLength = Math.Max(1, profile.MaxLength);
symbols = profile.AllowedSymbols.ToCharArray();
if (symbols.Length == 0)
{
symbols = new[] { BlankCharacter };
}
slots = new char[MaxLength];
for (var index = 0; index < slots.Length; index++)
{
slots[index] = BlankCharacter;
}
SetInitialValue(initialValue);
}
public int MaxLength { get; }
public int SelectedSlot => selectedSlot;
public ArcadeStringInputSelectionKind SelectionKind => selectionKind;
public ArcadeStringInputAction SelectedAction => ActionOrder[selectedActionIndex];
public string Value => new string(slots).TrimEnd();
public char GetSlot(int index)
{
return index >= 0 && index < slots.Length ? slots[index] : BlankCharacter;
}
public bool IsSlotSelected(int index)
{
return selectionKind == ArcadeStringInputSelectionKind.Character && selectedSlot == index;
}
public bool IsActionSelected(ArcadeStringInputAction action)
{
return selectionKind == ArcadeStringInputSelectionKind.Action && SelectedAction == action;
}
public void MoveLeft()
{
if (selectionKind == ArcadeStringInputSelectionKind.Action)
{
if (selectedActionIndex > 0)
{
selectedActionIndex--;
return;
}
selectionKind = ArcadeStringInputSelectionKind.Character;
selectedSlot = MaxLength - 1;
return;
}
if (selectedSlot <= 0)
{
selectionKind = ArcadeStringInputSelectionKind.Action;
selectedActionIndex = ActionOrder.Length - 1;
return;
}
selectedSlot--;
}
public void MoveRight()
{
if (selectionKind == ArcadeStringInputSelectionKind.Action)
{
if (selectedActionIndex < ActionOrder.Length - 1)
{
selectedActionIndex++;
return;
}
selectionKind = ArcadeStringInputSelectionKind.Character;
selectedSlot = 0;
return;
}
if (selectedSlot >= MaxLength - 1)
{
selectionKind = ArcadeStringInputSelectionKind.Action;
selectedActionIndex = 0;
return;
}
selectedSlot++;
}
public void MoveUp()
{
if (selectionKind == ArcadeStringInputSelectionKind.Action)
{
selectedActionIndex = Wrap(selectedActionIndex - 1, ActionOrder.Length);
return;
}
CycleSelectedCharacter(-1);
}
public void MoveDown()
{
if (selectionKind == ArcadeStringInputSelectionKind.Action)
{
selectedActionIndex = Wrap(selectedActionIndex + 1, ActionOrder.Length);
return;
}
CycleSelectedCharacter(1);
}
public ArcadeStringInputSubmitResult Submit()
{
if (selectionKind == ArcadeStringInputSelectionKind.Character)
{
MoveRight();
return ArcadeStringInputSubmitResult.None;
}
switch (SelectedAction)
{
case ArcadeStringInputAction.Confirm:
return ArcadeStringInputSubmitResult.Confirm;
case ArcadeStringInputAction.Cancel:
return ArcadeStringInputSubmitResult.Cancel;
case ArcadeStringInputAction.Delete:
DeleteCurrentCharacter();
return ArcadeStringInputSubmitResult.None;
default:
return ArcadeStringInputSubmitResult.None;
}
}
public void DeleteCurrentCharacter()
{
slots[selectedSlot] = BlankCharacter;
selectionKind = ArcadeStringInputSelectionKind.Character;
}
private void SetInitialValue(string initialValue)
{
if (string.IsNullOrEmpty(initialValue))
{
return;
}
var writeIndex = 0;
foreach (var symbol in initialValue)
{
if (writeIndex >= slots.Length)
{
break;
}
if (Array.IndexOf(symbols, symbol) < 0)
{
continue;
}
slots[writeIndex] = symbol;
writeIndex++;
}
}
private void CycleSelectedCharacter(int direction)
{
var symbolIndex = Array.IndexOf(symbols, slots[selectedSlot]);
if (symbolIndex < 0)
{
symbolIndex = 0;
}
slots[selectedSlot] = symbols[Wrap(symbolIndex + direction, symbols.Length)];
}
private static int Wrap(int value, int length)
{
if (length <= 0)
{
return 0;
}
var wrapped = value % length;
return wrapped < 0 ? wrapped + length : wrapped;
}
}
}