Danro/Assets/Scripts/Settings/UI/SettingUIItem.cs
Danil Iarosh cb2e83a596
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat(ui, prizes, spawners, editor): expand prize flow and add spawning/editor tooling
- доработан 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'ы и добавлены сопутствующие проектные артефакты,
  которые сейчас используются обновлённой конфигурацией проекта
2026-04-28 14:05:05 +03:00

379 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 RR.Common.Settings;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
namespace RR.Common.UI
{
public class SettingUIItem : MonoBehaviour
{
[Header("Common")] public TMP_Text titleText;
public TMP_Text descriptionText;
[Header("Controls")] public Toggle toggle;
public Slider slider;
public TMP_Dropdown dropdown;
public TMP_InputField input;
public Button button;
[Header("Option Picker")]
public Button prevButton;
public Button nextButton;
public TMP_Text optionValueText;
[Header("Optional")] public TMP_Text sliderValueText; // например справа от слайдера
private SettingEntry entry;
private bool showDescription;
/// <summary>
/// Безопасно устанавливает значение настройки с проверкой на уничтоженный SettingsManager
/// </summary>
private void SetSafe(string key, SettingValue value)
{
var manager = SettingsManager.Instance;
if (!manager)
{
Debug.LogWarning($"[SettingUIItem] SettingsManager.Instance is null, cannot set {key}");
return;
}
manager.Set(key, value);
}
public void Bind(SettingEntry entryToBind, bool showDescriptionOption)
{
this.entry = entryToBind;
this.showDescription = showDescriptionOption;
if (titleText)
titleText.text = string.IsNullOrWhiteSpace(entryToBind.displayName)
? entryToBind.key
: entryToBind.displayName;
if (descriptionText)
{
bool hasDesc = showDescriptionOption && !string.IsNullOrWhiteSpace(entryToBind.description);
descriptionText.gameObject.SetActive(hasDesc);
if (hasDesc) descriptionText.text = entryToBind.description;
}
// Отключаем все контролы, включим нужный
if (toggle) toggle.gameObject.SetActive(false);
if (slider) slider.gameObject.SetActive(false);
if (dropdown) dropdown.gameObject.SetActive(false);
if (input) input.gameObject.SetActive(false);
if (button) button.gameObject.SetActive(false);
if (prevButton) prevButton.gameObject.SetActive(false);
if (nextButton) nextButton.gameObject.SetActive(false);
if (optionValueText) optionValueText.gameObject.SetActive(false);
// Биндим по applyTarget/valueType
SetupControl();
RefreshFromSettings();
}
public void BindExternalDescriptionTarget(TMP_Text targetText)
{
if (entry == null) return;
var selectables = new List<Selectable>();
GetComponentsInChildren(true, selectables);
string description = entry.description ?? string.Empty;
foreach (Selectable selectable in selectables)
{
if (selectable == null) continue;
SelectableDescriptionTarget descriptionTarget =
selectable.GetComponent<SelectableDescriptionTarget>();
if (descriptionTarget == null)
descriptionTarget = selectable.gameObject.AddComponent<SelectableDescriptionTarget>();
descriptionTarget.Configure(targetText, description);
}
}
public void RefreshFromSettings()
{
if (entry == null) return;
SettingsManager manager = SettingsManager.Instance;
if (!manager) return;
var v = manager.Get(entry.key);
if (toggle && toggle.gameObject.activeSelf)
toggle.SetIsOnWithoutNotify(v.b);
if (slider && slider.gameObject.activeSelf)
slider.SetValueWithoutNotify(slider.wholeNumbers ? v.i : v.f);
if (dropdown && dropdown.gameObject.activeSelf)
dropdown.SetValueWithoutNotify(Mathf.Clamp(v.i, 0, dropdown.options.Count - 1));
if (input && input.gameObject.activeSelf)
input.SetTextWithoutNotify(v.s ?? "");
if (optionValueText && optionValueText.gameObject.activeSelf && entry.valueType == SettingValueType.OptionPicker)
UpdateOptionPickerDisplay(entry.options);
UpdateSliderValueText();
}
private void SetupControl()
{
if (entry == null) return;
// Настройки по valueType
switch (entry.valueType)
{
case SettingValueType.Bool:
UseToggle(v => SetSafe(entry.key, SettingValue.Bool(v)));
break;
case SettingValueType.Float:
// Для ValueSelector не создаём слайдер - его обрабатывает ValueSelectorRow
if (entry.floatDisplayFormat == FloatDisplayFormat.ValueSelector)
{
// ValueSelectorRow сам управляет значением
}
else
{
UseSliderFloat(entry.minFloat, entry.maxFloat, v =>
SetSafe(entry.key, SettingValue.Float(v)));
}
break;
case SettingValueType.Int:
// Для ValueSelector не создаём слайдер - его обрабатывает ValueSelectorRow
if (entry.intDisplayFormat == IntDisplayFormat.ValueSelector)
{
// ValueSelectorRow сам управляет значением
// SettingUIItem здесь только отображает label и description
}
else
{
// Int — делаем слайдер целочисленным (лучше чем инпут)
UseSliderInt(entry.minInt, entry.maxInt, v =>
SetSafe(entry.key, SettingValue.Int(v)));
}
break;
case SettingValueType.String:
UseInput(s => SetSafe(entry.key, SettingValue.String(s)));
break;
case SettingValueType.Button:
UseButton(() =>
{
// Вызываем биндеры на сцене (SettingsButtonBinder)
SettingsButtonDispatcher.Dispatch(entry.key);
});
break;
case SettingValueType.OptionPicker:
if (entry.optionPickerDisplayFormat == OptionPickerDisplayFormat.ValueSelector)
{
// ValueSelectorRow сам управляет значением и отображением.
}
else
{
UseOptionPicker(entry.options, idx =>
SetSafe(entry.key, SettingValue.Int(idx)));
}
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private void UseToggle(UnityEngine.Events.UnityAction<bool> onChanged)
{
if (!toggle) return;
toggle.gameObject.SetActive(true);
toggle.onValueChanged.RemoveAllListeners();
toggle.onValueChanged.AddListener(onChanged);
}
private void UseSliderFloat(float min, float max, UnityEngine.Events.UnityAction<float> onChanged)
{
if (!slider) return;
slider.gameObject.SetActive(true);
slider.wholeNumbers = false;
slider.minValue = Mathf.Min(min, max);
slider.maxValue = Mathf.Max(min, max);
slider.onValueChanged.RemoveAllListeners();
slider.onValueChanged.AddListener(v =>
{
onChanged(v);
UpdateSliderValueText();
});
}
private void UseSliderInt(int min, int max, System.Action<int> onChanged)
{
if (!slider) return;
slider.gameObject.SetActive(true);
slider.wholeNumbers = true;
slider.minValue = Mathf.Min(min, max);
slider.maxValue = Mathf.Max(min, max);
slider.onValueChanged.RemoveAllListeners();
slider.onValueChanged.AddListener(v =>
{
onChanged(Mathf.RoundToInt(v));
UpdateSliderValueText();
});
}
private void UseDropdown(List<string> options, System.Action<int> onChanged)
{
if (!dropdown) return;
dropdown.gameObject.SetActive(true);
dropdown.ClearOptions();
dropdown.AddOptions(options ?? new List<string>());
dropdown.onValueChanged.RemoveAllListeners();
dropdown.onValueChanged.AddListener(idx => onChanged(idx));
}
private void UseInput(System.Action<string> onEndEdit)
{
if (!input) return;
input.gameObject.SetActive(true);
input.onEndEdit.RemoveAllListeners();
input.onEndEdit.AddListener(text => onEndEdit(text));
}
private void UseButton(System.Action onClick)
{
if (!button) return;
button.gameObject.SetActive(true);
// Убеждаемся что кнопка интерактивна
button.interactable = true;
// Навигация настраивается централизованно в SettingsUI.SetupExplicitNavigation()
// Устанавливаем текст кнопки
var buttonText = button.GetComponentInChildren<TMP_Text>();
if (buttonText && entry != null)
{
string text = !string.IsNullOrEmpty(entry.buttonText) ? entry.buttonText : entry.displayName;
if (string.IsNullOrEmpty(text)) text = entry.key;
buttonText.text = text;
}
// Устанавливаем цвет и спрайт кнопки
if (entry != null)
{
var image = button.GetComponent<UnityEngine.UI.Image>();
if (image)
{
// Устанавливаем цвет напрямую на Image
image.color = entry.buttonColor;
// Устанавливаем спрайт
if (entry.buttonSprite != null)
{
image.sprite = entry.buttonSprite;
}
}
}
button.onClick.RemoveAllListeners();
button.onClick.AddListener(() => onClick());
}
private void UseOptionPicker(List<string> options, System.Action<int> onChanged)
{
if (options == null || options.Count == 0) return;
// Если есть dropdown - используем его
if (dropdown)
{
UseDropdown(options, onChanged);
return;
}
// Иначе используем кнопки prev/next
if (!prevButton || !nextButton || !optionValueText) return;
prevButton.gameObject.SetActive(true);
nextButton.gameObject.SetActive(true);
optionValueText.gameObject.SetActive(true);
prevButton.onClick.RemoveAllListeners();
nextButton.onClick.RemoveAllListeners();
prevButton.onClick.AddListener(() =>
{
if (entry == null) return;
int current = SettingsManager.Instance.Get(entry.key).i;
int newIndex = current - 1;
if (newIndex < 0) newIndex = options.Count - 1; // Wrap around
onChanged(newIndex);
UpdateOptionPickerDisplay(options);
});
nextButton.onClick.AddListener(() =>
{
if (entry == null) return;
int current = SettingsManager.Instance.Get(entry.key).i;
int newIndex = (current + 1) % options.Count; // Wrap around
onChanged(newIndex);
UpdateOptionPickerDisplay(options);
});
UpdateOptionPickerDisplay(options);
}
private void UpdateOptionPickerDisplay(List<string> options)
{
if (!optionValueText || entry == null) return;
int currentIndex = SettingsManager.Instance.Get(entry.key).i;
if (currentIndex >= 0 && currentIndex < options.Count)
optionValueText.text = options[currentIndex];
}
private void UpdateSliderValueText()
{
if (!sliderValueText || !slider || !slider.gameObject.activeSelf) return;
if (slider.wholeNumbers)
{
sliderValueText.text = Mathf.RoundToInt(slider.value).ToString();
}
else
{
// Проверяем флаг displayAsPercentage
if (entry != null && entry.displayAsPercentage)
{
// Отображаем как проценты (0-100%)
float percentage = slider.value * 100f;
sliderValueText.text = percentage.ToString("0") + "%";
}
else
{
sliderValueText.text = slider.value.ToString("0.##");
}
}
}
}
}