Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- Добавлена система ввода текста через модальное окно с ячейками для аркадного автомата - Реализован TextInputModal с поддержкой кастомных алфавитов и навигации - Добавлен TextInputCell для ввода отдельных символов с циклической навигацией - Создан TextInputSettingsBinder для интеграции ввода текста с системой настроек - Реализована компактная сериализация настроек (только нужные поля в JSON) - Внедрена система баланса игрока (BalanceManager) - Баланс начисляется при оплате импульсом, списывается при начале/продолжении игры - Добавлен InactivityManager для выхода в меню при отсутствии активности (5 минут) - Улучшена система настроек - Добавлены флаги shownInSettings и exposed для управления видимостью настроек - Реализована автоматическая регистрация/отписка SettingsButtonBinder при активации/деактивации - Исправлена загрузка настроек с проверкой типов через Sanitize() - SettingsApplier упрощен - оставлена только поддержка AudioManager - Добавлен ValueSelectorRow для выбора целочисленных значений с шагом - Удалены примеры и документация из production кода (QUICK_SETUP.md, TextInputExample, AdvancedLoggingExample) - Убран весь отладочный вывод Debug.Log из системы настроек и ввода текста - Очищены неразрывные пробелы из файлов проекта - Добавлены префабы UI (InputCell)
498 lines
19 KiB
C#
498 lines
19 KiB
C#
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();
|
||
}
|
||
}
|
||
}
|
||
|