Compose vessels from definitions and capabilities

This commit is contained in:
Loïc Wisniewski 2026-08-14 22:12:57 +02:00
commit 14081ad230
39 changed files with 705 additions and 386 deletions

View file

@ -1,6 +1,7 @@
extends Node2D
const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd")
const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
# Space-side building UI. Owns:
# - the build menu (place a mothership, ship crafter or command centre)
@ -32,6 +33,10 @@ const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd")
get_node("../UI/LabMenu/VesselC"),
]
@onready var lab_vessel_label: Label = get_node("../UI/LabMenu/VesselLabel")
@onready var lab_vessel_labels: Array = [
get_node("../UI/LabMenu/VesselALabel"), get_node("../UI/LabMenu/VesselBLabel"),
get_node("../UI/LabMenu/VesselCLabel"),
]
@onready var lab_detail: Label = get_node("../UI/LabMenu/DetailLabel")
@onready var lab_status: Label = get_node("../UI/LabMenu/StatusLabel")
@onready var lab_buy_button: IconButton = get_node("../UI/LabMenu/BuyButton")
@ -98,6 +103,12 @@ var lab_building_id: int = 0
@onready var craft_interceptor_label: Label = get_node("../UI/CraftMenu/InterceptorLabel")
@onready var craft_transporter_label: Label = get_node("../UI/CraftMenu/TransporterLabel")
@onready var craft_shield_label: Label = get_node("../UI/CraftMenu/ShieldLabel")
@onready var craft_vessel_buttons: Array = [
craft_interceptor_button, craft_shield_button, craft_transporter_button,
]
@onready var craft_vessel_labels: Array = [
craft_interceptor_label, craft_shield_label, craft_transporter_label,
]
# --- the transfer panel (#24) ---------------------------------------------
# Ore used to be moved from inside whichever panel happened to be open: "load
@ -159,7 +170,6 @@ func _ready() -> void:
build_button.pressed.connect(_on_build_pressed)
build_close_button.pressed.connect(_close_build_menu)
_configure_build_menu_entries()
craft_shield_button.pressed.connect(_on_craft_shield)
lab_close_button.pressed.connect(func(): lab_menu.visible = false)
lab_buy_button.pressed.connect(_on_lab_buy)
for i in range(lab_vessel_buttons.size()):
@ -172,8 +182,9 @@ func _ready() -> void:
action_button.pressed.connect(_on_context_action_pressed)
building_action_button.pressed.connect(_on_action_pressed)
craft_interceptor_button.pressed.connect(_on_craft_interceptor)
craft_transporter_button.pressed.connect(_on_craft_transporter)
for i in range(mini(craft_vessel_buttons.size(), VESSEL_DEFINITIONS.all().size())):
var definition: Resource = VESSEL_DEFINITIONS.all()[i]
craft_vessel_buttons[i].pressed.connect(_on_craft_vessel.bind(definition.definition_id))
craft_close_button.pressed.connect(func(): craft_menu.visible = false)
transfer_button.pressed.connect(_on_transfer_pressed)
@ -692,26 +703,19 @@ func _refresh_craft_status() -> void:
# Prices live with the buttons they describe. The top of the screen is
# intentionally reserved for the shared resource bar and a short action
# message; repeating fleet, queue and transport counts here buried the UI.
craft_interceptor_label.text = "Interceptor\n%d ore" % GameState.SHIP_COST
craft_transporter_label.text = "Transporter\n%d ore" % GameState.TRANSPORTER_COST
craft_shield_label.text = "Shield Vessel\n%d ore" % GameState.SHIELD_COST
craft_status.text = ""
# Nowhere to station one means the order would be refused, so say so
# rather than letting the button look available and do nothing.
craft_transporter_button.enabled = GameState.has_transport_hub() \
and GameState.can_station_transporter()
craft_transporter_button.queue_redraw()
craft_shield_button.enabled = GameState.shield_vessel_unlocked()
craft_shield_button.queue_redraw()
# 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()
var definitions: Array = VESSEL_DEFINITIONS.all()
for i in range(mini(definitions.size(), craft_vessel_buttons.size())):
var definition: Resource = definitions[i]
var button: IconButton = craft_vessel_buttons[i]
craft_vessel_labels[i].text = "%s\n%d ore" % [definition.display_name, definition.build_cost]
button.icon = definition.icon
button.glow_color = definition.glow_color
button.enabled = GameState.vessel_is_unlocked(definition.definition_id)
if definition.has_capability("transport"):
button.enabled = button.enabled and GameState.has_transport_hub() \
and GameState.can_station_transporter()
button.queue_redraw()
_refresh_queue()
if craft_message != "":
if Time.get_ticks_msec() < craft_message_until:
@ -721,40 +725,40 @@ func _refresh_craft_status() -> void:
func _on_craft_interceptor() -> void:
_on_craft_vessel(GameState.VESSEL_INTERCEPTOR)
func _on_craft_vessel(vessel: String) -> void:
if craft_building_id == 0:
_say("No crafter selected - reopen this panel from the building")
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
# happened and what the crafter is still short of.
GameState.queue_order(craft_building_id)
_report_order(GameState.PRODUCT_SHIP)
var definition: Resource = VESSEL_DEFINITIONS.get_definition(vessel)
if definition == null:
return
if not GameState.queue_order(craft_building_id, vessel):
if not GameState.vessel_is_unlocked(vessel):
_say("%s must be unlocked at a ship lab first" % definition.display_name)
elif definition.has_capability("transport") and not GameState.has_transport_hub():
_say("Build a Transport Hub first - a transporter needs somewhere to be stationed")
elif definition.has_capability("transport"):
_say("All %d berths are taken - build another hub, or research Freight Yards" %
GameState.transport_capacity())
return
_report_order(vessel)
func _report_order(product: String) -> void:
var short: int = GameState.outstanding_cost(craft_building_id)
if short > 0:
_say("%s queued - %d more ore must be delivered here" % [
GameState.VESSEL_NAMES.get(product, product), short
GameState.vessel_name(product), short
])
else:
_say("%s ordered - building now" % GameState.VESSEL_NAMES.get(product, product))
_say("%s ordered - building now" % GameState.vessel_name(product))
func _on_craft_transporter() -> void:
if craft_building_id == 0:
_say("No crafter selected - reopen this panel from the building")
return
if not GameState.queue_order(craft_building_id, GameState.PRODUCT_TRANSPORTER):
# Two different refusals, and the player needs to know WHICH.
if not GameState.has_transport_hub():
_say("Build a Transport Hub first - a transporter needs somewhere to be stationed")
else:
_say("All %d berths are taken - build another hub, or research Freight Yards" %
GameState.transport_capacity())
return
_refresh_craft_status()
_on_craft_vessel(GameState.VESSEL_TRANSPORTER)
# --- ship lab (#5) --------------------------------------------------------
@ -764,10 +768,13 @@ func _lab_options() -> Array:
# entry, because unlocking the type and upgrading it belong in the same
# place - and its upgrades are pointless until it exists.
var options: Array = []
if lab_vessel == GameState.VESSEL_SHIELD and not GameState.shield_vessel_unlocked():
options.append({"unlock": true, "name": "Unlock Shield Vessel",
"desc": "Lets the ship crafter build shield vessels. Its shield absorbs one lethal hit, then cycles back up.",
"cost": GameState.UNLOCK_SHIELD_COST})
var definition: Resource = VESSEL_DEFINITIONS.get_definition(lab_vessel)
if definition == null:
return options
if not GameState.vessel_is_unlocked(lab_vessel):
options.append({"unlock": true, "id": definition.unlock_id,
"name": definition.unlock_name, "desc": definition.unlock_description,
"cost": definition.unlock_cost, "icon": IconButton.Icon.UPGRADE_UNLOCK_SHIELD})
return options
for u in GameState.upgrades_for(lab_vessel):
options.append({"unlock": false, "id": u.id, "name": u.name, "desc": u.desc})
@ -775,8 +782,10 @@ func _lab_options() -> Array:
func _on_lab_vessel(index: int) -> void:
lab_vessel = [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
GameState.VESSEL_TRANSPORTER][index]
var definitions: Array = VESSEL_DEFINITIONS.all()
if index < 0 or index >= definitions.size():
return
lab_vessel = definitions[index].definition_id
lab_choice = 0
_refresh_lab()
@ -794,7 +803,7 @@ func _on_lab_buy() -> void:
if options.is_empty():
return
var choice: Dictionary = options[lab_choice]
var id: String = GameState.UNLOCK_SHIELD_VESSEL if choice.unlock else String(choice.id)
var id: String = String(choice.id)
if not GameState.queue_lab_research(lab_building_id, id):
lab_status.text = "Research is already queued or complete"
return
@ -803,14 +812,17 @@ func _on_lab_buy() -> void:
func _refresh_lab() -> void:
for i in range(lab_vessel_buttons.size()):
var this_vessel: String = [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
GameState.VESSEL_TRANSPORTER][i]
var definitions: Array = VESSEL_DEFINITIONS.all()
for i in range(mini(lab_vessel_buttons.size(), definitions.size())):
var definition: Resource = definitions[i]
var this_vessel: String = definition.definition_id
lab_vessel_buttons[i].enabled = true
lab_vessel_buttons[i].glow_color = _vessel_colour(this_vessel)
lab_vessel_buttons[i].icon = definition.icon
lab_vessel_buttons[i].glow_color = definition.glow_color
lab_vessel_labels[i].text = definition.display_name
lab_vessel_buttons[i].highlighted = this_vessel == lab_vessel
lab_vessel_buttons[i].queue_redraw()
lab_vessel_label.text = "%s research" % GameState.VESSEL_NAMES.get(lab_vessel, lab_vessel)
lab_vessel_label.text = "%s research" % GameState.vessel_name(lab_vessel)
var options: Array = _lab_options()
for i in range(lab_upgrade_buttons.size()):
@ -820,7 +832,7 @@ func _refresh_lab() -> void:
if not visible:
continue
var option: Dictionary = options[i]
var price_text: String = "%d ore" % (GameState.UNLOCK_SHIELD_COST if bool(option.unlock) else GameState.upgrade_cost(option.id))
var price_text: String = "%d ore" % (int(option.cost) if bool(option.unlock) else GameState.upgrade_cost(option.id))
if not bool(option.unlock) and GameState.upgrade_maxed(option.id):
price_text = "MAX"
var level_note: String = "" if bool(option.unlock) else " %d/%d" % [
@ -848,29 +860,11 @@ func _refresh_lab() -> void:
func _upgrade_icon(option: Dictionary) -> IconButton.Icon:
if bool(option.unlock):
return IconButton.Icon.UPGRADE_UNLOCK_SHIELD
match String(option.id):
"int_damage": return IconButton.Icon.UPGRADE_DAMAGE
"int_rate": return IconButton.Icon.UPGRADE_RATE
"int_range": return IconButton.Icon.UPGRADE_RANGE
"int_speed", "shd_speed": return IconButton.Icon.UPGRADE_THRUSTERS
"shd_hits": return IconButton.Icon.UPGRADE_SHIELD_HITS
"shd_recharge": return IconButton.Icon.UPGRADE_SHIELD_RECHARGE
"trn_capacity": return IconButton.Icon.UPGRADE_CARGO
"trn_speed": return IconButton.Icon.UPGRADE_ENGINES
_: return IconButton.Icon.SHIP_LAB
return int(option.get("icon", IconButton.Icon.SHIP_LAB))
func _on_craft_shield() -> void:
if craft_building_id == 0:
_say("No crafter selected - reopen this panel from the building")
return
if not GameState.shield_vessel_unlocked():
_say("Shield vessels must be unlocked at a ship lab first")
return
GameState.queue_order(craft_building_id, GameState.VESSEL_SHIELD)
_report_order(GameState.VESSEL_SHIELD)
_on_craft_vessel(GameState.VESSEL_SHIELD)
# --- moving ore, in both directions, at any building (#24) ----------------
@ -957,8 +951,8 @@ func _refresh_queue() -> void:
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
var definition: Resource = VESSEL_DEFINITIONS.get_definition(String(order.product))
slot.icon = definition.icon
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.
@ -971,29 +965,24 @@ func _refresh_queue() -> void:
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),
GameState.vessel_name(String(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)
GameState.vessel_name(String(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)
var definition: Resource = VESSEL_DEFINITIONS.get_definition(product)
return definition.glow_color if definition != null else 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 vessel: String = GameState.vessel_name(String(orders[index].product))
var refunded: bool = bool(orders[index].started)
if not GameState.cancel_order(craft_building_id, index):
return
@ -1018,12 +1007,12 @@ func _on_hangar_crew_type(index: int) -> void:
return
var stored_index: int = GameState.stored_vessels.find(type)
if stored_index < 0:
hangar_status.text = "No %s available" % GameState.VESSEL_NAMES.get(type, type)
hangar_status.text = "No %s available" % GameState.vessel_name(type)
return
if not fleet.add_ship(GameState.take_stored_vessel(stored_index)):
hangar_status.text = "Could not crew that vessel"
return
hangar_status.text = "%s joined the fleet" % GameState.VESSEL_NAMES.get(type, type)
hangar_status.text = "%s joined the fleet" % GameState.vessel_name(type)
_refresh_hangar()
@ -1035,32 +1024,40 @@ func _on_hangar_store_type(index: int) -> void:
ship_index = i
break
if ship_index < 0:
hangar_status.text = "No %s in the fleet" % GameState.VESSEL_NAMES.get(type, type)
hangar_status.text = "No %s in the fleet" % GameState.vessel_name(type)
return
var stored: String = fleet.store_ship(ship_index)
if stored == "":
hangar_status.text = "Keep one ship deployed"
return
GameState.stored_vessels.append(stored)
hangar_status.text = "%s stored in the hangar" % GameState.VESSEL_NAMES.get(stored, stored)
hangar_status.text = "%s stored in the hangar" % GameState.vessel_name(stored)
_refresh_hangar()
func _hangar_types() -> Array:
return [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD, GameState.VESSEL_TRANSPORTER]
var types: Array = []
for definition in VESSEL_DEFINITIONS.all():
types.append(definition.definition_id)
return types
func _refresh_hangar() -> void:
var types: Array = _hangar_types()
for i in range(types.size()):
var type: String = types[i]
var definition: Resource = VESSEL_DEFINITIONS.get_definition(type)
var available: int = GameState.stored_vessels.count(type)
var deployed: int = 0
for ship in fleet.ships:
if is_instance_valid(ship) and String(ship.vessel_type) == type:
deployed += 1
hangar_available_labels[i].text = "%s\n%d available" % [GameState.VESSEL_NAMES.get(type, type), available]
hangar_fleet_labels[i].text = "%s\n%d in fleet" % [GameState.VESSEL_NAMES.get(type, type), deployed]
hangar_available_labels[i].text = "%s\n%d available" % [definition.display_name, available]
hangar_fleet_labels[i].text = "%s\n%d in fleet" % [definition.display_name, deployed]
hangar_available_slots[i].icon = definition.icon
hangar_fleet_slots[i].icon = definition.icon
hangar_available_slots[i].glow_color = definition.glow_color
hangar_fleet_slots[i].glow_color = definition.glow_color
hangar_available_slots[i].enabled = available > 0 and fleet.ships.size() < GameState.max_fleet_size()
hangar_fleet_slots[i].enabled = deployed > 0 and fleet.ships.size() > 1
hangar_available_slots[i].queue_redraw()