extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_resource_transfer.gd # Forgejo #24: moving ore between the fleet and a building is its own # top-level action, not something buried inside the ship crafter or the # research screen. # # Verifies: # 1. the transfer button appears alongside the action button whenever the # fleet is standing on a building, and opens the transfer panel bound to # THAT building - the `== 0` guard failure that made every crafter # button look dead is the same trap here # 2. ore moves both ways at an ordinary building, capped by what is # actually at each end (no overdraft in either direction) # 3. at a mothership the building's holdings ARE the bank, so unloading # there banks the ore and loading there draws on it # 4. a REAL tap through the input path moves ore, not just the handler # 5. pressing against an empty end reports why instead of doing nothing # 6. the shared resource bar reports the building the panel was opened from # 7. ore is conserved across the whole exercise var game_state: Node var scene: Node var fleet: Node var controller: Node var crafter_id: int = 0 var mothership_id: int = 0 var stage: int = 0 var elapsed: float = 0.0 var total_before: int = 0 var failures: PackedStringArray = PackedStringArray() 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: if b.type != GameState.BUILDING_MOTHERSHIP: 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.transporters.clear() game_state.rocks = 100 game_state.fleet_cargo = 0 game_state.stored_vessels.clear() game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"] scene = load("res://Main.tscn").instantiate() root.add_child(scene) fleet = scene.get_node("Fleet") 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 _tap(button: Control) -> void: for pressed in [true, false]: var ev: InputEventScreenTouch = InputEventScreenTouch.new() ev.pressed = pressed ev.position = button.size / 2.0 button._gui_input(ev) func _open_transfer() -> void: # Contextual choices collapse into one button until it is opened. _tap(controller.action_button) _tap(controller.transfer_button) func _expect(condition: bool, message: String) -> void: if not condition: failures.append("stage %d: %s" % [stage, message]) 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) var field: Node = 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) total_before = _total() _park_fleet_on(CRAFTER_AT) elapsed = 0.0 stage = 1 return false 1: # --- 1. the button appears, and binds to this building ------ if elapsed < 1.2: return false _expect(controller.current_building != null, "parking on the crafter never registered") _open_transfer() _expect(controller.transfer_menu.visible, "tapping the transfer button did not open the panel") _expect(controller.transfer_building_id == crafter_id, "panel bound to building %d, crafter is %d" % [controller.transfer_building_id, crafter_id]) # --- 6. the shared bar follows the panel -------------------- for bar in controller.resource_bars: _expect(bar.building_id == crafter_id, "a resource bar is reporting building %d, not the crafter %d" % [bar.building_id, crafter_id]) # --- 5. an empty hold explains itself ----------------------- game_state.fleet_cargo = 0 controller._on_transfer(true, GameState.CARGO_LOAD_BATCH) _expect("carrying nothing" in controller.transfer_status.text, "unloading an empty hold said: %s" % controller.transfer_status.text) # --- 2. both directions at an ordinary building ------------- # Baseline taken AFTER the last hand-set figure: the conservation # check at the end is about the transfers, and ore conjured into # the hold to set the scene would otherwise read as a leak. game_state.fleet_cargo = 40 total_before = _total() controller._on_transfer(true, GameState.CARGO_LOAD_BATCH) _expect(game_state.building_stock(crafter_id) == GameState.CARGO_LOAD_BATCH, "a batch into the crafter left stock at %d" % game_state.building_stock(crafter_id)) _expect(game_state.fleet_cargo == 40 - GameState.CARGO_LOAD_BATCH, "the hold paid %d, not the batch" % (40 - game_state.fleet_cargo)) controller._on_transfer(false, 999) _expect(game_state.building_stock(crafter_id) == 0, "loading ALL left %d in the crafter" % game_state.building_stock(crafter_id)) _expect(game_state.fleet_cargo == 40, "the hold came back to %d, not 40" % game_state.fleet_cargo) # Asking for more than exists moves only what is there. var over: int = game_state.transfer_to_fleet(crafter_id, 999) _expect(over == 0, "overdrew %d from an empty crafter" % over) _park_fleet_on(MOTHERSHIP_AT) controller.transfer_menu.visible = false elapsed = 0.0 stage = 2 return false 2: # --- 3. the mothership's holdings are the bank -------------- if elapsed < 1.2: return false _open_transfer() _expect(controller.transfer_building_id == mothership_id, "panel bound to %d, mothership is %d" % [controller.transfer_building_id, mothership_id]) _expect(game_state.building_holdings(mothership_id) == game_state.rocks, "mothership holdings %d != bank %d" % [game_state.building_holdings(mothership_id), game_state.rocks]) var bank_before: int = game_state.rocks var hold_before: int = game_state.fleet_cargo controller._on_transfer(true, 999) _expect(game_state.fleet_cargo == 0, "unloading ALL at the mothership left %d in the hold" % game_state.fleet_cargo) _expect(game_state.rocks == bank_before + hold_before, "the bank went to %d, expected %d" % [game_state.rocks, bank_before + hold_before]) # --- 4. a REAL tap, through the input path ------------------ # The handler working while the BUTTON is inert is the exact bug # class this project keeps hitting, so one press goes the long way. var bank_at_tap: int = game_state.rocks _tap(scene.get_node("UI/TransferMenu/ResourceList/OreToFleetButton")) _expect(game_state.fleet_cargo == bank_at_tap, "a real tap did not load all banked ore") _expect(game_state.rocks == 0, "the bank did not pay for the tapped withdrawal") # --- 7. conservation ---------------------------------------- _expect(_total() == total_before, "ore created or destroyed: %d -> %d" % [total_before, _total()]) _finish() return true return false func _finish() -> void: if failures.is_empty(): print("TEST_RESOURCE_TRANSFER: PASS") quit(0) else: print("bank=%d hold=%d crafter=%d" % [ game_state.rocks, game_state.fleet_cargo, game_state.building_stock(crafter_id) ]) for f in failures: print("FAIL: %s" % f) print("TEST_RESOURCE_TRANSFER: FAIL") quit(1) func _clear_hazards() -> void: # Drifting rocks can destroy a parked ship and change the fleet size out # from under a logistics test. Same reason as test_manual_supply_route. 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()