first commit
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user