Base Weapons
Place in world, pick up and drop working on multiplayer
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
[gd_resource type="Resource" script_class="WeaponData" load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://level/scripts/weapon_data.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://level/scenes/weapons/shield_mesh.tscn" id="2"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
weapon_name = "Wooden Shield"
|
||||
description = "A sturdy wooden shield. Can be used to bash enemies."
|
||||
damage = 8.0
|
||||
attack_range = 2.5
|
||||
attack_cooldown = 0.8
|
||||
attack_animation = "Attack1"
|
||||
mesh_scene = ExtResource("2")
|
||||
pickup_radius = 1.5
|
||||
weight = 3.0
|
||||
@@ -0,0 +1,16 @@
|
||||
[gd_resource type="Resource" script_class="WeaponData" load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://level/scripts/weapon_data.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://level/scenes/weapons/sword_mesh.tscn" id="2"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
weapon_name = "Iron Sword"
|
||||
description = "A simple iron sword. Good for close combat."
|
||||
damage = 15.0
|
||||
attack_range = 3.5
|
||||
attack_cooldown = 0.6
|
||||
attack_animation = "Attack1"
|
||||
mesh_scene = ExtResource("2")
|
||||
pickup_radius = 1.5
|
||||
weight = 2.0
|
||||
@@ -0,0 +1,9 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://c76hql634gf5"]
|
||||
|
||||
[ext_resource type="ArrayMesh" uid="uid://cc1kxfbkvpo2d" path="res://assets/Objects/swordlowpoly.obj" id="1_8p8s3"]
|
||||
|
||||
[node name="BaseWeaponScene" type="Node3D"]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.5366993, -2.9846973, -3.4840996)
|
||||
mesh = ExtResource("1_8p8s3")
|
||||
@@ -4780,12 +4780,13 @@ properties/3/path = NodePath("3DGodotRobot:rotation")
|
||||
properties/3/spawn = true
|
||||
properties/3/replication_mode = 1
|
||||
|
||||
[node name="Player" type="CharacterBody3D" node_paths=PackedStringArray("_body", "_spring_arm_offset")]
|
||||
[node name="Player" type="CharacterBody3D" node_paths=PackedStringArray("_body", "_spring_arm_offset", "_weapon_attachment")]
|
||||
transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0)
|
||||
collision_mask = 3
|
||||
script = ExtResource("1_tdh26")
|
||||
_body = NodePath("3DGodotRobot")
|
||||
_spring_arm_offset = NodePath("SpringArmOffset")
|
||||
_weapon_attachment = NodePath("3DGodotRobot/RobotArmature/Skeleton3D/WeaponPoint")
|
||||
blue_texture = ExtResource("4_max2b")
|
||||
yellow_texture = ExtResource("3_l3dv8")
|
||||
green_texture = ExtResource("4_74ree")
|
||||
@@ -5006,6 +5007,11 @@ mesh = SubResource("ArrayMesh_0hcp6")
|
||||
skin = SubResource("Skin_gooby")
|
||||
surface_material_override/0 = SubResource("StandardMaterial3D_1ln88")
|
||||
|
||||
[node name="WeaponPoint" type="BoneAttachment3D" parent="3DGodotRobot/RobotArmature/Skeleton3D"]
|
||||
transform = Transform3D(-0.95200163, -0.30448738, -0.031308167, 0.30294672, -0.9519023, 0.045883153, -0.043773204, 0.03419612, 0.99845606, -0.64481956, 1.0056509, 0.02542795)
|
||||
bone_name = "hand.R"
|
||||
bone_idx = 16
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="3DGodotRobot"]
|
||||
libraries = {
|
||||
&"": SubResource("AnimationLibrary_f1v7x")
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://rbvk4mg40ceg"]
|
||||
|
||||
[ext_resource type="ArrayMesh" uid="uid://rsoymmi6yqhp" path="res://assets/Objects/shield.obj" id="1"]
|
||||
|
||||
[node name="ShieldMesh" type="Node3D"]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.28306955, -0.70267653)
|
||||
mesh = ExtResource("1")
|
||||
@@ -0,0 +1,9 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dyjfaq654xne3"]
|
||||
|
||||
[ext_resource type="ArrayMesh" uid="uid://dmottq5u3my52" path="res://assets/characters/Lobster/10029_Lobster_v1_iterations-2.obj" id="1_olchg"]
|
||||
|
||||
[node name="SwordMesh" type="Node3D"]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.3645025, -0.7985528, -1.0164871)
|
||||
mesh = ExtResource("1_olchg")
|
||||
@@ -0,0 +1,17 @@
|
||||
[gd_scene load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://level/scripts/world_weapon.gd" id="1"]
|
||||
[ext_resource type="Resource" path="res://level/resources/weapon_shield.tres" id="2"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="1"]
|
||||
size = Vector3(0.8, 1.0, 0.2)
|
||||
|
||||
[node name="WorldWeaponShield" type="RigidBody3D"]
|
||||
collision_layer = 4
|
||||
collision_mask = 2
|
||||
mass = 3.0
|
||||
script = ExtResource("1")
|
||||
weapon_data = ExtResource("2")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("1")
|
||||
@@ -0,0 +1,17 @@
|
||||
[gd_scene load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://level/scripts/world_weapon.gd" id="1"]
|
||||
[ext_resource type="Resource" path="res://level/resources/weapon_sword.tres" id="2"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="1"]
|
||||
size = Vector3(0.3, 0.3, 1.2)
|
||||
|
||||
[node name="WorldWeaponSword" type="RigidBody3D"]
|
||||
collision_layer = 4
|
||||
collision_mask = 2
|
||||
mass = 2.0
|
||||
script = ExtResource("1")
|
||||
weapon_data = ExtResource("2")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("1")
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://bknbrlgutvk6h
|
||||
@@ -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
@@ -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())
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://d2homvlmrg6xs
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://ccnnd0y4jqiot
|
||||
Reference in New Issue
Block a user