made some adjustments to brights of rooms

This commit is contained in:
2026-01-25 18:28:30 +01:00
parent 7abadb92a9
commit e7204b92d2
9 changed files with 476 additions and 219 deletions

View File

@@ -44,6 +44,7 @@ var grab_tap_threshold = 0.2 # Seconds to distinguish tap from hold
var is_shielding: bool = false # True when holding grab with shield in offhand and nothing to grab/lift
var is_reviving: bool = false # True when holding grab on a corpse and charging revive
var revive_charge: float = 0.0
var was_reviving_last_frame: bool = false
const REVIVE_DURATION: float = 2.0 # Seconds holding grab to revive
var last_movement_direction = Vector2.DOWN # Track last direction for placing objects
var push_axis = Vector2.ZERO # Locked axis for pushing/pulling
@@ -1782,165 +1783,180 @@ func _physics_process(delta):
else:
spell_charge_tint_pulse_time = 0.0
# Revive charge visuals - same as healing (healing_charging on AnimationIncantation)
if is_reviving:
was_reviving_last_frame = true
if has_node("AnimationIncantation") and not is_charging_spell:
$AnimationIncantation.play("healing_charging")
elif was_reviving_last_frame:
was_reviving_last_frame = false
_stop_spell_charge_incantation()
if is_local_player and is_multiplayer_authority():
# Skip all input and logic if dead
# When dead: only corpse knockback friction + sync; no input or other logic
if is_dead:
return
# Handle knockback timer (always handle knockback, even when controls are disabled)
if is_knocked_back:
knockback_time += delta
if knockback_time >= knockback_duration:
is_knocked_back = false
knockback_time = 0.0
# Update movement lock timer (for bow release)
if movement_lock_timer > 0.0:
movement_lock_timer -= delta
if movement_lock_timer <= 0.0:
movement_lock_timer = 0.0
# Update direction lock timer (for attacks)
if direction_lock_timer > 0.0:
direction_lock_timer -= delta
if direction_lock_timer <= 0.0:
direction_lock_timer = 0.0
# Update damage direction lock timer (block facing change when taking damage)
if damage_direction_lock_timer > 0.0:
damage_direction_lock_timer -= delta
if damage_direction_lock_timer <= 0.0:
damage_direction_lock_timer = 0.0
if shield_block_cooldown_timer > 0.0:
shield_block_cooldown_timer -= delta
if shield_block_cooldown_timer <= 0.0:
shield_block_cooldown_timer = 0.0
# Update bow charge tint (when fully charged)
if is_charging_bow:
var charge_time = (Time.get_ticks_msec() / 1000.0) - bow_charge_start_time
# Smooth curve: charge from 0.2s to 1.0s
var charge_progress = clamp((charge_time - 0.2) / 0.8, 0.0, 1.0) # 0.0 at 0.2s, 1.0 at 1.0s
if is_knocked_back:
knockback_time += delta
if knockback_time >= knockback_duration:
is_knocked_back = false
knockback_time = 0.0
else:
velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
else:
# Handle knockback timer (always handle knockback, even when controls are disabled)
if is_knocked_back:
knockback_time += delta
if knockback_time >= knockback_duration:
is_knocked_back = false
knockback_time = 0.0
# Update tint pulse timer when fully charged
if charge_progress >= 1.0:
# Use fast pulse speed when fully charged
bow_charge_tint_pulse_time += delta * bow_charge_tint_pulse_speed_charged
_apply_bow_charge_tint()
# Update movement lock timer (for bow release)
if movement_lock_timer > 0.0:
movement_lock_timer -= delta
if movement_lock_timer <= 0.0:
movement_lock_timer = 0.0
# Update direction lock timer (for attacks)
if direction_lock_timer > 0.0:
direction_lock_timer -= delta
if direction_lock_timer <= 0.0:
direction_lock_timer = 0.0
# Update damage direction lock timer (block facing change when taking damage)
if damage_direction_lock_timer > 0.0:
damage_direction_lock_timer -= delta
if damage_direction_lock_timer <= 0.0:
damage_direction_lock_timer = 0.0
if shield_block_cooldown_timer > 0.0:
shield_block_cooldown_timer -= delta
if shield_block_cooldown_timer <= 0.0:
shield_block_cooldown_timer = 0.0
# Update bow charge tint (when fully charged)
if is_charging_bow:
var charge_time = (Time.get_ticks_msec() / 1000.0) - bow_charge_start_time
# Smooth curve: charge from 0.2s to 1.0s
var charge_progress = clamp((charge_time - 0.2) / 0.8, 0.0, 1.0) # 0.0 at 0.2s, 1.0 at 1.0s
# Update tint pulse timer when fully charged
if charge_progress >= 1.0:
# Use fast pulse speed when fully charged
bow_charge_tint_pulse_time += delta * bow_charge_tint_pulse_speed_charged
_apply_bow_charge_tint()
else:
bow_charge_tint_pulse_time = 0.0
_clear_bow_charge_tint()
else:
# Reset pulse timer when not charging
bow_charge_tint_pulse_time = 0.0
_clear_bow_charge_tint()
else:
# Reset pulse timer when not charging
bow_charge_tint_pulse_time = 0.0
_clear_bow_charge_tint()
# Update burn debuff (works on both authority and clients)
if burn_debuff_timer > 0.0:
burn_debuff_timer -= delta
# Only deal damage on authority (where we have authority)
if is_multiplayer_authority():
burn_damage_timer += delta
# Update burn debuff (works on both authority and clients)
if burn_debuff_timer > 0.0:
burn_debuff_timer -= delta
# Deal burn damage every second (no knockback)
if burn_damage_timer >= 1.0:
# Only deal damage on authority (where we have authority)
if is_multiplayer_authority():
burn_damage_timer += delta
# Deal burn damage every second (no knockback)
if burn_damage_timer >= 1.0:
burn_damage_timer = 0.0
# Deal burn damage directly (no knockback, no animation)
if character_stats:
var old_hp = character_stats.hp
character_stats.modify_health(-burn_debuff_damage_per_second)
if character_stats.hp <= 0:
character_stats.no_health.emit()
character_stats.character_changed.emit(character_stats)
var actual_damage = old_hp - character_stats.hp
print(name, " takes ", actual_damage, " burn damage! Health: ", character_stats.hp, "/", character_stats.maxhp)
# Show damage number for burn damage
_show_damage_number(actual_damage, global_position, false, false, false) # Show burn damage number
# Sync burn damage visual to other clients
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_rpc_to_ready_peers("_sync_damage", [actual_damage, global_position])
# Animate burn visual if it's a sprite (works on both authority and clients)
if burn_debuff_visual and is_instance_valid(burn_debuff_visual):
if burn_debuff_visual is Sprite2D:
var sprite = burn_debuff_visual as Sprite2D
var anim_timer = sprite.get_meta("burn_animation_timer", 0.0)
anim_timer += delta
if anim_timer >= 0.1: # ~10 FPS
anim_timer = 0.0
var frame = sprite.get_meta("burn_animation_frame", 0)
frame = (frame + 1) % 16
sprite.frame = frame
sprite.set_meta("burn_animation_frame", frame)
sprite.set_meta("burn_animation_timer", anim_timer)
# Remove burn debuff when timer expires (works on both authority and clients)
if burn_debuff_timer <= 0.0:
burn_debuff_timer = 0.0
burn_damage_timer = 0.0
# Deal burn damage directly (no knockback, no animation)
if character_stats:
var old_hp = character_stats.hp
character_stats.modify_health(-burn_debuff_damage_per_second)
if character_stats.hp <= 0:
character_stats.no_health.emit()
character_stats.character_changed.emit(character_stats)
var actual_damage = old_hp - character_stats.hp
print(name, " takes ", actual_damage, " burn damage! Health: ", character_stats.hp, "/", character_stats.maxhp)
# Show damage number for burn damage
_show_damage_number(actual_damage, global_position, false, false, false) # Show burn damage number
# Sync burn damage visual to other clients
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_rpc_to_ready_peers("_sync_damage", [actual_damage, global_position])
_remove_burn_debuff()
# Animate burn visual if it's a sprite (works on both authority and clients)
if burn_debuff_visual and is_instance_valid(burn_debuff_visual):
if burn_debuff_visual is Sprite2D:
var sprite = burn_debuff_visual as Sprite2D
var anim_timer = sprite.get_meta("burn_animation_timer", 0.0)
anim_timer += delta
if anim_timer >= 0.1: # ~10 FPS
anim_timer = 0.0
var frame = sprite.get_meta("burn_animation_frame", 0)
frame = (frame + 1) % 16
sprite.frame = frame
sprite.set_meta("burn_animation_frame", frame)
sprite.set_meta("burn_animation_timer", anim_timer)
# Skip input if controls are disabled (e.g., when inventory is open)
# But still allow knockback to continue (handled above)
var skip_input = controls_disabled
if controls_disabled:
if not is_knocked_back:
# Immediately stop movement when controls are disabled (e.g., inventory opened)
velocity = Vector2.ZERO
# Reset animation to IDLE if not in a special state
if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF":
if is_lifting:
_set_animation("IDLE_HOLD")
elif is_pushing:
_set_animation("IDLE_PUSH")
else:
_set_animation("IDLE")
# Remove burn debuff when timer expires (works on both authority and clients)
if burn_debuff_timer <= 0.0:
burn_debuff_timer = 0.0
burn_damage_timer = 0.0
_remove_burn_debuff()
# Skip input if controls are disabled (e.g., when inventory is open)
# But still allow knockback to continue (handled above)
var skip_input = controls_disabled
if controls_disabled:
if not is_knocked_back:
# Immediately stop movement when controls are disabled (e.g., inventory opened)
velocity = Vector2.ZERO
# Reset animation to IDLE if not in a special state
if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF":
if is_lifting:
_set_animation("IDLE_HOLD")
elif is_pushing:
_set_animation("IDLE_PUSH")
else:
_set_animation("IDLE")
# Check if being held by someone
var being_held_by_someone = false
for other_player in get_tree().get_nodes_in_group("player"):
if other_player != self and other_player.held_object == self:
being_held_by_someone = true
being_held_by = other_player
break
if being_held_by_someone:
is_shielding = false
was_shielding_last_frame = false
_update_shield_visibility()
# Handle struggle mechanic
_handle_struggle(delta)
elif is_knocked_back:
is_shielding = false
was_shielding_last_frame = false
_update_shield_visibility()
# During knockback, no input control - just let velocity carry the player
# Apply friction to slow down knockback
velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
elif not is_airborne and not skip_input:
# Normal input handling (only if controls are not disabled)
struggle_time = 0.0 # Reset struggle timer
struggle_direction = Vector2.ZERO
_handle_input()
_handle_movement(delta)
_handle_interactions()
else:
is_shielding = false
was_shielding_last_frame = false
_update_shield_visibility()
# Reset struggle when airborne
struggle_time = 0.0
struggle_direction = Vector2.ZERO
# Update held object positions
if is_lifting:
_update_lifted_object()
elif is_pushing:
_update_pushed_object()
# Sync position, direction, and animation to other clients (unreliable broadcast)
# Check if being held by someone
var being_held_by_someone = false
for other_player in get_tree().get_nodes_in_group("player"):
if other_player != self and other_player.held_object == self:
being_held_by_someone = true
being_held_by = other_player
break
if being_held_by_someone:
is_shielding = false
was_shielding_last_frame = false
_update_shield_visibility()
# Handle struggle mechanic
_handle_struggle(delta)
elif is_knocked_back:
is_shielding = false
was_shielding_last_frame = false
_update_shield_visibility()
# During knockback, no input control - just let velocity carry the player
# Apply friction to slow down knockback
velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
elif not is_airborne and not skip_input:
# Normal input handling (only if controls are not disabled)
struggle_time = 0.0 # Reset struggle timer
struggle_direction = Vector2.ZERO
_handle_input()
_handle_movement(delta)
_handle_interactions()
else:
is_shielding = false
was_shielding_last_frame = false
_update_shield_visibility()
# Reset struggle when airborne
struggle_time = 0.0
struggle_direction = Vector2.ZERO
# Update held object positions
if is_lifting:
_update_lifted_object()
elif is_pushing:
_update_pushed_object()
# Sync position, direction, and animation to other clients (unreliable broadcast)
# Only send RPC if we're in the scene tree and ready to send RPCs (prevents errors when player hasn't spawned on all clients yet)
# On server, also wait for all clients to be ready
if multiplayer.has_multiplayer_peer() and is_inside_tree() and can_send_rpcs:
@@ -4035,7 +4051,7 @@ func _cast_heal_spell(target: Node):
var dungeon_seed: int = 0
if gw and "dungeon_seed" in gw:
dungeon_seed = gw.dungeon_seed
var seed_val = dungeon_seed + hash(target.name) + int(global_position.x) * 31 + int(global_position.y) * 17 + int(Time.get_ticks_msec() / 50)
var seed_val = dungeon_seed + hash(target.name) + int(global_position.x) * 31 + int(global_position.y) * 17 + int(Time.get_ticks_msec() / 50.0)
var rng = RandomNumberGenerator.new()
rng.seed = seed_val
@@ -4052,30 +4068,41 @@ func _cast_heal_spell(target: Node):
if is_crit:
amount = floor(amount * 2.0)
var overheal_chance_pct = 1.0 + lck_val * 0.3
var can_overheal = target.character_stats and target.character_stats.hp <= target.character_stats.maxhp
var is_overheal = can_overheal and rng.randf() * 100.0 < overheal_chance_pct
var is_revive = "is_dead" in target and target.is_dead
var display_amount = int(amount)
var actual_heal = amount
var allow_overheal = false
if is_overheal:
allow_overheal = true
var is_overheal = false
if is_revive:
# Tome revive: no overheal, full amount revives
actual_heal = amount
else:
var cap = 0.0
if target.character_stats:
cap = target.character_stats.maxhp - target.character_stats.hp
actual_heal = min(amount, max(0.0, cap))
var overheal_chance_pct = 1.0 + lck_val * 0.3
var can_overheal = target.character_stats and target.character_stats.hp <= target.character_stats.maxhp
is_overheal = can_overheal and rng.randf() * 100.0 < overheal_chance_pct
if is_overheal:
allow_overheal = true
else:
var cap = 0.0
if target.character_stats:
cap = target.character_stats.maxhp - target.character_stats.hp
actual_heal = min(amount, max(0.0, cap))
var me = multiplayer.get_unique_id()
var tid = target.get_multiplayer_authority()
if me == tid and actual_heal > 0:
target.heal(actual_heal, allow_overheal)
if is_revive:
if me == tid:
target._revive_from_heal(display_amount)
elif multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
target._revive_from_heal.rpc_id(tid, display_amount)
else:
if me == tid and actual_heal > 0:
target.heal(actual_heal, allow_overheal)
_spawn_heal_effect_and_text(target, display_amount, is_crit, is_overheal)
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
if gw and gw.has_method("_apply_heal_spell_sync"):
_rpc_to_ready_peers("_sync_heal_spell_via_gw", [target.name, actual_heal, display_amount, is_crit, is_overheal, allow_overheal])
print(name, " cast heal on ", target.name, " for ", display_amount, " HP", " (actual: ", actual_heal, ", crit: ", is_crit, ", overheal: ", is_overheal, ")")
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree() and gw and gw.has_method("_apply_heal_spell_sync"):
_rpc_to_ready_peers("_sync_heal_spell_via_gw", [target.name, actual_heal, display_amount, is_crit, is_overheal, allow_overheal, is_revive])
print(name, " cast heal on ", target.name, " for ", display_amount, " HP", " (actual: ", actual_heal, ", crit: ", is_crit, ", overheal: ", is_overheal, ", revive: ", is_revive, ")")
func _spawn_heal_effect_and_text(target: Node, display_amount: int, is_crit: bool, is_overheal: bool):
if not target or not is_instance_valid(target):
@@ -4107,12 +4134,12 @@ func _spawn_heal_effect_and_text(target: Node, display_amount: int, is_crit: boo
ft.setup(heal_text, Color.GREEN, 0.5, 0.5, null, 1, 1, 0)
@rpc("any_peer", "reliable")
func _sync_heal_spell_via_gw(target_name: String, amount_to_apply: float, display_amount: int, is_crit: bool, is_overheal: bool, allow_overheal: bool):
func _sync_heal_spell_via_gw(target_name: String, amount_to_apply: float, display_amount: int, is_crit: bool, is_overheal: bool, allow_overheal: bool, is_revive: bool = false):
if is_multiplayer_authority():
return
var gw = get_tree().get_first_node_in_group("game_world")
if gw and gw.has_method("_apply_heal_spell_sync"):
gw._apply_heal_spell_sync(target_name, amount_to_apply, display_amount, is_crit, is_overheal, allow_overheal)
gw._apply_heal_spell_sync(target_name, amount_to_apply, display_amount, is_crit, is_overheal, allow_overheal, is_revive)
func _is_healing_spell() -> bool:
if not character_stats or not character_stats.equipment.has("offhand"):
@@ -4138,8 +4165,6 @@ func _get_heal_target() -> Node:
for p in get_tree().get_nodes_in_group("player"):
if not is_instance_valid(p):
continue
if "is_dead" in p and p.is_dead:
continue
var d = p.global_position.distance_to(mouse_world)
if d < best_d:
best_d = d
@@ -5826,19 +5851,7 @@ func _die():
# Wait for DIE animation to complete (200+200+200+800 = 1400ms = 1.4s)
await get_tree().create_timer(1.4).timeout
# Fade out over 0.5 seconds (fade all sprite layers)
var fade_tween = create_tween()
fade_tween.set_parallel(true)
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]:
if sprite_layer:
fade_tween.tween_property(sprite_layer, "modulate:a", 0.0, 0.5)
# Wait for fade to finish
await fade_tween.finished
# Force holder to drop us NOW (after fade, before respawn)
# Force holder to drop us NOW (before respawn wait)
# Search for any player holding us (don't rely on being_held_by)
print(name, " searching for anyone holding us...")
var found_holder = false
@@ -5999,7 +6012,9 @@ func _respawn():
global_position = new_respawn_pos
respawn_point = new_respawn_pos # Update respawn point for next time
# Play idle animation
# Clear concussion and play idle animation (server)
if status_anim and status_anim.has_animation("idle"):
status_anim.play("idle")
_set_animation("IDLE")
# Sync respawn over network (only authority sends)
@@ -6049,7 +6064,10 @@ func _do_revive(corpse: Node):
if character_stats:
character_stats.hp = max(1, character_stats.hp - half_hp)
character_stats.character_changed.emit(character_stats)
# Show -X HP on reviver (we "took" that much to revive)
_show_revive_cost_number(half_hp)
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_rpc_to_ready_peers("_sync_revive_cost", [half_hp])
corpse._revive_from_player.rpc_id(corpse.get_multiplayer_authority(), half_hp)
else:
corpse._revive_from_player(half_hp)
@@ -6074,6 +6092,42 @@ func _revive_from_player(hp_amount: int):
if status_anim and status_anim.has_animation("idle"):
status_anim.play("idle")
_set_animation("IDLE")
# Same healing effect as Tome of Healing (green frames, pulse, +X HP)
_spawn_heal_effect_and_text(self, hp_amount, false, false)
# Clear concussion on all clients (authority already did above; broadcast for others)
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_rpc_to_ready_peers("_sync_revived_clear_concussion", [name])
@rpc("any_peer", "reliable")
func _sync_revived_clear_concussion(_player_name: String):
# Received on each peer's copy of the revived player; clear our concussion (server + clients)
var status_anim = get_node_or_null("Sprite2DStatus/AnimationPlayerStatus")
if status_anim and status_anim.has_animation("idle"):
status_anim.play("idle")
@rpc("any_peer", "reliable")
func _revive_from_heal(hp_amount: int):
if not is_dead:
return
was_revived = true
is_dead = false
is_processing_death = false
if character_stats:
character_stats.hp = float(hp_amount)
else:
current_health = float(hp_amount)
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]:
if sprite_layer:
sprite_layer.modulate.a = 1.0
var status_anim = get_node_or_null("Sprite2DStatus/AnimationPlayerStatus")
if status_anim and status_anim.has_animation("idle"):
status_anim.play("idle")
_set_animation("IDLE")
_spawn_heal_effect_and_text(self, hp_amount, false, false)
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_rpc_to_ready_peers("_sync_revived_clear_concussion", [name])
@rpc("any_peer", "reliable")
func _sync_death():
@@ -6098,11 +6152,16 @@ func _sync_respawn(spawn_pos: Vector2):
# Restore visibility
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]:
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]:
if sprite_layer:
sprite_layer.modulate.a = 1.0
# Clear concussion on clients (AnimationPlayerStatus -> idle)
var status_anim = get_node_or_null("Sprite2DStatus/AnimationPlayerStatus")
if status_anim and status_anim.has_animation("idle"):
status_anim.play("idle")
_set_animation("IDLE")
func add_coins(amount: int):
@@ -6440,7 +6499,8 @@ func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool =
damage_label.color = Color(0.4, 0.65, 1.0) # Light blue
else:
damage_label.label = str(int(amount))
damage_label.color = Color.ORANGE if is_crit else Color.RED
damage_label.color = Color(1.0, 0.6, 0.2) if is_crit else Color(1.0, 0.35, 0.35) # Bright orange / bright red
damage_label.z_index = 5
# Calculate direction from attacker (slight upward variation)
var direction_from_attacker = (global_position - from_position).normalized()
@@ -6462,6 +6522,32 @@ func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool =
get_tree().current_scene.add_child(damage_label)
damage_label.global_position = global_position + Vector2(0, -16)
func _show_revive_cost_number(amount: int):
var damage_number_scene = preload("res://scenes/damage_number.tscn")
if not damage_number_scene:
return
var damage_label = damage_number_scene.instantiate()
if not damage_label:
return
damage_label.label = "-" + str(amount) + " HP"
damage_label.color = Color(1.0, 0.35, 0.35)
damage_label.z_index = 5
damage_label.direction = Vector2(0, -1)
var game_world = get_tree().get_first_node_in_group("game_world")
var parent = game_world.get_node_or_null("Entities") if game_world else get_parent()
if parent:
parent.add_child(damage_label)
damage_label.global_position = global_position + Vector2(0, -16)
else:
get_tree().current_scene.add_child(damage_label)
damage_label.global_position = global_position + Vector2(0, -16)
@rpc("any_peer", "reliable")
func _sync_revive_cost(amount: int):
if is_multiplayer_authority():
return
_show_revive_cost_number(amount)
func _on_level_up_stats(stats_increased: Array):
# Show floating text for level up - "LEVEL UP!" and stat increases
# Use damage_number scene with damage_numbers font
@@ -6526,6 +6612,20 @@ func _on_level_up_stats(stats_increased: Array):
stat_text.global_position = global_position + Vector2(0, base_y_offset)
base_y_offset -= y_spacing
@rpc("any_peer", "reliable")
func rpc_apply_corpse_knockback(dir_x: float, dir_y: float, force: float):
if not is_multiplayer_authority():
return
if not is_dead:
return
var d = Vector2(dir_x, dir_y)
if d.length_squared() < 0.01:
return
d = d.normalized()
velocity = d * force
is_knocked_back = true
knockback_time = 0.0
@rpc("any_peer", "reliable")
func _sync_damage(_amount: float, attacker_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false, is_blocked: bool = false):
# This RPC only syncs visual effects, not damage application