Feat/2 weapons foundation #6

Merged
Dotts merged 30 commits from feat/2-weapons-foundation into main 2026-06-04 22:22:16 -07:00
8 changed files with 423 additions and 10 deletions
Showing only changes of commit 8eea7d6e36 - Show all commits
+12 -10
View File
@@ -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:
+75
View File
@@ -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"]
+1
View File
@@ -0,0 +1 @@
uid://ck4n30620akl4
+5
View File
@@ -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={
+250
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://bcvjb1h4fxcct
+78
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://bp5ujeumiq5j