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

326 lines
10 KiB
GDScript

class_name DemonSamurai
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 _combo_hitbox: HitBox = $ComboHitBox
@onready var _combo_hitbox_collision: CollisionShape2D = $ComboHitBox/CollisionShape2D
@onready var _combo_attack_range_area: Area2D = $ComboAttackRangeArea
@onready var _jump_attack_hitbox: HitBox = $JumpAttackHitBox
@onready var _jump_attack_hitbox_collision: CollisionShape2D = $JumpAttackHitBox/CollisionShape2D
@onready var _jump_attack_range_area: Area2D = $JumpAttackRangeArea
@onready var _ledge_detector: RayCast2D = $RayCast2D
# FHit frames for combo
const _COMBO_HIT_FRAMES: Dictionary = {
1: Vector2i(2, 4),
2: Vector2i(2, 4),
3: Vector2i(3, 5),
}
# Exported Variables
@export_group("Movement")
@export var move_speed: float = 80.0
@export var patrol_speed: float = 40.0
@export var ledge_check_offset: float = 10.0
@export_group("Combat")
@export var attack_cooldown: float = 0.6
@export var combo_damage: Array[int] = [2, 2, 2]
@export var jump_attack_damage: int = 4
@export var jump_attack_lunge_speed: float = 260.0
@export var jump_attack_hit_frame_start: int = 7
@export var jump_attack_hit_frame_end: int = 9
@export var hits_to_trigger_jump_attack: int = 3
@export_group("Phase")
@export var flame_phase_health_ratio: float = 0.5
@export var flame_move_speed_multiplier: float = 1.4
@export var flame_damage_multiplier: float = 2.0
@export_group("Defend")
@export var spam_hit_window: float = 0.65
@export var block_reaction_time: float = 0.1
@export var blocks_to_trigger_jump_attack: int = 3
# Runtime Variables
var _player: Node2D = null
var _player_in_combo_range: bool = false
var _player_in_jump_range: bool = false
var _combo_step: int = 0
var _hits_taken: int = 0
var _is_attacking: bool = false
var _is_jump_attacking: bool = false
var _is_jump_attack_uninterruptible: bool = false
var _is_shouting: bool = false
var _is_hurt: bool = false
var _is_blocking: bool = false
var _is_dead: bool = false
var _is_flame_phase: bool = false
var _attack_cooldown_timer: float = 0.0
var _time_since_last_action: float = 999.0
var _block_timer: float = 0.0
var _block_count: int = 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)
_combo_attack_range_area.area_entered.connect(_on_combo_range_area_entered)
_combo_attack_range_area.area_exited.connect(_on_combo_range_area_exited)
_jump_attack_range_area.area_entered.connect(_on_jump_range_area_entered)
_jump_attack_range_area.area_exited.connect(_on_jump_range_area_exited)
func _physics_process(delta: float) -> void:
_apply_gravity(delta)
_update_attack_cooldown(delta)
if not _is_dead:
_check_phase_transition()
_process_ai()
_update_hitboxes()
move_and_slide()
func _update_hitboxes() -> void:
var multiplier: float = flame_damage_multiplier if _is_flame_phase else 1.0
var combo_hit_frames: Vector2i = _COMBO_HIT_FRAMES.get(_combo_step, Vector2i(-1, -1))
var is_combo_hit_frame: bool = _sprite.frame >= combo_hit_frames.x and _sprite.frame <= combo_hit_frames.y
_combo_hitbox_collision.disabled = not (_is_attacking and is_combo_hit_frame)
if _combo_step >= 1 and _combo_step <= combo_damage.size():
_combo_hitbox.damage = int(round(combo_damage[_combo_step - 1] * multiplier))
var is_jump_hit_frame: bool = _sprite.frame >= jump_attack_hit_frame_start \
and _sprite.frame <= jump_attack_hit_frame_end
_jump_attack_hitbox_collision.disabled = not (_is_jump_attacking and is_jump_hit_frame)
_jump_attack_hitbox.damage = int(round(jump_attack_damage * multiplier))
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
_time_since_last_action += delta
if _is_blocking:
_block_timer -= delta
if _block_timer <= 0.0:
_is_blocking = false
func _check_phase_transition() -> void:
if _is_flame_phase or _is_shouting or _is_dead:
return
if _health.current_health <= _health.max_health * flame_phase_health_ratio:
_start_shout()
func _process_ai() -> void:
if _is_hurt or _is_shouting or _is_blocking:
velocity.x = 0.0
return
if _is_jump_attacking:
var is_jump_hit_frame: bool = _sprite.frame >= jump_attack_hit_frame_start \
and _sprite.frame <= jump_attack_hit_frame_end
velocity.x = _facing_direction * jump_attack_lunge_speed if is_jump_hit_frame else 0.0
return
if _is_attacking:
velocity.x = 0.0
return
if _player_in_jump_range and _hits_taken >= hits_to_trigger_jump_attack and _attack_cooldown_timer <= 0.0:
_start_jump_attack()
return
if _player_in_combo_range and _attack_cooldown_timer <= 0.0:
_start_combo_attack()
return
if _player_in_combo_range or _player_in_jump_range:
_facing_direction = signf(_player.global_position.x - global_position.x)
velocity.x = 0.0
if _sprite.animation != _phase_anim(&"idle"):
_sprite.play(_phase_anim(&"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 * _current_move_speed()
if _sprite.animation != _phase_anim(&"running"):
_sprite.play(_phase_anim(&"running"))
else:
velocity.x = 0.0
if _sprite.animation != _phase_anim(&"idle"):
_sprite.play(_phase_anim(&"idle"))
else:
_facing_direction = _patrol_direction
velocity.x = _patrol_direction * patrol_speed
if _sprite.animation != _phase_anim(&"running"):
_sprite.play(_phase_anim(&"running"))
_update_facing()
if not _player:
_update_patrol_direction()
func _current_move_speed() -> float:
return move_speed * flame_move_speed_multiplier if _is_flame_phase else move_speed
func _phase_anim(base_name: StringName) -> StringName:
return StringName(("flame_" if _is_flame_phase else "normal_") + String(base_name))
func _update_facing() -> void:
_sprite.flip_h = _facing_direction < 0.0
_combo_hitbox.scale.x = _facing_direction
_combo_attack_range_area.scale.x = _facing_direction
_jump_attack_hitbox.scale.x = _facing_direction
_jump_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_combo_attack() -> void:
_combo_step = (_combo_step % 3) + 1
_is_attacking = true
_attack_cooldown_timer = attack_cooldown
velocity.x = 0.0
_sprite.play(_phase_anim(StringName("attack%d" % _combo_step)))
func _start_jump_attack(uninterruptible: bool = false) -> void:
_is_jump_attacking = true
_is_jump_attack_uninterruptible = uninterruptible
_hits_taken = 0
_attack_cooldown_timer = attack_cooldown
velocity.x = 0.0
_sprite.play(_phase_anim(&"jump_attack"))
func _trigger_block() -> void:
_block_count += 1
_is_attacking = false
_combo_step = 0
_is_blocking = true
_block_timer = block_reaction_time
velocity.x = 0.0
_sprite.play(&"defend")
if _block_count >= blocks_to_trigger_jump_attack:
_block_count = 0
_is_blocking = false
_start_jump_attack(true)
func _start_shout() -> void:
_is_shouting = true
_is_attacking = false
_is_jump_attacking = false
_is_jump_attack_uninterruptible = false
_is_hurt = false
_is_blocking = false
_combo_step = 0
_block_count = 0
velocity.x = 0.0
_sprite.play(&"shout")
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_combo_range_area_entered(area: Area2D) -> void:
if area is HurtBox and area.get_parent() == _player:
_player_in_combo_range = true
func _on_combo_range_area_exited(area: Area2D) -> void:
if area is HurtBox and area.get_parent() == _player:
_player_in_combo_range = false
func _on_jump_range_area_entered(area: Area2D) -> void:
if area is HurtBox and area.get_parent() == _player:
_player_in_jump_range = true
func _on_jump_range_area_exited(area: Area2D) -> void:
if area is HurtBox and area.get_parent() == _player:
_player_in_jump_range = false
func _on_hurtbox_hit_received(hitbox: HitBox) -> void:
if _is_dead:
return
if _is_hurt or _is_shouting:
_time_since_last_action = 0.0
return
if not _is_jump_attacking and _time_since_last_action < spam_hit_window:
_time_since_last_action = 0.0
_trigger_block()
return
_health.take_damage(hitbox.damage)
_hits_taken += 1
_block_count = 0
_time_since_last_action = 0.0
func _on_health_changed(_new_value: int, _max_value: int) -> void:
if _is_shouting or _is_dead:
return
if _is_jump_attacking and _is_jump_attack_uninterruptible:
return
_is_attacking = false
_is_jump_attacking = false
_is_jump_attack_uninterruptible = false
_combo_step = 0
_is_hurt = true
_sprite.play(_phase_anim(&"hurt"))
func _on_died() -> void:
_is_dead = true
_is_blocking = false
velocity = Vector2.ZERO
_hurtbox.set_deferred("monitoring", false)
_combo_hitbox_collision.set_deferred("disabled", true)
_jump_attack_hitbox_collision.set_deferred("disabled", true)
remove_from_group("enemies")
EventBus.enemy_defeated.emit()
_sprite.play(&"death")
await _sprite.animation_finished
SceneManager.change_scene("res://scenes/components/outro.tscn")
func _on_animation_finished() -> void:
if _sprite.animation == _phase_anim(&"hurt"):
_is_hurt = false
_time_since_last_action = 0.0
if _is_dead:
_sprite.play(&"death")
elif String(_sprite.animation).ends_with("attack1") or String(_sprite.animation).ends_with("attack2") \
or String(_sprite.animation).ends_with("attack3"):
_is_attacking = false
if _combo_step == 3:
_combo_step = 0
elif _sprite.animation == _phase_anim(&"jump_attack"):
_is_jump_attacking = false
_is_jump_attack_uninterruptible = false
elif _sprite.animation == &"shout":
_is_shouting = false
_is_flame_phase = true
elif _sprite.animation == &"death":
queue_free()