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.
174 lines
5.0 KiB
174 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 |
|
|
|
# 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
|
|
|