Health and attacks!
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
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):
|
||||
# Only server can process damage
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
|
||||
if is_dead:
|
||||
return
|
||||
|
||||
var old_health = current_health
|
||||
current_health = max(0, current_health - amount)
|
||||
|
||||
# Broadcast health change to all clients
|
||||
rpc("sync_health", current_health)
|
||||
health_changed.emit(old_health, current_health)
|
||||
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
uid://nhw7amcyksft
|
||||
+199
-12
@@ -1,4 +1,4 @@
|
||||
extends CharacterBody3D
|
||||
extends BaseUnit
|
||||
class_name Character
|
||||
|
||||
const NORMAL_SPEED = 6.0
|
||||
@@ -8,6 +8,7 @@ const JUMP_VELOCITY = 10
|
||||
enum SkinColor { BLUE, YELLOW, GREEN, RED }
|
||||
|
||||
@onready var nickname: Label3D = $PlayerNick/Nickname
|
||||
@onready var health_label: Label3D = null # Optional 3D health label
|
||||
|
||||
@export_category("Objects")
|
||||
@export var _body: Node3D = null
|
||||
@@ -25,18 +26,45 @@ enum SkinColor { BLUE, YELLOW, GREEN, RED }
|
||||
@onready var _limbs_head_mesh: MeshInstance3D = get_node("3DGodotRobot/RobotArmature/Skeleton3D/Llimbs and head")
|
||||
|
||||
var _current_speed: float
|
||||
var _respawn_point = Vector3(0, 5, 0)
|
||||
var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
|
||||
|
||||
# Attack system
|
||||
@export var attack_damage: float = 10.0
|
||||
@export var attack_range: float = 3.0
|
||||
@export var attack_cooldown: float = 0.5
|
||||
var _attack_timer: float = 0.0
|
||||
|
||||
func _enter_tree():
|
||||
set_multiplayer_authority(str(name).to_int())
|
||||
super._enter_tree()
|
||||
$SpringArmOffset/SpringArm3D/Camera3D.current = is_multiplayer_authority()
|
||||
|
||||
func _ready():
|
||||
return
|
||||
super._ready()
|
||||
set_respawn_point(Vector3(0, 5, 0))
|
||||
|
||||
# Try to get optional health label
|
||||
if has_node("PlayerNick/HealthLabel"):
|
||||
health_label = get_node("PlayerNick/HealthLabel")
|
||||
|
||||
# Connect health signals
|
||||
health_changed.connect(_on_health_changed)
|
||||
died.connect(_on_died)
|
||||
respawned.connect(_on_respawned)
|
||||
|
||||
# Update health display
|
||||
_update_health_display()
|
||||
|
||||
# Create 2D UI health bar for local player
|
||||
if is_multiplayer_authority():
|
||||
_create_health_ui()
|
||||
|
||||
func _physics_process(delta):
|
||||
if not is_multiplayer_authority(): return
|
||||
# Check if multiplayer is ready
|
||||
if multiplayer.multiplayer_peer == null:
|
||||
return
|
||||
|
||||
if not is_multiplayer_authority():
|
||||
return
|
||||
|
||||
var current_scene = get_tree().get_current_scene()
|
||||
if current_scene and current_scene.has_method("is_chat_visible") and current_scene.is_chat_visible() and is_on_floor():
|
||||
@@ -57,10 +85,24 @@ func _physics_process(delta):
|
||||
move_and_slide()
|
||||
_body.animate(velocity)
|
||||
|
||||
func _process(_delta):
|
||||
if not is_multiplayer_authority(): return
|
||||
func _process(delta):
|
||||
# Check if multiplayer is ready
|
||||
if multiplayer.multiplayer_peer == null:
|
||||
return
|
||||
|
||||
if not is_multiplayer_authority():
|
||||
return
|
||||
|
||||
_check_fall_and_respawn()
|
||||
|
||||
# Update attack cooldown
|
||||
if _attack_timer > 0:
|
||||
_attack_timer -= delta
|
||||
|
||||
# Handle attack input
|
||||
if Input.is_action_just_pressed("attack") and _attack_timer <= 0 and not is_dead:
|
||||
_perform_attack()
|
||||
|
||||
func freeze():
|
||||
velocity.x = 0
|
||||
velocity.z = 0
|
||||
@@ -98,13 +140,10 @@ func is_running() -> bool:
|
||||
return false
|
||||
|
||||
func _check_fall_and_respawn():
|
||||
if global_transform.origin.y < -15.0:
|
||||
if global_transform.origin.y < -15.0 and multiplayer.is_server():
|
||||
# Use BaseUnit's respawn system
|
||||
_respawn()
|
||||
|
||||
func _respawn():
|
||||
global_transform.origin = _respawn_point
|
||||
velocity = Vector3.ZERO
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func change_nick(new_nick: String):
|
||||
if nickname:
|
||||
@@ -134,3 +173,151 @@ func set_mesh_texture(mesh_instance: MeshInstance3D, texture: CompressedTexture2
|
||||
var new_material := material
|
||||
new_material.albedo_texture = texture
|
||||
mesh_instance.set_surface_override_material(0, new_material)
|
||||
|
||||
## Attack system
|
||||
func _perform_attack():
|
||||
if not is_multiplayer_authority() or is_dead:
|
||||
return
|
||||
|
||||
_attack_timer = attack_cooldown
|
||||
|
||||
# Find nearest enemy in range
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
var query = PhysicsShapeQueryParameters3D.new()
|
||||
var sphere = SphereShape3D.new()
|
||||
sphere.radius = attack_range
|
||||
query.shape = sphere
|
||||
query.transform = global_transform
|
||||
query.collision_mask = 1 # Player layer
|
||||
|
||||
var results = space_state.intersect_shape(query)
|
||||
|
||||
for result in results:
|
||||
var hit_body = result["collider"]
|
||||
if hit_body != self and hit_body is BaseUnit:
|
||||
var attacker_id = multiplayer.get_unique_id()
|
||||
|
||||
# If we're the server, apply damage directly
|
||||
if multiplayer.is_server():
|
||||
_server_apply_damage(hit_body.name, attack_damage, attacker_id)
|
||||
else:
|
||||
# Otherwise, request server to apply damage
|
||||
rpc_id(1, "_server_apply_damage", hit_body.name, attack_damage, attacker_id)
|
||||
break # Only hit one target per attack
|
||||
|
||||
## Server-side damage application
|
||||
@rpc("any_peer", "reliable")
|
||||
func _server_apply_damage(target_name: String, damage: float, attacker_id: int):
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
|
||||
# Get the target from the players container
|
||||
var level = get_tree().get_current_scene()
|
||||
if not level or not level.has_node("PlayersContainer"):
|
||||
return
|
||||
|
||||
var players_container = level.get_node("PlayersContainer")
|
||||
if not players_container.has_node(target_name):
|
||||
return
|
||||
|
||||
var target = players_container.get_node(target_name)
|
||||
if target and target is BaseUnit:
|
||||
target.take_damage(damage, attacker_id)
|
||||
|
||||
## Health display and callbacks
|
||||
func _create_health_ui():
|
||||
# Create a 2D UI for the local player's health
|
||||
var canvas = CanvasLayer.new()
|
||||
canvas.name = "HealthUI"
|
||||
add_child(canvas)
|
||||
|
||||
# Health bar background
|
||||
var health_bg = ColorRect.new()
|
||||
health_bg.name = "HealthBG"
|
||||
health_bg.color = Color(0.2, 0.2, 0.2, 0.8)
|
||||
health_bg.position = Vector2(20, 20)
|
||||
health_bg.size = Vector2(200, 30)
|
||||
canvas.add_child(health_bg)
|
||||
|
||||
# Health bar (current health)
|
||||
var health_bar = ColorRect.new()
|
||||
health_bar.name = "HealthBar"
|
||||
health_bar.color = Color(0.0, 0.8, 0.0, 1.0) # Green
|
||||
health_bar.position = Vector2(22, 22)
|
||||
health_bar.size = Vector2(196, 26)
|
||||
canvas.add_child(health_bar)
|
||||
|
||||
# Health text
|
||||
var health_text = Label.new()
|
||||
health_text.name = "HealthText"
|
||||
health_text.position = Vector2(20, 20)
|
||||
health_text.size = Vector2(200, 30)
|
||||
health_text.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
health_text.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
health_text.add_theme_color_override("font_color", Color.WHITE)
|
||||
health_text.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||
health_text.add_theme_constant_override("outline_size", 2)
|
||||
health_text.text = "HP: %d/%d" % [int(current_health), int(max_health)]
|
||||
canvas.add_child(health_text)
|
||||
|
||||
func _update_health_display():
|
||||
# Update 3D label if it exists
|
||||
if health_label:
|
||||
health_label.text = "HP: %d/%d" % [int(current_health), int(max_health)]
|
||||
|
||||
# Update 2D UI for local player
|
||||
if is_multiplayer_authority() and has_node("HealthUI"):
|
||||
var health_bar = get_node_or_null("HealthUI/HealthBar")
|
||||
var health_text = get_node_or_null("HealthUI/HealthText")
|
||||
|
||||
if health_bar:
|
||||
var health_percent = get_health_percent()
|
||||
health_bar.size.x = 196 * health_percent
|
||||
|
||||
# Change color based on health
|
||||
if health_percent > 0.6:
|
||||
health_bar.color = Color(0.0, 0.8, 0.0) # Green
|
||||
elif health_percent > 0.3:
|
||||
health_bar.color = Color(1.0, 0.8, 0.0) # Yellow
|
||||
else:
|
||||
health_bar.color = Color(0.8, 0.0, 0.0) # Red
|
||||
|
||||
if health_text:
|
||||
health_text.text = "HP: %d/%d" % [int(current_health), int(max_health)]
|
||||
|
||||
func _on_health_changed(_old_health: float, _new_health: float):
|
||||
_update_health_display()
|
||||
|
||||
func _on_died(killer_id: int):
|
||||
# Disable player when dead
|
||||
set_physics_process(false)
|
||||
set_process(false)
|
||||
|
||||
# Visual feedback - could add death animation here
|
||||
if _body:
|
||||
_body.visible = false
|
||||
|
||||
# Print death message
|
||||
var killer_name = "Unknown"
|
||||
if Network.players.has(killer_id):
|
||||
killer_name = Network.players[killer_id]["nick"]
|
||||
|
||||
if is_multiplayer_authority():
|
||||
print("You were killed by ", killer_name)
|
||||
|
||||
# Show death message on UI
|
||||
if has_node("HealthUI/HealthText"):
|
||||
get_node("HealthUI/HealthText").text = "DEAD - Respawning..."
|
||||
|
||||
func _on_respawned():
|
||||
# Re-enable player
|
||||
set_physics_process(true)
|
||||
set_process(true)
|
||||
|
||||
if _body:
|
||||
_body.visible = true
|
||||
|
||||
_update_health_display()
|
||||
|
||||
if is_multiplayer_authority():
|
||||
print("You respawned!")
|
||||
|
||||
@@ -7,6 +7,9 @@ const MOUSE_SENSIBILITY: float = 0.005
|
||||
@export var _spring_arm: SpringArm3D = null
|
||||
|
||||
func _unhandled_input(_event) -> void:
|
||||
# Check if multiplayer is ready
|
||||
if multiplayer.multiplayer_peer == null:
|
||||
return
|
||||
|
||||
if (_event is InputEventMouseMotion) and is_multiplayer_authority():
|
||||
rotate_y(-_event.relative.x * MOUSE_SENSIBILITY)
|
||||
|
||||
Reference in New Issue
Block a user