extends CharacterBody2D var speed = 300 var direction = Vector2.ZERO var stick_duration = 3.0 # How long the arrow stays stuck to walls var is_stuck = false var stick_timer = 0.0 var initiated_by: Node2D = null var player_owner: Node = null # Like sword_projectile var hit_targets = {} # Track what we've already hit (Dictionary for O(1) lookup) @onready var arrow_area = $ArrowArea # Assuming you have an Area2D node named ArrowArea @onready var shadow = $Shadow # Assuming you have a Shadow node under the CharacterBody2D # Called when the node enters the scene tree for the first time. func _ready() -> void: arrow_area.set_deferred("monitoring", true) # Connect area signals if arrow_area: arrow_area.body_entered.connect(_on_arrow_area_body_entered) $SfxArrowFire.play() call_deferred("_initialize_arrow") func _initialize_arrow() -> void: var angle = direction.angle() self.rotation = angle - PI / 2 # Adjust for sprite orientation # Set initial rotation based on direction velocity = direction * speed # Set initial velocity to move the arrow # Calculate the offset for the shadow position, which should be below the arrow var shadow_offset = Vector2(0, 4) # Adjust the 16 to how far you want the shadow from the arrow (this is just an example) # Apply the rotation of the arrow to the shadow offset shadow.position += shadow_offset.rotated(-self.rotation) if abs(direction.x) == 1: shadow.scale.x = 0.26 shadow.scale.y = 0.062 elif abs(direction.x) > 0: shadow.scale.x = 0.18 shadow.scale.y = 0.08 else: shadow.scale.x = 0.1 shadow.scale.y = 0.1 # Calculate the shadow's scale based on the velocity or direction of the arrow #var velocity_magnitude = velocity.length() # Scale more in the horizontal direction if moving diagonally or horizontally #var scale_factor = 0.28 + abs(velocity.x) / velocity_magnitude # Adjust the factor to your preference # Apply the scaling to the shadow shadow.rotation = -(angle - PI / 2) func shoot(shoot_direction: Vector2, start_pos: Vector2, owner_player: Node = null) -> void: direction = shoot_direction.normalized() global_position = start_pos player_owner = owner_player initiated_by = owner_player # Called every frame. 'delta' is the e lapsed time since the previous frame. func _process(delta: float) -> void: if is_stuck: # Handle fade out here if it's stuck stick_timer += delta if stick_timer >= stick_duration: # Start fading out after it sticks modulate.a = max(0, 1 - (stick_timer - stick_duration) / 1.0) # Fade out over 1 second if stick_timer >= stick_duration + 1.0: # Extra second for fade out queue_free() # Remove the arrow after fade out move_and_slide() func _physics_process(_delta: float) -> void: # If the arrow is stuck, stop it from moving if is_stuck: velocity = Vector2.ZERO # Stop movement # Optional: disable further physics interaction by setting linear_velocity # move_and_slide(Vector2.ZERO) # You can also use this to stop the character func play_impact(): $SfxImpactSound.play() # Called when the arrow hits a wall or another object (like sword_projectile) func _on_arrow_area_body_entered(body: Node2D) -> void: if is_stuck: return # Don't hit the owner if body == player_owner or body == initiated_by: return # Don't hit the same target twice if body in hit_targets: return # CRITICAL: Only the projectile owner (authority) should deal damage if player_owner and not player_owner.is_multiplayer_authority(): return # Only the authority (creator) of the projectile can deal damage # Add to hit_targets IMMEDIATELY to prevent multiple hits hit_targets[body] = true # Deal damage to players if body.is_in_group("player") and body.has_method("rpc_take_damage"): play_impact() var attacker_pos = player_owner.global_position if player_owner else global_position var player_peer_id = body.get_multiplayer_authority() if player_peer_id != 0: if multiplayer.is_server() and player_peer_id == multiplayer.get_unique_id(): body.rpc_take_damage(20.0, attacker_pos) # TODO: Get actual damage from player else: body.rpc_take_damage.rpc_id(player_peer_id, 20.0, attacker_pos) else: body.rpc_take_damage.rpc(20.0, attacker_pos) _stick_to_target(body) return # Deal damage to enemies if body.is_in_group("enemy") and body.has_method("rpc_take_damage"): var attacker_pos = player_owner.global_position if player_owner else global_position var damage = 20.0 # TODO: Get actual damage from player if player_owner and player_owner.character_stats: damage = player_owner.character_stats.damage # Check hit chance (based on player's DEX stat) var hit_roll = randf() var hit_chance = 0.95 if player_owner and player_owner.character_stats: hit_chance = player_owner.character_stats.hit_chance var is_miss = hit_roll >= hit_chance if is_miss: if body.has_method("_show_damage_number"): body._show_damage_number(0.0, attacker_pos, false, true, false) # is_miss = true _stick_to_target(body) return play_impact() var enemy_peer_id = body.get_multiplayer_authority() if enemy_peer_id != 0: if multiplayer.is_server() and enemy_peer_id == multiplayer.get_unique_id(): body.rpc_take_damage(damage, attacker_pos, false) else: body.rpc_take_damage.rpc_id(enemy_peer_id, damage, attacker_pos, false) else: body.rpc_take_damage.rpc(damage, attacker_pos, false) _stick_to_target(body) return # Hit wall or other object $SfxImpactWall.play() _stick_to_wall() func _stick_to_target(target: Node2D): # Stop the arrow velocity = Vector2.ZERO is_stuck = true stick_timer = 0.0 arrow_area.set_deferred("monitoring", false) # Calculate the collision point - move arrow slightly back from its direction var collision_normal = -direction var offset_distance = 8 var stick_position = global_position + (collision_normal * offset_distance) # Make arrow a child of the target to stick to it var global_rot = global_rotation get_parent().call_deferred("remove_child", self) target.call_deferred("add_child", self) self.set_deferred("global_position", stick_position) self.set_deferred("global_rotation", global_rot) func _stick_to_wall(): # Stop the arrow velocity = Vector2.ZERO is_stuck = true stick_timer = 0.0 arrow_area.set_deferred("monitoring", false)