extends Node3D class_name BaseWeapon ## Base class for equipped weapons ## Attached to player's hand via BoneAttachment3D ## Uses HitBox/HurtBox system for damage detection signal attack_performed() signal hit_connected(target: Node) @export var weapon_data: WeaponData # Runtime references var owner_character: Character = null var _mesh_instance: Node3D = null var _attack_timer: float = 0.0 var _hitbox: HitBox = null func _ready(): if weapon_data and weapon_data.mesh_scene: _spawn_mesh() _setup_hitbox() 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) # Check if mesh has a HitBox child, use it instead of auto-generated one var mesh_hitbox = _mesh_instance.get_node_or_null("HitBox") if mesh_hitbox and mesh_hitbox is HitBox: # Use the hitbox from the mesh scene print("[BaseWeapon] Found manual HitBox in mesh scene") if _hitbox: _hitbox.queue_free() _hitbox = mesh_hitbox _configure_hitbox() else: print("[BaseWeapon] No manual HitBox found, will auto-generate") ## Setup the hitbox for this weapon func _setup_hitbox(): # Skip if we already have a hitbox (e.g., from mesh scene) if _hitbox: return # Create hitbox dynamically based on weapon range _hitbox = HitBox.new() _hitbox.name = "HitBox" add_child(_hitbox) # Add collision shape based on attack range - use sphere for consistent detection # regardless of weapon orientation during animations var collision = CollisionShape3D.new() var sphere = SphereShape3D.new() var range_val = weapon_data.attack_range if weapon_data else 1.5 sphere.radius = range_val collision.shape = sphere _hitbox.add_child(collision) _configure_hitbox() ## Configure hitbox with weapon stats and owner func _configure_hitbox(): if not _hitbox: return # Set damage stats from weapon if weapon_data: _hitbox.set_stats(weapon_data.damage, weapon_data.knockback_force) # Set owner to prevent self-damage _hitbox.owner_entity = owner_character # Connect to hit signal if not _hitbox.hit_landed.is_connected(_on_hitbox_hit): _hitbox.hit_landed.connect(_on_hitbox_hit) ## Called when hitbox connects with a hurtbox func _on_hitbox_hit(target: Node, damage_amount: float, knockback_amount: float, attacker_pos: Vector3): if not target or not owner_character: return # Flash the target's hurtbox red for visual feedback if target is Node: var hurtbox = target.find_child("HurtBox", true, false) if hurtbox and hurtbox.has_method("flash_hit"): hurtbox.flash_hit() hit_connected.emit(target) # Route damage through server var attacker_id = multiplayer.get_unique_id() if multiplayer.is_server(): # We are server, apply directly owner_character._server_apply_damage( target.name, damage_amount, attacker_id, knockback_amount, attacker_pos ) else: # Send to server owner_character.rpc_id(1, "_server_apply_damage", target.name, damage_amount, attacker_id, knockback_amount, attacker_pos ) ## 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) # Activate hitbox for the attack duration # Only activate on authority - they detect hits and send to server if owner_character.is_multiplayer_authority(): _activate_hitbox() attack_performed.emit() return true ## Activate the hitbox for attack detection - active for entire animation func _activate_hitbox(): if not _hitbox: return # Update owner reference in case it changed _hitbox.owner_entity = owner_character # Activate hitbox immediately for the duration specified in weapon data _hitbox.activate() # Calculate total active duration from weapon resource (startup + active) var startup = weapon_data.startup_time if weapon_data else 0.15 var active = weapon_data.active_time if weapon_data else 0.2 var duration = startup + active await get_tree().create_timer(duration).timeout # Deactivate when attack is complete if _hitbox and is_instance_valid(_hitbox): _hitbox.deactivate() ## 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 # Update hitbox owner if _hitbox: _hitbox.owner_entity = 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