extends SceneTree ## 玩家子弹弹道回归测试(时间驱动): ## L4 齐射后快照所有子弹位置,0.25s 后比对实际位移方向, ## 必须命中 UP / ±22° / ±30° 五个弹道之一,且各组数量正确。 ## 在 spawner 首波敌机(0.8s)出现前完成测量,保证确定性。 ## 运行:godot --headless -s tests/bullet_traj_test.gd const SNAP_DELAY := 0.1 const FLY_TIME := 0.25 const DOT_EPS := 0.995 const GLOBAL_TIMEOUT := 15.0 var _time := 0.0 var _snapped := false var _game: Game var _pbullets: Node2D var _fails: Array[String] = [] var _snapshot: Array = [] # [bullet: Node2D, pos: Vector2] func _initialize() -> void: _game = (load("res://scenes/game.tscn") as PackedScene).instantiate() root.add_child(_game) print("[traj_test] game instantiated") func _fail(msg: String) -> void: _fails.append(msg) print("[FAIL] " + msg) func _ensure_refs() -> bool: if _pbullets == null: _pbullets = _game.get_node_or_null("PlayerBullets") as Node2D return _pbullets != null func _process(delta: float) -> bool: _time += delta if _time > GLOBAL_TIMEOUT: _fail("global timeout") return _finish() if not _ensure_refs(): return false if not _snapped and _time >= SNAP_DELAY: _snapped = true # 先射一梭子 L4(fire 从首帧按住,此处已有一发齐射在飞) _game.player.weapon_level = 4 Input.action_release("fire") _game.player._fire() _snapshot = [] for bullet in _pbullets.get_children(): _snapshot.append([bullet, (bullet as Node2D).position]) if _snapshot.size() != 12: _fail("L4 volley=%d expected 12" % _snapshot.size()) return false if _snapped and _time >= SNAP_DELAY + FLY_TIME: var allowed := [ Vector2.UP, Vector2.UP.rotated(deg_to_rad(-22.0)), Vector2.UP.rotated(deg_to_rad(22.0)), Vector2.UP.rotated(deg_to_rad(-30.0)), Vector2.UP.rotated(deg_to_rad(30.0)), ] var counts := [0, 0, 0, 0, 0] var classified := 0 for entry in _snapshot: var bullet: Node2D = entry[0] var start: Vector2 = entry[1] if not is_instance_valid(bullet): _fail("bullet freed before measurement window ended") continue var disp: Vector2 = bullet.position - start if disp.length() < 50.0: _fail("bullet barely moved: %.1f px" % disp.length()) continue var dir := disp.normalized() for i in allowed.size(): if dir.dot(allowed[i]) >= DOT_EPS: counts[i] += 1 classified += 1 break if classified != _snapshot.size(): _fail("only %d/%d bullets on valid trajectories" % [classified, _snapshot.size()]) elif counts[0] != 6 or counts[1] != 2 or counts[2] != 2 or counts[3] != 1 or counts[4] != 1: _fail("trajectory group counts wrong: straight=%d L22=%d R22=%d L30=%d R30=%d" % counts) else: print("[traj_test] trajectories ok: straight=%d ±22=%d+%d ±30=%d+%d" % [counts[0], counts[1], counts[2], counts[3], counts[4]]) return _finish() return false func _finish() -> bool: if _fails.is_empty(): print("[traj_test] PASS") else: print("[traj_test] FAIL: %d issue(s)" % _fails.size()) quit(0 if _fails.is_empty() else 1) return true