Frame invincible enemy

Godot Version

4.6.2 stable

Question

I am making a 2D rpg game but the problem is when i attack the enemy at a certain frame of the attack animation the enemy is completely invincible to dmg i tried everything to my knowledge but to no avail please help kind sirs.

""" Skeleton code
extends CharacterBody2D
@onready var animated = $AnimatedSprite2D
@onready var hitbox = $Hitbox
const knockback_power = 500
const SPEED: float = 0.4
var is_attacking: bool = false
var can_be_hit: bool = true
var is_hurt: bool = false
var facing_right: bool = true
var knockback: Vector2 = Vector2.ZERO
var overlapping_hitbox = null
var target = null
var skeleton: Dictionary = {
	"atk" : 10,
	"hp" : 100,
	"def" : 2,
}

func _physics_process(_delta):
	knockback = knockback.move_toward(Vector2.ZERO, 20)
	velocity = knockback
	
	if target and not is_hurt and not is_attacking:
		_movement(_delta)
		_facing()
		animated.play("Run")
	
	if overlapping_hitbox != null and overlapping_hitbox.monitoring and can_be_hit and not is_hurt:
		_take_hit()
	
	move_and_slide()

func _take_hit():
	var player = overlapping_hitbox.get_parent()
	var direction = (global_position - player.global_position).normalized()
	is_hurt = true
	is_attacking = false
	can_be_hit = false
	
	var died = false
	if overlapping_hitbox.name == "Hitbox_PA":
		died = damaged(player.knight["atk"], 2.0)
	else:
		died = damaged(player.knight["atk"])
	
	if not died:
		animated.play("Hurt")
		knockback = direction * knockback_power

func _on_hurt_box_area_entered(area):
	if area.is_in_group("player_hitbox"):
		overlapping_hitbox = area

func _on_hurt_box_area_exited(area):
	if area.is_in_group("player_hitbox"):
		overlapping_hitbox = null

func damaged(atk, multiplier = 1.0):
	var total_damage = (atk - skeleton["def"]) * multiplier
	skeleton["hp"] -= total_damage
	print("Skelly only have %d left" % skeleton["hp"])
	
	if skeleton["hp"] <= 0:
		skeleton["hp"] = 0
		animated.play("Death")
		return true
	return false

func _on_animated_sprite_2d_animation_finished():
	if animated.animation == "Death":
		queue_free()
		return
	
	is_hurt = false
	is_attacking = false
	can_be_hit = true
	
	if target:
		animated.play("Run")
	else:
		animated.play("Idle")

func _facing() -> void:
	var direction = target.position - position
	if direction.x > 0:
		animated.flip_h = false
		facing_right = true
	elif direction.x < 0:
		facing_right = false
		animated.flip_h = true
	
	if facing_right:
		hitbox.position.x = abs(hitbox.position.x)
	else:
		hitbox.position.x = -abs(hitbox.position.x)

func _movement(delta: float) -> void:
	var direction = target.position - position
	position += direction * delta * SPEED

func _on_sight_body_entered(body):
	if body.name == "Knight":
		target = body

func _on_sight_body_exited(body):
	if body.name == "Knight":
		target = null
		if not is_hurt and not is_attacking:
			animated.play("Idle")

func _on_hitbox_area_entered(area):
	if not is_attacking and not is_hurt:
		is_attacking = true
		await get_tree().create_timer(0.3).timeout
		if is_hurt:
			return
		if randi() % 2 == 0:
			animated.play("Normal_Attack")
		else:
			animated.play("Heavy_Attack")"""

"""Player code
extends CharacterBody2D

@onready var animated = $AnimatedSprite2D
@onready var slash = $SFX/Slash
@onready var power_slash = $SFX/PowerSlash
@onready var double_slash = $SFX/DoubleSlash
@onready var hitbox_na = $Hitbox_NA
@onready var hitbox_ha = $Hitbox_HA
@onready var hitbox_pa = $Hitbox_PA

const SPEED = 300.0
var is_locked: bool = false
var facing_right: bool = true
var is_hurt: bool = false

var knight: Dictionary = {
	"atk" : 20,
	"hp" : 100,
	"def" : 6,
}

var hitboxes: Array

func _ready():
	hitboxes = [hitbox_na, hitbox_ha, hitbox_pa]
	disable_all_hitboxes()

func _physics_process(_delta) -> void:
	if not is_locked and not is_hurt:
		process_movement()
		attack()
		
	update_hitbox_direction()
	animation_hit()
	move_and_slide()

func get_current_hitbox() -> Area2D:
	if animated.animation == "Normal_Attack":
		return hitbox_na
	elif animated.animation == "Heavy_Attack":
		return hitbox_ha
	elif animated.animation == "Power_Attack":
		return hitbox_pa
	return hitbox_na
	
func disable_all_hitboxes():
	for hitbox in hitboxes:
		hitbox.monitoring = false
		hitbox.monitorable = false

func process_movement() -> void:
	var dir := Input.get_vector("Left", "Right", "Up", "Down")
	velocity = dir * SPEED
	
	if dir.x == 0 and dir.y == 0:
		animated.play("Idle")
	else:
		animated.play("Walking")
		if dir.x > 0:
			facing_right = true
			animated.flip_h = false
		elif dir.x < 0:
			facing_right = false
			animated.flip_h = true

func update_hitbox_direction():
	for hitbox in hitboxes:
		if facing_right:
			hitbox.position.x = abs(hitbox.position.x)
		else:
			hitbox.position.x = -abs(hitbox.position.x)

func start_attack():
	is_locked = true
	velocity = Vector2.ZERO
	disable_all_hitboxes() 

func animation_hit():
	if is_locked and not is_hurt:
		var frame = animated.frame
		var hitbox = get_current_hitbox()
		
		if animated.animation == "Normal_Attack":
			if frame >= 3 and frame <= 5:
				hitbox.monitorable = true
				hitbox.monitoring = true
			else:
				hitbox.monitorable = false
				hitbox.monitoring = false

		elif animated.animation == "Power_Attack":
			if frame >= 6 and frame <= 9:
				hitbox.monitorable = true
				hitbox.monitoring = true
			else:
				hitbox.monitorable = false
				hitbox.monitoring = false

		elif animated.animation == "Heavy_Attack":
			if (frame >= 3 and frame <= 5) or (frame >= 7 and frame <= 9):
				hitbox.monitorable = true
				hitbox.monitoring = true
			else:
				hitbox.monitorable = false
				hitbox.monitoring = false

func attack():
	if Input.is_action_just_pressed("Powered_Atk"):
		start_attack()
		power_slash.play()
		animated.play("Power_Attack")
	elif Input.is_action_just_pressed("Heavy_Atk"):
		start_attack()
		double_slash.play()
		animated.play("Heavy_Attack")
	elif Input.is_action_just_pressed("NA"):
		start_attack()
		slash.play()
		animated.play("Normal_Attack")

func _on_hurtbox_area_entered(area):
	if is_hurt or knight["hp"] <= 0:
		return
		
	# Process enemy attacks hitting the player body
	if area.name == "Hitbox" or area.is_in_group("enemy_hitbox"):
		var enemy = area.get_parent()
		
		is_hurt = true
		is_locked = false 
		disable_all_hitboxes()
		
		var incoming_atk = enemy.skeleton["atk"] if "skeleton" in enemy else 10
		var total_damage = max(1, incoming_atk - knight["def"])
		knight["hp"] -= total_damage
		print("Knight took damage! HP left: %d" % knight["hp"])
		
		if knight["hp"] <= 0:
			knight["hp"] = 0
			animated.play("Death")
		else:
			animated.play("Hurt")
			var knockback_dir = (global_position - enemy.global_position).normalized()
			velocity = knockback_dir * 300 

func _on_animated_sprite_2d_animation_finished():
	if animated.animation == "Death":
		return
		
	is_locked = false
	is_hurt = false
	disable_all_hitboxes()
	animated.play("Idle")
"""

bug is in your skeleton’s _physics_process:

  if overlapping_hitbox != null and overlapping_hitbox.monitoring and can_be_hit and not is_hurt:
      _take_hit()

Polling overlapping_hitbox.monitoring is the issue. Toggling monitoring/monitorable on an Area2D does NOT immediately re-fire area_entered - PhysicsServer2D queues the change and the signal fires several physics frames later, by which time your player’s animation_hit() has already flipped the hitbox back off. So your check sees monitoring = false at exactly the wrong moments.

I tested this in 4.6.3 with a stripped-down repro. Toggling hitbox.monitoring = true at physics frame 3 → area_entered didn’t actually fire until frame 6, and by then the test had already toggled it back to false. Polling missed the entire first cycle. Doing the same test but taking the hit inside area_entered itself caught every cycle.

Fix is don’t poll:

  func _on_hurt_box_area_entered(area):
      if not area.is_in_group("player_hitbox"):
          return
      overlapping_hitbox = area
      if can_be_hit and not is_hurt:
          _take_hit()

Then delete the if overlapping_hitbox != null and overlapping_hitbox.monitoring and ... block from _physics_process. The fact that area_entered fired AT ALL means the hitbox was active when the overlap was detected - you don’t need to (and can’t reliably) recheck monitoring after the fact.