feat: implement loadout management system, interactive pause menu, and weapon slot switching functionality
This commit is contained in:
+12
-10
@@ -74,13 +74,16 @@ func _build_input_map() -> void:
|
|||||||
"move_right": [KEY_D],
|
"move_right": [KEY_D],
|
||||||
"weapon_next": [MOUSE_BUTTON_WHEEL_UP],
|
"weapon_next": [MOUSE_BUTTON_WHEEL_UP],
|
||||||
"weapon_prev": [MOUSE_BUTTON_WHEEL_DOWN],
|
"weapon_prev": [MOUSE_BUTTON_WHEEL_DOWN],
|
||||||
|
"weapon_1": [KEY_1],
|
||||||
|
"weapon_2": [KEY_2],
|
||||||
|
"weapon_3": [KEY_3],
|
||||||
}
|
}
|
||||||
for action in bindings:
|
for action in bindings:
|
||||||
if not InputMap.has_action(action):
|
if not InputMap.has_action(action):
|
||||||
InputMap.add_action(action)
|
InputMap.add_action(action)
|
||||||
InputMap.action_erase_events(action)
|
InputMap.action_erase_events(action)
|
||||||
for code in bindings[action]:
|
for code in bindings[action]:
|
||||||
if code in [KEY_SPACE, KEY_SHIFT, KEY_CTRL, KEY_R, KEY_F, KEY_E, KEY_W, KEY_S, KEY_A, KEY_D]:
|
if code in [KEY_SPACE, KEY_SHIFT, KEY_CTRL, KEY_R, KEY_F, KEY_E, KEY_W, KEY_S, KEY_A, KEY_D, KEY_1, KEY_2, KEY_3]:
|
||||||
var ev := InputEventKey.new()
|
var ev := InputEventKey.new()
|
||||||
ev.keycode = code
|
ev.keycode = code
|
||||||
InputMap.action_add_event(action, ev)
|
InputMap.action_add_event(action, ev)
|
||||||
@@ -318,15 +321,14 @@ func _build_player() -> void:
|
|||||||
camera.fov = 90.0
|
camera.fov = 90.0
|
||||||
head_pivot.add_child(camera)
|
head_pivot.add_child(camera)
|
||||||
|
|
||||||
# ── Temporary Weapon Setup ──────────────────────────────────────────────────
|
# ── Weapon Setup ────────────────────────────────────────────────────────────
|
||||||
# TODO: Change how the player is given weapons in the future to a loadout schema.
|
var wmanager_script = preload("res://weapons/weapon_manager.gd")
|
||||||
var shotgun_script = preload("res://weapons/double_barrel_shotgun.gd")
|
if wmanager_script:
|
||||||
if shotgun_script:
|
var wman = wmanager_script.new()
|
||||||
var shotgun = shotgun_script.new()
|
wman.name = "WeaponManager"
|
||||||
shotgun.name = "DoubleBarrelShotgun"
|
wman.player = player
|
||||||
shotgun.player = player
|
wman.camera = camera
|
||||||
shotgun.camera = camera
|
camera.add_child(wman)
|
||||||
camera.add_child(shotgun)
|
|
||||||
|
|
||||||
var rig_script = preload("res://characters/player/fps_camera_rig.gd")
|
var rig_script = preload("res://characters/player/fps_camera_rig.gd")
|
||||||
if rig_script:
|
if rig_script:
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
const SAVE_PATH = "user://loadouts.json"
|
||||||
|
|
||||||
|
var weapon_db = {
|
||||||
|
"none": {
|
||||||
|
"name": "None",
|
||||||
|
"category": "none",
|
||||||
|
"script": ""
|
||||||
|
},
|
||||||
|
"double_barrel_shotgun": {
|
||||||
|
"name": "Double Barrel Shotgun",
|
||||||
|
"category": "primary",
|
||||||
|
"script": "res://weapons/double_barrel_shotgun.gd"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var active_loadout_index: int = 0
|
||||||
|
|
||||||
|
var loadouts: Array = []
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_ensure_default_loadouts()
|
||||||
|
load_loadouts()
|
||||||
|
|
||||||
|
func _ensure_default_loadouts() -> void:
|
||||||
|
loadouts.clear()
|
||||||
|
for i in range(5):
|
||||||
|
loadouts.append({
|
||||||
|
"name": "Loadout " + str(i + 1),
|
||||||
|
"primary_1": "double_barrel_shotgun",
|
||||||
|
"primary_2": "none",
|
||||||
|
"special": "none"
|
||||||
|
})
|
||||||
|
|
||||||
|
func get_active_loadout() -> Dictionary:
|
||||||
|
if active_loadout_index >= 0 and active_loadout_index < loadouts.size():
|
||||||
|
return loadouts[active_loadout_index]
|
||||||
|
return loadouts[0]
|
||||||
|
|
||||||
|
func save_loadouts() -> void:
|
||||||
|
var save_data = {
|
||||||
|
"active_index": active_loadout_index,
|
||||||
|
"loadouts": loadouts
|
||||||
|
}
|
||||||
|
var json_string = JSON.stringify(save_data)
|
||||||
|
var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
|
||||||
|
if file:
|
||||||
|
file.store_string(json_string)
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
func load_loadouts() -> void:
|
||||||
|
if FileAccess.file_exists(SAVE_PATH):
|
||||||
|
var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
|
||||||
|
if file:
|
||||||
|
var json_string = file.get_as_text()
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
var json = JSON.new()
|
||||||
|
var error = json.parse(json_string)
|
||||||
|
if error == OK:
|
||||||
|
var data = json.get_data()
|
||||||
|
if data is Dictionary:
|
||||||
|
if data.has("active_index"):
|
||||||
|
active_loadout_index = data["active_index"]
|
||||||
|
if data.has("loadouts") and data["loadouts"] is Array:
|
||||||
|
var saved_loadouts = data["loadouts"]
|
||||||
|
# Overwrite defaults, up to 5
|
||||||
|
for i in range(min(5, saved_loadouts.size())):
|
||||||
|
var sl = saved_loadouts[i]
|
||||||
|
if sl is Dictionary:
|
||||||
|
if sl.has("name"): loadouts[i]["name"] = sl["name"]
|
||||||
|
if sl.has("primary_1"): loadouts[i]["primary_1"] = sl["primary_1"]
|
||||||
|
if sl.has("primary_2"): loadouts[i]["primary_2"] = sl["primary_2"]
|
||||||
|
if sl.has("special"): loadouts[i]["special"] = sl["special"]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ck4n30620akl4
|
||||||
@@ -15,6 +15,11 @@ run/main_scene="res://scenes/test_level/test_level.tscn"
|
|||||||
config/features=PackedStringArray("4.6", "Forward Plus")
|
config/features=PackedStringArray("4.6", "Forward Plus")
|
||||||
config/icon="res://icon.svg"
|
config/icon="res://icon.svg"
|
||||||
|
|
||||||
|
[autoload]
|
||||||
|
|
||||||
|
LoadoutManager="*res://globals/loadout_manager.gd"
|
||||||
|
PauseMenu="*res://ui/pause_menu.gd"
|
||||||
|
|
||||||
[input]
|
[input]
|
||||||
|
|
||||||
move_forward={
|
move_forward={
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
extends CanvasLayer
|
||||||
|
|
||||||
|
var bg: ColorRect
|
||||||
|
var main_vbox: VBoxContainer
|
||||||
|
var resume_btn: Button
|
||||||
|
var loadouts_btn: Button
|
||||||
|
var exit_btn: Button
|
||||||
|
|
||||||
|
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 save_loadout_btn: Button
|
||||||
|
var back_to_main_btn: Button
|
||||||
|
|
||||||
|
var editing_index: int = -1
|
||||||
|
|
||||||
|
var quit_dialog: ConfirmationDialog
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
layer = 100 # Ensure it's on top
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
_build_ui()
|
||||||
|
_connect_signals()
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if event.is_action_pressed("ui_cancel"):
|
||||||
|
if visible:
|
||||||
|
_resume()
|
||||||
|
else:
|
||||||
|
_pause()
|
||||||
|
|
||||||
|
func _pause() -> void:
|
||||||
|
visible = true
|
||||||
|
get_tree().paused = true
|
||||||
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||||
|
_show_main_menu()
|
||||||
|
|
||||||
|
func _resume() -> void:
|
||||||
|
visible = false
|
||||||
|
get_tree().paused = false
|
||||||
|
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
bg = ColorRect.new()
|
||||||
|
bg.color = Color(0, 0, 0, 0.7)
|
||||||
|
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
add_child(bg)
|
||||||
|
|
||||||
|
# Main Menu
|
||||||
|
main_vbox = VBoxContainer.new()
|
||||||
|
main_vbox.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
|
main_vbox.add_theme_constant_override("separation", 20)
|
||||||
|
bg.add_child(main_vbox)
|
||||||
|
|
||||||
|
var title = Label.new()
|
||||||
|
title.text = "PAUSED"
|
||||||
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
title.add_theme_font_size_override("font_size", 48)
|
||||||
|
main_vbox.add_child(title)
|
||||||
|
|
||||||
|
resume_btn = Button.new()
|
||||||
|
resume_btn.text = "Resume"
|
||||||
|
resume_btn.custom_minimum_size = Vector2(200, 50)
|
||||||
|
main_vbox.add_child(resume_btn)
|
||||||
|
|
||||||
|
loadouts_btn = Button.new()
|
||||||
|
loadouts_btn.text = "Loadouts"
|
||||||
|
loadouts_btn.custom_minimum_size = Vector2(200, 50)
|
||||||
|
main_vbox.add_child(loadouts_btn)
|
||||||
|
|
||||||
|
exit_btn = Button.new()
|
||||||
|
exit_btn.text = "Exit Game"
|
||||||
|
exit_btn.custom_minimum_size = Vector2(200, 50)
|
||||||
|
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 editor_hbox = HBoxContainer.new()
|
||||||
|
editor_hbox.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
|
editor_hbox.add_theme_constant_override("separation", 40)
|
||||||
|
loadout_editor.add_child(editor_hbox)
|
||||||
|
|
||||||
|
# Loadout List
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
edit_panel = PanelContainer.new()
|
||||||
|
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)
|
||||||
|
|
||||||
|
name_edit = LineEdit.new()
|
||||||
|
name_edit.placeholder_text = "Loadout Name"
|
||||||
|
name_edit.custom_minimum_size = Vector2(250, 0)
|
||||||
|
edit_vbox.add_child(name_edit)
|
||||||
|
|
||||||
|
edit_vbox.add_child(_create_label("Primary 1:"))
|
||||||
|
primary1_opt = OptionButton.new()
|
||||||
|
edit_vbox.add_child(primary1_opt)
|
||||||
|
|
||||||
|
edit_vbox.add_child(_create_label("Primary 2:"))
|
||||||
|
primary2_opt = OptionButton.new()
|
||||||
|
edit_vbox.add_child(primary2_opt)
|
||||||
|
|
||||||
|
edit_vbox.add_child(_create_label("Special:"))
|
||||||
|
special_opt = OptionButton.new()
|
||||||
|
edit_vbox.add_child(special_opt)
|
||||||
|
|
||||||
|
save_loadout_btn = Button.new()
|
||||||
|
save_loadout_btn.text = "Save Loadout"
|
||||||
|
save_loadout_btn.custom_minimum_size = Vector2(0, 40)
|
||||||
|
edit_vbox.add_child(save_loadout_btn)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
func _create_label(text: String) -> Label:
|
||||||
|
var l = Label.new()
|
||||||
|
l.text = text
|
||||||
|
return l
|
||||||
|
|
||||||
|
func _connect_signals() -> void:
|
||||||
|
resume_btn.pressed.connect(_resume)
|
||||||
|
loadouts_btn.pressed.connect(_show_loadouts)
|
||||||
|
exit_btn.pressed.connect(_show_exit_dialog)
|
||||||
|
back_to_main_btn.pressed.connect(_show_main_menu)
|
||||||
|
save_loadout_btn.pressed.connect(_save_current_loadout)
|
||||||
|
quit_dialog.confirmed.connect(_quit_game)
|
||||||
|
|
||||||
|
func _show_main_menu() -> void:
|
||||||
|
main_vbox.visible = true
|
||||||
|
loadout_editor.visible = false
|
||||||
|
|
||||||
|
func _show_loadouts() -> void:
|
||||||
|
main_vbox.visible = false
|
||||||
|
loadout_editor.visible = true
|
||||||
|
edit_panel.visible = false
|
||||||
|
_populate_loadout_list()
|
||||||
|
|
||||||
|
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()
|
||||||
|
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)
|
||||||
|
|
||||||
|
func _equip_loadout(idx: int) -> void:
|
||||||
|
LoadoutManager.active_loadout_index = idx
|
||||||
|
LoadoutManager.save_loadouts()
|
||||||
|
_populate_loadout_list()
|
||||||
|
|
||||||
|
func _edit_loadout(idx: int) -> void:
|
||||||
|
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"])
|
||||||
|
|
||||||
|
edit_panel.visible = true
|
||||||
|
|
||||||
|
func _populate_options(opt: OptionButton, category: String, current_id: String) -> void:
|
||||||
|
opt.clear()
|
||||||
|
var idx = 0
|
||||||
|
var select_idx = 0
|
||||||
|
|
||||||
|
# Always add "none" as first option
|
||||||
|
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 = LoadoutManager.loadouts[editing_index]
|
||||||
|
l["name"] = name_edit.text
|
||||||
|
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())
|
||||||
|
LoadoutManager.save_loadouts()
|
||||||
|
_populate_loadout_list()
|
||||||
|
edit_panel.visible = false
|
||||||
|
|
||||||
|
func _show_exit_dialog() -> void:
|
||||||
|
quit_dialog.popup_centered()
|
||||||
|
|
||||||
|
func _quit_game() -> void:
|
||||||
|
get_tree().quit()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bcvjb1h4fxcct
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
extends Node3D
|
||||||
|
class_name WeaponManager
|
||||||
|
|
||||||
|
var player: CharacterBody3D
|
||||||
|
var camera: Camera3D
|
||||||
|
|
||||||
|
var active_slot: int = 1 # 1: Primary 1, 2: Primary 2, 3: Special
|
||||||
|
var weapons: Dictionary = {} # slot_index: Node3D
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
set_process_input(true)
|
||||||
|
|
||||||
|
# Wait one frame for LoadoutManager to be fully ready if needed
|
||||||
|
await get_tree().process_frame
|
||||||
|
_build_loadout()
|
||||||
|
|
||||||
|
func _build_loadout() -> void:
|
||||||
|
# Clear existing weapons
|
||||||
|
for w in weapons.values():
|
||||||
|
if is_instance_valid(w):
|
||||||
|
w.queue_free()
|
||||||
|
weapons.clear()
|
||||||
|
|
||||||
|
var l = LoadoutManager.get_active_loadout()
|
||||||
|
|
||||||
|
_spawn_weapon(1, l["primary_1"])
|
||||||
|
_spawn_weapon(2, l["primary_2"])
|
||||||
|
_spawn_weapon(3, l["special"])
|
||||||
|
|
||||||
|
_equip_slot(1)
|
||||||
|
|
||||||
|
func _spawn_weapon(slot: int, weapon_id: String) -> void:
|
||||||
|
if weapon_id == "" or weapon_id == "none" or not LoadoutManager.weapon_db.has(weapon_id):
|
||||||
|
return
|
||||||
|
|
||||||
|
var w_data = LoadoutManager.weapon_db[weapon_id]
|
||||||
|
var script_path = w_data["script"]
|
||||||
|
if script_path == "":
|
||||||
|
return
|
||||||
|
|
||||||
|
var script = load(script_path)
|
||||||
|
if script:
|
||||||
|
var w = script.new()
|
||||||
|
w.name = "Weapon_" + str(slot) + "_" + weapon_id
|
||||||
|
# Assuming all weapons have player and camera vars
|
||||||
|
if "player" in w: w.player = player
|
||||||
|
if "camera" in w: w.camera = camera
|
||||||
|
add_child(w)
|
||||||
|
weapons[slot] = w
|
||||||
|
w.visible = false
|
||||||
|
w.set_process_input(false)
|
||||||
|
|
||||||
|
func _equip_slot(slot: int) -> void:
|
||||||
|
if weapons.has(active_slot):
|
||||||
|
var w = weapons[active_slot]
|
||||||
|
w.visible = false
|
||||||
|
w.set_process_input(false)
|
||||||
|
|
||||||
|
active_slot = slot
|
||||||
|
|
||||||
|
if weapons.has(active_slot):
|
||||||
|
var w = weapons[active_slot]
|
||||||
|
w.visible = true
|
||||||
|
w.set_process_input(true)
|
||||||
|
print("Equipped slot ", slot)
|
||||||
|
else:
|
||||||
|
print("Slot ", slot, " is empty!")
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
|
||||||
|
return
|
||||||
|
|
||||||
|
if event.is_action_pressed("weapon_1"):
|
||||||
|
_equip_slot(1)
|
||||||
|
elif event.is_action_pressed("weapon_2"):
|
||||||
|
_equip_slot(2)
|
||||||
|
elif event.is_action_pressed("weapon_3"):
|
||||||
|
_equip_slot(3)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bp5ujeumiq5j
|
||||||
Reference in New Issue
Block a user