class_name Player extends Character signal combo_window_changed(slots: Array) signal combo_window_cleared(reason: String) signal charge_changed(current: float, maximum: float, ready: bool, active: bool) signal energy_changed(current: int, maximum: int) signal health_changed(current: int, maximum: int) signal skill_requested(skill_id: String) signal projectile_requested(projectile_scene: PackedScene, spawn_position: Vector2, direction: Vector2) @export var combo_clear_display_time := 0.35 @export var max_health := 100 @export var current_health := 100 @export var max_energy := 10 @export var current_energy := 0 @onready var state_machine: Node = $StateMachine @onready var input_component: Node = $InputComponent @onready var combo_window: Node = $ComboWindow @onready var action_controller: Node = $ActionController @onready var motion_executor: Node = $MotionExecutor @onready var burst_component: Node = $BurstComponent @onready var charge_component: Node = $ChargeComponent @onready var energy_component: Node = $EnergyComponent @onready var health_component: Node = $HealthComponent @onready var damage_receiver: Area2D = $DamageReceiver var last_requested_skill_id := "" var current_skill_animation := "" var _held_symbols: Dictionary = {} func _ready() -> void: combo_window.clear_display_time = combo_clear_display_time input_component.intent_created.connect(_on_input_intent_created) action_controller.action_started.connect(_on_action_started) action_controller.action_active_started.connect(_on_action_active_started) action_controller.action_finished.connect(_on_action_finished) action_controller.action_rejected.connect(_on_action_rejected) combo_window.combo_updated.connect(_on_combo_updated) combo_window.combo_cleared.connect(_on_combo_cleared) charge_component.charge_changed.connect(_on_charge_component_changed) charge_component.release_requested.connect(_execute_charge_release) energy_component.energy_changed.connect(_on_energy_component_changed) health_component.health_changed.connect(_on_health_component_changed) damage_receiver.damage_received.connect(_on_damage_received) energy_component.set_values(current_energy, max_energy) health_component.set_values(current_health, max_health) func _process(delta: float) -> void: charge_component.tick(delta, state == State.IDLE) if charge_component.is_active(): state = State.IDLE attack_time_left = 0.0 stop_attack_motion() func _input(event: InputEvent) -> void: if input_component.handle_input_event(event): _mark_input_handled() func _unhandled_input(event: InputEvent) -> void: if input_component.handle_input_event(event): _mark_input_handled() func handle_input() -> void: if charge_component.is_active(): velocity = Vector2.ZERO return _apply_horizontal_movement() func set_heading() -> void: if velocity.x > 0.0: heading = Vector2.RIGHT elif velocity.x < 0.0: heading = Vector2.LEFT func get_combo_slots() -> Array[StringName]: return combo_window.get_slots() func get_energy() -> int: current_energy = energy_component.current return current_energy func get_max_energy() -> int: max_energy = energy_component.maximum return max_energy func get_health() -> int: current_health = health_component.current return current_health func get_max_health() -> int: max_health = health_component.maximum return max_health func get_charge() -> float: return charge_component.value func get_max_charge() -> float: return charge_component.maximum() func is_charge_active() -> bool: return charge_component.is_active() func is_charge_ready() -> bool: return charge_component.is_ready() func submit_combo_input(symbol: String, forced_rating := "") -> String: var data := _symbol_to_intent_data(symbol) if data.is_empty(): return "" var intent: RefCounted = load("res://scenes/components/input_intent.gd").create(data["symbol"], data["rhythm_action"], &"pressed", float(Time.get_ticks_msec())) if not forced_rating.is_empty(): intent.judgement = _rating_result(StringName(forced_rating), 0.0) action_controller.submit_intent(intent) return last_requested_skill_id func _symbol_to_intent_data(symbol: String) -> Dictionary: match symbol: "W": return {"symbol": &"W", "rhythm_action": &"w"} "A": return {"symbol": &"A", "rhythm_action": &"a"} "D": return {"symbol": &"D", "rhythm_action": &"d"} "S": return {"symbol": &"S", "rhythm_action": &"s"} "SP": return {"symbol": &"SP", "rhythm_action": &"space"} return {} func flush_pending_combo_clear() -> void: combo_window.flush_pending_clear() func _play_skill_animation(animation_name: String, displacement_direction := Vector2.ZERO, skill: Resource = null) -> void: current_skill_animation = animation_name anim_map[State.ATTACK] = animation_name state = State.ATTACK state_machine.change_state(state) attack_time_left = _animation_length(animation_name) if displacement_direction == Vector2.ZERO: stop_attack_motion() else: heading = displacement_direction if skill != null: motion_executor.execute(skill, displacement_direction, _rhythm_beat_time(), attack_lunge_speed) attack_time_left = motion_executor.duration begin_attack_motion(motion_executor.duration, motion_executor.velocity) if animation_player != null and animation_player.has_animation(animation_name): animation_player.play(animation_name) func _skill_displacement_direction(skill: Resource) -> Vector2: match StringName(str(skill.get("displacement"))): &"left": return Vector2.LEFT &"right": return Vector2.RIGHT return Vector2.ZERO func _begin_charge_hold(symbol: StringName, direction: Vector2) -> void: charge_component.begin_hold(symbol, direction) func _finish_charge_hold(symbol: StringName) -> void: charge_component.finish_hold(symbol) func _execute_charge_release(skill_id: StringName, direction: Vector2) -> void: last_requested_skill_id = str(skill_id) current_skill_animation = "warrior_charge_release" skill_requested.emit(last_requested_skill_id) _play_skill_animation(current_skill_animation, direction) func _cancel_missed_direction_action() -> void: stop_attack_motion() attack_time_left = 0.0 state = State.IDLE current_skill_animation = "warrior_idle" if animation_player != null and animation_player.has_animation("warrior_idle"): animation_player.play("warrior_idle") func _apply_horizontal_movement() -> void: if state != State.IDLE and state != State.WALK: return var direction: float = input_component.get_horizontal_axis() if direction < 0.0: heading = Vector2.LEFT elif direction > 0.0: heading = Vector2.RIGHT velocity.x = direction * speed func _animation_length(animation_name: String) -> float: if animation_player != null and animation_player.has_animation(animation_name): return maxf(0.1, animation_player.get_animation(animation_name).length) return attack_duration func _request_projectile(skill: Resource) -> void: var spawn_position := global_position + Vector2(heading.x * 36.0, -30.0) var projectile_scene := skill.get("projectile_scene") as PackedScene projectile_requested.emit(projectile_scene, spawn_position, heading) _event_bus().emit_signal("projectile_requested", projectile_scene, spawn_position, heading) func _rhythm_beat_time() -> float: var rhythm := get_tree().root.get_node_or_null("RhythmManager") if is_inside_tree() else null if rhythm != null: return float(rhythm.get("beat_time")) return 0.5 func _on_input_intent_created(intent) -> void: if intent.is_pressed(): _held_symbols[intent.symbol] = true elif intent.is_released(): _held_symbols.erase(intent.symbol) action_controller.submit_intent(intent) if intent.is_pressed() and (intent.symbol == &"A" or intent.symbol == &"D"): input_component.set_direction_suppressed(intent.symbol, true) if intent.is_released() and (intent.symbol == &"A" or intent.symbol == &"D"): input_component.set_direction_suppressed(intent.symbol, false) _finish_charge_hold(intent.symbol) func _on_action_started(action: Resource, intent) -> void: current_energy = energy_component.current last_requested_skill_id = str(action.get("id")) current_skill_animation = str(action.get("animation")) skill_requested.emit(last_requested_skill_id) var displacement_direction := _skill_displacement_direction(action) if displacement_direction != Vector2.ZERO: heading = displacement_direction _play_skill_animation(current_skill_animation) if intent.is_pressed() and _is_symbol_held(intent.symbol) and (intent.symbol == &"A" or intent.symbol == &"D"): _begin_charge_hold(intent.symbol, Vector2.LEFT if intent.symbol == &"A" else Vector2.RIGHT) func _on_action_active_started(action: Resource, intent) -> void: current_energy = energy_component.current _event_bus().emit_signal("skill_executed", action, StringName(str(intent.judgement.get("label", "perfect")))) _start_skill_motion(action) if bool(action.get("spawns_projectile")): _request_projectile(action) func _on_action_finished(_action: Resource) -> void: _set_idle_presentation() func _on_action_rejected(intent, reason: StringName) -> void: if reason == &"miss" and (intent.symbol == &"A" or intent.symbol == &"D"): _cancel_missed_direction_action() func _start_skill_motion(action: Resource) -> void: var displacement_direction := _skill_displacement_direction(action) if displacement_direction == Vector2.ZERO: return heading = displacement_direction motion_executor.execute(action, displacement_direction, _rhythm_beat_time(), attack_lunge_speed) attack_time_left = maxf(attack_time_left, motion_executor.duration) begin_attack_motion(motion_executor.duration, motion_executor.velocity) func _is_symbol_held(symbol: StringName) -> bool: return bool(_held_symbols.get(symbol, false)) func _set_idle_presentation() -> void: stop_attack_motion() attack_time_left = 0.0 state = State.IDLE state_machine.change_state(state) current_skill_animation = "warrior_idle" if animation_player != null and animation_player.has_animation("warrior_idle"): animation_player.play("warrior_idle") func _on_combo_updated(slots: Array[StringName]) -> void: combo_window_changed.emit(slots) func _on_combo_cleared(reason: StringName) -> void: combo_window_cleared.emit(str(reason)) func _on_charge_component_changed(current: float, maximum: float, ready: bool, active: bool) -> void: charge_changed.emit(current, maximum, ready, active) _event_bus().emit_signal("player_charge_changed", current, maximum, ready, active) func _on_energy_component_changed(current: int, maximum: int) -> void: current_energy = current max_energy = maximum energy_changed.emit(current, maximum) func _on_health_component_changed(current: int, maximum: int) -> void: current_health = current max_health = maximum health_changed.emit(current, maximum) func _on_damage_received(amount: int, _hit_type: StringName, _from: Vector2) -> void: health_component.apply_damage(amount) func _rating_result(label: StringName, offset_ms: float) -> Dictionary: return { "label": str(label), "diff": offset_ms / 1000.0, "abs_diff": absf(offset_ms / 1000.0), } func _mark_input_handled() -> void: var viewport := get_viewport() if viewport != null: viewport.set_input_as_handled() func _event_bus() -> Node: var root := get_tree().root var bus := root.get_node_or_null("EventBus") if bus == null: bus = load("res://autoload/event_bus.gd").new() bus.name = "EventBus" root.add_child(bus) return bus