146 lines
5.7 KiB
GDScript3
146 lines
5.7 KiB
GDScript3
|
|
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
|