Groundwork for #5. vessels_owed carries the VESSEL TYPE rather than a bare count, so a shield vessel finishing while the player is on a planet comes back as a shield vessel. Fleet.add_ship takes a type, and vessels already flying are re-statted when an upgrade is researched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
205 lines
6.9 KiB
GDScript
205 lines
6.9 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 banks the hold, but doing so is EDGE
|
|
# triggered - ore withdrawn while parked there is not instantly taken
|
|
# back, which would make the button look broken
|
|
# 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 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)
|
|
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.withdraw_to_fleet(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. still parked on the mothership ----------------------
|
|
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 instantly banked again - delivery is not edge triggered)")
|
|
quit(1)
|
|
return true
|
|
print("still parked -> hold kept its %d (delivery is edge triggered)" % 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.unload_into_building(crafter_id)
|
|
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.withdraw_to_fleet(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()
|