Files
2026-07-02 09:47:52 -07:00

354 lines
11 KiB
GDScript

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