#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
245 lines
9.5 KiB
GDScript
245 lines
9.5 KiB
GDScript
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 shortcut appears beside Build whenever the fleet is
|
|
# standing on a transferable building, and opens the 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 += 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.transporters.clear()
|
|
game_state.rocks = 100
|
|
game_state.fleet_cargo = 0
|
|
game_state.stored_vessels.clear()
|
|
game_state.unlocked = ["shipyards", "coordination"]
|
|
# #219: fleet capacity is built now - one berth free, one per Fleet
|
|
# Office. This test flies more than one vessel, so it needs the offices
|
|
# standing before the fleet spawns into them.
|
|
# Eight, which is what every doctrine used to allow: the amounts below are
|
|
# hand-picked to sit under a hold that size (8 interceptors x 20 = 160).
|
|
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 8)
|
|
scene = load("res://world/Main.tscn").instantiate()
|
|
root.add_child(scene)
|
|
fleet = scene.get_node("Fleet")
|
|
# A new game now flies STARTING_SHIP_COUNT (1) vessel into BASE_FLEET_SIZE
|
|
# (3) slots. This test needs a crewed fleet, so it asks for one outright
|
|
# rather than inheriting whatever a fresh game happens to open with -
|
|
# which is exactly what changed underneath it.
|
|
fleet.spawn_ships(5, Vector2.ZERO)
|
|
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:
|
|
_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()
|
|
# #56 gave the hold a finite, composition-driven capacity. This
|
|
# test is about the transfer UI, not that cap - test_fleet_cargo_
|
|
# limit.gd owns the cap itself - so it spawns into every berth the
|
|
# offices above bought (8 interceptors x 20 = 160), comfortably
|
|
# above every hand-picked amount below.
|
|
fleet.spawn_ships(game_state.max_fleet_size(), 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")
|
|
_expect(controller.transfer_button.visible,
|
|
"transfer shortcut did not appear beside Build")
|
|
_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 --------------------
|
|
_expect(controller.transfer_menu.building_id == crafter_id,
|
|
"transfer scene did not own the selected building context")
|
|
|
|
# --- 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_menu.status.text,
|
|
"unloading an empty hold said: %s" % controller.transfer_menu.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
|
|
var resource_list: Node = scene.get_node("UI/TransferMenu/ResourceList")
|
|
# Default is ALL. One minus selects 25, proving the amount control
|
|
# affects a real transfer rather than only changing its caption.
|
|
_tap(resource_list.get_node("OreDecreaseButton"))
|
|
_tap(resource_list.get_node("OreToFleetButton"))
|
|
_expect(game_state.fleet_cargo == mini(GameState.CARGO_LOAD_BATCH, bank_at_tap),
|
|
"selected 25 but the real tap loaded %d" % game_state.fleet_cargo)
|
|
_expect(game_state.rocks == bank_at_tap - game_state.fleet_cargo,
|
|
"the bank did not pay the selected transfer amount")
|
|
# Back to ALL and move the remainder, preserving the original
|
|
# end-to-end all-transfer coverage as well.
|
|
_tap(resource_list.get_node("OreIncreaseButton"))
|
|
_tap(resource_list.get_node("OreToFleetButton"))
|
|
_expect(game_state.fleet_cargo == bank_at_tap,
|
|
"switching back to ALL did not load the remainder")
|
|
_expect(game_state.rocks == 0, "ALL left ore in the bank")
|
|
|
|
# --- 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()
|