extends Node2D # Sword Slash - Swings around player and deals damage @export var damage: float = 20.0 @export var swing_speed: float = 720.0 # Degrees per second @export var swing_radius: float = 40.0 # Distance from player center (closer swing) @export var lifetime: float = 0.3 # How long the slash lasts var swing_angle: float = 0.0 # Current angle var swing_start_angle: float = 0.0 # Starting angle var swing_arc: float = 180.0 # Total arc to swing (180 degrees) var elapsed_time: float = 0.0 var player_owner: Node = null var hit_targets = {} # Track what we've already hit (Dictionary for O(1) lookup) @onready var sprite = $Sprite2D @onready var hit_area = $Area2D func _ready(): # Connect area signals if hit_area: hit_area.body_entered.connect(_on_body_entered) # Set initial rotation rotation = deg_to_rad(swing_start_angle) func setup(start_angle: float, owner_player: Node, arc_direction: float = 1.0): swing_start_angle = start_angle swing_angle = start_angle player_owner = owner_player swing_arc = 180.0 * arc_direction # Positive or negative arc rotation = deg_to_rad(swing_start_angle) func _physics_process(delta): elapsed_time += delta # Check lifetime if elapsed_time >= lifetime: queue_free() return # Calculate swing progress (0 to 1) var progress = elapsed_time / lifetime # Swing the sword in an arc swing_angle = swing_start_angle + (swing_arc * progress) rotation = deg_to_rad(swing_angle) # Position around player if we have one if player_owner and is_instance_valid(player_owner): var offset = Vector2(swing_radius, 0).rotated(deg_to_rad(swing_angle)) global_position = player_owner.global_position + offset func _on_body_entered(body): # Don't hit the owner if body == player_owner: return # Don't hit the same target twice - use Dictionary for O(1) lookup to prevent race conditions if body in hit_targets: return # Add to hit_targets IMMEDIATELY to prevent multiple hits (mark as hit before processing) hit_targets[body] = true # Friendly fire: only skip when owner is also a player. Enemies can hit players. if body.is_in_group("player") and body.has_method("take_damage"): if player_owner and player_owner.is_in_group("player"): return # Owner is player → pass through, no damage # Owner is enemy → deal damage to player var attacker_pos = player_owner.global_position if player_owner and is_instance_valid(player_owner) else global_position var player_peer_id = body.get_multiplayer_authority() if player_peer_id != 0: if multiplayer.get_unique_id() == player_peer_id: body.rpc_take_damage(damage, attacker_pos, false, false) else: body.rpc_take_damage.rpc_id(player_peer_id, damage, attacker_pos, false, false) else: body.rpc_take_damage.rpc(damage, attacker_pos, false, false) return # Deal damage to boxes or other damageable objects if "health" in body: # Boxes have health property body.health -= damage if body.health <= 0 and body.has_method("_break_into_pieces"): body._break_into_pieces() print("Sword hit object: ", body.name) # Push the hit target away slightly if body is CharacterBody2D: var knockback_dir = (body.global_position - global_position).normalized() if body.is_in_group("player") and "is_dead" in body and body.is_dead: # Corpse: reduced force, sync to victim's client so they see the push const CORPSE_KNOCKBACK: float = 50.0 var pid = body.get_multiplayer_authority() if pid == multiplayer.get_unique_id(): body.rpc_apply_corpse_knockback(knockback_dir.x, knockback_dir.y, CORPSE_KNOCKBACK) elif pid != 0: body.rpc_apply_corpse_knockback.rpc_id(pid, knockback_dir.x, knockback_dir.y, CORPSE_KNOCKBACK) else: body.rpc_apply_corpse_knockback.rpc(knockback_dir.x, knockback_dir.y, CORPSE_KNOCKBACK) else: body.velocity = knockback_dir * 200.0