spaceship/tests/test_outpost_crewing.gd
Loïc Wisniewski 8122e280b7 #104 (#97f): outposts are crewed automatically from the mothership
Vessels come from the HANGAR and never from the fleet. The fleet is what the
player is flying, and taking a vessel out of it to crew a building would be
the game undoing the player's own arrangement. "Whenever available" means
"when one is sitting at the mothership", and an outpost asking for a hull
nobody has built simply waits.

They travel, down the same vessel_deliveries path a finished crafter order
already uses to reach the mothership, rather than teleporting.

What is already in the air counts against the shortfall. Without that the
dispatcher sends the whole hangar to fill one berth, because the crew does
not grow until the first arrival lands.

Two directions, not one: lowering the order flies the surplus home rather
than stranding it, and a vessel whose outpost is levelled under it lands in
the hangar instead of being destroyed - the same rule a transporter
mid-delivery already follows with its cargo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:13:46 +02:00

160 lines
7.1 KiB
GDScript

extends SceneTree
# Headless test: run with
# godot --headless -s res://tests/test_outpost_crewing.gd
# Forgejo #104 (#97f): "When an outpost is configured to have a given amount
# of ship, the game should transfer, whenever available, ships from the
# mothership to the outpost to always fullfill the outpost conditions if
# possible."
#
# Vessels come from the HANGAR and never from the fleet: the fleet is what
# the player is flying, and taking a vessel out of it to crew a building
# would be the game undoing the player's own arrangement.
#
# Covers:
# A. an order with nothing in the hangar waits. It does not conjure a
# vessel, and it does not take one out of the fleet
# B. a matching vessel in the hangar is dispatched, travels, and joins the
# crew on arrival
# C. it dispatches exactly the shortfall - what is already in the air
# counts, or the whole hangar leaves to fill one berth
# D. lowering the order releases the surplus back to the hangar
# E. an outpost levelled while a vessel is inbound does not destroy it:
# the vessel lands in the hangar instead
var game_state: Node
var outpost_id: int = 0
var second_id: int = 0
var stage: int = 0
var elapsed: float = 0.0
var fleet_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.buildings.clear()
game_state.extractors.clear()
game_state.transporters.clear()
game_state.stored_vessels.clear()
game_state.vessel_deliveries.clear()
game_state.reset_progression()
game_state.reset_waves()
game_state.unlocked.append("forward_bases")
game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
outpost_id = game_state.add_building(GameState.BUILDING_OUTPOST, Vector2(1200.0, 0.0))
func _fail(message: String) -> bool:
print("TEST_OUTPOST_CREWING: FAIL (%s)" % message)
quit(1)
return true
# Runs the autoload's own clock without waiting out the real seconds: this
# test is about the dispatcher, not about how long a vessel takes to fly.
func _simulate(seconds: float) -> void:
var step: float = 1.0 / 60.0
var ticks: int = int(seconds / step)
for i in range(ticks):
game_state._advance_vessel_deliveries(step)
game_state._advance_garrison_dispatch()
func _process(_delta: float) -> bool:
# --- A. an order nobody can fill waits ------------------------------
game_state.set_garrison_desired(outpost_id, GameState.VESSEL_INTERCEPTOR, 2)
if game_state.garrison_desired_total(outpost_id) != 2:
return _fail("the order did not take")
_simulate(20.0)
if not game_state.garrison_crew(outpost_id).is_empty():
return _fail("an outpost crewed itself out of an empty hangar")
if not game_state.vessel_deliveries.is_empty():
return _fail("something was dispatched with nothing in the hangar")
if game_state.garrison_desired_total(outpost_id) != 2:
return _fail("the unfillable order was dropped instead of waiting")
print("an order of 2 with an empty hangar waits, and stays an order")
# --- B. a hull in the hangar is sent ---------------------------------
game_state.stored_vessels.append(GameState.VESSEL_INTERCEPTOR)
game_state._advance_garrison_dispatch()
if game_state.vessel_deliveries.size() != 1:
return _fail("a vessel in the hangar produced %d deliveries" \
% game_state.vessel_deliveries.size())
if not game_state.stored_vessels.is_empty():
return _fail("the vessel was dispatched and is still in the hangar too")
var delivery: Dictionary = game_state.vessel_deliveries[0]
if int(delivery.get("to_outpost", 0)) != outpost_id:
return _fail("the delivery is not bound for the outpost")
if float(delivery.progress) >= 1.0:
return _fail("the vessel teleported - it should have to fly there")
_simulate(30.0)
if game_state.garrison_crew_of(outpost_id, GameState.VESSEL_INTERCEPTOR) != 1:
return _fail("the vessel never arrived (crew=%s)" \
% str(game_state.garrison_crew(outpost_id)))
print("one hull in the hangar flies out and joins the crew")
# --- C. exactly the shortfall ---------------------------------------
for i in range(6):
game_state.stored_vessels.append(GameState.VESSEL_INTERCEPTOR)
game_state._advance_garrison_dispatch()
if game_state.vessel_deliveries.size() != 1:
return _fail("one empty berth dispatched %d vessels" \
% game_state.vessel_deliveries.size())
# A second call before the first lands must not send another.
game_state._advance_garrison_dispatch()
if game_state.vessel_deliveries.size() != 1:
return _fail("the dispatcher sent a second vessel for a berth already in the air")
if game_state.stored_vessels.size() != 5:
return _fail("the hangar holds %d, expected 5 left" % game_state.stored_vessels.size())
_simulate(30.0)
if game_state.garrison_crew(outpost_id).size() != 2:
return _fail("the outpost holds %d of its 2 berths" \
% game_state.garrison_crew(outpost_id).size())
game_state._advance_garrison_dispatch()
if not game_state.vessel_deliveries.is_empty():
return _fail("a full outpost is still being sent vessels")
print("2 berths, 6 spare hulls: it sent exactly 1 and then stopped")
# --- D. lowering the order releases the surplus ----------------------
var hangar_before: int = game_state.stored_vessels.size()
game_state.set_garrison_desired(outpost_id, GameState.VESSEL_INTERCEPTOR, 1)
game_state._advance_garrison_dispatch()
if game_state.garrison_crew(outpost_id).size() != 1:
return _fail("lowering the order to 1 left %d aboard" \
% game_state.garrison_crew(outpost_id).size())
_simulate(30.0)
if game_state.stored_vessels.size() != hangar_before + 1:
return _fail("the released vessel did not come home (hangar %d -> %d)" \
% [hangar_before, game_state.stored_vessels.size()])
print("lowering the order flies the surplus back to the hangar")
# --- E. a destination that stops existing ---------------------------
second_id = game_state.add_building(GameState.BUILDING_OUTPOST, Vector2(-1400.0, 0.0))
game_state.set_garrison_desired(second_id, GameState.VESSEL_INTERCEPTOR, 1)
game_state._advance_garrison_dispatch()
if game_state.garrison_inbound(second_id) != 1:
return _fail("nothing was dispatched to the second outpost")
hangar_before = game_state.stored_vessels.size()
if not game_state.destroy_building(second_id):
return _fail("the second outpost could not be destroyed")
_simulate(30.0)
if game_state.stored_vessels.size() != hangar_before + 1:
return _fail("a vessel inbound to a destroyed outpost was lost")
print("a vessel whose outpost was levelled under it lands back in the hangar")
# --- A (again). the fleet is never raided ---------------------------
game_state.stored_vessels.clear()
game_state.set_garrison_desired(outpost_id, GameState.VESSEL_INTERCEPTOR, 3)
fleet_before = game_state.surviving_ship_count
_simulate(20.0)
if game_state.surviving_ship_count != fleet_before:
return _fail("the dispatcher took a vessel out of the fleet")
if game_state.garrison_crew(outpost_id).size() != 1:
return _fail("the outpost gained a vessel from nowhere")
print("with an empty hangar it takes nothing from the fleet")
print("TEST_OUTPOST_CREWING: PASS")
quit(0)
return true