Files
Fighting_Rthythm_game/tests/test_player_combo_input.gd
2026-07-02 09:47:52 -07:00

317 lines
14 KiB
GDScript

extends SceneTree
const InputIntentScript := preload("res://scenes/components/input_intent.gd")
var failures: Array[String] = []
var requested_skills: Array[String] = []
var projectile_requests: Array[Dictionary] = []
var judged_labels: Array[String] = []
func _init() -> void:
_run.call_deferred()
func _run() -> void:
var scene: PackedScene = load("res://scenes/characters/player.tscn")
if scene == null:
push_error("Could not load player.tscn")
quit(1)
return
var player: Node = scene.instantiate()
root.add_child(player)
await process_frame
for node_name: String in ["InputComponent", "ComboWindow", "ActionResolver", "ActionExecutor", "MotionExecutor", "BurstComponent", "ChargeComponent", "EnergyComponent", "HealthComponent", "DamageReceiver", "DamageEmitter"]:
if not player.has_node(node_name):
failures.append("Player missing component %s" % node_name)
for signal_name: String in ["skill_requested", "energy_changed", "health_changed", "charge_changed", "projectile_requested"]:
if not player.has_signal(signal_name):
failures.append("Player missing signal %s" % signal_name)
player.connect("skill_requested", _on_skill_requested)
player.connect("projectile_requested", _on_projectile_requested)
_event_bus().connect("action_judged", _on_action_judged)
_expect_action("move_left", KEY_A)
_expect_action("move_right", KEY_D)
_expect_action("combo_w", KEY_W)
_expect_action("combo_a", KEY_A)
_expect_action("combo_d", KEY_D)
_expect_action("combo_s", KEY_S)
_expect_action("combo_space", KEY_SPACE)
_expect_bool(InputMap.has_action("player_space"), false, "player_space should be removed")
_expect_int(player.call("get_max_energy"), 10, "energy bar should have ten segments")
_expect_int(player.call("get_energy"), 0, "energy should start empty")
_expect_int(player.call("get_health"), player.call("get_max_health"), "health should start full")
player.call("submit_combo_input", "W", "perfect")
_expect_last_judgement("perfect", "W should display Perfect judgement")
_expect_array(player.call("get_combo_slots"), [&"W"], "W should enter the combo window")
_expect_last_skill("skill_w", "W should request skill_w")
_expect_string(str(player.get("current_skill_animation")), "warrior_w", "W should play warrior_w")
_expect_int(player.call("get_energy"), 0, "W should not add energy")
player.get_node("ComboWindow").clear(&"test-reset")
requested_skills.clear()
await _wait_for_action_settle()
player.call("submit_combo_input", "A", "good")
_expect_last_judgement("good", "A should display Good judgement")
player.get_node("ChargeComponent").finish_hold(&"A")
_expect_last_skill("skill_a", "A should request skill_a")
_expect_int(player.call("get_energy"), 0, "A should not reward during startup")
await _wait_for_active_phase()
_expect_int(player.call("get_energy"), 1, "A should reward one energy")
_expect_negative((player as CharacterBody2D).velocity.x, "A should lunge left")
player.get_node("ComboWindow").clear(&"test-reset")
await _wait_for_action_settle()
player.call("submit_combo_input", "D", "bad")
_expect_last_judgement("bad", "D should display Bad judgement")
player.get_node("ChargeComponent").finish_hold(&"D")
_expect_last_skill("skill_d", "D should request skill_d")
await _wait_for_active_phase()
_expect_int(player.call("get_energy"), 2, "D should reward one energy")
_expect_positive((player as CharacterBody2D).velocity.x, "D should lunge right")
player.get_node("ComboWindow").clear(&"test-reset")
requested_skills.clear()
await _wait_for_action_settle()
_reset_player_action_state(player)
_press_release_symbol(player, &"D")
await _wait_for_cancel_consumption()
_press_release_symbol(player, &"D")
await _wait_for_cancel_consumption()
player.call("_on_input_intent_created", _perfect_intent(&"D", &"pressed"))
_expect_last_skill("skill_ddd", "D+D+D should request the third D skill")
_expect_string(str(player.get("current_skill_animation")), "warrior_aaa", "D+D+D should start warrior_aaa")
player.call("_on_input_intent_created", _perfect_intent(&"D", &"released"))
await _wait_for_cancel_consumption()
_expect_array(player.call("get_combo_slots"), [&"D", &"D", &"D"], "D+D+D should keep the three input slots")
_expect_int(int(player.get("state")), Character.State.IDLE, "D+D+D should return player presentation to idle when the action ends")
_expect_string(_current_animation(player), "warrior_idle", "D+D+D should not leave warrior_aaa stuck after the action ends")
player.call("_on_input_intent_created", _perfect_intent(&"D", &"pressed"))
_expect_array(player.call("get_combo_slots"), [&"D", &"D", &"D", &"D"], "Fourth D should fill the four-slot window before clear")
_expect_last_skill("skill_d", "Fourth D after D+D+D should fall back to normal D")
_expect_string(str(player.get("current_skill_animation")), "warrior_a", "Fourth D should play normal D animation")
_expect_string(str(player.get("heading")), str(Vector2.RIGHT), "Fourth D should face right")
player.call("_on_input_intent_created", _perfect_intent(&"D", &"released"))
await create_timer(0.4).timeout
await process_frame
_expect_array(player.call("get_combo_slots"), [], "Fourth D full window should clear after display")
_reset_player_action_state(player)
player.call("_on_input_intent_created", _perfect_intent(&"A", &"pressed"))
player.call("_on_input_intent_created", _perfect_intent(&"A", &"released"))
player.call("_on_input_intent_created", _perfect_intent(&"D", &"pressed"))
_expect_array(player.call("get_combo_slots"), [&"A"], "A then immediate D should keep D outside the window during A startup")
_expect_string(str(player.get("heading")), str(Vector2.LEFT), "Pending D should not change heading before it is consumed")
player.call("_on_input_intent_created", _perfect_intent(&"D", &"released"))
await _wait_for_cancel_consumption()
_expect_array(player.call("get_combo_slots"), [&"A", &"D"], "A then D should enter the window only when the phase allows it")
_expect_last_skill("skill_d", "A then D unresolved prefix should fall back to normal D")
_expect_string(str(player.get("current_skill_animation")), "warrior_a", "A then D fallback should play normal D animation")
_expect_string(str(player.get("heading")), str(Vector2.RIGHT), "A then D fallback should face right")
await _wait_for_action_settle()
_expect_int(int(player.get("state")), Character.State.IDLE, "A then D fallback should settle back to idle")
_expect_string(_current_animation(player), "warrior_idle", "A then D fallback should not leave the previous presentation stuck")
_reset_player_action_state(player)
player.get_node("EnergyComponent").set_current(2)
player.call("submit_combo_input", "S", "miss")
_expect_last_judgement("miss", "S should display Miss judgement")
_expect_array(player.call("get_combo_slots"), [&"Ø"], "miss should enter the combo window as an explicit placeholder")
_expect_no_skill_requested("miss should not request a skill")
_expect_int(player.call("get_energy"), 2, "miss should not change energy")
await create_timer(0.4).timeout
await process_frame
_expect_array(player.call("get_combo_slots"), [&"Ø"], "miss should not clear the combo window by itself")
player.get_node("ComboWindow").clear(&"test-reset")
await _wait_for_action_settle()
player.call("submit_combo_input", "A", "perfect")
player.get_node("ChargeComponent").finish_hold(&"A")
player.call("submit_combo_input", "SP", "perfect")
_expect_array(player.call("get_combo_slots"), [&"A"], "A+Space should wait for cancel window before adding Space")
await _wait_for_cancel_consumption()
_expect_last_skill("skill_a_space", "A+Space should request clear-window skill")
_expect_array(player.call("get_combo_slots"), [&"A", &"SP"], "A+Space should stay visible before clear")
await _wait_for_action_settle()
_expect_array(player.call("get_combo_slots"), [], "A+Space should clear when the action finishes")
player.get_node("ComboWindow").clear(&"test-reset")
await _wait_for_action_settle()
player.get_node("EnergyComponent").set_current(3)
projectile_requests.clear()
requested_skills.clear()
player.call("submit_combo_input", "S", "perfect")
player.call("submit_combo_input", "SP", "perfect")
_expect_array(player.call("get_combo_slots"), [&"S"], "S+Space should wait for cancel window before adding Space")
await _wait_for_cancel_consumption()
_expect_last_skill("skill_s_projectile_1", "S+Space should request projectile skill")
await _wait_for_active_phase()
_expect_int(player.call("get_energy"), 0, "S+Space should spend three energy")
_expect_int(projectile_requests.size(), 1, "S+Space should emit one projectile request")
await _wait_for_action_settle()
player.get_node("ChargeComponent").cancel()
Input.action_press(&"move_left")
player.set("state", Character.State.IDLE)
player.set("velocity", Vector2.ZERO)
player.call("handle_input")
Input.action_release(&"move_left")
_expect_negative((player as CharacterBody2D).velocity.x, "move_left should move the player left")
player.queue_free()
_finish()
func _expect_action(action_name: String, key: Key) -> void:
if not InputMap.has_action(action_name):
failures.append("Missing input action: %s" % action_name)
return
for event: InputEvent in InputMap.action_get_events(action_name):
var key_event := event as InputEventKey
if key_event != null and (key_event.keycode == key or key_event.physical_keycode == key):
return
failures.append("Input action %s should be bound to %s" % [action_name, OS.get_keycode_string(key)])
func _expect_last_skill(expected: String, label: String) -> void:
if requested_skills.is_empty():
failures.append("%s: no skill requested" % label)
return
var actual := requested_skills[requested_skills.size() - 1]
if actual != expected:
failures.append("%s: expected %s, got %s" % [label, expected, actual])
func _expect_no_skill_requested(label: String) -> void:
if not requested_skills.is_empty():
failures.append("%s: expected no skill, got %s" % [label, requested_skills[requested_skills.size() - 1]])
func _expect_last_judgement(expected: String, label: String) -> void:
if judged_labels.is_empty():
failures.append("%s: no judgement displayed" % label)
return
var actual := judged_labels[judged_labels.size() - 1]
if actual != expected:
failures.append("%s: expected %s, got %s" % [label, expected, actual])
func _expect_array(actual: Array, expected: Array, label: String) -> void:
if actual != expected:
failures.append("%s: expected %s, got %s" % [label, expected, actual])
func _expect_string(actual: String, expected: String, label: String) -> void:
if actual != expected:
failures.append("%s: expected %s, got %s" % [label, expected, actual])
func _expect_int(actual: int, expected: int, label: String) -> void:
if actual != expected:
failures.append("%s: expected %d, got %d" % [label, expected, actual])
func _expect_bool(actual: bool, expected: bool, label: String) -> void:
if actual != expected:
failures.append("%s: expected %s, got %s" % [label, expected, actual])
func _expect_negative(actual: float, label: String) -> void:
if actual >= 0.0:
failures.append("%s: expected negative x velocity, got %.3f" % [label, actual])
func _expect_positive(actual: float, label: String) -> void:
if actual <= 0.0:
failures.append("%s: expected positive x velocity, got %.3f" % [label, actual])
func _on_skill_requested(skill_id: String) -> void:
requested_skills.append(skill_id)
func _on_projectile_requested(projectile_scene: PackedScene, spawn_position: Vector2, direction: Vector2) -> void:
projectile_requests.append({
"scene": projectile_scene,
"position": spawn_position,
"direction": direction,
})
func _on_action_judged(_action_name: StringName, rating: Dictionary) -> void:
judged_labels.append(str(rating.get("label", "")))
func _event_bus() -> Node:
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
func _reset_player_action_state(player: Node) -> void:
player.get_node("ComboWindow").clear(&"test-reset")
player.get_node("ActionController").call("_reset_to_idle")
player.get_node("ChargeComponent").cancel()
player.set("state", Character.State.IDLE)
player.set("attack_time_left", 0.0)
player.set("velocity", Vector2.ZERO)
player.set("heading", Vector2.RIGHT)
player.set("current_skill_animation", "warrior_idle")
var animation_player := player.get_node("AnimationPlayer") as AnimationPlayer
if animation_player != null and animation_player.has_animation("warrior_idle"):
animation_player.play("warrior_idle")
requested_skills.clear()
func _press_release_symbol(player: Node, symbol: StringName) -> void:
player.call("_on_input_intent_created", _perfect_intent(symbol, &"pressed"))
player.call("_on_input_intent_created", _perfect_intent(symbol, &"released"))
func _perfect_intent(symbol: StringName, event_type: StringName) -> RefCounted:
var rhythm_action: StringName = symbol.to_lower()
if symbol == &"SP":
rhythm_action = &"space"
var intent: RefCounted = InputIntentScript.create(symbol, rhythm_action, event_type, float(Time.get_ticks_msec()))
intent.judgement = {"label": "perfect", "diff": 0.0}
return intent
func _current_animation(player: Node) -> String:
var animation_player := player.get_node("AnimationPlayer") as AnimationPlayer
return animation_player.current_animation if animation_player != null else ""
func _wait_for_active_phase() -> void:
await create_timer(0.2).timeout
await physics_frame
func _wait_for_cancel_consumption() -> void:
await create_timer(0.55).timeout
await physics_frame
func _wait_for_action_settle() -> void:
await create_timer(0.7).timeout
await physics_frame
func _finish() -> void:
if failures.is_empty():
print("PASS player combo input")
quit(0)
else:
for failure: String in failures:
push_error(failure)
quit(1)