Damage numbers and knockback

Dashfix
Twirpytherobot 3 weeks ago
parent 61446a0bcd
commit c7f946a9e2
  1. 1
      level/resources/weapon_shield.tres
  2. 1
      level/resources/weapon_sword.tres
  3. 51
      level/scripts/base_unit.gd
  4. 16
      level/scripts/base_weapon.gd
  5. 86
      level/scripts/damage_number.gd
  6. 1
      level/scripts/damage_number.gd.uid
  7. 40
      level/scripts/player.gd
  8. 1
      level/scripts/weapon_data.gd

@ -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

@ -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

Loading…
Cancel
Save