Files
Omens-Ascent/scenes/actors/morph_demon.gd

211 lines
7.0 KiB
GDScript

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()