extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_aim_converges.gd # Forgejo #172: "when aiming, whether with mouse or touch controls, we should # aim for a given point on the space (still using mouse or the joystick). As # it's now a point and not a direction, it lets the player try to get the # fleet's shot converge and not go in parallel. The auto aim when trying to # aim for an enemy vessel should still work." # # The failure this replaces is worth stating, because it is invisible in a # fleet of one and gets worse with every hull added: every gun was handed the # SAME direction, so a fleet firing at something threw a sheet of parallel # bolts and only the ship that happened to be lined up could hit. The wider # the formation, the further the fire missed by. # # Covers: # A. two hulls either side of a target aim at IT, not along one shared # heading - and each one's line actually arrives at the point # B. the mouse hands over a place: the cursor's world position, distance # included, rather than a bearing from the fleet's centre # C. stick displacement controls convergence distance: a partial push aims # nearby and a full push reaches the guns' maximum range # D. the assist (#89) still bends a shot onto an enemy, now per hull and # around each hull's own heading to the point # E. releasing stops every gun in the fleet # F. range belongs to the gun, not to the aim: a point past reach is fired # toward rather than refused or clamped into a different heading const WEAPON = preload("res://vessels/VesselWeaponComponent.gd") var game_state: Node var main: Node var fleet: Node var controller: Node var mouse: Node var stage: int = 0 var elapsed: float = 0.0 var left_ship: Node2D var right_ship: Node2D func _initialize() -> void: game_state = root.get_node("GameState") game_state.buildings.clear() game_state.extractors.clear() game_state.reset_progression() game_state.intro_active = false game_state.reset_quests() main = load("res://world/Main.tscn").instantiate() root.add_child(main) current_scene = main fleet = main.get_node("Fleet") controller = main.get_node("FireStickController") mouse = main.get_node("MouseCombatController") func _fail(message: String) -> bool: print("TEST_AIM_CONVERGES: FAIL (%s)" % message) quit(1) return true func _clear() -> void: for node in get_nodes_in_group("enemies") + get_nodes_in_group("asteroids") \ + get_nodes_in_group("player_projectiles"): if is_instance_valid(node): node.free() func _weapon(ship: Node2D) -> Node: return ship.get_component("weapon") # Where a shot from this hull would arrive if it flew far enough: the hull, # plus its own heading, times the distance to the aimed point. The whole of # "converging" is that this comes out the same for every hull. func _lands_at(ship: Node2D, point: Vector2) -> Vector2: var weapon: Node = _weapon(ship) return ship.global_position \ + weapon.firing_heading() * ship.global_position.distance_to(point) func _process(delta: float) -> bool: elapsed += delta match stage: 0: if elapsed < 0.3: return false main.get_node("EnemySpawner").set_process(false) main.get_node("AsteroidField").set_process(false) _clear() fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray( [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_INTERCEPTOR])) if fleet.ships.size() != 2: return _fail("wanted two hulls, got %d" % fleet.ships.size()) # Placed by hand, wide apart and level, so the geometry the test # talks about is the geometry it set up rather than whatever the # current default formation happens to be. left_ship = fleet.ships[0] right_ship = fleet.ships[1] left_ship.global_position = Vector2(-400.0, 0.0) right_ship.global_position = Vector2(400.0, 0.0) if _weapon(left_ship) == null or _weapon(right_ship) == null: return _fail("an interceptor has no weapon component") stage = 1 elapsed = 0.0 return false 1: if elapsed < 0.1: return false # --- A. two hulls, one point -------------------------------- var point := Vector2(0.0, -900.0) controller.aim_at(point) var left: Vector2 = _weapon(left_ship).firing_heading() var right: Vector2 = _weapon(right_ship).firing_heading() if left == Vector2.ZERO or right == Vector2.ZERO: return _fail("aiming at a point left a gun with no heading") if left.distance_to(right) < 0.1: return _fail("both hulls fire along %s - that is a direction, not a point" % left) for ship in [left_ship, right_ship]: var lands: Vector2 = _lands_at(ship, point) if lands.distance_to(point) > 1.0: return _fail("a hull at %s fires at %s, %.0f from the point it was given" \ % [ship.global_position, lands, lands.distance_to(point)]) print("hulls %.0f apart fire on headings %.0f degrees apart, and both arrive at the point" \ % [left_ship.global_position.distance_to(right_ship.global_position), rad_to_deg(left.angle_to(right))]) # --- B. the mouse hands over a place ------------------------ mouse.pointer_mode = mouse.PointerMode.ON mouse.select_weapon(0) # Deliberately NOT on the line from the fleet's centre through # anything: a bearing-from-the-centre implementation would still # pass a point straight ahead. var cursor := Vector2(650.0, -300.0) mouse.aim_world = cursor mouse.firing = true mouse._process(0.0) controller._process(0.0) for ship in [left_ship, right_ship]: if _weapon(ship).fire_target.distance_to(cursor) > 0.5: return _fail("the mouse aimed a hull at %s, the cursor was at %s" \ % [_weapon(ship).fire_target, cursor]) if _lands_at(left_ship, cursor).distance_to(_lands_at(right_ship, cursor)) > 1.0: return _fail("the two hulls' fire does not meet at the cursor") print("holding the button aims every hull at the cursor's own position") # --- E. and releasing stops all of them --------------------- mouse.firing = false mouse._process(0.0) controller._process(0.0) for ship in [left_ship, right_ship]: if _weapon(ship).is_firing(): return _fail("releasing the button left a gun firing") print("releasing stops every gun") # --- C. a stick still resolves to a point ------------------- var reach: float = controller.guide_range() if reach <= 0.0: return _fail("the fleet reports no gun range") var pushed := Vector2(0.0, -1.0) var resolved: Vector2 = controller.stick_point(pushed) var wanted: Vector2 = fleet.get_centroid() + pushed * reach if resolved.distance_to(wanted) > 1.0: return _fail("a stick pushed up resolved to %s, expected %s" % [resolved, wanted]) controller.aim(pushed) for ship in [left_ship, right_ship]: if not _weapon(ship).is_firing(): return _fail("a pushed stick did not fire a gun") if _weapon(ship).fire_target.distance_to(wanted) > 1.0: return _fail("a pushed stick aimed a hull somewhere other than the point it names") print("a pushed stick names the point at the guns' reach (%.0f) along it" % reach) # A half push must keep its magnitude. TouchJoystick already reports # displacement in the 0..1 range; normalizing it here used to turn # every nonzero push into a maximum-range aim. var partial := Vector2(0.0, -0.45) var partial_point: Vector2 = controller.stick_point(partial) var partial_wanted: Vector2 = fleet.get_centroid() + partial * reach if partial_point.distance_to(partial_wanted) > 1.0: return _fail("a 45%% stick push resolved to %s, expected nearby point %s" \ % [partial_point, partial_wanted]) controller.aim(partial) for ship in [left_ship, right_ship]: if _weapon(ship).fire_target.distance_to(partial_wanted) > 1.0: return _fail("a partial stick push did not aim the fleet at its nearer point") print("a 45%% stick push aims at 45%% range (%.0f), not maximum range" \ % fleet.get_centroid().distance_to(partial_point)) # --- F. reach is the gun's, not the aim's ------------------- var far_point: Vector2 = fleet.get_centroid() + Vector2(0.0, -reach * 3.0) controller.aim_at(far_point) var to_far: Vector2 = (far_point - left_ship.global_position).normalized() if _weapon(left_ship).firing_heading().distance_to(to_far) > 0.01: return _fail("a point past the guns' reach bent the heading to %s, expected %s" \ % [_weapon(left_ship).firing_heading(), to_far]) print("a point beyond reach is still fired toward - the bolt falls short, the aim does not bend") # --- D. the assist still works, per hull -------------------- # Each hull asks its own question now - "is anything within MY # reach, near MY heading to the point" - so the case worth # checking is one where the two hulls answer differently. The # right hull is moved out past its own gun range of the enemy, so # it cannot be assisted onto it however the angles fall. _clear() right_ship.global_position = Vector2(reach * 1.5, 0.0) var target_point := Vector2(0.0, -300.0) var enemy: Node2D = load("res://enemies/Enemy.tscn").instantiate() enemy.global_position = left_ship.global_position \ + (target_point - left_ship.global_position).normalized().rotated( deg_to_rad(15.0)) * 150.0 main.add_child(enemy) enemy.speed = 0.0 if right_ship.global_position.distance_to(enemy.global_position) <= reach: return _fail("the test failed to put the far hull out of range of the enemy") controller.aim_at(target_point) var to_enemy: Vector2 = (enemy.global_position - left_ship.global_position).normalized() if _weapon(left_ship).firing_heading().distance_to(to_enemy) > 0.02: return _fail("the assist no longer bends a shot onto an enemy near the aim (heading %s, enemy %s)" \ % [_weapon(left_ship).firing_heading(), to_enemy]) var right_wanted: Vector2 = (target_point - right_ship.global_position).normalized() if _weapon(right_ship).firing_heading().distance_to(right_wanted) > 0.02: return _fail("a hull out of range of the enemy was still bent onto it - the assist is not per hull") print("the assist still snaps onto an enemy near the aim, and only for the hull that can reach it") enemy.free() print("TEST_AIM_CONVERGES: PASS") quit(0) return true return false