extends SceneTree var failures: Array[String] = [] func _init() -> void: _check_event_bus_autoload() _check_main_is_thin() _check_player_components() _check_skill_resources() _check_stage_and_ui_scenes() _check_project_layers_and_inputs() _finish() func _check_event_bus_autoload() -> void: _expect(ProjectSettings.has_setting("autoload/EventBus"), "EventBus should be registered as a game autoload") if ProjectSettings.has_setting("autoload/EventBus"): var path := str(ProjectSettings.get_setting("autoload/EventBus")) _expect(path.contains("res://autoload/event_bus.gd"), "EventBus autoload should point at autoload/event_bus.gd") var event_bus_script := load("res://autoload/event_bus.gd") _expect(event_bus_script != null, "autoload/event_bus.gd should exist") func _check_main_is_thin() -> void: var main_scene: PackedScene = load("res://scenes/main/main.tscn") _expect(main_scene != null, "main.tscn should load") if main_scene != null: var main := main_scene.instantiate() get_root().add_child(main) _expect(main.has_node("Stage"), "Main should instance a Stage child") _expect(main.has_node("UI"), "Main should instance a standalone UI child") _expect(not main.has_node("RhythmConductor"), "Main should use RhythmManager autoload instead of a RhythmConductor child") main.free() var source := _read_text("res://scenes/main/main.gd") _expect_not_contains(source, "has_signal", "Main should not probe child signals with has_signal") _expect_not_contains(source, "has_method", "Main should not probe child methods with has_method") _expect_not_contains(source, ".call(", "Main should not use dynamic call() for game wiring") 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: var player := player_scene.instantiate() get_root().add_child(player) for node_name: String in [ "StateMachine", "InputComponent", "ComboWindow", "ActionResolver", "ActionExecutor", "MotionExecutor", "BurstComponent", "ChargeComponent", "EnergyComponent", "HealthComponent", "DamageReceiver", "DamageEmitter", ]: _expect(player.has_node(node_name), "Player should have %s child component" % node_name) player.free() var source := _read_text("res://scenes/characters/player.gd") _expect_not_contains(source, "KEY_", "Player should not match raw KEY_* values") _expect_not_contains(source, "get_first_node_in_group(\"rhythm_conductor\")", "Player should not look up RhythmConductor by group") _expect_not_contains(source, "PlayerProjectile.new()", "Player should not instantiate projectiles directly") _expect_not_contains(source, "get_parent()", "Player should not reach upward with get_parent()") func _check_skill_resources() -> void: var action_script := load("res://resources/action_data.gd") _expect(action_script != null, "resources/action_data.gd should exist") var action_dir := DirAccess.open("res://resources/actions") _expect(action_dir != null, "resources/actions should exist") if action_dir != null: var action_files: Array[String] = [] for file_name: String in action_dir.get_files(): if file_name.ends_with(".tres"): action_files.append(file_name) _expect(action_files.size() >= 20, "actions should be saved as migrated .tres resources") var tracker_script := load("res://scenes/components/combo_window.gd") var resolver_script := load("res://scenes/combat/action_resolver.gd") _expect(tracker_script != null, "ComboWindow script should load") _expect(resolver_script != null, "ActionResolver script should load") if tracker_script == null or resolver_script == null: return var window = tracker_script.new() window.record(&"A") var resolved = resolver_script.resolve(window) _expect(resolved is Resource, "ActionResolver.resolve should return an ActionData Resource, not a Dictionary") if resolved is Resource: _expect(resolved.get("input_pattern") is Array, "Resolved ActionData should expose input_pattern") _expect(str(resolved.get("id")) == "skill_a", "A pattern should resolve to skill_a") resolver_script.clear_cache() window.free() func _check_stage_and_ui_scenes() -> void: var stage_scene: PackedScene = load("res://scenes/stage/stage.tscn") _expect(stage_scene != null, "stage.tscn should exist") if stage_scene != null: var stage := stage_scene.instantiate() get_root().add_child(stage) _expect(stage.has_node("ActorsContainer"), "Stage should own ActorsContainer") _expect(stage.has_node("ActorsContainer/Player"), "ActorsContainer should own Player") stage.free() for scene_path: String in [ "res://scenes/ui/main_ui.tscn", "res://scenes/ui/rhythm_track.tscn", "res://scenes/ui/combo_window_hud.tscn", "res://scenes/ui/energy_bar.tscn", ]: _expect(load(scene_path) != null, "%s should exist" % scene_path) func _check_project_layers_and_inputs() -> void: var expected_layers := { "layer_names/2d_physics/layer_1": "world", "layer_names/2d_physics/layer_2": "player_hurtbox", "layer_names/2d_physics/layer_3": "enemy_hurtbox", "layer_names/2d_physics/layer_4": "player_hitbox", "layer_names/2d_physics/layer_5": "enemy_hitbox", } for key: String in expected_layers: _expect(ProjectSettings.has_setting(key), "%s should be configured" % key) if ProjectSettings.has_setting(key): _expect(str(ProjectSettings.get_setting(key)) == expected_layers[key], "%s should be named %s" % [key, expected_layers[key]]) for action_name: String in ["move_left", "move_right", "combo_w", "combo_a", "combo_d", "combo_s", "combo_space"]: _expect(InputMap.has_action(action_name), "InputMap should define %s" % action_name) _expect(not InputMap.has_action("player_space"), "InputMap should remove duplicate player_space action") 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 _expect(condition: bool, label: String) -> void: if not condition: failures.append(label) func _expect_not_contains(source: String, needle: String, label: String) -> void: if source.contains(needle): failures.append(label) func _finish() -> void: if failures.is_empty(): print("PASS architecture refactor") quit(0) else: for failure: String in failures: push_error(failure) quit(1)