extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_fleet_cargo_limit.gd # Forgejo #56: the fleet hold has a finite, composition-driven capacity # instead of being unbounded. # # Covers: # A. capacity is data-driven: only vessels with the "cargo" capability # contribute their cargo_capacity stat - combat specialists (shield, # gas bomber) contribute zero # B. a lone interceptor cannot carry more than its capacity - a further # Rock pickup is refused and the rock is not consumed # C. losing a cargo-capable ship lowers the ceiling immediately without # deleting cargo already held (clamps on the next deposit instead) # D. a transfer into a full hold clamps to the remaining space and # reports the amount actually moved # E. capacity survives the space -> surface scene swap var game_state: Node var scene: Node var fleet: Node var stage: int = 0 var elapsed: float = 0.0 var mothership_id: int = 0 var rock: Node func _initialize() -> void: game_state = root.get_node("GameState") game_state.buildings.clear() game_state.extractors.clear() game_state.rocks = 500 game_state.fleet_cargo = 0 mothership_id = game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO) scene = load("res://world/Main.tscn").instantiate() root.add_child(scene) current_scene = scene func _process(delta: float) -> bool: elapsed += delta match stage: 0: if elapsed < 0.3: return false scene.get_node("AsteroidField").set_process(false) scene.get_node("EnemySpawner").set_process(false) fleet = scene.get_node("Fleet") # --- A. capacity is data-driven --------------------------------- fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray(["interceptor"])) if game_state.fleet_cargo_total_capacity() != 20: return _fail("one interceptor: capacity %d, expected 20" % game_state.fleet_cargo_total_capacity()) fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray(["shield", "gas_bomber"])) if game_state.fleet_cargo_total_capacity() != 0: return _fail("shield+gas_bomber: capacity %d, expected 0" % game_state.fleet_cargo_total_capacity()) print("capacity is data-driven: interceptor=20, shield+gas_bomber=0") # --- B. a full hold refuses a pickup, the rock stays ------------ fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray(["interceptor"])) game_state.fleet_cargo = 20 rock = load("res://surface/Rock.tscn").instantiate() rock.build_shape(1) rock.global_position = fleet.ships[0].global_position scene.add_child(rock) elapsed = 0.0 stage = 1 return false 1: if elapsed < 0.3: return false if game_state.fleet_cargo != 20: return _fail("a full hold still absorbed a pickup: hold=%d" % game_state.fleet_cargo) if not is_instance_valid(rock): return _fail("the rock was consumed even though the hold was full") print("full hold (20/20) refused a pickup, the rock remains") # --- C. losing a cargo ship drops the ceiling, keeps the cargo -- # Mirrors Ship.gd's own death sequence (destroyed.emit() then # queue_free()) rather than just calling the handler, so no # untracked ghost ship is left sitting in the scene. var dying_ship: Node = fleet.ships[0] fleet._on_ship_destroyed(dying_ship) dying_ship.queue_free() if game_state.fleet_cargo_total_capacity() != 0: return _fail("empty fleet: capacity %d, expected 0" % game_state.fleet_cargo_total_capacity()) if game_state.fleet_cargo != 20: return _fail("existing cargo was deleted when capacity dropped below it: %d" % game_state.fleet_cargo) print("losing the ship: capacity=0, existing cargo untouched at %d" % game_state.fleet_cargo) if game_state.collect_cargo(5) != 0: return _fail("an over-capacity hold accepted a further deposit") print("over-capacity hold clamps further deposits to zero, as documented") # --- D. a transfer into a hold with partial room clamps --------- fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray(["interceptor", "interceptor"])) if game_state.fleet_cargo_total_capacity() != 40: return _fail("two interceptors: capacity %d, expected 40" % game_state.fleet_cargo_total_capacity()) var bank_before: int = game_state.rocks var moved: int = game_state.transfer_resource_to_fleet(mothership_id, GameState.RESOURCE_ORE, 100) if moved != 20: return _fail("transfer into 20/40 room reported moved=%d, expected 20" % moved) if game_state.fleet_cargo != 40: return _fail("hold after the clamped transfer is %d, expected 40 (full)" % game_state.fleet_cargo) if game_state.rocks != bank_before - 20: return _fail("bank dropped by %d, expected exactly the 20 that moved" % (bank_before - game_state.rocks)) print("transfer into 20/40 room clamped to 20, hold now full, bank debited exactly 20") # --- E. capacity survives the space -> surface scene swap ------- game_state.surviving_ship_count = 2 game_state.surviving_vessel_types = PackedStringArray(["interceptor", "interceptor"]) game_state.planet_id = "test_fleet_cargo_limit" game_state.planet_name = "Cargo Limit Test" scene.queue_free() elapsed = 0.0 stage = 2 return false 2: # A tick for queue_free() to actually leave the tree before adding # the surface scene, matching the shape of every other scene-swap # test in this suite. if elapsed < 0.3: return false scene = load("res://surface/PlanetSurface.tscn").instantiate() root.add_child(scene) current_scene = scene elapsed = 0.0 stage = 3 return false 3: if elapsed < 0.3: return false if game_state.fleet_cargo_total_capacity() != 40: return _fail("capacity after the surface restore is %d, expected 40" % game_state.fleet_cargo_total_capacity()) if game_state.fleet_cargo != 40: return _fail("cargo did not survive the scene swap: %d, expected 40" % game_state.fleet_cargo) print("capacity (40) and cargo (40) both survived the space -> surface swap") print("TEST_FLEET_CARGO_LIMIT: PASS") quit(0) return true return false func _fail(message: String) -> bool: print("TEST_FLEET_CARGO_LIMIT: FAIL (%s)" % message) quit(1) return true