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 game_state.research_points = 1 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