spaceship/tests/test_space_time_distorder.gd

239 lines
9.7 KiB
GDScript3
Raw Normal View History

extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_space_time_distorder.gd
# Forgejo #59: a researchable vessel that lets the whole fleet travel faster
# than its default cruise speed, via a fader driving
# game_state.fleet_speed_multiplier.
#
# Covers:
# A. the fader is absent with no distorder ship, appears once one joins
# B. dragging it up measurably increases distance travelled per second;
# at 1.0 the fleet moves exactly as it does today
# C. formation holds together under boost - ships stay near their slot
# D. losing the vessel resets the multiplier and hides the fader
# E. the multiplier survives the space -> surface scene swap (it lives
# in the autoload)
var game_state: Node
var main: Node
var fleet: Node
var fader: Node
var stage: int = 0
var elapsed: float = 0.0
var baseline_distance: float = 0.0
var boosted_distance: float = 0.0
var start_centroid: Vector2 = Vector2.ZERO
var unboosted_lag: float = 0.0
const MEASURE_TIME: float = 1.0
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.transporters.clear()
game_state.stored_vessels.clear()
game_state.reset_progression()
game_state.vessel_levels["unlock_distorder"] = 1
# #62: sustaining a boost now drains fleet energy continuously - stock
# plenty so this test's own boosted phases aren't cut short by running
# out mid-measurement. Set BEFORE the first spawn_ships() call (inside
# Main.tscn's own _ready(), triggered by instantiate() below): the fleet
# hold has no capacity cap yet at this point, so this can't be clamped
# by the soon-to-exist fleet's cargo capability.
game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 500)
main = load("res://Main.tscn").instantiate()
root.add_child(main)
fleet = main.get_node("Fleet")
fader = main.get_node("UI/SpeedFader")
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)
# --- A. absent, then appears -------------------------------------
if fader.visible:
return _fail_now("fader appeared without a distorder ship")
fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray([
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_DISTORDER]))
fader._process(0.0)
if not fader.visible:
return _fail_now("a distorder ship in the fleet did not reveal the fader")
print("fader hidden with no distorder ship, visible once one joins")
# --- B. baseline @ 1.0x -------------------------------------------
if not is_equal_approx(game_state.fleet_speed_multiplier, 1.0):
return _fail_now("multiplier is not 1.0 by default: %f" % game_state.fleet_speed_multiplier)
start_centroid = fleet.get_centroid()
fleet.move_in_direction(Vector2(1.0, 0.0))
elapsed = 0.0
stage = 1
return false
1:
if elapsed < MEASURE_TIME:
return false
baseline_distance = fleet.get_centroid().distance_to(start_centroid)
# Reference for the formation check below: pure pursuit (a ship
# always aiming straight at its CURRENT, still-moving target
# rather than leading it) has its own inherent non-zero steady-
# state following distance even at 1.0x - it scales with the
# speeds involved, so "holds together under boost" has to mean
# staying proportional to this, not shrinking to some fixed
# absolute number regardless of how fast the fleet is going.
for i in range(fleet.ships.size()):
var ship: Node2D = fleet.ships[i]
if not is_instance_valid(ship):
continue
var expected: Vector2 = fleet.fleet_target + fleet.formation_offsets[i]
unboosted_lag = maxf(unboosted_lag, ship.position.distance_to(expected))
fleet.stop()
print("baseline (1.0x): travelled %.1f over %.1fs, formation lag %.1f" \
% [baseline_distance, MEASURE_TIME, unboosted_lag])
elapsed = 0.0
stage = 2
return false
2:
# Let the braking manoeuvre from stop() fully settle before
# measuring the boosted leg, so leftover momentum from the
# baseline pass can't pad the comparison either way.
if elapsed < 0.5:
return false
var press := InputEventScreenTouch.new()
press.index = 5
press.position = fader.global_position + Vector2(fader.size.x * 0.5, fader.size.y - 4.0)
press.pressed = true
main._handle_touch(press)
if not fader.is_active():
return _fail_now("pressing the fader did not begin dragging")
var drag := InputEventScreenDrag.new()
drag.index = 5
# All the way to the top of the track = MAX_FLEET_SPEED_MULTIPLIER.
drag.position = fader.global_position + Vector2(fader.size.x * 0.5, 0.0)
main._handle_drag(drag)
if not is_equal_approx(game_state.fleet_speed_multiplier, GameState.MAX_FLEET_SPEED_MULTIPLIER):
return _fail_now("dragging to the top did not reach the max multiplier: %f" \
% game_state.fleet_speed_multiplier)
print("fader dragged to %.1fx" % game_state.fleet_speed_multiplier)
start_centroid = fleet.get_centroid()
fleet.move_in_direction(Vector2(1.0, 0.0))
elapsed = 0.0
stage = 3
return false
3:
if elapsed < MEASURE_TIME:
return false
boosted_distance = fleet.get_centroid().distance_to(start_centroid)
print("boosted (%.1fx): travelled %.1f over %.1fs" \
% [game_state.fleet_speed_multiplier, boosted_distance, MEASURE_TIME])
if boosted_distance <= baseline_distance * 1.3:
return _fail_now("boosted travel (%.1f) was not measurably more than baseline (%.1f)" \
% [boosted_distance, baseline_distance])
elapsed = 0.0
stage = 4
return false
4:
# --- C. formation holds together under boost ----------------------
# Both the anchor's advance and the ships' own catch-up speed are
# scaled by the SAME multiplier, so the initial accelerate-from-
# rest transient covers a proportionally larger absolute distance
# under a 3x boost - a moment right after the boost begins isn't
# "trailing", it just hasn't finished the same (time-invariant)
# catch-up burst yet. Keep steering a while longer so that burst
# is over before judging whether formation holds.
if elapsed < 1.5:
return false
# Allow the lag to scale with the boost (it inherently does, even
# with both the anchor and ship speeds multiplied by the exact
# same factor - see the note by unboosted_lag above) but not
# beyond that, with slack for measurement noise.
var allowed_lag: float = maxf(unboosted_lag, 20.0) * game_state.fleet_speed_multiplier * 1.5
for i in range(fleet.ships.size()):
var ship: Node2D = fleet.ships[i]
if not is_instance_valid(ship):
continue
var expected: Vector2 = fleet.fleet_target + fleet.formation_offsets[i]
var strayed: float = ship.position.distance_to(expected)
if strayed > allowed_lag:
return _fail_now("a ship strayed %.1f from its formation slot under sustained boost (unboosted lag was %.1f, allowed %.1f)" \
% [strayed, unboosted_lag, allowed_lag])
print("formation held together under sustained boost (lag stayed proportional to the boost)")
fleet.stop()
var release := InputEventScreenTouch.new()
release.index = 5
release.position = fader.global_position + Vector2(fader.size.x * 0.5, 0.0)
release.pressed = false
main._handle_touch(release)
# --- E. survives the space -> surface scene swap -------------------
# Done here, with the distorder ship still crewed and the fleet
# still boosted: tearing this scene down while nothing else
# changes the fleet's own composition proves the multiplier
# lives in the autoload rather than being reset as a side effect
# of the old scene's own SpeedFader still ticking once more
# during teardown (it would, correctly, self-reset the instant
# it next saw a fleet with no distorder ship - not a real
# scenario here since this fleet keeps its distorder ship the
# whole way across the swap, via GameState.surviving_vessel_types
# exactly like every other vessel type).
game_state.surviving_ship_count = fleet.ships.size()
game_state.surviving_vessel_types = fleet.vessel_types()
game_state.planet_id = "test_space_time_distorder"
game_state.planet_name = "Distorder Test"
main.queue_free()
elapsed = 0.0
stage = 5
return false
5:
if elapsed < 0.3:
return false
main = load("res://PlanetSurface.tscn").instantiate()
root.add_child(main)
current_scene = main
fleet = main.get_node("Fleet")
fader = main.get_node("UI/SpeedFader")
elapsed = 0.0
stage = 6
return false
6:
if elapsed < 0.3:
return false
if not is_equal_approx(game_state.fleet_speed_multiplier, 3.0):
return _fail_now("multiplier did not survive the space -> surface swap: %f" \
% game_state.fleet_speed_multiplier)
fader._process(0.0)
if not fader.visible:
return _fail_now("the surface scene's fader did not show the surviving distorder ship")
print("multiplier and fader both survived the space -> surface swap")
# --- D. losing the vessel resets the multiplier -------------------
fleet.spawn_ships(1, fleet.get_centroid(), PackedStringArray([GameState.VESSEL_INTERCEPTOR]))
fader._process(0.0)
if fader.visible:
return _fail_now("fader stayed visible after losing the last distorder ship")
if not is_equal_approx(game_state.fleet_speed_multiplier, 1.0):
return _fail_now("multiplier was not reset after losing the distorder ship: %f" \
% game_state.fleet_speed_multiplier)
print("losing the distorder ship reset the multiplier and hid the fader")
print("TEST_SPACE_TIME_DISTORDER: PASS")
quit(0)
return true
return false
func _fail_now(message: String) -> bool:
print("TEST_SPACE_TIME_DISTORDER: FAIL (%s)" % message)
quit(1)
return true