extends SceneTree var failures: Array[String] = [] func _init() -> void: _run.call_deferred() func _run() -> void: var resolver: Script = load("res://scenes/combat/action_resolver.gd") if resolver != null and resolver.has_method("clear_cache"): resolver.call("clear_cache") _check_player_action_set() _check_pattern_export() _check_charge_bindings() await _check_player_animation_coverage() _check_reserved_terms() _finish() func _check_player_action_set() -> void: var expected := { &"ground_attack_left_1": {"pattern": [&"A"], "damage": 1.0, "reward": 10.0, "cost": 0.0, "move_x": -1.0, "move_y": 0.0, "animation": &"atk_ground_1"}, &"ground_attack_left_2": {"pattern": [&"A", &"A"], "damage": 1.2, "reward": 12.0, "cost": 0.0, "move_x": -1.1, "move_y": 0.0, "animation": &"atk_ground_2"}, &"ground_attack_left_3": {"pattern": [&"A", &"A", &"A"], "damage": 1.5, "reward": 15.0, "cost": 0.0, "move_x": -1.2, "move_y": 0.0, "animation": &"atk_ground_3"}, &"dash_slash_left": {"pattern": [&"A", &"SP"], "damage": 1.6, "reward": 0.0, "cost": 20.0, "move_x": -2.6, "move_y": 0.0, "animation": &"dash_slash", "tag": &"dash_through"}, &"combo_finisher_left": {"pattern": [&"A", &"A", &"SP"], "damage": 2.2, "reward": 0.0, "cost": 30.0, "move_x": -1.3, "move_y": 0.0, "animation": &"combo_finisher"}, &"ground_smash_left": {"pattern": [&"A", &"A", &"A", &"SP"], "damage": 2.8, "reward": 0.0, "cost": 40.0, "move_x": -0.8, "move_y": 0.0, "knock_y": 1.8, "animation": &"ground_smash", "tag": &"knockup"}, &"ground_attack_right_1": {"pattern": [&"D"], "damage": 1.0, "reward": 10.0, "cost": 0.0, "move_x": 1.0, "move_y": 0.0, "animation": &"atk_ground_1"}, &"ground_attack_right_2": {"pattern": [&"D", &"D"], "damage": 1.2, "reward": 12.0, "cost": 0.0, "move_x": 1.1, "move_y": 0.0, "animation": &"atk_ground_2"}, &"ground_attack_right_3": {"pattern": [&"D", &"D", &"D"], "damage": 1.5, "reward": 15.0, "cost": 0.0, "move_x": 1.2, "move_y": 0.0, "animation": &"atk_ground_3"}, &"dash_slash_right": {"pattern": [&"D", &"SP"], "damage": 1.6, "reward": 0.0, "cost": 20.0, "move_x": 2.6, "move_y": 0.0, "animation": &"dash_slash", "tag": &"dash_through"}, &"combo_finisher_right": {"pattern": [&"D", &"D", &"SP"], "damage": 2.2, "reward": 0.0, "cost": 30.0, "move_x": 1.3, "move_y": 0.0, "animation": &"combo_finisher"}, &"ground_smash_right": {"pattern": [&"D", &"D", &"D", &"SP"], "damage": 2.8, "reward": 0.0, "cost": 40.0, "move_x": 0.8, "move_y": 0.0, "knock_y": 1.8, "animation": &"ground_smash", "tag": &"knockup"}, &"launcher_up": {"pattern": [&"W"], "damage": 1.2, "reward": 12.0, "cost": 0.0, "move_x": 0.6, "move_y": 1.8, "knock_y": 1.8, "animation": &"rising_slash"}, &"air_attack_left": {"pattern": [&"A"], "damage": 1.3, "reward": 10.0, "cost": 0.0, "move_x": -5.8, "move_y": -1.6, "animation": &"atk_air", "ground": [&"Airborne"]}, &"air_attack_right": {"pattern": [&"D"], "damage": 1.3, "reward": 10.0, "cost": 0.0, "move_x": 5.8, "move_y": -1.6, "animation": &"atk_air", "ground": [&"Airborne"]}, &"plunging_strike": {"pattern": [&"S"], "damage": 1.8, "reward": 15.0, "cost": 0.0, "move_x": 0.0, "move_y": -2.6, "knock_y": 1.4, "animation": &"plunge_start", "ground": [&"Airborne"], "tag": &"knockup"}, &"block_start": {"pattern": [&"S"], "damage": 0.0, "reward": 0.0, "cost": 0.0, "move_x": 0.0, "move_y": 0.0, "animation": &"block_start", "ground": [&"Grounded"], "hit_type": &"defense", "defense": &"parry"}, &"jian_yu_left_lv1": {"pattern": [], "damage": 1.8, "reward": 0.0, "cost": 20.0, "move_x": 0.0, "move_y": 0.0, "animation": &"blade_rain_cast", "phase": [&"Charging"], "defense": &"super_armor"}, &"jian_yu_left_lv2": {"pattern": [], "damage": 2.8, "reward": 0.0, "cost": 35.0, "move_x": 0.0, "move_y": 0.0, "animation": &"blade_rain_cast", "phase": [&"Charging"], "defense": &"super_armor"}, &"jian_yu_left_lv3": {"pattern": [], "damage": 4.2, "reward": 0.0, "cost": 50.0, "move_x": 0.0, "move_y": 0.0, "animation": &"blade_rain_cast", "phase": [&"Charging"], "defense": &"super_armor"}, &"jian_yu_right_lv1": {"pattern": [], "damage": 1.8, "reward": 0.0, "cost": 20.0, "move_x": 0.0, "move_y": 0.0, "animation": &"blade_rain_cast", "phase": [&"Charging"], "defense": &"super_armor"}, &"jian_yu_right_lv2": {"pattern": [], "damage": 2.8, "reward": 0.0, "cost": 35.0, "move_x": 0.0, "move_y": 0.0, "animation": &"blade_rain_cast", "phase": [&"Charging"], "defense": &"super_armor"}, &"jian_yu_right_lv3": {"pattern": [], "damage": 4.2, "reward": 0.0, "cost": 50.0, "move_x": 0.0, "move_y": 0.0, "animation": &"blade_rain_cast", "phase": [&"Charging"], "defense": &"super_armor"}, &"zhan_bo_lv1": {"pattern": [], "damage": 1.2, "reward": 0.0, "cost": 15.0, "move_x": 0.0, "move_y": 0.0, "animation": &"blade_wave_cast", "phase": [&"Charging"], "hit_type": &"projectile"}, &"zhan_bo_lv2": {"pattern": [], "damage": 2.0, "reward": 0.0, "cost": 25.0, "move_x": 0.0, "move_y": 0.0, "animation": &"blade_wave_cast", "phase": [&"Charging"], "hit_type": &"projectile"}, &"zhan_bo_lv3": {"pattern": [], "damage": 3.2, "reward": 0.0, "cost": 40.0, "move_x": 0.0, "move_y": 0.0, "animation": &"blade_wave_cast", "phase": [&"Charging"], "hit_type": &"projectile"}, } for action_id: StringName in expected.keys(): var action := _load_action(action_id) _expect(action != null, "%s should exist as phase-9 ActionData" % action_id) if action == null: continue var spec: Dictionary = expected[action_id] _expect_array(action.get("input_pattern"), spec.get("pattern", []), "%s input_pattern" % action_id) _expect_float(float(action.get("damage_mult")), float(spec["damage"]), "%s damage_mult" % action_id) _expect_float(float(action.get("base_reward")), float(spec["reward"]), "%s base_reward" % action_id) _expect_float(float(action.get("base_cost")), float(spec["cost"]), "%s base_cost" % action_id) _expect_float(float(action.get("move_mult_x")), float(spec["move_x"]), "%s move_mult_x" % action_id) _expect_float(float(action.get("move_mult_y")), float(spec["move_y"]), "%s move_mult_y" % action_id) if spec.has("knock_y"): _expect_float(float(action.get("knockback_mult_y")), float(spec["knock_y"]), "%s knockback_mult_y" % action_id) _expect_equal(StringName(str(action.get("animation"))), spec["animation"], "%s animation" % action_id) if spec.has("ground"): _expect_array(action.get("allowed_ground_states"), spec["ground"], "%s allowed_ground_states" % action_id) if spec.has("phase"): _expect_array(action.get("allowed_action_phases"), spec["phase"], "%s allowed_action_phases" % action_id) if spec.has("hit_type"): _expect_equal(StringName(str(action.get("hit_type"))), spec["hit_type"], "%s hit_type" % action_id) if spec.has("tag"): _expect((action.get("action_tags") as Array).has(spec["tag"]), "%s should include action tag %s" % [action_id, spec["tag"]]) if spec.has("defense"): _expect((action.get("defense_tags") as Array).has(spec["defense"]), "%s should include defense tag %s" % [action_id, spec["defense"]]) func _check_pattern_export() -> void: var exporter: Script = load("res://tools/export_action_patterns.gd") _expect(exporter != null, "ActionPatternExporter should load") if exporter == null: return var patterns: Dictionary = exporter.call("export_patterns") var expected := { "A": &"ground_attack_left_1", "AA": &"ground_attack_left_2", "AAA": &"ground_attack_left_3", "ASP": &"dash_slash_left", "AASP": &"combo_finisher_left", "AAASP": &"ground_smash_left", "D": &"ground_attack_right_1", "DD": &"ground_attack_right_2", "DDD": &"ground_attack_right_3", "DSP": &"dash_slash_right", "DDSP": &"combo_finisher_right", "DDDSP": &"ground_smash_right", "W": &"launcher_up", "S": &"block_start", } for key: String in expected: _expect_equal(patterns.get(key), expected[key], "Pattern %s" % key) for forbidden: String in ["AD", "DA", "ADSP", "DASP", "ASPSP", "SS"]: _expect(not patterns.has(forbidden), "Forbidden pattern %s should not exist" % forbidden) func _check_charge_bindings() -> void: var bindings := load("res://resources/player_action_bindings.tres") _expect(bindings != null, "PlayerSkillBindings resource should exist") if bindings == null: return var entries: Array = bindings.get("charge_entries") _expect_int(entries.size(), 3, "charge_entries size") _expect_charge_entry(entries, &"A", &"after_tap", &"on_release", [&"jian_yu_left_lv1", &"jian_yu_left_lv2", &"jian_yu_left_lv3"]) _expect_charge_entry(entries, &"D", &"after_tap", &"on_release", [&"jian_yu_right_lv1", &"jian_yu_right_lv2", &"jian_yu_right_lv3"]) _expect_charge_entry(entries, &"S", &"after_tap", &"on_secondary_key", [&"zhan_bo_lv1", &"zhan_bo_lv2", &"zhan_bo_lv3"]) func _check_player_animation_coverage() -> void: var scene := load("res://scenes/characters/player.tscn") as PackedScene _expect(scene != null, "Player scene should load") if scene == null: return var player := scene.instantiate() root.add_child(player) await process_frame _expect(player.has_method("has_player_animation"), "Player should expose animation coverage checks") if player.has_method("has_player_animation"): for animation_name: StringName in [&"atk_ground_1", &"atk_ground_2", &"atk_ground_3", &"dash_slash", &"combo_finisher", &"ground_smash", &"rising_slash", &"block_start", &"blade_rain_cast", &"blade_wave_cast", &"atk_air", &"plunge_start", &"idle", &"turn"]: _expect(bool(player.call("has_player_animation", animation_name)), "Player should be able to play %s" % animation_name) player.free() func _check_reserved_terms() -> void: var required := ["TimePhaseManager", "TimeAnchorSystem"] var files := _gd_files("res://autoload") + _gd_files("res://resources") + _gd_files("res://scenes") + _gd_files("res://scripts") var corpus := "" for path: String in files: var file := FileAccess.open(path, FileAccess.READ) if file == null: continue corpus += file.get_as_text() for needle: String in required: _expect(corpus.contains(needle), "implemented time systems should reference %s" % needle) func _expect_charge_entry(entries: Array, symbol: StringName, mode: StringName, trigger: StringName, ids: Array[StringName]) -> void: for entry: Variant in entries: if entry is Dictionary and StringName(str(entry.get("symbol", &""))) == symbol: _expect_equal(StringName(str(entry.get("entry_mode", &""))), mode, "%s charge entry mode" % symbol) _expect_equal(StringName(str(entry.get("cast_trigger", &""))), trigger, "%s charge trigger" % symbol) var cast_ids: Dictionary = entry.get("cast_action_ids", {}) for index: int in range(ids.size()): _expect_equal(cast_ids.get(index + 1), ids[index], "%s charge level %d" % [symbol, index + 1]) return failures.append("Missing charge entry %s" % symbol) func _load_action(action_id: StringName) -> Resource: var files := _tres_files("res://resources/actions") for path: String in files: var resource := load(path) as Resource if resource != null and StringName(str(resource.get("id"))) == action_id: return resource return null func _tres_files(path: String) -> Array[String]: return _files_with_extension(path, ".tres") func _gd_files(path: String) -> Array[String]: return _files_with_extension(path, ".gd") func _files_with_extension(path: String, extension: String) -> Array[String]: var result: Array[String] = [] var dir := DirAccess.open(path) if dir == null: return result for subdir: String in dir.get_directories(): result.append_array(_files_with_extension("%s/%s" % [path, subdir], extension)) for file_name: String in dir.get_files(): if file_name.ends_with(extension): result.append("%s/%s" % [path, file_name]) return result func _expect(condition: bool, label: String) -> void: if not condition: failures.append(label) func _expect_equal(actual: Variant, expected: Variant, label: String) -> void: if actual != expected: failures.append("%s: expected %s, got %s" % [label, expected, actual]) func _expect_array(actual: Variant, expected: Array, label: String) -> void: if not actual is Array or actual != expected: failures.append("%s: expected %s, got %s" % [label, expected, actual]) func _expect_float(actual: float, expected: float, label: String) -> void: if absf(actual - expected) > 0.001: failures.append("%s: expected %.3f, got %.3f" % [label, expected, actual]) func _expect_int(actual: int, expected: int, label: String) -> void: if actual != expected: failures.append("%s: expected %d, got %d" % [label, expected, actual]) func _finish() -> void: if failures.is_empty(): print("PASS phase9 content completion") quit(0) else: for failure: String in failures: push_error(failure) quit(1)