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

185 lines
8.0 KiB
GDScript

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)