5f6258e3d4
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
298 lines
10 KiB
GDScript
298 lines
10 KiB
GDScript
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
|