extends Control # Settings screen: player-facing video knobs on top of VideoSettings (the # autoload holding + persisting them). Preset/AA/vsync/fps-cap/resolution # scale apply immediately since they're Viewport- or DisplayServer-wide; # glow/brightness/shadow/SDFGI/SSIL/SSAO apply the next time an arena loads # an Environment, or instantly to an already-loaded one via # VideoSettings.settings_changed (see arena.gd) — task 0.17. const AA_OPTIONS := [ {"name": "Off", "mode": VideoSettings.AAMode.OFF}, {"name": "FXAA", "mode": VideoSettings.AAMode.FXAA}, {"name": "MSAA 2x", "mode": VideoSettings.AAMode.MSAA_2X}, {"name": "MSAA 4x", "mode": VideoSettings.AAMode.MSAA}, {"name": "MSAA 4x + FXAA", "mode": VideoSettings.AAMode.MSAA_FXAA}, ] const PRESET_OPTIONS := [ {"name": "Low", "preset": VideoSettings.Preset.LOW}, {"name": "Medium", "preset": VideoSettings.Preset.MEDIUM}, {"name": "High", "preset": VideoSettings.Preset.HIGH}, {"name": "Custom", "preset": VideoSettings.Preset.CUSTOM}, ] const VSYNC_OPTIONS := [ {"name": "Disabled", "mode": VideoSettings.VsyncMode.DISABLED}, {"name": "Enabled", "mode": VideoSettings.VsyncMode.ENABLED}, {"name": "Adaptive", "mode": VideoSettings.VsyncMode.ADAPTIVE}, ] @onready var preset_dropdown: OptionButton = %PresetDropdown @onready var aa_dropdown: OptionButton = %AADropdown @onready var glow_slider: HSlider = %GlowSlider @onready var glow_value_label: Label = %GlowValueLabel @onready var brightness_slider: HSlider = %BrightnessSlider @onready var brightness_value_label: Label = %BrightnessValueLabel @onready var resolution_slider: HSlider = %ResolutionSlider @onready var resolution_value_label: Label = %ResolutionValueLabel @onready var vsync_dropdown: OptionButton = %VsyncDropdown @onready var fps_cap_dropdown: OptionButton = %FpsCapDropdown @onready var fps_readout_label: Label = %FpsReadoutLabel # fps_cap_dropdown item index -> VideoSettings divisor (0 = uncapped). Built # in _ready() from the live refresh rate so the menu never hardcodes a # specific display's numbers. var _fps_cap_divisors: Array[int] = [] var _populating := false func _ready() -> void: AudioManager.bind_tree_buttons(self) # An idle settings screen has no reason to render past the display's own # refresh rate; _on_back_pressed only returns to another capped menu, so # no uncap is needed there (contrast main_menu.gd's _leave_to_gameplay). var refresh_rate := DisplayServer.screen_get_refresh_rate() Engine.max_fps = int(refresh_rate) if refresh_rate > 0 else 0 _populating = true _populate_preset_dropdown() _populate_aa_dropdown() _populate_vsync_dropdown() _populate_fps_cap_dropdown(refresh_rate) _populating = false glow_slider.value = VideoSettings.glow_scale brightness_slider.value = VideoSettings.brightness resolution_slider.value = VideoSettings.resolution_scale _update_glow_label() _update_brightness_label() _update_resolution_label() _update_fps_cap_enabled() func _process(_delta: float) -> void: fps_readout_label.text = "%d fps" % Performance.get_monitor(Performance.TIME_FPS) func _populate_preset_dropdown() -> void: preset_dropdown.clear() var selected := 0 for i in PRESET_OPTIONS.size(): preset_dropdown.add_item(PRESET_OPTIONS[i]["name"]) if PRESET_OPTIONS[i]["preset"] == VideoSettings.preset: selected = i preset_dropdown.select(selected) func _populate_aa_dropdown() -> void: aa_dropdown.clear() var selected := 0 for i in AA_OPTIONS.size(): aa_dropdown.add_item(AA_OPTIONS[i]["name"]) if AA_OPTIONS[i]["mode"] == VideoSettings.aa_mode: selected = i aa_dropdown.select(selected) func _populate_vsync_dropdown() -> void: vsync_dropdown.clear() var selected := 0 for i in VSYNC_OPTIONS.size(): vsync_dropdown.add_item(VSYNC_OPTIONS[i]["name"]) if VSYNC_OPTIONS[i]["mode"] == VideoSettings.vsync_mode: selected = i vsync_dropdown.select(selected) # Options derive from the live refresh rate rather than a fixed list, per # task 0.17's "fps cap derived from screen_get_refresh_rate() divisors". # A -1 (or otherwise non-positive) query falls back to "Uncapped" only, # rather than presenting cap choices the engine can't compute a value for. func _populate_fps_cap_dropdown(refresh_rate: float) -> void: fps_cap_dropdown.clear() _fps_cap_divisors.clear() fps_cap_dropdown.add_item("Uncapped") _fps_cap_divisors.append(0) if refresh_rate > 0.0: for divisor in [1, 2, 3, 4]: var hz := refresh_rate / float(divisor) fps_cap_dropdown.add_item("%d fps (refresh / %d)" % [roundi(hz), divisor]) _fps_cap_divisors.append(divisor) var selected := _fps_cap_divisors.find(VideoSettings.fps_cap_divisor) fps_cap_dropdown.select(maxi(selected, 0)) # The fps cap dropdown only means anything with vsync Disabled — vsync # itself already caps to (a multiple of) the refresh rate otherwise. func _update_fps_cap_enabled() -> void: fps_cap_dropdown.disabled = VideoSettings.vsync_mode != VideoSettings.VsyncMode.DISABLED func _update_glow_label() -> void: glow_value_label.text = "%d%%" % roundi(glow_slider.value * 100.0) func _update_brightness_label() -> void: brightness_value_label.text = "%d%%" % roundi(brightness_slider.value * 100.0) func _update_resolution_label() -> void: resolution_value_label.text = "%d%%" % roundi(resolution_slider.value * 100.0) func _on_preset_dropdown_item_selected(index: int) -> void: VideoSettings.apply_preset(PRESET_OPTIONS[index]["preset"]) # The bundle may have changed AA/resolution scale underneath the other # controls — resync them without re-triggering their own "user changed # this by hand" mark_custom() path. _populating = true _populate_aa_dropdown() resolution_slider.value = VideoSettings.resolution_scale _populating = false _update_resolution_label() func _on_aa_dropdown_item_selected(index: int) -> void: VideoSettings.aa_mode = AA_OPTIONS[index]["mode"] VideoSettings.apply_aa() _mark_custom_if_user_driven() func _on_glow_slider_value_changed(value: float) -> void: VideoSettings.glow_scale = value _update_glow_label() func _on_brightness_slider_value_changed(value: float) -> void: VideoSettings.brightness = value _update_brightness_label() func _on_resolution_slider_value_changed(value: float) -> void: VideoSettings.resolution_scale = value VideoSettings.apply_resolution_scale() _update_resolution_label() _mark_custom_if_user_driven() func _on_vsync_dropdown_item_selected(index: int) -> void: VideoSettings.vsync_mode = VSYNC_OPTIONS[index]["mode"] VideoSettings.apply_vsync() _update_fps_cap_enabled() func _on_fps_cap_dropdown_item_selected(index: int) -> void: VideoSettings.fps_cap_divisor = _fps_cap_divisors[index] VideoSettings.apply_fps_cap() # Preset-gated fields (AA, resolution scale — glow/shadows/SDFGI/SSIL/SSAO # have no direct control in this menu yet) flip the preset dropdown to # Custom when the player overrides them by hand, so the dropdown never shows # a preset name next to settings that preset doesn't actually produce. Not # called while _populating (initial load) or while apply_preset() itself is # writing these same fields (VideoSettings.mark_custom() no-ops there too, # but skipping the redundant dropdown repopulation here is cheap). func _mark_custom_if_user_driven() -> void: if _populating: return VideoSettings.mark_custom() _populating = true _populate_preset_dropdown() _populating = false func _on_back_pressed() -> void: VideoSettings.save() get_tree().change_scene_to_file(ScenePaths.MAIN_MENU) func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("ui_cancel"): _on_back_pressed()