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
202 lines
8.7 KiB
GDScript
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
|