extends CanvasLayer # SettingsMenu - Handles UI logic for the premium settings panel @onready var tab_container = $PanelContainer/VBoxContainer/ContentSection/TabContainer @onready var close_button = $PanelContainer/VBoxContainer/Header/CloseButton # Video Controls @onready var fullscreen_toggle = %FullscreenToggle @onready var vsync_toggle = %VSyncToggle @onready var resolution_btn = %ResolutionBtn @onready var msaa_btn = %MSAABtn @onready var shadow_btn = %ShadowBtn @onready var fps_btn = %FPSCapBtn # Audio Controls @onready var master_slider = %MasterSlider @onready var music_slider = %MusicSlider @onready var sfx_slider = %SFXSlider # Controls (Keybinds) @onready var use_controller_btn = %UseControllerBtn @onready var SettingsManager = get_node_or_null("/root/SettingsManager") var listening_action: String = "" # Set when waiting for a keypress # Keys removed from the game - skip them even if stale user config has them const DEPRECATED_ACTIONS: Array = ["put", "put_alt"] # Touch Input Controls @onready var joystick_size_slider = %JoystickSizeSlider @onready var joystick_size_value = %JoystickSizeValue @onready var joystick_enabled_toggle = %JoystickEnabledToggle @onready var button_opacity_slider = %ButtonOpacitySlider @onready var button_opacity_value = %ButtonOpacityValue @onready var touch_buttons_enabled_toggle = %TouchButtonsEnabledToggle const TOUCH_CONFIG_PATH = "user://touch_controls_settings.cfg" # Controller Rebinding @onready var ctrl_grab_btn = %CtrlGrabBtn @onready var ctrl_powerup_btn = %CtrlPowerupBtn @onready var ctrl_tekton_grab_btn = %CtrlTektonGrabBtn @onready var ctrl_attack_btn = %CtrlAttackBtn @onready var ctrl_reset_btn = %CtrlResetBtn var listening_ctrl_key: String = "" # ctrl_* key being rebound func _ready(): # Theme inheritance is broken by CanvasLayer root, no need for theme = null _load_ui_values() if SettingsManager: _connect_signals() # Initial visibility visible = false func _load_ui_values(): if not SettingsManager: print("[SettingsMenu] ERROR: SettingsManager not found") return var s = SettingsManager.settings # Video fullscreen_toggle.button_pressed = s.video.fullscreen vsync_toggle.button_pressed = s.video.vsync # Populate Resolution dropdown if not already populated if resolution_btn.item_count == 0: for i in range(SettingsManager.RESOLUTIONS.size()): var res = SettingsManager.RESOLUTIONS[i] resolution_btn.add_item("%dx%d" % [res.x, res.y]) resolution_btn.selected = s.video.resolution_idx # MSAA if msaa_btn.item_count == 0: msaa_btn.add_item("Disabled") msaa_btn.add_item("2x") msaa_btn.add_item("4x") msaa_btn.add_item("8x") msaa_btn.selected = s.video.msaa # Shadows if shadow_btn.item_count == 0: shadow_btn.add_item("Low") shadow_btn.add_item("Medium") shadow_btn.add_item("High") shadow_btn.add_item("Ultra") shadow_btn.selected = s.video.shadow_quality # FPS Cap if fps_btn.item_count == 0: fps_btn.add_item("Unlimited") fps_btn.add_item("30 FPS") fps_btn.add_item("60 FPS") fps_btn.add_item("120 FPS") fps_btn.add_item("144 FPS") fps_btn.selected = s.video.fps_cap # Audio master_slider.value = s.audio.master_volume music_slider.value = s.audio.music_volume sfx_slider.value = s.audio.sfx_volume # Controls use_controller_btn.button_pressed = s.controls.get("use_controller", false) _update_all_key_labels() # Touch Input _load_touch_ui_values() # Controller Bindings _update_all_ctrl_labels() func _load_touch_ui_values(): """Load touch control settings from config file.""" var config = ConfigFile.new() var joystick_size: float = 60.0 var joystick_on: bool = true var btn_opacity: float = 0.7 var touch_btns_on: bool = true if config.load(TOUCH_CONFIG_PATH) == OK: joystick_size = config.get_value("touch_controls", "joystick_size", 60.0) joystick_on = config.get_value("touch_controls", "joystick_enabled", true) btn_opacity = config.get_value("touch_controls", "button_opacity", 0.7) touch_btns_on = config.get_value("touch_controls", "touch_buttons_enabled", true) joystick_size_slider.value = joystick_size joystick_size_value.text = str(int(joystick_size)) joystick_enabled_toggle.button_pressed = joystick_on button_opacity_slider.value = btn_opacity button_opacity_value.text = str(int(btn_opacity * 100)) + "%" touch_buttons_enabled_toggle.button_pressed = touch_btns_on func _connect_signals(): # Video fullscreen_toggle.toggled.connect(_on_video_setting_changed) vsync_toggle.toggled.connect(_on_video_setting_changed) resolution_btn.item_selected.connect(_on_resolution_selected) msaa_btn.item_selected.connect(_on_video_property_changed.bind("msaa")) shadow_btn.item_selected.connect(_on_video_property_changed.bind("shadow_quality")) fps_btn.item_selected.connect(_on_video_property_changed.bind("fps_cap")) # Audio master_slider.value_changed.connect(_on_audio_setting_changed.bind("master_volume")) music_slider.value_changed.connect(_on_audio_setting_changed.bind("music_volume")) sfx_slider.value_changed.connect(_on_audio_setting_changed.bind("sfx_volume")) # Controls use_controller_btn.toggled.connect(_on_control_setting_changed.bind("use_controller")) # Touch Input joystick_size_slider.value_changed.connect(_on_joystick_size_changed) joystick_enabled_toggle.toggled.connect(_on_touch_toggle_changed.bind("joystick_enabled")) button_opacity_slider.value_changed.connect(_on_button_opacity_changed) touch_buttons_enabled_toggle.toggled.connect(_on_touch_toggle_changed.bind("touch_buttons_enabled")) # Controller Rebinding ctrl_grab_btn.pressed.connect(_on_ctrl_remap_pressed.bind("ctrl_grab")) ctrl_powerup_btn.pressed.connect(_on_ctrl_remap_pressed.bind("ctrl_use_powerup")) ctrl_tekton_grab_btn.pressed.connect(_on_ctrl_remap_pressed.bind("ctrl_tekton_grab")) ctrl_attack_btn.pressed.connect(_on_ctrl_remap_pressed.bind("ctrl_attack_mode")) ctrl_reset_btn.pressed.connect(_on_ctrl_reset_pressed) # Close close_button.pressed.connect(func(): listening_ctrl_key = ""; visible = false) # Connect remapping buttons for action_name in SettingsManager.settings.controls.keys(): if action_name == "use_controller" or action_name in DEPRECATED_ACTIONS: continue # Check Primary Button var primary_btn = get_node_or_null("%" + action_name.to_pascal_case() + "Btn") if primary_btn: if primary_btn.pressed.is_connected(_on_remap_button_pressed): primary_btn.pressed.disconnect(_on_remap_button_pressed) primary_btn.pressed.connect(_on_remap_button_pressed.bind(action_name)) # Check Alt Button if not action_name.ends_with("_alt"): var alt_action = action_name + "_alt" var alt_btn = get_node_or_null("%" + action_name.to_pascal_case() + "AltBtn") if alt_btn: if alt_btn.pressed.is_connected(_on_remap_button_pressed): alt_btn.pressed.disconnect(_on_remap_button_pressed) alt_btn.pressed.connect(_on_remap_button_pressed.bind(alt_action)) func _on_video_setting_changed(_unused = false): SettingsManager.settings.video.fullscreen = fullscreen_toggle.button_pressed SettingsManager.settings.video.vsync = vsync_toggle.button_pressed SettingsManager.save_settings() SettingsManager.apply_video_settings() func _on_resolution_selected(idx: int): SettingsManager.settings.video.resolution_idx = idx SettingsManager.save_settings() SettingsManager.apply_video_settings() func _on_video_property_changed(idx: int, property_name: String): SettingsManager.settings.video[property_name] = idx SettingsManager.save_settings() SettingsManager.apply_video_settings() func _on_audio_setting_changed(value: float, key: String): SettingsManager.settings.audio[key] = value SettingsManager.save_settings() SettingsManager.apply_audio_settings() func _on_control_setting_changed(value: bool, key: String): SettingsManager.settings.controls[key] = value SettingsManager.save_settings() # Emit settings_applied so all UI (touch buttons, prompts) refresh their shortcut labels if key == "use_controller": SettingsManager.apply_control_settings() SettingsManager.emit_signal("settings_applied") func _on_joystick_size_changed(value: float): joystick_size_value.text = str(int(value)) _save_touch_value("joystick_size", value) # Live update the joystick if it's in the scene var tc = _get_touch_controls() if tc: tc.joystick_size = value if tc.virtual_joystick and tc.virtual_joystick.has_method("set_radius"): tc.virtual_joystick.set_radius(value) func _on_button_opacity_changed(value: float): button_opacity_value.text = str(int(value * 100)) + "%" _save_touch_value("button_opacity", value) var tc = _get_touch_controls() if tc: tc.button_opacity = value if tc.has_method("_apply_settings"): tc._apply_settings() func _on_touch_toggle_changed(state: bool, key: String): _save_touch_value(key, state) var tc = _get_touch_controls() if tc: if key == "joystick_enabled": tc.joystick_enabled = state elif key == "touch_buttons_enabled": tc.touch_buttons_enabled = state if tc.has_method("_apply_settings"): tc._apply_settings() func _save_touch_value(key: String, value) -> void: """Save a single touch config key without overwriting other keys.""" var config = ConfigFile.new() config.load(TOUCH_CONFIG_PATH) # Load existing (ignore error if not found) config.set_value("touch_controls", key, value) config.save(TOUCH_CONFIG_PATH) func _get_touch_controls() -> Node: """Try to find the TouchControls node in the Main scene.""" var main = get_tree().get_root().get_node_or_null("Main") if main: return main.get_node_or_null("TouchLayer/TouchControls") return null func _on_remap_button_pressed(action_name: String): listening_action = action_name var btn = get_node_or_null("%" + action_name.to_pascal_case() + "Btn") if btn: btn.text = "..." btn.modulate = Color(0.5, 1.0, 0.5) # Signal "Listening" func _input(event): # Keyboard remapping if listening_action != "" and event is InputEventKey and event.pressed: var keycode = event.keycode # PREVENT DUPLICATES (User Request) var existing_action = SettingsManager.is_key_used(keycode) if existing_action != "" and existing_action != listening_action: var error_btn = get_node_or_null("%" + listening_action.to_pascal_case() + "Btn") if error_btn: error_btn.text = "ALREADY USED!" error_btn.modulate = Color(1.0, 0.3, 0.3) var sfx = get_node_or_null("/root/SfxManager") if sfx and sfx.has_method("play_rpc"): sfx.rpc("play_rpc", "error") listening_action = "" await get_tree().create_timer(1.2).timeout _update_all_key_labels() get_viewport().set_input_as_handled() return SettingsManager.set_control(listening_action, keycode) _update_key_label(listening_action) listening_action = "" get_viewport().set_input_as_handled() # Controller button remapping if listening_ctrl_key != "" and event is InputEventJoypadButton and event.pressed: var btn_idx = event.button_index # Duplicate check var existing = SettingsManager.is_controller_button_used(btn_idx) var ctrl_btn = _get_ctrl_btn(listening_ctrl_key) if existing != "" and existing != listening_ctrl_key: if ctrl_btn: ctrl_btn.text = "ALREADY USED!" ctrl_btn.modulate = Color(1.0, 0.3, 0.3) listening_ctrl_key = "" await get_tree().create_timer(1.2).timeout _update_all_ctrl_labels() get_viewport().set_input_as_handled() return SettingsManager.set_controller_binding(listening_ctrl_key, btn_idx) listening_ctrl_key = "" _update_all_ctrl_labels() get_viewport().set_input_as_handled() func _update_all_key_labels(): for action_name in SettingsManager.settings.controls.keys(): if action_name in DEPRECATED_ACTIONS: continue _update_key_label(action_name) func _update_key_label(action_name: String): var btn = get_node_or_null("%" + action_name.to_pascal_case() + "Btn") if btn and SettingsManager: btn.text = SettingsManager.get_control_text(action_name) btn.modulate = Color.WHITE func _update_all_ctrl_labels(): """Update all controller button labels from saved settings.""" if not SettingsManager: return ctrl_grab_btn.text = SettingsManager.get_controller_binding_text("ctrl_grab") ctrl_grab_btn.modulate = Color.WHITE ctrl_powerup_btn.text = SettingsManager.get_controller_binding_text("ctrl_use_powerup") ctrl_powerup_btn.modulate = Color.WHITE ctrl_tekton_grab_btn.text = SettingsManager.get_controller_binding_text("ctrl_tekton_grab") ctrl_tekton_grab_btn.modulate = Color.WHITE ctrl_attack_btn.text = SettingsManager.get_controller_binding_text("ctrl_attack_mode") ctrl_attack_btn.modulate = Color.WHITE func _on_ctrl_remap_pressed(ctrl_key: String): listening_ctrl_key = ctrl_key var btn = _get_ctrl_btn(ctrl_key) if btn: btn.text = "Press a button..." btn.modulate = Color(0.5, 1.0, 0.5) func _get_ctrl_btn(ctrl_key: String) -> Button: match ctrl_key: "ctrl_grab": return ctrl_grab_btn "ctrl_use_powerup": return ctrl_powerup_btn "ctrl_tekton_grab": return ctrl_tekton_grab_btn "ctrl_attack_mode": return ctrl_attack_btn return null func _on_ctrl_reset_pressed(): """Reset controller bindings to defaults.""" if not SettingsManager: return SettingsManager.settings.controls["ctrl_grab"] = JOY_BUTTON_A SettingsManager.settings.controls["ctrl_use_powerup"] = JOY_BUTTON_Y SettingsManager.settings.controls["ctrl_tekton_grab"] = JOY_BUTTON_RIGHT_SHOULDER SettingsManager.settings.controls["ctrl_attack_mode"] = JOY_BUTTON_X SettingsManager.apply_control_settings() SettingsManager.save_settings() _update_all_ctrl_labels() func open(): _load_ui_values() visible = true