Danro/Assets/Scripts/Settings/UI/SettingsUIGenerator.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

362 lines
14 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;
using System.Collections.Generic;
using System.Linq;
using RR.Common.Settings;
using RR.Common.Settings.UI;
using TMPro;
using UnityEngine;
namespace RR.Common.UI
{
public class SettingsUIGenerator : MonoBehaviour
{
[Header("Source")] [SerializeField] private SettingsDatabase database;
[Header("Layout")] [SerializeField] private Transform contentRoot;
/// <summary>
/// Корневой Transform для контента настроек
/// </summary>
public Transform ContentRoot => contentRoot;
[Header("Grouping")] [SerializeField] private bool groupByGroup = true;
[SerializeField] private bool collapsibleGroups = true;
[SerializeField] private SettingsGroupPanel groupPanelPrefab; // префаб группы (foldout)
[SerializeField] private bool defaultGroupsExpanded = true;
[SerializeField] private bool rememberGroupFoldout = true;
[Tooltip("Если заполнить — группы будут идти в этом порядке, остальные добавятся в конец по алфавиту.")]
[SerializeField]
private List<string> groupOrder = new();
[Header("Sorting")] [SerializeField] private bool sortByNameInsideGroup = true;
[Header("Prefabs (rows)")] [SerializeField]
private SettingUIItem toggleRowPrefab;
[SerializeField] private SettingUIItem sliderRowPrefab;
[SerializeField] private SettingUIItem valueSelectorRowPrefab;
[SerializeField] private SettingUIItem dropdownRowPrefab;
[SerializeField] private SettingUIItem inputRowPrefab;
[SerializeField] private SettingUIItem buttonRowPrefab;
[SerializeField] private SettingUIItem optionPickerRowPrefab;
[Header("Behaviour")] [SerializeField] private bool buildOnEnable = true;
[SerializeField] private bool clearBeforeBuild = true;
[SerializeField] private bool showDescription = true;
private readonly List<SettingUIItem> spawnedItems = new();
private readonly List<SettingsGroupPanel> spawnedGroups = new();
private Coroutine _buildCoroutine;
/// <summary>
/// Событие вызывается когда построение UI завершено
/// </summary>
public event Action OnBuildComplete;
/// <summary>
/// Событие вызывается при сворачивании/разворачивании любой группы настроек
/// </summary>
public event Action OnGroupExpandedChanged;
/// <summary>
/// Первый созданный Selectable элемент
/// </summary>
public UnityEngine.UI.Selectable FirstSelectable { get; private set; }
private void OnEnable()
{
if (buildOnEnable)
{
// Запускаем корутину, которая ждёт SettingsManager
if (_buildCoroutine != null) StopCoroutine(_buildCoroutine);
_buildCoroutine = StartCoroutine(BuildWhenReady());
}
else
{
RefreshValues();
}
}
private void OnDisable()
{
if (_buildCoroutine != null)
{
StopCoroutine(_buildCoroutine);
_buildCoroutine = null;
}
}
private IEnumerator BuildWhenReady()
{
// Ждём пока SettingsManager инициализируется (максимум 2 секунды)
float timeout = 2f;
float elapsed = 0f;
while (!SettingsManager.Instance && elapsed < timeout)
{
elapsed += Time.unscaledDeltaTime;
yield return null;
}
if (!SettingsManager.Instance)
{
Debug.LogError("[SettingsUIGenerator] SettingsManager.Instance not found after waiting. Make sure SettingsManager is in scene.");
yield break;
}
Build();
// Ждём кадр чтобы layout обновился
yield return null;
// Принудительно обновляем layout для корректной навигации
if (contentRoot != null && contentRoot is RectTransform rectTransform)
{
UnityEngine.UI.LayoutRebuilder.ForceRebuildLayoutImmediate(rectTransform);
}
// Ещё кадр для применения layout
yield return null;
// Находим первый Selectable элемент
if (contentRoot != null)
{
FirstSelectable = contentRoot.GetComponentInChildren<UnityEngine.UI.Selectable>();
}
// Оповещаем о завершении
OnBuildComplete?.Invoke();
_buildCoroutine = null;
}
public void Build()
{
if (!database || !contentRoot)
{
Debug.LogWarning("[SettingsUIGenerator] database/contentRoot not set.");
return;
}
if (!SettingsManager.Instance)
{
Debug.LogWarning("[SettingsUIGenerator] SettingsManager.Instance not found in scene.");
return;
}
if (clearBeforeBuild) Clear();
spawnedItems.Clear();
List<SettingEntry> entries = database.entries
.Where(e => e != null && !string.IsNullOrWhiteSpace(e.key) && e.shownInSettings)
.ToList();
if (!groupByGroup)
{
// Без групп — просто одним списком
if (sortByNameInsideGroup)
entries = entries.OrderBy(e => e.displayName ?? e.key).ToList();
foreach (SettingEntry entry in entries)
SpawnSettingRow(entry, contentRoot);
return;
}
// Группировка по e.group
Dictionary<string, List<SettingEntry>> groups = entries
.GroupBy(e => string.IsNullOrWhiteSpace(e.group) ? "General" : e.group)
.ToDictionary(g => g.Key, g => g.ToList());
// Порядок групп: сначала заданные, потом остальные по алфавиту
List<string> orderedGroupNames = new();
if (groupOrder != null && groupOrder.Count > 0)
{
orderedGroupNames.AddRange(
groupOrder.Where(g => !string.IsNullOrWhiteSpace(g) && groups.ContainsKey(g)));
}
List<string> rest = groups.Keys
.Except(orderedGroupNames)
.OrderBy(x => x)
.ToList();
orderedGroupNames.AddRange(rest);
foreach (string groupName in orderedGroupNames)
{
List<SettingEntry> list = groups[groupName];
if (sortByNameInsideGroup)
list = list.OrderBy(e => e.displayName ?? e.key).ToList();
Transform parentForRows = contentRoot;
// Создаём UI группы (foldout) если включено
if (collapsibleGroups && groupPanelPrefab)
{
SettingsGroupPanel panel = Instantiate(groupPanelPrefab, contentRoot);
panel.name = $"Group_{groupName}";
string prefsKey = $"settings.ui.group.{groupName}";
panel.Init(groupName, prefsKey, defaultGroupsExpanded, rememberGroupFoldout);
// Подписываемся на изменение состояния группы
panel.OnExpandedChanged += OnGroupPanelExpandedChanged;
spawnedGroups.Add(panel);
parentForRows = panel.ContentRoot;
}
else
{
// Если не используем foldout — можно хотя бы заголовок простым текстом
// (не обязательно; если не надо — удали блок)
GameObject headerGO = new GameObject($"Header_{groupName}", typeof(RectTransform),
typeof(TextMeshProUGUI));
headerGO.transform.SetParent(contentRoot, false);
TextMeshProUGUI text = headerGO.GetComponent<TextMeshProUGUI>();
text.text = groupName;
text.fontSize = 28;
text.fontStyle = FontStyles.Bold;
}
foreach (var entry in list)
SpawnSettingRow(entry, parentForRows);
}
}
private void SpawnSettingRow(SettingEntry entry, Transform parent)
{
if (entry == null || parent == null)
{
Debug.LogWarning("[SettingsUIGenerator] Cannot spawn setting row: entry or parent is null");
return;
}
SettingUIItem prefab = PickPrefab(entry);
if (!prefab)
{
Debug.LogWarning(
$"[SettingsUIGenerator] No prefab for '{entry.key}' ({entry.valueType}/{entry.applyTarget})");
return;
}
SettingUIItem item = Instantiate(prefab, parent);
item.name = $"Setting_{entry.key}";
item.Bind(entry, showDescription);
// Если это Int ValueSelector, настраиваем компонент ValueSelectorRow
if (entry.valueType == SettingValueType.Int &&
entry.intDisplayFormat == IntDisplayFormat.ValueSelector)
{
ValueSelectorRow valueSelector = item.GetComponent<ValueSelectorRow>();
if (valueSelector != null)
{
int currentValue = SettingsManager.Instance.GetInt(entry.key);
valueSelector.Setup(entry, currentValue);
}
else
{
Debug.LogWarning($"[SettingsUIGenerator] ValueSelectorRow component not found on prefab for '{entry.key}'");
}
}
// Если это Float ValueSelector, настраиваем компонент ValueSelectorRow
if (entry.valueType == SettingValueType.Float &&
entry.floatDisplayFormat == FloatDisplayFormat.ValueSelector)
{
ValueSelectorRow valueSelector = item.GetComponent<ValueSelectorRow>();
if (valueSelector != null)
{
float currentValue = SettingsManager.Instance.GetFloat(entry.key);
valueSelector.Setup(entry, currentValue);
}
else
{
Debug.LogWarning($"[SettingsUIGenerator] ValueSelectorRow component not found on prefab for '{entry.key}'");
}
}
spawnedItems.Add(item);
}
private SettingUIItem PickPrefab(SettingEntry e)
{
return e.valueType switch
{
SettingValueType.Bool => toggleRowPrefab,
SettingValueType.Float => e.floatDisplayFormat == FloatDisplayFormat.ValueSelector
? valueSelectorRowPrefab
: sliderRowPrefab,
SettingValueType.Int => e.intDisplayFormat == IntDisplayFormat.ValueSelector
? valueSelectorRowPrefab
: sliderRowPrefab ? sliderRowPrefab : dropdownRowPrefab,
SettingValueType.String => inputRowPrefab,
SettingValueType.Button => buttonRowPrefab,
SettingValueType.OptionPicker => optionPickerRowPrefab ? optionPickerRowPrefab : dropdownRowPrefab,
_ => null
};
}
public void RefreshValues()
{
for (int i = 0; i < spawnedItems.Count; i++)
{
if (spawnedItems[i])
{
spawnedItems[i].RefreshFromSettings();
// Если есть компонент ValueSelectorRow, обновляем и его
ValueSelectorRow valueSelector = spawnedItems[i].GetComponent<ValueSelectorRow>();
if (valueSelector != null && !string.IsNullOrEmpty(valueSelector.Key))
{
if (valueSelector.IsFloat)
{
float fValue = SettingsManager.Instance.GetFloat(valueSelector.Key);
valueSelector.SetValue(fValue);
}
else
{
int iValue = SettingsManager.Instance.GetInt(valueSelector.Key);
valueSelector.SetValue(iValue);
}
}
}
}
}
private void OnGroupPanelExpandedChanged(SettingsGroupPanel panel, bool expanded)
{
// Уведомляем подписчиков (например SettingsUI) об изменении состояния группы
OnGroupExpandedChanged?.Invoke();
}
public void Clear()
{
// Отписываемся от событий групп
foreach (var group in spawnedGroups)
{
if (group != null)
group.OnExpandedChanged -= OnGroupPanelExpandedChanged;
}
spawnedGroups.Clear();
if (contentRoot != null)
{
for (int i = contentRoot.childCount - 1; i >= 0; i--)
{
Transform child = contentRoot.GetChild(i);
if (child != null)
Destroy(child.gameObject);
}
}
spawnedItems.Clear();
}
}
}