extends Node # SettingsManager - Global singleton for handling game settings persistence # Autoloaded as "SettingsManager" const SETTINGS_FILE = "user://game_settings.cfg" const RESOLUTIONS = [ Vector2i(1024, 576), Vector2i(1280, 720), Vector2i(1366, 768), Vector2i(1600, 900), Vector2i(1920, 1080), Vector2i(2560, 1440) ] # Default values var settings = { "video": { "fullscreen": true, "vsync": true, "resolution_idx": 1, "msaa": 0, # 0: Disabled, 1: 2x, 2: 4x, 3: 8x "shadow_quality": 2, # 0: Low, 1: Med, 2: High, 3: Ultra "fps_cap": 2 # 0: Unlimited, 1: 30, 2: 60, 3: 120, 4: 144 }, "audio": { "master_volume": 0.8, "music_volume": 0.7, "sfx_volume": 0.9 }, "controls": { "use_controller": false, # Movement "move_up": KEY_W, "move_up_alt": KEY_UP, "move_down": KEY_S, "move_down_alt": KEY_DOWN, "move_left": KEY_A, "move_left_alt": KEY_LEFT, "move_right": KEY_D, "move_right_alt": KEY_RIGHT, # Actions "grab": KEY_SPACE, "grab_alt": KEY_J, "tekton_grab": KEY_G, "tekton_grab_alt": KEY_L, # Power-Up Controls "use_powerup": KEY_F, "use_powerup_alt": KEY_SHIFT, # Power Bar Controls / Special "attack_mode": KEY_Q, "attack_mode_alt": KEY_U, # Controller Button Bindings (JOY_BUTTON_* indices) "ctrl_grab": JOY_BUTTON_A, "ctrl_tekton_grab": JOY_BUTTON_RIGHT_SHOULDER, "ctrl_use_powerup": JOY_BUTTON_Y, "ctrl_attack_mode": JOY_BUTTON_X } } signal settings_applied signal control_remapped(action: String, key: int) func _ready(): load_settings() apply_all_settings() func load_settings(): var config = ConfigFile.new() var err = config.load(SETTINGS_FILE) if err == OK: for section in settings.keys(): for key in settings[section].keys(): settings[section][key] = config.get_value(section, key, settings[section][key]) print("[Settings] Loaded.") else: print("[Settings] Using defaults.") # Remove deprecated keys that may persist in old saved configs var deprecated_keys = ["put", "put_alt"] for k in deprecated_keys: if settings.controls.has(k): settings.controls.erase(k) print("[Settings] Removed deprecated control: ", k) func save_settings(): var config = ConfigFile.new() for section in settings.keys(): for key in settings[section].keys(): config.set_value(section, key, settings[section][key]) config.save(SETTINGS_FILE) func apply_all_settings(): apply_video_settings() apply_audio_settings() apply_control_settings() emit_signal("settings_applied") func apply_video_settings(): var video = settings.video if video.fullscreen: DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN) else: DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) # Apply resolution size when windowed var res_idx = video.resolution_idx if res_idx >= 0 and res_idx < RESOLUTIONS.size(): var target_res = RESOLUTIONS[res_idx] DisplayServer.window_set_size(target_res) # Center window after resize var screen = DisplayServer.window_get_current_screen() var screen_rect = DisplayServer.screen_get_usable_rect(screen) var window_size = DisplayServer.window_get_size() DisplayServer.window_set_position(screen_rect.position + (screen_rect.size - window_size) / 2) if video.vsync: DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED) else: DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED) # Apply MSAA var viewport = get_viewport() match video.msaa: 0: viewport.msaa_3d = Viewport.MSAA_DISABLED 1: viewport.msaa_3d = Viewport.MSAA_2X 2: viewport.msaa_3d = Viewport.MSAA_4X 3: viewport.msaa_3d = Viewport.MSAA_8X # Apply FPS Cap match video.fps_cap: 0: Engine.max_fps = 0 1: Engine.max_fps = 30 2: Engine.max_fps = 60 3: Engine.max_fps = 120 4: Engine.max_fps = 144 # Apply Shadow Quality (Simplified for Forward+) match video.shadow_quality: 0: # Low RenderingServer.directional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_LOW) RenderingServer.positional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_LOW) 1: # Medium RenderingServer.directional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_MEDIUM) RenderingServer.positional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_MEDIUM) 2: # High RenderingServer.directional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_HIGH) RenderingServer.positional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_HIGH) 3: # Ultra RenderingServer.directional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_ULTRA) RenderingServer.positional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_ULTRA) func apply_audio_settings(): var audio = settings.audio print("[SettingsManager] Applying audio settings: ", audio) set_bus_volume("Master", audio.master_volume) set_bus_volume("Music", audio.music_volume) set_bus_volume("SFX", audio.sfx_volume) # Explicitly ensure Music and SFX are routed to Master if they exist var master_idx = AudioServer.get_bus_index("Master") if master_idx != -1: var music_idx = AudioServer.get_bus_index("Music") if music_idx != -1: AudioServer.set_bus_send(music_idx, "Master") var sfx_idx = AudioServer.get_bus_index("SFX") if sfx_idx != -1: AudioServer.set_bus_send(sfx_idx, "Master") func set_bus_volume(bus_name: String, volume_linear: float): var bus_idx = AudioServer.get_bus_index(bus_name) if bus_idx != -1: AudioServer.set_bus_volume_db(bus_idx, linear_to_db(volume_linear)) AudioServer.set_bus_mute(bus_idx, volume_linear <= 0.001) func apply_control_settings(): # Sync custom settings with InputMap var mapping = { "move_up": "move_north", "move_down": "move_south", "move_left": "move_west", "move_right": "move_east", "grab": "action_grab", "use_powerup": "use_powerup", "tekton_grab": "action_grab_tekton", "attack_mode": "action_knock_tekton" } for setting_key in mapping.keys(): var action_name = mapping[setting_key] if not InputMap.has_action(action_name): InputMap.add_action(action_name) InputMap.action_erase_events(action_name) # Add Primary var primary_key = settings.controls.get(setting_key) if primary_key: var event = InputEventKey.new() event.keycode = primary_key InputMap.action_add_event(action_name, event) # Add Secondary var secondary_key = settings.controls.get(setting_key + "_alt") if secondary_key: var event = InputEventKey.new() event.keycode = secondary_key InputMap.action_add_event(action_name, event) # Add Joypad defaults for actions - use saved ctrl_* bindings var joy_button_mapped = -1 match action_name: "action_grab": joy_button_mapped = settings.controls.get("ctrl_grab", JOY_BUTTON_A) "use_powerup": joy_button_mapped = settings.controls.get("ctrl_use_powerup", JOY_BUTTON_Y) "action_grab_tekton": joy_button_mapped = settings.controls.get("ctrl_tekton_grab", JOY_BUTTON_RIGHT_SHOULDER) "action_knock_tekton": joy_button_mapped = settings.controls.get("ctrl_attack_mode", JOY_BUTTON_X) if action_name == "action_put": joy_button_mapped = JOY_BUTTON_B if joy_button_mapped != -1: var joy_btn_event = InputEventJoypadButton.new() joy_btn_event.button_index = joy_button_mapped InputMap.action_add_event(action_name, joy_btn_event) # Add Joypad defaults for movement if action_name.begins_with("move_"): var joy_axis = -1 var axis_val = 0.0 match action_name: "move_north": joy_axis = JOY_AXIS_LEFT_Y; axis_val = -1.0 "move_south": joy_axis = JOY_AXIS_LEFT_Y; axis_val = 1.0 "move_west": joy_axis = JOY_AXIS_LEFT_X; axis_val = -1.0 "move_east": joy_axis = JOY_AXIS_LEFT_X; axis_val = 1.0 if joy_axis != -1: var joy_event = InputEventJoypadMotion.new() joy_event.axis = joy_axis joy_event.axis_value = axis_val InputMap.action_add_event(action_name, joy_event) func set_control(action_name: String, keycode: int): if settings.controls.has(action_name): settings.controls[action_name] = keycode apply_control_settings() # Apply immediately emit_signal("control_remapped", action_name, keycode) save_settings() # --- Controller Binding --- # Godot 4 JoyButton name map (var not const — enum keys can't be used in const dicts) var CTRL_BUTTON_NAMES: Dictionary = { JOY_BUTTON_A: "A / Cross", JOY_BUTTON_B: "B / Circle", JOY_BUTTON_X: "X / Square", JOY_BUTTON_Y: "Y / Triangle", JOY_BUTTON_LEFT_SHOULDER: "LB / L1", JOY_BUTTON_RIGHT_SHOULDER: "RB / R1", JOY_BUTTON_LEFT_STICK: "L3", JOY_BUTTON_RIGHT_STICK: "R3", JOY_BUTTON_BACK: "Select / Share", JOY_BUTTON_START: "Start / Options", JOY_BUTTON_DPAD_UP: "D-Up", JOY_BUTTON_DPAD_DOWN: "D-Down", JOY_BUTTON_DPAD_LEFT: "D-Left", JOY_BUTTON_DPAD_RIGHT: "D-Right", } func get_joy_button_name(button_index: int) -> String: return CTRL_BUTTON_NAMES.get(button_index, "Btn %d" % button_index) func get_controller_binding_text(ctrl_key: String) -> String: """Return the button name for a ctrl_* settings key.""" var idx = settings.controls.get(ctrl_key, -1) if idx == -1: return "Unbound" return get_joy_button_name(idx) func get_action_display(action_key: String) -> String: """Return keyboard text or controller button name based on use_controller setting.""" if settings.controls.get("use_controller", false): # Map action key to its ctrl_* key var ctrl_key_map = { "grab": "ctrl_grab", "use_powerup": "ctrl_use_powerup", "tekton_grab": "ctrl_tekton_grab", "action_grab_tekton": "ctrl_tekton_grab", "attack_mode": "ctrl_attack_mode", "action_knock_tekton": "ctrl_attack_mode", } if ctrl_key_map.has(action_key): return get_controller_binding_text(ctrl_key_map[action_key]) # Movement axes on left stick if action_key.begins_with("move_"): return "L-Stick" return get_control_text(action_key) return get_control_text(action_key) func set_controller_binding(ctrl_key: String, button_index: int): """Save a controller button binding and re-apply input maps.""" if settings.controls.has(ctrl_key): settings.controls[ctrl_key] = button_index apply_control_settings() emit_signal("control_remapped", ctrl_key, button_index) save_settings() func is_controller_button_used(button_index: int) -> String: """Check if a controller button is already assigned.""" for key in settings.controls.keys(): if key.begins_with("ctrl_") and settings.controls[key] == button_index: return key return "" func get_control_keycode(action_name: String) -> int: # settings.controls stores keys by the same name callers pass in # (e.g. "tekton_grab", "attack_mode", "grab"). The InputMap action name # (e.g. "action_grab_tekton") is a separate concept handled by # apply_control_settings(). Do NOT remap here. return settings.controls.get(action_name, -1) func get_control_text(action_name: String) -> String: var code = get_control_keycode(action_name) if code == -1: return "Unbound" return OS.get_keycode_string(code) func is_key_used(keycode: int) -> String: """Check if a keycode is already assigned to any action.""" for action in settings.controls.keys(): if action == "use_controller": continue if settings.controls[action] == keycode: return action return ""