354 lines
11 KiB
GDScript
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
|