Danro/Assets/Scripts/UI/LevelSettingsModal.cs
Danil Iarosh a40b761c81
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat(prizes, ui): add configurable level prize flow and launcher assets
- добавлена новая система призов:
  PrizeBase, PrizeType, PrizeDatabase, LevelPrizeConfig и реализации
  для hopper, QR-кода, отсутствия приза и стойки с призом
- добавлена конфигурация призов уровней через `prizes.json`
  и связанная логика загрузки/обработки в менеджерах игры
- расширен UI главного меню и настроек уровней:
  добавлены LevelSettingsModal, LevelPrizeSettingsModal,
  QRCodeModal и PrizeStandModal
- обновлена навигация модальных окон и связанный UI-код:
  ModalManager, MainMenu, RulesModal, PauseMenu,
  PrizeErrorDisplay, UIRowPointer и WinBannerController
- доработаны настройки и генерация UI-элементов:
  обновлены SettingsDatabase, SettingsUIGenerator,
  SettingUIItem, ValueSelectorRow и editor drawer для настроек
- обновлены и добавлены UI-prefab'ы для кнопок, строк настроек,
  панели групп и кнопки настроек уровня
- обновлены сцены `MainMenu` и `Level UI` под новый флоу
  настройки уровней, выбора призов и отображения модалок
- добавлены файлы лаунчера в `StreamingAssets`,
  включая `RRLauncher.exe`, служебные meta-файлы и документацию
- улучшено логирование через FileLogger
  и удален устаревший код лицензирования
- удалены больше неиспользуемые файлы:
  `Licensing.cs`, `SteppedSlider.cs` и recovery-сцены
2026-03-16 08:15:36 +03:00

283 lines
12 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.Collections.Generic;
using System.IO;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
namespace RR.Common.UI
{
/// <summary>
/// Модальное окно выбора уровня для настройки награды за его прохождение.
/// Строит список уровней из <see cref="SceneManager.sceneCountInBuildSettings"/> и
/// предоставляет выбор текущего уровня как контекст для дальнейшего редактирования призов.
/// </summary>
public class LevelSettingsModal : Modal
{
[Header("References")]
[SerializeField] private Transform contentRoot;
[SerializeField] private GameObject levelButtonPrefab;
[Header("Display")]
[SerializeField] private bool rebuildOnOpen = true;
[SerializeField] private bool includeOnlyLevelsFolder = true;
[SerializeField] private string levelsFolderName = "Levels";
[SerializeField] private List<string> ignoredSceneNames = new() { "MainMenu", "Level UI" };
[Header("Selection Visual")]
[SerializeField] private Vector3 normalScale = Vector3.one;
[SerializeField] private Vector3 selectedScale = new(1.05f, 1.05f, 1f);
[Header("Модальное окно настройки приза")]
[Tooltip("Ссылка на LevelPrizeSettingsModal. Открывается при нажатии на кнопку уровня.")]
[SerializeField] private LevelPrizeSettingsModal levelPrizeSettingsModal;
private readonly List<LevelButtonView> _spawnedButtons = new();
private LevelButtonView _selectedButton;
/// <summary>
/// Имя сцены выбранного уровня.
/// Используется внешними компонентами как ключ для загрузки или сохранения настроек приза.
/// </summary>
public string SelectedSceneName => _selectedButton != null ? _selectedButton.SceneName : string.Empty;
/// <summary>
/// Открывает окно выбора уровня.
/// Перед открытием при необходимости пересобирает список кнопок из актуального набора сцен в Build Settings.
/// </summary>
public override void Open()
{
if (rebuildOnOpen || _spawnedButtons.Count == 0)
{
Rebuild();
}
base.Open();
}
[ContextMenu("Rebuild Level Buttons")]
public void Rebuild()
{
if (!ValidateReferences())
{
return;
}
ClearButtons();
List<string> sceneNames = GetLevelSceneNames();
foreach (string t in sceneNames)
{
CreateButton(t);
}
if (_spawnedButtons.Count <= 0) return;
SelectButton(_spawnedButtons[0]);
SetFirstSelected(_spawnedButtons[0].Button);
}
/// <summary>
/// Проверяет, что в инспекторе назначены контейнер для кнопок и префаб кнопки уровня.
/// </summary>
/// <returns>
/// <see langword="true"/>, если окно готово к построению списка уровней;
/// иначе <see langword="false"/>.
/// </returns>
private bool ValidateReferences()
{
if (contentRoot == null)
{
Debug.LogError($"[{nameof(LevelSettingsModal)}] Content Root is not assigned on {name}.", this);
return false;
}
if (levelButtonPrefab == null)
{
Debug.LogError($"[{nameof(LevelSettingsModal)}] Level Button Prefab is not assigned on {name}.", this);
return false;
}
return true;
}
/// <summary>
/// Создает кнопку для конкретной сцены уровня и настраивает ее отображение.
/// Нажатие на кнопку только выбирает уровень для последующей настройки награды.
/// </summary>
/// <param name="sceneName">Имя сцены уровня, отображаемое в интерфейсе.</param>
private void CreateButton(string sceneName)
{
GameObject buttonObject = Instantiate(levelButtonPrefab, contentRoot);
buttonObject.name = $"LevelButton_{sceneName}";
Button button = buttonObject.GetComponentInChildren<Button>(true);
if (button == null)
{
Debug.LogError(
$"[{nameof(LevelSettingsModal)}] Prefab '{levelButtonPrefab.name}' must contain a Button component.",
buttonObject);
Destroy(buttonObject);
return;
}
TMP_Text tmpText = buttonObject.GetComponentInChildren<TMP_Text>(true);
if (tmpText != null)
{
tmpText.text = sceneName;
}
else
{
Text legacyText = buttonObject.GetComponentInChildren<Text>(true);
if (legacyText != null)
{
legacyText.text = sceneName;
}
}
LevelButtonView buttonView = new(buttonObject.transform, sceneName, buttonObject.transform as RectTransform, button);
button.onClick.RemoveAllListeners();
button.onClick.AddListener(() => OnLevelButtonClicked(buttonView));
ApplySelectedVisual(buttonView, false);
_spawnedButtons.Add(buttonView);
}
/// <summary>
/// Обрабатывает нажатие на кнопку уровня.
/// Не запускает уровень, а только делает его текущим выбранным элементом для настройки приза.
/// </summary>
/// <param name="buttonView">Представление нажатой кнопки уровня.</param>
private void OnLevelButtonClicked(LevelButtonView buttonView)
{
SelectButton(buttonView);
Debug.Log($"[{nameof(LevelSettingsModal)}] Selected level: {buttonView.SceneName}");
// Открываем окно настройки приза для выбранного уровня
if (levelPrizeSettingsModal != null)
levelPrizeSettingsModal.OpenForLevel(buttonView.SceneName);
}
/// <summary>
/// Обновляет текущий выбранный уровень и переключает визуальное выделение между кнопками.
/// </summary>
/// <param name="buttonView">Кнопка уровня, которая должна стать активной.</param>
private void SelectButton(LevelButtonView buttonView)
{
if (_selectedButton != null)
{
ApplySelectedVisual(_selectedButton, false);
}
_selectedButton = buttonView;
if (_selectedButton != null)
{
ApplySelectedVisual(_selectedButton, true);
}
}
/// <summary>
/// Включает или отключает визуальное состояние выбранного уровня.
/// Используется, чтобы в интерфейсе было видно, для какого уровня сейчас настраивается награда.
/// </summary>
/// <param name="buttonView">Кнопка, для которой обновляется внешний вид.</param>
/// <param name="isSelected">
/// <see langword="true"/>, если кнопка должна быть показана как выбранная;
/// иначе <see langword="false"/>.
/// </param>
private void ApplySelectedVisual(LevelButtonView buttonView, bool isSelected)
{
if (buttonView.RectTransform != null)
{
buttonView.RectTransform.localScale = isSelected ? selectedScale : normalScale;
}
}
/// <summary>
/// Собирает список имен сцен уровней, доступных для настройки наград.
/// Источником служат сцены, добавленные в Build Settings, с учетом настроенной фильтрации.
/// </summary>
/// <returns>Список имен сцен уровней для построения кнопок в окне настроек.</returns>
private List<string> GetLevelSceneNames()
{
var result = new List<string>();
int sceneCount = SceneManager.sceneCountInBuildSettings;
for (int i = 0; i < sceneCount; i++)
{
string scenePath = SceneUtility.GetScenePathByBuildIndex(i);
if (string.IsNullOrWhiteSpace(scenePath))
{
continue;
}
if (includeOnlyLevelsFolder)
{
string folderToken = $"/{levelsFolderName}/";
if (!scenePath.Replace("\\", "/").Contains(folderToken))
{
continue;
}
}
string sceneName = Path.GetFileNameWithoutExtension(scenePath);
if (ignoredSceneNames.Contains(sceneName))
{
continue;
}
result.Add(sceneName);
}
return result;
}
/// <summary>
/// Удаляет ранее созданные кнопки уровней и сбрасывает текущий выбор.
/// Используется перед полной пересборкой списка.
/// </summary>
private void ClearButtons()
{
foreach (LevelButtonView buttonView in _spawnedButtons)
{
if (buttonView.Root != null)
{
Destroy(buttonView.Root.gameObject);
}
}
_spawnedButtons.Clear();
_selectedButton = null;
}
/// <summary>
/// Внутреннее представление кнопки уровня, используемое модальным окном
/// для хранения ссылки на UI-элементы и имя сцены выбранного уровня.
/// </summary>
private sealed class LevelButtonView
{
/// <summary>
/// Инициализирует представление кнопки уровня, используемое для выбора уровня
/// при настройке награды за его прохождение.
/// </summary>
/// <param name="root">Корневой объект экземпляра префаба кнопки.</param>
/// <param name="sceneName">Имя сцены уровня.</param>
/// <param name="rectTransform">Прямоугольная трансформация корневого UI-элемента.</param>
/// <param name="button">Компонент кнопки, принимающий пользовательский ввод.</param>
public LevelButtonView(Transform root, string sceneName, RectTransform rectTransform, Button button)
{
Root = root;
SceneName = sceneName;
RectTransform = rectTransform;
Button = button;
}
public Transform Root { get; }
public string SceneName { get; }
public RectTransform RectTransform { get; }
public Button Button { get; }
}
}
}