test: 八项无头回归测试(道具/弹道/Boss/海面/地面单位/浮游炮/炸弹/冒烟模拟)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-08-31 00:31:48 +08:00
parent 3c6f31b503
commit 5f6258e3d4
16 changed files with 1113 additions and 0 deletions
+297
View File
@@ -0,0 +1,297 @@
extends SceneTree
## 浮游炮机型回归测试(时间驱动):
## A) 默认战斗机无浮游炮;GameSession 传递变体 → 新局为浮游炮机
## B) 等级 1/2/3/4 → 浮游炮 0/2/4/6 台
## C) 常态散开:侧位浮游炮激光打得中侧旁堡垒,打不中中线堡垒
## D) 按 I 聚焦:中线高血堡垒被合体粗激光快速击毁
## E) focus 输入映射含 I 键
## 运行:godot --headless -s tests/funnel_test.gd
const GLOBAL_TIMEOUT := 40.0
var _phase := 0
var _step := 0
var _phase_time := 0.0
var _time := 0.0
var _game: Game
var _fort_center: EnemyFortress
var _fort_left: EnemyFortress
var _fort_right: EnemyFortress
var _fort_focus: EnemyFortress
var _fails: Array[String] = []
func _initialize() -> void:
GameSession.plane_variant = 0
_game = (load("res://scenes/game.tscn") as PackedScene).instantiate()
root.add_child(_game)
print("[funnel_test] game A instantiated (fighter)")
func _fail(msg: String) -> void:
_fails.append(msg)
print("[FAIL] " + msg)
func _next_phase() -> void:
_phase += 1
_step = 0
_phase_time = 0.0
func _restart_step() -> void:
_step += 1
_phase_time = 0.0
func _finish() -> bool:
GameSession.plane_variant = 0
if _fails.is_empty():
print("[funnel_test] PASS")
else:
print("[funnel_test] FAIL: %d issue(s)" % _fails.size())
quit(0 if _fails.is_empty() else 1)
return true
func _clear_field() -> void:
for enemy in _game._enemies.get_children():
(enemy as Node).queue_free()
_game.clear_enemy_bullets()
# 防止上一阶段流弹干扰断言
_game.player.invuln = 6.0
func _spawn_fortress(x: float, y: float, hp: int) -> EnemyFortress:
var fortress := EnemyFortress.new()
fortress.setup(x, y, 1.0)
_game._enemies.add_child(fortress)
fortress.hp = hp
return fortress
func _process(delta: float) -> bool:
_time += delta
if _time > GLOBAL_TIMEOUT:
_fail("global timeout in phase %d" % _phase)
return _finish()
if _game._enemies == null:
return false
_phase_time += delta
match _phase:
0:
# A: 战斗机默认无浮游炮
if _step == 0:
if _game.player._funnels.size() != 0:
_fail("fighter has funnels: %d" % _game.player._funnels.size())
else:
print("[funnel_test] fighter has no funnels ok")
_restart_step()
elif _phase_time >= 0.1:
_next_phase()
1:
# A2: 切换变体 → 新局生效(旧 game 立即移除,避免跨实例物理污染)
if _step == 0:
GameSession.plane_variant = 1
var game_b := (load("res://scenes/game.tscn") as PackedScene).instantiate()
root.add_child(game_b)
_game.queue_free()
_game = game_b
_restart_step()
elif _game.player != null:
if _game.player.variant != Player.Variant.FUNNEL:
_fail("variant not applied: %d" % _game.player.variant)
elif _game.player._funnels.size() != 0:
_fail("L1 should have 0 funnels, got %d" % _game.player._funnels.size())
else:
print("[funnel_test] variant applied, L1=0 funnels ok")
_next_phase()
2:
# B: 等级→浮游炮数 + 展开位置分散(两两间距 ≥30px)
if _step == 0:
for level in [2, 3, 4]:
_game.player.weapon_level = level
_game.player._sync_funnels()
var expected: int = (level - 1) * 2
if _game.player._funnels.size() != expected:
_fail("L%d funnels=%d expected %d" % [level, _game.player._funnels.size(), expected])
if _fails.is_empty():
print("[funnel_test] L2/L3/L4 = 2/4/6 funnels ok")
_restart_step()
elif _phase_time >= 0.9:
var positions: Array = []
for funnel in _game.player._funnels:
positions.append((funnel as Node2D).position)
var min_dist := 9999.0
for a in positions.size():
for b in range(a + 1, positions.size()):
min_dist = minf(min_dist, (positions[a] as Vector2).distance_to(positions[b]))
if min_dist < 30.0:
_fail("funnels not spread: min pairwise dist %.1f" % min_dist)
else:
print("[funnel_test] spread formation min pairwise dist %.1f ok" % min_dist)
_next_phase()
3:
# E: focus 映射含 I(73)
if _step == 0:
var focus_ok := false
for event in InputMap.action_get_events("focus"):
if event is InputEventKey and (event as InputEventKey).physical_keycode == 73:
focus_ok = true
if not focus_ok:
_fail("focus action missing I key")
else:
print("[funnel_test] focus=I mapping ok")
_next_phase()
4:
# C: 散开覆盖(L2:±38 侧位)。目标放高处、玩家放低处,保证足够照射窗口
match _step:
0:
_clear_field()
_game.player.weapon_level = 2
_game.player._sync_funnels()
_game.player.position = Vector2(270.0, 700.0)
_fort_center = _spawn_fortress(270.0, 150.0, 60) # 仅机体激光 18dps
_fort_left = _spawn_fortress(234.0, 150.0, 10) # 左浮游炮位(-36)7dps
_fort_right = _spawn_fortress(306.0, 150.0, 10) # 右浮游炮位(+36)
Input.action_press("fire")
_restart_step()
1:
if _phase_time >= 2.2:
var center_stands := is_instance_valid(_fort_center) and not _fort_center.dead
var left_dead := (not is_instance_valid(_fort_left)) or _fort_left.dead
var right_dead := (not is_instance_valid(_fort_right)) or _fort_right.dead
if center_stands and left_dead and right_dead:
print("[funnel_test] spread: sides killed, center survives (hp=%d) ok" % [_fort_center.hp])
else:
_fail("spread outcome wrong: center_stands=%s L=%s R=%s" % [str(center_stands), str(left_dead), str(right_dead)])
Input.action_release("fire")
_next_phase()
5:
# D: 聚焦粗激光(L4 合体 = (18+6×7)×1.5 = 90dps)
match _step:
0:
_clear_field()
_game.player.weapon_level = 4
_game.player._sync_funnels()
_game.player.position = Vector2(270.0, 700.0)
_fort_focus = _spawn_fortress(270.0, 150.0, 60) # 60hp / 90dps ≈ 0.67s
Input.action_press("fire")
Input.action_press("focus")
_restart_step()
1:
var killed := (not is_instance_valid(_fort_focus)) or _fort_focus.dead
if killed:
var dps := _game.player._focus_beam.dps
if dps < 89.0 or dps > 91.0:
_fail("focus dps=%.1f expected 90.0" % dps)
elif _phase_time < 1.6:
print("[funnel_test] focused beam dps=%.0f, killed 60hp in %.2fs ok" % [dps, _phase_time])
else:
_fail("focused beam too slow: %.2fs" % _phase_time)
Input.action_release("fire")
Input.action_release("focus")
_next_phase()
elif _phase_time > 3.0:
_fail("focused beam failed to kill (hp=%d)" % _fort_focus.hp)
Input.action_release("fire")
Input.action_release("focus")
_next_phase()
6:
# E2: 聚焦期间移动速度减半(0.5s 期望位移 ~85px,全速为 170px)
match _step:
0:
_game.player.position = Vector2(100.0, 700.0)
Input.action_press("fire")
Input.action_press("focus")
Input.action_press("move_right")
_restart_step()
1:
if _phase_time >= 0.5:
Input.action_release("move_right")
var dx := _game.player.position.x - 100.0
if dx < 45.0 or dx > 125.0:
_fail("focus speed wrong: dx=%.0f px in 0.5s (expect ~85)" % dx)
else:
print("[funnel_test] focus half-speed ok (dx=%.0f px)" % dx)
Input.action_release("fire")
Input.action_release("focus")
_next_phase()
7:
# E3: 侧身帧状态切换;机头不歪不变量(无旋转、无缩放变换)
match _step:
0:
Input.action_press("move_left")
_restart_step()
1:
if _phase_time >= 0.5:
var state := _game.player._bank_state
if state != -1:
_fail("bank state=%d expected -1 (left)" % state)
elif absf(_game.player.sprite.rotation) > 0.001 or absf(_game.player.sprite.scale.x - 1.0) > 0.001:
_fail("bank must be sprite-swap only: rot=%.3f scale.x=%.3f" % [_game.player.sprite.rotation, _game.player.sprite.scale.x])
else:
print("[funnel_test] bank-left state ok (nose straight, no transform)")
Input.action_release("move_left")
_restart_step()
2:
if _phase_time >= 0.6:
if _game.player._bank_state != 0:
_fail("bank not restored: state=%d" % _game.player._bank_state)
else:
print("[funnel_test] bank restored to neutral ok")
_next_phase()
8:
# E4: 战斗机(P-38)侧身帧真实切换(素材已生成时应换贴图)
match _step:
0:
var fighter := Player.new()
fighter.variant = Player.Variant.FIGHTER
fighter.position = Vector2(270.0, 700.0)
_game.add_child(fighter)
Input.action_press("move_left")
_restart_step()
1:
if _phase_time >= 0.5:
var p := _game.get_child(_game.get_child_count() - 1) as Player
if p == null:
_fail("fighter not found in tree")
elif p._tex_bank_left == null:
_fail("P-38-bank-left.png not loaded (missing art?)")
elif p.sprite.texture != p._tex_bank_left:
_fail("bank texture not swapped for fighter")
elif p._bank_state != -1:
_fail("fighter bank state=%d expected -1" % p._bank_state)
else:
print("[funnel_test] fighter P-38 bank texture swap ok")
Input.action_release("move_left")
_next_phase()
9:
# E5: 雷光(FUNNEL)侧身帧真实切换 + 平飞帧已是美术 PNG
match _step:
0:
var funnel_p := Player.new()
funnel_p.variant = Player.Variant.FUNNEL
funnel_p.position = Vector2(270.0, 700.0)
_game.add_child(funnel_p)
Input.action_press("move_right")
_restart_step()
1:
if _phase_time >= 0.5:
var p := _game.get_child(_game.get_child_count() - 1) as Player
if p == null:
_fail("funnel player not found in tree")
elif p._tex_neutral != null and p._tex_neutral == Player.TEX_FUNNEL:
_fail("funnel neutral still SVG placeholder (plane_funnel.png not loaded?)")
elif p._tex_bank_right == null:
_fail("plane_funnel-bank-right.png not loaded (missing art?)")
elif p.sprite.texture != p._tex_bank_right:
_fail("bank texture not swapped for funnel variant")
elif p._bank_state != 1:
_fail("funnel bank state=%d expected 1" % p._bank_state)
else:
print("[funnel_test] funnel plane_funnel bank texture swap ok")
Input.action_release("move_right")
return _finish()
return false