|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
## Blocking functionality
|
|
|
|
|
func can_block() -> bool:
|
|
|
|
|
return weapon_data.can_block if weapon_data else false
|
|
|
|
|
|
|
|
|
|
func get_block_reduction() -> float:
|
|
|
|
|
return weapon_data.block_reduction if weapon_data else 0.0
|