137 lines
4.8 KiB
GDScript3
137 lines
4.8 KiB
GDScript3
|
|
extends SceneTree
|
||
|
|
|
||
|
|
# Headless gameplay test: run with
|
||
|
|
# godot --headless -s res://tests/test_shot_reach.gd
|
||
|
|
# Forgejo #86: the default gun shoots faster and further. #87: the ship that
|
||
|
|
# fires throws sparks, so a shot is visibly attached to the vessel it came
|
||
|
|
# from.
|
||
|
|
#
|
||
|
|
# The interesting part is not the numbers, it is that a bolt now stops where
|
||
|
|
# the gun's range ends. It used to fly speed * lifetime - 750 units - while
|
||
|
|
# the aim guide drew fire_range, 320. The guide was a promise the shot did not
|
||
|
|
# keep, in the direction that matters: shots landed on things well past the
|
||
|
|
# line saying where they would stop.
|
||
|
|
#
|
||
|
|
# Covers:
|
||
|
|
# A. a bolt's travel distance IS the gun's range, whatever either is set to
|
||
|
|
# B. the aim guide draws that same range, so the line and the shot agree
|
||
|
|
# C. firing throws sparks from the firing ship, and they clear themselves up
|
||
|
|
|
||
|
|
const PICKUP_TOLERANCE: float = 60.0
|
||
|
|
|
||
|
|
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
|
||
|
|
var bolt: Node2D
|
||
|
|
var launch_point: Vector2 = Vector2.ZERO
|
||
|
|
var furthest: float = 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 _sparks() -> Array:
|
||
|
|
var found: Array = []
|
||
|
|
for node in root.get_children():
|
||
|
|
if node.get_script() != null and String(node.get_script().resource_path).ends_with("MuzzleSparkVFX.gd"):
|
||
|
|
found.append(node)
|
||
|
|
return found
|
||
|
|
|
||
|
|
|
||
|
|
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)
|
||
|
|
for hazard in get_nodes_in_group("asteroids") + get_nodes_in_group("enemies") \
|
||
|
|
+ get_nodes_in_group("player_projectiles"):
|
||
|
|
if is_instance_valid(hazard):
|
||
|
|
hazard.free()
|
||
|
|
fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_INTERCEPTOR]))
|
||
|
|
shooter = fleet.ships[0]
|
||
|
|
weapon = shooter.get_component("weapon")
|
||
|
|
if weapon == null:
|
||
|
|
return _fail("the default vessel has no weapon component")
|
||
|
|
|
||
|
|
# --- B. the guide draws the gun's own range --------------------
|
||
|
|
controller.aim(Vector2(1.0, 0.0))
|
||
|
|
if not is_equal_approx(controller.guide_range(), weapon.fire_range):
|
||
|
|
return _fail("the aim guide reaches %.0f but the gun's range is %.0f" \
|
||
|
|
% [controller.guide_range(), weapon.fire_range])
|
||
|
|
print("aim guide and gun range agree at %.0f" % weapon.fire_range)
|
||
|
|
|
||
|
|
launch_point = shooter.global_position
|
||
|
|
weapon.fire_timer = 0.0
|
||
|
|
weapon.tick(0.0)
|
||
|
|
var bolts: Array = get_nodes_in_group("player_projectiles")
|
||
|
|
if bolts.is_empty():
|
||
|
|
return _fail("the gun fired nothing")
|
||
|
|
bolt = bolts[0]
|
||
|
|
|
||
|
|
# --- C. sparks at the muzzle -----------------------------------
|
||
|
|
var sparks: Array = _sparks()
|
||
|
|
if sparks.is_empty():
|
||
|
|
return _fail("firing threw no sparks")
|
||
|
|
if sparks[0].global_position.distance_to(shooter.global_position) > PICKUP_TOLERANCE:
|
||
|
|
return _fail("the sparks appeared %.0f from the ship that fired" \
|
||
|
|
% sparks[0].global_position.distance_to(shooter.global_position))
|
||
|
|
print("firing threw sparks at the firing ship")
|
||
|
|
stage = 1
|
||
|
|
elapsed = 0.0
|
||
|
|
return false
|
||
|
|
1:
|
||
|
|
# --- A. the bolt stops where the range ends --------------------
|
||
|
|
if is_instance_valid(bolt) and not bolt.is_queued_for_deletion():
|
||
|
|
furthest = maxf(furthest, launch_point.distance_to(bolt.global_position))
|
||
|
|
return false
|
||
|
|
var reach: float = weapon.fire_range
|
||
|
|
# One physics step of slack either way: the bolt is freed on the
|
||
|
|
# tick its age passes the limit, so it lands just short of or just
|
||
|
|
# past the exact figure.
|
||
|
|
var slack: float = bolt_step()
|
||
|
|
if absf(furthest - reach) > slack:
|
||
|
|
return _fail("the bolt travelled %.0f but the gun's range is %.0f (slack %.0f)" \
|
||
|
|
% [furthest, reach, slack])
|
||
|
|
print("bolt stopped at %.0f against a range of %.0f" % [furthest, reach])
|
||
|
|
|
||
|
|
if not _sparks().is_empty():
|
||
|
|
return _fail("%d spark burst(s) still alive long after firing" % _sparks().size())
|
||
|
|
print("sparks cleared themselves up")
|
||
|
|
|
||
|
|
print("TEST_SHOT_REACH: PASS")
|
||
|
|
quit(0)
|
||
|
|
return true
|
||
|
|
return false
|
||
|
|
|
||
|
|
|
||
|
|
func bolt_step() -> float:
|
||
|
|
# How far a bolt moves in a single physics tick, which is the granularity
|
||
|
|
# its lifetime can be enforced at.
|
||
|
|
var speed: float = 850.0
|
||
|
|
var probe: Node2D = load("res://vessels/Projectile.tscn").instantiate()
|
||
|
|
speed = float(probe.speed)
|
||
|
|
probe.free()
|
||
|
|
return speed / float(Engine.physics_ticks_per_second) * 1.5
|
||
|
|
|
||
|
|
|
||
|
|
func _fail(message: String) -> bool:
|
||
|
|
print("TEST_SHOT_REACH: FAIL (%s)" % message)
|
||
|
|
quit(1)
|
||
|
|
return true
|