extends SceneTree var failures: Array[String] = [] var requested_skills: Array[String] = [] func _init() -> 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() get_root().add_child(player) var animation_player: AnimationPlayer = player.get_node("AnimationPlayer") as AnimationPlayer var supports_energy := player.has_method("get_energy") and player.has_method("get_max_energy") var supports_charge := player.has_method("get_charge") and player.has_method("get_max_charge") and player.has_method("is_charge_active") and player.has_method("is_charge_ready") if player.has_signal("skill_requested"): player.connect("skill_requested", _on_skill_requested) else: failures.append("Player missing skill_requested signal") if not player.has_signal("charge_changed"): failures.append("Player should expose charge_changed signal") if supports_charge: _expect_zero(player.call("get_charge"), "charge should start empty") _expect_bool(player.call("is_charge_ready"), false, "charge should not start ready") else: failures.append("Player should expose charge getters") if not player.has_signal("energy_changed"): failures.append("Player should expose energy_changed signal") if not player.has_signal("health_changed"): failures.append("Player should expose health_changed signal") if supports_energy: _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") else: failures.append("Player should expose get_energy and get_max_energy") if player.has_method("get_health") and player.has_method("get_max_health"): _expect_int(player.call("get_health"), player.call("get_max_health"), "health should start full") else: failures.append("Player should expose get_health and get_max_health") _expect_action("player_w", KEY_W) _expect_action("player_a", KEY_A) _expect_action("player_d", KEY_D) _expect_action("player_s", KEY_S) _expect_action("player_space", KEY_SPACE) var w_event := InputEventKey.new() w_event.pressed = true w_event.physical_keycode = KEY_W player.call("_input", w_event) w_event.echo = true player.call("_input", w_event) _expect_array(player.call("get_combo_slots"), ["W"], "W key press should enter once and ignore echo repeat") _expect_no_skill_requested("W alone should not request a skill") player.get("combo_window").clear("test-reset") var a_event := InputEventKey.new() a_event.pressed = true a_event.physical_keycode = KEY_A player.call("_input", a_event) _expect_array(player.call("get_combo_slots"), ["A"], "A alone should stay visible in the combo window") _expect_last_skill("skill_a", "A should request row 10 skill") _expect_string(str(player.get("current_skill_animation")), "warrior_a", "A should play row 10 animation") _expect_negative((player as CharacterBody2D).velocity.x, "A should lunge left") player.get("combo_window").clear("test-reset") requested_skills.clear() if supports_charge: player.call("_input", a_event) player.set("state", Character.State.IDLE) player.set("attack_time_left", 0.0) player.call("_process", 0.2) _expect_bool(player.call("is_charge_active"), true, "holding A after its animation should enter charge state") _expect_string(animation_player.current_animation, "warrior_idle", "holding A charge should keep idle animation") _expect_positive(player.call("get_charge"), "holding A should grow charge") var charge_effect := player.get_node_or_null("ChargeEffectSprite") as Sprite2D if charge_effect == null: failures.append("ChargeEffectSprite missing during A charge test") else: _expect_bool(charge_effect.visible, true, "holding A should show charge effect") requested_skills.clear() var a_release_event := InputEventKey.new() a_release_event.pressed = false a_release_event.physical_keycode = KEY_A player.call("_input", a_release_event) _expect_bool(player.call("is_charge_active"), false, "early A release should cancel charge") _expect_bool(player.call("is_charge_ready"), false, "early A release should not be ready") _expect_no_skill_requested("early A release should not request charge release skill") player.get("combo_window").clear("test-reset") requested_skills.clear() player.call("_input", a_event) player.set("state", Character.State.IDLE) player.set("attack_time_left", 0.0) player.call("_process", player.call("get_max_charge") + 0.1) _expect_bool(player.call("is_charge_ready"), true, "held A should become ready when charge is full") requested_skills.clear() player.call("_input", a_release_event) _expect_last_skill("skill_a_charge_release", "full A release should request charge release skill") _expect_string(str(player.get("current_skill_animation")), "warrior_charge_release", "full A release should play row 13 animation") _expect_negative((player as CharacterBody2D).velocity.x, "full A release should lunge left") _expect_bool(player.call("is_charge_active"), false, "full A release should leave charge state") player.get("combo_window").clear("test-reset") requested_skills.clear() player.call("_input", a_event) player.call("_input", a_event) _expect_array(player.call("get_combo_slots"), ["A", "A"], "two separate A presses should both enter the combo window") _expect_last_skill("skill_aa", "A+A should request row 11 skill") player.get("combo_window").clear("test-reset") requested_skills.clear() player.call("_input", a_event) Input.action_press("player_a") player.call("handle_input") Input.action_release("player_a") _expect_array(player.call("get_combo_slots"), ["A"], "one A key event should not be recorded again by physics polling") _expect_last_skill("skill_a", "single A key event should still be the last requested skill after physics polling") player.call("flush_pending_combo_clear") player.get("combo_window").clear("test-reset") requested_skills.clear() var d_event := InputEventKey.new() d_event.pressed = true d_event.physical_keycode = KEY_D player.call("_input", d_event) _expect_array(player.call("get_combo_slots"), ["D"], "D key press should enter the combo window") _expect_last_skill("skill_d", "D should request mirrored row 10 skill") _expect_string(str(player.get("current_skill_animation")), "warrior_a", "D should reuse row 10 animation") _expect_positive((player as CharacterBody2D).velocity.x, "D should lunge right") player.call("flush_pending_combo_clear") player.get("combo_window").clear("test-reset") requested_skills.clear() if supports_charge: player.call("_input", d_event) player.set("state", Character.State.IDLE) player.set("attack_time_left", 0.0) player.call("_process", player.call("get_max_charge") + 0.1) var d_release_event := InputEventKey.new() d_release_event.pressed = false d_release_event.physical_keycode = KEY_D requested_skills.clear() player.call("_input", d_release_event) _expect_last_skill("skill_d_charge_release", "full D release should request charge release skill") _expect_string(str(player.get("current_skill_animation")), "warrior_charge_release", "full D release should reuse row 13 animation") _expect_positive((player as CharacterBody2D).velocity.x, "full D release should lunge right") player.get("combo_window").clear("test-reset") requested_skills.clear() Input.action_press("player_a") player.set("state", Character.State.IDLE) player.set("velocity", Vector2.ZERO) player.call("handle_input") _expect_negative((player as CharacterBody2D).velocity.x, "A should move the player left") _expect_vector(player.get("heading"), Vector2.LEFT, "A should face left") _expect_array(player.call("get_combo_slots"), [], "physics-only movement polling should not write combo slots") Input.action_release("player_a") player.call("flush_pending_combo_clear") player.get("combo_window").clear("test-reset") Input.action_press("player_d") player.set("state", Character.State.IDLE) player.set("velocity", Vector2.ZERO) player.call("handle_input") _expect_positive((player as CharacterBody2D).velocity.x, "D should move the player right") _expect_vector(player.get("heading"), Vector2.RIGHT, "D should face right") Input.action_release("player_d") player.get("combo_window").clear("test-reset") var unhandled_s_event := InputEventKey.new() unhandled_s_event.pressed = true unhandled_s_event.physical_keycode = KEY_S player.call("_unhandled_input", unhandled_s_event) _expect_array(player.call("get_combo_slots"), ["S"], "unhandled S should enter S") _expect_no_skill_requested("S alone should not request a skill") player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "S", "miss") _expect_array(player.call("get_combo_slots"), ["Ø"], "miss should display Ø in the combo window") player.get("combo_window").clear("test-reset") if supports_energy: player.set("current_energy", 0) player.call("submit_combo_input", "W", "perfect") _expect_int(player.call("get_energy"), 2, "perfect input should add two energy segments") player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "A", "good") _expect_int(player.call("get_energy"), 3, "good input should add one energy segment") player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "D", "bad") _expect_int(player.call("get_energy"), 3, "bad input should not add energy") player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "S", "miss") _expect_int(player.call("get_energy"), 3, "miss input should not add energy") player.get("combo_window").clear("test-reset") player.set("current_energy", 9) player.call("submit_combo_input", "W", "perfect") _expect_int(player.call("get_energy"), 10, "energy should cap at ten segments") player.get("combo_window").clear("test-reset") requested_skills.clear() player.call("_play_skill_animation", "warrior_a", Vector2.LEFT) player.call("submit_combo_input", "A", "miss") _expect_array(player.call("get_combo_slots"), ["Ø"], "missed A should display Ø in the combo window") _expect_no_skill_requested("missed A should not request a skill") _expect_zero((player as CharacterBody2D).velocity.x, "missed A should stop horizontal lunge") _expect_int(int(player.get("state")), Character.State.IDLE, "missed A should return to idle state") _expect_string(animation_player.current_animation, "warrior_idle", "missed A should keep idle animation") player.get("combo_window").clear("test-reset") requested_skills.clear() player.call("_play_skill_animation", "warrior_a", Vector2.RIGHT) player.call("submit_combo_input", "D", "miss") _expect_array(player.call("get_combo_slots"), ["Ø"], "missed D should display Ø in the combo window") _expect_no_skill_requested("missed D should not request a skill") _expect_zero((player as CharacterBody2D).velocity.x, "missed D should stop horizontal lunge") _expect_int(int(player.get("state")), Character.State.IDLE, "missed D should return to idle state") _expect_string(animation_player.current_animation, "warrior_idle", "missed D should keep idle animation") player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "W", "perfect") player.call("submit_combo_input", "A", "good") _expect_array(player.call("get_combo_slots"), ["W", "A"], "W+A should stay visible after skill trigger") _expect_last_skill("skill_wa", "W+A should request row 7 skill") _expect_string(str(player.get("current_skill_animation")), "warrior_wa", "W+A should play row 7 animation") _expect_negative((player as CharacterBody2D).velocity.x, "W+A should lunge left") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), ["W", "A"], "W+A should not clear combo window") player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "W", "perfect") player.call("submit_combo_input", "D", "good") _expect_array(player.call("get_combo_slots"), ["W", "D"], "W+D should stay visible after skill trigger") _expect_last_skill("skill_wd", "W+D should request mirrored row 7 skill") _expect_string(str(player.get("current_skill_animation")), "warrior_wa", "W+D should reuse row 7 animation") _expect_positive((player as CharacterBody2D).velocity.x, "W+D should lunge right") _expect_vector(player.get("heading"), Vector2.RIGHT, "W+D should face right") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), ["W", "D"], "W+D should not clear combo window") player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "A") player.call("submit_combo_input", "A") player.call("submit_combo_input", "A") _expect_array(player.call("get_combo_slots"), ["A", "A", "A"], "A+A+A should stay visible after skill trigger") _expect_last_skill("skill_aaa", "A+A+A should request row 12 skill") _expect_string(str(player.get("current_skill_animation")), "warrior_aaa", "A+A+A should play row 12 animation") _expect_negative((player as CharacterBody2D).velocity.x, "A+A+A should lunge left") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), ["A", "A", "A"], "A+A+A should not clear combo window") player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "A") player.call("submit_combo_input", "A") player.call("submit_combo_input", "A") player.call("submit_combo_input", "A") _expect_array(player.call("get_combo_slots"), ["A", "A", "A", "A"], "fourth A should still fill the old four-slot window before clear") _expect_last_skill("skill_a", "fourth A after A+A+A should play normal A animation") _expect_string(str(player.get("current_skill_animation")), "warrior_a", "fourth A should fall back to row 10 animation") _expect_negative((player as CharacterBody2D).velocity.x, "fourth A should lunge left as a normal A") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), [], "fourth A full window should clear after display") player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "D") player.call("submit_combo_input", "D") player.call("submit_combo_input", "D") _expect_array(player.call("get_combo_slots"), ["D", "D", "D"], "D+D+D should stay visible after skill trigger") _expect_last_skill("skill_ddd", "D+D+D should request mirrored row 12 skill") _expect_string(str(player.get("current_skill_animation")), "warrior_aaa", "D+D+D should reuse row 12 animation") _expect_positive((player as CharacterBody2D).velocity.x, "D+D+D should lunge right") _expect_vector(player.get("heading"), Vector2.RIGHT, "D+D+D should face right") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), ["D", "D", "D"], "D+D+D should not clear combo window") player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "D") player.call("submit_combo_input", "D") player.call("submit_combo_input", "D") player.call("submit_combo_input", "D") _expect_array(player.call("get_combo_slots"), ["D", "D", "D", "D"], "fourth D should still fill the old four-slot window before clear") _expect_last_skill("skill_d", "fourth D after D+D+D should play normal D animation") _expect_string(str(player.get("current_skill_animation")), "warrior_a", "fourth D should fall back to row 10 animation") _expect_positive((player as CharacterBody2D).velocity.x, "fourth D should lunge right as a normal D") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), [], "fourth D full window should clear after display") player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "A") player.call("submit_combo_input", "SP") _expect_array(player.call("get_combo_slots"), ["A", "SP"], "A+Space should be visible before skill clear") _expect_last_skill("skill_a_space", "A+Space should request row 17 skill") _expect_string(str(player.get("current_skill_animation")), "warrior_a_space", "A+Space should play row 17 animation") _expect_negative((player as CharacterBody2D).velocity.x, "A+Space should lunge left") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), [], "A+Space should clear combo window") player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "D") player.call("submit_combo_input", "SP") _expect_array(player.call("get_combo_slots"), ["D", "SP"], "D+Space should be visible before skill clear") _expect_last_skill("skill_d_space", "D+Space should request mirrored row 17 skill") _expect_string(str(player.get("current_skill_animation")), "warrior_a_space", "D+Space should reuse row 17 animation") _expect_positive((player as CharacterBody2D).velocity.x, "D+Space should lunge right") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), [], "D+Space should clear combo window") player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "A") player.call("submit_combo_input", "SP") player.call("submit_combo_input", "SP") _expect_array(player.call("get_combo_slots"), ["A", "SP", "SP"], "A+Space+Space should cancel the pending A+Space clear and stay visible before its own clear") _expect_last_skill("skill_a_space_space", "A+Space+Space should request row 15 skill") _expect_string(str(player.get("current_skill_animation")), "warrior_a_space_space", "A+Space+Space should play row 15 animation") _expect_negative((player as CharacterBody2D).velocity.x, "A+Space+Space should lunge left") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), [], "A+Space+Space should clear combo window") player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "D") player.call("submit_combo_input", "SP") player.call("submit_combo_input", "SP") _expect_array(player.call("get_combo_slots"), ["D", "SP", "SP"], "D+Space+Space should cancel the pending D+Space clear and stay visible before its own clear") _expect_last_skill("skill_d_space_space", "D+Space+Space should request mirrored row 15 skill") _expect_string(str(player.get("current_skill_animation")), "warrior_a_space_space", "D+Space+Space should reuse row 15 animation") _expect_positive((player as CharacterBody2D).velocity.x, "D+Space+Space should lunge right") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), [], "D+Space+Space should clear combo window") player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "A") player.call("submit_combo_input", "A") player.call("submit_combo_input", "SP") _expect_array(player.call("get_combo_slots"), ["A", "A", "SP"], "A+A+Space should be visible before skill clear") _expect_last_skill("skill_aa_space", "A+A+Space should request clear skill") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), [], "A+A+Space should clear combo window") player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "D") player.call("submit_combo_input", "D") player.call("submit_combo_input", "SP") _expect_array(player.call("get_combo_slots"), ["D", "D", "SP"], "D+D+Space should be visible before skill clear") _expect_last_skill("skill_dd_space", "D+D+Space should request clear skill") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), [], "D+D+Space should clear combo window") player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "SP") _expect_array(player.call("get_combo_slots"), ["SP"], "Space should be visible before space clear") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), [], "Space should clear combo window") if supports_energy: player.set("current_energy", 0) player.call("submit_combo_input", "S", "perfect") player.call("submit_combo_input", "SP", "perfect") _expect_last_skill("skill_s_projectile_1", "S+Space should request projectile skill") _expect_projectile_count(1, "S+Space should fire one projectile") if supports_energy: _expect_int(player.call("get_energy"), 1, "S+Space should spend three energy after two perfect inputs") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), ["S", "SP"], "S+Space should not clear combo window") player.call("submit_combo_input", "SP", "perfect") _expect_last_skill("skill_s_projectile_2", "S+Space+Space should request projectile skill") _expect_projectile_count(2, "Second Space should fire another projectile") if supports_energy: _expect_int(player.call("get_energy"), 1, "S+Space+Space should spend two energy after the next perfect input") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), ["S", "SP", "SP"], "S+Space+Space should not clear combo window") player.call("submit_combo_input", "SP", "perfect") _expect_last_skill("skill_s_projectile_3", "S+Space+Space+Space should request projectile skill") _expect_projectile_count(3, "Third Space should fire another projectile") if supports_energy: _expect_int(player.call("get_energy"), 2, "S+Space+Space+Space should spend one energy after the next perfect input") _expect_array(player.call("get_combo_slots"), ["S", "SP", "SP", "SP"], "projectile chain should fill four slots before clear") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), [], "projectile chain should clear combo window because four slots are full") if supports_energy: requested_skills.clear() player.set("current_energy", 0) player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "S", "bad") player.call("submit_combo_input", "SP", "bad") _expect_no_skill_requested("S+Space should not execute when energy is insufficient") _expect_projectile_count(3, "insufficient energy should not fire another projectile") _expect_int(player.call("get_energy"), 0, "insufficient projectile attempt should leave energy at zero") player.get("combo_window").clear("test-reset") requested_skills.clear() if supports_energy: player.set("current_energy", 10) player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "S", "perfect") player.call("submit_combo_input", "A", "miss") player.call("submit_combo_input", "SP", "perfect") _expect_array(player.call("get_combo_slots"), ["S", "Ø", "SP"], "miss should remain visible between S and Space") _expect_no_skill_requested("S miss Space should not execute projectile skill") _expect_projectile_count(3, "S miss Space should not fire another projectile") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), [], "S miss Space should clear as a normal Space input") player.get("combo_window").clear("test-reset") player.call("submit_combo_input", "W") player.call("submit_combo_input", "W") player.call("submit_combo_input", "W") player.call("submit_combo_input", "W") _expect_array(player.call("get_combo_slots"), ["W", "W", "W", "W"], "four non-skill inputs should be visible before clear") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), [], "four non-skill inputs should clear combo window") 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_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_projectile_count(expected: int, label: String) -> void: var actual := _count_projectiles(get_root()) if actual != expected: failures.append("%s: expected %d, got %d" % [label, expected, actual]) func _count_projectiles(node: Node) -> int: var total := 1 if node.is_in_group("player_projectiles") else 0 for child: Node in node.get_children(): total += _count_projectiles(child) return total 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_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 _expect_bool(actual: bool, expected: bool, label: String) -> void: if actual != expected: failures.append("%s: expected %s, got %s" % [label, expected, actual]) func _expect_zero(actual: float, label: String) -> void: if not is_zero_approx(actual): failures.append("%s: expected zero x velocity, got %.3f" % [label, actual]) func _expect_vector(actual: Vector2, expected: Vector2, label: String) -> void: if not actual.is_equal_approx(expected): failures.append("%s: expected %s, got %s" % [label, expected, actual]) func _on_skill_requested(skill_id: String) -> void: requested_skills.append(skill_id) 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)