Add timed craft queue and hangar

This commit is contained in:
Loïc Wisniewski 2026-08-12 22:47:46 +02:00
commit 82552464d2
14 changed files with 660 additions and 191 deletions

View file

@ -42,6 +42,21 @@ extends Node2D
@onready var lab_buy_button: IconButton = get_node("../UI/LabMenu/BuyButton")
@onready var lab_close_button: IconButton = get_node("../UI/LabMenu/CloseButton")
@onready var craft_shield_button: IconButton = get_node("../UI/CraftMenu/ShieldButton")
@onready var queue_slots: Array = [
get_node("../UI/CraftMenu/QueueSlot0"), get_node("../UI/CraftMenu/QueueSlot1"),
get_node("../UI/CraftMenu/QueueSlot2"), get_node("../UI/CraftMenu/QueueSlot3"),
]
@onready var queue_detail: Label = get_node("../UI/CraftMenu/QueueDetail")
@onready var hangar_button: IconButton = get_node("../UI/HangarButton")
@onready var hangar_menu: Control = get_node("../UI/HangarMenu")
@onready var hangar_list: Label = get_node("../UI/HangarMenu/ListLabel")
@onready var hangar_status: Label = get_node("../UI/HangarMenu/StatusLabel")
@onready var hangar_next_button: IconButton = get_node("../UI/HangarMenu/NextButton")
@onready var hangar_crew_button: IconButton = get_node("../UI/HangarMenu/CrewButton")
@onready var hangar_close_button: IconButton = get_node("../UI/HangarMenu/CloseButton")
# Which stored vessel the hangar is offering to crew.
var hangar_choice: int = 0
@onready var build_menu_entries: Array = [
[GameState.BUILDING_MOTHERSHIP, build_mothership_button,
get_node("../UI/BuildMenu/MothershipLabel"), get_node("../UI/BuildMenu/MothershipCost")],
@ -199,6 +214,12 @@ func _ready() -> void:
craft_close_button.pressed.connect(func(): craft_menu.visible = false)
transfer_button.pressed.connect(_on_transfer_pressed)
hangar_button.pressed.connect(_on_hangar_pressed)
hangar_close_button.pressed.connect(func(): hangar_menu.visible = false)
hangar_next_button.pressed.connect(_on_hangar_next)
hangar_crew_button.pressed.connect(_on_hangar_crew)
for i in range(queue_slots.size()):
queue_slots[i].pressed.connect(_on_queue_slot.bind(i))
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))
@ -222,13 +243,16 @@ func _ready() -> void:
transfer_menu.visible = false
action_button.visible = false
transfer_button.visible = false
hangar_button.visible = false
hangar_menu.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 transfer_menu.visible
or formation_menu.visible or skill_menu.visible or transfer_menu.visible \
or hangar_menu.visible
func _unhandled_input(event: InputEvent) -> void:
@ -254,7 +278,7 @@ func _unhandled_input(event: InputEvent) -> void:
get_viewport().set_input_as_handled()
return
if build_menu.visible or craft_menu.visible or world_map.visible or lab_menu.visible \
or transfer_menu.visible:
or transfer_menu.visible or hangar_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()
@ -282,11 +306,12 @@ func _process(delta: float) -> void:
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 transfer_menu.visible:
or transfer_menu.visible or hangar_menu.visible:
current_building = null
pending_building = null
action_button.visible = false
transfer_button.visible = false
hangar_button.visible = false
return
# Debounced exactly like the enter-atmosphere prompt, so the action
@ -304,6 +329,10 @@ func _process(delta: float) -> void:
current_building = pending_building
action_button.visible = current_building != null
# The hangar is where finished vessels are delivered (#23), so it belongs
# at the mothership and nowhere else.
hangar_button.visible = current_building != null \
and current_building.building_type == GameState.BUILDING_MOTHERSHIP
# 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.
@ -662,27 +691,19 @@ func _refresh_craft_status() -> void:
craft_shield_button.enabled = GameState.shield_vessel_unlocked()
craft_shield_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"
# and a button that looks live and silently does nothing when pressed.
var room: bool = fleet.ships.size() + GameState.vessels_owed.size() \
+ GameState.pending_orders_of(craft_building_id, GameState.PRODUCT_SHIP) \
< GameState.max_fleet_size()
craft_interceptor_button.enabled = room
# Fleet capacity no longer gates ORDERING (#23). A finished vessel waits
# at the mothership until the player crews it, so a full fleet is a
# reason not to collect one yet - not a reason the crafter cannot build.
# This is also what stopped the crafter being inert on a fresh save,
# where the fleet starts at exactly its cap.
craft_interceptor_button.enabled = true
craft_interceptor_button.queue_redraw()
# One statement of the fleet count, either way round: the capacity line
# replaces the plain readout rather than being bolted in front of it.
if room:
craft_status.text += " Fleet %d/%d" % [fleet.ships.size(), GameState.max_fleet_size()]
else:
var queued_ships: int = GameState.pending_orders_of(craft_building_id, GameState.PRODUCT_SHIP)
var owed: int = GameState.vessels_owed.size() + queued_ships
var pending_note: String = "" if owed == 0 else " (%d already on order)" % owed
craft_status.text = "No room in the fleet: %d/%d%s - research capacity at the mothership\n%s" % [
fleet.ships.size(), GameState.max_fleet_size(), pending_note, craft_status.text
]
craft_status.text += " Fleet %d/%d Ready %d" % [
fleet.ships.size(), GameState.max_fleet_size(), GameState.stored_vessels.size()
]
# LAST, so a message about what just happened is the first line the
# player reads rather than being buried under the standing state.
_refresh_queue()
if craft_message != "":
if Time.get_ticks_msec() < craft_message_until:
craft_status.text = "%s\n%s" % [craft_message, craft_status.text]
@ -694,13 +715,6 @@ func _on_craft_interceptor() -> void:
if craft_building_id == 0:
_say("No crafter selected - reopen this panel from the building")
return
# Still refused when there is nowhere to put the vessel - queueing an
# order that can never be delivered would just bank vessels_owed forever.
if fleet.ships.size() + GameState.vessels_owed.size() \
+ GameState.pending_orders_of(craft_building_id, GameState.PRODUCT_SHIP) \
>= GameState.max_fleet_size():
_say("Fleet is full (%d slots)" % GameState.max_fleet_size())
return
# No affordability check: an order the crafter cannot pay for yet is
# QUEUED, and completes by itself once ore is delivered here (#16). But
# queueing silently looks identical to a dead button, so it says which
@ -839,10 +853,6 @@ func _on_craft_shield() -> void:
if not GameState.shield_vessel_unlocked():
_say("Shield vessels must be unlocked at a ship lab first")
return
if fleet.ships.size() + GameState.vessels_owed.size() \
+ GameState.pending_orders(craft_building_id) >= GameState.max_fleet_size():
_say("Fleet is full (%d slots)" % GameState.max_fleet_size())
return
GameState.queue_order(craft_building_id, GameState.VESSEL_SHIELD)
_report_order(GameState.VESSEL_SHIELD)
@ -927,6 +937,115 @@ func _refresh_transfer() -> void:
button.queue_redraw()
func _refresh_queue() -> void:
# The queue down the right of the craft panel: one slot per pending
# vessel, the first being the one actually under construction. Tapping a
# slot cancels it (#23).
var orders: Array = GameState.order_list(craft_building_id)
for i in range(queue_slots.size()):
var slot: IconButton = queue_slots[i]
slot.visible = i < orders.size()
if not slot.visible:
continue
var order: Dictionary = orders[i]
slot.icon = IconButton.Icon.TRANSPORT_HUB if order.product == GameState.PRODUCT_TRANSPORTER \
else IconButton.Icon.SHIP
slot.glow_color = _vessel_colour(String(order.product))
# Only the one under construction reads as active; the rest are
# waiting, and greyed says so without needing a caption each.
slot.enabled = bool(order.started)
slot.queue_redraw()
if orders.is_empty():
queue_detail.text = "Nothing queued"
return
var head: Dictionary = orders[0]
if bool(head.started):
queue_detail.text = "Building %s %d%%\nTap to cancel" % [
GameState.VESSEL_NAMES.get(head.product, head.product),
int(round(float(head.progress) * 100.0)),
]
else:
queue_detail.text = "%s waiting for ore\nTap to cancel" % \
GameState.VESSEL_NAMES.get(head.product, head.product)
func _vessel_colour(product: String) -> Color:
match product:
GameState.VESSEL_SHIELD:
return Color(0.45, 0.8, 1.0)
GameState.PRODUCT_TRANSPORTER:
return Color(0.95, 0.55, 0.85)
_:
return Color(0.25, 0.85, 1.0)
func _on_queue_slot(index: int) -> void:
var orders: Array = GameState.order_list(craft_building_id)
if index >= orders.size():
return
var vessel: String = GameState.VESSEL_NAMES.get(orders[index].product, orders[index].product)
var refunded: bool = bool(orders[index].started)
if not GameState.cancel_order(craft_building_id, index):
return
_say("%s cancelled%s" % [vessel, " - ore refunded here" if refunded else ""])
_refresh_queue()
# --- hangar (#23) ---------------------------------------------------------
func _on_hangar_pressed() -> void:
hangar_choice = 0
hangar_status.text = ""
hangar_menu.visible = true
_refresh_hangar()
func _on_hangar_next() -> void:
if GameState.stored_vessels.is_empty():
return
hangar_choice = (hangar_choice + 1) % GameState.stored_vessels.size()
_refresh_hangar()
func _on_hangar_crew() -> void:
if GameState.stored_vessels.is_empty():
hangar_status.text = "Nothing built yet - order vessels at a ship crafter"
return
if fleet.ships.size() >= GameState.max_fleet_size():
hangar_status.text = "Fleet is full (%d/%d) - research capacity at the mothership" % [
fleet.ships.size(), GameState.max_fleet_size()
]
return
var product: String = GameState.claim_stored_vessel(hangar_choice)
if product == "" or not fleet.add_ship(product):
hangar_status.text = "Could not crew that vessel"
return
GameState.take_stored_vessel(hangar_choice)
hangar_status.text = "%s joined the fleet" % GameState.VESSEL_NAMES.get(product, product)
hangar_choice = 0
_refresh_hangar()
func _refresh_hangar() -> void:
var stored: Array = GameState.stored_vessels
if stored.is_empty():
hangar_list.text = "No vessels waiting.\n\nOrder them at a ship crafter - finished vessels are delivered here."
hangar_crew_button.enabled = false
hangar_next_button.enabled = false
else:
hangar_choice = clampi(hangar_choice, 0, stored.size() - 1)
var lines: Array = []
for i in range(stored.size()):
var marker: String = ">" if i == hangar_choice else " "
lines.append("%s %s" % [marker, GameState.VESSEL_NAMES.get(stored[i], stored[i])])
hangar_list.text = "\n".join(lines)
hangar_crew_button.enabled = fleet.ships.size() < GameState.max_fleet_size()
hangar_next_button.enabled = stored.size() > 1
hangar_crew_button.queue_redraw()
hangar_next_button.queue_redraw()
func _on_formation_changed() -> void:
fleet.apply_formation()