extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_ship_lab.gd # Forgejo #5: the ship lab researches upgrades per vessel type and unlocks # new vessel types. The shield vessel is the first of those. # # Verifies: # 1. the lab is gated behind its own research node, like every structure # 2. upgrades cannot be bought without a lab built - it is a place you go, # not a menu that exists once researched # 3. buying raises the level, charges the escalating price, and stops at # max # 4. an upgrade improves the ships ALREADY FLYING, not just the next one # built # 5. the shield vessel is unlocked at the lab, and only then can the # crafter build one # 6. a shield vessel is what comes back from production - a typed order # must not decay into an interceptor # 7. upgrades are per vessel type: an interceptor upgrade does not tune # the transporter var game_state: Node var scene: Node var fleet: Node var field: Node var controller: Node var lab_id: int = 0 var crafter_id: int = 0 var stage: int = 0 var elapsed: float = 0.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.build_time_scale = 0.02 game_state.reset_progression() game_state.rocks = 1200 game_state.fleet_cargo = 0 # #219: fleet capacity is built now - one berth free, one per Fleet # Office. This test flies more than one vessel, so it needs the offices # standing before the fleet spawns into them. load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6) scene = load("res://world/Main.tscn").instantiate() root.add_child(scene) fleet = scene.get_node("Fleet") field = scene.get_node("BuildingField") controller = scene.get_node("BuildController") func _process(delta: float) -> bool: elapsed += delta match stage: 0: scene.get_node("AsteroidField").set_process(false) scene.get_node("EnemySpawner").set_process(false) for a in get_nodes_in_group("asteroids"): if is_instance_valid(a): a.queue_free() # --- 1. gated behind research ------------------------------- if game_state.is_building_unlocked(GameState.BUILDING_SHIP_LAB): print("TEST_SHIP_LAB: FAIL (lab buildable with no research)") quit(1) return true var gate: String = game_state.research_for_building(GameState.BUILDING_SHIP_LAB) if gate == "": print("TEST_SHIP_LAB: FAIL (no research node gates the lab)") quit(1) return true print("lab gated behind '%s'" % gate) # --- 2. no lab, no upgrades --------------------------------- if game_state.can_buy_upgrade("int_damage"): print("TEST_SHIP_LAB: FAIL (upgrades purchasable with no lab built)") quit(1) return true if game_state.buy_upgrade("int_damage"): print("TEST_SHIP_LAB: FAIL (bought an upgrade with no lab built)") quit(1) return true print("no lab -> no upgrades") game_state.unlocked = ["shipyards", "vessel_science", "coordination"] game_state.research_points = 10 field.place(GameState.BUILDING_SHIP_LAB, Vector2(-700.0, 0.0)) lab_id = int(game_state.buildings[-1].id) field.place(GameState.BUILDING_SHIP_CRAFTER, Vector2(700.0, 0.0)) crafter_id = int(game_state.buildings[-1].id) elapsed = 0.0 stage = 1 return false 1: if elapsed < 0.3: return false # The lab is a direct vessel -> upgrade flow: tapping a vessel # shows its own upgrade cards, and tapping a card changes the # purchase detail. A locked vessel must show its unlock alone. var lab_menu: Control = controller.get_node("../UI/LabMenu") var lab_card_0: Control = controller.get_node("../UI/LabMenu/Upgrade0") var lab_card_1: Control = controller.get_node("../UI/LabMenu/Upgrade1") var lab_card_2: Control = controller.get_node("../UI/LabMenu/Upgrade2") var lab_card_3: Control = controller.get_node("../UI/LabMenu/Upgrade3") var lab_detail: Label = controller.get_node("../UI/LabMenu/DetailLabel") lab_menu.visible = true controller.call("_on_lab_vessel", 1) if not lab_card_0.visible or lab_card_1.visible: print("TEST_SHIP_LAB: FAIL (locked shield page did not show only its unlock)") quit(1) return true controller.call("_on_lab_upgrade", 0) # lab_can_buy, not a button's `enabled`: #164 removed the separate # buy button, so the option itself is the control and a second tap # on it is the purchase. What is being checked is unchanged - # selecting the unlock card offers something actually buyable. if not "Unlock Shield Vessel" in lab_detail.text or not controller.lab_can_buy: print("TEST_SHIP_LAB: FAIL (unlock card did not select a purchasable detail)") quit(1) return true controller.call("_on_lab_vessel", 0) # #73: the interceptor's Thrusters upgrade was removed (global # fleet speed now belongs to the distorder fader, #59/#62) # leaving 3 upgrade cards, not 4 - card 3 is correctly hidden now. if not lab_card_0.visible or not lab_card_2.visible or lab_card_3.visible: print("TEST_SHIP_LAB: FAIL (interceptor upgrade cards were not shown)") quit(1) return true controller.call("_on_lab_upgrade", 2) if not "Targeting Array" in lab_detail.text or not lab_card_2.highlighted or lab_card_0.highlighted: print("TEST_SHIP_LAB: FAIL (tapping an upgrade did not update its detail)") quit(1) return true print("lab cards are vessel-specific and selectable") # #184: a queued research uses the bar as its changing value. The # status names the state but never repeats it as a percentage. var lab_progress: ProgressBar = controller.get_node( "../UI/LabMenu/ResearchProgress") var lab_status: Label = controller.get_node("../UI/LabMenu/StatusLabel") controller.lab_building_id = lab_id if not game_state.queue_lab_research(lab_id, "int_rate"): return _fail("could not queue research for the progress-bar check") controller.call("_refresh_lab") if not lab_progress.visible or lab_progress.show_percentage: return _fail("pending research did not show a percentage-free progress bar") if "%" in lab_status.text: return _fail("pending research still prints a percentage: '%s'" % lab_status.text) game_state.deposit_resource_to_building(lab_id, GameState.RESOURCE_ORE, game_state.upgrade_cost("int_rate")) game_state.call("_advance_lab_research", game_state.lab_research_time() * 0.25) controller.call("_refresh_lab") if lab_progress.value < 0.2 or lab_progress.value > 0.3 \ or lab_status.text != "Researching": return _fail("research bar did not track active work (value %.2f, status '%s')" \ % [lab_progress.value, lab_status.text]) print("pending lab research is shown by a live progress bar, without percentage text") # --- 3. buying raises the level and charges ------------------ var before_rocks: int = game_state.rocks var first_cost: int = game_state.upgrade_cost("int_damage") if not game_state.buy_upgrade("int_damage"): print("TEST_SHIP_LAB: FAIL (could not buy an upgrade with a lab built)") quit(1) return true if game_state.upgrade_level("int_damage") != 1: print("TEST_SHIP_LAB: FAIL (level did not rise)") quit(1) return true if game_state.rocks != before_rocks - first_cost: print("TEST_SHIP_LAB: FAIL (wrong price charged)") quit(1) return true var second_cost: int = game_state.upgrade_cost("int_damage") if second_cost <= first_cost: print("TEST_SHIP_LAB: FAIL (level 2 costs %d, no more than level 1 at %d)" % [second_cost, first_cost]) quit(1) return true print("bought level 1 for %d, level 2 now costs %d" % [first_cost, second_cost]) # --- 4. it reaches ships already flying ---------------------- var flying: Node = fleet.ships[0] if flying.projectile_damage != GameState.SHIP_BASE_DAMAGE + 1: print("TEST_SHIP_LAB: FAIL (a ship already flying still does %d damage)" % flying.projectile_damage) quit(1) return true print("ship already flying now does %d damage" % flying.projectile_damage) # --- 7. per vessel type -------------------------------------- var trn_speed_before: float = game_state.transporter_speed() game_state.buy_upgrade("int_range") if game_state.transporter_speed() != trn_speed_before: print("TEST_SHIP_LAB: FAIL (an interceptor upgrade changed the transporter)") quit(1) return true print("interceptor upgrades leave the transporter alone") # --- 5. the shield vessel is unlocked at the lab ------------- if game_state.shield_vessel_unlocked(): print("TEST_SHIP_LAB: FAIL (shield vessel available before it was unlocked)") quit(1) return true if game_state.queue_order(crafter_id, GameState.VESSEL_SHIELD): print("TEST_SHIP_LAB: FAIL (crafter took a shield order before the type was unlocked)") quit(1) return true if not game_state.unlock_shield_vessel(): print("TEST_SHIP_LAB: FAIL (could not unlock the shield vessel at the lab)") quit(1) return true print("shield vessel unlocked at the lab") # --- 6. a typed order comes back as that type ---------------- fleet.spawn_ships(1, Vector2.ZERO) if not game_state.queue_order(crafter_id, GameState.VESSEL_SHIELD): print("TEST_SHIP_LAB: FAIL (crafter refused a shield order after unlocking)") quit(1) return true game_state.deposit_to_building(crafter_id, GameState.SHIELD_COST) elapsed = 0.0 stage = 2 return false 2: if elapsed < game_state.build_time(GameState.VESSEL_SHIELD) * 3.0: return false # Finished vessels wait at the mothership (#23); crew it here so # the rest of the checks can look at a real ship. if game_state.stored_vessels.size() != 1: print("TEST_SHIP_LAB: FAIL (the shield order produced nothing: %s)" % [game_state.stored_vessels]) quit(1) return true if String(game_state.stored_vessels[0]) != GameState.VESSEL_SHIELD: print("TEST_SHIP_LAB: FAIL (a shield order came back as %s)" % game_state.stored_vessels[0]) quit(1) return true fleet.add_ship(game_state.take_stored_vessel(0)) var shield_ship: Node = null for ship in fleet.ships: if is_instance_valid(ship) and ship.vessel_type == GameState.VESSEL_SHIELD: shield_ship = ship if shield_ship == null: print("TEST_SHIP_LAB: FAIL (crewing the stored shield vessel produced none: %d ships)" % fleet.ships.size()) quit(1) return true if not shield_ship.has_shield(): print("TEST_SHIP_LAB: FAIL (the shield vessel arrived with no shield up)") quit(1) return true print("shield vessel built, shield up with %d charge(s)" % shield_ship.shield_charges) print("TEST_SHIP_LAB: PASS") quit(0) return true return false func _fail(message: String) -> bool: print("TEST_SHIP_LAB: FAIL (%s)" % message) quit(1) return true