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:
@@ -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
|
||||
Reference in New Issue
Block a user