extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_lopsided_formation_movement.gd # Regression test for the fleet lurching the wrong way on the first frames # of a move after the formation was edited at a command centre. # # Ships sit at anchor + offset, so the fleet CENTROID is anchor + the # AVERAGE offset. Seeding the steering anchor from the centroid therefore # displaced every slot by that average the moment the stick was pushed. The # default wedge is balanced (average ~zero), so this was invisible; drag # every slot into one corner and the average becomes large, and the fleet # jerks towards that corner before the anchor catches up. # # Verifies, with every slot dragged to the top-right: # 1. starting a move does NOT displace the ships' targets sideways # 2. the fleet actually travels bottom-left from the outset - no frame in # which the centroid moves the wrong way # 3. releasing the stick leaves the formation origin where the ships are, # so re-applying the layout does not teleport them var scene: Node var fleet: Node var game_state: Node var stage: int = 0 var elapsed: float = 0.0 var samples: Array = [] var start_centroid: Vector2 = Vector2.ZERO var worst_backwards: float = 0.0 const HEADING: Vector2 = Vector2(-0.7071, 0.7071) # bottom-left func _initialize() -> void: game_state = root.get_node("GameState") game_state.reset_formation() # Every slot dragged into the top-right corner, as the command centre # allows. The layout keeps its shape; only its centre is offset. for i in range(game_state.formation.size()): game_state.formation[i] += Vector2(90.0, -70.0) scene = load("res://Main.tscn").instantiate() root.add_child(scene) fleet = scene.get_node("Fleet") 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.apply_formation() elapsed = 0.0 stage = 1 return false 1: # Let the ships settle into the lopsided layout first. if elapsed < 1.0: return false start_centroid = fleet.get_centroid() var origin_before: Vector2 = fleet.get_formation_origin() fleet.move_in_direction(HEADING) # Direct assertion of the bug: the anchor steering starts from # must be the point the ships are already arranged around. Taking # the centroid instead puts it a whole average-offset away, which # is what yanked the fleet sideways. var drift: float = fleet.anchor.distance_to(origin_before) print("anchor is %.2f units from the formation origin (centroid is %.2f away)" % [ drift, fleet.get_centroid().distance_to(origin_before) ]) if drift > 1.0: print("TEST_LOPSIDED_FORMATION_MOVEMENT: FAIL (steering anchored off the formation)") quit(1) return true elapsed = 0.0 stage = 2 return false 2: # Watch the centroid every frame: it must make progress along the # heading from the very first one, never against it. var travelled: float = (fleet.get_centroid() - start_centroid).dot(HEADING) worst_backwards = minf(worst_backwards, travelled) samples.append(travelled) if elapsed < 0.6: return false print("progress along heading: first=%.2f last=%.2f worst=%.2f" % [ samples[0], samples[-1], worst_backwards ]) if worst_backwards < -2.0: print("TEST_LOPSIDED_FORMATION_MOVEMENT: FAIL (fleet moved backwards before setting off)") quit(1) return true if samples[-1] < 50.0: print("TEST_LOPSIDED_FORMATION_MOVEMENT: FAIL (fleet did not travel along the heading)") quit(1) return true # --- releasing the stick -------------------------------------- fleet.stop() var origin: Vector2 = fleet.fleet_target var expected: Vector2 = fleet.get_formation_origin() var settled_ok: bool = origin.distance_to(expected) < 0.01 print("after stop: fleet_target=%s formation origin=%s ok=%s" % [ origin, expected, settled_ok ]) if settled_ok: print("TEST_LOPSIDED_FORMATION_MOVEMENT: PASS") quit(0) else: print("TEST_LOPSIDED_FORMATION_MOVEMENT: FAIL (stop left the anchor off the formation)") quit(1) return true return false