Damage numbers and knockback
This commit is contained in:
@@ -11,6 +11,7 @@ hand_type = 1
|
|||||||
damage = 8.0
|
damage = 8.0
|
||||||
attack_range = 2.5
|
attack_range = 2.5
|
||||||
attack_cooldown = 0.8
|
attack_cooldown = 0.8
|
||||||
|
knockback_force = 15.0
|
||||||
can_block = true
|
can_block = true
|
||||||
block_reduction = 0.7
|
block_reduction = 0.7
|
||||||
mesh_scene = ExtResource("2")
|
mesh_scene = ExtResource("2")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ description = "A simple iron sword. Good for close combat."
|
|||||||
damage = 15.0
|
damage = 15.0
|
||||||
attack_range = 3.5
|
attack_range = 3.5
|
||||||
attack_cooldown = 0.6
|
attack_cooldown = 0.6
|
||||||
|
knockback_force = 12.0
|
||||||
attack_animation = "Attack1"
|
attack_animation = "Attack1"
|
||||||
mesh_scene = ExtResource("2")
|
mesh_scene = ExtResource("2")
|
||||||
pickup_radius = 1.5
|
pickup_radius = 1.5
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func _enter_tree():
|
|||||||
## Take damage from an attacker
|
## Take damage from an attacker
|
||||||
## Should only be called on the server for authority
|
## Should only be called on the server for authority
|
||||||
@rpc("any_peer", "reliable")
|
@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
|
# Only server can process damage
|
||||||
if not multiplayer.is_server():
|
if not multiplayer.is_server():
|
||||||
return
|
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)
|
# Apply blocking reduction if applicable (duck typing - check if method exists)
|
||||||
var final_damage = amount
|
var final_damage = amount
|
||||||
|
var final_knockback = knockback
|
||||||
|
var was_blocked = false
|
||||||
if has_method("get_block_reduction"):
|
if has_method("get_block_reduction"):
|
||||||
var block_reduction = call("get_block_reduction")
|
var block_reduction = call("get_block_reduction")
|
||||||
if block_reduction > 0.0:
|
if block_reduction > 0.0:
|
||||||
final_damage = amount * (1.0 - block_reduction)
|
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)")
|
print("Blocked! Damage reduced from ", amount, " to ", final_damage, " (", block_reduction * 100, "% reduction)")
|
||||||
|
|
||||||
var old_health = current_health
|
var old_health = current_health
|
||||||
@@ -49,6 +53,18 @@ func take_damage(amount: float, attacker_id: int = -1):
|
|||||||
rpc("sync_health", current_health)
|
rpc("sync_health", current_health)
|
||||||
health_changed.emit(old_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:
|
if current_health <= 0:
|
||||||
_die(attacker_id)
|
_die(attacker_id)
|
||||||
|
|
||||||
@@ -123,3 +139,36 @@ func set_respawn_point(point: Vector3):
|
|||||||
## Get health percentage (0.0 to 1.0)
|
## Get health percentage (0.0 to 1.0)
|
||||||
func get_health_percent() -> float:
|
func get_health_percent() -> float:
|
||||||
return current_health / max_health if max_health > 0 else 0.0
|
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 we're the server, apply damage directly
|
||||||
if multiplayer.is_server():
|
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:
|
else:
|
||||||
# Otherwise, request server to apply damage
|
# 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
|
break # Only hit one target per attack
|
||||||
|
|
||||||
## Check if weapon can 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:
|
if hit_body != self and hit_body is BaseUnit:
|
||||||
var attacker_id = multiplayer.get_unique_id()
|
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():
|
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:
|
else:
|
||||||
# Otherwise, request server to apply damage
|
# 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
|
break # Only hit one target per attack
|
||||||
|
|
||||||
## Server-side damage application
|
## Server-side damage application
|
||||||
@rpc("any_peer", "reliable")
|
@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():
|
if not multiplayer.is_server():
|
||||||
return
|
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)
|
var target = players_container.get_node(target_name)
|
||||||
if target and target is BaseUnit:
|
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
|
## Health display and callbacks
|
||||||
func _create_health_ui():
|
func _create_health_ui():
|
||||||
@@ -520,6 +520,36 @@ func _reset_dash_rotation(rotation_value: float):
|
|||||||
if _body:
|
if _body:
|
||||||
_body.rotation.x = rotation_value
|
_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
|
## Weapon System
|
||||||
func _setup_weapon_pickup_area():
|
func _setup_weapon_pickup_area():
|
||||||
# Create an Area3D to detect nearby weapons
|
# 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_range: float = 3.0
|
||||||
@export var attack_cooldown: float = 0.5
|
@export var attack_cooldown: float = 0.5
|
||||||
@export var attack_animation: String = "Attack1" # Animation to play when attacking
|
@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_category("Defense Stats")
|
||||||
@export var can_block: bool = false
|
@export var can_block: bool = false
|
||||||
|
|||||||
Reference in New Issue
Block a user