spaceship/tests/test_aim_converges.gd
2026-09-01 23:50:05 +02:00

232 lines
10 KiB
GDScript

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