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

116 lines
4.4 KiB
GDScript

extends SceneTree
## 小怪行为状态机运行时验证(docs/Level1Design.md §2):
## 近战怪:玩家在警戒外巡逻 → 玩家进入警戒后下一拍追击 → 进入攻击距离按拍出手。
## 远程怪:站位固定 → 玩家近身过久触发后撤 → 拉开距离后回到站位固定。
var failures: Array[String] = []
var started_actions := 0
func _init() -> void:
_run.call_deferred()
func _run() -> void:
await process_frame
var rhythm := root.get_node_or_null("RhythmManager")
if rhythm != null and rhythm.has_method("stop_manager"):
rhythm.call("stop_manager")
var bus := _ensure_event_bus()
var manager := _ensure_time_phase_manager()
var stage: Node = (load("res://scenes/stage/stage.tscn") as PackedScene).instantiate()
root.add_child(stage)
await process_frame
manager.call("reset_to_initial", &"past")
await process_frame
var container: Node = stage.get_node("ActorsContainer")
var player: Node2D = container.get_node("Player")
var melee: Node2D = container.get_node("JinZhanMinion")
var behavior: Node = melee.get_node("MinionBehavior")
var director: Node = stage.get_node("LevelDirector")
var ground_y := float(director.get("ground_y"))
# ---- 巡逻:玩家在警戒范围外 ----
player.global_position = Vector2(1250.0, ground_y)
for i: int in range(8):
await physics_frame
_expect_equal(behavior.get("state"), &"Patrol", "Melee minion should patrol while the player is outside alert range")
_expect(absf(float(melee.get("approach_direction"))) > 0.0, "Patrol should keep the minion strolling")
# ---- 警戒 → 下一拍进入追击 ----
player.global_position = Vector2(melee.global_position.x - 300.0, ground_y)
await physics_frame
await physics_frame
_expect_equal(behavior.get("state"), &"Patrol", "Alert alone must not change state before the next beat")
bus.emit_signal("beat_ticked", 100)
await physics_frame
_expect_equal(behavior.get("state"), &"Chase", "Melee minion should switch to Chase on the beat after alert")
# ---- 进入攻击距离 → 按拍出手 ----
var controller: Node = melee.get_node("ActionController")
controller.connect("action_started", func(_action: Resource, _intent: Variant) -> void: started_actions += 1)
player.global_position = Vector2(melee.global_position.x - 80.0, ground_y)
await physics_frame
bus.emit_signal("beat_ticked", 101)
await process_frame
_expect(started_actions >= 1, "Melee minion should attack on the beat once in range")
# ---- 远程怪:站位固定 → 近身触发后撤 → 重新固定 ----
var ranged: Node2D = director.call("spawn_minion", &"ranged", Vector2(2050.0, ground_y))
var ranged_behavior: Node = ranged.get_node("MinionBehavior")
ranged_behavior.set("too_close_seconds", 0.2)
await process_frame
_expect_equal(ranged_behavior.get("state"), &"Hold", "Ranged minion should hold its station after spawning")
var station_x := ranged.global_position.x
player.global_position = Vector2(station_x - 60.0, ground_y)
await create_timer(0.5).timeout
var retreat_state: StringName = ranged_behavior.get("state")
_expect(retreat_state == &"Retreat" or ranged.global_position.x > station_x + 40.0, "Crowded ranged minion should retreat (state %s)" % retreat_state)
await create_timer(1.4).timeout
_expect_equal(ranged_behavior.get("state"), &"Hold", "Ranged minion should settle on a new station after retreating")
_expect(ranged.global_position.x > station_x + 100.0, "Retreat should actually gain distance (moved %.0f px)" % (ranged.global_position.x - station_x))
stage.free()
_finish()
func _ensure_event_bus() -> Node:
var bus := root.get_node_or_null("EventBus")
if bus == null:
bus = load("res://autoload/event_bus.gd").new()
bus.name = "EventBus"
root.add_child(bus)
return bus
func _ensure_time_phase_manager() -> Node:
var manager := root.get_node_or_null("TimePhaseManager")
if manager == null:
manager = load("res://autoload/time_phase_manager.gd").new()
manager.name = "TimePhaseManager"
root.add_child(manager)
return manager
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 _finish() -> void:
if failures.is_empty():
print("PASS minion behavior states")
quit(0)
else:
for failure: String in failures:
push_error(failure)
quit(1)