183 lines
7.4 KiB
GDScript
183 lines
7.4 KiB
GDScript
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)
|