Files
tekton/scripts/managers/settings_manager.gd
T
2026-06-12 18:05:04 +08:00

338 lines
11 KiB
GDScript

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 ""