You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
200 lines
5.4 KiB
200 lines
5.4 KiB
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 |
|
|
|
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 using frame data timing |
|
func _activate_hitbox(): |
|
if not _hitbox: |
|
return |
|
|
|
# Update owner reference in case it changed |
|
_hitbox.owner_entity = owner_character |
|
|
|
# STARTUP PHASE - Wait before activating hitbox (wind-up) |
|
var startup = weapon_data.startup_time if weapon_data else 0.15 |
|
if startup > 0: |
|
await get_tree().create_timer(startup).timeout |
|
|
|
if not _hitbox or not is_instance_valid(_hitbox): |
|
return |
|
|
|
# ACTIVE PHASE - Hitbox is on, can deal damage |
|
_hitbox.activate() |
|
|
|
var active = weapon_data.active_time if weapon_data else 0.2 |
|
await get_tree().create_timer(active).timeout |
|
|
|
# RECOVERY PHASE - Hitbox off (recovery time is remaining cooldown) |
|
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
|
|
|