181 lines
6.4 KiB
GDScript3
181 lines
6.4 KiB
GDScript3
|
|
extends SceneTree
|
||
|
|
|
||
|
|
# Headless gameplay test: run with
|
||
|
|
# godot --headless -s res://tests/test_laser_auto_aim.gd
|
||
|
|
# Tapping the laser button without dragging used to do nothing whatsoever:
|
||
|
|
# the press was claimed and the aim begun, then releasing inside the aim
|
||
|
|
# deadzone threw the whole thing away. It reads as a dead button. A tap now
|
||
|
|
# fires at the nearest enemy in range; dragging is still how a shot is placed
|
||
|
|
# precisely.
|
||
|
|
#
|
||
|
|
# Covers:
|
||
|
|
# A. a tap picks the NEAREST enemy, not merely any enemy, and fires
|
||
|
|
# B. it fires at all - the shot lands and the target takes the hit
|
||
|
|
# C. a tap with nothing in range does not fire, says so, and leaves the
|
||
|
|
# shot unspent rather than wasting it on empty space
|
||
|
|
# D. a tap does not consume the drag-aim path: dragging past the deadzone
|
||
|
|
# still aims where it was dragged, not at the nearest enemy
|
||
|
|
|
||
|
|
var game_state: Node
|
||
|
|
var main: Node
|
||
|
|
var fleet: Node
|
||
|
|
var skill: Node
|
||
|
|
var shooter: Node2D
|
||
|
|
var near_enemy: Node
|
||
|
|
var far_enemy: Node
|
||
|
|
var near_position: Vector2 = Vector2.ZERO
|
||
|
|
var far_position: Vector2 = Vector2.ZERO
|
||
|
|
var stage: int = 0
|
||
|
|
var elapsed: float = 0.0
|
||
|
|
|
||
|
|
|
||
|
|
func _initialize() -> void:
|
||
|
|
game_state = root.get_node("GameState")
|
||
|
|
game_state.buildings.clear()
|
||
|
|
game_state.extractors.clear()
|
||
|
|
game_state.stored_vessels.clear()
|
||
|
|
game_state.reset_progression()
|
||
|
|
game_state.vessel_levels["unlock_laser"] = 1
|
||
|
|
game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 500)
|
||
|
|
main = load("res://Main.tscn").instantiate()
|
||
|
|
root.add_child(main)
|
||
|
|
current_scene = main
|
||
|
|
fleet = main.get_node("Fleet")
|
||
|
|
skill = main.get_node("LaserController")
|
||
|
|
|
||
|
|
|
||
|
|
func _tap() -> void:
|
||
|
|
# Press and release at the same point - inside AIM_DEADZONE, so this is a
|
||
|
|
# tap and not a drag.
|
||
|
|
var button: Control = main.get_node("UI/LaserButton")
|
||
|
|
var at: Vector2 = button.global_position + button.size * 0.5
|
||
|
|
for pressed in [true, false]:
|
||
|
|
var event := InputEventScreenTouch.new()
|
||
|
|
event.index = 7
|
||
|
|
event.position = at
|
||
|
|
event.pressed = pressed
|
||
|
|
main._handle_touch(event)
|
||
|
|
|
||
|
|
|
||
|
|
func _process(delta: float) -> bool:
|
||
|
|
elapsed += delta
|
||
|
|
match stage:
|
||
|
|
0:
|
||
|
|
if elapsed < 0.2:
|
||
|
|
return false
|
||
|
|
main.get_node("EnemySpawner").set_process(false)
|
||
|
|
main.get_node("AsteroidField").set_process(false)
|
||
|
|
# Whatever the world spawned would be a second, unintended target.
|
||
|
|
for hazard in get_nodes_in_group("asteroids") + get_nodes_in_group("enemies"):
|
||
|
|
if is_instance_valid(hazard):
|
||
|
|
hazard.queue_free()
|
||
|
|
|
||
|
|
fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_LASER]))
|
||
|
|
skill._process(0.0)
|
||
|
|
if not main.get_node("UI/LaserButton").visible:
|
||
|
|
return _fail("a laser ship in the fleet did not reveal the laser control")
|
||
|
|
shooter = skill._next_ready_laser_ship()
|
||
|
|
if shooter == null:
|
||
|
|
return _fail("no laser ship was ready right after spawning")
|
||
|
|
stage = 1
|
||
|
|
elapsed = 0.0
|
||
|
|
return false
|
||
|
|
1:
|
||
|
|
if elapsed < 0.1:
|
||
|
|
return false
|
||
|
|
|
||
|
|
# --- C. nothing in range: no shot, and it says why ---------------
|
||
|
|
_tap()
|
||
|
|
if not skill.beams.is_empty():
|
||
|
|
return _fail("tapping with no enemy in range fired anyway")
|
||
|
|
if skill.aiming:
|
||
|
|
return _fail("tapping with no target left the control stuck in aiming")
|
||
|
|
if main.get_node("UI/LaserStatus").text == "":
|
||
|
|
return _fail("tapping with no target fired nothing and said nothing")
|
||
|
|
print("no target in range: no shot fired, status explains why")
|
||
|
|
|
||
|
|
# --- A/B. two targets, the nearer one is picked ------------------
|
||
|
|
# Deliberately placed in DIFFERENT directions, so hitting the near
|
||
|
|
# one cannot be explained by the beam happening to pass through
|
||
|
|
# both. Both are well inside range.
|
||
|
|
far_enemy = _spawn_enemy(Vector2(0.0, -900.0))
|
||
|
|
near_enemy = _spawn_enemy(Vector2(400.0, 0.0))
|
||
|
|
far_position = far_enemy.global_position
|
||
|
|
near_position = near_enemy.global_position
|
||
|
|
stage = 2
|
||
|
|
elapsed = 0.0
|
||
|
|
return false
|
||
|
|
2:
|
||
|
|
if elapsed < 0.1:
|
||
|
|
return false
|
||
|
|
_tap()
|
||
|
|
if skill.beams.is_empty():
|
||
|
|
return _fail("tapping with an enemy in range did not fire")
|
||
|
|
if skill.last_hit_count < 1:
|
||
|
|
return _fail("the auto-aimed shot hit nothing")
|
||
|
|
# Asserted on where it AIMED, not on a body count. Enemies have
|
||
|
|
# hit points and a beam damages rather than always destroys, so
|
||
|
|
# "did it die" would be measuring the damage numbers instead of
|
||
|
|
# the targeting this test is about.
|
||
|
|
if skill.aim_target.distance_to(near_position) > 1.0:
|
||
|
|
return _fail("the tap aimed at %s, but the nearest enemy is at %s" \
|
||
|
|
% [skill.aim_target, near_position])
|
||
|
|
print("tap aimed at the nearest enemy (%.0f away), not the one %.0f away in another direction" \
|
||
|
|
% [shooter.global_position.distance_to(near_position),
|
||
|
|
shooter.global_position.distance_to(far_position)])
|
||
|
|
|
||
|
|
# --- D. dragging still aims where it is dragged ------------------
|
||
|
|
stage = 3
|
||
|
|
elapsed = 0.0
|
||
|
|
return false
|
||
|
|
3:
|
||
|
|
# Wait out the per-ship cooldown so the next shot is available.
|
||
|
|
if elapsed < 6.0:
|
||
|
|
return false
|
||
|
|
if skill._next_ready_laser_ship() == null:
|
||
|
|
return _fail("the laser ship never came off cooldown")
|
||
|
|
var button: Control = main.get_node("UI/LaserButton")
|
||
|
|
var at: Vector2 = button.global_position + button.size * 0.5
|
||
|
|
var press := InputEventScreenTouch.new()
|
||
|
|
press.index = 8
|
||
|
|
press.position = at
|
||
|
|
press.pressed = true
|
||
|
|
main._handle_touch(press)
|
||
|
|
# Dragged straight UP, towards the far enemy and away from where
|
||
|
|
# auto-aim would have pointed.
|
||
|
|
var drag := InputEventScreenDrag.new()
|
||
|
|
drag.index = 8
|
||
|
|
drag.position = at + Vector2(0.0, -300.0)
|
||
|
|
main._handle_drag(drag)
|
||
|
|
var release := InputEventScreenTouch.new()
|
||
|
|
release.index = 8
|
||
|
|
release.position = drag.position
|
||
|
|
release.pressed = false
|
||
|
|
main._handle_touch(release)
|
||
|
|
|
||
|
|
# Dragged up, so the shot must go UP - not to the nearest enemy,
|
||
|
|
# which lies to the right. Checked as a direction because the
|
||
|
|
# laser always extends to its full range along the aimed heading.
|
||
|
|
var heading: Vector2 = (skill.aim_target - shooter.global_position).normalized()
|
||
|
|
if heading.y > -0.9:
|
||
|
|
return _fail("dragging upward aimed along %s - auto-aim overrode a deliberate aim" % heading)
|
||
|
|
print("dragging still aims where it is dragged (heading %s), not at the nearest enemy" % heading)
|
||
|
|
|
||
|
|
print("TEST_LASER_AUTO_AIM: PASS")
|
||
|
|
quit(0)
|
||
|
|
return true
|
||
|
|
return false
|
||
|
|
|
||
|
|
|
||
|
|
func _spawn_enemy(offset: Vector2) -> Node:
|
||
|
|
var enemy: Node = load("res://Enemy.tscn").instantiate()
|
||
|
|
enemy.position = shooter.global_position + offset
|
||
|
|
main.add_child(enemy)
|
||
|
|
enemy.speed = 0.0
|
||
|
|
return enemy
|
||
|
|
|
||
|
|
|
||
|
|
func _fail(message: String) -> bool:
|
||
|
|
print("TEST_LASER_AUTO_AIM: FAIL (%s)" % message)
|
||
|
|
quit(1)
|
||
|
|
return true
|