Refactor rhythm action architecture
This commit is contained in:
@@ -131,3 +131,13 @@ func start_directional_air_attack(direction: Vector2) -> void:
|
||||
attack_time_left = air_attack_duration
|
||||
attack_lunge_time_left = air_attack_lunge_duration
|
||||
velocity = Vector2(attack_x * air_attack_lunge_speed, 0.0)
|
||||
|
||||
|
||||
func begin_attack_motion(duration: float, next_velocity: Vector2) -> void:
|
||||
attack_lunge_time_left = maxf(0.0, duration)
|
||||
velocity = next_velocity
|
||||
|
||||
|
||||
func stop_attack_motion() -> void:
|
||||
attack_lunge_time_left = 0.0
|
||||
velocity = Vector2.ZERO
|
||||
|
||||
@@ -7,109 +7,71 @@ 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 charge_duration := 1.1
|
||||
@export var max_health := 100
|
||||
@export var current_health := 100
|
||||
@export var max_energy := 10
|
||||
@export var current_energy := 0
|
||||
|
||||
var combo_window := ComboWindow.new()
|
||||
@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 combo_clear_timer: Timer
|
||||
var charge_value := 0.0
|
||||
var charge_ready := false
|
||||
var charge_active := false
|
||||
var _pending_combo_clear_reason := ""
|
||||
var _charge_effect_time := 0.0
|
||||
var _charge_animation_time := 0.0
|
||||
var _charge_hold_symbol := ""
|
||||
var _charge_hold_direction := Vector2.ZERO
|
||||
var _suppressed_movement_actions := {
|
||||
"player_a": false,
|
||||
"player_d": false,
|
||||
}
|
||||
var _last_combo_input_accepted := false
|
||||
var _held_symbols: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
combo_clear_timer = Timer.new()
|
||||
combo_clear_timer.one_shot = true
|
||||
combo_clear_timer.wait_time = combo_clear_display_time
|
||||
combo_clear_timer.timeout.connect(flush_pending_combo_clear)
|
||||
add_child(combo_clear_timer)
|
||||
combo_window.window_cleared.connect(_on_combo_window_cleared)
|
||||
_emit_combo_window_changed()
|
||||
_emit_charge_changed()
|
||||
_emit_health_changed()
|
||||
_emit_energy_changed()
|
||||
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:
|
||||
_update_charge(delta)
|
||||
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 _handle_combo_key_event(event):
|
||||
if input_component.handle_input_event(event):
|
||||
_mark_input_handled()
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if _handle_combo_key_event(event):
|
||||
if input_component.handle_input_event(event):
|
||||
_mark_input_handled()
|
||||
|
||||
|
||||
func _handle_combo_key_event(event: InputEvent) -> bool:
|
||||
var key_event := event as InputEventKey
|
||||
if key_event == null or key_event.echo:
|
||||
return false
|
||||
if not key_event.pressed:
|
||||
if _event_matches_key(key_event, KEY_A):
|
||||
_set_movement_action_suppressed("player_a", false)
|
||||
_finish_charge_hold("A")
|
||||
return true
|
||||
elif _event_matches_key(key_event, KEY_D):
|
||||
_set_movement_action_suppressed("player_d", false)
|
||||
_finish_charge_hold("D")
|
||||
return true
|
||||
return false
|
||||
if _event_matches_key(key_event, KEY_W):
|
||||
_submit_combo_input_from_event("W")
|
||||
return true
|
||||
elif _event_matches_key(key_event, KEY_A):
|
||||
_set_movement_action_suppressed("player_a", true)
|
||||
heading = Vector2.LEFT
|
||||
_submit_combo_input_from_event("A")
|
||||
if _last_combo_input_accepted:
|
||||
_begin_charge_hold("A", Vector2.LEFT)
|
||||
return true
|
||||
elif _event_matches_key(key_event, KEY_D):
|
||||
_set_movement_action_suppressed("player_d", true)
|
||||
heading = Vector2.RIGHT
|
||||
_submit_combo_input_from_event("D")
|
||||
if _last_combo_input_accepted:
|
||||
_begin_charge_hold("D", Vector2.RIGHT)
|
||||
return true
|
||||
elif _event_matches_key(key_event, KEY_S):
|
||||
_submit_combo_input_from_event("S")
|
||||
return true
|
||||
elif _event_matches_key(key_event, KEY_SPACE):
|
||||
_submit_combo_input_from_event("SP")
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func handle_input() -> void:
|
||||
if charge_active:
|
||||
if charge_component.is_active():
|
||||
velocity = Vector2.ZERO
|
||||
return
|
||||
_apply_horizontal_movement()
|
||||
if Input.is_action_just_pressed("jump") and not Input.is_action_just_pressed("player_space"):
|
||||
judge_rhythm_action("jump")
|
||||
if can_jump():
|
||||
start_jump()
|
||||
|
||||
|
||||
func set_heading() -> void:
|
||||
@@ -119,90 +81,260 @@ func set_heading() -> void:
|
||||
heading = Vector2.LEFT
|
||||
|
||||
|
||||
func get_combo_slots() -> Array[String]:
|
||||
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_value
|
||||
return charge_component.value
|
||||
|
||||
|
||||
func get_max_charge() -> float:
|
||||
return charge_duration
|
||||
return charge_component.maximum()
|
||||
|
||||
|
||||
func is_charge_active() -> bool:
|
||||
return charge_active
|
||||
return charge_component.is_active()
|
||||
|
||||
|
||||
func is_charge_ready() -> bool:
|
||||
return charge_ready
|
||||
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 _record_combo_symbol("W", "w", forced_rating)
|
||||
return {"symbol": &"W", "rhythm_action": &"w"}
|
||||
"A":
|
||||
return _record_combo_symbol("A", "a", forced_rating)
|
||||
return {"symbol": &"A", "rhythm_action": &"a"}
|
||||
"D":
|
||||
return _record_combo_symbol("D", "d", forced_rating)
|
||||
return {"symbol": &"D", "rhythm_action": &"d"}
|
||||
"S":
|
||||
return _record_combo_symbol("S", "s", forced_rating)
|
||||
return {"symbol": &"S", "rhythm_action": &"s"}
|
||||
"SP":
|
||||
return _record_combo_symbol("SP", "space", forced_rating)
|
||||
return ""
|
||||
return {"symbol": &"SP", "rhythm_action": &"space"}
|
||||
return {}
|
||||
|
||||
|
||||
func _record_combo_symbol(symbol: String, rhythm_action: String, forced_rating := "") -> String:
|
||||
_last_combo_input_accepted = false
|
||||
var rating := _rating_or_forced(judge_rhythm_action(rhythm_action), forced_rating)
|
||||
if not _record_rated_combo_input(symbol, rating):
|
||||
if symbol == "A" or symbol == "D":
|
||||
_cancel_missed_direction_action()
|
||||
return ""
|
||||
_last_combo_input_accepted = true
|
||||
var resolved := InputResolver.resolve(combo_window)
|
||||
if resolved.is_empty() and _pending_combo_clear_reason == "full":
|
||||
resolved = _resolve_full_window_fallback(symbol)
|
||||
if not resolved.is_empty():
|
||||
if not _execute_combo_skill(resolved):
|
||||
return ""
|
||||
_apply_skill_energy_reward(last_requested_skill_id)
|
||||
if symbol == "SP" and not _is_projectile_space_chain() and _pending_combo_clear_reason.is_empty():
|
||||
_schedule_combo_clear("space")
|
||||
return last_requested_skill_id if not resolved.is_empty() else ""
|
||||
func flush_pending_combo_clear() -> void:
|
||||
combo_window.flush_pending_clear()
|
||||
|
||||
|
||||
func _submit_combo_input_from_event(symbol: String) -> String:
|
||||
return submit_combo_input(symbol)
|
||||
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 _event_matches_key(event: InputEventKey, key: Key) -> bool:
|
||||
return event.physical_keycode == key or event.keycode == key
|
||||
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 _event_matches_any_key(event: InputEventKey, keys: Array[Key]) -> bool:
|
||||
for key: Key in keys:
|
||||
if _event_matches_key(event, key):
|
||||
return true
|
||||
return false
|
||||
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:
|
||||
@@ -211,338 +343,11 @@ func _mark_input_handled() -> void:
|
||||
viewport.set_input_as_handled()
|
||||
|
||||
|
||||
func _record_combo_direction(symbol: String, direction: Vector2, rhythm_action: String, forced_rating := "") -> void:
|
||||
heading = direction
|
||||
var rating := _rating_or_forced(judge_rhythm_action(rhythm_action), forced_rating)
|
||||
_record_rated_combo_input(symbol, rating)
|
||||
if state == State.IDLE or state == State.WALK:
|
||||
velocity.x = 0.0
|
||||
|
||||
|
||||
func _record_rated_combo_input(symbol: String, rating: Dictionary) -> bool:
|
||||
if str(rating.get("label", "perfect")) == "miss":
|
||||
_record_combo_input("Ø")
|
||||
return false
|
||||
_record_combo_input(symbol)
|
||||
return true
|
||||
|
||||
|
||||
func _record_combo_input(symbol: String) -> void:
|
||||
if combo_window.has_pending_clear() or not _pending_combo_clear_reason.is_empty():
|
||||
if _pending_combo_clear_reason.begins_with("skill:") and not combo_window.has_pending_clear():
|
||||
_cancel_pending_combo_clear()
|
||||
else:
|
||||
flush_pending_combo_clear()
|
||||
combo_window.record(symbol)
|
||||
_emit_combo_window_changed()
|
||||
var reason := combo_window.consume_pending_clear_reason()
|
||||
if not reason.is_empty():
|
||||
_schedule_combo_clear(reason)
|
||||
|
||||
|
||||
func _rating_or_forced(rating: Dictionary, forced_rating: String) -> Dictionary:
|
||||
if forced_rating.is_empty():
|
||||
return rating
|
||||
var forced := rating.duplicate()
|
||||
forced["label"] = forced_rating
|
||||
return forced
|
||||
|
||||
|
||||
func _execute_combo_skill(skill: Dictionary) -> bool:
|
||||
if not _spend_skill_energy(skill):
|
||||
return false
|
||||
last_requested_skill_id = str(skill.get("id", ""))
|
||||
current_skill_animation = str(skill.get("animation", "warrior_idle"))
|
||||
skill_requested.emit(last_requested_skill_id)
|
||||
judge_rhythm_action(last_requested_skill_id)
|
||||
_play_skill_animation(current_skill_animation, _skill_displacement_direction(skill))
|
||||
if bool(skill.get("projectile", false)):
|
||||
_fire_projectile()
|
||||
_emit_combo_window_changed()
|
||||
if bool(skill.get("clear_window", false)):
|
||||
_schedule_combo_clear("skill:%s" % last_requested_skill_id)
|
||||
return true
|
||||
|
||||
|
||||
func _play_skill_animation(animation_name: String, displacement_direction := Vector2.ZERO) -> void:
|
||||
var player_animation := _get_animation_player()
|
||||
anim_map[State.ATTACK] = animation_name
|
||||
state = State.ATTACK
|
||||
attack_time_left = _animation_length(animation_name)
|
||||
if displacement_direction == Vector2.ZERO:
|
||||
attack_lunge_time_left = 0.0
|
||||
velocity = Vector2.ZERO
|
||||
else:
|
||||
heading = displacement_direction
|
||||
attack_lunge_time_left = attack_lunge_duration
|
||||
velocity = Vector2(displacement_direction.x * attack_lunge_speed, 0.0)
|
||||
if player_animation != null and player_animation.has_animation(animation_name):
|
||||
player_animation.play(animation_name)
|
||||
|
||||
|
||||
func _skill_displacement_direction(skill: Dictionary) -> Vector2:
|
||||
match str(skill.get("displacement", "")):
|
||||
"left":
|
||||
return Vector2.LEFT
|
||||
"right":
|
||||
return Vector2.RIGHT
|
||||
return Vector2.ZERO
|
||||
|
||||
|
||||
func _apply_skill_energy_reward(skill_id: String) -> void:
|
||||
match skill_id:
|
||||
"skill_a", "skill_aa", "skill_aaa", "skill_d", "skill_dd", "skill_ddd":
|
||||
_change_energy(1)
|
||||
|
||||
|
||||
func _spend_skill_energy(skill: Dictionary) -> bool:
|
||||
var energy_cost := int(skill.get("energy_cost", 0))
|
||||
if energy_cost <= 0:
|
||||
return true
|
||||
if current_energy < energy_cost:
|
||||
return false
|
||||
_change_energy(-energy_cost)
|
||||
return true
|
||||
|
||||
|
||||
func _change_energy(delta: int) -> void:
|
||||
var next_energy := clampi(current_energy + delta, 0, max_energy)
|
||||
if next_energy == current_energy:
|
||||
return
|
||||
current_energy = next_energy
|
||||
_emit_energy_changed()
|
||||
|
||||
|
||||
func _begin_charge_hold(symbol: String, direction: Vector2) -> void:
|
||||
_charge_hold_symbol = symbol
|
||||
_charge_hold_direction = direction
|
||||
|
||||
|
||||
func _finish_charge_hold(symbol: String) -> void:
|
||||
if _charge_hold_symbol != symbol:
|
||||
return
|
||||
var release_ready := charge_active and charge_ready
|
||||
var release_direction := _charge_hold_direction
|
||||
var release_skill := "skill_a_charge_release" if symbol == "A" else "skill_d_charge_release"
|
||||
_cancel_charge()
|
||||
if release_ready:
|
||||
_execute_charge_release(release_skill, release_direction)
|
||||
|
||||
|
||||
func _update_charge(delta: float) -> void:
|
||||
if _charge_hold_symbol.is_empty():
|
||||
return
|
||||
if not charge_active:
|
||||
if state != State.IDLE:
|
||||
return
|
||||
_start_charge()
|
||||
if not charge_active:
|
||||
return
|
||||
state = State.IDLE
|
||||
attack_time_left = 0.0
|
||||
attack_lunge_time_left = 0.0
|
||||
velocity = Vector2.ZERO
|
||||
_update_charge_animation(delta)
|
||||
charge_value = minf(charge_duration, charge_value + delta)
|
||||
charge_ready = charge_value >= charge_duration
|
||||
_update_charge_effect(delta)
|
||||
_emit_charge_changed()
|
||||
|
||||
|
||||
func _start_charge() -> void:
|
||||
charge_active = true
|
||||
charge_value = 0.0
|
||||
charge_ready = false
|
||||
_charge_effect_time = 0.0
|
||||
_charge_animation_time = 0.0
|
||||
_play_charge_animation("warrior_charge_intro")
|
||||
_update_charge_effect(0.0)
|
||||
_emit_charge_changed()
|
||||
|
||||
|
||||
func _cancel_charge() -> void:
|
||||
_charge_hold_symbol = ""
|
||||
_charge_hold_direction = Vector2.ZERO
|
||||
charge_active = false
|
||||
charge_value = 0.0
|
||||
charge_ready = false
|
||||
_charge_animation_time = 0.0
|
||||
_set_charge_effect_visible(false)
|
||||
_emit_charge_changed()
|
||||
|
||||
|
||||
func _execute_charge_release(skill_id: String, direction: Vector2) -> void:
|
||||
last_requested_skill_id = skill_id
|
||||
current_skill_animation = "warrior_charge_release"
|
||||
skill_requested.emit(last_requested_skill_id)
|
||||
_play_skill_animation(current_skill_animation, direction)
|
||||
|
||||
|
||||
func _update_charge_effect(delta: float) -> void:
|
||||
var sprite := _get_charge_effect_sprite()
|
||||
if sprite == null:
|
||||
return
|
||||
sprite.visible = charge_active
|
||||
if not charge_active:
|
||||
return
|
||||
_charge_effect_time += delta
|
||||
sprite.frame = int(_charge_effect_time * 12.0) % 5
|
||||
|
||||
|
||||
func _update_charge_animation(delta: float) -> void:
|
||||
_charge_animation_time += delta
|
||||
var intro_length := _animation_length("warrior_charge_intro")
|
||||
if _charge_animation_time < intro_length:
|
||||
_play_charge_animation("warrior_charge_intro")
|
||||
else:
|
||||
_play_charge_animation("warrior_charge_loop")
|
||||
|
||||
|
||||
func _play_charge_animation(animation_name: String) -> void:
|
||||
var player_animation := _get_animation_player()
|
||||
if player_animation != null and player_animation.has_animation(animation_name) and player_animation.current_animation != animation_name:
|
||||
player_animation.play(animation_name)
|
||||
|
||||
|
||||
func _set_charge_effect_visible(is_visible: bool) -> void:
|
||||
var sprite := _get_charge_effect_sprite()
|
||||
if sprite != null:
|
||||
sprite.visible = is_visible
|
||||
|
||||
|
||||
func _get_charge_effect_sprite() -> Sprite2D:
|
||||
if has_node("ChargeEffectSprite"):
|
||||
return get_node("ChargeEffectSprite") as Sprite2D
|
||||
return null
|
||||
|
||||
|
||||
func _resolve_full_window_fallback(symbol: String) -> Dictionary:
|
||||
if symbol != "A" and symbol != "D":
|
||||
return {}
|
||||
return InputResolver.resolve_pattern(symbol)
|
||||
|
||||
|
||||
func _cancel_missed_direction_action() -> void:
|
||||
velocity = Vector2.ZERO
|
||||
attack_lunge_time_left = 0.0
|
||||
attack_time_left = 0.0
|
||||
state = State.IDLE
|
||||
anim_map[State.ATTACK] = "warrior_a"
|
||||
var player_animation := _get_animation_player()
|
||||
if player_animation != null and player_animation.has_animation("warrior_idle"):
|
||||
player_animation.play("warrior_idle")
|
||||
|
||||
|
||||
func _is_projectile_space_chain() -> bool:
|
||||
var pattern := combo_window.get_contiguous_pattern()
|
||||
return pattern == "SSP" or pattern == "SSPSP" or pattern == "SSPSPSP"
|
||||
|
||||
|
||||
func _apply_horizontal_movement() -> void:
|
||||
if charge_active:
|
||||
velocity.x = 0.0
|
||||
return
|
||||
if state != State.IDLE and state != State.WALK:
|
||||
return
|
||||
var direction := 0.0
|
||||
if _is_movement_action_pressed("player_a"):
|
||||
direction -= 1.0
|
||||
if _is_movement_action_pressed("player_d"):
|
||||
direction += 1.0
|
||||
if direction < 0.0:
|
||||
heading = Vector2.LEFT
|
||||
elif direction > 0.0:
|
||||
heading = Vector2.RIGHT
|
||||
velocity.x = direction * speed
|
||||
|
||||
|
||||
func _set_movement_action_suppressed(action_name: String, suppressed: bool) -> void:
|
||||
_suppressed_movement_actions[action_name] = suppressed
|
||||
|
||||
|
||||
func _is_movement_action_pressed(action_name: String) -> bool:
|
||||
return Input.is_action_pressed(action_name) and not bool(_suppressed_movement_actions.get(action_name, false))
|
||||
|
||||
|
||||
func _animation_length(animation_name: String) -> float:
|
||||
var player_animation := _get_animation_player()
|
||||
if player_animation != null and player_animation.has_animation(animation_name):
|
||||
return maxf(0.1, player_animation.get_animation(animation_name).length)
|
||||
return attack_duration
|
||||
|
||||
|
||||
func _get_animation_player() -> AnimationPlayer:
|
||||
if animation_player == null and has_node("AnimationPlayer"):
|
||||
animation_player = get_node("AnimationPlayer") as AnimationPlayer
|
||||
return animation_player
|
||||
|
||||
|
||||
func _fire_projectile() -> void:
|
||||
var projectile := PlayerProjectile.new()
|
||||
projectile.direction = heading
|
||||
projectile.global_position = global_position + Vector2(heading.x * 36.0, -30.0)
|
||||
var parent := get_parent()
|
||||
if parent != null:
|
||||
parent.add_child(projectile)
|
||||
else:
|
||||
add_child(projectile)
|
||||
projectile.add_to_group("player_projectiles")
|
||||
|
||||
|
||||
func _cancel_pending_combo_clear() -> void:
|
||||
_pending_combo_clear_reason = ""
|
||||
if combo_clear_timer != null:
|
||||
combo_clear_timer.stop()
|
||||
|
||||
|
||||
func _schedule_combo_clear(reason: String) -> void:
|
||||
_pending_combo_clear_reason = reason
|
||||
if combo_clear_timer == null:
|
||||
return
|
||||
combo_clear_timer.stop()
|
||||
combo_clear_timer.wait_time = combo_clear_display_time
|
||||
combo_clear_timer.start()
|
||||
|
||||
|
||||
func flush_pending_combo_clear() -> void:
|
||||
var reason := _pending_combo_clear_reason
|
||||
if reason.is_empty():
|
||||
reason = combo_window.consume_pending_clear_reason()
|
||||
else:
|
||||
combo_window.consume_pending_clear_reason()
|
||||
if reason.is_empty():
|
||||
return
|
||||
_pending_combo_clear_reason = ""
|
||||
if combo_clear_timer != null:
|
||||
combo_clear_timer.stop()
|
||||
combo_window.clear(reason)
|
||||
|
||||
|
||||
func _on_combo_window_cleared(reason: String) -> void:
|
||||
combo_window_cleared.emit(reason)
|
||||
_emit_combo_window_changed()
|
||||
|
||||
|
||||
func _emit_combo_window_changed() -> void:
|
||||
combo_window_changed.emit(combo_window.get_slots())
|
||||
|
||||
|
||||
func _emit_charge_changed() -> void:
|
||||
charge_changed.emit(charge_value, charge_duration, charge_ready, charge_active)
|
||||
|
||||
|
||||
func _emit_energy_changed() -> void:
|
||||
energy_changed.emit(current_energy, max_energy)
|
||||
|
||||
|
||||
func _emit_health_changed() -> void:
|
||||
health_changed.emit(current_health, max_health)
|
||||
|
||||
|
||||
func judge_rhythm_action(action_name: String) -> Dictionary:
|
||||
if not is_inside_tree():
|
||||
return {"label": "perfect"}
|
||||
var conductor: Node = get_tree().get_first_node_in_group("rhythm_conductor")
|
||||
if conductor != null and conductor.has_method("judge_action"):
|
||||
return conductor.call("judge_action", action_name) as Dictionary
|
||||
return {"label": "perfect"}
|
||||
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
|
||||
|
||||
@@ -4,6 +4,19 @@
|
||||
[ext_resource type="Texture2D" uid="uid://bbkamgcdsw5g6" path="res://assets/art/characters/warrior_man_sheet.png" id="2_yewv4"]
|
||||
[ext_resource type="Texture2D" uid="uid://womoel71g8ae" path="res://assets/art/characters/warrior_woman_sheet.png" id="3_dyp2m"]
|
||||
[ext_resource type="Texture2D" uid="uid://1p2uqgg1jole" path="res://assets/art/effects/effect_hp_mp_sheet.png" id="4_atpat"]
|
||||
[ext_resource type="Script" path="res://scenes/components/state_machine.gd" id="5_state_machine"]
|
||||
[ext_resource type="Script" path="res://scenes/components/input_component.gd" id="6_input_component"]
|
||||
[ext_resource type="Script" path="res://scenes/components/energy_component.gd" id="8_energy_component"]
|
||||
[ext_resource type="Script" path="res://scenes/components/health_component.gd" id="9_health_component"]
|
||||
[ext_resource type="Script" path="res://scenes/components/damage_receiver.gd" id="10_damage_receiver"]
|
||||
[ext_resource type="Script" path="res://scenes/components/damage_emitter.gd" id="11_damage_emitter"]
|
||||
[ext_resource type="Script" path="res://scenes/components/combo_window.gd" id="12_combo_window"]
|
||||
[ext_resource type="Script" path="res://scenes/combat/action_resolver.gd" id="13_action_resolver"]
|
||||
[ext_resource type="Script" path="res://scenes/components/motion_executor.gd" id="14_motion_executor"]
|
||||
[ext_resource type="Script" path="res://scenes/components/burst_component.gd" id="15_burst_component"]
|
||||
[ext_resource type="Script" path="res://scenes/components/charge_component.gd" id="16_charge_component"]
|
||||
[ext_resource type="Script" path="res://scenes/components/action_executor.gd" id="17_action_executor"]
|
||||
[ext_resource type="Script" path="res://scenes/components/action_controller.gd" id="18_action_controller"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_player"]
|
||||
size = Vector2(16, 36)
|
||||
@@ -596,7 +609,7 @@ tracks/4/keys = {
|
||||
|
||||
[sub_resource type="Animation" id="Animation_6eyoc"]
|
||||
resource_name = "warrior_s"
|
||||
length = 0.71428573
|
||||
length = 0.21428573
|
||||
step = 0.071428575
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
@@ -653,10 +666,10 @@ tracks/4/path = NodePath("CharacterSprite:frame")
|
||||
tracks/4/interp = 1
|
||||
tracks/4/loop_wrap = true
|
||||
tracks/4/keys = {
|
||||
"times": PackedFloat32Array(0, 0.071428575, 0.14285715, 0.21428572, 0.2857143, 0.35714287, 0.42857143, 0.5, 0.5714286, 0.64285713),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
|
||||
"times": PackedFloat32Array(0, 0.071428575, 0.14285715),
|
||||
"transitions": PackedFloat32Array(1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [128, 129, 130, 131, 132, 133, 134, 135, 136, 137]
|
||||
"values": [128, 129, 130]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_eojnx"]
|
||||
@@ -905,3 +918,64 @@ position = Vector2(0, -8)
|
||||
texture = ExtResource("4_atpat")
|
||||
hframes = 5
|
||||
vframes = 2
|
||||
|
||||
[node name="StateMachine" type="Node" parent="."]
|
||||
script = ExtResource("5_state_machine")
|
||||
|
||||
[node name="InputComponent" type="Node" parent="."]
|
||||
script = ExtResource("6_input_component")
|
||||
|
||||
[node name="ComboWindow" type="Node" parent="."]
|
||||
script = ExtResource("12_combo_window")
|
||||
|
||||
[node name="ActionResolver" type="Node" parent="."]
|
||||
script = ExtResource("13_action_resolver")
|
||||
|
||||
[node name="ActionExecutor" type="Node" parent="."]
|
||||
script = ExtResource("17_action_executor")
|
||||
energy_component_path = NodePath("../EnergyComponent")
|
||||
damage_emitter_path = NodePath("../DamageEmitter")
|
||||
|
||||
[node name="ActionController" type="Node" parent="."]
|
||||
script = ExtResource("18_action_controller")
|
||||
combo_window_path = NodePath("../ComboWindow")
|
||||
action_resolver_path = NodePath("../ActionResolver")
|
||||
action_executor_path = NodePath("../ActionExecutor")
|
||||
state_machine_path = NodePath("../StateMachine")
|
||||
burst_component_path = NodePath("../BurstComponent")
|
||||
|
||||
[node name="MotionExecutor" type="Node" parent="."]
|
||||
script = ExtResource("14_motion_executor")
|
||||
|
||||
[node name="BurstComponent" type="Node" parent="."]
|
||||
script = ExtResource("15_burst_component")
|
||||
|
||||
[node name="ChargeComponent" type="Node" parent="."]
|
||||
script = ExtResource("16_charge_component")
|
||||
animation_player_path = NodePath("../AnimationPlayer")
|
||||
effect_sprite_path = NodePath("../ChargeEffectSprite")
|
||||
|
||||
[node name="EnergyComponent" type="Node" parent="."]
|
||||
script = ExtResource("8_energy_component")
|
||||
|
||||
[node name="HealthComponent" type="Node" parent="."]
|
||||
script = ExtResource("9_health_component")
|
||||
|
||||
[node name="DamageReceiver" type="Area2D" parent="."]
|
||||
collision_layer = 2
|
||||
collision_mask = 0
|
||||
script = ExtResource("10_damage_receiver")
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="DamageReceiver"]
|
||||
position = Vector2(0, -18)
|
||||
shape = SubResource("RectangleShape2D_player")
|
||||
|
||||
[node name="DamageEmitter" type="Area2D" parent="."]
|
||||
collision_layer = 8
|
||||
collision_mask = 4
|
||||
monitoring = false
|
||||
script = ExtResource("11_damage_emitter")
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="DamageEmitter"]
|
||||
position = Vector2(22, -18)
|
||||
shape = SubResource("RectangleShape2D_player")
|
||||
|
||||
Reference in New Issue
Block a user