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()