Files
rthythm_archor_game/tests/test_enemy_action_pipeline.gd
T
2026-07-06 22:59:47 -07:00

194 lines
7.2 KiB
GDScript

extends SceneTree
var failures: Array[String] = []
var started_actions: Array[StringName] = []
func _init() -> void:
_run.call_deferred()
func _run() -> void:
await process_frame
_check_scripts_and_scene()
_check_boss_action_resources()
await _check_enemy_driver_uses_action_controller()
await _check_chart_event_reaches_boss_driver()
_check_chart_event_action_id()
_finish()
func _check_scripts_and_scene() -> void:
for path: String in [
"res://scenes/components/ai_intent.gd",
"res://scenes/enemies/enemy_action_driver.gd",
"res://scenes/enemies/enemy.tscn",
"res://scenes/enemies/boss.tscn",
]:
_expect(load(path) != null, "%s should load" % path)
var action_dir := DirAccess.open("res://resources/actions/enemies")
_expect(action_dir != null, "Enemy action resource directory should exist")
func _check_boss_action_resources() -> void:
var resolver_script: Script = load("res://scenes/combat/action_resolver.gd")
_expect(resolver_script != null, "ActionResolver should load for enemy resources")
if resolver_script == null:
return
resolver_script.clear_cache()
for action_id: StringName in _boss_action_ids():
var action: Resource = resolver_script.get_action(action_id)
_expect(action != null, "%s should be a real Boss ActionData resource" % action_id)
if action == null:
continue
_expect_bool(str(action.resource_path).begins_with("res://resources/actions/enemies/"), true, "%s should load from the enemy action directory" % action_id)
_expect_bool(Array(action.get("input_pattern")).is_empty(), true, "%s should not expose a player input pattern" % action_id)
_expect_bool(Array(action.get("action_tags")).has(&"enemy"), true, "%s should carry the enemy action tag" % action_id)
_expect_bool(Array(action.get("action_tags")).has(&"boss"), true, "%s should carry the boss action tag" % action_id)
_expect(float(action.get("startup_beats")) > 0.0, "%s should have a startup phase" % action_id)
_expect(float(action.get("active_beats")) > 0.0, "%s should have an active phase" % action_id)
_expect(float(action.get("recovery_beats")) > 0.0, "%s should have a recovery phase" % action_id)
_expect_bool(str(action.get("id")).begins_with("skill_"), false, "%s should not keep legacy skill_* naming" % action_id)
var stab: Resource = resolver_script.get_action(&"boss_lunging_stab")
if stab != null:
_expect_float(float(stab.get("damage_mult")), 3.0, "boss_lunging_stab damage")
_expect_float(float(stab.get("move_mult_x")), 2.0, "boss_lunging_stab lunge distance")
_expect_bool(Array(stab.get("defense_tags")).has(&"super_armor"), true, "boss_lunging_stab should carry super_armor defense tag")
var shoot: Resource = resolver_script.get_action(&"boss_shoot_1")
if shoot != null:
_expect_equal(shoot.get("hit_type"), &"projectile", "boss_shoot_1 should use projectile hit_type")
_expect_bool(resolver_script.resolve_pattern("boss_combo_1") == null, true, "Boss action ids should not resolve as player combo patterns")
resolver_script.clear_cache()
func _check_enemy_driver_uses_action_controller() -> void:
var scene: PackedScene = load("res://scenes/enemies/enemy.tscn")
if scene == null:
return
var enemy := scene.instantiate()
root.add_child(enemy)
await process_frame
var controller: Node = enemy.get_node("ActionController")
var driver: Node = enemy.get_node("EnemyActionDriver")
controller.connect("action_started", _on_action_started)
started_actions.clear()
driver.call("start_action", &"boss_combo_1")
_expect(started_actions.has(&"boss_combo_1"), "EnemyActionDriver should start Boss actions through ActionController")
_expect_int(int(controller.get("phase")), 1, "Enemy action should enter Startup, not direct Active")
enemy.queue_free()
await process_frame
func _check_chart_event_reaches_boss_driver() -> void:
started_actions.clear()
var actors := Node2D.new()
actors.name = "ActorsContainer"
root.add_child(actors)
var boss_scene: PackedScene = load("res://scenes/enemies/boss.tscn")
if boss_scene == null:
actors.queue_free()
return
var boss := boss_scene.instantiate()
boss.name = "Boss"
actors.add_child(boss)
await process_frame
boss.get_node("ActionController").connect("action_started", _on_action_started)
var chart_script: Script = load("res://resources/beat_chart.gd")
var track_script: Script = load("res://resources/chart_track.gd")
var chart: Resource = chart_script.new()
var track: Resource = track_script.new()
chart.set("chart_id", &"boss_dispatch_chart")
track.set("track_id", &"track_boss_melee")
track.set("track_type", &"boss")
track.set("events", [_make_event(1, &"Boss", &"boss_combo_1")])
chart.set("tracks", [track])
var runner: Node = load("res://scenes/chart/chart_runner.gd").new()
root.add_child(runner)
runner.set("beat_time_override", 0.5)
runner.set("actors_container_path", runner.get_path_to(actors))
runner.call("set_chart", chart)
runner.call("update_for_song_time", 0.5)
_expect(started_actions.has(&"boss_combo_1"), "ChartRunner should dispatch Boss action_id to Boss EnemyActionDriver")
runner.queue_free()
actors.queue_free()
await process_frame
func _check_chart_event_action_id() -> void:
var event_script: Script = load("res://resources/chart_event.gd")
var event: Resource = event_script.new()
_expect(_has_property(event, "action_id"), "ChartEvent should expose action_id for AIIntent dispatch")
event.set("action_id", &"boss_2way_shoot")
_expect_equal(event.get("action_id"), &"boss_2way_shoot", "ChartEvent action_id should be data-driven")
func _make_event(beat: int, target_id: StringName, action_id: StringName) -> Resource:
var event: Resource = load("res://resources/chart_event.gd").new()
event.set("beat_index", beat)
event.set("event_type", &"enemy_action")
event.set("target_id", target_id)
event.set("lead_beats", 0.0)
event.set("action_id", action_id)
return event
func _boss_action_ids() -> Array[StringName]:
return [
&"boss_combo_1",
&"boss_combo_2",
&"boss_combo_3",
&"boss_lunging_stab",
&"boss_dash",
&"boss_shoot_1",
&"boss_shoot_2",
&"boss_2way_shoot",
]
func _on_action_started(action: Resource, _intent) -> void:
started_actions.append(StringName(str(action.get("id"))))
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 _expect_equal(actual: Variant, expected: Variant, label: String) -> void:
if actual != expected:
failures.append("%s: expected %s, got %s" % [label, expected, actual])
func _expect_bool(actual: bool, expected: bool, label: String) -> void:
if actual != expected:
failures.append("%s: expected %s, got %s" % [label, expected, actual])
func _expect_float(actual: float, expected: float, label: String) -> void:
if not is_equal_approx(actual, expected):
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 enemy action pipeline")
quit(0)
else:
for failure: String in failures:
push_error(failure)
quit(1)