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)