Refactor rhythm action architecture
This commit is contained in:
236
tests/test_rhythm_action_architecture.gd
Normal file
236
tests/test_rhythm_action_architecture.gd
Normal file
@@ -0,0 +1,236 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user