MultiplayerFighter
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.

175 lines
5.0 KiB

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
3 weeks ago
# Apply blocking reduction if applicable (duck typing - check if method exists)
var final_damage = amount
var final_knockback = knockback
var was_blocked = false
3 weeks ago
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
3 weeks ago
print("Blocked! Damage reduced from ", amount, " to ", final_damage, " (", block_reduction * 100, "% reduction)")
var old_health = current_health
3 weeks ago
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