class_name MorphDemon extends CharacterBody2D const SPRITE_OFFSET: float = 32.0 const _DEFAULT_COLLISION_LAYER: int = 8 @onready var _hurtbox: HurtBox = $HurtBox @onready var _health: HealthComponent = $HealthComponent @onready var _sprite: AnimatedSprite2D = $AnimatedSprite2D @onready var _detection_area: Area2D = $DetectionArea @onready var _dash_hitbox: HitBox = $DashHitBox @onready var _dash_hitbox_collision: CollisionShape2D = $DashHitBox/CollisionShape2D @onready var _dash_attack_range_area: Area2D = $DashAttackRangeArea @onready var _poison_hitbox: HitBox = $PoisonHitBox @onready var _poison_hitbox_collision: CollisionShape2D = $PoisonHitBox/CollisionShape2D @onready var _poison_attack_range_area: Area2D = $PoisonAttackRangeArea @onready var _ledge_detector: RayCast2D = $RayCast2D # Exported Variables @export_group("Movement") @export var move_speed: float = 30.0 @export var patrol_speed: float = 10.0 @export var ledge_check_offset: float = 10.0 @export_group("Combat") @export var attack_cooldown: float = 1.5 @export var dash_lunge_speed: float = 220.0 @export var dash_hit_frame_start: int = 2 @export var dash_hit_frame_end: int = 4 @export var poison_hit_frame_start: int = 2 @export var poison_hit_frame_end: int = 4 # Runtime Variables var _player: Node2D = null var _player_in_dash_range: bool = false var _player_in_poison_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: add_to_group("enemies") _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) _dash_attack_range_area.area_entered.connect(_on_dash_range_area_entered) _dash_attack_range_area.area_exited.connect(_on_dash_range_area_exited) _poison_attack_range_area.area_entered.connect(_on_poison_range_area_entered) _poison_attack_range_area.area_exited.connect(_on_poison_range_area_exited) func _physics_process(delta: float) -> void: _apply_gravity(delta) _update_attack_cooldown(delta) if not _is_dead: _process_ai() _update_hitboxes() move_and_slide() func _update_hitboxes() -> void: var is_dash_hit_frame: bool = _sprite.animation == &"dash_attack" \ and _sprite.frame >= dash_hit_frame_start \ and _sprite.frame <= dash_hit_frame_end var is_dashing_through: bool = _is_attacking and is_dash_hit_frame _dash_hitbox_collision.disabled = not is_dashing_through collision_layer = 0 if is_dashing_through else _DEFAULT_COLLISION_LAYER var is_poison_hit_frame: bool = _sprite.animation == &"poison_attack" \ and _sprite.frame >= poison_hit_frame_start \ and _sprite.frame <= poison_hit_frame_end _poison_hitbox_collision.disabled = not (_is_attacking and is_poison_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: var is_dash_hit_frame: bool = _sprite.animation == &"dash_attack" \ and _sprite.frame >= dash_hit_frame_start \ and _sprite.frame <= dash_hit_frame_end velocity.x = _facing_direction * dash_lunge_speed if is_dash_hit_frame else 0.0 return if _player_in_dash_range and _attack_cooldown_timer <= 0.0: _start_attack(&"dash_attack") return if _player_in_poison_range and _attack_cooldown_timer <= 0.0: _start_attack(&"poison_attack") return if _player_in_dash_range or _player_in_poison_range: _facing_direction = signf(_player.global_position.x - global_position.x) velocity.x = 0.0 if _sprite.animation != &"idle": _sprite.play(&"idle") _update_facing() 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": _sprite.play(&"walk") 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": _sprite.play(&"walk") _update_facing() if not _player: _update_patrol_direction() func _update_facing() -> void: _sprite.flip_h = _facing_direction < 0.0 _sprite.offset.x = -SPRITE_OFFSET if _sprite.flip_h else SPRITE_OFFSET _dash_hitbox.scale.x = -_facing_direction _dash_attack_range_area.scale.x = -_facing_direction _poison_hitbox.scale.x = _facing_direction _poison_attack_range_area.scale.x = _facing_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(attack_animation: StringName) -> void: _is_attacking = true _attack_cooldown_timer = attack_cooldown velocity.x = 0.0 _sprite.play(attack_animation) 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_dash_range_area_entered(area: Area2D) -> void: if area is HurtBox and area.get_parent() == _player: _player_in_dash_range = true func _on_dash_range_area_exited(area: Area2D) -> void: if area is HurtBox and area.get_parent() == _player: _player_in_dash_range = false func _on_poison_range_area_entered(area: Area2D) -> void: if area is HurtBox and area.get_parent() == _player: _player_in_poison_range = true func _on_poison_range_area_exited(area: Area2D) -> void: if area is HurtBox and area.get_parent() == _player: _player_in_poison_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) _dash_hitbox_collision.set_deferred("disabled", true) _poison_hitbox_collision.set_deferred("disabled", true) remove_from_group("enemies") EventBus.enemy_defeated.emit() func _on_animation_finished() -> void: if _sprite.animation == &"hurt": _is_hurt = false if _is_dead: _sprite.play(&"death") else: _sprite.play(&"idle") elif _sprite.animation == &"dash_attack" or _sprite.animation == &"poison_attack": _is_attacking = false elif _sprite.animation == &"death": queue_free()