Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- доработан flow призовой витрины и связанного UI: добавлены `PrizeStandButtonView` и `RulesPrizeListPage`, обновлены `PrizeStandController`, `PrizeStandPrize`, `PrizeStandModal`, `PrizeStandSettingsModal`, `LevelPrizeSettingsModal`, `QRCodeSettingsModal`, `BalanceDisplay`, `ButtonTextFocusColor` и связанные prefab'ы, чтобы улучшить настройку, навигацию, отображение кнопок и prize flow - расширена система настроек и сохранения значений: `OptionPicker` получил отдельный формат `ValueSelector`, обновлены `SettingsDatabase`, `SettingEntryDrawer`, `SettingUIItem`, `SettingsUIGenerator`, `ValueSelectorRow` и `MainSettingsDatabase.asset`, а сохранение option picker-значений, включая `gameplay.gameDifficulty`, исправлено и синхронизировано с остальными настройками - добавлены универсальные спавнеры сущностей: реализованы `EntitySpawner` и `EntitySpawnerManager` с поддержкой весов префабов, точек спавна, заполнения по проценту, ограничений на количество и автоспавна при инициализации - доработана логика игрового спавна и сложности: процент инициализационного спавна может зависеть от настройки `gameplay.gameDifficulty` с градациями 25/50/75/100, в одном цикле массового спавна повторно не используется та же точка, а gizmo спавнера теперь показывает только сами точки без линий - упрощена работа в редакторе Unity и добавлены editor-инструменты: в `GameManager` лицензия пропускается при запуске в `UNITY_EDITOR`, а `HierarchySelectionCounter`, `HierarchySelectionCountOverlay` и `HierarchyBulkRenamerWindow` добавляют подсчет выделения, realtime overlay в `SceneView` и массовое переименование объектов по правилам нумерации из Unity Project Settings - синхронизированы сцены, ресурсы и UI-ассеты под новый flow: обновлены `MainMenu`, `Level UI`, уровни `1-1` и `1-2`, строки настроек, кнопки, TMP/font assets, изображения UI, `prizes.json` и recovery-данные после правок интерфейса и логики - очищены и переразложены часть внешних и служебных ресурсов: удалён пакет `Lava_Flowing_Shader`, переименованы/заменены отдельные UI-prefab'ы и добавлены сопутствующие проектные артефакты, которые сейчас используются обновлённой конфигурацией проекта
379 lines
15 KiB
C#
379 lines
15 KiB
C#
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}'");
|
||
}
|
||
}
|
||
|
||
if (entry.valueType == SettingValueType.OptionPicker &&
|
||
entry.optionPickerDisplayFormat == OptionPickerDisplayFormat.ValueSelector)
|
||
{
|
||
ValueSelectorRow valueSelector = item.GetComponent<ValueSelectorRow>();
|
||
if (valueSelector != null)
|
||
{
|
||
int currentValue = SettingsManager.Instance.GetInt(entry.key);
|
||
valueSelector.SetupOptionPicker(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 => e.optionPickerDisplayFormat == OptionPickerDisplayFormat.ValueSelector
|
||
? valueSelectorRowPrefab
|
||
: 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();
|
||
}
|
||
}
|
||
}
|