Damage numbers and knockback
This commit is contained in:
@@ -26,7 +26,7 @@ func _enter_tree():
|
||||
## 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):
|
||||
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
|
||||
@@ -36,10 +36,14 @@ func take_damage(amount: float, attacker_id: int = -1):
|
||||
|
||||
# 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
|
||||
@@ -49,6 +53,18 @@ func take_damage(amount: float, attacker_id: int = -1):
|
||||
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)
|
||||
|
||||
@@ -123,3 +139,36 @@ func set_respawn_point(point: Vector3):
|
||||
## 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
|
||||
|
||||
@@ -80,10 +80,22 @@ func _find_and_damage_targets():
|
||||
|
||||
# If we're the server, apply damage directly
|
||||
if multiplayer.is_server():
|
||||
owner_character._server_apply_damage(hit_body.name, weapon_data.damage, attacker_id)
|
||||
owner_character._server_apply_damage(
|
||||
hit_body.name,
|
||||
weapon_data.damage,
|
||||
attacker_id,
|
||||
weapon_data.knockback_force,
|
||||
owner_character.global_position
|
||||
)
|
||||
else:
|
||||
# Otherwise, request server to apply damage
|
||||
owner_character.rpc_id(1, "_server_apply_damage", hit_body.name, weapon_data.damage, attacker_id)
|
||||
owner_character.rpc_id(1, "_server_apply_damage",
|
||||
hit_body.name,
|
||||
weapon_data.damage,
|
||||
attacker_id,
|
||||
weapon_data.knockback_force,
|
||||
owner_character.global_position
|
||||
)
|
||||
break # Only hit one target per attack
|
||||
|
||||
## Check if weapon can attack
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
extends Label3D
|
||||
class_name DamageNumber
|
||||
|
||||
## Floating damage number that animates upward and fades out
|
||||
|
||||
@export var float_speed: float = 3.0
|
||||
@export var lifetime: float = 1.8
|
||||
@export var spread: float = 0.7 # Random horizontal spread
|
||||
|
||||
var _elapsed: float = 0.0
|
||||
var _velocity: Vector3 = Vector3.ZERO
|
||||
var _initial_scale: float = 1.5
|
||||
|
||||
func _ready():
|
||||
# Random horizontal spread
|
||||
_velocity = Vector3(
|
||||
randf_range(-spread, spread),
|
||||
float_speed,
|
||||
randf_range(-spread, spread)
|
||||
)
|
||||
|
||||
# Billboard mode so it always faces camera
|
||||
billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
|
||||
# Start with full opacity
|
||||
modulate.a = 1.0
|
||||
|
||||
# Larger initial scale for impact
|
||||
scale = Vector3.ONE * 0.5
|
||||
|
||||
func _process(delta):
|
||||
_elapsed += delta
|
||||
|
||||
# Move upward with velocity
|
||||
position += _velocity * delta
|
||||
_velocity.y -= delta * 1.5 # Slight gravity effect
|
||||
|
||||
# Fade out over lifetime (keep visible longer, then quick fade)
|
||||
var fade_progress = _elapsed / lifetime
|
||||
if fade_progress < 0.7:
|
||||
modulate.a = 1.0
|
||||
else:
|
||||
var fade_amount = (fade_progress - 0.7) / 0.3
|
||||
modulate.a = 1.0 - fade_amount
|
||||
|
||||
# Pop animation - quick scale up, then gentle scale down
|
||||
if fade_progress < 0.15:
|
||||
# Quick pop up
|
||||
var scale_progress = fade_progress / 0.15
|
||||
var target_scale = _initial_scale * (0.5 + (0.5 * scale_progress))
|
||||
scale = Vector3.ONE * target_scale
|
||||
elif fade_progress < 0.3:
|
||||
# Slight bounce/overshoot
|
||||
var bounce_progress = (fade_progress - 0.15) / 0.15
|
||||
var overshoot = sin(bounce_progress * PI) * 0.15
|
||||
var target_scale = _initial_scale * (1.0 + overshoot)
|
||||
scale = Vector3.ONE * target_scale
|
||||
else:
|
||||
# Gentle shrink
|
||||
var shrink_progress = (fade_progress - 0.3) / 0.7
|
||||
var target_scale = _initial_scale * (1.0 - (shrink_progress * 0.3))
|
||||
scale = Vector3.ONE * target_scale
|
||||
|
||||
# Delete when lifetime expired
|
||||
if _elapsed >= lifetime:
|
||||
queue_free()
|
||||
|
||||
## Set the damage number text and color
|
||||
func set_damage(amount: float, was_blocked: bool = false):
|
||||
text = str(int(amount))
|
||||
|
||||
if was_blocked:
|
||||
# Bright yellow/orange for blocked damage with strong contrast
|
||||
modulate = Color(1.0, 0.9, 0.0) # Bright yellow
|
||||
outline_modulate = Color(0.3, 0.15, 0.0) # Dark brown/black outline
|
||||
else:
|
||||
# Vibrant red for normal damage with black outline
|
||||
modulate = Color(1.0, 0.15, 0.15) # Bright red
|
||||
outline_modulate = Color(0.1, 0.0, 0.0) # Almost black outline
|
||||
|
||||
# Much larger and bolder
|
||||
outline_size = 16
|
||||
font_size = 72
|
||||
|
||||
# Add depth with larger outline
|
||||
pixel_size = 0.002 # Slightly larger pixels for better visibility
|
||||
@@ -0,0 +1 @@
|
||||
uid://c563plaqxfk5d
|
||||
+35
-5
@@ -354,17 +354,17 @@ func _perform_attack():
|
||||
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 we're the server, apply damage directly (default unarmed knockback)
|
||||
if multiplayer.is_server():
|
||||
_server_apply_damage(hit_body.name, attack_damage, attacker_id)
|
||||
_server_apply_damage(hit_body.name, attack_damage, attacker_id, 5.0, global_position)
|
||||
else:
|
||||
# Otherwise, request server to apply damage
|
||||
rpc_id(1, "_server_apply_damage", hit_body.name, attack_damage, attacker_id)
|
||||
rpc_id(1, "_server_apply_damage", hit_body.name, attack_damage, attacker_id, 5.0, global_position)
|
||||
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):
|
||||
func _server_apply_damage(target_name: String, damage: float, attacker_id: int, knockback: float = 0.0, attacker_pos: Vector3 = Vector3.ZERO):
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
|
||||
@@ -379,7 +379,7 @@ func _server_apply_damage(target_name: String, damage: float, attacker_id: int):
|
||||
|
||||
var target = players_container.get_node(target_name)
|
||||
if target and target is BaseUnit:
|
||||
target.take_damage(damage, attacker_id)
|
||||
target.take_damage(damage, attacker_id, knockback, attacker_pos)
|
||||
|
||||
## Health display and callbacks
|
||||
func _create_health_ui():
|
||||
@@ -520,6 +520,36 @@ func _reset_dash_rotation(rotation_value: float):
|
||||
if _body:
|
||||
_body.rotation.x = rotation_value
|
||||
|
||||
## Override hurt animation from BaseUnit
|
||||
func _play_hurt_animation():
|
||||
if _body and _body.animation_player:
|
||||
# Try to play a hurt/hit animation if it exists
|
||||
if _body.animation_player.has_animation("Hurt"):
|
||||
_body.animation_player.play("Hurt")
|
||||
elif _body.animation_player.has_animation("Hit"):
|
||||
_body.animation_player.play("Hit")
|
||||
elif _body.animation_player.has_animation("TakeDamage"):
|
||||
_body.animation_player.play("TakeDamage")
|
||||
else:
|
||||
# Fallback: briefly flash the character red
|
||||
_flash_hurt()
|
||||
|
||||
## Flash effect when no hurt animation exists
|
||||
func _flash_hurt():
|
||||
if not _body:
|
||||
return
|
||||
|
||||
# Store original modulate
|
||||
var original_modulate = _body.modulate
|
||||
|
||||
# Flash red
|
||||
_body.modulate = Color(1.5, 0.5, 0.5, 1.0)
|
||||
|
||||
# Return to normal after a brief moment
|
||||
await get_tree().create_timer(0.15).timeout
|
||||
if _body:
|
||||
_body.modulate = original_modulate
|
||||
|
||||
## Weapon System
|
||||
func _setup_weapon_pickup_area():
|
||||
# Create an Area3D to detect nearby weapons
|
||||
|
||||
@@ -16,6 +16,7 @@ enum Hand { MAIN_HAND, OFF_HAND, TWO_HAND }
|
||||
@export var attack_range: float = 3.0
|
||||
@export var attack_cooldown: float = 0.5
|
||||
@export var attack_animation: String = "Attack1" # Animation to play when attacking
|
||||
@export var knockback_force: float = 8.0 # How much to push the target back
|
||||
|
||||
@export_category("Defense Stats")
|
||||
@export var can_block: bool = false
|
||||
|
||||
Reference in New Issue
Block a user