Files
g-world/scripts/ui/settings_menu.gd
T
2026-08-10 20:39:06 +03:00

259 lines
8.7 KiB
GDScript
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.
extends Control
## Экран настроек: видео, звук, управление.
##
## Сам ничего не хранит и не применяет — только показывает состояние
## автозагрузки GameSettings и дёргает её сеттеры. Открывается через open(),
## закрывается кнопкой «Готово» или Esc и сообщает об этом сигналом closed.
signal closed
const ACCENT := Color("#2ee6c5")
@onready var _settings: GameSettingsService = get_node("/root/GameSettings")
@onready var _card: Control = $Center/Card
@onready var _tabs: TabContainer = $Center/Card/Body/Tabs
@onready var _window_mode_option: OptionButton = $Center/Card/Body/Tabs/VideoTab/Rows/WindowModeRow/WindowModeOption
@onready var _resolution_option: OptionButton = $Center/Card/Body/Tabs/VideoTab/Rows/ResolutionRow/ResolutionOption
@onready var _vsync_toggle: Button = $Center/Card/Body/Tabs/VideoTab/Rows/VsyncRow/VsyncToggle
@onready var _fps_option: OptionButton = $Center/Card/Body/Tabs/VideoTab/Rows/FpsRow/FpsOption
@onready var _audio_rows: VBoxContainer = $Center/Card/Body/Tabs/AudioTab/Rows
@onready var _bindings_box: VBoxContainer = $Center/Card/Body/Tabs/ControlsTab/Wrap/Scroll/Bindings
@onready var _bindings_hint: Label = $Center/Card/Body/Tabs/ControlsTab/Wrap/Hint
@onready var _reset_button: Button = $Center/Card/Body/Actions/ResetButton
@onready var _done_button: Button = $Center/Card/Body/Actions/DoneButton
## Пока не пусто — ждём клавишу для этого действия.
var _capturing_action := ""
## Защита от реакции на сигналы во время программного обновления контролов.
var _syncing := false
var _hint_default := ""
var _volume_rows := {}
var _binding_buttons := {}
func _ready() -> void:
_hint_default = _bindings_hint.text
_name_tabs()
_fill_options()
_build_audio_rows()
_build_binding_rows()
_window_mode_option.item_selected.connect(_on_window_mode_selected)
_resolution_option.item_selected.connect(_on_resolution_selected)
_fps_option.item_selected.connect(_on_fps_selected)
_vsync_toggle.toggled.connect(_on_vsync_toggled)
_reset_button.pressed.connect(_on_reset_pressed)
_done_button.pressed.connect(close)
_sync_from_settings()
func _input(event: InputEvent) -> void:
if _capturing_action == "":
return
if not (event is InputEventKey and event.is_pressed() and not event.is_echo()):
return
var key_event := event as InputEventKey
var action := _capturing_action
_capturing_action = ""
if key_event.keycode != KEY_ESCAPE:
var conflict := _settings.set_binding(action, key_event.keycode)
if conflict != "":
var label: String = GameSettingsService.ACTION_LABELS[conflict]
_bindings_hint.text = "%s: клавиша освобождена, назначьте новую." % label
else:
_bindings_hint.text = _hint_default
_refresh_bindings()
get_viewport().set_input_as_handled()
func _unhandled_input(event: InputEvent) -> void:
if not visible or not event.is_action_pressed("ui_cancel"):
return
close()
get_viewport().set_input_as_handled()
# --- Открытие / закрытие ----------------------------------------------------
func open() -> void:
visible = true
_sync_from_settings()
modulate.a = 0.0
# Контейнеры раскладываются только после того, как узел стал видимым.
await get_tree().process_frame
_card.pivot_offset = _card.size * 0.5
_card.scale = Vector2(0.97, 0.97)
var tween := create_tween().set_parallel(true).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
tween.tween_property(self, "modulate:a", 1.0, 0.18)
tween.tween_property(_card, "scale", Vector2.ONE, 0.22)
_tabs.get_tab_bar().grab_focus()
func close() -> void:
# Esc во время ожидания клавиши сначала отменяет назначение.
if _capturing_action != "":
_capturing_action = ""
_bindings_hint.text = _hint_default
_refresh_bindings()
return
var tween := create_tween().set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_CUBIC)
tween.tween_property(self, "modulate:a", 0.0, 0.15)
await tween.finished
visible = false
closed.emit()
# --- Построение вкладок -----------------------------------------------------
func _name_tabs() -> void:
_tabs.set_tab_title(0, "Видео")
_tabs.set_tab_title(1, "Звук")
_tabs.set_tab_title(2, "Управление")
func _fill_options() -> void:
for label: String in GameSettingsService.WINDOW_MODE_LABELS:
_window_mode_option.add_item(label)
for resolution: Vector2i in GameSettingsService.RESOLUTIONS:
_resolution_option.add_item("%d × %d" % [resolution.x, resolution.y])
for limit: int in GameSettingsService.FPS_LIMITS:
_fps_option.add_item("Без ограничения" if limit == 0 else str(limit))
func _build_audio_rows() -> void:
for bus_name: String in GameSettingsService.AUDIO_BUSES:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 20)
var caption := Label.new()
caption.text = GameSettingsService.AUDIO_BUSES[bus_name]
caption.custom_minimum_size.x = 300
row.add_child(caption)
var slider := HSlider.new()
slider.min_value = 0.0
slider.max_value = 1.0
slider.step = 0.01
slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
slider.size_flags_vertical = Control.SIZE_SHRINK_CENTER
row.add_child(slider)
var value_label := Label.new()
value_label.custom_minimum_size.x = 90
value_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
value_label.add_theme_color_override("font_color", ACCENT)
row.add_child(value_label)
slider.value_changed.connect(_on_volume_changed.bind(bus_name))
_volume_rows[bus_name] = {"slider": slider, "label": value_label}
_audio_rows.add_child(row)
func _build_binding_rows() -> void:
for action: String in GameSettingsService.ACTION_LABELS:
var row := HBoxContainer.new()
var caption := Label.new()
caption.text = GameSettingsService.ACTION_LABELS[action]
caption.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(caption)
var button := Button.new()
button.custom_minimum_size.x = 260
button.add_theme_font_size_override("font_size", 20)
button.pressed.connect(_start_capture.bind(action))
row.add_child(button)
_binding_buttons[action] = button
_bindings_box.add_child(row)
# --- Синхронизация UI -------------------------------------------------------
func _sync_from_settings() -> void:
_syncing = true
_window_mode_option.selected = _settings.window_mode
_resolution_option.selected = GameSettingsService.RESOLUTIONS.find(_settings.resolution)
_resolution_option.disabled = _settings.window_mode != GameSettingsService.WindowMode.WINDOWED
_fps_option.selected = GameSettingsService.FPS_LIMITS.find(_settings.fps_limit)
_vsync_toggle.button_pressed = _settings.vsync
_vsync_toggle.text = "Вкл" if _settings.vsync else "Выкл"
for bus_name: String in _volume_rows:
var value: float = _settings.volumes[bus_name]
_volume_rows[bus_name]["slider"].value = value
_volume_rows[bus_name]["label"].text = "%d%%" % roundi(value * 100.0)
_refresh_bindings()
_syncing = false
func _refresh_bindings() -> void:
for action: String in _binding_buttons:
var button: Button = _binding_buttons[action]
if action == _capturing_action:
button.text = "Нажмите клавишу…"
else:
button.text = GameSettingsService.key_name(int(_settings.bindings.get(action, KEY_NONE)))
# --- Обработчики ------------------------------------------------------------
func _on_window_mode_selected(index: int) -> void:
if _syncing:
return
_settings.set_window_mode(index)
_resolution_option.disabled = index != GameSettingsService.WindowMode.WINDOWED
func _on_resolution_selected(index: int) -> void:
if _syncing:
return
_settings.set_resolution(GameSettingsService.RESOLUTIONS[index])
func _on_fps_selected(index: int) -> void:
if _syncing:
return
_settings.set_fps_limit(GameSettingsService.FPS_LIMITS[index])
func _on_vsync_toggled(enabled: bool) -> void:
_vsync_toggle.text = "Вкл" if enabled else "Выкл"
if _syncing:
return
_settings.set_vsync(enabled)
func _on_volume_changed(value: float, bus_name: String) -> void:
_volume_rows[bus_name]["label"].text = "%d%%" % roundi(value * 100.0)
if _syncing:
return
_settings.set_volume(bus_name, value)
func _on_reset_pressed() -> void:
_settings.reset_to_defaults()
_bindings_hint.text = _hint_default
_sync_from_settings()
func _start_capture(action: String) -> void:
if _capturing_action != "":
return
_capturing_action = action
_bindings_hint.text = _hint_default
_refresh_bindings()