#219: fleet capacity is a built, supplied thing rather than a researched one. The eight fleet-size doctrines are gone; capacity is one berth free plus one per Fleet Office standing. An office charges every 25s for the vessel in its berth, priced off that vessel's own build cost so an expensive hull is expensive to keep, and drawn from its own store the way a gas burner draws gas. An office that cannot pay goes dark, its vessel is scuttled, and the player is told through the same queue quest lines use - which reaches them on a planet surface too. An empty berth is idle rather than starved: an office is only failing while there is something in it to fail, and holding it disabled would leave a berth that could never be crewed and so could never be paid for again. Ships have no hit points and a shield absorbs a hit wholesale, so the lost vessel is scuttled rather than damaged - otherwise a shielded hull would survive its own eviction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
248 lines
9.3 KiB
GDScript
248 lines
9.3 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 - and since #158
|
|
# it does not need to be asked: a queued order at a building the fleet
|
|
# is PARKED ON pays itself out of the hold, so the hand-off happens on
|
|
# arrival rather than through the transfer menu. The manual move still
|
|
# works and is still what stocks a building ahead of an order, which is
|
|
# checked separately here
|
|
# 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:
|
|
if b.type != GameState.BUILDING_MOTHERSHIP:
|
|
sum += game_state.building_resource(int(b.id), GameState.RESOURCE_ORE)
|
|
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"]
|
|
scene = load("res://world/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
|
|
# #158: parked on the crafter with the ore aboard, the queued
|
|
# order takes it - the flight IS the delivery now, and the hold
|
|
# is empty because it has been spent rather than because nothing
|
|
# happened.
|
|
if game_state.fleet_cargo != 0:
|
|
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (parked on the crafter with %d still in the hold)" % game_state.fleet_cargo)
|
|
quit(1)
|
|
return true
|
|
# Orders run at build_time_scale 0.02 here, so by now it has not
|
|
# only started on the hold's ore but finished.
|
|
if game_state.pending_orders(crafter_id) != 0:
|
|
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (the hold emptied but the order is still queued)")
|
|
quit(1)
|
|
return true
|
|
print("parked on the crafter: the queued order took the hold's %d itself" % GameState.SHIP_COST)
|
|
|
|
# ...and the manual move still works, which is what stocks a
|
|
# building AHEAD of an order rather than paying for one.
|
|
game_state.collect_cargo(4)
|
|
var moved: int = game_state.transfer_to_building(crafter_id, 4)
|
|
if moved != 4 or game_state.fleet_cargo != 0:
|
|
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (hand-unloaded %d of 4, hold left at %d)" % [moved, game_state.fleet_cargo])
|
|
quit(1)
|
|
return true
|
|
print("and a hand transfer still moves %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 ----------------------------------------
|
|
# The 4 mined above to exercise the hand transfer are still in
|
|
# the crafter's stock, so they are part of the total.
|
|
var expected: int = total_before - GameState.SHIP_COST + 4
|
|
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()
|