Remove the one-way ore API superseded by the transfer menu

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>
This commit is contained in:
Loïc Wisniewski 2026-08-12 13:07:52 +02:00
commit b6427b080d
27 changed files with 1085 additions and 275 deletions

View file

@ -74,12 +74,34 @@ var lab_choice: int = 0
@onready var craft_menu: Control = get_node("../UI/CraftMenu")
@onready var craft_interceptor_button: IconButton = get_node("../UI/CraftMenu/InterceptorButton")
@onready var craft_transporter_button: IconButton = get_node("../UI/CraftMenu/TransporterButton")
@onready var craft_store_button: IconButton = get_node("../UI/CraftMenu/StoreOreButton")
@onready var craft_store_label: Label = get_node("../UI/CraftMenu/StoreOreLabel")
@onready var craft_close_button: IconButton = get_node("../UI/CraftMenu/CloseButton")
@onready var craft_status: Label = get_node("../UI/CraftMenu/StatusLabel")
@onready var load_ore_button: IconButton = get_node("../UI/SkillMenu/LoadOreButton")
@onready var load_ore_label: Label = get_node("../UI/SkillMenu/LoadOreLabel")
# --- the transfer panel (#24) ---------------------------------------------
# Ore used to be moved from inside whichever panel happened to be open: "load
# ore" lived in the research screen, "store ore here" in the ship crafter.
# Both are now one top-level button that works at every building and in both
# directions, so moving ore is never buried under an unrelated menu.
@onready var transfer_button: IconButton = get_node("../UI/TransferButton")
@onready var transfer_menu: Control = get_node("../UI/TransferMenu")
@onready var transfer_status: Label = get_node("../UI/TransferMenu/StatusLabel")
@onready var transfer_close_button: IconButton = get_node("../UI/TransferMenu/CloseButton")
@onready var to_building_label: Label = get_node("../UI/TransferMenu/ToBuildingLabel")
@onready var to_building_batch_button: IconButton = get_node("../UI/TransferMenu/ToBuildingBatchButton")
@onready var to_building_batch_label: Label = get_node("../UI/TransferMenu/ToBuildingBatchLabel")
@onready var to_building_all_button: IconButton = get_node("../UI/TransferMenu/ToBuildingAllButton")
@onready var to_fleet_label: Label = get_node("../UI/TransferMenu/ToFleetLabel")
@onready var to_fleet_batch_button: IconButton = get_node("../UI/TransferMenu/ToFleetBatchButton")
@onready var to_fleet_batch_label: Label = get_node("../UI/TransferMenu/ToFleetBatchLabel")
@onready var to_fleet_all_button: IconButton = get_node("../UI/TransferMenu/ToFleetAllButton")
# Every building menu carries the same header (#24). Held as a list because
# the only thing any of them needs is to be told which building it is
# reporting on when its panel opens.
@onready var resource_bars: Array = [
get_node("../UI/CraftMenu/Bar"), get_node("../UI/SkillMenu/Bar"),
get_node("../UI/LabMenu/Bar"), get_node("../UI/TransferMenu/Bar"),
]
@onready var skill_menu: Control = get_node("../UI/SkillMenu")
@onready var skill_tree: Control = get_node("../UI/SkillMenu/Tree")
@ -141,6 +163,12 @@ var craft_building_id: int = 0
var craft_message: String = ""
var craft_message_until: int = 0
# Which building the transfer panel is moving ore in and out of. Remembered
# for the same reason craft_building_id is: _process() clears
# current_building while any menu is open, so by the next frame the panel
# would not know which building it belongs to.
var transfer_building_id: int = 0
var current_building: Node2D = null
var pending_building: Node2D = null
var pending_time: float = 0.0
@ -167,11 +195,16 @@ func _ready() -> void:
action_button.pressed.connect(_on_action_pressed)
craft_interceptor_button.pressed.connect(_on_craft_interceptor)
load_ore_button.pressed.connect(_on_load_ore)
craft_transporter_button.pressed.connect(_on_craft_transporter)
craft_store_button.pressed.connect(_on_store_ore)
craft_close_button.pressed.connect(func(): craft_menu.visible = false)
transfer_button.pressed.connect(_on_transfer_pressed)
transfer_close_button.pressed.connect(func(): transfer_menu.visible = false)
to_building_batch_button.pressed.connect(_on_transfer.bind(true, GameState.CARGO_LOAD_BATCH))
to_building_all_button.pressed.connect(_on_transfer.bind(true, ALL))
to_fleet_batch_button.pressed.connect(_on_transfer.bind(false, GameState.CARGO_LOAD_BATCH))
to_fleet_all_button.pressed.connect(_on_transfer.bind(false, ALL))
formation_close_button.pressed.connect(func(): formation_menu.visible = false)
formation_reset_button.pressed.connect(_on_reset_formation)
formation_editor.formation_changed.connect(_on_formation_changed)
@ -186,14 +219,16 @@ func _ready() -> void:
formation_menu.visible = false
skill_menu.visible = false
lab_menu.visible = false
transfer_menu.visible = false
action_button.visible = false
transfer_button.visible = false
# True while a building prompt or panel owns the shared action slot, so the
# enter-atmosphere prompt knows to stay out of the way.
func is_showing_building_prompt() -> bool:
return current_building != null or build_menu.visible or craft_menu.visible \
or formation_menu.visible or skill_menu.visible
or formation_menu.visible or skill_menu.visible or transfer_menu.visible
func _unhandled_input(event: InputEvent) -> void:
@ -218,7 +253,8 @@ func _unhandled_input(event: InputEvent) -> void:
formation_editor.handle_drag(event)
get_viewport().set_input_as_handled()
return
if build_menu.visible or craft_menu.visible or world_map.visible or lab_menu.visible:
if build_menu.visible or craft_menu.visible or world_map.visible or lab_menu.visible \
or transfer_menu.visible:
# These panels have no raw-input handler of their own, but the world
# behind them must still not react.
get_viewport().set_input_as_handled()
@ -239,11 +275,18 @@ func _process(delta: float) -> void:
# the queue down instead of looking stuck.
_refresh_craft_status()
if transfer_menu.visible:
# Also live: a hauler banking ore while the panel is open should make
# the "to fleet" side become available without a reopen.
_refresh_transfer()
if world_map.visible or build_menu.visible or craft_menu.visible \
or formation_menu.visible or skill_menu.visible or lab_menu.visible:
or formation_menu.visible or skill_menu.visible or lab_menu.visible \
or transfer_menu.visible:
current_building = null
pending_building = null
action_button.visible = false
transfer_button.visible = false
return
# Debounced exactly like the enter-atmosphere prompt, so the action
@ -261,6 +304,10 @@ func _process(delta: float) -> void:
current_building = pending_building
action_button.visible = current_building != null
# Every building can hold ore, so the transfer button rides alongside the
# action button rather than being one of the things the action button
# turns into - the two are independent choices at the same structure.
transfer_button.visible = current_building != null
if current_building != null:
action_button.icon = _action_icon_for(current_building.building_type)
action_button.queue_redraw()
@ -292,9 +339,14 @@ func _check_fleet_wipe() -> void:
func _on_action_pressed() -> void:
if current_building == null:
return
# Whichever panel opens, its header reports on the building it was opened
# from. Set once here rather than in each branch, so a new building menu
# cannot forget to do it and silently show a blank HERE column.
for bar in resource_bars:
bar.building_id = current_building.building_id
if current_building.building_type == GameState.BUILDING_MOTHERSHIP:
skill_menu.visible = true
_refresh_load_ore()
_refresh_skill_menu()
elif current_building.building_type == GameState.BUILDING_SHIP_CRAFTER:
# Remembered explicitly: _process() clears current_building while any
@ -591,14 +643,14 @@ func _say(message: String) -> void:
func _refresh_craft_status() -> void:
# "Here" is the ore delivered to THIS crafter, not the bank: since #16 a
# building builds out of its own stock, and showing the bank would say
# you can afford something the crafter cannot actually make.
# The bank/hold/here figures moved out to the shared header (#24), so this
# line is now only what is specific to a crafter: prices, the queue, and
# berth pressure. It used to restate "Here %d" itself, which is exactly
# the per-panel improvisation the header replaces.
var queued: int = GameState.pending_orders(craft_building_id)
var stock: int = GameState.building_stock(craft_building_id)
var queue_text: String = "" if queued == 0 else " Queued %d" % queued
craft_status.text = "Interceptor %d Transporter %d Here %d%s Transporters %d/%d" % [
GameState.SHIP_COST, GameState.TRANSPORTER_COST, stock, queue_text,
craft_status.text = "Interceptor %d Transporter %d%s Transporters %d/%d" % [
GameState.SHIP_COST, GameState.TRANSPORTER_COST, queue_text,
GameState.transporters_claimed(), GameState.transport_capacity()
]
# Nowhere to station one means the order would be refused, so say so
@ -609,12 +661,6 @@ func _refresh_craft_status() -> void:
craft_shield_button.enabled = GameState.shield_vessel_unlocked()
craft_shield_button.queue_redraw()
craft_store_label.text = "Store ore here\nHold %d" % GameState.fleet_cargo
# Deliberately left ENABLED with an empty hold: a greyed-out button that
# cannot say why is exactly the dead-button problem this panel already
# had. Pressing it explains instead.
craft_store_button.enabled = true
craft_store_button.queue_redraw()
# A full fleet cannot take another vessel, so the order would be refused.
# Showing that on the button is the difference between "not available yet"
@ -663,17 +709,6 @@ func _on_craft_interceptor() -> void:
_report_order(GameState.PRODUCT_SHIP)
func _refresh_load_ore() -> void:
load_ore_label.text = "Load ore\nBank %d Hold %d" % [GameState.rocks, GameState.fleet_cargo]
load_ore_button.enabled = GameState.rocks > 0
load_ore_button.queue_redraw()
func _on_load_ore() -> void:
GameState.withdraw_to_fleet()
_refresh_load_ore()
func _report_order(product: String) -> void:
var short: int = GameState.outstanding_cost(craft_building_id)
if short > 0:
@ -684,17 +719,6 @@ func _report_order(product: String) -> void:
_say("%s ordered - building now" % GameState.VESSEL_NAMES.get(product, product))
func _on_store_ore() -> void:
if craft_building_id == 0:
_say("No crafter selected - reopen this panel from the building")
return
if GameState.fleet_cargo <= 0:
_say("The fleet is carrying nothing - load ore at the mothership first")
return
var moved: int = GameState.unload_into_building(craft_building_id)
_say("Stored %d ore here" % moved)
func _on_craft_transporter() -> void:
if craft_building_id == 0:
_say("No crafter selected - reopen this panel from the building")
@ -823,6 +847,86 @@ func _on_craft_shield() -> void:
_report_order(GameState.VESSEL_SHIELD)
# --- moving ore, in both directions, at any building (#24) ----------------
# Sentinel for the "All" buttons. The transfer primitives cap what they move
# by what is actually there, so asking for more than could possibly exist is
# how you say "everything" without the UI having to work out how much that
# is first - and the answer cannot go stale between the read and the press.
const ALL: int = 1 << 30
func _on_transfer_pressed() -> void:
if current_building == null:
return
transfer_building_id = current_building.building_id
transfer_menu.visible = true
transfer_status.text = ""
_refresh_transfer()
func _on_transfer(to_building: bool, amount: int) -> void:
if transfer_building_id == 0:
transfer_status.text = "No building selected - reopen this panel from the building"
return
var moved: int = 0
if to_building:
moved = GameState.transfer_to_building(transfer_building_id, amount)
else:
moved = GameState.transfer_to_fleet(transfer_building_id, amount)
# Nothing moving is the common case at an empty end, and a silent no-op
# is the dead-button problem this panel exists to remove. Say which side
# was empty rather than just refusing.
if moved <= 0:
if to_building:
transfer_status.text = "The fleet is carrying nothing to unload"
else:
transfer_status.text = "%s has no ore to load" % _transfer_place_name()
elif to_building:
transfer_status.text = "Moved %d ore into %s" % [moved, _transfer_place_name()]
else:
transfer_status.text = "Loaded %d ore into the fleet" % moved
_refresh_transfer()
func _transfer_place_name() -> String:
var b: Dictionary = GameState.find_building(transfer_building_id)
if b.is_empty():
return "this building"
if b.type == GameState.BUILDING_MOTHERSHIP:
# Worth naming as the bank rather than as a structure: this is the
# one building where unloading makes the ore spendable everywhere.
return "the bank"
return "this building"
func _refresh_transfer() -> void:
for bar in resource_bars:
bar.building_id = transfer_building_id
var hold: int = GameState.fleet_cargo
var here: int = GameState.building_holdings(transfer_building_id)
var batch: int = GameState.CARGO_LOAD_BATCH
to_building_label.text = "Unload into %s" % _transfer_place_name()
to_fleet_label.text = "Load from %s" % _transfer_place_name()
# The batch buttons say what they would actually move, not the nominal
# batch size: offering "25" when the hold has 4 in it is a promise the
# press cannot keep.
to_building_batch_label.text = "%d" % mini(batch, hold)
to_fleet_batch_label.text = "%d" % mini(batch, here)
to_building_batch_button.enabled = hold > 0
to_building_all_button.enabled = hold > 0
to_fleet_batch_button.enabled = here > 0
to_fleet_all_button.enabled = here > 0
for button in [to_building_batch_button, to_building_all_button,
to_fleet_batch_button, to_fleet_all_button]:
button.queue_redraw()
func _on_formation_changed() -> void:
fleet.apply_formation()