diff --git a/scenes/characters/player.gd b/scenes/characters/player.gd index 77106a5..2b1b969 100644 --- a/scenes/characters/player.gd +++ b/scenes/characters/player.gd @@ -24,8 +24,14 @@ 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 func _ready() -> void: @@ -61,9 +67,11 @@ func _handle_combo_key_event(event: InputEvent) -> bool: 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 @@ -71,15 +79,17 @@ func _handle_combo_key_event(event: InputEvent) -> bool: _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 - var skill_id := _submit_combo_input_from_event("A") - if skill_id == "skill_a": + _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 - var skill_id := _submit_combo_input_from_event("D") - if skill_id == "skill_d": + _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): @@ -96,7 +106,7 @@ func handle_input() -> void: velocity = Vector2.ZERO return _apply_horizontal_movement() - if Input.is_action_just_pressed("jump"): + 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() @@ -161,18 +171,20 @@ func submit_combo_input(symbol: String, forced_rating := "") -> String: 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) - _apply_energy_reward(str(rating.get("label", "perfect"))) 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 "" @@ -277,11 +289,9 @@ func _skill_displacement_direction(skill: Dictionary) -> Vector2: return Vector2.ZERO -func _apply_energy_reward(rating_label: String) -> void: - match rating_label: - "perfect": - _change_energy(2) - "good": +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) @@ -332,9 +342,7 @@ func _update_charge(delta: float) -> void: attack_time_left = 0.0 attack_lunge_time_left = 0.0 velocity = Vector2.ZERO - var player_animation := _get_animation_player() - if player_animation != null and player_animation.has_animation("warrior_idle") and player_animation.current_animation != "warrior_idle": - player_animation.play("warrior_idle") + _update_charge_animation(delta) charge_value = minf(charge_duration, charge_value + delta) charge_ready = charge_value >= charge_duration _update_charge_effect(delta) @@ -346,6 +354,8 @@ func _start_charge() -> void: 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() @@ -356,6 +366,7 @@ func _cancel_charge() -> void: charge_active = false charge_value = 0.0 charge_ready = false + _charge_animation_time = 0.0 _set_charge_effect_visible(false) _emit_charge_changed() @@ -378,6 +389,21 @@ func _update_charge_effect(delta: float) -> void: 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: @@ -419,9 +445,9 @@ func _apply_horizontal_movement() -> void: if state != State.IDLE and state != State.WALK: return var direction := 0.0 - if Input.is_action_pressed("player_a"): + if _is_movement_action_pressed("player_a"): direction -= 1.0 - if Input.is_action_pressed("player_d"): + if _is_movement_action_pressed("player_d"): direction += 1.0 if direction < 0.0: heading = Vector2.LEFT @@ -430,6 +456,14 @@ func _apply_horizontal_movement() -> void: 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): diff --git a/scenes/characters/player.tscn b/scenes/characters/player.tscn index a12ddd0..6ffc18b 100644 --- a/scenes/characters/player.tscn +++ b/scenes/characters/player.tscn @@ -332,9 +332,9 @@ tracks/4/keys = { "values": [176, 177, 178, 179, 180, 181, 182, 183] } -[sub_resource type="Animation" id="Animation_jk2m4"] -resource_name = "warrior_charge_release" -length = 1.3333334 +[sub_resource type="Animation" id="Animation_charge_intro"] +resource_name = "warrior_charge_intro" +length = 0.6666667 step = 0.083333336 tracks/0/type = "value" tracks/0/imported = false @@ -391,10 +391,141 @@ tracks/4/path = NodePath("CharacterSprite:frame") tracks/4/interp = 1 tracks/4/loop_wrap = true tracks/4/keys = { -"times": PackedFloat32Array(0, 0.083333336, 0.16666667, 0.25, 0.33333334, 0.41666666, 0.5, 0.5833333, 0.6666667, 0.75, 0.8333333, 0.9166667, 1, 1.0833334, 1.1666666, 1.25), -"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"times": PackedFloat32Array(0, 0.083333336, 0.16666667, 0.25, 0.33333334, 0.41666666, 0.5, 0.5833333), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1), "update": 1, -"values": [192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207] +"values": [192, 193, 194, 195, 196, 197, 198, 199] +} + +[sub_resource type="Animation" id="Animation_charge_loop"] +resource_name = "warrior_charge_loop" +length = 0.33333334 +loop_mode = 1 +step = 0.083333336 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("CharacterSprite:texture") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [ExtResource("3_dyp2m")] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("CharacterSprite:hframes") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [16] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("CharacterSprite:vframes") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [25] +} +tracks/3/type = "value" +tracks/3/imported = false +tracks/3/enabled = true +tracks/3/path = NodePath("CharacterSprite:offset") +tracks/3/interp = 1 +tracks/3/loop_wrap = true +tracks/3/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [Vector2(-40, -48)] +} +tracks/4/type = "value" +tracks/4/imported = false +tracks/4/enabled = true +tracks/4/path = NodePath("CharacterSprite:frame") +tracks/4/interp = 1 +tracks/4/loop_wrap = true +tracks/4/keys = { +"times": PackedFloat32Array(0, 0.083333336, 0.16666667, 0.25), +"transitions": PackedFloat32Array(1, 1, 1, 1), +"update": 1, +"values": [196, 197, 198, 199] +} + +[sub_resource type="Animation" id="Animation_jk2m4"] +resource_name = "warrior_charge_release" +length = 0.6666667 +step = 0.083333336 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("CharacterSprite:texture") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [ExtResource("3_dyp2m")] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("CharacterSprite:hframes") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [16] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("CharacterSprite:vframes") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [25] +} +tracks/3/type = "value" +tracks/3/imported = false +tracks/3/enabled = true +tracks/3/path = NodePath("CharacterSprite:offset") +tracks/3/interp = 1 +tracks/3/loop_wrap = true +tracks/3/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [Vector2(-40, -48)] +} +tracks/4/type = "value" +tracks/4/imported = false +tracks/4/enabled = true +tracks/4/path = NodePath("CharacterSprite:frame") +tracks/4/interp = 1 +tracks/4/loop_wrap = true +tracks/4/keys = { +"times": PackedFloat32Array(0, 0.083333336, 0.16666667, 0.25, 0.33333334, 0.41666666, 0.5, 0.5833333), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [200, 201, 202, 203, 204, 205, 206, 207] } [sub_resource type="Animation" id="Animation_kqtwu"] @@ -730,6 +861,8 @@ _data = { &"warrior_a_space_space": SubResource("Animation_2l4js"), &"warrior_aa": SubResource("Animation_dyp2m"), &"warrior_aaa": SubResource("Animation_atpat"), +&"warrior_charge_intro": SubResource("Animation_charge_intro"), +&"warrior_charge_loop": SubResource("Animation_charge_loop"), &"warrior_charge_release": SubResource("Animation_jk2m4"), &"warrior_idle": SubResource("Animation_kqtwu"), &"warrior_s": SubResource("Animation_6eyoc"), diff --git a/scenes/combat/input_resolver.gd b/scenes/combat/input_resolver.gd index 843654b..f9832d8 100644 --- a/scenes/combat/input_resolver.gd +++ b/scenes/combat/input_resolver.gd @@ -2,6 +2,12 @@ class_name InputResolver extends RefCounted const SKILLS := { + "W": { + "type": "skill", + "id": "skill_w", + "animation": "warrior_w", + "clear_window": false, + }, "A": { "type": "skill", "id": "skill_a", diff --git a/tests/test_combo_window.gd b/tests/test_combo_window.gd index ed46a03..abb471c 100644 --- a/tests/test_combo_window.gd +++ b/tests/test_combo_window.gd @@ -34,7 +34,9 @@ func _init() -> void: window.record("W") var resolved: Dictionary = resolver_script.resolve(window) - _expect_bool(resolved.is_empty(), true, "W alone should not resolve a skill") + _expect_string(str(resolved.get("id", "")), "skill_w", "W alone skill") + _expect_string(str(resolved.get("animation", "")), "warrior_w", "W should play row 6 animation") + _expect_bool(bool(resolved.get("clear_window", true)), false, "W skill should not clear window") window.record("A") resolved = resolver_script.resolve(window) _expect_string(str(resolved.get("id", "")), "skill_wa", "W+A skill") @@ -50,7 +52,7 @@ func _init() -> void: window.record("W") resolved = resolver_script.resolve(window) - _expect_bool(resolved.is_empty(), true, "W alone should not resolve before mirrored W+D") + _expect_string(str(resolved.get("id", "")), "skill_w", "W alone before mirrored W+D") window.record("D") resolved = resolver_script.resolve(window) _expect_string(str(resolved.get("id", "")), "skill_wd", "W+D should mirror W+A skill") diff --git a/tests/test_player_air_attack.gd b/tests/test_player_air_attack.gd index 3c6d597..f32cdbd 100644 --- a/tests/test_player_air_attack.gd +++ b/tests/test_player_air_attack.gd @@ -35,7 +35,10 @@ func _init() -> void: _expect_warrior_animation(animation_player, "warrior_aa", 11, 5) _expect_warrior_animation(animation_player, "warrior_aaa", 12, 8) _expect_warrior_animation(animation_player, "warrior_s_projectile", 14, 13) - _expect_warrior_animation(animation_player, "warrior_charge_release", 13, 16, WARRIOR_WOMAN_TEXTURE) + _expect_warrior_animation(animation_player, "warrior_charge_intro", 13, 8, WARRIOR_WOMAN_TEXTURE) + _expect_warrior_animation(animation_player, "warrior_charge_loop", 13, 4, WARRIOR_WOMAN_TEXTURE, 4) + _expect_animation_loops(animation_player, "warrior_charge_loop") + _expect_warrior_animation(animation_player, "warrior_charge_release", 13, 8, WARRIOR_WOMAN_TEXTURE, 8) _expect_warrior_animation(animation_player, "warrior_a_space_space", 15, 12) _expect_warrior_animation(animation_player, "warrior_a_space", 17, 10) _expect_charge_effect(player) @@ -46,7 +49,8 @@ func _init() -> void: failures.append("Old slash animation should be removed") player.call("submit_combo_input", "W") - _expect_string(str(player.get("last_requested_skill_id")), "", "W alone should not request a skill") + _expect_string(str(player.get("last_requested_skill_id")), "skill_w", "W alone should request row 6 skill") + _expect_string(str(player.get("current_skill_animation")), "warrior_w", "W alone should play warrior_w") player.call("submit_combo_input", "A") _expect_string(str(player.get("last_requested_skill_id")), "skill_wa", "W+A should request row 7 skill") _expect_string(str(player.get("current_skill_animation")), "warrior_wa", "W+A should play warrior_wa") @@ -71,7 +75,7 @@ func _expect_action_has_key(action_name: String, key: Key) -> void: failures.append("Input action %s should be bound to key %s" % [action_name, OS.get_keycode_string(key)]) -func _expect_warrior_animation(animation_player: AnimationPlayer, animation_name: String, row: int, expected_frames: int, texture_path := WARRIOR_TEXTURE) -> void: +func _expect_warrior_animation(animation_player: AnimationPlayer, animation_name: String, row: int, expected_frames: int, texture_path := WARRIOR_TEXTURE, start_column := 0) -> void: if not animation_player.has_animation(animation_name): failures.append("Missing animation: %s" % animation_name) return @@ -106,7 +110,7 @@ func _expect_warrior_animation(animation_player: AnimationPlayer, animation_name failures.append("Missing vframes track: %s" % animation_name) if frame_values.size() != expected_frames: failures.append("%s should key %d frames, got %d" % [animation_name, expected_frames, frame_values.size()]) - var first_frame := (row - 1) * WARRIOR_COLUMNS + var first_frame := (row - 1) * WARRIOR_COLUMNS + start_column for index: int in range(frame_values.size()): var expected := first_frame + index if frame_values[index] != expected: @@ -123,6 +127,15 @@ func _expect_string(actual: String, expected: String, label: String) -> void: failures.append("%s: expected %s, got %s" % [label, expected, actual]) +func _expect_animation_loops(animation_player: AnimationPlayer, animation_name: String) -> void: + if not animation_player.has_animation(animation_name): + failures.append("Missing animation for loop check: %s" % animation_name) + return + var animation: Animation = animation_player.get_animation(animation_name) + if int(animation.loop_mode) != 1: + failures.append("%s should loop, got loop_mode %d" % [animation_name, int(animation.loop_mode)]) + + func _expect_projectile_animation(projectile: Node) -> void: if projectile.get_child_count() == 0: failures.append("Projectile should create a Sprite2D child") diff --git a/tests/test_player_combo_input.gd b/tests/test_player_combo_input.gd index e8fec61..8b4028c 100644 --- a/tests/test_player_combo_input.gd +++ b/tests/test_player_combo_input.gd @@ -54,12 +54,17 @@ func _init() -> void: 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") + _expect_last_skill("skill_w", "W alone should request row 6 skill") + _expect_string(str(player.get("current_skill_animation")), "warrior_w", "W should play row 6 animation") player.get("combo_window").clear("test-reset") + requested_skills.clear() var a_event := InputEventKey.new() a_event.pressed = true a_event.physical_keycode = KEY_A + var a_release_event := InputEventKey.new() + a_release_event.pressed = false + a_release_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") @@ -74,7 +79,7 @@ func _init() -> void: 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_string(animation_player.current_animation, "warrior_charge_intro", "holding A charge should start with charge intro 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: @@ -82,9 +87,6 @@ func _init() -> void: 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") @@ -97,6 +99,7 @@ func _init() -> void: 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") + _expect_string(animation_player.current_animation, "warrior_charge_loop", "full A hold should keep charge loop animation") requested_skills.clear() player.call("_input", a_release_event) _expect_last_skill("skill_a_charge_release", "full A release should request charge release skill") @@ -115,10 +118,17 @@ func _init() -> void: player.call("_input", a_event) Input.action_press("player_a") + player.set("state", Character.State.IDLE) + player.set("velocity", Vector2.ZERO) 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") + _expect_zero((player as CharacterBody2D).velocity.x, "holding consumed A key should not keep sliding after combo input") + var a_release_after_single_hold := InputEventKey.new() + a_release_after_single_hold.pressed = false + a_release_after_single_hold.physical_keycode = KEY_A + player.call("_input", a_release_after_single_hold) player.call("flush_pending_combo_clear") player.get("combo_window").clear("test-reset") requested_skills.clear() @@ -140,6 +150,7 @@ func _init() -> void: player.set("state", Character.State.IDLE) player.set("attack_time_left", 0.0) player.call("_process", player.call("get_max_charge") + 0.1) + _expect_string(animation_player.current_animation, "warrior_charge_loop", "full D hold should keep charge loop animation") var d_release_event := InputEventKey.new() d_release_event.pressed = false d_release_event.physical_keycode = KEY_D @@ -150,6 +161,74 @@ func _init() -> void: _expect_positive((player as CharacterBody2D).velocity.x, "full D release should lunge right") player.get("combo_window").clear("test-reset") requested_skills.clear() + + player.call("_input", d_event) + player.call("_input", d_release_event) + player.call("_input", d_event) + player.set("state", Character.State.IDLE) + player.set("attack_time_left", 0.0) + player.call("_process", 0.2) + _expect_array(player.call("get_combo_slots"), ["D", "D"], "second held D should keep D+D in the combo window") + _expect_last_skill("skill_dd", "second held D should trigger D+D skill before charging") + _expect_bool(player.call("is_charge_active"), true, "holding second D after D+D animation should enter charge state") + _expect_string(animation_player.current_animation, "warrior_charge_intro", "holding second D should start with charge intro animation") + _expect_positive(player.call("get_charge"), "holding second D should grow charge after D+D") + requested_skills.clear() + player.call("_input", d_release_event) + _expect_bool(player.call("is_charge_active"), false, "releasing held second D should cancel D+D charge") + player.get("combo_window").clear("test-reset") + requested_skills.clear() + + player.call("_input", a_event) + player.call("_input", a_release_event) + player.call("_input", a_event) + player.set("state", Character.State.IDLE) + player.set("attack_time_left", 0.0) + player.call("_process", 0.2) + _expect_array(player.call("get_combo_slots"), ["A", "A"], "second held A should keep A+A in the combo window") + _expect_last_skill("skill_aa", "second held A should trigger A+A skill before charging") + _expect_bool(player.call("is_charge_active"), true, "holding second A after A+A animation should enter charge state") + _expect_string(animation_player.current_animation, "warrior_charge_intro", "holding second A should start with charge intro animation") + _expect_positive(player.call("get_charge"), "holding second A should grow charge after A+A") + requested_skills.clear() + player.call("_input", a_release_event) + _expect_bool(player.call("is_charge_active"), false, "releasing held second A should cancel A+A charge") + player.get("combo_window").clear("test-reset") + requested_skills.clear() + + player.call("_input", d_event) + player.call("_input", d_event) + Input.action_press("player_d") + player.set("state", Character.State.IDLE) + player.set("velocity", Vector2.ZERO) + player.call("handle_input") + Input.action_release("player_d") + _expect_array(player.call("get_combo_slots"), ["D", "D"], "held second D should still record D+D once") + _expect_last_skill("skill_dd", "held second D should request D+D skill") + _expect_zero((player as CharacterBody2D).velocity.x, "holding consumed second D should not slide in idle") + var d_release_after_hold := InputEventKey.new() + d_release_after_hold.pressed = false + d_release_after_hold.physical_keycode = KEY_D + player.call("_input", d_release_after_hold) + player.get("combo_window").clear("test-reset") + requested_skills.clear() + + player.call("_input", a_event) + player.call("_input", a_event) + Input.action_press("player_a") + player.set("state", Character.State.IDLE) + player.set("velocity", Vector2.ZERO) + player.call("handle_input") + Input.action_release("player_a") + _expect_array(player.call("get_combo_slots"), ["A", "A"], "held second A should still record A+A once") + _expect_last_skill("skill_aa", "held second A should request A+A skill") + _expect_zero((player as CharacterBody2D).velocity.x, "holding consumed second A should not slide in idle") + var a_release_after_hold := InputEventKey.new() + a_release_after_hold.pressed = false + a_release_after_hold.physical_keycode = KEY_A + player.call("_input", a_release_after_hold) + player.get("combo_window").clear("test-reset") + requested_skills.clear() Input.action_press("player_a") player.set("state", Character.State.IDLE) @@ -186,20 +265,39 @@ func _init() -> void: 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") + _expect_int(player.call("get_energy"), 0, "W should not add energy") 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") + _expect_int(player.call("get_energy"), 1, "A skill 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") + _expect_int(player.call("get_energy"), 2, "D skill should add one energy segment") 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") + _expect_int(player.call("get_energy"), 2, "miss input should not add energy") + player.get("combo_window").clear("test-reset") + player.call("submit_combo_input", "A", "perfect") + player.call("submit_combo_input", "A", "perfect") + _expect_int(player.call("get_energy"), 4, "A+A skill should add one energy segment") + player.get("combo_window").clear("test-reset") + player.call("submit_combo_input", "D", "perfect") + player.call("submit_combo_input", "D", "perfect") + player.call("submit_combo_input", "D", "perfect") + _expect_int(player.call("get_energy"), 7, "D, D+D, and D+D+D skills should each add one energy segment") + player.get("combo_window").clear("test-reset") + player.call("submit_combo_input", "A", "miss") + _expect_int(player.call("get_energy"), 7, "missed A should not add energy") + player.get("combo_window").clear("test-reset") + player.call("submit_combo_input", "SP", "perfect") + _expect_int(player.call("get_energy"), 7, "Space should not add energy") + player.call("flush_pending_combo_clear") 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") + _expect_int(player.call("get_energy"), 9, "W should not change energy near cap") + player.get("combo_window").clear("test-reset") + player.call("submit_combo_input", "A", "perfect") + _expect_int(player.call("get_energy"), 10, "A energy reward should cap at ten segments") player.get("combo_window").clear("test-reset") requested_skills.clear() @@ -349,34 +447,58 @@ func _init() -> void: 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") + requested_skills.clear() player.call("submit_combo_input", "SP") _expect_array(player.call("get_combo_slots"), ["SP"], "Space should be visible before space clear") + _expect_no_skill_requested("Space alone should not request a skill") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), [], "Space should clear combo window") + requested_skills.clear() + var space_event := InputEventKey.new() + space_event.pressed = true + space_event.physical_keycode = KEY_SPACE + player.set("state", Character.State.IDLE) + player.set("current_skill_animation", "") + animation_player.play("warrior_idle") + player.call("_input", space_event) + Input.action_press("player_space") + Input.action_press("jump") + player.call("handle_input") + Input.action_release("jump") + Input.action_release("player_space") + _expect_int(int(player.get("state")), Character.State.IDLE, "direct Space should keep idle state") + _expect_string(animation_player.current_animation, "warrior_idle", "direct Space should keep idle animation") + _expect_no_skill_requested("direct Space key should not request a skill") + player.call("flush_pending_combo_clear") + player.get("combo_window").clear("test-reset") if supports_energy: - player.set("current_energy", 0) + player.set("current_energy", 3) 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") + _expect_int(player.call("get_energy"), 0, "S+Space should spend three energy without input rewards") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), ["S", "SP"], "S+Space should not clear combo window") + if supports_energy: + player.set("current_energy", 2) 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") + _expect_int(player.call("get_energy"), 0, "S+Space+Space should spend two energy without Space reward") player.call("flush_pending_combo_clear") _expect_array(player.call("get_combo_slots"), ["S", "SP", "SP"], "S+Space+Space should not clear combo window") + if supports_energy: + player.set("current_energy", 1) 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_int(player.call("get_energy"), 0, "S+Space+Space+Space should spend one energy without Space reward") _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") diff --git a/tests/test_player_scale.gd b/tests/test_player_scale.gd index 92a00f4..060f08d 100644 --- a/tests/test_player_scale.gd +++ b/tests/test_player_scale.gd @@ -10,6 +10,8 @@ const WARRIOR_ANIMATIONS := [ "warrior_a", "warrior_aa", "warrior_aaa", + "warrior_charge_intro", + "warrior_charge_loop", "warrior_charge_release", "warrior_s_projectile", "warrior_a_space_space",