extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_ship_facing.gd # Regression test for ships freezing at a fixed heading. # # Ships holding formation sit INSIDE arrive_radius of their slot (the fleet # anchor advances less than that per frame at cruise speed), so they snap # to it with zero velocity. Facing used to be derived purely from a ship's # own velocity behind a "> 8.0" gate, which meant a station-keeping ship # had nothing to face along and stayed pointing whichever way it last # happened to travel - it would not turn when the fleet changed direction. var scene: Node var fleet: Node var stage: int = 0 var elapsed: float = 0.0 var heading_east: float = 0.0 func _initialize() -> void: root.get_node("GameState").reset_formation() scene = load("res://world/Main.tscn").instantiate() root.add_child(scene) fleet = scene.get_node("Fleet") func _angles_match(a: float, b: float, tolerance: float) -> bool: return absf(angle_difference(a, b)) <= tolerance func _process(delta: float) -> bool: elapsed += delta match stage: 0: scene.get_node("AsteroidField").set_process(false) scene.get_node("EnemySpawner").set_process(false) fleet.move_in_direction(Vector2.RIGHT) elapsed = 0.0 stage = 1 return false 1: if elapsed < 1.5: return false # Structural check, because the behavioural ones below cannot # reproduce the actual failure: on a real device at 60fps the # anchor advances ~4.3 units per frame, LESS than arrive_radius, # so a station-keeping ship snaps to its slot with zero velocity # and has no heading of its own to face along. Headless runs # _process uncapped against 60Hz physics, so ships here always # pick up real velocity and look fine either way. Asserting the # imposed heading is wired is what actually guards the fix. var expected_fleet_velocity: Vector2 = Vector2.RIGHT * fleet.cruise_speed for ship in fleet.ships: if not ship.fleet_velocity.is_equal_approx(expected_fleet_velocity): print("fleet_velocity=%s, expected %s" % [ship.fleet_velocity, expected_fleet_velocity]) print("TEST_SHIP_FACING: FAIL (fleet motion not supplied to ships)") quit(1) return true # Flying east: every ship should point east (sprites are drawn # nose-up, hence the +PI/2 convention in Ship._physics_process). heading_east = Vector2.RIGHT.angle() + PI / 2.0 for ship in fleet.ships: if not _angles_match(ship.rotation, heading_east, 0.25): print("ship facing %.3f, expected ~%.3f while flying east" % [ ship.rotation, heading_east ]) print("TEST_SHIP_FACING: FAIL (not facing travel direction)") quit(1) return true print("flying east: all ships facing %.3f" % fleet.ships[0].rotation) # Turn by a SMALL angle. This is the discriminating case: a big # turn shoves each ship sideways hard enough that its own # velocity happens to point roughly the new way, so facing looks # right even when broken. A gentle turn moves each ship only a # few units, its velocity never clears the "> 8.0" gate, and a # ship given no fleet motion simply stays pointing the old way. fleet.move_in_direction(Vector2.RIGHT.rotated(-0.6)) elapsed = 0.0 stage = 2 return false 2: if elapsed < 1.5: return false var heading_north: float = Vector2.RIGHT.rotated(-0.6).angle() + PI / 2.0 for ship in fleet.ships: if not _angles_match(ship.rotation, heading_north, 0.15): print("ship stuck at %.3f, expected ~%.3f after the gentle turn" % [ ship.rotation, heading_north ]) print("TEST_SHIP_FACING: FAIL (did not turn with the fleet)") quit(1) return true print("gentle turn: all ships facing %.3f (was %.3f)" % [ fleet.ships[0].rotation, heading_east ]) print("TEST_SHIP_FACING: PASS") quit(0) return true return false