first commit

This commit is contained in:
Leonid Pershin
2026-08-10 20:39:06 +03:00
commit f16f60445c
281 changed files with 43576 additions and 0 deletions
+177
View File
@@ -0,0 +1,177 @@
extends Control
## Главное меню GWorld.
##
## Визуал собран из трёх частей: анимированный фон (shaders/menu_background.gdshader),
## тема кнопок (themes/main_menu.theme.tres) и этот скрипт — появление, наведение
## и вызов экранов настроек, сохранений и «Об игре».
## Сцена, в которую уходим по «Новая игра».
@export var game_scene: PackedScene
## Игрок нажал «Продолжить» — до того, как начнётся загрузка слота.
signal continue_requested
## Игрок нажал «Новая игра».
signal new_game_requested
const INTRO_TIME := 0.5
const HOVER_SCALE := 1.035
@onready var _saves: SaveManagerService = get_node("/root/SaveManager")
@onready var _header: Control = $Content/Column/Header
@onready var _accent_bar: ColorRect = $Content/Column/Header/AccentBar
@onready var _menu: VBoxContainer = $Content/Column/Menu
@onready var _footer: Control = $Footer
@onready var _continue_button: Button = $Content/Column/Menu/ContinueButton
@onready var _load_button: Button = $Content/Column/Menu/LoadButton
@onready var _settings_screen: Control = $SettingsScreen
@onready var _save_screen: SaveMenuScreen = $SaveScreen
@onready var _about_overlay: Control = $AboutOverlay
func _ready() -> void:
# В меню время не идёт: сессия считается только внутри игровой сцены.
_saves.end_session()
var has_saves := _saves.has_any_save()
_continue_button.visible = has_saves
_load_button.visible = has_saves
_wire_menu_buttons()
_settings_screen.closed.connect(_focus_first_button)
_save_screen.closed.connect(_focus_first_button)
var about_back: Button = $AboutOverlay/Center/Card/Body/Actions/AboutBackButton
about_back.pressed.connect(_close_about)
_animate_accent_bar()
await _play_intro()
_focus_first_button()
func _unhandled_input(event: InputEvent) -> void:
if not _about_overlay.visible or not event.is_action_pressed("ui_cancel"):
return
_close_about()
get_viewport().set_input_as_handled()
# --- Меню -------------------------------------------------------------------
func _wire_menu_buttons() -> void:
for button: Button in _menu.get_children():
# Мышь и клавиатура делят одну подсветку: наведение = фокус.
button.mouse_entered.connect(button.grab_focus)
button.focus_entered.connect(_on_button_focus.bind(button, true))
button.focus_exited.connect(_on_button_focus.bind(button, false))
button.pressed.connect(_on_menu_pressed.bind(button.name))
func _on_button_focus(button: Button, focused: bool) -> void:
button.pivot_offset = Vector2(0.0, button.size.y * 0.5)
var target := Vector2.ONE * (HOVER_SCALE if focused else 1.0)
var tween := create_tween().set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
tween.tween_property(button, "scale", target, 0.18)
func _on_menu_pressed(button_name: StringName) -> void:
match String(button_name):
"ContinueButton":
continue_requested.emit()
_continue_latest()
"NewGameButton":
new_game_requested.emit()
_start_game()
"LoadButton":
_save_screen.open(SaveMenuScreen.Mode.LOAD)
"SettingsButton":
_settings_screen.open()
"AboutButton":
_open_about()
"QuitButton":
_quit()
func _focus_first_button() -> void:
for button: Button in _menu.get_children():
if button.visible:
button.grab_focus()
return
func _continue_latest() -> void:
var slot := _saves.latest_slot()
if slot < 0:
return
# Не ждём: смена сцены уничтожит это меню вместе со старым деревом.
_saves.load_slot(slot)
func _start_game() -> void:
if game_scene == null:
push_warning("MainMenu: game_scene не назначена — назначьте её в инспекторе.")
return
get_tree().change_scene_to_packed(game_scene)
func _quit() -> void:
var tween := create_tween()
tween.tween_property(self, "modulate:a", 0.0, 0.25)
await tween.finished
get_tree().quit()
# --- «Об игре» --------------------------------------------------------------
func _open_about() -> void:
_about_overlay.modulate.a = 0.0
_about_overlay.visible = true
# Контейнеры раскладываются только после того, как узел стал видимым.
await get_tree().process_frame
var card: Control = _about_overlay.get_node("Center/Card")
card.pivot_offset = card.size * 0.5
card.scale = Vector2(0.96, 0.96)
var tween := create_tween().set_parallel(true).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
tween.tween_property(_about_overlay, "modulate:a", 1.0, 0.18)
tween.tween_property(card, "scale", Vector2.ONE, 0.22)
var back_button: Button = card.get_node("Body/Actions").get_child(0)
back_button.grab_focus()
func _close_about() -> void:
var tween := create_tween().set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_CUBIC)
tween.tween_property(_about_overlay, "modulate:a", 0.0, 0.15)
await tween.finished
_about_overlay.visible = false
_focus_first_button()
# --- Анимация ---------------------------------------------------------------
func _play_intro() -> void:
var tween := create_tween().set_parallel(true).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
_header.modulate.a = 0.0
tween.tween_property(_header, "modulate:a", 1.0, INTRO_TIME)
_footer.modulate.a = 0.0
tween.tween_property(_footer, "modulate:a", 1.0, INTRO_TIME).set_delay(0.35)
var index := 0
for button: Button in _menu.get_children():
if not button.visible:
continue
button.modulate.a = 0.0
tween.tween_property(button, "modulate:a", 1.0, 0.35).set_delay(0.2 + index * 0.07)
index += 1
await tween.finished
func _animate_accent_bar() -> void:
# Медленное «дыхание» акцентной черты под заголовком.
var tween := create_tween().set_loops().set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_SINE)
tween.tween_property(_accent_bar, "custom_minimum_size:x", 230.0, 2.6)
tween.tween_property(_accent_bar, "custom_minimum_size:x", 140.0, 2.6)
+1
View File
@@ -0,0 +1 @@
uid://3plo51qjjlpl
+124
View File
@@ -0,0 +1,124 @@
extends CanvasLayer
## Меню паузы. Кладётся внутрь игровой сцены и работает само по себе:
## ловит действие «pause», останавливает дерево и переиспользует те же
## экраны настроек и сохранений, что и главное меню.
##
## Слой обрабатывается всегда (process_mode = ALWAYS), поэтому анимации и
## ввод продолжают работать, пока остальная игра стоит на паузе.
## Куда уходим по «В главное меню».
@export_file("*.tscn") var main_menu_scene: String = "res://scenes/ui/main_menu.tscn"
signal opened
signal closed
@onready var _saves: SaveManagerService = get_node("/root/SaveManager")
@onready var _root: Control = $Root
@onready var _card: Control = $Root/Center/Card
@onready var _menu: VBoxContainer = $Root/Center/Card/Body/Menu
@onready var _settings_screen: Control = $Root/SettingsScreen
@onready var _save_screen: SaveMenuScreen = $Root/SaveScreen
## Режим курсора до паузы — игра могла держать его захваченным.
var _previous_mouse_mode := Input.MOUSE_MODE_VISIBLE
func _ready() -> void:
_root.visible = false
for button: Button in _menu.get_children():
# Мышь и клавиатура делят одну подсветку: наведение = фокус.
button.mouse_entered.connect(button.grab_focus)
button.pressed.connect(_on_menu_pressed.bind(button.name))
_settings_screen.closed.connect(_focus_first_button)
_save_screen.closed.connect(_focus_first_button)
func _unhandled_input(event: InputEvent) -> void:
# Вложенные экраны сами обрабатывают Esc и гасят событие — сюда оно не
# дойдёт, но проверка оставлена на случай другой раскладки для «pause».
if _settings_screen.visible or _save_screen.visible:
return
if not event.is_action_pressed("pause"):
return
toggle()
get_viewport().set_input_as_handled()
func toggle() -> void:
if _root.visible:
close()
else:
open()
func open() -> void:
# Миниатюру снимаем до показа меню, иначе в слот попадёт само меню.
_saves.capture_thumbnail()
get_tree().paused = true
_previous_mouse_mode = Input.mouse_mode
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_root.modulate.a = 0.0
_root.visible = true
# Контейнеры раскладываются только после того, как узел стал видимым.
await get_tree().process_frame
_card.pivot_offset = _card.size * 0.5
_card.scale = Vector2(0.96, 0.96)
var tween := create_tween().set_parallel(true).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
tween.tween_property(_root, "modulate:a", 1.0, 0.15)
tween.tween_property(_card, "scale", Vector2.ONE, 0.2)
_focus_first_button()
opened.emit()
func close() -> void:
var tween := create_tween().set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_CUBIC)
tween.tween_property(_root, "modulate:a", 0.0, 0.12)
await tween.finished
_root.visible = false
get_tree().paused = false
Input.mouse_mode = _previous_mouse_mode
closed.emit()
func _on_menu_pressed(button_name: StringName) -> void:
match String(button_name):
"ResumeButton":
close()
"SaveButton":
_save_screen.open(SaveMenuScreen.Mode.SAVE)
"LoadButton":
_save_screen.open(SaveMenuScreen.Mode.LOAD)
"SettingsButton":
_settings_screen.open()
"MainMenuButton":
_return_to_main_menu()
"QuitButton":
_quit()
func _focus_first_button() -> void:
for button: Button in _menu.get_children():
if button.visible:
button.grab_focus()
return
func _return_to_main_menu() -> void:
get_tree().paused = false
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_saves.end_session()
get_tree().change_scene_to_file(main_menu_scene)
func _quit() -> void:
var tween := create_tween()
tween.tween_property(_root, "modulate:a", 0.0, 0.2)
await tween.finished
get_tree().paused = false
get_tree().quit()
+1
View File
@@ -0,0 +1 @@
uid://bajc7dq7tn5ik
+220
View File
@@ -0,0 +1,220 @@
class_name SaveMenuScreen
extends Control
## Экран слотов сохранения. Один и тот же и для записи, и для чтения —
## режим задаётся при открытии: open(Mode.SAVE) из паузы, open(Mode.LOAD)
## из главного меню.
##
## Опасные действия (перезапись и удаление) требуют второго нажатия:
## кнопка меняет подпись на «Точно?», строка статуса объясняет, что будет.
signal closed
enum Mode { SAVE, LOAD }
const MUTED := Color("#8fa5ba")
const THUMB_SIZE := Vector2(192, 108)
@onready var _saves: SaveManagerService = get_node("/root/SaveManager")
@onready var _card: Control = $Center/Card
@onready var _heading: Label = $Center/Card/Body/Heading
@onready var _slots_box: VBoxContainer = $Center/Card/Body/Scroll/Slots
@onready var _status: Label = $Center/Card/Body/Actions/Status
@onready var _back_button: Button = $Center/Card/Body/Actions/BackButton
var _mode := Mode.LOAD
## {"action": "save" | "delete", "index": int} — ждём подтверждения.
var _pending := {}
func _ready() -> void:
_back_button.pressed.connect(close)
_refresh()
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(mode: int) -> void:
_mode = mode
_pending = {}
_heading.text = "СОХРАНЕНИЕ" if mode == Mode.SAVE else "ЗАГРУЗКА"
_status.text = "Выберите слот для записи." if mode == Mode.SAVE else "Выберите сохранение."
_refresh()
modulate.a = 0.0
visible = true
# Контейнеры раскладываются только после того, как узел стал видимым.
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)
_focus_first_action()
func close() -> void:
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 _refresh() -> void:
for child in _slots_box.get_children():
child.queue_free()
for info: Dictionary in _saves.list_slots():
_slots_box.add_child(_make_row(info))
func _make_row(info: Dictionary) -> PanelContainer:
var index: int = info["index"]
var exists: bool = info["exists"]
var panel := PanelContainer.new()
panel.add_theme_stylebox_override("panel", _row_style(exists))
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 18)
panel.add_child(row)
var thumb := TextureRect.new()
thumb.custom_minimum_size = THUMB_SIZE
thumb.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
thumb.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
thumb.clip_contents = true
thumb.texture = info["thumbnail"]
row.add_child(thumb)
var info_box := VBoxContainer.new()
info_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
info_box.size_flags_vertical = Control.SIZE_SHRINK_CENTER
info_box.add_theme_constant_override("separation", 4)
row.add_child(info_box)
var title := Label.new()
title.text = "Слот %d" % (index + 1)
title.add_theme_font_size_override("font_size", 22)
info_box.add_child(title)
var meta := Label.new()
meta.add_theme_font_size_override("font_size", 16)
meta.add_theme_color_override("font_color", MUTED)
if exists:
meta.text = "%s · %s" % [
SaveManagerService.format_timestamp(info["saved_at"]),
SaveManagerService.format_playtime(info["playtime"]),
]
else:
meta.text = "пусто"
info_box.add_child(meta)
row.add_child(_make_action_button(index, exists))
row.add_child(_make_delete_button(index, exists))
return panel
func _make_action_button(index: int, exists: bool) -> Button:
var button := Button.new()
button.custom_minimum_size.x = 230
button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
button.add_theme_font_size_override("font_size", 20)
if _is_pending("save", index):
button.text = "Точно?"
elif _mode == Mode.SAVE:
button.text = "Перезаписать" if exists else "Сохранить"
else:
button.text = "Загрузить"
button.disabled = not exists
button.pressed.connect(_on_action_pressed.bind(index))
return button
func _make_delete_button(index: int, exists: bool) -> Button:
var button := Button.new()
button.custom_minimum_size.x = 170
button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
button.add_theme_font_size_override("font_size", 20)
button.text = "Точно?" if _is_pending("delete", index) else "Удалить"
button.disabled = not exists
button.pressed.connect(_on_delete_pressed.bind(index))
return button
func _row_style(filled: bool) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = Color("#ffffff0f") if filled else Color("#ffffff05")
style.border_color = Color("#ffffff1a")
style.set_border_width_all(1)
style.set_corner_radius_all(10)
style.content_margin_left = 14
style.content_margin_right = 14
style.content_margin_top = 12
style.content_margin_bottom = 12
return style
func _focus_first_action() -> void:
for row: Node in _slots_box.get_children():
for control: Node in row.get_child(0).get_children():
if control is Button and not (control as Button).disabled:
(control as Button).grab_focus()
return
_back_button.grab_focus()
# --- Действия ---------------------------------------------------------------
func _on_action_pressed(index: int) -> void:
if _mode == Mode.LOAD:
_status.text = "Загрузка слота %d" % (index + 1)
# Не ждём: смена сцены уничтожит этот экран вместе со старым деревом.
_saves.load_slot(index)
return
var info := _saves.slot_info(index)
if info["exists"] and not _is_pending("save", index):
_set_pending("save", index, "Слот %d будет перезаписан — нажмите ещё раз." % (index + 1))
return
_pending = {}
if _saves.save_to_slot(index):
_status.text = "Сохранено в слот %d." % (index + 1)
else:
_status.text = "Не удалось сохранить в слот %d." % (index + 1)
_refresh()
func _on_delete_pressed(index: int) -> void:
if not _is_pending("delete", index):
_set_pending("delete", index, "Слот %d будет удалён — нажмите ещё раз." % (index + 1))
return
_pending = {}
_saves.delete_slot(index)
_status.text = "Слот %d удалён." % (index + 1)
_refresh()
func _set_pending(action: String, index: int, message: String) -> void:
_pending = {"action": action, "index": index}
_status.text = message
_refresh()
func _is_pending(action: String, index: int) -> bool:
return _pending.get("action", "") == action and _pending.get("index", -1) == index
+1
View File
@@ -0,0 +1 @@
uid://dtjbpbm5rqb72
+258
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://jkhpwi3ngait