Danro/Assets/Scripts/UI/TextInput/TextInputModal.cs
Danil Iarosh 2417f58f61
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat: Add text input system and balance management with settings improvements
- Добавлена система ввода текста через модальное окно с ячейками для аркадного автомата
- Реализован TextInputModal с поддержкой кастомных алфавитов и навигации
- Добавлен TextInputCell для ввода отдельных символов с циклической навигацией
- Создан TextInputSettingsBinder для интеграции ввода текста с системой настроек
- Реализована компактная сериализация настроек (только нужные поля в JSON)

- Внедрена система баланса игрока (BalanceManager)
- Баланс начисляется при оплате импульсом, списывается при начале/продолжении игры
- Добавлен InactivityManager для выхода в меню при отсутствии активности (5 минут)

- Улучшена система настроек
- Добавлены флаги shownInSettings и exposed для управления видимостью настроек
- Реализована автоматическая регистрация/отписка SettingsButtonBinder при активации/деактивации
- Исправлена загрузка настроек с проверкой типов через Sanitize()
- SettingsApplier упрощен - оставлена только поддержка AudioManager
- Добавлен ValueSelectorRow для выбора целочисленных значений с шагом

- Удалены примеры и документация из production кода (QUICK_SETUP.md, TextInputExample, AdvancedLoggingExample)
- Убран весь отладочный вывод Debug.Log из системы настроек и ввода текста
- Очищены неразрывные пробелы из файлов проекта
- Добавлены префабы UI (InputCell)
2026-02-11 16:27:33 +03:00

498 lines
19 KiB
C#
Raw Permalink 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;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
namespace RR.Common.UI
{
/// <summary>
/// Модальное окно для ввода текста через выбор символов.
/// Использует ячейки TextInputCell для ввода каждого символа.
/// </summary>
public class TextInputModal : Modal
{
[Header("Text Input Settings")]
[SerializeField] private int cellCount = 10;
[SerializeField] private string[] alphabet = new string[]
{
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
"U", "V", "W", "X", "Y", "Z", " ", "-", "_"
};
[Header("UI References")]
[SerializeField] private Transform cellsContainer;
[SerializeField] private GameObject cellPrefab;
[SerializeField] private TextMeshProUGUI titleText;
[SerializeField] private TextMeshProUGUI previewText;
[SerializeField] private Button confirmButton;
[SerializeField] private Button cancelButton;
private List<TextInputCell> cells = new List<TextInputCell>();
private int currentCellIndex;
// Параметры для отложенной генерации (когда модалка станет активной)
private bool needsRegeneration = false;
private string pendingText = "";
// События
public event Action<string> OnTextConfirmed;
public event Action OnTextCancelled;
protected override void Awake()
{
base.Awake();
// НЕ генерируем ячейки здесь - они будут созданы через SetCellCount() или SetAlphabet()
// Настраиваем кнопки
if (confirmButton)
{
confirmButton.onClick.AddListener(ConfirmInput);
}
if (cancelButton)
{
cancelButton.onClick.AddListener(CancelInput);
}
}
private void OnDestroy()
{
// Отписываемся от всех событий ячеек
foreach (var cell in cells)
{
if (cell != null)
{
cell.OnSymbolChanged -= OnCellSymbolChanged;
}
}
// Отписываемся от кнопок
if (confirmButton)
{
confirmButton.onClick.RemoveListener(ConfirmInput);
}
if (cancelButton)
{
cancelButton.onClick.RemoveListener(CancelInput);
}
}
private void Update()
{
if (!gameObject.activeInHierarchy) return;
// Синхронизируем currentCellIndex с текущим выбранным элементом EventSystem
SyncCurrentCellWithEventSystem();
}
/// <summary>
/// Синхронизирует currentCellIndex с текущим выбранным элементом EventSystem
/// </summary>
private void SyncCurrentCellWithEventSystem()
{
if (UnityEngine.EventSystems.EventSystem.current == null) return;
var selected = UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject;
if (selected == null) return;
// Проверяем, является ли выбранный объект одной из наших ячеек
var selectedCell = selected.GetComponent<TextInputCell>();
if (selectedCell != null)
{
int index = cells.IndexOf(selectedCell);
if (index >= 0 && index != currentCellIndex)
{
currentCellIndex = index;
// Обновляем визуальное выделение без установки фокуса (чтобы избежать рекурсии)
for (int i = 0; i < cells.Count; i++)
{
cells[i].SetCellSelected(i == currentCellIndex);
}
UpdatePreview();
}
}
}
/// <summary>
/// Генерирует ячейки для ввода текста
/// </summary>
/// <param name="initialText">Начальный текст для заполнения ячеек</param>
private void GenerateCells(string initialText = "")
{
Debug.Log($"[TextInputModal] GenerateCells: cellCount={cellCount}, initialText='{initialText}', activeInHierarchy={gameObject.activeInHierarchy}");
if (cellPrefab == null || cellsContainer == null)
{
Debug.LogError("[TextInputModal] Cell prefab or container is not assigned!");
return;
}
// Отписываемся от событий старых ячеек и очищаем их
foreach (var cell in cells)
{
if (cell != null)
{
cell.OnSymbolChanged -= OnCellSymbolChanged;
}
}
// Очищаем существующие ячейки
foreach (Transform child in cellsContainer)
{
Destroy(child.gameObject);
}
cells.Clear();
// Создаем новые ячейки с начальными символами
for (int i = 0; i < cellCount; i++)
{
GameObject cellObj = Instantiate(cellPrefab, cellsContainer);
TextInputCell cell = cellObj.GetComponent<TextInputCell>();
if (cell != null)
{
// Определяем начальный символ для этой ячейки
string symbol = (i < initialText.Length) ? initialText[i].ToString() : null;
// Инициализируем ячейку с алфавитом и символом
cell.Initialize(alphabet, 0, symbol);
// Подписываемся на изменение символа
cell.OnSymbolChanged += OnCellSymbolChanged;
cells.Add(cell);
}
else
{
Debug.LogError($"[TextInputModal] Cell prefab doesn't have TextInputCell component!");
}
}
Debug.Log($"[TextInputModal] Created {cells.Count} cells. Setting up navigation...");
// Настраиваем циклическую горизонтальную навигацию
SetupCyclicNavigation();
// Выделяем первую ячейку и устанавливаем фокус
if (cells.Count > 0)
{
currentCellIndex = 0;
SetFirstSelected(cells[0]);
UpdateCellSelection();
}
// Обновляем preview с актуальными данными из ячеек
UpdatePreview();
Debug.Log($"[TextInputModal] GenerateCells completed successfully");
}
/// <summary>
/// Настраивает циклическую горизонтальную навигацию между всеми элементами
/// </summary>
private void SetupCyclicNavigation()
{
if (cells.Count == 0) return;
// Создаем список всех элементов для навигации: ячейки + кнопки
List<Selectable> allElements = new List<Selectable>();
// Добавляем все ячейки
allElements.AddRange(cells);
// Добавляем кнопки в конец
if (confirmButton != null) allElements.Add(confirmButton);
if (cancelButton != null) allElements.Add(cancelButton);
// Настраиваем циклическую горизонтальную навигацию для всех элементов
for (int i = 0; i < allElements.Count; i++)
{
var element = allElements[i];
var nav = new Navigation();
nav.mode = Navigation.Mode.Explicit;
// Предыдущий элемент (циклично)
int prevIndex = (i - 1 + allElements.Count) % allElements.Count;
nav.selectOnLeft = allElements[prevIndex];
// Следующий элемент (циклично)
int nextIndex = (i + 1) % allElements.Count;
nav.selectOnRight = allElements[nextIndex];
// Вверх/вниз: только для ячеек (не для кнопок)
if (element is TextInputCell)
{
// Для ячеек вверх/вниз обрабатываются в OnMove (изменение символа)
nav.selectOnUp = null;
nav.selectOnDown = null;
}
else
{
// Для кнопок вверх/вниз не используются
nav.selectOnUp = null;
nav.selectOnDown = null;
}
element.navigation = nav;
}
}
/// <summary>
/// Открывает модальное окно с заданным заголовком и начальным текстом
/// </summary>
public void OpenForInput(string title, string initialText = "")
{
Debug.Log($"[TextInputModal] OpenForInput called. Title: '{title}', InitialText: '{initialText}'");
if (titleText)
{
titleText.text = title;
}
// Сохраняем текст для генерации после активации
pendingText = initialText ?? "";
// Помечаем что нужна регенерация (если еще не помечено)
if (cells.Count != cellCount || cells.Count == 0)
{
needsRegeneration = true;
}
// Проверяем что ячейки не null
if (!needsRegeneration && cells.Count > 0)
{
for (int i = 0; i < cells.Count; i++)
{
if (cells[i] == null)
{
Debug.LogWarning($"[TextInputModal] Cell {i} is null, will regenerate");
needsRegeneration = true;
break;
}
}
}
Debug.Log($"[TextInputModal] needsRegeneration: {needsRegeneration}, calling Open()");
// Open() активирует модалку и вызовет генерацию ячеек
Open();
}
/// <summary>
/// Переопределяем Open для генерации ячеек ПОСЛЕ активации
/// </summary>
public override void Open()
{
Debug.Log($"[TextInputModal] Open() called. needsRegeneration: {needsRegeneration}, pendingText: '{pendingText}'");
Debug.Log($"[TextInputModal] BEFORE base.Open() - gameObject.activeSelf: {gameObject.activeSelf}, activeInHierarchy: {gameObject.activeInHierarchy}");
// Сначала активируем модалку
base.Open();
Debug.Log($"[TextInputModal] AFTER base.Open() - gameObject.activeSelf: {gameObject.activeSelf}, activeInHierarchy: {gameObject.activeInHierarchy}");
// Теперь, когда модалка активна, генерируем ячейки если нужно
if (needsRegeneration)
{
Debug.Log($"[TextInputModal] Generating cells NOW (after activation) with text: '{pendingText}'");
GenerateCells(pendingText);
needsRegeneration = false;
}
else if (!string.IsNullOrEmpty(pendingText))
{
Debug.Log($"[TextInputModal] Setting text to existing cells: '{pendingText}'");
SetText(pendingText);
}
// Очищаем pending text
pendingText = "";
Debug.Log($"[TextInputModal] Open() completed. cells.Count: {cells.Count}");
}
/// <summary>
/// Устанавливает текст в ячейки (переинициализирует ячейки с новыми символами)
/// </summary>
public void SetText(string text)
{
if (text == null) text = "";
Debug.Log($"[TextInputModal] SetText called with: '{text}'");
// Проверяем, что ячейки созданы
if (cells.Count == 0)
{
Debug.LogWarning("[TextInputModal] No cells available, cannot set text!");
return;
}
Debug.Log($"[TextInputModal] Setting text to {cells.Count} cells");
// Переинициализируем каждую ячейку с новым символом
for (int i = 0; i < cells.Count; i++)
{
if (cells[i] == null)
{
Debug.LogWarning($"[TextInputModal] Cell {i} is null!");
continue;
}
// Определяем символ для этой ячейки
string symbol = (i < text.Length) ? text[i].ToString() : null;
Debug.Log($"[TextInputModal] Cell {i}: initializing with symbol '{symbol}', alphabet has {(cells[i] != null ? alphabet.Length : 0)} symbols");
// Переинициализируем ячейку с новым символом
cells[i].Initialize(alphabet, 0, symbol);
}
Debug.Log($"[TextInputModal] SetText completed, updating preview");
UpdatePreview();
}
/// <summary>
/// Получает текущий введенный текст
/// </summary>
public string GetText()
{
string result = "";
foreach (var cell in cells)
{
string symbol = cell.CurrentSymbol;
// Добавляем символ, но первый символ алфавита (обычно пробел или 0) считается "пустым"
if (cell.CurrentIndex == 0 && string.IsNullOrEmpty(symbol.Trim()))
{
result += " "; // Добавляем пробел для пустых ячеек
}
else
{
result += symbol;
}
}
return result.TrimEnd(); // Убираем пробелы в конце
}
/// <summary>
/// Вызывается когда символ в ячейке изменяется
/// </summary>
private void OnCellSymbolChanged(TextInputCell cell)
{
UpdatePreview();
}
/// <summary>
/// Обновляет выделение ячеек
/// </summary>
private void UpdateCellSelection()
{
for (int i = 0; i < cells.Count; i++)
{
cells[i].SetCellSelected(i == currentCellIndex);
}
// Устанавливаем фокус EventSystem на текущую ячейку
if (currentCellIndex >= 0 && currentCellIndex < cells.Count && cells[currentCellIndex] != null)
{
if (UnityEngine.EventSystems.EventSystem.current != null)
{
UnityEngine.EventSystems.EventSystem.current.SetSelectedGameObject(cells[currentCellIndex].gameObject);
}
}
}
/// <summary>
/// Обновляет предпросмотр текста
/// </summary>
private void UpdatePreview()
{
if (previewText)
{
previewText.text = GetText();
}
}
/// <summary>
/// Подтверждает ввод текста
/// </summary>
private void ConfirmInput()
{
string text = GetText();
OnTextConfirmed?.Invoke(text);
Close();
}
/// <summary>
/// Отменяет ввод текста
/// </summary>
private void CancelInput()
{
OnTextCancelled?.Invoke();
Close();
}
/// <summary>
/// Устанавливает алфавит для всех ячеек
/// </summary>
public void SetAlphabet(string[] newAlphabet)
{
Debug.Log($"[TextInputModal] SetAlphabet called with {(newAlphabet != null ? newAlphabet.Length : 0)} symbols");
if (newAlphabet == null || newAlphabet.Length == 0)
{
Debug.LogWarning("[TextInputModal] Cannot set empty alphabet!");
return;
}
alphabet = newAlphabet;
// Помечаем что нужна регенерация (она произойдет при открытии модалки)
needsRegeneration = true;
Debug.Log($"[TextInputModal] Alphabet set, marked for regeneration");
}
/// <summary>
/// Устанавливает количество ячеек и перегенерирует их
/// </summary>
public void SetCellCount(int count)
{
if (count <= 0)
{
return;
}
cellCount = count;
// Помечаем что нужна регенерация (она произойдет при открытии модалки)
needsRegeneration = true;
}
/// <summary>
/// Сбрасывает все ячейки к первому символу
/// </summary>
public void ResetAllCells()
{
foreach (var cell in cells)
{
if (cell != null)
{
cell.Initialize(alphabet);
}
}
currentCellIndex = 0;
UpdateCellSelection();
UpdatePreview();
}
}
}