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)