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 whole tree, researched, must not add a single berth: #219 # moved fleet size out of research entirely. 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 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, and no research anywhere raises it") # --- 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