class_name Player extends CharacterBody2D # Player States enum State { IDLE, RUN, JUMP, ATTACK_1, ATTACK_2, ATTACK_3, HEAVY_ATTACK, # A rajouter ? DASH, DASH_ATTACK, # A rajouter DEFEND, # A rajouter HURT, DEATH, } # Player Nodes Variables @onready var _sprite: AnimatedSprite2D = $AnimatedSprite2D @onready var _hitbox: HitBox = $HitBox @onready var _hitbox_collision: CollisionShape2D = $HitBox/CollisionShape2D @onready var _health: HealthComponent = $HealthComponent @onready var _hurtbox: HurtBox = $HurtBox # Collision masks (World = 1, EnemyBody = 8) const _DEFAULT_COLLISION_MASK: int = 1 | 8 const _DASH_COLLISION_MASK: int = 1 const _DEATH_SCREEN_PATH: String = "res://scenes/components/death_screen.tscn" # Damage dealt per attack state const _ATTACK_DAMAGE: Dictionary = { State.ATTACK_1: 3, State.ATTACK_2: 3, State.ATTACK_3: 5, State.HEAVY_ATTACK: 8, State.DASH_ATTACK: 2, } # Hitbox-active frame range per attack state const _ATTACK_HIT_FRAMES: Dictionary = { State.ATTACK_1: Vector2i(2, 4), State.ATTACK_2: Vector2i(2, 4), State.ATTACK_3: Vector2i(2, 4), State.DASH_ATTACK: Vector2i(4, 6), State.HEAVY_ATTACK: Vector2i(6, 8), } # Player Runtime Variables var direction: float = 0.0 var facing_direction: float = 1.0 var current_state: State = State.IDLE var is_attacking: bool = false var combo_step: int = 0 var combo_timer: float = 0.0 var is_dashing: bool = false var dash_timer: float = 0.0 var dash_cooldown_timer: float = 0.0 var is_hurt: bool = false var is_dead: bool = false # Player Exported Variables @export_group("Movement") @export var move_speed: float = 200.0 @export_group("Jump") @export var jump_velocity: float = -350.0 @export_group("Combat") @export var attack_cooldown: float = 0.4 @export var heavy_attack_cooldown: float = 0.6 @export var combo_window: float = 0.6 @export var attack_move_speed: float = 0.0 @export_group("Dash") @export var dash_speed: float = 500.0 @export var dash_duration: float = 0.2 @export var dash_cooldown: float = 0.8 func _ready() -> void: add_to_group("player") _sprite.animation_finished.connect(_on_animation_finished) _health.health_changed.connect(_on_health_changed) _health.died.connect(_on_died) _hurtbox.hit_received.connect(_on_hurtbox_hit_received) if EventBus.has_checkpoint: global_position = EventBus.last_checkpoint_position _on_health_changed.call_deferred(_health.current_health, _health.max_health) func _physics_process(delta: float) -> void: _apply_gravity(delta) _process_dash_input() _apply_movement(delta) _process_attack_input() _update_combo_timer(delta) _update_dash_timers(delta) _update_state() _update_animation() _update_hitbox() move_and_slide() func _apply_gravity(delta: float) -> void: if is_dashing: return if not is_on_floor(): velocity.y += ProjectSettings.get_setting("physics/2d/default_gravity") * delta func _apply_movement(delta: float) -> void: if is_dead or is_hurt: velocity.x = 0.0 return if is_dashing: velocity.x = facing_direction * dash_speed return if is_attacking: velocity.x = facing_direction * attack_move_speed return direction = Input.get_axis("move_left", "move_right") velocity.x = direction * move_speed if direction != 0.0: facing_direction = direction if is_on_floor() and Input.is_action_just_pressed("jump"): velocity.y = jump_velocity func _process_dash_input() -> void: if is_dashing or is_attacking or is_hurt or is_dead or dash_cooldown_timer > 0.0: return if not Input.is_action_just_pressed("dash"): return is_dashing = true dash_timer = dash_duration dash_cooldown_timer = dash_cooldown velocity.y = 0.0 current_state = State.DASH collision_mask = _DASH_COLLISION_MASK func _process_attack_input() -> void: if is_attacking or is_dashing or is_hurt or is_dead or not Input.is_action_just_pressed("light_attack"): return if combo_step > 0 and combo_timer > 0.0: combo_step += 1 else: combo_step = 1 is_attacking = true combo_timer = 0.0 current_state = State.values()[State.ATTACK_1 + combo_step - 1] func _update_combo_timer(delta: float) -> void: if combo_timer > 0.0: combo_timer -= delta if combo_timer <= 0.0: combo_step = 0 func _update_dash_timers(delta: float) -> void: if is_dashing: dash_timer -= delta if dash_timer <= 0.0: is_dashing = false collision_mask = _DEFAULT_COLLISION_MASK if dash_cooldown_timer > 0.0: dash_cooldown_timer -= delta func _on_animation_finished() -> void: if current_state == State.HURT: is_hurt = false return if not is_attacking: return is_attacking = false if current_state == State.ATTACK_3: combo_step = 0 else: combo_timer = combo_window func _update_state() -> void: if is_dead: return if is_hurt: current_state = State.HURT return if is_attacking: return if is_dashing: current_state = State.DASH return if not is_on_floor(): current_state = State.JUMP elif direction != 0.0: current_state = State.RUN else: current_state = State.IDLE func _update_animation() -> void: var animation_name: String = State.keys()[current_state].to_lower() if _sprite.animation != animation_name: _sprite.play(animation_name) _sprite.flip_h = facing_direction < 0.0 func _update_hitbox() -> void: _hitbox.scale.x = facing_direction _hitbox.damage = _ATTACK_DAMAGE.get(current_state, 0) var hit_frames: Vector2i = _ATTACK_HIT_FRAMES.get(current_state, Vector2i(-1, -1)) var is_active_hit_frame: bool = _sprite.frame >= hit_frames.x and _sprite.frame <= hit_frames.y _hitbox_collision.disabled = not (is_attacking and is_active_hit_frame) func kill() -> void: _health.kill() func _on_hurtbox_hit_received(hitbox: HitBox) -> void: _health.take_damage(hitbox.damage) is_attacking = false if is_dashing: is_dashing = false collision_mask = _DEFAULT_COLLISION_MASK is_hurt = true current_state = State.HURT func _on_health_changed(current_health: int, max_health: int) -> void: EventBus.player_health_changed.emit(current_health, max_health) func _on_died() -> void: EventBus.player_died.emit() is_dead = true _hurtbox.set_deferred("monitoring", false) _hitbox_collision.set_deferred("disabled", true) _switch_to_death_screen() func _switch_to_death_screen() -> void: EventBus.last_level_path = get_tree().current_scene.scene_file_path EventBus.last_death_facing_direction = facing_direction SceneManager.call_deferred("change_scene", _DEATH_SCREEN_PATH)