using System; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UI; namespace RR.Common.UI { /// /// Модальное окно с правилами игры. /// Поддерживает несколько страниц с переключением физическими кнопками влево/вправо. /// public class RulesModal : Modal { // ───────────────────────── Страницы ───────────────────────── [Header("Страницы правил")] [Tooltip("Список страниц правил. Каждая страница — это отдельный GameObject, " + "который будет показываться/скрываться при переключении.")] [SerializeField] private List pages = new(); // ───────────────────────── Навигация ───────────────────────── [Header("Навигация (Input System)")] [Tooltip("InputAction навигации (Navigate, тип Value/Vector2)")] [SerializeField] private InputActionReference navigateAction; [Tooltip("Порог срабатывания по оси X (0..1)")] [SerializeField] private float deadZone = 0.5f; [Tooltip("Зацикливать навигацию (с последней страницы на первую и наоборот)")] [SerializeField] private bool loop; // ───────────────────────── Индикатор ───────────────────────── [Header("Индикатор страниц (опционально)")] [Tooltip("Текст с номером страницы (например '2 / 5'). Может быть null.")] [SerializeField] private TMP_Text pageIndicatorText; [Tooltip("Формат текста индикатора. {0} — текущая, {1} — всего.")] [SerializeField] private string pageIndicatorFormat = "{0} / {1}"; [Tooltip("Контейнер с точками-индикаторами. Дочерние объекты = точки. Может быть null.")] [SerializeField] private Transform dotsContainer; [Tooltip("Цвет активной точки")] [SerializeField] private Color dotActiveColor = Color.white; [Tooltip("Цвет неактивной точки")] [SerializeField] private Color dotInactiveColor = new Color(1f, 1f, 1f, 0.35f); [Header("Стрелки навигации (опционально)")] [Tooltip("Изображение стрелки 'назад'. Скрывается, если предыдущей страницы нет.")] [SerializeField] private RawImage prevArrowImage; [Tooltip("Изображение стрелки 'вперёд'. Скрывается, если следующей страницы нет.")] [SerializeField] private RawImage nextArrowImage; // ───────────────────────── Состояние ───────────────────────── private int _currentPage; private bool _axisHeld; /// Текущий индекс страницы (0-based). public int CurrentPage => _currentPage; /// Общее количество страниц. public int PageCount => pages.Count; /// Вызывается при смене страницы. Параметр — новый индекс. public event Action OnPageChanged; // ───────────────────────── Lifecycle ───────────────────────── protected override void OnEnable() { base.OnEnable(); ShowPage(0); if (navigateAction != null && navigateAction.action != null) { navigateAction.action.Enable(); navigateAction.action.performed += OnNavigatePerformed; navigateAction.action.canceled += OnNavigateCanceled; } } protected override void OnDisable() { if (navigateAction != null && navigateAction.action != null) { navigateAction.action.performed -= OnNavigatePerformed; navigateAction.action.canceled -= OnNavigateCanceled; } _axisHeld = false; base.OnDisable(); } // ───────────────────────── Ввод (Input System) ───────────────────────── private void OnNavigatePerformed(InputAction.CallbackContext ctx) { float x = ctx.ReadValue().x; if (Mathf.Abs(x) < deadZone) { _axisHeld = false; return; } if (_axisHeld) return; _axisHeld = true; if (x < 0f) PrevPage(); else NextPage(); } private void OnNavigateCanceled(InputAction.CallbackContext ctx) { _axisHeld = false; } // ───────────────────────── Навигация ───────────────────────── /// Перейти на предыдущую страницу. public void PrevPage() { if (pages.Count == 0) return; int target = _currentPage - 1; if (target < 0) target = loop ? pages.Count - 1 : 0; ShowPage(target); } /// Перейти на следующую страницу. public void NextPage() { if (pages.Count == 0) return; int target = _currentPage + 1; if (target >= pages.Count) target = loop ? 0 : pages.Count - 1; ShowPage(target); } /// Показать страницу по индексу. public void ShowPage(int index) { if (pages.Count == 0) return; index = Mathf.Clamp(index, 0, pages.Count - 1); for (int i = 0; i < pages.Count; i++) { if (pages[i] != null) pages[i].SetActive(i == index); } _currentPage = index; UpdateIndicator(); OnPageChanged?.Invoke(_currentPage); } // ───────────────────────── UI Update ───────────────────────── private void UpdateIndicator() { if (pageIndicatorText != null) { pageIndicatorText.text = string.Format(pageIndicatorFormat, _currentPage + 1, pages.Count); } if (dotsContainer != null) { for (int i = 0; i < dotsContainer.childCount; i++) { var graphic = dotsContainer.GetChild(i).GetComponent(); if (graphic != null) graphic.color = i == _currentPage ? dotActiveColor : dotInactiveColor; } } if (prevArrowImage != null) { prevArrowImage.gameObject.SetActive(loop || _currentPage > 0); } if (nextArrowImage != null) { nextArrowImage.gameObject.SetActive(loop || _currentPage < pages.Count - 1); } } } }