1243 lines
43 KiB
GDScript
1243 lines
43 KiB
GDScript
extends CanvasLayer
|
|
|
|
var bg: ColorRect
|
|
var main_vbox: VBoxContainer
|
|
var resume_btn: Button
|
|
var respawn_btn: Button
|
|
var loadouts_btn: Button
|
|
var settings_btn: Button
|
|
var return_btn: Button
|
|
var exit_btn: Button
|
|
|
|
# Character preview integrated into the loadout screen.
|
|
var character_desc: Label
|
|
var _preview_viewport: SubViewport
|
|
var _preview_model: SkinnedPlayerModel
|
|
var _preview_pivot: Node3D
|
|
var _preview_id: String = ""
|
|
|
|
# Loadout UI
|
|
var loadout_editor: Control
|
|
var loadout_list_vbox: VBoxContainer
|
|
var edit_panel: PanelContainer
|
|
var name_edit: LineEdit
|
|
var primary1_opt: OptionButton
|
|
var primary2_opt: OptionButton
|
|
var special_opt: OptionButton
|
|
var melee_opt: OptionButton
|
|
var skin_opt: OptionButton
|
|
var loadout_summary: Label
|
|
var edit_title_label: Label
|
|
var save_loadout_btn: Button
|
|
var back_to_main_btn: Button
|
|
var editing_index: int = -1
|
|
|
|
# Settings UI
|
|
var settings_editor: Control
|
|
var settings_back_btn: Button
|
|
var _world_fov_slider: HSlider
|
|
var _world_fov_input: LineEdit
|
|
var _debug_ui_checkbox: CheckButton
|
|
var _mouse_sens_slider: HSlider
|
|
var _mouse_sens_input: LineEdit
|
|
var _ads_sens_slider: HSlider
|
|
var _ads_sens_input: LineEdit
|
|
var _display_mode_opt: OptionButton
|
|
var _resolution_opt: OptionButton
|
|
var _fps_slider: HSlider
|
|
var _fps_input: LineEdit
|
|
var _show_fps_checkbox: CheckButton
|
|
var _show_movement_speed_checkbox: CheckButton
|
|
var _vol_master_slider: HSlider
|
|
var _vol_master_input: LineEdit
|
|
var _vol_sfx_slider: HSlider
|
|
var _vol_sfx_input: LineEdit
|
|
var _vol_music_slider: HSlider
|
|
var _vol_music_input: LineEdit
|
|
var _vol_wind_slider: HSlider
|
|
var _vol_wind_input: LineEdit
|
|
var keybind_vbox: VBoxContainer
|
|
var rebind_overlay: ColorRect
|
|
var rebind_label: Label
|
|
var waiting_for_input_action: String = ""
|
|
|
|
var quit_dialog: ConfirmationDialog
|
|
var return_dialog: ConfirmationDialog
|
|
|
|
func _ready() -> void:
|
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
|
layer = 100 # Ensure it's on top
|
|
visible = false
|
|
|
|
# This screen is an autoload, so it can be the FIRST thing the player sees
|
|
# styled — opened over a level that never loaded the HUD, it used to render
|
|
# in Godot's default grey because it relied on some other screen having
|
|
# applied the theme. apply_global is idempotent; just call it.
|
|
UITheme.apply_global(get_tree())
|
|
_build_ui()
|
|
_connect_signals()
|
|
UITheme.wire_sounds(self)
|
|
|
|
func _input(event: InputEvent) -> void:
|
|
if waiting_for_input_action != "":
|
|
if event is InputEventKey or event is InputEventMouseButton:
|
|
if event.is_pressed():
|
|
SettingsManager.rebind_action(waiting_for_input_action, event)
|
|
waiting_for_input_action = ""
|
|
rebind_overlay.visible = false
|
|
_populate_keybindings()
|
|
# Consume event so it doesn't trigger anything else
|
|
get_viewport().set_input_as_handled()
|
|
return
|
|
|
|
if event.is_action_pressed("ui_cancel"):
|
|
# Don't open the pause menu if we are already in the main menu
|
|
if get_tree().current_scene and get_tree().current_scene.name == "MainMenu":
|
|
return
|
|
|
|
if visible:
|
|
_resume()
|
|
else:
|
|
_pause()
|
|
|
|
func _pause() -> void:
|
|
visible = true
|
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
|
_show_main_menu()
|
|
|
|
func _resume() -> void:
|
|
visible = false
|
|
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
|
|
|
func _respawn() -> void:
|
|
_resume() # Unpause
|
|
|
|
# Find local player using their unique multiplayer ID
|
|
var local_player: Node3D = null
|
|
var pid = multiplayer.get_unique_id()
|
|
|
|
if get_tree().current_scene:
|
|
local_player = get_tree().current_scene.get_node_or_null(str(pid))
|
|
if not local_player:
|
|
var spawner = get_tree().current_scene.get_node_or_null("PlayerSpawner")
|
|
if spawner:
|
|
local_player = spawner.get_node_or_null(str(pid))
|
|
|
|
if local_player and local_player.has_method("rpc_request_respawn"):
|
|
local_player.rpc_request_respawn.rpc_id(1)
|
|
|
|
func _build_ui() -> void:
|
|
bg = ColorRect.new()
|
|
bg.color = Color(0, 0, 0, 0.7)
|
|
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
# Assigned here as well as on the root Window, because this screen hangs off
|
|
# a CanvasLayer. A Control inherits its theme from its nearest Control
|
|
# ANCESTOR, and a CanvasLayer is not one — so the chain breaks here and
|
|
# everything below fell back to Godot's default grey. It was not obvious,
|
|
# because the parts built with UITheme.title() carry their own overrides and
|
|
# looked correct next to a list and a button that did not.
|
|
bg.theme = UITheme.build()
|
|
add_child(bg)
|
|
|
|
# ── Main Menu ──
|
|
var main_center = CenterContainer.new()
|
|
main_center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
bg.add_child(main_center)
|
|
|
|
main_vbox = VBoxContainer.new()
|
|
main_vbox.add_theme_constant_override("separation", 20)
|
|
main_center.add_child(main_vbox)
|
|
|
|
var title = UITheme.title("PAUSED", 56)
|
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
main_vbox.add_child(title)
|
|
main_vbox.add_child(UITheme.divider(0.62))
|
|
|
|
resume_btn = Button.new()
|
|
resume_btn.text = "Resume"
|
|
resume_btn.custom_minimum_size = Vector2(260, 52)
|
|
main_vbox.add_child(resume_btn)
|
|
|
|
respawn_btn = Button.new()
|
|
respawn_btn.text = "Respawn"
|
|
respawn_btn.custom_minimum_size = Vector2(260, 52)
|
|
main_vbox.add_child(respawn_btn)
|
|
|
|
loadouts_btn = Button.new()
|
|
loadouts_btn.text = "Loadouts"
|
|
loadouts_btn.custom_minimum_size = Vector2(260, 52)
|
|
main_vbox.add_child(loadouts_btn)
|
|
|
|
settings_btn = Button.new()
|
|
settings_btn.text = "Settings"
|
|
settings_btn.custom_minimum_size = Vector2(260, 52)
|
|
main_vbox.add_child(settings_btn)
|
|
|
|
return_btn = Button.new()
|
|
return_btn.text = "Main Menu"
|
|
return_btn.custom_minimum_size = Vector2(260, 52)
|
|
main_vbox.add_child(return_btn)
|
|
|
|
exit_btn = Button.new()
|
|
exit_btn.text = "Exit Game"
|
|
exit_btn.custom_minimum_size = Vector2(260, 52)
|
|
main_vbox.add_child(exit_btn)
|
|
|
|
# ── Loadout Editor Root ──
|
|
loadout_editor = Control.new()
|
|
loadout_editor.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
bg.add_child(loadout_editor)
|
|
|
|
var screen_fill := ColorRect.new()
|
|
screen_fill.color = Color(0.025, 0.030, 0.060, 0.96)
|
|
screen_fill.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
loadout_editor.add_child(screen_fill)
|
|
|
|
var margin := MarginContainer.new()
|
|
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
for side in ["left", "right"]:
|
|
margin.add_theme_constant_override("margin_" + side, 42)
|
|
margin.add_theme_constant_override("margin_top", 30)
|
|
margin.add_theme_constant_override("margin_bottom", 30)
|
|
loadout_editor.add_child(margin)
|
|
|
|
var screen_vbox := VBoxContainer.new()
|
|
screen_vbox.add_theme_constant_override("separation", 22)
|
|
margin.add_child(screen_vbox)
|
|
|
|
var header := HBoxContainer.new()
|
|
screen_vbox.add_child(header)
|
|
header.add_child(UITheme.title("LOADOUTS", 54))
|
|
var header_spacer := Control.new()
|
|
header_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
header.add_child(header_spacer)
|
|
back_to_main_btn = Button.new()
|
|
back_to_main_btn.text = "Back"
|
|
back_to_main_btn.custom_minimum_size = Vector2(170, 46)
|
|
header.add_child(back_to_main_btn)
|
|
screen_vbox.add_child(UITheme.divider(0.82))
|
|
|
|
var editor_hbox := HBoxContainer.new()
|
|
editor_hbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
editor_hbox.add_theme_constant_override("separation", 22)
|
|
screen_vbox.add_child(editor_hbox)
|
|
|
|
var left_vbox := VBoxContainer.new()
|
|
left_vbox.custom_minimum_size = Vector2(380, 0)
|
|
left_vbox.add_theme_constant_override("separation", 12)
|
|
editor_hbox.add_child(left_vbox)
|
|
left_vbox.add_child(UITheme.heading("YOUR LOADOUTS", 24))
|
|
var loadout_scroll := ScrollContainer.new()
|
|
loadout_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
left_vbox.add_child(loadout_scroll)
|
|
loadout_list_vbox = VBoxContainer.new()
|
|
loadout_list_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
loadout_list_vbox.add_theme_constant_override("separation", 10)
|
|
loadout_scroll.add_child(loadout_list_vbox)
|
|
|
|
edit_panel = UITheme.card()
|
|
edit_panel.custom_minimum_size = Vector2(500, 0)
|
|
edit_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
editor_hbox.add_child(edit_panel)
|
|
|
|
var edit_margin := MarginContainer.new()
|
|
edit_margin.add_theme_constant_override("margin_left", 24)
|
|
edit_margin.add_theme_constant_override("margin_right", 24)
|
|
edit_margin.add_theme_constant_override("margin_top", 20)
|
|
edit_margin.add_theme_constant_override("margin_bottom", 20)
|
|
edit_panel.add_child(edit_margin)
|
|
var edit_vbox := VBoxContainer.new()
|
|
edit_vbox.add_theme_constant_override("separation", 12)
|
|
edit_margin.add_child(edit_vbox)
|
|
edit_title_label = UITheme.heading("EDIT LOADOUT", 28)
|
|
edit_vbox.add_child(edit_title_label)
|
|
edit_vbox.add_child(UITheme.divider(0.72))
|
|
edit_vbox.add_child(UITheme.caption("LOADOUT NAME", 16))
|
|
name_edit = LineEdit.new()
|
|
name_edit.placeholder_text = "Loadout Name"
|
|
name_edit.custom_minimum_size = Vector2(0, 44)
|
|
edit_vbox.add_child(name_edit)
|
|
|
|
var options_grid := GridContainer.new()
|
|
options_grid.columns = 2
|
|
options_grid.add_theme_constant_override("h_separation", 16)
|
|
options_grid.add_theme_constant_override("v_separation", 12)
|
|
edit_vbox.add_child(options_grid)
|
|
options_grid.add_child(_create_label("Character / Skin"))
|
|
skin_opt = OptionButton.new()
|
|
skin_opt.custom_minimum_size = Vector2(285, 42)
|
|
options_grid.add_child(skin_opt)
|
|
options_grid.add_child(_create_label("Primary 1"))
|
|
primary1_opt = OptionButton.new()
|
|
primary1_opt.custom_minimum_size = Vector2(285, 42)
|
|
options_grid.add_child(primary1_opt)
|
|
options_grid.add_child(_create_label("Primary 2"))
|
|
primary2_opt = OptionButton.new()
|
|
primary2_opt.custom_minimum_size = Vector2(285, 42)
|
|
options_grid.add_child(primary2_opt)
|
|
options_grid.add_child(_create_label("Special"))
|
|
special_opt = OptionButton.new()
|
|
special_opt.custom_minimum_size = Vector2(285, 42)
|
|
options_grid.add_child(special_opt)
|
|
options_grid.add_child(_create_label("Melee"))
|
|
melee_opt = OptionButton.new()
|
|
melee_opt.custom_minimum_size = Vector2(285, 42)
|
|
options_grid.add_child(melee_opt)
|
|
|
|
edit_vbox.add_child(UITheme.caption("SELECTED ITEMS", 16))
|
|
loadout_summary = Label.new()
|
|
loadout_summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
loadout_summary.custom_minimum_size = Vector2(0, 92)
|
|
loadout_summary.add_theme_font_size_override("font_size", 18)
|
|
edit_vbox.add_child(loadout_summary)
|
|
var edit_spacer := Control.new()
|
|
edit_spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
edit_vbox.add_child(edit_spacer)
|
|
save_loadout_btn = Button.new()
|
|
save_loadout_btn.text = "Save Loadout"
|
|
save_loadout_btn.custom_minimum_size = Vector2(0, 48)
|
|
edit_vbox.add_child(save_loadout_btn)
|
|
|
|
var preview_card := UITheme.card()
|
|
preview_card.custom_minimum_size = Vector2(440, 0)
|
|
editor_hbox.add_child(preview_card)
|
|
_build_loadout_character_preview(preview_card)
|
|
|
|
# ── Settings Editor Root ──
|
|
settings_editor = Control.new()
|
|
settings_editor.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
bg.add_child(settings_editor)
|
|
|
|
var settings_center = CenterContainer.new()
|
|
settings_center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
settings_editor.add_child(settings_center)
|
|
|
|
var settings_vbox = VBoxContainer.new()
|
|
settings_vbox.custom_minimum_size = Vector2(600, 400)
|
|
settings_center.add_child(settings_vbox)
|
|
|
|
settings_back_btn = Button.new()
|
|
settings_back_btn.text = "Back to Menu"
|
|
settings_vbox.add_child(settings_back_btn)
|
|
|
|
var tab_container = TabContainer.new()
|
|
tab_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
settings_vbox.add_child(tab_container)
|
|
|
|
# General Tab
|
|
var general_tab = VBoxContainer.new()
|
|
general_tab.name = "General"
|
|
tab_container.add_child(general_tab)
|
|
var user_hbox = HBoxContainer.new()
|
|
general_tab.add_child(user_hbox)
|
|
|
|
var user_lbl = Label.new()
|
|
user_lbl.text = "Username:"
|
|
user_lbl.custom_minimum_size = Vector2(150, 0)
|
|
user_hbox.add_child(user_lbl)
|
|
|
|
var user_input = LineEdit.new()
|
|
user_input.text = SettingsManager.username
|
|
user_input.custom_minimum_size = Vector2(200, 0)
|
|
user_hbox.add_child(user_input)
|
|
|
|
var user_save = Button.new()
|
|
user_save.text = "Save"
|
|
user_save.pressed.connect(func():
|
|
var final_name = user_input.text.strip_edges()
|
|
user_input.text = final_name
|
|
SettingsManager.username = final_name
|
|
SettingsManager._save_settings()
|
|
var nm = get_node_or_null("/root/NetworkManager")
|
|
if nm:
|
|
if nm.multiplayer.has_multiplayer_peer() and not nm.multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
|
|
nm.update_username.rpc_id(1, final_name)
|
|
else:
|
|
nm.update_username(final_name)
|
|
)
|
|
user_hbox.add_child(user_save)
|
|
|
|
var fov_hbox = HBoxContainer.new()
|
|
general_tab.add_child(fov_hbox)
|
|
|
|
var fov_lbl = Label.new()
|
|
fov_lbl.text = "World FOV"
|
|
fov_lbl.custom_minimum_size = Vector2(150, 0)
|
|
fov_hbox.add_child(fov_lbl)
|
|
|
|
_world_fov_slider = HSlider.new()
|
|
_world_fov_slider.min_value = 70.0
|
|
_world_fov_slider.max_value = 120.0
|
|
_world_fov_slider.step = 1.0
|
|
_world_fov_slider.value = SettingsManager.world_fov
|
|
_world_fov_slider.custom_minimum_size = Vector2(200, 0)
|
|
_world_fov_slider.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
|
fov_hbox.add_child(_world_fov_slider)
|
|
|
|
_world_fov_input = LineEdit.new()
|
|
_world_fov_input.text = str(round(SettingsManager.world_fov))
|
|
_world_fov_input.custom_minimum_size = Vector2(60, 0)
|
|
fov_hbox.add_child(_world_fov_input)
|
|
|
|
_world_fov_slider.value_changed.connect(func(value: float):
|
|
_world_fov_input.text = str(round(value))
|
|
SettingsManager.world_fov = value
|
|
SettingsManager._save_settings()
|
|
)
|
|
|
|
_world_fov_input.text_submitted.connect(func(new_text: String):
|
|
var val = new_text.to_float()
|
|
val = clampf(val, 70.0, 120.0)
|
|
_world_fov_slider.value = val
|
|
_world_fov_input.text = str(round(val))
|
|
SettingsManager.world_fov = val
|
|
SettingsManager._save_settings()
|
|
)
|
|
|
|
var spacer_gen = Control.new()
|
|
spacer_gen.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
general_tab.add_child(spacer_gen)
|
|
|
|
var reset_gen_btn = Button.new()
|
|
reset_gen_btn.text = "Reset to Defaults"
|
|
reset_gen_btn.pressed.connect(func():
|
|
_world_fov_slider.value = 90.0
|
|
SettingsManager.world_fov = 90.0
|
|
SettingsManager._save_settings()
|
|
)
|
|
general_tab.add_child(reset_gen_btn)
|
|
|
|
# Video Tab
|
|
var video_tab = VBoxContainer.new()
|
|
video_tab.name = "Video"
|
|
video_tab.add_theme_constant_override("separation", 10)
|
|
tab_container.add_child(video_tab)
|
|
|
|
var mode_hbox = HBoxContainer.new()
|
|
video_tab.add_child(mode_hbox)
|
|
var mode_lbl = Label.new()
|
|
mode_lbl.text = "Display Mode"
|
|
mode_lbl.custom_minimum_size = Vector2(150, 0)
|
|
mode_hbox.add_child(mode_lbl)
|
|
_display_mode_opt = OptionButton.new()
|
|
_display_mode_opt.add_item("Windowed", DisplayServer.WINDOW_MODE_WINDOWED)
|
|
_display_mode_opt.add_item("Borderless Fullscreen", DisplayServer.WINDOW_MODE_FULLSCREEN)
|
|
_display_mode_opt.add_item("Exclusive Fullscreen", DisplayServer.WINDOW_MODE_EXCLUSIVE_FULLSCREEN)
|
|
_display_mode_opt.item_selected.connect(func(index: int):
|
|
SettingsManager.display_mode = _display_mode_opt.get_item_id(index)
|
|
SettingsManager._save_settings()
|
|
SettingsManager.apply_video_settings()
|
|
)
|
|
mode_hbox.add_child(_display_mode_opt)
|
|
|
|
var res_hbox = HBoxContainer.new()
|
|
video_tab.add_child(res_hbox)
|
|
var res_lbl = Label.new()
|
|
res_lbl.text = "Resolution"
|
|
res_lbl.custom_minimum_size = Vector2(150, 0)
|
|
res_hbox.add_child(res_lbl)
|
|
_resolution_opt = OptionButton.new()
|
|
var res_names = ["640x480 (480p)", "1280x720 (720p)", "1920x1080 (1080p)", "2560x1080 (UW-1080p)", "2560x1440 (1440p)", "3440x1440 (UW-1440p)", "3840x2160 (4K)"]
|
|
for i in range(res_names.size()):
|
|
_resolution_opt.add_item(res_names[i], i)
|
|
_resolution_opt.item_selected.connect(func(index: int):
|
|
SettingsManager.resolution_index = index
|
|
SettingsManager._save_settings()
|
|
SettingsManager.apply_video_settings()
|
|
)
|
|
res_hbox.add_child(_resolution_opt)
|
|
|
|
var fps_hbox = HBoxContainer.new()
|
|
video_tab.add_child(fps_hbox)
|
|
var fps_lbl = Label.new()
|
|
fps_lbl.text = "FPS Cap"
|
|
fps_lbl.custom_minimum_size = Vector2(150, 0)
|
|
fps_hbox.add_child(fps_lbl)
|
|
_fps_slider = HSlider.new()
|
|
_fps_slider.min_value = 30.0
|
|
_fps_slider.max_value = 361.0
|
|
_fps_slider.step = 1.0
|
|
_fps_slider.custom_minimum_size = Vector2(200, 0)
|
|
_fps_slider.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
|
fps_hbox.add_child(_fps_slider)
|
|
_fps_input = LineEdit.new()
|
|
_fps_input.custom_minimum_size = Vector2(80, 0)
|
|
fps_hbox.add_child(_fps_input)
|
|
_fps_slider.value_changed.connect(func(value: float):
|
|
if value > 360.0:
|
|
_fps_input.text = "Uncapped"
|
|
SettingsManager.fps_cap = 0
|
|
else:
|
|
_fps_input.text = str(int(value))
|
|
SettingsManager.fps_cap = int(value)
|
|
SettingsManager._save_settings()
|
|
SettingsManager.apply_video_settings()
|
|
)
|
|
_fps_input.text_submitted.connect(func(new_text: String):
|
|
var val = new_text.to_int()
|
|
if val > 360 or val <= 0:
|
|
_fps_slider.value = 361.0
|
|
_fps_input.text = "Uncapped"
|
|
SettingsManager.fps_cap = 0
|
|
else:
|
|
val = max(30, val)
|
|
_fps_slider.value = float(val)
|
|
_fps_input.text = str(val)
|
|
SettingsManager.fps_cap = val
|
|
SettingsManager._save_settings()
|
|
SettingsManager.apply_video_settings()
|
|
)
|
|
|
|
var spacer_vid = Control.new()
|
|
spacer_vid.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
video_tab.add_child(spacer_vid)
|
|
var reset_vid_btn = Button.new()
|
|
reset_vid_btn.text = "Reset to Defaults"
|
|
reset_vid_btn.pressed.connect(func():
|
|
SettingsManager.display_mode = DisplayServer.WINDOW_MODE_WINDOWED
|
|
SettingsManager.resolution_index = 2
|
|
SettingsManager.fps_cap = 0
|
|
SettingsManager._save_settings()
|
|
SettingsManager.apply_video_settings()
|
|
_populate_video_ui()
|
|
)
|
|
video_tab.add_child(reset_vid_btn)
|
|
|
|
# Audio Tab
|
|
var audio_tab = VBoxContainer.new()
|
|
audio_tab.name = "Audio"
|
|
audio_tab.add_theme_constant_override("separation", 10)
|
|
tab_container.add_child(audio_tab)
|
|
|
|
# Master
|
|
var master_arr = _build_vol_control(audio_tab, "Master Volume")
|
|
_vol_master_slider = master_arr[0]
|
|
_vol_master_input = master_arr[1]
|
|
_vol_master_slider.value_changed.connect(func(v): _on_vol_changed("vol_master", v, _vol_master_input))
|
|
_vol_master_input.text_submitted.connect(func(t): _on_vol_text_changed("vol_master", t, _vol_master_slider, _vol_master_input))
|
|
|
|
# SFX
|
|
var sfx_arr = _build_vol_control(audio_tab, "Sound Effects")
|
|
_vol_sfx_slider = sfx_arr[0]
|
|
_vol_sfx_input = sfx_arr[1]
|
|
_vol_sfx_slider.value_changed.connect(func(v): _on_vol_changed("vol_sfx", v, _vol_sfx_input))
|
|
_vol_sfx_input.text_submitted.connect(func(t): _on_vol_text_changed("vol_sfx", t, _vol_sfx_slider, _vol_sfx_input))
|
|
|
|
# Music
|
|
var music_arr = _build_vol_control(audio_tab, "Music")
|
|
_vol_music_slider = music_arr[0]
|
|
_vol_music_input = music_arr[1]
|
|
_vol_music_slider.value_changed.connect(func(v): _on_vol_changed("vol_music", v, _vol_music_input))
|
|
_vol_music_input.text_submitted.connect(func(t): _on_vol_text_changed("vol_music", t, _vol_music_slider, _vol_music_input))
|
|
|
|
# Wind
|
|
var wind_arr = _build_vol_control(audio_tab, "Wind")
|
|
_vol_wind_slider = wind_arr[0]
|
|
_vol_wind_input = wind_arr[1]
|
|
_vol_wind_slider.value_changed.connect(func(v): _on_vol_changed("vol_wind", v, _vol_wind_input))
|
|
_vol_wind_input.text_submitted.connect(func(t): _on_vol_text_changed("vol_wind", t, _vol_wind_slider, _vol_wind_input))
|
|
|
|
var spacer_aud = Control.new()
|
|
spacer_aud.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
audio_tab.add_child(spacer_aud)
|
|
var reset_aud_btn = Button.new()
|
|
reset_aud_btn.text = "Reset to Defaults"
|
|
reset_aud_btn.pressed.connect(func():
|
|
SettingsManager.vol_master = 100.0
|
|
SettingsManager.vol_sfx = 100.0
|
|
SettingsManager.vol_music = 100.0
|
|
SettingsManager.vol_wind = 100.0
|
|
SettingsManager._save_settings()
|
|
SettingsManager.apply_audio_settings()
|
|
_populate_audio_ui()
|
|
)
|
|
audio_tab.add_child(reset_aud_btn)
|
|
|
|
# Mouse Tab
|
|
var mouse_tab = VBoxContainer.new()
|
|
mouse_tab.name = "Mouse"
|
|
mouse_tab.add_theme_constant_override("separation", 10)
|
|
tab_container.add_child(mouse_tab)
|
|
|
|
var sens_hbox = HBoxContainer.new()
|
|
mouse_tab.add_child(sens_hbox)
|
|
|
|
var sens_title = Label.new()
|
|
sens_title.text = "Sensitivity"
|
|
sens_title.custom_minimum_size = Vector2(150, 0)
|
|
sens_hbox.add_child(sens_title)
|
|
|
|
_mouse_sens_slider = HSlider.new()
|
|
_mouse_sens_slider.min_value = 1.0
|
|
_mouse_sens_slider.max_value = 100.0
|
|
_mouse_sens_slider.step = 1.0
|
|
_mouse_sens_slider.value = SettingsManager.mouse_sensitivity * 10000.0
|
|
_mouse_sens_slider.custom_minimum_size = Vector2(200, 0)
|
|
_mouse_sens_slider.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
|
sens_hbox.add_child(_mouse_sens_slider)
|
|
|
|
_mouse_sens_input = LineEdit.new()
|
|
_mouse_sens_input.text = str(round(SettingsManager.mouse_sensitivity * 10000.0))
|
|
_mouse_sens_input.custom_minimum_size = Vector2(60, 0)
|
|
sens_hbox.add_child(_mouse_sens_input)
|
|
|
|
_mouse_sens_slider.value_changed.connect(func(value: float):
|
|
_mouse_sens_input.text = str(round(value))
|
|
SettingsManager.mouse_sensitivity = value / 10000.0
|
|
SettingsManager._save_settings()
|
|
)
|
|
|
|
_mouse_sens_input.text_submitted.connect(func(new_text: String):
|
|
var val = new_text.to_float()
|
|
val = clampf(val, 1.0, 100.0)
|
|
_mouse_sens_slider.value = val
|
|
_mouse_sens_input.text = str(round(val))
|
|
SettingsManager.mouse_sensitivity = val / 10000.0
|
|
SettingsManager._save_settings()
|
|
)
|
|
|
|
var ads_sens_hbox = HBoxContainer.new()
|
|
mouse_tab.add_child(ads_sens_hbox)
|
|
|
|
var ads_sens_title = Label.new()
|
|
ads_sens_title.text = "ADS Sensitivity"
|
|
ads_sens_title.custom_minimum_size = Vector2(150, 0)
|
|
ads_sens_hbox.add_child(ads_sens_title)
|
|
|
|
_ads_sens_slider = HSlider.new()
|
|
_ads_sens_slider.min_value = 1.0
|
|
_ads_sens_slider.max_value = 100.0
|
|
_ads_sens_slider.step = 1.0
|
|
_ads_sens_slider.value = SettingsManager.ads_sensitivity * 10000.0
|
|
_ads_sens_slider.custom_minimum_size = Vector2(200, 0)
|
|
_ads_sens_slider.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
|
ads_sens_hbox.add_child(_ads_sens_slider)
|
|
|
|
_ads_sens_input = LineEdit.new()
|
|
_ads_sens_input.text = str(round(SettingsManager.ads_sensitivity * 10000.0))
|
|
_ads_sens_input.custom_minimum_size = Vector2(60, 0)
|
|
ads_sens_hbox.add_child(_ads_sens_input)
|
|
|
|
_ads_sens_slider.value_changed.connect(func(value: float):
|
|
_ads_sens_input.text = str(round(value))
|
|
SettingsManager.ads_sensitivity = value / 10000.0
|
|
SettingsManager._save_settings()
|
|
)
|
|
|
|
_ads_sens_input.text_submitted.connect(func(new_text: String):
|
|
var val = new_text.to_float()
|
|
val = clampf(val, 1.0, 100.0)
|
|
_ads_sens_slider.value = val
|
|
_ads_sens_input.text = str(round(val))
|
|
SettingsManager.ads_sensitivity = val / 10000.0
|
|
SettingsManager._save_settings()
|
|
)
|
|
|
|
var spacer_mouse = Control.new()
|
|
spacer_mouse.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
mouse_tab.add_child(spacer_mouse)
|
|
|
|
var reset_mouse_btn = Button.new()
|
|
reset_mouse_btn.text = "Reset to Defaults"
|
|
reset_mouse_btn.pressed.connect(func():
|
|
_mouse_sens_slider.value = 20.0
|
|
_ads_sens_slider.value = 10.0
|
|
SettingsManager.mouse_sensitivity = 0.002
|
|
SettingsManager.ads_sensitivity = 0.001
|
|
SettingsManager._save_settings()
|
|
)
|
|
mouse_tab.add_child(reset_mouse_btn)
|
|
|
|
# Keybindings Tab
|
|
var key_tab = VBoxContainer.new()
|
|
key_tab.name = "Keybindings"
|
|
tab_container.add_child(key_tab)
|
|
|
|
var scroll = ScrollContainer.new()
|
|
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
key_tab.add_child(scroll)
|
|
|
|
keybind_vbox = VBoxContainer.new()
|
|
keybind_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
scroll.add_child(keybind_vbox)
|
|
|
|
var reset_key_btn = Button.new()
|
|
reset_key_btn.text = "Reset to Defaults"
|
|
reset_key_btn.pressed.connect(func():
|
|
SettingsManager.reset_to_defaults()
|
|
_populate_keybindings()
|
|
)
|
|
key_tab.add_child(reset_key_btn)
|
|
|
|
# Advanced Tab
|
|
var advanced_tab = VBoxContainer.new()
|
|
advanced_tab.name = "Advanced"
|
|
advanced_tab.add_theme_constant_override("separation", 10)
|
|
tab_container.add_child(advanced_tab)
|
|
|
|
var debug_label = Label.new()
|
|
debug_label.text = "--- Debug ---"
|
|
debug_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
advanced_tab.add_child(debug_label)
|
|
|
|
_debug_ui_checkbox = CheckButton.new()
|
|
_debug_ui_checkbox.text = "Show Debug UI (Speed, State, Buffs)"
|
|
_debug_ui_checkbox.button_pressed = SettingsManager.show_debug_ui
|
|
_debug_ui_checkbox.toggled.connect(func(toggled_on: bool):
|
|
SettingsManager.show_debug_ui = toggled_on
|
|
SettingsManager._save_settings()
|
|
)
|
|
advanced_tab.add_child(_debug_ui_checkbox)
|
|
|
|
_show_fps_checkbox = CheckButton.new()
|
|
_show_fps_checkbox.text = "Show FPS Counter"
|
|
_show_fps_checkbox.button_pressed = SettingsManager.show_fps
|
|
_show_fps_checkbox.toggled.connect(func(toggled_on: bool):
|
|
SettingsManager.show_fps = toggled_on
|
|
SettingsManager._save_settings()
|
|
)
|
|
advanced_tab.add_child(_show_fps_checkbox)
|
|
|
|
_show_movement_speed_checkbox = CheckButton.new()
|
|
_show_movement_speed_checkbox.text = "Show Movement Speed Indicator"
|
|
_show_movement_speed_checkbox.button_pressed = SettingsManager.show_movement_speed
|
|
_show_movement_speed_checkbox.toggled.connect(func(toggled_on: bool):
|
|
SettingsManager.show_movement_speed = toggled_on
|
|
SettingsManager._save_settings()
|
|
)
|
|
advanced_tab.add_child(_show_movement_speed_checkbox)
|
|
|
|
var spacer_adv = Control.new()
|
|
spacer_adv.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
advanced_tab.add_child(spacer_adv)
|
|
|
|
var reset_adv_btn = Button.new()
|
|
reset_adv_btn.text = "Reset to Defaults"
|
|
reset_adv_btn.pressed.connect(func():
|
|
_debug_ui_checkbox.button_pressed = false
|
|
SettingsManager.show_debug_ui = false
|
|
_show_fps_checkbox.button_pressed = false
|
|
SettingsManager.show_fps = false
|
|
_show_movement_speed_checkbox.button_pressed = false
|
|
SettingsManager.show_movement_speed = false
|
|
SettingsManager._save_settings()
|
|
)
|
|
advanced_tab.add_child(reset_adv_btn)
|
|
|
|
# Rebind Overlay
|
|
rebind_overlay = ColorRect.new()
|
|
rebind_overlay.color = Color(0, 0, 0, 0.85)
|
|
rebind_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
rebind_overlay.visible = false
|
|
add_child(rebind_overlay)
|
|
|
|
var rebind_center = CenterContainer.new()
|
|
rebind_center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
rebind_overlay.add_child(rebind_center)
|
|
|
|
rebind_label = Label.new()
|
|
rebind_label.text = "Press any key..."
|
|
rebind_label.add_theme_font_size_override("font_size", 32)
|
|
rebind_center.add_child(rebind_label)
|
|
|
|
|
|
# ── Quit Dialog ──
|
|
quit_dialog = ConfirmationDialog.new()
|
|
quit_dialog.title = "Quit Game"
|
|
quit_dialog.dialog_text = "Are you sure you want to close the game?"
|
|
add_child(quit_dialog)
|
|
|
|
# ── Return Dialog ──
|
|
return_dialog = ConfirmationDialog.new()
|
|
return_dialog.title = "Main Menu"
|
|
return_dialog.dialog_text = "Are you sure you want to return to the main menu?"
|
|
add_child(return_dialog)
|
|
|
|
# ── Character picker ─────────────────────────────────────────────────────────
|
|
#
|
|
# The list on the left, the character themselves on the right, turning. A name
|
|
# in a dropdown is not a character selection screen — you pick a character by
|
|
# looking at them, which is the whole reason the models exist.
|
|
#
|
|
# The preview is a real SkinnedPlayerModel in its own world, not a rendered
|
|
# thumbnail, so it shows exactly what will spawn: the same cel look, the same
|
|
# per-class outlines, the same cloth and hair on springs. A thumbnail would go
|
|
# stale the first time a character was re-imported.
|
|
|
|
const PREVIEW_SIZE := Vector2i(390, 560)
|
|
|
|
|
|
func _build_loadout_character_preview(parent: PanelContainer) -> void:
|
|
var preview_margin := MarginContainer.new()
|
|
preview_margin.add_theme_constant_override("margin_left", 18)
|
|
preview_margin.add_theme_constant_override("margin_right", 18)
|
|
preview_margin.add_theme_constant_override("margin_top", 18)
|
|
preview_margin.add_theme_constant_override("margin_bottom", 18)
|
|
parent.add_child(preview_margin)
|
|
var column := VBoxContainer.new()
|
|
column.add_theme_constant_override("separation", 12)
|
|
preview_margin.add_child(column)
|
|
column.add_child(UITheme.heading("CHARACTER PREVIEW", 24))
|
|
character_desc = UITheme.caption("", 17)
|
|
character_desc.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
character_desc.custom_minimum_size = Vector2(0, 48)
|
|
column.add_child(character_desc)
|
|
|
|
var vp_container := SubViewportContainer.new()
|
|
vp_container.stretch = true
|
|
vp_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
vp_container.custom_minimum_size = Vector2(PREVIEW_SIZE)
|
|
column.add_child(vp_container)
|
|
_preview_viewport = SubViewport.new()
|
|
_preview_viewport.own_world_3d = true
|
|
_preview_viewport.size = PREVIEW_SIZE
|
|
_preview_viewport.msaa_3d = Viewport.MSAA_4X
|
|
_preview_viewport.transparent_bg = true
|
|
_preview_viewport.process_mode = Node.PROCESS_MODE_ALWAYS
|
|
vp_container.add_child(_preview_viewport)
|
|
|
|
var world := Node3D.new()
|
|
_preview_viewport.add_child(world)
|
|
var env := WorldEnvironment.new()
|
|
env.environment = LevelEnvironment.make_environment("sunset")
|
|
world.add_child(env)
|
|
var key := DirectionalLight3D.new()
|
|
key.rotation_degrees = Vector3(-28, 38, 0)
|
|
key.light_color = Color(1.0, 0.93, 0.84)
|
|
key.light_energy = 1.6
|
|
world.add_child(key)
|
|
var fill := DirectionalLight3D.new()
|
|
fill.rotation_degrees = Vector3(-16, -128, 0)
|
|
fill.light_color = Color(0.48, 0.62, 1.0)
|
|
fill.light_energy = 0.55
|
|
world.add_child(fill)
|
|
|
|
_preview_pivot = Node3D.new()
|
|
_preview_pivot.rotation_degrees.y = 180.0
|
|
world.add_child(_preview_pivot)
|
|
var camera := Camera3D.new()
|
|
camera.position = Vector3(0, 1.05, 2.65)
|
|
camera.rotation_degrees = Vector3(-4, 0, 0)
|
|
camera.fov = 38.0
|
|
world.add_child(camera)
|
|
|
|
## Build the turntable model for one skin. A colour-tint skin has no GLB, so the
|
|
## preview is emptied rather than left showing whoever was selected before it.
|
|
func _load_preview(id: String, skin) -> void:
|
|
if id == _preview_id:
|
|
return
|
|
_preview_id = id
|
|
if is_instance_valid(_preview_model):
|
|
_preview_model.queue_free()
|
|
_preview_model = null
|
|
# Every character gets shown from the front. Without this the turntable
|
|
# carries on from wherever the last one left it, so the third character you
|
|
# look at is side-on and the fourth has their back to you.
|
|
if is_instance_valid(_preview_pivot):
|
|
_preview_pivot.rotation_degrees.y = 180.0
|
|
if skin == null or skin.model_path == "" \
|
|
or not ResourceLoader.exists(skin.model_path):
|
|
return
|
|
var model := SkinnedPlayerModel.new()
|
|
model.model_path = skin.model_path
|
|
model.skin_id = id
|
|
model.mecha_theme = str(skin.get("mecha_theme"))
|
|
# Not first-person and not shadows-only: this is the one place the local
|
|
# player is supposed to see their own character in full.
|
|
model.first_person_mode = false
|
|
model.shadows_only = false
|
|
model.process_mode = Node.PROCESS_MODE_ALWAYS
|
|
_preview_pivot.add_child(model)
|
|
_preview_model = model
|
|
_refresh_preview_weapon()
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
# Turntable. Only while the picker is actually on screen — the rest of the
|
|
# time this node is an invisible autoload and has no business spending
|
|
# frames.
|
|
if loadout_editor and loadout_editor.visible and is_instance_valid(_preview_pivot):
|
|
_preview_pivot.rotate_y(delta * 0.45)
|
|
|
|
|
|
## The player this menu belongs to. Spawners differ between the built levels and
|
|
## the test harness, so both places are checked.
|
|
func _local_player() -> Node:
|
|
var pid := str(multiplayer.get_unique_id())
|
|
var scene := get_tree().current_scene
|
|
if scene == null:
|
|
return null
|
|
var found := scene.get_node_or_null(pid)
|
|
if found == null:
|
|
var spawner := scene.get_node_or_null("PlayerSpawner")
|
|
if spawner:
|
|
found = spawner.get_node_or_null(pid)
|
|
return found
|
|
|
|
|
|
func _build_vol_control(parent: Node, label_name: String) -> Array:
|
|
var hbox = HBoxContainer.new()
|
|
parent.add_child(hbox)
|
|
|
|
var lbl = Label.new()
|
|
lbl.text = label_name
|
|
lbl.custom_minimum_size = Vector2(150, 0)
|
|
hbox.add_child(lbl)
|
|
|
|
var slider = HSlider.new()
|
|
slider.min_value = 0.0
|
|
slider.max_value = 100.0
|
|
slider.step = 1.0
|
|
slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
slider.custom_minimum_size = Vector2(150, 0)
|
|
hbox.add_child(slider)
|
|
|
|
var input = LineEdit.new()
|
|
input.custom_minimum_size = Vector2(60, 0)
|
|
hbox.add_child(input)
|
|
|
|
return [slider, input]
|
|
|
|
func _on_vol_changed(setting_name: String, value: float, input_node: LineEdit) -> void:
|
|
SettingsManager.set(setting_name, value)
|
|
input_node.text = str(value)
|
|
SettingsManager._save_settings()
|
|
SettingsManager.apply_audio_settings()
|
|
|
|
func _on_vol_text_changed(setting_name: String, text: String, slider_node: HSlider, input_node: LineEdit) -> void:
|
|
var val = clampf(text.to_float(), 0.0, 100.0)
|
|
SettingsManager.set(setting_name, val)
|
|
slider_node.value = val
|
|
input_node.text = str(val)
|
|
SettingsManager._save_settings()
|
|
SettingsManager.apply_audio_settings()
|
|
|
|
func _create_label(text: String) -> Label:
|
|
var l = Label.new()
|
|
l.text = text
|
|
return l
|
|
|
|
func _connect_signals() -> void:
|
|
resume_btn.pressed.connect(_resume)
|
|
respawn_btn.pressed.connect(_respawn)
|
|
loadouts_btn.pressed.connect(_show_loadouts)
|
|
settings_btn.pressed.connect(_show_settings)
|
|
return_btn.pressed.connect(_show_return_dialog)
|
|
exit_btn.pressed.connect(_show_exit_dialog)
|
|
back_to_main_btn.pressed.connect(_show_main_menu)
|
|
settings_back_btn.pressed.connect(_show_main_menu)
|
|
save_loadout_btn.pressed.connect(_save_current_loadout)
|
|
skin_opt.item_selected.connect(_on_skin_option_selected)
|
|
for option in [primary1_opt, primary2_opt, special_opt, melee_opt]:
|
|
option.item_selected.connect(func(_index: int): _refresh_loadout_summary())
|
|
quit_dialog.confirmed.connect(_quit_game)
|
|
return_dialog.confirmed.connect(_return_to_main_menu)
|
|
|
|
func _show_return_dialog() -> void:
|
|
return_dialog.popup_centered()
|
|
|
|
func _return_to_main_menu() -> void:
|
|
_resume() # Reset pause state and cursor mode
|
|
var nm = get_node_or_null("/root/NetworkManager")
|
|
if nm:
|
|
nm.disconnect_game()
|
|
get_tree().change_scene_to_file("res://ui/main_menu/main_menu.tscn")
|
|
|
|
func _show_main_menu() -> void:
|
|
if get_tree().current_scene and get_tree().current_scene.name == "MainMenu":
|
|
visible = false
|
|
return
|
|
|
|
main_vbox.visible = true
|
|
loadout_editor.visible = false
|
|
settings_editor.visible = false
|
|
|
|
func _show_loadouts() -> void:
|
|
main_vbox.visible = false
|
|
settings_editor.visible = false
|
|
loadout_editor.visible = true
|
|
_populate_loadout_list()
|
|
var index := clampi(LoadoutManager.active_loadout_index, 0, LoadoutManager.loadouts.size() - 1)
|
|
_edit_loadout(index)
|
|
|
|
func _populate_video_ui() -> void:
|
|
if _display_mode_opt:
|
|
for i in range(_display_mode_opt.item_count):
|
|
if _display_mode_opt.get_item_id(i) == SettingsManager.display_mode:
|
|
_display_mode_opt.select(i)
|
|
break
|
|
if _resolution_opt:
|
|
_resolution_opt.select(SettingsManager.resolution_index)
|
|
if _fps_slider and _fps_input:
|
|
if SettingsManager.fps_cap == 0:
|
|
_fps_slider.value = 361.0
|
|
_fps_input.text = "Uncapped"
|
|
else:
|
|
_fps_slider.value = float(SettingsManager.fps_cap)
|
|
_fps_input.text = str(SettingsManager.fps_cap)
|
|
|
|
func _populate_audio_ui() -> void:
|
|
if _vol_master_slider: _vol_master_slider.value = SettingsManager.vol_master
|
|
if _vol_master_input: _vol_master_input.text = str(SettingsManager.vol_master)
|
|
if _vol_sfx_slider: _vol_sfx_slider.value = SettingsManager.vol_sfx
|
|
if _vol_sfx_input: _vol_sfx_input.text = str(SettingsManager.vol_sfx)
|
|
if _vol_music_slider: _vol_music_slider.value = SettingsManager.vol_music
|
|
if _vol_music_input: _vol_music_input.text = str(SettingsManager.vol_music)
|
|
if _vol_wind_slider: _vol_wind_slider.value = SettingsManager.vol_wind
|
|
if _vol_wind_input: _vol_wind_input.text = str(SettingsManager.vol_wind)
|
|
|
|
func _show_settings() -> void:
|
|
main_vbox.visible = false
|
|
loadout_editor.visible = false
|
|
settings_editor.visible = true
|
|
_populate_keybindings()
|
|
_populate_video_ui()
|
|
_populate_audio_ui()
|
|
if _debug_ui_checkbox:
|
|
_debug_ui_checkbox.button_pressed = SettingsManager.show_debug_ui
|
|
if _show_fps_checkbox:
|
|
_show_fps_checkbox.button_pressed = SettingsManager.show_fps
|
|
if _show_movement_speed_checkbox:
|
|
_show_movement_speed_checkbox.button_pressed = SettingsManager.show_movement_speed
|
|
if _world_fov_slider:
|
|
_world_fov_slider.value = SettingsManager.world_fov
|
|
_world_fov_input.text = str(round(SettingsManager.world_fov))
|
|
if _mouse_sens_slider:
|
|
var display_val = round(SettingsManager.mouse_sensitivity * 10000.0)
|
|
_mouse_sens_slider.value = display_val
|
|
_mouse_sens_input.text = str(display_val)
|
|
|
|
var ads_display_val = round(SettingsManager.ads_sensitivity * 10000.0)
|
|
_ads_sens_slider.value = ads_display_val
|
|
_ads_sens_input.text = str(ads_display_val)
|
|
_mouse_sens_slider.value = display_val
|
|
if _mouse_sens_input:
|
|
_mouse_sens_input.text = str(display_val)
|
|
|
|
func _populate_keybindings() -> void:
|
|
for c in keybind_vbox.get_children():
|
|
c.queue_free()
|
|
|
|
var actions = SettingsManager.default_bindings.keys()
|
|
for action in actions:
|
|
var hbox = HBoxContainer.new()
|
|
var lbl = Label.new()
|
|
lbl.text = action.capitalize()
|
|
lbl.custom_minimum_size = Vector2(200, 0)
|
|
hbox.add_child(lbl)
|
|
|
|
var btn = Button.new()
|
|
btn.text = _get_key_name(action)
|
|
btn.custom_minimum_size = Vector2(150, 0)
|
|
btn.pressed.connect(func(): _start_rebind(action))
|
|
hbox.add_child(btn)
|
|
|
|
keybind_vbox.add_child(hbox)
|
|
|
|
func _start_rebind(action: String) -> void:
|
|
waiting_for_input_action = action
|
|
rebind_label.text = "Press any key for: " + action.capitalize()
|
|
rebind_overlay.visible = true
|
|
|
|
func _get_key_name(action: String) -> String:
|
|
if not InputMap.has_action(action):
|
|
return "?"
|
|
var events = InputMap.action_get_events(action)
|
|
for e in events:
|
|
if e is InputEventKey:
|
|
var code = e.physical_keycode if e.physical_keycode != 0 else e.keycode
|
|
return OS.get_keycode_string(code)
|
|
elif e is InputEventMouseButton:
|
|
if e.button_index == MOUSE_BUTTON_LEFT: return "LClick"
|
|
elif e.button_index == MOUSE_BUTTON_RIGHT: return "RClick"
|
|
elif e.button_index == MOUSE_BUTTON_MIDDLE: return "MClick"
|
|
elif e.button_index == MOUSE_BUTTON_WHEEL_UP: return "MWheel Up"
|
|
elif e.button_index == MOUSE_BUTTON_WHEEL_DOWN: return "MWheel Down"
|
|
return "?"
|
|
|
|
func _populate_loadout_list() -> void:
|
|
for c in loadout_list_vbox.get_children():
|
|
c.queue_free()
|
|
|
|
for i in range(LoadoutManager.loadouts.size()):
|
|
var loadout: Dictionary = LoadoutManager.loadouts[i]
|
|
var loadout_index := i
|
|
var is_active := loadout_index == LoadoutManager.active_loadout_index
|
|
var card := UITheme.card()
|
|
card.custom_minimum_size = Vector2(350, 138)
|
|
loadout_list_vbox.add_child(card)
|
|
|
|
var margin := MarginContainer.new()
|
|
for side in ["left", "right", "top", "bottom"]:
|
|
margin.add_theme_constant_override("margin_" + side, 14)
|
|
card.add_child(margin)
|
|
var content := VBoxContainer.new()
|
|
content.add_theme_constant_override("separation", 7)
|
|
margin.add_child(content)
|
|
|
|
var heading := UITheme.heading(
|
|
str(loadout.get("name", "Loadout")) + (" • ACTIVE" if is_active else ""), 21
|
|
)
|
|
content.add_child(heading)
|
|
var details := Label.new()
|
|
details.text = "%s\n%s · %s\n%s · %s" % [
|
|
_skin_name(str(loadout.get("skin", "default"))),
|
|
_weapon_name(str(loadout.get("primary_1", "none"))),
|
|
_weapon_name(str(loadout.get("primary_2", "none"))),
|
|
_weapon_name(str(loadout.get("special", "none"))),
|
|
_weapon_name(str(loadout.get("melee", "none"))),
|
|
]
|
|
details.modulate = Color(0.82, 0.86, 0.94)
|
|
content.add_child(details)
|
|
|
|
var actions := HBoxContainer.new()
|
|
actions.add_theme_constant_override("separation", 8)
|
|
content.add_child(actions)
|
|
var edit_btn := Button.new()
|
|
edit_btn.text = "Edit"
|
|
edit_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
edit_btn.pressed.connect(func(): _edit_loadout(loadout_index))
|
|
actions.add_child(edit_btn)
|
|
var equip_btn := Button.new()
|
|
equip_btn.text = "Equip"
|
|
equip_btn.disabled = is_active
|
|
equip_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
equip_btn.pressed.connect(func(): _equip_loadout(loadout_index))
|
|
actions.add_child(equip_btn)
|
|
|
|
func _equip_loadout(idx: int) -> void:
|
|
if not LoadoutManager.set_active_loadout(idx):
|
|
return
|
|
_apply_active_loadout_skin()
|
|
_populate_loadout_list()
|
|
_edit_loadout(idx)
|
|
|
|
func _edit_loadout(idx: int) -> void:
|
|
if idx < 0 or idx >= LoadoutManager.loadouts.size():
|
|
return
|
|
editing_index = idx
|
|
var loadout: Dictionary = LoadoutManager.loadouts[idx]
|
|
name_edit.text = str(loadout.get("name", "Loadout"))
|
|
edit_title_label.text = "EDIT " + name_edit.text.to_upper()
|
|
_populate_skin_options(str(loadout.get("skin", "default")))
|
|
_populate_options(primary1_opt, "primary", str(loadout.get("primary_1", "none")))
|
|
_populate_options(primary2_opt, "primary", str(loadout.get("primary_2", "none")))
|
|
_populate_options(special_opt, "special", str(loadout.get("special", "none")))
|
|
_populate_options(melee_opt, "melee", str(loadout.get("melee", "none")))
|
|
edit_panel.visible = true
|
|
_on_skin_option_selected(skin_opt.selected)
|
|
_refresh_loadout_summary()
|
|
|
|
func _populate_skin_options(current_id: String) -> void:
|
|
skin_opt.clear()
|
|
var skin_ids: Array = SkinManager.get_skin_ids()
|
|
skin_ids.sort()
|
|
var selected_index := 0
|
|
for id in skin_ids:
|
|
var skin = SkinManager.get_skin(id)
|
|
if skin == null or not skin.is_unlocked:
|
|
continue
|
|
var item_index := skin_opt.item_count
|
|
skin_opt.add_item(skin.skin_name)
|
|
skin_opt.set_item_metadata(item_index, id)
|
|
if id == current_id:
|
|
selected_index = item_index
|
|
if skin_opt.item_count > 0:
|
|
skin_opt.select(selected_index)
|
|
|
|
func _on_skin_option_selected(index: int) -> void:
|
|
if index < 0 or index >= skin_opt.item_count:
|
|
return
|
|
var skin_id := str(skin_opt.get_item_metadata(index))
|
|
var skin = SkinManager.get_skin(skin_id)
|
|
character_desc.text = skin.description if skin.description != "" else skin.skin_name
|
|
_load_preview(skin_id, skin)
|
|
_refresh_loadout_summary()
|
|
|
|
func _selected_metadata(option: OptionButton) -> String:
|
|
if option == null or option.selected < 0 or option.selected >= option.item_count:
|
|
return "none"
|
|
return str(option.get_item_metadata(option.selected))
|
|
|
|
func _weapon_name(id: String) -> String:
|
|
var weapon: Dictionary = LoadoutManager.weapon_db.get(id, LoadoutManager.weapon_db["none"])
|
|
return str(weapon.get("name", id))
|
|
|
|
func _skin_name(id: String) -> String:
|
|
var skin = SkinManager.get_skin(id)
|
|
return skin.skin_name if skin != null else id
|
|
|
|
func _refresh_loadout_summary() -> void:
|
|
if loadout_summary == null:
|
|
return
|
|
loadout_summary.text = "CHARACTER %s\nPRIMARY %s · %s\nSPECIAL %s\nMELEE %s" % [
|
|
_skin_name(_selected_metadata(skin_opt)),
|
|
_weapon_name(_selected_metadata(primary1_opt)),
|
|
_weapon_name(_selected_metadata(primary2_opt)),
|
|
_weapon_name(_selected_metadata(special_opt)),
|
|
_weapon_name(_selected_metadata(melee_opt)),
|
|
]
|
|
_refresh_preview_weapon()
|
|
|
|
func _refresh_preview_weapon() -> void:
|
|
if not is_instance_valid(_preview_model):
|
|
return
|
|
var weapon_id := _selected_metadata(primary1_opt)
|
|
if weapon_id == "none":
|
|
weapon_id = _selected_metadata(primary2_opt)
|
|
var weapon: Dictionary = LoadoutManager.weapon_db.get(weapon_id, LoadoutManager.weapon_db["none"])
|
|
_preview_model.set_weapon(str(weapon.get("script", "")))
|
|
|
|
func _apply_active_loadout_skin() -> void:
|
|
var skin_id := LoadoutManager.get_active_skin_id()
|
|
SkinManager.set_active_skin(skin_id)
|
|
var player := _local_player()
|
|
if player and player.has_method("set_skin"):
|
|
player.set_skin(skin_id)
|
|
|
|
func _populate_options(opt: OptionButton, category: String, current_id: String) -> void:
|
|
opt.clear()
|
|
var idx = 0
|
|
var select_idx = 0
|
|
|
|
opt.add_item("None", 0)
|
|
opt.set_item_metadata(0, "none")
|
|
if current_id == "none": select_idx = 0
|
|
idx += 1
|
|
|
|
for key in LoadoutManager.weapon_db:
|
|
if key == "none": continue
|
|
var w = LoadoutManager.weapon_db[key]
|
|
if w["category"] == category:
|
|
opt.add_item(w["name"], idx)
|
|
opt.set_item_metadata(idx, key)
|
|
if key == current_id:
|
|
select_idx = idx
|
|
idx += 1
|
|
|
|
opt.select(select_idx)
|
|
|
|
func _save_current_loadout() -> void:
|
|
if editing_index >= 0 and editing_index < LoadoutManager.loadouts.size():
|
|
var l: Dictionary = LoadoutManager.loadouts[editing_index]
|
|
if name_edit.text.strip_edges() != "":
|
|
l["name"] = name_edit.text.strip_edges()
|
|
name_edit.text = l["name"]
|
|
l["skin"] = _selected_metadata(skin_opt)
|
|
l["primary_1"] = _selected_metadata(primary1_opt)
|
|
l["primary_2"] = _selected_metadata(primary2_opt)
|
|
l["special"] = _selected_metadata(special_opt)
|
|
l["melee"] = _selected_metadata(melee_opt)
|
|
LoadoutManager.save_loadouts()
|
|
if editing_index == LoadoutManager.active_loadout_index:
|
|
_apply_active_loadout_skin()
|
|
_populate_loadout_list()
|
|
edit_title_label.text = "EDIT " + str(l["name"]).to_upper()
|
|
_refresh_loadout_summary()
|
|
|
|
func _show_exit_dialog() -> void:
|
|
quit_dialog.popup_centered()
|
|
|
|
func _quit_game() -> void:
|
|
get_tree().quit()
|