#162 - a single tap bought a research node outright. One physical tap arrives TWICE: Godot emulates a mouse button from every touch AND a touch from every mouse button, both project settings are on, and the menu's dispatcher routes InputEventScreenTouch and InputEventMouseButton alike to the tree. The second arrival found the node already selected and took itself for the confirming tap. Measured, not reasoned about - one tap produced two picks and two confirms. What is ignored is specifically the TWIN: same frame, same place, arriving through the other input kind. Deduping on frame and position alone also swallowed two genuine taps in one frame, which the test caught. #164 - the build menu predicted a caption's height from the COLUMN WIDTH: wide column, assume one line. Right for most names and wrong for the long ones, so "Spacial Workshop" wrapped onto the price beneath it. Captions are now measured against the font that draws them, and prices sized from PriceTag.wanted_height(), which existed for exactly this and was not being asked. Prices are set before anything is measured, because the old order laid out first and priced afterwards. The ship lab: its buy button is gone and a second tap on an option buys it, the same rule as the research tree; the explanation drops into the room that freed; research points move up into the resource table, where everything else being spent already is; the second row of options is placed from the first row's measured bottom rather than a constant; and the vessel strip is spaced from the panel's own width, so the eighth vessel is no longer half off the right edge. #160 - the quest panel was drawn BEFORE the sticks and the pivot pair, so they sat on top of it. Moved after them, under the narration banner. Its title rule ran straight through the close button; the header band is now as tall as that button. #161 - the prologue widens the formation into a freight convoy by writing over GameState.formation, and nothing put it back. The player flew the convoy's 600-unit spread for the rest of the game - a shape nobody chose, which reads as the fleet having drifted apart. #163 - a quest to station a vessel at the outpost, after the one that asks for the outpost. Building one and crewing it are different acts, and an empty outpost holds nothing. #165 - the gas bomb's blast and a vessel arriving fast both damage a structure. The blast used to stop politely at anything the player owned, which made a bomb dropped on a besieged mothership a free way to sweep the attackers off it. Collisions are caught on ENTRY, not on the 0.7s strike tick: a vessel is only moving fast for the instant it arrives, and a ship sent in at 900 did no damage at all until this moved. Docking is free - the fleet sits on its own mothership constantly to bank ore - and garrison vessels are exempt outright, because they launch from their outpost and re-dock with it at speed. The suite caught that one: the ram rule was destroying the garrison as it launched. Suite 153/153. Four tests changed because what they pinned moved: the lab's buy button, the intro's formation, the research tree's tap delivery, and the build menu's own preview. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
227 lines
9 KiB
GDScript
227 lines
9 KiB
GDScript
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 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
|
|
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", "wing", "squadron"]
|
|
game_state.research_points = 10
|
|
field.place(GameState.BUILDING_SHIP_LAB, Vector2(-700.0, 0.0))
|
|
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")
|
|
|
|
# --- 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
|