spaceship/tests/test_carrier_vessel.gd

239 lines
10 KiB
GDScript3
Raw Normal View History

extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_carrier_vessel.gd
# Forgejo #57: an unarmed hauler that substantially raises how much the
# fleet can carry (#56).
#
# Covers:
# A. crewing a carrier raises GameState.fleet_cargo_capacity() by its stat
# B. a carrier never fires - no weapon component, no projectile ever
# originates from it even with an enemy parked in interceptor fire range
# C. it is unbuildable until researched
# D. it appears in the crafter, hangar and ship lab with correct name,
# cost, icon and colour, and is buildable once researched
var game_state: Node
var scene: Node
var fleet: Node
var controller: Node
var field: Node
var crafter_id: int = 0
var mothership_id: int = 0
var definition: Resource
var capacity_before: int = 0
var enemy: Node
var stage: int = 0
var elapsed: float = 0.0
const MOTHERSHIP_AT: Vector2 = Vector2(0.0, 0.0)
const CRAFTER_AT: Vector2 = Vector2(3000.0, 0.0)
# Same slot the crafter/hangar UI arrays give carrier: VesselDefinitions.ORDER
# appends carrier after transporter, and VesselDefinitions.fleet() filters
# transporter out, so carrier lands last among the 4 fleet-eligible vessels
# (interceptor, shield, gas_bomber, carrier) in both craft_vessel_* and the
# hangar_* arrays.
const FLEET_UI_INDEX: int = 3
# The ship lab shows every vessel including the transporter, so carrier is
# the 5th and last slot there.
const LAB_UI_INDEX: int = 4
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.reset_progression()
game_state.build_time_scale = 0.02
game_state.rocks = 500
scene = load("res://Main.tscn").instantiate()
root.add_child(scene)
fleet = scene.get_node("Fleet")
controller = scene.get_node("BuildController")
func _clear_hazards() -> void:
for a in get_nodes_in_group("asteroids"):
if is_instance_valid(a):
a.queue_free()
for e in get_nodes_in_group("enemies"):
if is_instance_valid(e):
e.queue_free()
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
if elapsed < 0.2:
return false
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
_clear_hazards()
# --- structural: unarmed, cargo-capable, fleet-eligible ---------
definition = game_state.vessel_definition(GameState.VESSEL_CARRIER)
if definition == null or not definition.fleet_eligible:
return _fail("carrier is not a fleet-eligible vessel definition")
if definition.capabilities.has("weapon"):
return _fail("carrier declares a weapon capability")
if not definition.capabilities.has("cargo"):
return _fail("carrier does not declare the cargo capability")
# --- A. capacity math, directly -----------------------------------
if game_state.fleet_cargo_capacity(PackedStringArray(["carrier"])) != 120:
return _fail("one carrier's capacity contribution is %d, expected 120" \
% game_state.fleet_cargo_capacity(PackedStringArray(["carrier"])))
print("structural: unarmed, cargo capability, contributes 120 alone")
# --- C. gated until researched --------------------------------
field = scene.get_node("BuildingField")
field.place(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
mothership_id = int(game_state.buildings[-1].id)
field.place(GameState.BUILDING_SHIP_CRAFTER, CRAFTER_AT)
crafter_id = int(game_state.buildings[-1].id)
field.place(GameState.BUILDING_SHIP_LAB, CRAFTER_AT + Vector2(0.0, 400.0))
if game_state.vessel_is_unlocked(GameState.VESSEL_CARRIER):
return _fail("carrier starts unlocked")
if game_state.queue_order(crafter_id, GameState.VESSEL_CARRIER):
return _fail("queue_order accepted an unresearched carrier")
print("unresearched carrier is unbuildable, as expected")
# --- D. locked state in the crafter UI -------------------------
controller.craft_building_id = crafter_id
controller._refresh_craft_status()
if controller.craft_vessel_buttons[FLEET_UI_INDEX].enabled:
return _fail("a locked carrier shows enabled in the crafter menu")
stage = 1
elapsed = 0.0
return false
1:
if elapsed < 0.1:
return false
# --- C continued: unlocking ------------------------------------
if not game_state.unlock_vessel(GameState.VESSEL_CARRIER):
return _fail("unlock_vessel(carrier) failed with 500 ore in the bank")
if not game_state.vessel_is_unlocked(GameState.VESSEL_CARRIER):
return _fail("carrier still reports locked after unlocking")
if game_state.product_costs(GameState.VESSEL_CARRIER) != {GameState.RESOURCE_ORE: 24}:
return _fail("carrier's build cost is %s, expected 24 ore" \
% game_state.product_costs(GameState.VESSEL_CARRIER))
print("researched: unlocked, costs %s" % game_state.product_costs(GameState.VESSEL_CARRIER))
# --- D. unlocked state in the crafter UI ------------------------
controller._refresh_craft_status()
var craft_button: IconButton = controller.craft_vessel_buttons[FLEET_UI_INDEX]
var craft_label: Label = controller.craft_vessel_labels[FLEET_UI_INDEX]
if not craft_button.enabled:
return _fail("an unlocked carrier still shows disabled in the crafter menu")
if craft_button.icon != definition.icon or craft_button.glow_color != definition.glow_color:
return _fail("crafter carrier button does not match its definition's icon/colour")
if not craft_label.text.begins_with(definition.display_name):
return _fail("crafter carrier label reads '%s', expected to start with '%s'" \
% [craft_label.text, definition.display_name])
if not game_state.queue_order(crafter_id, GameState.VESSEL_CARRIER):
return _fail("queue_order rejected a researched carrier")
print("crafter: enabled, correct icon/colour, correct label, buildable")
# Bypass the build timer/delivery wait - that queue mechanism is
# generic and already covered for every other vessel
# (test_building_stock_queue.gd, test_resources_and_buildings.gd
# etc.); this ticket is about the carrier's OWN behaviour, not
# re-proving crafting works.
game_state.stored_vessels.append(GameState.VESSEL_CARRIER)
# --- D. hangar display -------------------------------------------
controller._refresh_hangar()
var available_slot: IconButton = controller.hangar_available_slots[FLEET_UI_INDEX]
var available_label: Label = controller.hangar_available_labels[FLEET_UI_INDEX]
if available_slot.icon != definition.icon or available_slot.glow_color != definition.glow_color:
return _fail("hangar carrier slot does not match its definition's icon/colour")
var expected_available: String = "%s\n1 available" % definition.display_name
if available_label.text != expected_available:
return _fail("hangar carrier label reads '%s', expected '%s'" \
% [available_label.text, expected_available])
print("hangar: correct icon/colour, shows 1 available")
# --- A continued: crewing it raises capacity ---------------------
# The default fleet (5 interceptors) already sits at
# max_fleet_size() with reset_progression()'s cleared skills, which
# would make _on_hangar_crew_type refuse for being full - shrink
# to 1 ship first so there is room to crew the carrier at all.
fleet.spawn_ships(1, MOTHERSHIP_AT, PackedStringArray([GameState.VESSEL_INTERCEPTOR]))
capacity_before = game_state.fleet_cargo_total_capacity()
controller._on_hangar_crew_type(FLEET_UI_INDEX)
if not fleet.has_vessel_type(GameState.VESSEL_CARRIER):
return _fail("crewing from the hangar did not add a carrier to the fleet")
var capacity_after: int = game_state.fleet_cargo_total_capacity()
if capacity_after != capacity_before + 120:
return _fail("crewing a carrier moved capacity %d -> %d, expected +120" \
% [capacity_before, capacity_after])
print("crewed: capacity %d -> %d (+120)" % [capacity_before, capacity_after])
# --- D. ship lab display -------------------------------------------
controller.lab_vessel = GameState.VESSEL_CARRIER
controller._refresh_lab()
var lab_button: IconButton = controller.lab_vessel_buttons[LAB_UI_INDEX]
var lab_label: Label = controller.lab_vessel_labels[LAB_UI_INDEX]
if lab_button.icon != definition.icon or lab_button.glow_color != definition.glow_color:
return _fail("ship lab carrier slot does not match its definition's icon/colour")
if lab_label.text != definition.display_name:
return _fail("ship lab carrier label reads '%s', expected '%s'" \
% [lab_label.text, definition.display_name])
print("ship lab: correct icon/colour/label")
stage = 2
elapsed = 0.0
return false
2:
# --- B. never fires, even with a target in range -----------------
# Isolate: JUST a carrier in the fleet, nothing else nearby that
# could fire instead and make the check meaningless.
fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_CARRIER]))
stage = 3
elapsed = 0.0
return false
3:
# A settling tick before the enemy appears: queue_free() on the
# ships spawn_ships() just replaced does not remove them from the
# tree instantly, so a stale interceptor's weapon could otherwise
# still get one last physics tick against a target added in the
# same frame - a false positive, not the carrier firing.
if elapsed < 0.3:
return false
var enemy_scene: PackedScene = load("res://Enemy.tscn")
enemy = enemy_scene.instantiate()
enemy.position = fleet.get_centroid() + Vector2(220.0, 0.0)
scene.add_child(enemy)
# Frozen in place: a chaser closes distance and kills on contact,
# and an unarmed carrier has nothing to stop that. Letting it
# approach would wipe the fleet, trip the "rebuild at the
# mothership" safety net (a default INTERCEPTOR), and that
# interceptor firing would look like a carrier-firing false
# positive - this test is about whether the carrier fires, not
# whether it can survive unescorted.
enemy.set_physics_process(false)
stage = 4
elapsed = 0.0
return false
4:
if elapsed < 2.0:
return false
var projectiles: Array = get_nodes_in_group("player_projectiles")
if not projectiles.is_empty():
return _fail("%d projectile(s) originated from a fleet made only of a carrier" \
% projectiles.size())
print("no projectiles after %.1fs with an enemy in interceptor fire range" % elapsed)
print("TEST_CARRIER_VESSEL: PASS")
quit(0)
return true
return false
func _fail(message: String) -> bool:
print("TEST_CARRIER_VESSEL: FAIL (%s)" % message)
quit(1)
return true