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. 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