Danro-Jump/Assets/Scripts/Runtime/UI/ArcadeInput/ArcadeStringInputState.cs
Danil Iarosh c1b72854f4 Implement prize QR and service UI polish
Подробные изменения:

- Добавлена система призовых уровней и выдачи призов с QR-контуром Telegram, шаблоном сообщения и генерацией QR без внешних библиотек.

- Доработаны сервисные настройки: действия кнопок, модальные окна, ввод с джойстика/клавиатуры, статистика, настройки призов и маршрутизация QR.

- Переработаны gameplay HUD и overlay-панели на UI Toolkit со спрайтовыми плашками, DEV BUILD-статистикой, экраном продолжения и экраном результата приза.

- Исправлены сценарии смерти, GameOver, продолжения игры, начисления очков за высоту и перехода в главное меню.

- Перенесены UI-шаблоны из Resources в Assets/UI, подключена Addressables-загрузка шаблонов настроек и standalone-стили для MainMenuModals.

- Добавлены EditMode/PlayMode тесты для призов, QR, arcade input, UI-модалок, scene flow, HUD и gameplay-сессии.

- Обновлены сцены, настройки Addressables, service settings database, документация и дорожная карта.

Проверки:

- Targeted EditMode QR/UI/settings: 23/23 passed

- Targeted UI templates/EditMode: 17/17 passed

- MainMenuUxmlTests: 3/3 passed

- Full EditMode: 129/129 passed

- PlayMode: 6/6 passed

- Runtime Addressables template load check: loaded=5

- Unity Console после финального refresh: 0 errors
2026-06-12 16:43:28 +03:00

287 lines
8.5 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 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);
}
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;
}
}
}