extends CharacterBody3D class_name BaseUnit ## Base class for all units (players, enemies, NPCs) in the game ## Provides common functionality like health management and taking damage signal health_changed(old_health: float, new_health: float) signal died(killer_id: int) signal respawned() @export var max_health: float = 100.0 @export var can_respawn: bool = true @export var respawn_delay: float = 3.0 var current_health: float = 100.0 var is_dead: bool = false var _respawn_point: Vector3 = Vector3.ZERO func _ready(): current_health = max_health _respawn_point = global_position func _enter_tree(): set_multiplayer_authority(str(name).to_int()) ## Take damage from an attacker ## Should only be called on the server for authority @rpc("any_peer", "reliable") func take_damage(amount: float, attacker_id: int = -1, knockback: float = 0.0, attacker_pos: Vector3 = Vector3.ZERO): # Only server can process damage if not multiplayer.is_server(): return if is_dead: return # Apply blocking reduction if applicable (duck typing - check if method exists) var final_damage = amount var final_knockback = knockback var was_blocked = false if has_method("get_block_reduction"): var block_reduction = call("get_block_reduction") if block_reduction > 0.0: final_damage = amount * (1.0 - block_reduction) final_knockback = knockback * (1.0 - block_reduction) # Reduce knockback too was_blocked = true print("Blocked! Damage reduced from ", amount, " to ", final_damage, " (", block_reduction * 100, "% reduction)") var old_health = current_health current_health = max(0, current_health - final_damage) # Broadcast health change to all clients rpc("sync_health", current_health) health_changed.emit(old_health, current_health) # Show damage number on all clients rpc("_show_damage_number", final_damage, was_blocked) # Play hurt animation on all clients rpc("_play_hurt_animation") # Apply knockback if applicable if final_knockback > 0.0 and attacker_pos != Vector3.ZERO: var knockback_dir = (global_position - attacker_pos).normalized() knockback_dir.y = 0.3 # Slight upward component rpc("_apply_knockback", knockback_dir * final_knockback) if current_health <= 0: _die(attacker_id) ## Heal the unit @rpc("any_peer", "reliable") func heal(amount: float): if not multiplayer.is_server(): return if is_dead: return var old_health = current_health current_health = min(max_health, current_health + amount) rpc("sync_health", current_health) health_changed.emit(old_health, current_health) ## Sync health across all clients @rpc("any_peer", "call_local", "reliable") func sync_health(new_health: float): var old_health = current_health current_health = new_health health_changed.emit(old_health, current_health) ## Handle death func _die(killer_id: int): if is_dead: return is_dead = true died.emit(killer_id) rpc("sync_death", killer_id) if can_respawn and multiplayer.is_server(): await get_tree().create_timer(respawn_delay).timeout _respawn() ## Sync death state to all clients @rpc("any_peer", "call_local", "reliable") func sync_death(killer_id: int): is_dead = true died.emit(killer_id) # Subclasses should override to add visual effects, disable collision, etc. ## Respawn the unit func _respawn(): if not multiplayer.is_server(): return is_dead = false current_health = max_health global_position = _respawn_point velocity = Vector3.ZERO rpc("sync_respawn", _respawn_point) respawned.emit() ## Sync respawn to all clients @rpc("any_peer", "call_local", "reliable") func sync_respawn(spawn_pos: Vector3): is_dead = false current_health = max_health global_position = spawn_pos velocity = Vector3.ZERO respawned.emit() ## Set the respawn point func set_respawn_point(point: Vector3): _respawn_point = point ## Get health percentage (0.0 to 1.0) func get_health_percent() -> float: return current_health / max_health if max_health > 0 else 0.0 ## Show floating damage number @rpc("any_peer", "call_local", "reliable") func _show_damage_number(damage: float, was_blocked: bool): print("Spawning damage number: ", damage, " blocked: ", was_blocked, " on peer: ", multiplayer.get_unique_id()) # Create damage number label var damage_label = Label3D.new() var damage_script = load("res://level/scripts/damage_number.gd") damage_label.set_script(damage_script) # Position above the character damage_label.position = global_position + Vector3(0, 2.5, 0) # Add to scene (not as child of this node, so it doesn't get affected by player movement) get_tree().get_current_scene().add_child(damage_label) # Set damage value and color damage_label.call("set_damage", damage, was_blocked) ## Apply knockback force @rpc("any_peer", "call_local", "reliable") func _apply_knockback(knockback_velocity: Vector3): # Only applies to CharacterBody3D (players) if self is CharacterBody3D: velocity += knockback_velocity print("Applied knockback: ", knockback_velocity) ## Play hurt animation (override in subclasses) @rpc("any_peer", "call_local", "reliable") func _play_hurt_animation(): # Subclasses should implement this pass