sea-battle/Assets/Scripts/Settings/SettingsScreen.cs
2026-01-09 08:45:22 +03:00

595 lines
24 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;
using System.Collections.Generic;
using System.Reflection;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class SettingsScreen : MonoBehaviour
{
[Header("Панель с опциями")]
[SerializeField] RectTransform OptionsRect;
[Header("Панель с вариантами опций")]
[SerializeField] RectTransform ValuesRect;
[Header("Панель с статистикой")]
[SerializeField] CountersView StatisticRect;
[Header("Указатель опций")]
[SerializeField] RectTransform PointerRect;
[Header("Префаб текста настройки")]
[SerializeField] GameObject SettingPrefab;
[Header("Префаб текста опции")]
[SerializeField] GameObject OptionPrefab;
[Header("Префаб прогресс-бара (для Panel/Action)")]
[SerializeField] GameObject ProgressBarPrefab;
[Header("Префаб слайдера (для Svitcher)")]
[SerializeField] GameObject SwitcherSliderPrefab;
[Header("Настройки прокрутки")]
[SerializeField] int maxVisibleSettings = 5; // максимально видимых элементов
[SerializeField] float scrollSpeed = 10f; // скорость сглаживания прокрутки
// Скроллбар UI
[Header("UI Scrollbar")]
[SerializeField] Scrollbar Scrollbar;
List<GameObject> settingsLabels;
List<GameObject> optionsLabels;
Dictionary<int, Image> progressBars; // индекс настройки -> прогресс-бар
Dictionary<int, TextMeshProUGUI> progressTexts; // индекс настройки -> текст процентов
Dictionary<int, Slider> switcherSliders; // индекс настройки -> слайдер для Svitcher
substate currentState;
List<SettingOption> gameSettings => SettingsManager.instance.gameSettings.settings;
int settingsAmount => SettingsManager.instance.gameSettings.settings.Count;
List<string> chosenSettingOptions => SettingsManager.instance.gameSettings.settings[currentSettingIndex].options;
int chosenSettingOptionsAmount => chosenSettingOptions.Count;
public int currentSettingIndex = 0;
int currentOptionIndex = 0;
float startx = 50;
float starty = -50;
//float offsetx = 100;
float offsetx = 0;
float offsety = 70;
Coroutine chooseOption;
[SerializeField] float optionsPadding = 20;
// Переменные для прокрутки
int scrollOffset = 0; // текущее смещение (индекс верхнего видимого элемента)
float targetScrollY = 0f; // целевая позиция прокрутки
float currentScrollY = 0f; // текущая позиция прокрутки (интерполируемая)
// Вспомогательные переменные для скроллбара
int maxScrollSteps = 0; // settingsAmount - maxVisibleSettings
public void Open()
{
optionsLabels = new List<GameObject>();
settingsLabels = new List<GameObject>();
progressBars = new Dictionary<int, Image>();
progressTexts = new Dictionary<int, TextMeshProUGUI>();
switcherSliders = new Dictionary<int, Slider>();
PointerRect.gameObject.SetActive(true);
currentState = substate.SettingChoosing;
currentSettingIndex = 0;
// обнуляем прокрутку
scrollOffset = 0;
targetScrollY = 0f;
currentScrollY = 0f;
CreateSettingsLabels();
StatisticRect.Initialize();
// Подписка на скроллбар
if (Scrollbar != null)
{
// вычислим параметры
maxScrollSteps = Mathf.Max(0, settingsAmount - maxVisibleSettings);
Scrollbar.gameObject.SetActive(maxScrollSteps > 0);
if (maxScrollSteps > 0)
{
// размер «видимого окна» на скроллбаре
Scrollbar.size = (float)maxVisibleSettings / (float)settingsAmount;
float value = (maxScrollSteps > 0) ? 1f - (float)scrollOffset / (float)maxScrollSteps : 1f;
Scrollbar.SetValueWithoutNotify(value);
Scrollbar.onValueChanged.AddListener(OnScrollbarChanged);
}
else
{
Scrollbar.onValueChanged.RemoveListener(OnScrollbarChanged);
}
}
}
public void Close()
{
// отписываемся от событий скроллбара
if (Scrollbar != null)
{
Scrollbar.onValueChanged.RemoveListener(OnScrollbarChanged);
}
ClearOptionsPanel();
ClearSettingsPanel();
if (chooseOption != null) StopCoroutine(chooseOption);
}
public void Switch(int direction)
{
switch (currentState)
{
case substate.SettingChoosing:
currentSettingIndex = (currentSettingIndex + settingsAmount + direction) % settingsAmount;
UpdateScrollPosition();
// не устанавливаем pointer напрямую на целевой, пусть он позиционируется относительно currentScrollY
UpdatePointerPosition();
break;
case substate.OptionChoosing:
currentOptionIndex = (currentOptionIndex + chosenSettingOptionsAmount + direction) % chosenSettingOptionsAmount;//ТУТ НАДО КОЛИЧЕСТВО ОПЦИЙ НАСТРОЙКИ ПОЛУЧИТЬ
break;
}
//SoundManager.instance.playSwitch();
}
//менять состояние, сохранять данные
public void Accept()
{
if (currentState == substate.SettingChoosing)
{
createOptionsLabels(gameSettings[currentSettingIndex]);
currentState = substate.OptionChoosing;
PointerRect.gameObject.SetActive(false);
if (chooseOption != null) StopCoroutine(chooseOption);
chooseOption = StartCoroutine(DisplayChosenOption());
}
else if (currentState == substate.OptionChoosing)
{
SettingsManager.instance.ChangeSetting(settingName: gameSettings[currentSettingIndex].settingName,
newIndex: currentOptionIndex);
PointerRect.gameObject.SetActive(true);
ClearOptionsPanel();
//Обновляем для учета изменений
ClearSettingsPanel();
CreateSettingsLabels();
if (chooseOption != null) StopCoroutine(chooseOption);
currentState = substate.SettingChoosing;
}
//SoundManager.instance.playChoose();
}
public void AcceptView()
{
StatisticRect.View();
}
/// <summary>
/// Можно ли открыть панель или выполнить экшн для текущей настройки
/// </summary>
public bool CanOpenPanel()
{
if (currentState != substate.SettingChoosing) return false;
if (currentSettingIndex < 0 || currentSettingIndex >= gameSettings.Count) return false;
SettingType type = gameSettings[currentSettingIndex].type;
return type == SettingType.Panel || type == SettingType.Action;
}
/// <summary>
/// Возвращает ID панели для текущей выбранной настройки (-1 если не панель)
/// </summary>
public int GetCurrentPanelId()
{
if (currentState != substate.SettingChoosing) return -1;
if (currentSettingIndex < 0 || currentSettingIndex >= gameSettings.Count) return -1;
return gameSettings[currentSettingIndex].panelId;
}
/// <summary>
/// Возвращает тип текущей выбранной настройки
/// </summary>
public SettingType GetCurrentSettingType()
{
if (currentState != substate.SettingChoosing) return SettingType.Normal;
if (currentSettingIndex < 0 || currentSettingIndex >= gameSettings.Count) return SettingType.Normal;
return gameSettings[currentSettingIndex].type;
}
/// <summary>
/// Обновляет заполнение прогресс-бара для текущей настройки (если она типа Panel/Action)
/// </summary>
public void UpdateProgressBar(float fillAmount)
{
fillAmount = Mathf.Clamp01(fillAmount);
if (progressBars.TryGetValue(currentSettingIndex, out Image progressBar))
{
progressBar.fillAmount = fillAmount;
}
if (progressTexts.TryGetValue(currentSettingIndex, out TextMeshProUGUI progressText))
{
// Показываем текст только если прогресс-бар заполняется
if (fillAmount > 0f)
{
progressText.gameObject.SetActive(true);
int percentage = Mathf.RoundToInt(fillAmount * 100f);
progressText.text = $"{percentage}%";
}
else
{
progressText.gameObject.SetActive(false);
}
}
}
//Шерстим все существующие настройки
//Создаем для каждой настройки текстовое поле
private void CreateSettingsLabels()
{
// ...existing code...
settingsLabels = new List<GameObject>();
for (int i = 0; i < SettingsManager.instance.gameSettings.settings.Count; i++)
{
createLabelFromSettingOption(SettingsManager.instance.gameSettings.settings[i], i);
}
// Обновляем прокрутку и позиции после создания
UpdateScrollPosition();
currentScrollY = targetScrollY;
UpdateSettingsPositions();
//Процедура которая собственно создает лейблы
void createLabelFromSettingOption(SettingOption option, int index)
{
GameObject label = Instantiate(SettingPrefab, OptionsRect);
label.name = option.settingName + " Setting";
settingsLabels.Add(label);
RectTransform rt = label.GetComponent<RectTransform>();
// Добавляем CanvasGroup для управления видимостью
CanvasGroup canvasGroup = rt.GetComponent<CanvasGroup>();
if (canvasGroup == null) canvasGroup = rt.gameObject.AddComponent<CanvasGroup>();
// Изначальная позиция будет обновлена в UpdateSettingsPositions()
rt.anchoredPosition = new Vector2(startx + offsetx * index, starty - offsety * index);
//Загоняем название настройки
TextMeshProUGUI text = rt.gameObject.GetComponent<TextMeshProUGUI>();
text.text = option.settingName;
// Для Panel/Action создаём прогресс-бар, для Svitcher — слайдер, для Normal — текст опц
if (option.type == SettingType.Panel || option.type == SettingType.Action)
{
if (ProgressBarPrefab != null)
{
GameObject progressBarObj = Instantiate(ProgressBarPrefab, rt);
progressBarObj.name = option.settingName + " ProgressBar";
RectTransform progressRT = progressBarObj.GetComponent<RectTransform>();
progressRT.anchoredPosition = new Vector2(rt.sizeDelta.x, 0);
Image progressImage = progressBarObj.GetComponentInChildren<Image>();
if (progressImage != null)
{
progressImage.fillAmount = 0f;
progressBars[index] = progressImage;
}
// Находим TextMeshProUGUI для отображения процентов
TextMeshProUGUI progressText = progressBarObj.GetComponentInChildren<TextMeshProUGUI>();
if (progressText != null)
{
progressText.text = "0%";
progressText.gameObject.SetActive(false); // скрываем до начала заполнения
progressTexts[index] = progressText;
}
}
}
else if (option.type == SettingType.Svitcher)
{
// Создаём слайдер для переключателя
if (SwitcherSliderPrefab != null)
{
GameObject sliderObj = Instantiate(SwitcherSliderPrefab, rt);
sliderObj.name = option.settingName + " Slider";
RectTransform sliderRT = sliderObj.GetComponent<RectTransform>();
sliderRT.anchoredPosition = new Vector2(rt.sizeDelta.x, 0);
Slider slider = sliderObj.GetComponent<Slider>();
if (slider != null)
{
// Устанавливаем начальное значение: инвертируем для корректного отображения
// 0 (выкл визуально) = индекс 1 ("Выкл"), 1 (вкл визуально) = индекс 0 ("Вкл")
slider.minValue = 0;
slider.maxValue = 1;
// ВАЖНО: используем SetValueWithoutNotify, чтобы не вызывать событие при инициализации
int initialValue = (option.chosenIndex == 0) ? 1 : 0;
slider.SetValueWithoutNotify(initialValue);
// Сохраняем ссылку на слайдер
switcherSliders[index] = slider;
// НЕ подписываемся на onValueChanged - сохранение происходит через callback в ToggleSwitcher
}
}
}
else
{
// Обычная настройка — показываем текущую опцию
GameObject label2 = Instantiate(OptionPrefab, rt);
label2.name = option.settingName + " Value";
RectTransform option_rt = label2.GetComponent<RectTransform>();
option_rt.anchoredPosition = new Vector2(rt.sizeDelta.x, 0);
text = option_rt.gameObject.GetComponent<TextMeshProUGUI>();
text.text = option.ChosenOption;
}
}
}
// Вычисляем и задаём смещение прокрутки и целевую позицию
private void UpdateScrollPosition()
{
if (settingsAmount <= maxVisibleSettings)
{
scrollOffset = 0;
targetScrollY = 0f;
if (Scrollbar != null) Scrollbar.gameObject.SetActive(false);
return;
}
if (currentSettingIndex >= scrollOffset + maxVisibleSettings)
{
scrollOffset = currentSettingIndex - maxVisibleSettings + 1;
}
else if (currentSettingIndex < scrollOffset)
{
scrollOffset = currentSettingIndex;
}
targetScrollY = scrollOffset * offsety;
// обновим скроллбар без вызова слушателя
if (Scrollbar != null && maxScrollSteps > 0)
{
float value = 1f - (float)scrollOffset / (float)maxScrollSteps;
Scrollbar.SetValueWithoutNotify(value);
}
}
// Обновляем позицию указателя относительно видимой области
private void UpdatePointerPosition()
{
// Используем плавающее смещение, чтобы указатель плавно следовал за прокруткой
float scrollFloat = (offsety != 0) ? currentScrollY / offsety : 0f;
float visibleIndex = currentSettingIndex - scrollFloat;
PointerRect.anchoredPosition = new Vector2(startx + 35 + currentSettingIndex * offsetx, starty - visibleIndex * offsety);
}
private void UpdateSettingsPositions()
{
if (settingsLabels == null) return;
// Плавающее смещение в строках
float scrollFloat = (offsety != 0) ? currentScrollY / offsety : 0f;
for (int i = 0; i < settingsLabels.Count; i++)
{
RectTransform rt = settingsLabels[i].GetComponent<RectTransform>();
// Вычисляем видимую позицию с учётом плавающего смещения
float visiblePosition = i - scrollFloat;
float targetY = starty - visiblePosition * offsety;
rt.anchoredPosition = new Vector2(startx + offsetx * i, targetY);
// Можно также добавить прозрачность для невидимых элементов
CanvasGroup canvasGroup = rt.GetComponent<CanvasGroup>();
if (canvasGroup == null) canvasGroup = rt.gameObject.AddComponent<CanvasGroup>();
// Элементы вне видимой области делаем невидимыми
if (i < scrollOffset || i >= scrollOffset + maxVisibleSettings)
{
canvasGroup.alpha = 0f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
else
{
canvasGroup.alpha = 1f;
canvasGroup.interactable = true;
canvasGroup.blocksRaycasts = true;
}
}
// Обновляем позицию указателя
UpdatePointerPosition();
}
// обрабтчик изменения значения Scrollbar (0..1). Значение 1 = верх (scrollOffset=0), 0 = низ (максимальное смещение)
private void OnScrollbarChanged(float value)
{
if (settingsAmount <= maxVisibleSettings) return;
maxScrollSteps = Mathf.Max(0, settingsAmount - maxVisibleSettings);
if (maxScrollSteps == 0) return;
// инвертируем значение: 1 -> 0, 0 -> maxScrollSteps
float t = 1f - value;
float desiredOffsetF = t * maxScrollSteps;
// Применяем дробную цель к targetScrollY (плавнее)
targetScrollY = desiredOffsetF * offsety;
// и обновляем целевой scrollOffset целым числом для маски видимости
scrollOffset = Mathf.Clamp(Mathf.RoundToInt(desiredOffsetF), 0, maxScrollSteps);
}
void Update()
{
// Плавная прокрутка
if (currentState == substate.SettingChoosing && Mathf.Abs(currentScrollY - targetScrollY) > 0.1f)
{
currentScrollY = Mathf.Lerp(currentScrollY, targetScrollY, Time.deltaTime * scrollSpeed);
UpdateSettingsPositions();
// при программной прокрутке обновляем скроллбар, чтобы отражать текущее положение
if (Scrollbar != null && maxScrollSteps > 0)
{
float scrollFloat = currentScrollY / offsety; // может быть дробным
float value = 1f - (scrollFloat / (float)maxScrollSteps);
// защита от NaN/Inf
if (float.IsNaN(value) || float.IsInfinity(value)) value = 1f;
Scrollbar.SetValueWithoutNotify(Mathf.Clamp01(value));
}
}
else if (currentScrollY != targetScrollY)
{
currentScrollY = targetScrollY;
UpdateSettingsPositions();
}
}
// Опции настроек
void createOptionsLabels(SettingOption option)
{
if (optionsLabels == null) optionsLabels = new List<GameObject>();
currentOptionIndex = 0;
float labelWidth = (ValuesRect.sizeDelta.x - optionsPadding * 2) / option.options.Count;
float labelHeight = ValuesRect.sizeDelta.y - optionsPadding * 2;
print(ValuesRect.sizeDelta);
foreach (string optionName in option.options)
{
GameObject label = Instantiate(OptionPrefab, ValuesRect);
optionsLabels.Add(label);
RectTransform rt = label.GetComponent<RectTransform>();
rt.pivot = new Vector2(0, 1f);
rt.sizeDelta = new Vector2(labelWidth, labelHeight);
rt.anchoredPosition = new Vector2(optionsPadding + labelWidth * currentOptionIndex, optionsPadding);
TextMeshProUGUI text = label.GetComponent<TextMeshProUGUI>();
text.text = optionName;
text.alignment = TextAlignmentOptions.Center;
text.color = Color.gray;
currentOptionIndex++;
}
currentOptionIndex = option.chosenIndex;
}
void ClearSettingsPanel()
{
if (settingsLabels == null) return;
foreach (GameObject label in settingsLabels)
{
if (label != null) Object.Destroy(label);
}
settingsLabels.Clear();
}
void ClearOptionsPanel()
{
if (optionsLabels == null) return;
foreach (GameObject label in optionsLabels)
{
if (label != null) Object.Destroy(label);
}
optionsLabels.Clear();
}
IEnumerator DisplayChosenOption()
{
Color normalColor = Color.gray;
Color highlightedColor = Color.white;
int normalSize = 26;
int chosenSize = 40;
List<TextMeshProUGUI> options = new List<TextMeshProUGUI>();
if (optionsLabels != null)
{
foreach (GameObject label in optionsLabels)
{
options.Add(label.GetComponent<TextMeshProUGUI>());
}
}
while (true)
{
for (int i = 0; i < options.Count; i++)
{
if (i == currentOptionIndex)
{
options[i].color = Vector4.MoveTowards(options[i].color, highlightedColor, Time.deltaTime * 5);
options[i].fontSize = Mathf.MoveTowards(options[i].fontSize, chosenSize, Time.deltaTime * 140);
}
else
{
options[i].color = Vector4.MoveTowards(options[i].color, normalColor, Time.deltaTime * 5);
options[i].fontSize = Mathf.MoveTowards(options[i].fontSize, normalSize, Time.deltaTime * 140);
}
}
yield return null;
}
}
/// <summary>
/// Переключает слайдер текущей выбранной настройки (для типа Svitcher)
/// Вызывается из SettingsInput при нажатии кнопки
/// </summary>
public void ToggleSwitcher()
{
if (currentState != substate.SettingChoosing || !switcherSliders.TryGetValue(currentSettingIndex, out Slider slider)) return;
// ВАЖНО: Округляем текущее значение до 0 или 1, чтобы избежать промежуточных значений при быстрых нажатиях
int currentValue = Mathf.RoundToInt(slider.value);
float newValue = currentValue == 0 ? 1f : 0f;
string settingName = gameSettings[currentSettingIndex].settingName;
// Анимируем с callback для сохранения
SmoothSlider smoothSlider = slider.GetComponent<SmoothSlider>();
if (smoothSlider != null)
{
smoothSlider.AnimateTo(newValue, (val) =>
SettingsManager.instance.ChangeSetting(settingName, val == 1 ? 0 : 1));
}
else
{
slider.value = newValue; // Fallback без анимации
}
}
enum substate { SettingChoosing, OptionChoosing }
}