221 lines
12 KiB
GDScript
221 lines
12 KiB
GDScript
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)
|