Base Weapons

Place in world, pick up and drop working on multiplayer
This commit is contained in:
Twirpytherobot
2025-11-15 16:03:38 +00:00
parent 791b1557c7
commit 86db6865b7
144 changed files with 3582 additions and 2 deletions
+105
View File
@@ -0,0 +1,105 @@
extends Node3D
class_name BaseWeapon
## Base class for equipped weapons
## Attached to player's hand via BoneAttachment3D
## Provides common weapon functionality and stats
signal attack_performed()
@export var weapon_data: WeaponData
# Runtime references
var owner_character: Character = null
var _mesh_instance: Node3D = null
var _attack_timer: float = 0.0
func _ready():
if weapon_data and weapon_data.mesh_scene:
_spawn_mesh()
func _process(delta):
if _attack_timer > 0:
_attack_timer -= delta
## Spawn the visual mesh for this weapon
func _spawn_mesh():
# Remove old mesh if exists
if _mesh_instance:
_mesh_instance.queue_free()
# Instantiate new mesh
_mesh_instance = weapon_data.mesh_scene.instantiate()
add_child(_mesh_instance)
## Perform an attack with this weapon
## Called by the character who owns this weapon
func perform_attack() -> bool:
if not weapon_data or not owner_character:
return false
# Check cooldown
if _attack_timer > 0:
return false
_attack_timer = weapon_data.attack_cooldown
# Play attack animation on owner
if owner_character._body:
owner_character._body.play_attack()
# Find targets in range
_find_and_damage_targets()
attack_performed.emit()
return true
## Find targets in range and apply damage
func _find_and_damage_targets():
if not owner_character:
return
# Check if the owner character has authority (not this node)
if not owner_character.is_multiplayer_authority():
return
var space_state = get_world_3d().direct_space_state
var query = PhysicsShapeQueryParameters3D.new()
var sphere = SphereShape3D.new()
sphere.radius = weapon_data.attack_range
query.shape = sphere
query.transform = global_transform
query.collision_mask = 1 # Player layer
var results = space_state.intersect_shape(query)
for result in results:
var hit_body = result["collider"]
if hit_body != owner_character and hit_body is BaseUnit:
var attacker_id = multiplayer.get_unique_id()
# If we're the server, apply damage directly
if multiplayer.is_server():
owner_character._server_apply_damage(hit_body.name, weapon_data.damage, attacker_id)
else:
# Otherwise, request server to apply damage
owner_character.rpc_id(1, "_server_apply_damage", hit_body.name, weapon_data.damage, attacker_id)
break # Only hit one target per attack
## Check if weapon can attack
func can_attack() -> bool:
return _attack_timer <= 0
## Set the character who owns this weapon
func set_owner_character(character: Character):
owner_character = character
## Get weapon stats
func get_damage() -> float:
return weapon_data.damage if weapon_data else 0.0
func get_range() -> float:
return weapon_data.attack_range if weapon_data else 0.0
func get_cooldown() -> float:
return weapon_data.attack_cooldown if weapon_data else 0.0
+1
View File
@@ -0,0 +1 @@
uid://bknbrlgutvk6h
+71
View File
@@ -4,10 +4,14 @@ extends Node3D
@onready var nick_input: LineEdit = $Menu/MainContainer/MainMenu/Option1/NickInput
@onready var address_input: LineEdit = $Menu/MainContainer/MainMenu/Option3/AddressInput
@onready var players_container: Node3D = $PlayersContainer
@onready var weapons_container: Node3D = null # Will be created if doesn't exist
@onready var menu: Control = $Menu
@onready var main_menu: VBoxContainer = $Menu/MainContainer/MainMenu
@export var player_scene: PackedScene
# Weapon spawning counter (server-side only)
var _weapon_spawn_counter: int = 0
# multiplayer chat
@onready var message: LineEdit = $MultiplayerChat/VBoxContainer/HBoxContainer/Message
@onready var send: Button = $MultiplayerChat/VBoxContainer/HBoxContainer/Send
@@ -24,12 +28,49 @@ func _ready():
# Add quick-fill preset buttons
_create_preset_buttons()
# Create or find weapons container
if has_node("WeaponsContainer"):
weapons_container = get_node("WeaponsContainer")
else:
weapons_container = Node3D.new()
weapons_container.name = "WeaponsContainer"
add_child(weapons_container)
print("Created WeaponsContainer")
if not multiplayer.is_server():
return
Network.connect("player_connected", Callable(self, "_on_player_connected"))
multiplayer.peer_disconnected.connect(_remove_player)
# Spawn initial weapons when server starts
_spawn_initial_weapons()
func _spawn_initial_weapons():
if not multiplayer.is_server():
return
# Wait a frame for everything to be ready
await get_tree().process_frame
# Spawn a sword
_weapon_spawn_counter += 1
rpc("spawn_world_weapon",
"res://level/resources/weapon_sword.tres",
Vector3(5, 1, 0),
Vector3.ZERO,
_weapon_spawn_counter
)
# Spawn a shield
_weapon_spawn_counter += 1
rpc("spawn_world_weapon",
"res://level/resources/weapon_shield.tres",
Vector3(-5, 1, 0),
Vector3.ZERO,
_weapon_spawn_counter
)
func _on_player_connected(peer_id, player_info):
# for id in Network.players.keys():
# var player_data = Network.players[id]
@@ -162,3 +203,33 @@ func _on_jemz_preset():
nick_input.text = "Jemz"
skin_input.text = "Red"
address_input.text = "127.0.0.1"
# ---------- WEAPON SPAWNING ----------
## Spawn a weapon in the world (called from server, syncs to all clients)
@rpc("authority", "call_local", "reliable")
func spawn_world_weapon(weapon_data_path: String, spawn_position: Vector3, initial_velocity: Vector3, weapon_id: int):
if not weapons_container:
push_error("WeaponsContainer not found in level!")
return
# Load the weapon data resource
var weapon_data = load(weapon_data_path) as WeaponData
if not weapon_data:
push_error("Failed to load weapon data from: " + weapon_data_path)
return
# Create WorldWeapon instance
var world_weapon = WorldWeapon.new()
world_weapon.weapon_data = weapon_data
world_weapon.name = "WorldWeapon_" + str(weapon_id) # Deterministic name
world_weapon.position = spawn_position
# Add to weapons container
weapons_container.add_child(world_weapon, true)
print("Spawned weapon: ", world_weapon.name, " at ", spawn_position)
# Apply velocity after physics is ready
if initial_velocity != Vector3.ZERO:
await get_tree().process_frame
world_weapon.linear_velocity = initial_velocity
+192 -1
View File
@@ -13,6 +13,7 @@ enum SkinColor { BLUE, YELLOW, GREEN, RED }
@export_category("Objects")
@export var _body: Node3D = null
@export var _spring_arm_offset: Node3D = null
@export var _weapon_attachment: BoneAttachment3D = null # WeaponPoint bone attachment
@export_category("Skin Colors")
@export var blue_texture : CompressedTexture2D
@@ -28,6 +29,10 @@ enum SkinColor { BLUE, YELLOW, GREEN, RED }
var _current_speed: float
var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
# Weapon system
var equipped_weapon: BaseWeapon = null
var _nearby_weapons: Array[WorldWeapon] = []
# Attack system
@export var attack_damage: float = 10.0
@export var attack_range: float = 3.0
@@ -67,6 +72,18 @@ func _ready():
if is_multiplayer_authority():
_create_health_ui()
# Setup weapon pickup detection area
_setup_weapon_pickup_area()
# Auto-find weapon attachment if not set
if _weapon_attachment == null:
var bone_attach = get_node_or_null("3DGodotRobot/RobotArmature/Skeleton3D/BoneAttachment3D")
if bone_attach:
_weapon_attachment = bone_attach
print("Auto-found weapon attachment point")
else:
push_warning("Could not find BoneAttachment3D for weapons!")
func _physics_process(delta):
# Check if multiplayer is ready
if multiplayer.multiplayer_peer == null:
@@ -122,9 +139,23 @@ func _process(delta):
_perform_dash()
# Handle attack input
if Input.is_action_just_pressed("attack") and _attack_timer <= 0 and not is_dead and not _is_dashing:
if Input.is_action_just_pressed("attack") and not is_dead and not _is_dashing:
_perform_attack()
# Handle weapon pickup/drop
if Input.is_action_just_pressed("pickup") and not is_dead:
print("Pickup pressed! Equipped: ", equipped_weapon != null, " Nearby: ", _nearby_weapons.size())
if equipped_weapon:
# Tell server to drop (server will handle spawning)
if multiplayer.is_server():
drop_weapon()
else:
rpc_id(1, "drop_weapon") # Only send to server
elif _nearby_weapons.size() > 0:
_pickup_nearest_weapon()
else:
print("No weapons nearby to pick up")
func freeze():
velocity.x = 0
velocity.z = 0
@@ -208,10 +239,19 @@ func _perform_attack():
if not is_multiplayer_authority() or is_dead:
return
# Use equipped weapon if available
if equipped_weapon and equipped_weapon.can_attack():
equipped_weapon.perform_attack()
return
# Fallback to default unarmed attack
# Don't attack if already attacking
if _body and _body.animation_player and _body.animation_player.current_animation == "Attack1":
return
if _attack_timer > 0:
return
_attack_timer = attack_cooldown
# Play attack animation once
@@ -399,3 +439,154 @@ func _perform_dash():
func _reset_dash_rotation(rotation_value: float):
if _body:
_body.rotation.x = rotation_value
## Weapon System
func _setup_weapon_pickup_area():
# Create an Area3D to detect nearby weapons
var pickup_area = Area3D.new()
pickup_area.name = "WeaponPickupArea"
pickup_area.collision_layer = 0
pickup_area.collision_mask = 4 # Will detect WorldWeapons (we'll use layer 3)
add_child(pickup_area)
# Create collision shape for pickup range
var pickup_collision = CollisionShape3D.new()
var sphere = SphereShape3D.new()
sphere.radius = 2.0 # Pickup range
pickup_collision.shape = sphere
pickup_area.add_child(pickup_collision)
# Connect signals to track nearby weapons
pickup_area.area_entered.connect(_on_weapon_area_entered)
pickup_area.area_exited.connect(_on_weapon_area_exited)
func _on_weapon_area_entered(area: Area3D):
# Check if the area belongs to a WorldWeapon
var weapon = area.get_parent()
if weapon is WorldWeapon and weapon not in _nearby_weapons:
_nearby_weapons.append(weapon)
if is_multiplayer_authority():
print("Weapon nearby: ", weapon.weapon_data.weapon_name if weapon.weapon_data else "Unknown")
func _on_weapon_area_exited(area: Area3D):
# Remove weapon from nearby list
var weapon = area.get_parent()
if weapon is WorldWeapon and weapon in _nearby_weapons:
_nearby_weapons.erase(weapon)
if is_multiplayer_authority():
print("Weapon left range: ", weapon.weapon_data.weapon_name if weapon.weapon_data else "Unknown")
## Equip a weapon from WorldWeapon data (receives resource path)
@rpc("any_peer", "call_local", "reliable")
func equip_weapon_from_world(weapon_data_path: String):
var data = load(weapon_data_path) as WeaponData
if data:
equip_weapon(data)
else:
push_error("Failed to load weapon data from: " + weapon_data_path)
## Equip a weapon with given data
func equip_weapon(data: WeaponData):
# Unequip current weapon first
if equipped_weapon:
unequip_weapon()
if not _weapon_attachment:
push_error("No weapon attachment point found!")
return
# Create new weapon instance
var weapon = BaseWeapon.new()
weapon.weapon_data = data
weapon.name = "EquippedWeapon"
weapon.set_owner_character(self)
# Attach to bone
_weapon_attachment.add_child(weapon)
equipped_weapon = weapon
if is_multiplayer_authority():
print("Equipped: ", data.weapon_name)
## Unequip current weapon (local only)
func unequip_weapon():
if equipped_weapon:
equipped_weapon.queue_free()
equipped_weapon = null
## Sync unequip across all clients
@rpc("any_peer", "call_local", "reliable")
func _unequip_weapon_sync():
unequip_weapon()
## Drop currently equipped weapon
@rpc("any_peer", "reliable")
func drop_weapon():
print("drop_weapon called on peer ", multiplayer.get_unique_id(), " is_server: ", multiplayer.is_server(), " has weapon: ", equipped_weapon != null)
if not equipped_weapon:
print("No weapon equipped, cannot drop")
return
# Only server spawns the world weapon
if multiplayer.is_server():
print("Server spawning dropped weapon")
_spawn_world_weapon(equipped_weapon.weapon_data)
# Tell all clients to unequip
rpc("_unequip_weapon_sync")
# Unequip locally
unequip_weapon()
if is_multiplayer_authority():
print("Dropped weapon")
## Spawn a weapon in the world (server only)
func _spawn_world_weapon(data: WeaponData):
if not multiplayer.is_server():
return
# Get the resource path
var resource_path = data.resource_path
if resource_path == "":
push_error("WeaponData has no resource path! Make sure to save it as a .tres file")
return
# Position in front of player
var spawn_pos = global_position + (-transform.basis.z * 2.0)
spawn_pos.y += 1.0 # Spawn at chest height
# Calculate forward velocity
var velocity = -transform.basis.z * 3.0
# Tell level to spawn weapon on all clients
var level = get_tree().get_current_scene()
if level and level.has_method("spawn_world_weapon"):
# Increment the level's weapon counter
level._weapon_spawn_counter += 1
level.rpc("spawn_world_weapon", resource_path, spawn_pos, velocity, level._weapon_spawn_counter)
## Pick up nearest weapon
func _pickup_nearest_weapon():
if _nearby_weapons.size() == 0:
return
# Find closest weapon
var closest_weapon: WorldWeapon = null
var closest_distance: float = INF
for weapon in _nearby_weapons:
if not is_instance_valid(weapon):
continue
var distance = global_position.distance_to(weapon.global_position)
if distance < closest_distance:
closest_distance = distance
closest_weapon = weapon
if closest_weapon:
# Request server to pickup (server validates)
if multiplayer.is_server():
closest_weapon.try_pickup(multiplayer.get_unique_id())
else:
closest_weapon.rpc_id(1, "try_pickup", multiplayer.get_unique_id())
+23
View File
@@ -0,0 +1,23 @@
extends Resource
class_name WeaponData
## Resource that stores weapon statistics and properties
## Can be reused for both equipped weapons and world pickups
@export_category("Weapon Info")
@export var weapon_name: String = "Weapon"
@export_multiline var description: String = ""
@export_category("Combat Stats")
@export var damage: float = 10.0
@export var attack_range: float = 3.0
@export var attack_cooldown: float = 0.5
@export var attack_animation: String = "Attack1" # Animation to play when attacking
@export_category("Visual")
@export var mesh_scene: PackedScene # The 3D mesh for this weapon
@export var icon: Texture2D # Optional icon for UI
@export_category("Physics (for world pickups)")
@export var pickup_radius: float = 1.5 # How close player needs to be to pick up
@export var weight: float = 1.0 # Mass when dropped in world
+1
View File
@@ -0,0 +1 @@
uid://d2homvlmrg6xs
+139
View File
@@ -0,0 +1,139 @@
extends RigidBody3D
class_name WorldWeapon
## Weapon as a physics object in the world
## Can be picked up by players
## Spawned when a weapon is dropped or placed in the level
@export var weapon_data: WeaponData
var _mesh_instance: Node3D = null
var _collision_shape: CollisionShape3D = null
var _pickup_area: Area3D = null
var _is_being_picked_up: bool = false
func _ready():
# Set collision layer to "weapon" (layer 3 = bit 4)
collision_layer = 4
collision_mask = 2 # Collide with world
# Set up physics
if weapon_data:
mass = weapon_data.weight
# Spawn mesh
if weapon_data and weapon_data.mesh_scene:
_spawn_mesh()
# Create collision shape if not exists
_setup_collision()
# Create pickup area
_setup_pickup_area()
# Only server manages pickup logic
if multiplayer.is_server():
_pickup_area.body_entered.connect(_on_body_entered_pickup_area)
func _spawn_mesh():
# Remove old mesh if exists
if _mesh_instance:
_mesh_instance.queue_free()
# Instantiate mesh
_mesh_instance = weapon_data.mesh_scene.instantiate()
add_child(_mesh_instance)
func _setup_collision():
# Check if we already have a collision shape
for child in get_children():
if child is CollisionShape3D:
_collision_shape = child
return
# Create a basic box collision if none exists
_collision_shape = CollisionShape3D.new()
var box_shape = BoxShape3D.new()
box_shape.size = Vector3(0.5, 0.5, 1.5) # Approximate weapon size
_collision_shape.shape = box_shape
add_child(_collision_shape)
func _setup_pickup_area():
# Create Area3D for pickup detection
_pickup_area = Area3D.new()
_pickup_area.name = "PickupArea"
_pickup_area.collision_layer = 4 # Layer 3 (weapon layer) so player can detect it
_pickup_area.collision_mask = 1 # Detect players
add_child(_pickup_area)
# Create sphere collision for pickup range
var pickup_collision = CollisionShape3D.new()
var sphere = SphereShape3D.new()
sphere.radius = weapon_data.pickup_radius if weapon_data else 1.5
pickup_collision.shape = sphere
_pickup_area.add_child(pickup_collision)
func _on_body_entered_pickup_area(body: Node3D):
if _is_being_picked_up:
return
# Check if it's a player trying to pick up
if body is Character and body.is_multiplayer_authority():
# Let the player handle the pickup
# The player will call try_pickup() via RPC
pass
## Called by player to attempt pickup
@rpc("any_peer", "reliable")
func try_pickup(player_id: int):
# Only server validates pickup
if not multiplayer.is_server():
return
if _is_being_picked_up:
return
# Find the player
var level = get_tree().get_current_scene()
if not level or not level.has_node("PlayersContainer"):
return
var players_container = level.get_node("PlayersContainer")
if not players_container.has_node(str(player_id)):
return
var player = players_container.get_node(str(player_id))
if not player is Character:
return
# Check distance
var distance = global_position.distance_to(player.global_position)
if distance > (weapon_data.pickup_radius if weapon_data else 1.5):
return
_is_being_picked_up = true
# Get the resource path
var resource_path = weapon_data.resource_path
if resource_path == "":
push_error("WeaponData has no resource path!")
return
# Tell the player to equip this weapon (on all clients)
player.rpc("equip_weapon_from_world", resource_path)
# Remove this world weapon from all clients
rpc("_remove_from_all_clients")
## Remove weapon from all clients
@rpc("any_peer", "call_local", "reliable")
func _remove_from_all_clients():
queue_free()
## Set weapon data and refresh
func set_weapon_data(data: WeaponData):
weapon_data = data
if is_inside_tree():
_spawn_mesh()
if weapon_data:
mass = weapon_data.weight
+1
View File
@@ -0,0 +1 @@
uid://ccnnd0y4jqiot