|
|
|
|
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
|
|
|
|
|
|
Add comprehensive UI system with action bar, unit frame, and character sheet
- Created UI system in level/ui/ folder
- Action bar with 12 ability slots (Attack, Block, Dash, Jump + 8 expansion slots)
- Unit frame showing player portrait with health bar
- Character sheet/spellbook (toggle with Tab) displaying stats, weapons, and abilities
- Tab hint indicator showing how to open character sheet
- Custom theme with golden borders and dark backgrounds
- UI Components:
- HUD Manager autoload handles all UI initialization and player connections
- Ability buttons with cooldown overlay and keybind display
- Real-time health and cooldown tracking via signals
- Scrollable character sheet with two-page layout
- Player Integration:
- Added UI signals: dash_cooldown_updated, attack_cooldown_updated, weapon_equipped_changed
- Mouse capture system (Escape to toggle, click to recapture)
- Synced attack cooldown from weapons to player for UI tracking
- Updated level.gd to connect local player to HUD Manager
- Updated CLAUDE.md with git commit guidelines
3 weeks ago
|
|
|
# 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 entire animation duration
|
|
|
|
|
_hitbox.activate()
|
|
|
|
|
|
|
|
|
|
# Keep active for the full attack cooldown
|
|
|
|
|
var duration = weapon_data.attack_cooldown if weapon_data else 0.5
|
|
|
|
|
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
|