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)