Files
rthythm_archor_game/tests/test_player_combo_input.gd
2026-07-06 22:59:47 -07:00

170 lines
6.1 KiB
GDScript

extends SceneTree
var failures: Array[String] = []
var started: Array[StringName] = []
func _init() -> void:
_run.call_deferred()
func _run() -> void:
var fixture := await _fixture()
if fixture.is_empty():
_finish()
return
var input_component: Node = fixture["input"]
var controller: Node = fixture["controller"]
var combo: Node = fixture["combo"]
controller.connect("action_started", _on_action_started)
input_component.connect("intent_created", controller.submit_intent)
# 每次按键前把节奏钟钉到一个新的拍心:判定确定为 perfect(消除旧的
# 墙钟 flake),且不与 new1 的"节奏点一次性消耗"规则冲突。
_pin_beat(100)
input_component.call("handle_input_event", _key_event(KEY_A))
_expect_bool(started.is_empty(), false, "physical A should start an action")
_expect_string(_last_started(), "ground_attack_left_1", "physical A should start left attack")
controller.call("_reset_to_idle")
_pin_beat(101)
input_component.call("handle_input_event", _key_event(KEY_A))
_expect_array(combo.call("get_slots"), [&"A", &"A"], "physical A A should fill two slots")
_expect_string(_last_started(), "ground_attack_left_2", "physical A A should start left second attack")
controller.call("_reset_to_idle")
combo.call("clear", &"test-reset")
started.clear()
_pin_beat(102)
input_component.call("handle_input_event", _key_event(KEY_D))
controller.call("_reset_to_idle")
_pin_beat(103)
input_component.call("handle_input_event", _key_event(KEY_D))
_expect_array(combo.call("get_slots"), [&"D", &"D"], "physical D D should fill two slots")
_expect_string(_last_started(), "ground_attack_right_2", "physical D D should start right second attack")
controller.call("_reset_to_idle")
combo.call("clear", &"test-reset")
started.clear()
_pin_beat(104)
input_component.call("handle_input_event", _key_event(KEY_W))
_expect_string(_last_started(), "launcher_up", "physical W should start launcher")
_expect_array(combo.call("get_slots"), [&"W"], "physical W should enter ComboWindow")
controller.call("_reset_to_idle")
combo.call("clear", &"test-reset")
started.clear()
_pin_beat(105)
input_component.call("handle_input_event", _key_event(KEY_S))
_expect_string(_last_started(), "block_start", "physical S should start temporary block animation")
_expect_array(combo.call("get_slots"), [&"S"], "physical S should enter ComboWindow")
controller.call("_reset_to_idle")
started.clear()
_pin_beat(106)
input_component.call("handle_input_event", _key_event(KEY_A))
controller.call("_reset_to_idle")
_pin_beat(107)
input_component.call("handle_input_event", _key_event(KEY_S))
_expect_string(_last_started(), "block_start", "physical S should still block after a previous basic input remains in ComboWindow")
fixture["root"].free()
_finish()
func _last_started() -> String:
if started.is_empty():
return ""
return str(started[started.size() - 1])
## 把 RhythmManager 的时钟偏移钉到指定拍心:紧随其后的实时按键判定为 perfect。
func _pin_beat(beat: int) -> void:
var rhythm := root.get_node_or_null("RhythmManager")
if rhythm == null:
failures.append("RhythmManager autoload should exist for beat pinning")
return
rhythm.set("running", true)
var beat_time := float(rhythm.get("beat_time"))
var beat_offset := float(rhythm.get("beat_offset"))
rhythm.set("_clock_offset", float(beat) * beat_time - beat_offset - Time.get_ticks_msec() / 1000.0)
func _expect_bool(actual: bool, expected: bool, label: String) -> void:
if actual != expected:
failures.append("%s: expected %s, got %s" % [label, expected, actual])
func _fixture() -> Dictionary:
var input_script: Script = load("res://scenes/components/input_component.gd")
var combo_script: Script = load("res://scenes/components/combo_window.gd")
var resolver_script: Script = load("res://scenes/combat/action_resolver.gd")
var state_script: Script = load("res://scenes/components/state_machine.gd")
var energy_script: Script = load("res://scenes/components/energy_component.gd")
var controller_script: Script = load("res://scenes/components/action_controller.gd")
if input_script == null or combo_script == null or resolver_script == null or state_script == null or energy_script == null or controller_script == null:
failures.append("Phase 3 input/action scripts should load")
return {}
var fixture_root := Node.new()
root.add_child(fixture_root)
var input_component: Node = input_script.new()
input_component.name = "InputComponent"
fixture_root.add_child(input_component)
var combo: Node = combo_script.new()
combo.name = "ComboWindow"
fixture_root.add_child(combo)
var resolver: Node = resolver_script.new()
resolver.name = "ActionResolver"
fixture_root.add_child(resolver)
var state: Node = state_script.new()
state.name = "StateMachine"
fixture_root.add_child(state)
var energy: Node = energy_script.new()
energy.name = "EnergyComponent"
fixture_root.add_child(energy)
var controller: Node = controller_script.new()
controller.name = "ActionController"
controller.set("combo_window_path", NodePath("../ComboWindow"))
controller.set("action_resolver_path", NodePath("../ActionResolver"))
controller.set("state_machine_path", NodePath("../StateMachine"))
fixture_root.add_child(controller)
await process_frame
energy.call("set_values", 99, 99)
return {
"root": fixture_root,
"input": input_component,
"combo": combo,
"controller": controller,
}
func _key_event(key: Key) -> InputEventKey:
var event := InputEventKey.new()
event.keycode = key
event.physical_keycode = key
event.pressed = true
return event
func _on_action_started(action: Resource, _intent) -> void:
started.append(StringName(str(action.get("id"))))
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 _finish() -> void:
if failures.is_empty():
print("PASS player combo input")
quit(0)
else:
for failure: String in failures:
push_error(failure)
quit(1)