spaceship/tests/test_fleet_growth.gd
Loïc Wisniewski cc71811d89 Research the Fleet Office, and give it one hull
Fleet Offices sit behind a new Berth Authority doctrine. It costs ore and no
research points, deliberately: points come only from conquering a planet,
conquering needs a fleet, and the fleet caps at its opening hull until an
office exists - so pricing the fleet's own gate in them would lock a player
behind a fight that one vessel has to win first. The research unlocks the
STRUCTURE; capacity is still the built, supplied thing #219 made it.

The tree gains a sixth lane for it, so the branch that fleet size vacated is
occupied again rather than reading as a hole.

Also fixes a shape #216 exposed: a stationed transporter drew a hull written
into its own script while a delivered one drew the definition's, and the
transporter's definition declared none at all - so one in flight was an
empty polygon. Both vessels now draw the definition, and a test pins that
the two paths agree. The repair drone had the same fault, off by two units.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-05 21:57:53 +02:00

202 lines
8.7 KiB
GDScript

extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_fleet_growth.gd
# A new game opens with a SINGLE vessel and a single berth, and the fleet
# grows by building Fleet Offices - one berth each, each of which has to be
# kept supplied.
#
# Forgejo #219 replaced the fleet-size doctrine chain with this. Researching
# a permanent +1 asked nothing of the player afterwards; a berth that has to
# be fed makes fleet size a standing cost against their supply lines rather
# than a purchase they make once.
#
# BASE_FLEET_SIZE and STARTING_SHIP_COUNT remain separate constants - the
# first is capacity, the second is how many hulls are actually flying - and
# this test still pins the distinction.
#
# Covers:
# A. a new game starts with one vessel and one berth, and no research
# anywhere in the tree can raise that
# B. each supplied office adds exactly one berth, and the cap bites
# C. an office that cannot pay loses its vessel, and says so
# D. a restocked office goes back to work without being rebuilt
# E. capacity can never outrun the formation's defined positions
var game_state: Node
var scene: Node
var fleet: Node
var stage: int = 0
var elapsed: float = 0.0
var office_ids: Array = []
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.reset_new_game()
# #108: reset_new_game() arms the prologue, and the prologue opens with a
# convoy rather than the game's own starting fleet. This test is about
# what the GAME starts from, which is where the prologue hands over -
# see test_intro_scene.gd for the prologue itself. The quest chain goes
# with it: its briefings are dialogs, and a dialog stops the tree (#128).
game_state.intro_active = false
game_state.reset_quests()
func _stock(office_id: int, amount: int) -> void:
var inventory: RefCounted = game_state.building_inventory(office_id)
for resource_type in game_state.resource_types():
inventory.set_amount(String(resource_type), amount)
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
# --- A. the opening position ---------------------------------
if GameState.STARTING_SHIP_COUNT != 1:
return _fail("a new game starts with %d vessels, expected 1" % GameState.STARTING_SHIP_COUNT)
if GameState.BASE_FLEET_SIZE != 1:
return _fail("a new game opens with %d berths, expected the one that needs no office" \
% GameState.BASE_FLEET_SIZE)
if game_state.max_fleet_size() != 1:
return _fail("an unbuilt fleet caps at %d, expected 1" % game_state.max_fleet_size())
# --- the gate on the office ----------------------------------
# Fleet Offices are researched (Berth Authority), but the research
# only unlocks the STRUCTURE - it grants no capacity by itself.
if game_state.is_building_unlocked(GameState.BUILDING_FLEET_OFFICE):
return _fail("Fleet Offices are buildable with nothing researched")
var gate: Dictionary = game_state.skill_node("berth_authority")
if gate.is_empty():
return _fail("no skill node unlocks the Fleet Office")
# Ore only, deliberately: research points come solely from
# conquering a planet, and conquering one needs a fleet. Pricing
# the fleet's own gate in them would lock the player at their
# opening hull behind a fight that hull has to win first.
if int(gate.get("research_points", 0)) != 0:
return _fail("Berth Authority costs %d research points - unreachable with one vessel" \
% int(gate.get("research_points", 0)))
for cost_resource in game_state.skill_costs("berth_authority"):
if String(cost_resource) != GameState.RESOURCE_ORE:
return _fail("Berth Authority asks for %s, which needs a built economy first" \
% cost_resource)
# The whole tree, researched, must still not add a single berth:
# #219 moved fleet SIZE out of research and onto the structure.
for node in GameState.SKILL_TREE:
if int(node.get("fleet_bonus", 0)) != 0:
return _fail("skill node '%s' still grants fleet capacity" % node.id)
game_state.unlocked.append(String(node.id))
if not game_state.is_building_unlocked(GameState.BUILDING_FLEET_OFFICE):
return _fail("researching Berth Authority did not unlock the Fleet Office")
if game_state.max_fleet_size() != 1:
return _fail("researching the entire tree raised the fleet cap to %d" \
% game_state.max_fleet_size())
print("a new game: 1 vessel, 1 berth; research unlocks the office, not the berth")
# --- B. offices are the berths -------------------------------
for i in range(3):
var id: int = game_state.add_building(GameState.BUILDING_FLEET_OFFICE,
Vector2(400.0 * float(i + 1), 0.0))
office_ids.append(id)
_stock(id, 500)
if game_state.max_fleet_size() != GameState.BASE_FLEET_SIZE + i + 1:
return _fail("%d offices gave %d berths, expected %d" % [
i + 1, game_state.max_fleet_size(),
GameState.BASE_FLEET_SIZE + i + 1])
print("3 supplied offices -> %d berths" % game_state.max_fleet_size())
# --- E. and never more than the formation can seat ------------
if game_state.max_fleet_size() > GameState.DEFAULT_FORMATION.size():
return _fail("capacity %d exceeds the %d positions the formation defines" \
% [game_state.max_fleet_size(), GameState.DEFAULT_FORMATION.size()])
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.3:
return false
if fleet.ships.size() != GameState.STARTING_SHIP_COUNT:
return _fail("a fresh scene spawned %d vessels, expected %d" \
% [fleet.ships.size(), GameState.STARTING_SHIP_COUNT])
# The berths the offices bought are fillable, and the cap bites
# exactly one past them.
for i in range(3):
if not fleet.add_ship(GameState.VESSEL_INTERCEPTOR):
return _fail("could not crew the berth bought by office %d" % (i + 1))
if fleet.ships.size() != 4:
return _fail("the fleet holds %d after filling its berths, expected 4" \
% fleet.ships.size())
if fleet.add_ship(GameState.VESSEL_INTERCEPTOR):
return _fail("a 5th vessel joined a fleet with 4 berths")
print("the fleet fills to %d and refuses a 5th" % fleet.ships.size())
# --- C. an office that cannot pay loses its vessel ------------
# Emptied and its clock wound past due, so the next tick bills it.
_stock(office_ids[2], 0)
var starved: Dictionary = game_state.find_building(int(office_ids[2]))
starved.upkeep_progress = GameState.FLEET_OFFICE_UPKEEP_SECONDS
game_state.take_quest_messages()
var before: int = fleet.ships.size()
game_state._advance_fleet_office_upkeep(0.1)
if not bool(starved.get("starved", false)):
return _fail("an office with an empty store kept paying")
if game_state.max_fleet_size() != 3:
return _fail("a starved office still counts: %d berths" % game_state.max_fleet_size())
if game_state.take_quest_messages().is_empty():
return _fail("the fleet lost a vessel to supply and nobody was told")
elapsed = 0.0
_lost_before = before
stage = 2
return false
2:
if elapsed < 0.3:
return false
if fleet.ships.size() != _lost_before - 1:
return _fail("a starved office left %d vessels flying, expected %d" \
% [fleet.ships.size(), _lost_before - 1])
print("a dry office destroyed its vessel and warned the player")
# --- D. the berth comes back, and a supplied office keeps it -
# The office is idle now that its vessel is gone, so the berth is
# offered again on the next tick - an office that could never be
# crewed again could never pay again either.
var revived: Dictionary = game_state.find_building(int(office_ids[2]))
game_state._advance_fleet_office_upkeep(0.1)
if bool(revived.get("starved", false)):
return _fail("an office with an empty berth stayed disabled")
if game_state.max_fleet_size() != 4:
return _fail("an emptied office did not offer its berth again")
# Restocked and re-crewed, it pays and the vessel stays.
_stock(office_ids[2], 500)
if not fleet.add_ship(GameState.VESSEL_INTERCEPTOR):
return _fail("could not re-crew the berth after restocking")
revived.upkeep_progress = GameState.FLEET_OFFICE_UPKEEP_SECONDS
game_state._advance_fleet_office_upkeep(0.1)
if bool(revived.get("starved", false)):
return _fail("a restocked office starved with a full store")
if fleet.ships.size() != 4:
return _fail("a supplied office lost its vessel anyway")
print("restocked and re-crewed: the office pays and the vessel stays")
print("TEST_FLEET_GROWTH: PASS")
quit(0)
return true
return false
var _lost_before: int = 0
func _fail(message: String) -> bool:
print("TEST_FLEET_GROWTH: FAIL (%s)" % message)
quit(1)
return true