class_name Creeper extends CharacterBody2D @onready var _hurtbox: HurtBox = $HurtBox @onready var _health: HealthComponent = $HealthComponent @onready var _sprite: AnimatedSprite2D = $AnimatedSprite2D @onready var _detection_area: Area2D = $DetectionArea @onready var _hitbox: HitBox = $HitBox @onready var _hitbox_collision: CollisionShape2D = $HitBox/CollisionShape2D @onready var _attack_range_area: Area2D = $AttackRangeArea @onready var _ledge_detector: RayCast2D = $RayCast2D # Creeper Exported Variables @export_group("Movement") @export var move_speed: float = 60.0 @export var patrol_speed: float = 20.0 @export var ledge_check_offset: float = 10.0 @export_group("Combat") @export var attack_cooldown: float = 1.0 @export var backoff_min_distance: float = 30.0 @export var attack_hit_frame_start: int = 4 @export var attack_hit_frame_end: int = 5 # Creeper Runtime Variables var _player: Node2D = null var _player_in_attack_range: bool = false var _is_attacking: bool = false var _is_hurt: bool = false var _is_dead: bool = false var _attack_cooldown_timer: float = 0.0 var _facing_direction: float = 1.0 var _patrol_direction: float = 1.0 func _ready() -> void: _hurtbox.hit_received.connect(_on_hurtbox_hit_received) _health.health_changed.connect(_on_health_changed) _health.died.connect(_on_died) _sprite.animation_finished.connect(_on_animation_finished) _detection_area.body_entered.connect(_on_detection_area_body_entered) _detection_area.body_exited.connect(_on_detection_area_body_exited) _attack_range_area.area_entered.connect(_on_hitbox_area_entered) _attack_range_area.area_exited.connect(_on_hitbox_area_exited) func _physics_process(delta: float) -> void: _apply_gravity(delta) _update_attack_cooldown(delta) if not _is_dead: _process_ai() _update_hitbox() move_and_slide() func _update_hitbox() -> void: var is_active_hit_frame: bool = _sprite.animation == &"attack" \ and _sprite.frame >= attack_hit_frame_start \ and _sprite.frame <= attack_hit_frame_end _hitbox_collision.disabled = not (_is_attacking and is_active_hit_frame) func _apply_gravity(delta: float) -> void: if not is_on_floor(): velocity.y += ProjectSettings.get_setting("physics/2d/default_gravity") * delta func _update_attack_cooldown(delta: float) -> void: if _attack_cooldown_timer > 0.0: _attack_cooldown_timer -= delta func _process_ai() -> void: if _is_hurt: velocity.x = 0.0 return if _is_attacking: velocity.x = 0.0 return if _player_in_attack_range and _attack_cooldown_timer <= 0.0: _start_attack() return if _player_in_attack_range: _facing_direction = signf(_player.global_position.x - global_position.x) var distance_to_player: float = absf(_player.global_position.x - global_position.x) if distance_to_player < backoff_min_distance: velocity.x = -_facing_direction * move_speed if _sprite.animation != &"walk_1": _sprite.play(&"walk_1") else: velocity.x = 0.0 if _sprite.animation != &"idle": _sprite.play(&"idle") _sprite.flip_h = _facing_direction > 0.0 _hitbox.scale.x = -_facing_direction _attack_range_area.scale.x = -_facing_direction return if _player: _facing_direction = signf(_player.global_position.x - global_position.x) if _has_ground_ahead(_facing_direction): velocity.x = _facing_direction * move_speed if _sprite.animation != &"walk_1": _sprite.play(&"walk_1") else: velocity.x = 0.0 if _sprite.animation != &"idle": _sprite.play(&"idle") else: _facing_direction = _patrol_direction velocity.x = _patrol_direction * patrol_speed if _sprite.animation != &"walk_2": _sprite.play(&"walk_2") _sprite.flip_h = _facing_direction > 0.0 _hitbox.scale.x = -_facing_direction _attack_range_area.scale.x = -_facing_direction if not _player: _update_patrol_direction() func _has_ground_ahead(direction: float) -> bool: _ledge_detector.position.x = ledge_check_offset * direction _ledge_detector.force_raycast_update() return _ledge_detector.is_colliding() func _update_patrol_direction() -> void: if is_on_wall() or not _has_ground_ahead(_patrol_direction): _patrol_direction = -_patrol_direction func _start_attack() -> void: _is_attacking = true _attack_cooldown_timer = attack_cooldown velocity.x = 0.0 _sprite.play(&"attack") func _on_detection_area_body_entered(body: Node2D) -> void: _player = body func _on_detection_area_body_exited(body: Node2D) -> void: if body == _player: _player = null func _on_hitbox_area_entered(area: Area2D) -> void: if area is HurtBox and area.get_parent() == _player: _player_in_attack_range = true func _on_hitbox_area_exited(area: Area2D) -> void: if area is HurtBox and area.get_parent() == _player: _player_in_attack_range = false func _on_hurtbox_hit_received(hitbox: HitBox) -> void: _health.take_damage(hitbox.damage) func _on_health_changed(_new_value: int, _max_value: int) -> void: _is_attacking = false _is_hurt = true _sprite.play(&"hurt") func _on_died() -> void: _is_dead = true velocity = Vector2.ZERO _hurtbox.set_deferred("monitoring", false) _hitbox_collision.set_deferred("disabled", true) func _on_animation_finished() -> void: if _sprite.animation == &"hurt": _is_hurt = false if _is_dead: _sprite.play(&"death_1") else: _sprite.play(&"idle") elif _sprite.animation == &"attack": _is_attacking = false elif _sprite.animation == &"death_1": queue_free()