spaceship/tests/test_ship_lab.gd

224 lines
8.8 KiB
GDScript3
Raw Normal View History

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()
2026-08-12 22:47:46 +02:00
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
#84: move 215 root files into a folder hierarchy Everything lived in one flat directory, so a diff gave no clue what area it touched and finding the file you wanted meant knowing its exact name. Twelve folders now, grouped by what a thing IS rather than what type it is - a vessel's definition, its component scenes and its upgrade resources sit together, because that is the set you read when you change one of them. buildings core controllers enemies logistics planets resources shaders surface ui vessels vfx world 332 files moved and every res:// reference rewritten - 306 files touched, including the autoload and main-scene paths in project.godot. Nothing renamed: this is purely where files sit, so the diff is a pile of pure moves and no behaviour is buried in it. ui_theme.tres, icon.svg and web_shell.html stay at the root, where Godot and the export preset expect them. Verified three ways, because a suite alone would not have caught all of it: the full 117 pass, the web export builds clean, and the game renders. The export is why that matters. The FIRST export after the move failed with "Attempt to open script 'res://GameState.gd'" - the old path - while project.godot on disk plainly said res://core/GameState.gd. That is this project's documented Android FUSE mtime trap: Godot served a cached copy of a file whose content had changed. A `touch` fixed it. Nothing in the test suite would have found that, because the suite runs from a copy on a native filesystem - the one place the bug cannot happen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 19:44:14 +02:00
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
2026-08-13 16:45:08 +02:00
# 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")
2026-08-13 20:56:24 +02:00
var lab_card_2: Control = controller.get_node("../UI/LabMenu/Upgrade2")
2026-08-13 16:45:08 +02:00
var lab_card_3: Control = controller.get_node("../UI/LabMenu/Upgrade3")
var lab_detail: Label = controller.get_node("../UI/LabMenu/DetailLabel")
var lab_buy: IconButton = controller.get_node("../UI/LabMenu/BuyButton")
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)
if not "Unlock Shield Vessel" in lab_detail.text or not lab_buy.enabled:
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:
2026-08-13 16:45:08 +02:00
print("TEST_SHIP_LAB: FAIL (interceptor upgrade cards were not shown)")
quit(1)
return true
controller.call("_on_lab_upgrade", 2)
2026-08-13 20:56:24 +02:00
if not "Targeting Array" in lab_detail.text or not lab_card_2.highlighted or lab_card_0.highlighted:
2026-08-13 16:45:08 +02:00
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:
2026-08-12 22:47:46 +02:00
if elapsed < game_state.build_time(GameState.VESSEL_SHIELD) * 3.0:
return false
2026-08-12 22:47:46 +02:00
# 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:
2026-08-12 22:47:46 +02:00
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