237 lines
10 KiB
GDScript
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)
|