This commit is contained in:
Nicholas Butzke
2026-08-09 01:31:44 -04:00
parent 922983429e
commit 9358746582
43 changed files with 4324 additions and 481 deletions
+274 -215
View File
@@ -4,17 +4,13 @@ var bg: ColorRect
var main_vbox: VBoxContainer
var resume_btn: Button
var respawn_btn: Button
var character_btn: Button
var loadouts_btn: Button
var settings_btn: Button
var return_btn: Button
var exit_btn: Button
# Character picker
var character_editor: Control
var character_list: ItemList
# Character preview integrated into the loadout screen.
var character_desc: Label
var character_back_btn: Button
var _preview_viewport: SubViewport
var _preview_model: SkinnedPlayerModel
var _preview_pivot: Node3D
@@ -29,6 +25,9 @@ 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
@@ -164,11 +163,6 @@ func _build_ui() -> void:
respawn_btn.custom_minimum_size = Vector2(260, 52)
main_vbox.add_child(respawn_btn)
character_btn = Button.new()
character_btn.text = "Character"
character_btn.custom_minimum_size = Vector2(260, 52)
main_vbox.add_child(character_btn)
loadouts_btn = Button.new()
loadouts_btn.text = "Loadouts"
loadouts_btn.custom_minimum_size = Vector2(260, 52)
@@ -189,76 +183,125 @@ func _build_ui() -> void:
exit_btn.custom_minimum_size = Vector2(260, 52)
main_vbox.add_child(exit_btn)
_build_character_editor()
# ── Loadout Editor Root ──
loadout_editor = Control.new()
loadout_editor.set_anchors_preset(Control.PRESET_FULL_RECT)
bg.add_child(loadout_editor)
var editor_center = CenterContainer.new()
editor_center.set_anchors_preset(Control.PRESET_FULL_RECT)
loadout_editor.add_child(editor_center)
var editor_hbox = HBoxContainer.new()
editor_hbox.add_theme_constant_override("separation", 40)
editor_center.add_child(editor_hbox)
var left_vbox = VBoxContainer.new()
editor_hbox.add_child(left_vbox)
var list_title = Label.new()
list_title.text = "Your Loadouts"
list_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
left_vbox.add_child(list_title)
loadout_list_vbox = VBoxContainer.new()
left_vbox.add_child(loadout_list_vbox)
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 to Menu"
back_to_main_btn.custom_minimum_size = Vector2(0, 40)
left_vbox.add_child(back_to_main_btn)
edit_panel = PanelContainer.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_vbox = VBoxContainer.new()
edit_vbox.add_theme_constant_override("separation", 10)
edit_panel.add_child(edit_vbox)
var edit_title = Label.new()
edit_title.text = "Edit Loadout"
edit_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
edit_vbox.add_child(edit_title)
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(250, 0)
name_edit.custom_minimum_size = Vector2(0, 44)
edit_vbox.add_child(name_edit)
edit_vbox.add_child(_create_label("Primary 1:"))
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()
edit_vbox.add_child(primary1_opt)
edit_vbox.add_child(_create_label("Primary 2:"))
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()
edit_vbox.add_child(primary2_opt)
edit_vbox.add_child(_create_label("Special:"))
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()
edit_vbox.add_child(special_opt)
var melee_lbl = Label.new()
melee_lbl.text = "Melee"
edit_vbox.add_child(melee_lbl)
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()
edit_vbox.add_child(melee_opt)
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, 40)
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()
@@ -721,150 +764,63 @@ func _build_ui() -> void:
# 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(520, 640)
const PREVIEW_SIZE := Vector2i(390, 560)
func _build_character_editor() -> void:
character_editor = Control.new()
character_editor.set_anchors_preset(Control.PRESET_FULL_RECT)
character_editor.visible = false
bg.add_child(character_editor)
var center = CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
character_editor.add_child(center)
var row = HBoxContainer.new()
row.add_theme_constant_override("separation", 34)
center.add_child(row)
# Left: the roster
var left = VBoxContainer.new()
left.add_theme_constant_override("separation", 14)
left.custom_minimum_size = Vector2(340, 0)
row.add_child(left)
var heading := UITheme.title("CHARACTER", 52)
left.add_child(heading)
left.add_child(UITheme.divider(0.28))
character_list = ItemList.new()
character_list.custom_minimum_size = Vector2(340, 380)
character_list.auto_height = false
left.add_child(character_list)
character_desc = UITheme.heading("", 20)
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(340, 60)
left.add_child(character_desc)
character_desc.custom_minimum_size = Vector2(0, 48)
column.add_child(character_desc)
character_back_btn = Button.new()
character_back_btn.text = "Back"
character_back_btn.custom_minimum_size = Vector2(0, 46)
left.add_child(character_back_btn)
# Right: the character, in a real 3D viewport
var frame := UITheme.card()
row.add_child(frame)
var vp_container = SubViewportContainer.new()
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)
frame.add_child(vp_container)
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
# The tree is paused while this menu is up. Without this the preview would
# be a still frame: no turntable, and — worse — no spring solver, so the
# character's hair and skirt would hang in their bind pose.
_preview_viewport.process_mode = Node.PROCESS_MODE_ALWAYS
vp_container.add_child(_preview_viewport)
var world = Node3D.new()
var world := Node3D.new()
_preview_viewport.add_child(world)
var env = WorldEnvironment.new()
var env := WorldEnvironment.new()
env.environment = LevelEnvironment.make_environment("sunset")
world.add_child(env)
# Three-point-ish lighting, warm key and cool fill, which is what makes a
# cel-shaded character read as rounded instead of as a sticker.
var key = DirectionalLight3D.new()
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()
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()
# Turned to face the camera. SkinnedPlayerModel yaws itself 180° on load
# because glTF forward is +Z and players face -Z, which is right in a level
# and means a preview camera sitting on +Z gets the back of the character's
# head. The turntable starts from here.
_preview_pivot.rotation_degrees.y = 180.0
world.add_child(_preview_pivot)
var camera = Camera3D.new()
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)
func _show_character() -> void:
main_vbox.visible = false
loadout_editor.visible = false
settings_editor.visible = false
character_editor.visible = true
_populate_character_list()
func _populate_character_list() -> void:
character_list.clear()
var active: String = SkinManager.active_skin_id
var selected := -1
for id in SkinManager.get_skin_ids():
var skin = SkinManager.get_skin(id)
if not skin.is_unlocked:
continue
var idx := character_list.add_item(skin.skin_name)
character_list.set_item_metadata(idx, id)
if id == active:
selected = idx
if selected >= 0:
character_list.select(selected)
_on_character_selected(selected)
elif character_list.item_count > 0:
character_list.select(0)
_on_character_selected(0)
## Selecting a character equips it immediately — there is no separate confirm.
##
## A picker that needs a second click to take effect makes you compare a preview
## against a memory of the last one. Applying on selection means the character
## behind the menu changes as you arrow through the list, which IS the
## comparison. Nothing here is destructive, so there is nothing to confirm.
func _on_character_selected(index: int) -> void:
if index < 0 or index >= character_list.item_count:
return
var id: String = character_list.get_item_metadata(index)
var skin = SkinManager.get_skin(id)
character_desc.text = skin.description if skin.description != "" else skin.skin_name
_load_preview(id, skin)
SkinManager.set_active_skin(id)
var player := _local_player()
if player and player.has_method("set_skin"):
player.set_skin(id)
## 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:
@@ -885,6 +841,7 @@ func _load_preview(id: String, skin) -> void:
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
@@ -892,13 +849,14 @@ func _load_preview(id: String, skin) -> void:
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 character_editor and character_editor.visible and is_instance_valid(_preview_pivot):
if loadout_editor and loadout_editor.visible and is_instance_valid(_preview_pivot):
_preview_pivot.rotate_y(delta * 0.45)
@@ -962,9 +920,6 @@ func _create_label(text: String) -> Label:
func _connect_signals() -> void:
resume_btn.pressed.connect(_resume)
respawn_btn.pressed.connect(_respawn)
character_btn.pressed.connect(_show_character)
character_back_btn.pressed.connect(_show_main_menu)
character_list.item_selected.connect(_on_character_selected)
loadouts_btn.pressed.connect(_show_loadouts)
settings_btn.pressed.connect(_show_settings)
return_btn.pressed.connect(_show_return_dialog)
@@ -972,6 +927,9 @@ func _connect_signals() -> void:
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)
@@ -993,15 +951,14 @@ func _show_main_menu() -> void:
main_vbox.visible = true
loadout_editor.visible = false
settings_editor.visible = false
character_editor.visible = false
func _show_loadouts() -> void:
main_vbox.visible = false
settings_editor.visible = false
character_editor.visible = false
loadout_editor.visible = true
edit_panel.visible = false
_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:
@@ -1032,7 +989,6 @@ func _populate_audio_ui() -> void:
func _show_settings() -> void:
main_vbox.visible = false
loadout_editor.visible = false
character_editor.visible = false
settings_editor.visible = true
_populate_keybindings()
_populate_video_ui()
@@ -1102,43 +1058,142 @@ func _get_key_name(action: String) -> String:
func _populate_loadout_list() -> void:
for c in loadout_list_vbox.get_children():
c.queue_free()
for i in range(LoadoutManager.loadouts.size()):
var l = LoadoutManager.loadouts[i]
var hbox = HBoxContainer.new()
var btn = Button.new()
var is_active = (i == LoadoutManager.active_loadout_index)
btn.text = l["name"] + (" (Active)" if is_active else "")
btn.custom_minimum_size = Vector2(200, 40)
btn.pressed.connect(func(): _edit_loadout(i))
hbox.add_child(btn)
var equip_btn = Button.new()
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.pressed.connect(func(): _equip_loadout(i))
hbox.add_child(equip_btn)
loadout_list_vbox.add_child(hbox)
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:
LoadoutManager.active_loadout_index = idx
LoadoutManager.save_loadouts()
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 l = LoadoutManager.loadouts[idx]
name_edit.text = l["name"]
_populate_options(primary1_opt, "primary", l["primary_1"])
_populate_options(primary2_opt, "primary", l["primary_2"])
_populate_options(special_opt, "special", l["special"])
if l.has("melee"):
_populate_options(melee_opt, "melee", l["melee"])
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()
@@ -1164,17 +1219,21 @@ func _populate_options(opt: OptionButton, category: String, current_id: String)
func _save_current_loadout() -> void:
if editing_index >= 0 and editing_index < LoadoutManager.loadouts.size():
var l = LoadoutManager.loadouts[editing_index]
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["primary_1"] = primary1_opt.get_item_metadata(primary1_opt.get_selected_id())
l["primary_2"] = primary2_opt.get_item_metadata(primary2_opt.get_selected_id())
l["special"] = special_opt.get_item_metadata(special_opt.get_selected_id())
l["melee"] = melee_opt.get_item_metadata(melee_opt.get_selected_id())
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_panel.visible = false
edit_title_label.text = "EDIT " + str(l["name"]).to_upper()
_refresh_loadout_summary()
func _show_exit_dialog() -> void:
quit_dialog.popup_centered()