mirror of
https://github.com/kodzukye/omens-ascent.git
synced 2026-08-10 13:23:10 +00:00
326 lines
10 KiB
GDScript
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()
|