extends SceneTree ## 第一关出怪流程(关卡设计指南 §7/§8): ## 阶段一预置近战怪 → 击杀后阶段二在玩家附近刷远程怪 → 再击杀后阶段三 ## 左右交替增援 2近战+2远程 → 全灭 6 只解封 Boss 房门。 var failures: Array[String] = [] var cleared_signals := 0 const MINION_GATE_CLEARANCE := 220.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() if bus.has_signal("front_area_cleared"): bus.connect("front_area_cleared", func() -> void: cleared_signals += 1) var stage_scene: PackedScene = load("res://scenes/stage/stage.tscn") _expect(stage_scene != null, "stage.tscn should load") if stage_scene == null: _finish() return var stage: Node = stage_scene.instantiate() root.add_child(stage) await process_frame await process_frame var director: Node = stage.get_node_or_null("LevelDirector") var gate: Node = stage.get_node_or_null("BossRoomGate") var container: Node = stage.get_node("ActorsContainer") var player: Node2D = container.get_node("Player") _expect(director != null, "Stage should carry a LevelDirector") _expect(gate != null, "Stage should carry a BossRoomGate") if director == null or gate == null: stage.free() _finish() return director.set("reinforcement_time_scale", 0.01) # ---- 阶段一:只有 1 只预置近战怪,Boss 房门封锁 ---- var initial := _alive_minions(container) _expect_int(initial.size(), 1, "Phase 1 should start with exactly one pre-placed minion") if initial.size() == 1: _expect_equal(initial[0].get("strength_profile"), &"past_strong", "The pre-placed minion should be the melee (past-strong) one") _expect_minions_left_of_gate(initial, gate, "Initial pre-placed minion") var initial_behavior := initial[0].get_node_or_null("MinionBehavior") if initial_behavior != null: _expect_float(float(initial_behavior.get("arena_max_x")), float(gate.get("gate_x")) - MINION_GATE_CLEARANCE, "Initial pre-placed minion should use the front-area safe patrol max") _expect(bool(gate.get("sealed")), "Gate should start sealed") var wall_shape := gate.get_node("GateWall").get_child(0) as CollisionShape2D _expect(not wall_shape.disabled, "Sealed gate should enable its blocking wall") _expect_int(int(director.get("phase")), 1, "Director should start in phase 1") # ---- 击杀近战怪 → 阶段二远程怪在玩家附近刷新(可感知、不贴脸)---- _kill(initial[0]) await process_frame _expect_int(int(director.get("phase")), 2, "First kill should advance to phase 2") var phase2 := _alive_minions(container) _expect_int(phase2.size(), 1, "Phase 2 should spawn exactly one new minion") if phase2.size() == 1: var ranged: Node2D = phase2[0] _expect_equal(ranged.get("strength_profile"), &"future_strong", "Phase 2 minion should be the ranged (future-strong) one") _expect_minions_left_of_gate(phase2, gate, "Phase 2 minion") var distance := absf(ranged.global_position.x - player.global_position.x) _expect(distance >= 200.0 and distance <= 480.0, "Ranged minion should spawn near but not on the player (got %.0f px)" % distance) var behavior := ranged.get_node_or_null("MinionBehavior") _expect(behavior != null and behavior.get("role") == &"ranged", "Phase 2 minion behavior should use the ranged role") # ---- 击杀远程怪 → 阶段三增援:近战L → 远程R → 近战R → 远程L ---- _kill(ranged) await process_frame _expect_int(int(director.get("phase")), 3, "Second kill should advance to phase 3") # 缩放后的增援间隔与 headless 帧长同数量级:只断言 0 秒那只已经 # 立即出现,后续数量交给下面“全部四只”断言覆盖。 var first_wave := _alive_minions(container) _expect(first_wave.size() >= 1, "First reinforcement should spawn immediately (got %d)" % first_wave.size()) if first_wave.size() >= 1: _expect_equal(first_wave[0].get("strength_profile"), &"past_strong", "First reinforcement should be melee") _expect(first_wave[0].global_position.x < 1500.0, "First reinforcement should enter from the LEFT edge") # 其余三只按 0.01 缩放的间隔陆续出现。 await create_timer(0.5).timeout await process_frame var reinforcements := _alive_minions(container) _expect_int(reinforcements.size(), 4, "All four reinforcements should have spawned") _expect_minions_left_of_gate(reinforcements, gate, "Phase 3 reinforcement") _expect_int(int(director.get("total_spawned")), 6, "Level should field six minions in total") var melee_count := 0 var ranged_count := 0 for minion: Node2D in reinforcements: if minion.get("strength_profile") == &"past_strong": melee_count += 1 else: ranged_count += 1 _expect_int(melee_count, 2, "Reinforcements should include two melee minions") _expect_int(ranged_count, 2, "Reinforcements should include two ranged minions") # ---- 全灭 → 解封 Boss 房门 ---- _expect(bool(gate.get("sealed")), "Gate must stay sealed while reinforcements live") for minion: Node2D in reinforcements: _kill(minion) await process_frame await process_frame _expect(bool(director.get("cleared")), "Director should report the front area cleared") _expect(not bool(gate.get("sealed")), "Clearing all six minions should unseal the gate") _expect(wall_shape.disabled, "Unsealed gate should disable its blocking wall") _expect_int(cleared_signals, 1, "front_area_cleared should broadcast exactly once") stage.free() _finish() func _alive_minions(container: Node) -> Array[Node2D]: var result: Array[Node2D] = [] for child: Node in container.get_children(): if not child.is_in_group("enemies"): continue var health := child.get_node_or_null("HealthComponent") if health != null and int(health.get("current")) > 0: result.append(child as Node2D) return result func _kill(minion: Node) -> void: var health := minion.get_node_or_null("HealthComponent") if health != null: health.call("apply_damage", 999999) func _expect_minions_left_of_gate(minions: Array[Node2D], gate: Node, label: String) -> void: var limit := float(gate.get("gate_x")) - MINION_GATE_CLEARANCE for minion: Node2D in minions: _expect( minion.global_position.x <= limit, "%s should stay left of the boss-room air wall safety line %.0f, got %.0f" % [label, limit, minion.global_position.x] ) 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 _expect(condition: bool, label: String) -> void: if not condition: failures.append(label) func _expect_int(actual: int, expected: int, label: String) -> void: if actual != expected: failures.append("%s: expected %d, got %d" % [label, expected, actual]) 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_float(actual: float, expected: float, label: String, tolerance := 0.01) -> void: if absf(actual - expected) > tolerance: failures.append("%s: expected %.2f, got %.2f" % [label, expected, actual]) func _finish() -> void: if failures.is_empty(): print("PASS level1 spawn flow") quit(0) else: for failure: String in failures: push_error(failure) quit(1)