extends SceneTree var failures: Array[String] = [] func _init() -> void: _run.call_deferred() func _run() -> void: var ui_scene: PackedScene = load("res://scenes/ui/main_ui.tscn") var player_scene: PackedScene = load("res://scenes/characters/player.tscn") if ui_scene == null or player_scene == null: failures.append("Debug UI test scenes should load") _finish() return var ui := ui_scene.instantiate() var player := player_scene.instantiate() root.add_child(player) root.add_child(ui) await process_frame for node_path: String in [ "DebugPanel", "DebugPanel/StateAxesLabel", "DebugPanel/ActionLabel", "DebugPanel/AnchorLabel", "DebugPanel/EffectsLabel", "DebugPanel/DefenseLabel", "DebugPanel/PatternsLabel", "DebugPanel/HitLogLabel", "DebugPanel/CalibrationLabel", ]: _expect(ui.has_node(node_path), "MainUI should expose %s" % node_path) if ui.has_method("bind_debug_actor"): ui.call("bind_debug_actor", player) else: failures.append("MainUI should expose bind_debug_actor(actor)") var state_machine: Node = player.get_node("StateMachine") state_machine.call("set_ground_state", &"Airborne") state_machine.call("set_action_phase", &"Startup") state_machine.call("set_life_state", &"Hitstun") state_machine.call("set_defense_state", &"Parrying") var action_controller: Node = player.get_node("ActionController") action_controller.set("current_action", load("res://resources/actions/ground_attack_left_1.tres")) action_controller.set("startup_stretch_seconds", 0.125) var effect_container: Node = player.get_node("EffectContainer") if _method_arg_count(effect_container, "add_effect") >= 2: effect_container.call("add_effect", load("res://resources/effects/effect_root.tres"), &"debug_test") effect_container.call("add_effect", load("res://resources/effects/effect_temporary_invincible.tres"), &"debug_defense") else: failures.append("EffectContainer.add_effect should accept a source argument for debug UI") effect_container.call("add_effect", load("res://resources/effects/effect_root.tres")) if ui.has_method("refresh_debug_panel"): ui.call("refresh_debug_panel") else: failures.append("MainUI should expose refresh_debug_panel()") _expect_label_contains(ui, "DebugPanel/StateAxesLabel", "Airborne", "Debug state axes should include GroundState") _expect_label_contains(ui, "DebugPanel/StateAxesLabel", "Startup", "Debug state axes should include ActionPhaseState") _expect_label_contains(ui, "DebugPanel/StateAxesLabel", "Hitstun", "Debug state axes should include LifeState") _expect_label_contains(ui, "DebugPanel/StateAxesLabel", "Parrying", "Debug state axes should include DefenseState") _expect_label_contains(ui, "DebugPanel/ActionLabel", "ground_attack_left_1", "Debug action label should show current action id") _expect_label_contains(ui, "DebugPanel/AnchorLabel", "0.125", "Debug anchor label should show Startup stretch seconds") _expect_label_contains(ui, "DebugPanel/EffectsLabel", "effect_root", "Debug effects label should show active effect ids") _expect_label_contains(ui, "DebugPanel/EffectsLabel", "1.5s", "Debug effects label should show active effect remaining seconds") _expect_label_contains(ui, "DebugPanel/EffectsLabel", "x1", "Debug effects label should show active effect stacks") _expect_label_contains(ui, "DebugPanel/EffectsLabel", "debug_test", "Debug effects label should show active effect source") _expect_label_contains(ui, "DebugPanel/DefenseLabel", "Baseline", "Debug defense label should identify baseline defense") _expect_label_contains(ui, "DebugPanel/DefenseLabel", "Invincible", "Debug defense label should show synthesized defense state") _expect_label_contains(ui, "DebugPanel/DefenseLabel", "damage 0.00", "Debug defense label should show baseline damage multiplier") _expect_label_contains(ui, "DebugPanel/DefenseLabel", "interrupt false", "Debug defense label should show baseline interrupt result") _expect_label_contains(ui, "DebugPanel/PatternsLabel", "A->ground_attack_left_1", "Debug patterns label should show exported legal patterns") _expect_label_contains(ui, "DebugPanel/PatternsLabel", "S->block_start", "Debug patterns label should show standalone S block") _expect_label_not_contains(ui, "DebugPanel/PatternsLabel", "SS->", "Debug patterns label should not show removed S S combo") _expect_label_contains(ui, "DebugPanel/CalibrationLabel", "Diff", "Debug calibration label should show rhythm diff calibration data") var bus := root.get_node_or_null("EventBus") if bus != null and bus.has_signal("hit_confirmed"): bus.emit_signal("hit_confirmed", { "damage": 7, "hit_type": &"melee", "action": load("res://resources/actions/ground_attack_left_1.tres"), "defense": {"defense_state": &"Parrying"}, "interrupts": false, }) else: failures.append("EventBus should expose hit_confirmed(result) for debug hit logs") _expect_label_contains(ui, "DebugPanel/HitLogLabel", "ground_attack_left_1", "Debug hit log should show hit action id") _expect_label_contains(ui, "DebugPanel/HitLogLabel", "dmg 7", "Debug hit log should show resolved damage") _expect_label_contains(ui, "DebugPanel/HitLogLabel", "Parrying", "Debug hit log should show effective defense") _expect_label_contains(ui, "DebugPanel/HitLogLabel", "interrupt false", "Debug hit log should show interrupt result") if bus != null and bus.has_signal("hit_confirmed"): for index: int in range(4): bus.emit_signal("hit_confirmed", { "damage": index + 1, "hit_type": &"melee", "action": load("res://resources/actions/ground_attack_left_1.tres"), "defense": {"defense_state": &"Vulnerable"}, "interrupts": true, }) if ui.has_method("get_hit_log_entries"): var entries: Array = ui.call("get_hit_log_entries") _expect_int(entries.size(), 5, "Debug UI should retain full hit history beyond the compact label") else: failures.append("MainUI should expose get_hit_log_entries()") if ui.has_method("export_hit_log"): _expect(ui.call("export_hit_log").contains("ground_attack_left_1"), "Debug UI should export hit history text") else: failures.append("MainUI should expose export_hit_log()") ui.free() player.free() await _check_main_scene_binds_debug_actor() _finish() func _expect_label_contains(root_node: Node, path: String, text: String, label: String) -> void: var node := root_node.get_node_or_null(path) as Label if node == null: failures.append("%s: missing %s" % [label, path]) return if not node.text.contains(text): failures.append("%s: expected '%s' to contain '%s'" % [label, node.text, text]) func _expect_label_not_contains(root_node: Node, path: String, text: String, label: String) -> void: var node := root_node.get_node_or_null(path) as Label if node == null: failures.append("%s: missing %s" % [label, path]) return if node.text.contains(text): failures.append("%s: expected '%s' not to contain '%s'" % [label, node.text, text]) func _method_arg_count(object: Object, method_name: String) -> int: for method: Dictionary in object.get_method_list(): if str(method.get("name", "")) != method_name: continue var args = method.get("args", []) return args.size() if args is Array else 0 return 0 func _check_main_scene_binds_debug_actor() -> void: var main_scene: PackedScene = load("res://scenes/main/main.tscn") if main_scene == null: failures.append("main.tscn should load") return var main := main_scene.instantiate() root.add_child(main) await process_frame var player: Node = main.call("get_player") var ui: Node = main.get_node("UI") player.get_node("StateMachine").call("set_action_phase", &"Startup") ui.call("refresh_debug_panel") _expect_label_contains(ui, "DebugPanel/StateAxesLabel", "Startup", "Main should bind Player to UI DebugPanel") main.free() func _expect(condition: bool, label: String) -> void: if not condition: failures.append(label) func _expect_int(actual: int, expected: int, label: String) -> void: if actual != expected: failures.append("%s: expected %d, got %d" % [label, expected, actual]) func _finish() -> void: if failures.is_empty(): print("PASS debug ui") quit(0) else: for failure: String in failures: push_error(failure) quit(1)