224 lines
8.5 KiB
GDScript3
224 lines
8.5 KiB
GDScript3
|
|
extends SceneTree
|
||
|
|
|
||
|
|
# Headless test: run with
|
||
|
|
# godot --headless -s res://tests/test_ship_death.gd
|
||
|
|
# Forgejo #130: "they would break away from the fleet by a few metres, and
|
||
|
|
# end up exploding a little further off, with a rather more explosive
|
||
|
|
# explosion than just a light animation."
|
||
|
|
#
|
||
|
|
# Three claims, and the first one is the one that could quietly break
|
||
|
|
# everything else: the wreck must be SCENERY. A dying ship kept alive to
|
||
|
|
# animate would still be an Area2D in the player_ships group with live
|
||
|
|
# weapon components - still shooting, still a target, still colliding - so
|
||
|
|
# the test checks that the fleet, the group and the node are all rid of it
|
||
|
|
# on the same frame they always were.
|
||
|
|
#
|
||
|
|
# Covers:
|
||
|
|
# A. the ship itself dies immediately: out of the fleet, out of the group,
|
||
|
|
# freed - exactly as before
|
||
|
|
# B. a wreck is left behind, and it travels AWAY from the fleet
|
||
|
|
# C. it explodes where it ended up, not where it was hit
|
||
|
|
# D. the explosion is a real one - and no quieter than the one an enemy
|
||
|
|
# gets, which is the comparison #130 is making
|
||
|
|
# E. #135 - the prologue hands over an interceptor even when the belt has
|
||
|
|
# taken every escort
|
||
|
|
|
||
|
|
var game_state: Node
|
||
|
|
var scene: Node
|
||
|
|
var fleet: Node2D
|
||
|
|
var stage: int = 0
|
||
|
|
var elapsed: float = 0.0
|
||
|
|
var wreck: Node2D
|
||
|
|
var wreck_start: Vector2 = Vector2.ZERO
|
||
|
|
var fleet_centre: Vector2 = Vector2.ZERO
|
||
|
|
var last_wreck_at: Vector2 = Vector2.ZERO
|
||
|
|
var ships_before: int = 0
|
||
|
|
|
||
|
|
|
||
|
|
func _initialize() -> void:
|
||
|
|
# GameState is an autoload: a bare GameState.<instance member> does not
|
||
|
|
# compile in a SceneTree script, so the node is fetched by name instead.
|
||
|
|
game_state = root.get_node("GameState")
|
||
|
|
game_state.reset_new_game()
|
||
|
|
game_state.intro_active = false
|
||
|
|
game_state.reset_quests()
|
||
|
|
|
||
|
|
|
||
|
|
func _fail(message: String) -> bool:
|
||
|
|
print("TEST_SHIP_DEATH: FAIL (%s)" % message)
|
||
|
|
quit(1)
|
||
|
|
return true
|
||
|
|
|
||
|
|
|
||
|
|
func _wrecks() -> Array:
|
||
|
|
var out: Array = []
|
||
|
|
for node in root.get_children():
|
||
|
|
if node is ShipWreckVFX:
|
||
|
|
out.append(node)
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
func _explosions() -> Array:
|
||
|
|
var out: Array = []
|
||
|
|
for node in root.get_children():
|
||
|
|
if node is ShipExplosionVFX:
|
||
|
|
out.append(node)
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
func _process(delta: float) -> bool:
|
||
|
|
elapsed += delta
|
||
|
|
match stage:
|
||
|
|
0:
|
||
|
|
scene = load("res://world/Main.tscn").instantiate()
|
||
|
|
root.add_child(scene)
|
||
|
|
current_scene = scene
|
||
|
|
fleet = scene.get_node("Fleet")
|
||
|
|
elapsed = 0.0
|
||
|
|
stage = 1
|
||
|
|
return false
|
||
|
|
1:
|
||
|
|
if elapsed < 0.4:
|
||
|
|
return false
|
||
|
|
scene.get_node("AsteroidField").set_process(false)
|
||
|
|
scene.get_node("EnemySpawner").set_process(false)
|
||
|
|
# A wedge, so there is a formation to break away FROM.
|
||
|
|
fleet.spawn_ships(3, Vector2.ZERO,
|
||
|
|
PackedStringArray([GameState.VESSEL_INTERCEPTOR,
|
||
|
|
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_INTERCEPTOR]))
|
||
|
|
elapsed = 0.0
|
||
|
|
stage = 2
|
||
|
|
return false
|
||
|
|
2:
|
||
|
|
if elapsed < 0.3:
|
||
|
|
return false
|
||
|
|
if fleet.ships.size() != 3:
|
||
|
|
return _fail("wanted a fleet of 3 to kill one out of, got %d" % fleet.ships.size())
|
||
|
|
fleet_centre = fleet.get_centroid()
|
||
|
|
var doomed: Node2D = fleet.ships[0]
|
||
|
|
wreck_start = doomed.position
|
||
|
|
var before: int = get_nodes_in_group("player_ships").size()
|
||
|
|
doomed.take_damage(1)
|
||
|
|
|
||
|
|
# --- A. the ship is gone, on this frame ------------------
|
||
|
|
if fleet.ships.size() != 2:
|
||
|
|
return _fail("a killed ship is still in the fleet (%d ships)" % fleet.ships.size())
|
||
|
|
if is_instance_valid(doomed) and not doomed.is_queued_for_deletion():
|
||
|
|
return _fail("a killed ship was not freed")
|
||
|
|
ships_before = before
|
||
|
|
print("the ship leaves the fleet and is freed on the frame it dies")
|
||
|
|
|
||
|
|
# --- B. and leaves a wreck behind ------------------------
|
||
|
|
var wrecks: Array = _wrecks()
|
||
|
|
if wrecks.size() != 1:
|
||
|
|
return _fail("%d wrecks left behind, expected 1" % wrecks.size())
|
||
|
|
wreck = wrecks[0]
|
||
|
|
if wreck.position.distance_to(wreck_start) > 1.0:
|
||
|
|
return _fail("the wreck did not start where the ship died")
|
||
|
|
last_wreck_at = wreck.position
|
||
|
|
elapsed = 0.0
|
||
|
|
stage = 3
|
||
|
|
return false
|
||
|
|
3:
|
||
|
|
# Halfway through the drift: still there, and further out.
|
||
|
|
if elapsed < ShipWreckVFX.DRIFT_SECONDS * 0.5:
|
||
|
|
return false
|
||
|
|
if not is_instance_valid(wreck):
|
||
|
|
return _fail("the wreck was gone before it had finished drifting")
|
||
|
|
# A frame has passed, so the deletion queue has been flushed: the
|
||
|
|
# dead ship is out of the group the enemies pick targets from.
|
||
|
|
if get_nodes_in_group("player_ships").size() != ships_before - 1:
|
||
|
|
return _fail("a killed ship is still in the player_ships group a frame later")
|
||
|
|
var travelled: float = wreck.position.distance_to(wreck_start)
|
||
|
|
if travelled < 40.0:
|
||
|
|
return _fail("the wreck drifted %.0fpx - it is meant to break away" % travelled)
|
||
|
|
# Away from the fleet, not through it.
|
||
|
|
if wreck.position.distance_to(fleet_centre) <= wreck_start.distance_to(fleet_centre):
|
||
|
|
return _fail("the wreck drifted towards the fleet, not away from it")
|
||
|
|
if wreck.rotation == 0.0:
|
||
|
|
return _fail("the wreck is not tumbling")
|
||
|
|
last_wreck_at = wreck.position
|
||
|
|
print("wreck drifted %.0fpx clear of the fleet, tumbling" % travelled)
|
||
|
|
elapsed = 0.0
|
||
|
|
stage = 4
|
||
|
|
return false
|
||
|
|
4:
|
||
|
|
if elapsed < ShipWreckVFX.DRIFT_SECONDS:
|
||
|
|
return false
|
||
|
|
# --- C. it went up where it ended up ---------------------
|
||
|
|
if is_instance_valid(wreck) and not wreck.is_queued_for_deletion():
|
||
|
|
return _fail("the wreck never exploded")
|
||
|
|
var blasts: Array = _explosions()
|
||
|
|
if blasts.is_empty():
|
||
|
|
return _fail("the wreck vanished without an explosion")
|
||
|
|
var at: Vector2 = blasts[0].position
|
||
|
|
if at.distance_to(wreck_start) < 40.0:
|
||
|
|
return _fail("the explosion happened at the kill (%.0fpx away), not out where the wreck drifted to" \
|
||
|
|
% at.distance_to(wreck_start))
|
||
|
|
print("it exploded %.0fpx from where the ship was hit" % at.distance_to(wreck_start))
|
||
|
|
|
||
|
|
# --- D. and it is a real explosion -----------------------
|
||
|
|
var ours: ShipExplosionVFX = ShipExplosionVFX.new()
|
||
|
|
var theirs: EnemyExplosionVFX = EnemyExplosionVFX.new()
|
||
|
|
if ours.shockwave_radius <= 0.0:
|
||
|
|
return _fail("a lost vessel still goes up without a shockwave")
|
||
|
|
if ours.particle_count < theirs.particle_count:
|
||
|
|
return _fail("losing a vessel (%d particles) is quieter than killing an enemy (%d)" \
|
||
|
|
% [ours.particle_count, theirs.particle_count])
|
||
|
|
if ours.lifetime < theirs.lifetime:
|
||
|
|
return _fail("a lost vessel's explosion is shorter than an enemy's")
|
||
|
|
print("player explosion: %d particles over %.2fs, shockwave %.0f (enemy: %d over %.2fs)" \
|
||
|
|
% [ours.particle_count, ours.lifetime, ours.shockwave_radius,
|
||
|
|
theirs.particle_count, theirs.lifetime])
|
||
|
|
ours.free()
|
||
|
|
theirs.free()
|
||
|
|
|
||
|
|
# --- E. #135: the prologue's survivor --------------------
|
||
|
|
scene.free()
|
||
|
|
game_state.reset_new_game()
|
||
|
|
scene = load("res://world/Main.tscn").instantiate()
|
||
|
|
root.add_child(scene)
|
||
|
|
current_scene = scene
|
||
|
|
fleet = scene.get_node("Fleet")
|
||
|
|
elapsed = 0.0
|
||
|
|
stage = 5
|
||
|
|
return false
|
||
|
|
5:
|
||
|
|
if elapsed < 0.4:
|
||
|
|
return false
|
||
|
|
var director: Node2D = scene.get_node("IntroDirector")
|
||
|
|
var narration: Control = scene.get_node("UI/NarrationBanner")
|
||
|
|
narration.clear()
|
||
|
|
# The belt takes every escort on the way through - the corners of
|
||
|
|
# the convoy are the exposed part of it. This is the case #135
|
||
|
|
# reported: what is left is all cargo.
|
||
|
|
for i in range(fleet.ships.size() - 1, -1, -1):
|
||
|
|
var ship: Node2D = fleet.ships[i]
|
||
|
|
if String(ship.vessel_type) == GameState.VESSEL_INTERCEPTOR:
|
||
|
|
fleet.ships.remove_at(i)
|
||
|
|
fleet.formation_offsets.remove_at(i)
|
||
|
|
ship.free()
|
||
|
|
if fleet.ships.is_empty():
|
||
|
|
return _fail("the test emptied the convoy instead of stripping its escorts")
|
||
|
|
for ship in fleet.ships:
|
||
|
|
if String(ship.vessel_type) == GameState.VESSEL_INTERCEPTOR:
|
||
|
|
return _fail("an escort survived the test's own belt")
|
||
|
|
print("convoy stripped to %d holds and no escorts" % fleet.ships.size())
|
||
|
|
|
||
|
|
director._begin(director.Beat.VORTEX)
|
||
|
|
if not fleet.has_vessel_type(GameState.VESSEL_INTERCEPTOR):
|
||
|
|
return _fail("the vortex began with no basic vessel to leave behind")
|
||
|
|
if String(director._survivor().vessel_type) != GameState.VESSEL_INTERCEPTOR:
|
||
|
|
return _fail("the survivor is a %s" % director._survivor().vessel_type)
|
||
|
|
director.finish()
|
||
|
|
if fleet.ships.size() != GameState.STARTING_SHIP_COUNT:
|
||
|
|
return _fail("the prologue handed over %d vessels" % fleet.ships.size())
|
||
|
|
if String(fleet.ships[0].vessel_type) != GameState.VESSEL_INTERCEPTOR:
|
||
|
|
return _fail("the prologue handed over a %s, not the basic vessel" \
|
||
|
|
% fleet.ships[0].vessel_type)
|
||
|
|
print("with every escort lost, the prologue still hands over an interceptor")
|
||
|
|
|
||
|
|
print("TEST_SHIP_DEATH: PASS")
|
||
|
|
quit(0)
|
||
|
|
return true
|
||
|
|
return false
|