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 # Notify owner character of attack cooldown (for UI) if owner_character and owner_character.is_multiplayer_authority(): owner_character._attack_timer = weapon_data.attack_cooldown # Play attack animation on owner (use weapon's animation) if owner_character._body: var anim_name = weapon_data.attack_animation if weapon_data.attack_animation else "Attack_OneHand" owner_character._body.play_attack(anim_name) # Sync animation to other clients owner_character._sync_attack_animation.rpc(anim_name) # Delay damage until animation hits (roughly 70% through the animation) # This makes the damage apply when the swing actually connects var damage_delay = weapon_data.attack_cooldown * 0.4 # Adjust this multiplier to change when damage happens get_tree().create_timer(damage_delay).timeout.connect(_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, weapon_data.knockback_force, owner_character.global_position ) else: # Otherwise, request server to apply damage owner_character.rpc_id(1, "_server_apply_damage", hit_body.name, weapon_data.damage, attacker_id, weapon_data.knockback_force, owner_character.global_position ) 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