spaceship/tests/test_aim_assist.gd

146 lines
5.7 KiB
GDScript3
Raw Normal View History

extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_aim_assist.gd
# Forgejo #89: a shot fired roughly toward an enemy bends onto it, so aiming
# with a thumb on a stick does not have to be exact.
#
# Covers:
# A. an enemy just off the aimed heading is picked up, and the bolt flies at
# IT rather than where the stick pointed
# B. an enemy far off the heading is not - past the cone the shot goes where
# the player pointed, or the gun would be choosing its own targets again,
# which is exactly what #80 took away
# C. the NEAREST candidate inside the cone wins
# D. asteroids are never assisted onto. Snapping to a rock while the player
# is trying to hit the thing shooting at them is the assist working
# against them
# E. the aim guide follows the assist, so the dotted line keeps telling the
# truth about where fire is going
var game_state: Node
var main: Node
var fleet: Node
var controller: Node
var weapon: Node
var shooter: Node2D
var stage: int = 0
var elapsed: float = 0.0
const AIM := Vector2(1.0, 0.0)
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.reset_progression()
main = load("res://world/Main.tscn").instantiate()
root.add_child(main)
current_scene = main
fleet = main.get_node("Fleet")
controller = main.get_node("FireStickController")
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 _enemy_at(offset: Vector2) -> Node2D:
var enemy: Node2D = load("res://enemies/Enemy.tscn").instantiate()
enemy.position = shooter.global_position + offset
main.add_child(enemy)
enemy.speed = 0.0
return enemy
func _heading(direction: Vector2) -> Vector2:
var script: Script = load("res://vessels/VesselWeaponComponent.gd")
return script.assisted_heading(self, shooter.global_position, direction,
weapon.fire_range)
func _process(delta: float) -> bool:
elapsed += delta
if stage != 0:
return false
if elapsed < 0.3:
return false
stage = 1
main.get_node("EnemySpawner").set_process(false)
main.get_node("AsteroidField").set_process(false)
_clear()
fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_INTERCEPTOR]))
shooter = fleet.ships[0]
weapon = shooter.get_component("weapon")
# --- A. just off the heading is picked up -----------------------------
var near_angle: float = deg_to_rad(18.0)
var target: Node2D = _enemy_at(Vector2(600.0, 0.0).rotated(near_angle))
var assisted: Vector2 = _heading(AIM)
var wanted: Vector2 = (target.global_position - shooter.global_position).normalized()
if assisted.distance_to(wanted) > 0.01:
return _fail("an enemy 18 degrees off the heading was not picked up (heading %s)" % assisted)
print("an enemy 18 degrees off the aim is picked up")
# --- B. far off the heading is not ------------------------------------
_clear()
_enemy_at(Vector2(600.0, 0.0).rotated(deg_to_rad(55.0)))
assisted = _heading(AIM)
if assisted.distance_to(AIM) > 0.01:
return _fail("an enemy 55 degrees off the heading was still picked up (heading %s)" % assisted)
print("an enemy 55 degrees off the aim is ignored - the shot goes where it was aimed")
# --- C. the nearest inside the cone wins ------------------------------
_clear()
var far_target: Node2D = _enemy_at(Vector2(800.0, 0.0).rotated(deg_to_rad(5.0)))
var close_target: Node2D = _enemy_at(Vector2(300.0, 0.0).rotated(deg_to_rad(20.0)))
assisted = _heading(AIM)
var to_close: Vector2 = (close_target.global_position - shooter.global_position).normalized()
var to_far: Vector2 = (far_target.global_position - shooter.global_position).normalized()
if assisted.distance_to(to_close) > 0.01:
return _fail("the assist chose the enemy at 800 over the one at 300 (heading %s, close %s, far %s)" \
% [assisted, to_close, to_far])
print("with two in the cone, the nearer one wins")
# --- D. never onto an asteroid ----------------------------------------
_clear()
var rock: Node2D = load("res://world/Asteroid.tscn").instantiate()
rock.position = shooter.global_position + Vector2(400.0, 0.0).rotated(deg_to_rad(15.0))
main.add_child(rock)
assisted = _heading(AIM)
if assisted.distance_to(AIM) > 0.01:
return _fail("the assist snapped onto an asteroid (heading %s)" % assisted)
print("asteroids are never assisted onto")
# --- E. the guide follows the assist ----------------------------------
_clear()
var guide_target: Node2D = _enemy_at(Vector2(500.0, 0.0).rotated(deg_to_rad(20.0)))
controller.aim(AIM)
controller.aim_direction = AIM
var guided: Vector2 = load("res://vessels/VesselWeaponComponent.gd").assisted_heading(
self, fleet.get_centroid(), controller.aim_direction, controller.guide_range())
var from_fleet: Vector2 = (guide_target.global_position - fleet.get_centroid()).normalized()
if guided.distance_to(from_fleet) > 0.02:
return _fail("the aim guide does not follow the assist (guide %s, target %s)" % [guided, from_fleet])
# And the guide must be drawn from that same helper, not a copy of the maths.
var source: FileAccess = FileAccess.open("res://controllers/FireStickController.gd", FileAccess.READ)
var text: String = source.get_as_text()
source.close()
if "assisted_heading" not in text:
return _fail("FireStickController draws its guide without the assist, so the line and the shot would diverge")
print("the aim guide is drawn along the assisted heading, from the same helper the gun uses")
print("TEST_AIM_ASSIST: PASS")
quit(0)
return true
func _fail(message: String) -> bool:
print("TEST_AIM_ASSIST: FAIL (%s)" % message)
quit(1)
return true