extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_manual_supply_route.gd # Forgejo #17 (part 4 of #12): the player can supply a building by hand - # withdraw banked ore into the fleet's hold at the mothership, fly it over, # and have the building take it. # # This is what keeps the game unblockable. #16 lets an order queue waiting # for ore, and until transporters exist (#19) there is otherwise no way to # fulfil one - a player with a queued order and no Transport Hub would be # stuck forever. # # Verifies: # 1. withdrawing moves ore bank -> hold, and conserves the total # 2. withdrawing is capped by what is actually banked (no overdraft) # 3. arriving at the mothership does NOT bank the hold. It used to, edge # triggered on arrival (#14); #24 removed that entirely, because an # automatic one-way move overrides a player who has just loaded the # hold in order to carry ore somewhere else # 4. a building takes the fleet's whole hold when opened # 5. end to end: a queued order the bank cannot reach is fulfilled using # only the fleet, with no Transport Hub in existence # 6. nothing is created or destroyed anywhere along the route var game_state: Node var scene: Node var fleet: Node var field: Node var controller: Node var crafter_id: int = 0 var mothership_id: int = 0 var stage: int = 0 var elapsed: float = 0.0 var ships_before: int = 0 var total_before: int = 0 const MOTHERSHIP_AT: Vector2 = Vector2(0.0, 0.0) const CRAFTER_AT: Vector2 = Vector2(3000.0, 0.0) func _total() -> int: var sum: int = game_state.rocks + game_state.fleet_cargo for b in game_state.buildings: sum += int(b.get("stock", 0)) return sum func _initialize() -> void: game_state = root.get_node("GameState") game_state.buildings.clear() game_state.extractors.clear() game_state.rocks = 40 game_state.fleet_cargo = 0 game_state.stored_vessels.clear() game_state.build_time_scale = 0.02 game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"] scene = load("res://Main.tscn").instantiate() root.add_child(scene) fleet = scene.get_node("Fleet") field = scene.get_node("BuildingField") controller = scene.get_node("BuildController") func _park_fleet_on(at: Vector2) -> void: for ship in fleet.ships: ship.position = at fleet.move_to(at) func _process(delta: float) -> bool: elapsed += delta match stage: 0: scene.get_node("AsteroidField").set_process(false) scene.get_node("EnemySpawner").set_process(false) _clear_hazards() fleet.spawn_ships(2, MOTHERSHIP_AT) 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) total_before = _total() _park_fleet_on(CRAFTER_AT) elapsed = 0.0 stage = 1 return false 1: if elapsed < 0.3: return false # --- 5. an order the crafter cannot pay for ----------------- ships_before = fleet.ships.size() game_state.queue_order(crafter_id) elapsed = 0.0 stage = 2 return false 2: if elapsed < 0.3: return false if game_state.pending_orders(crafter_id) != 1: print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (order did not queue)") quit(1) return true # --- 1 + 2. withdraw at the mothership ---------------------- _park_fleet_on(MOTHERSHIP_AT) elapsed = 0.0 stage = 3 return false 3: if elapsed < 0.3: return false var bank_before: int = game_state.rocks var took: int = game_state.transfer_to_fleet(mothership_id, GameState.SHIP_COST) if took != GameState.SHIP_COST or game_state.fleet_cargo != GameState.SHIP_COST: print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (withdrew %d into a hold of %d)" % [took, game_state.fleet_cargo]) quit(1) return true if game_state.rocks != bank_before - GameState.SHIP_COST: print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (bank did not pay for the withdrawal)") quit(1) return true print("withdrew %d at the mothership -> bank=%d hold=%d" % [ took, game_state.rocks, game_state.fleet_cargo ]) elapsed = 0.0 stage = 4 return false 4: # --- 3. parked on the mothership, and arriving again -------- if elapsed < 0.5: return false if game_state.fleet_cargo != GameState.SHIP_COST: print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (ore withdrawn while parked on the mothership was banked again by itself)") quit(1) return true # Leaving and coming back is the case the old edge-triggered # delivery fired on, so it is the one worth re-entering. _park_fleet_on(CRAFTER_AT) _park_fleet_on(MOTHERSHIP_AT) if game_state.fleet_cargo != GameState.SHIP_COST: print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (arriving at the mothership auto-banked the hold - #24 removed that)") quit(1) return true print("hold kept its %d across an arrival (no auto-banking)" % game_state.fleet_cargo) # --- 4. carry it to the crafter and open it ----------------- _park_fleet_on(CRAFTER_AT) elapsed = 0.0 stage = 5 return false 5: if elapsed < 0.6: return false var moved: int = game_state.transfer_to_building(crafter_id, game_state.fleet_cargo) if moved != GameState.SHIP_COST or game_state.fleet_cargo != 0: print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (unloaded %d, hold left at %d)" % [moved, game_state.fleet_cargo]) quit(1) return true print("unloaded %d into the crafter" % moved) elapsed = 0.0 stage = 6 return false 6: # Long enough for both construction and the physical delivery trip # from this remote crafter to the mothership hangar. var delivery_time: float = CRAFTER_AT.distance_to(MOTHERSHIP_AT) \ / GameState.VESSEL_DELIVERY_SPEED if elapsed < game_state.build_time(GameState.PRODUCT_SHIP) * 3.0 + delivery_time + 0.5: return false # --- 5. the queued order completed off the fleet's own ore --- if game_state.pending_orders(crafter_id) != 0: print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (order still queued after a hand delivery)") quit(1) return true # Since #23 a finished vessel waits in the hangar rather than # crewing itself, so this is where it lands. if game_state.stored_vessels.size() != 1: print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (no vessel built: %d in the hangar)" % game_state.stored_vessels.size()) quit(1) return true print("queued order fulfilled by hand, no transport hub: %d vessel in the hangar" % game_state.stored_vessels.size()) # --- 6. conservation ---------------------------------------- var expected: int = total_before - GameState.SHIP_COST if _total() != expected: print("bank=%d hold=%d stocks total=%d, expected %d" % [ game_state.rocks, game_state.fleet_cargo, _total(), expected ]) print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (ore created or destroyed along the route)") quit(1) return true print("ore conserved: %d -> %d, exactly the %d the vessel cost" % [ total_before, _total(), GameState.SHIP_COST ]) # --- 2. no overdraft ---------------------------------------- game_state.rocks = 3 var over: int = game_state.transfer_to_fleet(mothership_id, 999) if over != 3 or game_state.rocks != 0: print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (overdrew: took %d from a bank of 3, leaving %d)" % [over, game_state.rocks]) quit(1) return true print("withdrawing 999 from a bank of 3 took %d" % over) print("TEST_MANUAL_SUPPLY_ROUTE: PASS") quit(0) return true return false func _clear_hazards() -> void: # set_process(false) on AsteroidField only stops NEW chunks spawning - # rocks already in the world keep drifting and can destroy a parked ship, # which silently changes the fleet size this test asserts on. This is a # logistics test, not a survival one. 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()