Files
Fighting_Rthythm_game/tests/test_rhythm_action_architecture.gd
2026-07-02 09:47:52 -07:00

237 lines
10 KiB
GDScript

extends SceneTree
var failures: Array[String] = []
func _init() -> void:
_run.call_deferred()
func _run() -> void:
_check_autoloads()
_check_chart_layer()
_check_action_data()
_check_combo_window()
_check_action_resolver()
_check_player_components()
_check_no_legacy_runtime_architecture()
_check_combat_manager()
_finish()
func _check_autoloads() -> void:
_expect(ProjectSettings.has_setting("autoload/RhythmManager"), "RhythmManager should be registered as an autoload")
if ProjectSettings.has_setting("autoload/RhythmManager"):
_expect(str(ProjectSettings.get_setting("autoload/RhythmManager")).contains("res://autoload/rhythm_manager.gd"), "RhythmManager autoload should point at autoload/rhythm_manager.gd")
_expect(ProjectSettings.has_setting("autoload/CombatManager"), "CombatManager should be registered as an autoload")
if ProjectSettings.has_setting("autoload/CombatManager"):
_expect(str(ProjectSettings.get_setting("autoload/CombatManager")).contains("res://autoload/combat_manager.gd"), "CombatManager autoload should point at autoload/combat_manager.gd")
var rhythm_script: Script = load("res://autoload/rhythm_manager.gd")
_expect(rhythm_script != null, "autoload/rhythm_manager.gd should load")
if rhythm_script != null:
var rhythm: Node = rhythm_script.new() as Node
_expect(_has_property(rhythm, "beat_time"), "RhythmManager should expose beat_time")
_expect(_has_property(rhythm, "beat_index"), "RhythmManager should expose beat_index")
_expect(_has_property(rhythm, "judgement_scale"), "RhythmManager should expose judgement_scale")
_expect(rhythm.has_method("judge"), "RhythmManager should expose judge(timestamp)")
_expect(rhythm.has_method("judge_action"), "RhythmManager should expose judge_action(action_name)")
rhythm.free()
func _check_chart_layer() -> void:
for path: String in [
"res://resources/chart_event.gd",
"res://resources/chart_track.gd",
"res://resources/beat_chart.gd",
"res://scenes/chart/chart_runner.gd",
]:
_expect(load(path) != null, "%s should load" % path)
var bus_script: Script = load("res://autoload/event_bus.gd")
_expect(bus_script != null, "EventBus should load for chart signal checks")
if bus_script != null:
var bus: Node = bus_script.new()
_expect(bus.has_signal("chart_event_upcoming"), "EventBus should expose chart_event_upcoming")
_expect(bus.has_signal("chart_event_triggered"), "EventBus should expose chart_event_triggered")
_expect(bus.has_signal("chart_reset"), "EventBus should expose chart_reset")
bus.free()
func _check_action_data() -> void:
var action_script: Script = load("res://resources/action_data.gd")
_expect(action_script != null, "resources/action_data.gd should load")
if action_script == null:
return
var action: Resource = action_script.new() as Resource
for property_name: String in [
"id",
"display_name",
"input_pattern",
"required_state",
"base_cost",
"damage_mult",
"move_mult_x",
"move_mult_y",
"action_beats",
"hit_type",
"range",
"target_type",
"armor_level",
"clear_window",
"can_chain",
"special",
"startup_beats",
"active_beats",
"recovery_beats",
"cancel_from",
]:
_expect(_has_property(action, property_name), "ActionData should expose %s" % property_name)
_expect(DirAccess.open("res://resources/actions") != null, "resources/actions should exist")
var action_dir := DirAccess.open("res://resources/actions")
if action_dir != null:
var action_count := 0
for file_name: String in action_dir.get_files():
if file_name.ends_with(".tres"):
action_count += 1
_expect(action_count >= 20, "resources/actions should contain the full migrated action set")
func _check_combo_window() -> void:
var combo_script: Script = load("res://scenes/components/combo_window.gd")
_expect(combo_script != null, "scenes/components/combo_window.gd should load")
if combo_script == null:
return
var combo: Node = combo_script.new() as Node
root.add_child(combo)
await process_frame
combo.call("record", &"A")
_expect(combo.call("get_slots") == [&"A"], "ComboWindow should record explicit inputs")
combo.call("record", &"Ø")
_expect(combo.call("get_slots") == [&"A", &"Ø"], "ComboWindow should keep explicit miss placeholders")
combo.free()
func _check_action_resolver() -> void:
var resolver_script: Script = load("res://scenes/combat/action_resolver.gd")
_expect(resolver_script != null, "scenes/combat/action_resolver.gd should load")
if resolver_script == null:
return
if resolver_script.has_method("clear_cache"):
resolver_script.clear_cache()
var action: Resource = resolver_script.resolve_pattern("A", null, {"state": &"ground"})
_expect(action != null, "ActionResolver should resolve A from resources/actions")
if action != null:
_expect(str(action.get("id")) == "skill_a", "ActionResolver should resolve A to skill_a")
_expect(_has_property(action, "action_beats"), "Resolved action should be full ActionData")
_expect(str(action.resource_path).contains("res://resources/actions/"), "ActionResolver should resolve migrated ActionData resources, not legacy skill resources")
_expect(resolver_script.has_method("space_priority_labels"), "ActionResolver should expose ordered Space priority labels")
func _check_player_components() -> void:
var player_scene: PackedScene = load("res://scenes/characters/player.tscn")
_expect(player_scene != null, "player.tscn should load")
if player_scene == null:
return
var player := player_scene.instantiate()
root.add_child(player)
await process_frame
for node_path: String in [
"ComboWindow",
"ActionResolver",
"ActionExecutor",
"MotionExecutor",
"BurstComponent",
"ChargeComponent",
"EnergyComponent",
"DamageEmitter",
"DamageReceiver",
]:
_expect(player.get_node_or_null(node_path) != null, "Player should own %s component" % node_path)
_expect(player.get_node_or_null("ComboTracker") == null, "Player should not keep legacy ComboTracker component")
player.free()
func _check_no_legacy_runtime_architecture() -> void:
var player_source := _read_text("res://scenes/characters/player.gd")
_expect(not player_source.contains("InputResolver"), "Player should not reference legacy InputResolver")
_expect(not player_source.contains("charge_duration"), "Player charge timing should live in a component")
_expect(not player_source.contains("func _update_charge"), "Player should not own charge update loop")
_expect(not player_source.contains("attack_lunge_time_left ="), "Player should not set lunge timers directly")
_expect(not player_source.contains("energy_component.spend"), "Player should delegate action costs to ActionExecutor")
_expect(not player_source.contains("damage_emitter.configure_hit"), "Player should delegate damage context setup to ActionExecutor")
_expect(not player_source.contains("resolve_cost"), "Player should not resolve action costs directly")
_expect(player_source.contains("intent_created.connect"), "Player should connect InputComponent intents to ActionController")
_expect(not player_source.contains("action_resolver.resolve_window"), "Player should not resolve actions directly")
_expect(not player_source.contains("action_executor.execute"), "Player should not execute actions directly")
_expect(not player_source.contains("func _record_combo_symbol"), "Player should not own combo symbol adjudication")
_expect(not player_source.contains("func _execute_combo_skill"), "Player should not own action execution")
_expect(not player_source.contains("func judge_rhythm_action"), "Player should not own rhythm judgement")
_expect(player_source.contains("func _on_action_started"), "Player should present actions from ActionController")
var resolver_source := _read_text("res://scenes/combat/action_resolver.gd")
_expect(not resolver_source.contains("resources/skills"), "ActionResolver should not load legacy resources/skills")
_expect(FileAccess.file_exists("res://scenes/combat/input_resolver.gd") == false, "Legacy InputResolver file should be removed")
_expect(FileAccess.file_exists("res://resources/skill_data.gd") == false, "Legacy SkillData script should be removed")
var damage_source := _read_text("res://scenes/components/damage_emitter.gd")
_expect(damage_source.contains("resolve_damage"), "DamageEmitter should route damage through CombatManager")
func _check_combat_manager() -> void:
var combat_script: Script = load("res://autoload/combat_manager.gd")
_expect(combat_script != null, "autoload/combat_manager.gd should load")
if combat_script == null:
return
var combat: Node = combat_script.new() as Node
_expect(combat.has_method("resolve_damage"), "CombatManager should expose resolve_damage")
_expect(combat.has_method("resolve_cost"), "CombatManager should expose resolve_cost")
_expect(combat.has_method("resolve_move"), "CombatManager should expose resolve_move")
var action_script: Script = load("res://resources/action_data.gd")
if action_script != null and combat.has_method("resolve_damage"):
var action: Resource = action_script.new() as Resource
action.set("damage_mult", 2.0)
var damage := float(combat.call("resolve_damage", 10.0, action, {"damage_mult": 1.5}, null, null))
_expect(is_equal_approx(damage, 30.0), "CombatManager should apply action and judgement damage multipliers")
combat.free()
func _rhythm_manager() -> Node:
var rhythm := root.get_node_or_null("RhythmManager")
if rhythm != null:
return rhythm
var rhythm_script: Script = load("res://autoload/rhythm_manager.gd")
if rhythm_script == null:
return null
rhythm = rhythm_script.new()
rhythm.name = "RhythmManager"
root.add_child(rhythm)
return rhythm
func _has_property(object: Object, property_name: String) -> bool:
for property: Dictionary in object.get_property_list():
if str(property.get("name", "")) == property_name:
return true
return false
func _expect(condition: bool, label: String) -> void:
if not condition:
failures.append(label)
func _read_text(path: String) -> String:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
failures.append("Could not read %s" % path)
return ""
return file.get_as_text()
func _finish() -> void:
if failures.is_empty():
print("PASS rhythm action architecture")
quit(0)
else:
for failure: String in failures:
push_error(failure)
quit(1)