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