The loose end from #24. withdraw_to_fleet, unload_into_building and deliver_cargo were the old one-way moves behind "Load ore" and "Store ore here"; every one of them is a special case of transfer_to_building / transfer_to_fleet, and none had a caller left outside tests. The cargo_delivered signal had no listeners at all. Not just tidying: unload_into_building routed through deposit_to_building, which does NOT special-case the mothership - it writes b.stock, while building_holdings() returns `rocks` for a mothership and ignores stock entirely. So unload_into_building( mothership_id) would have parked ore in a field nothing reads, with no error. Nothing called it that way, but it is a plausibly-named function sitting there to be reached for later. transfer_to_building handles it. CARGO_LOAD_BATCH stays - the transfer menu's batch buttons use it; only its role as withdraw_to_fleet's default argument is gone. The #24 note expected folding these in to be costly for test_cargo_readout, which places no buildings. It is not: the transfer primitives work off GameState records rather than spawned nodes, so a single add_building() in setup is enough and it stays a display test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
216 lines
7.4 KiB
GDScript
216 lines
7.4 KiB
GDScript
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.vessels_owed.clear()
|
|
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:
|
|
if elapsed < 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
|
|
if fleet.ships.size() != ships_before + 1:
|
|
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (no vessel: %d -> %d)" % [ships_before, fleet.ships.size()])
|
|
quit(1)
|
|
return true
|
|
print("queued order fulfilled by hand, no transport hub: ships %d->%d" % [
|
|
ships_before, fleet.ships.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()
|