Improve ship crafter and hangar controls

This commit is contained in:
Loïc Wisniewski 2026-08-12 23:08:43 +02:00
commit 06081c41b0
4 changed files with 348 additions and 56 deletions

View file

@ -49,14 +49,28 @@ extends Node2D
@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 hangar_available_slots: Array = [
get_node("../UI/HangarMenu/AvailableInterceptor"),
get_node("../UI/HangarMenu/AvailableShield"),
get_node("../UI/HangarMenu/AvailableTransporter"),
]
@onready var hangar_available_labels: Array = [
get_node("../UI/HangarMenu/AvailableInterceptorLabel"),
get_node("../UI/HangarMenu/AvailableShieldLabel"),
get_node("../UI/HangarMenu/AvailableTransporterLabel"),
]
@onready var hangar_fleet_slots: Array = [
get_node("../UI/HangarMenu/FleetInterceptor"),
get_node("../UI/HangarMenu/FleetShield"),
get_node("../UI/HangarMenu/FleetTransporter"),
]
@onready var hangar_fleet_labels: Array = [
get_node("../UI/HangarMenu/FleetInterceptorLabel"),
get_node("../UI/HangarMenu/FleetShieldLabel"),
get_node("../UI/HangarMenu/FleetTransporterLabel"),
]
@onready var build_menu_entries: Array = [
[GameState.BUILDING_MOTHERSHIP, build_mothership_button,
get_node("../UI/BuildMenu/MothershipLabel"), get_node("../UI/BuildMenu/MothershipCost")],
@ -91,6 +105,9 @@ var lab_choice: int = 0
@onready var craft_transporter_button: IconButton = get_node("../UI/CraftMenu/TransporterButton")
@onready var craft_close_button: IconButton = get_node("../UI/CraftMenu/CloseButton")
@onready var craft_status: Label = get_node("../UI/CraftMenu/StatusLabel")
@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")
# --- the transfer panel (#24) ---------------------------------------------
# Ore used to be moved from inside whichever panel happened to be open: "load
@ -216,8 +233,9 @@ func _ready() -> void:
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(hangar_available_slots.size()):
hangar_available_slots[i].pressed.connect(_on_hangar_crew_type.bind(i))
hangar_fleet_slots[i].pressed.connect(_on_hangar_store_type.bind(i))
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)
@ -245,6 +263,7 @@ func _ready() -> void:
transfer_button.visible = false
hangar_button.visible = false
hangar_menu.visible = false
_layout_context_buttons()
# True while a building prompt or panel owns the shared action slot, so the
@ -340,6 +359,20 @@ func _process(delta: float) -> void:
if current_building != null:
action_button.icon = _action_icon_for(current_building.building_type)
action_button.queue_redraw()
_layout_context_buttons()
func _layout_context_buttons() -> void:
# Every contextual action uses a distinct, fixed berth above Build. This
# replaces the old hand-picked offsets where Hangar overlapped Action at a
# mothership (#25).
var buttons: Array = [action_button, transfer_button]
if hangar_button.visible:
buttons.append(hangar_button)
for i in range(buttons.size()):
var button: Control = buttons[i]
button.offset_top = -720 + i * 164
button.offset_bottom = button.offset_top + 148
func _action_icon_for(type: String) -> int:
@ -672,16 +705,13 @@ func _say(message: String) -> void:
func _refresh_craft_status() -> void:
# 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 queue_text: String = "" if queued == 0 else " Queued %d" % queued
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()
]
# 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() \
@ -698,11 +728,6 @@ func _refresh_craft_status() -> void:
# where the fleet starts at exactly its cap.
craft_interceptor_button.enabled = true
craft_interceptor_button.queue_redraw()
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:
@ -995,55 +1020,67 @@ func _on_queue_slot(index: int) -> void:
# --- 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
func _on_hangar_crew_type(index: int) -> void:
var type: String = _hangar_types()[index]
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):
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)
return
if not fleet.add_ship(GameState.take_stored_vessel(stored_index)):
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
hangar_status.text = "%s joined the fleet" % GameState.VESSEL_NAMES.get(type, type)
_refresh_hangar()
func _on_hangar_store_type(index: int) -> void:
var type: String = _hangar_types()[index]
var ship_index: int = -1
for i in range(fleet.ships.size()):
if String(fleet.ships[i].vessel_type) == type:
ship_index = i
break
if ship_index < 0:
hangar_status.text = "No %s in the fleet" % GameState.VESSEL_NAMES.get(type, 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)
_refresh_hangar()
func _hangar_types() -> Array:
return [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD, GameState.VESSEL_TRANSPORTER]
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()
var types: Array = _hangar_types()
for i in range(types.size()):
var type: String = types[i]
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_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()
hangar_fleet_slots[i].queue_redraw()
func _on_formation_changed() -> void:

View file

@ -92,6 +92,23 @@ func add_ship(vessel_type: String = GameState.VESSEL_INTERCEPTOR) -> bool:
return true
func store_ship(index: int) -> String:
# A fleet always keeps one ship deployed. Removing the last one would trip
# the wiped-out-fleet safety net on the next frame and immediately spawn a
# replacement, which is the opposite of storing it at the mothership.
if ships.size() <= 1 or index < 0 or index >= ships.size():
return ""
var ship: Node = ships[index]
if not is_instance_valid(ship):
return ""
var vessel_type: String = String(ship.vessel_type)
ships.remove_at(index)
formation_offsets.remove_at(index)
ship.queue_free()
apply_formation()
return vessel_type
func apply_formation() -> void:
# Re-seats the EXISTING ships into the current GameState.formation.
# Without this, editing the layout at a command centre (or pivoting it)

152
Main.tscn
View file

@ -665,6 +665,7 @@ text = "Hangar"
horizontal_alignment = 1
[node name="ListLabel" type="Label" parent="UI/HangarMenu"]
visible = false
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
@ -692,6 +693,7 @@ horizontal_alignment = 1
text = ""
[node name="NextButton" type="Control" parent="UI/HangarMenu"]
visible = false
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
@ -705,6 +707,7 @@ icon = 6
glow_color = Color(0.55, 0.95, 0.9, 1)
[node name="CrewButton" type="Control" parent="UI/HangarMenu"]
visible = false
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
@ -718,6 +721,7 @@ icon = 2
glow_color = Color(0.45, 0.9, 0.55, 1)
[node name="HintLabel" type="Label" parent="UI/HangarMenu"]
visible = false
mouse_filter = 2
anchor_right = 1.0
anchor_top = 1.0
@ -731,6 +735,154 @@ autowrap_mode = 2
horizontal_alignment = 1
text = "Next Crew"
[node name="AvailableTitle" type="Label" parent="UI/HangarMenu"]
mouse_filter = 2
anchor_right = 1.0
offset_top = 190
offset_bottom = 240
theme_override_font_sizes/font_size = 28
text = "AVAILABLE — TAP TO CREW"
horizontal_alignment = 1
[node name="AvailableInterceptor" type="Control" parent="UI/HangarMenu"]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -250
offset_right = -130
offset_top = 255
offset_bottom = 375
script = ExtResource("10")
icon = 2
[node name="AvailableInterceptorLabel" type="Label" parent="UI/HangarMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -300
offset_right = -80
offset_top = 380
offset_bottom = 460
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="AvailableShield" type="Control" parent="UI/HangarMenu"]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -60
offset_right = 60
offset_top = 255
offset_bottom = 375
script = ExtResource("10")
icon = 2
glow_color = Color(0.45, 0.8, 1, 1)
[node name="AvailableShieldLabel" type="Label" parent="UI/HangarMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -110
offset_right = 110
offset_top = 380
offset_bottom = 460
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="AvailableTransporter" type="Control" parent="UI/HangarMenu"]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 130
offset_right = 250
offset_top = 255
offset_bottom = 375
script = ExtResource("10")
icon = 12
glow_color = Color(0.95, 0.55, 0.85, 1)
[node name="AvailableTransporterLabel" type="Label" parent="UI/HangarMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 80
offset_right = 300
offset_top = 380
offset_bottom = 460
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="FleetTitle" type="Label" parent="UI/HangarMenu"]
mouse_filter = 2
anchor_right = 1.0
offset_top = 500
offset_bottom = 550
theme_override_font_sizes/font_size = 28
text = "FLEET — TAP TO STORE"
horizontal_alignment = 1
[node name="FleetInterceptor" type="Control" parent="UI/HangarMenu"]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -250
offset_right = -130
offset_top = 565
offset_bottom = 685
script = ExtResource("10")
icon = 2
[node name="FleetInterceptorLabel" type="Label" parent="UI/HangarMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -300
offset_right = -80
offset_top = 690
offset_bottom = 770
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="FleetShield" type="Control" parent="UI/HangarMenu"]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -60
offset_right = 60
offset_top = 565
offset_bottom = 685
script = ExtResource("10")
icon = 2
glow_color = Color(0.45, 0.8, 1, 1)
[node name="FleetShieldLabel" type="Label" parent="UI/HangarMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -110
offset_right = 110
offset_top = 690
offset_bottom = 770
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="FleetTransporter" type="Control" parent="UI/HangarMenu"]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 130
offset_right = 250
offset_top = 565
offset_bottom = 685
script = ExtResource("10")
icon = 12
glow_color = Color(0.95, 0.55, 0.85, 1)
[node name="FleetTransporterLabel" type="Label" parent="UI/HangarMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 80
offset_right = 300
offset_top = 690
offset_bottom = 770
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="CloseButton" type="Control" parent="UI/HangarMenu"]
anchor_left = 1.0
anchor_right = 1.0

View file

@ -0,0 +1,86 @@
extends SceneTree
# Forgejo #23 + #25: the mothership hangar presents vessels by type, tapping
# an available type crews one, tapping a fleet type stores one, and contextual
# world buttons each get their own touch berth.
var game_state: Node
var scene: Node
var fleet: Node
var controller: Node
var stage: int = 0
var elapsed: float = 0.0
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.transporters.clear()
game_state.stored_vessels.clear()
game_state.rocks = 100
scene = load("res://Main.tscn").instantiate()
root.add_child(scene)
fleet = scene.get_node("Fleet")
controller = scene.get_node("BuildController")
func _process(delta: float) -> bool:
elapsed += delta
if stage == 0:
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
for a in get_nodes_in_group("asteroids"):
if is_instance_valid(a):
a.queue_free()
fleet.spawn_ships(1, Vector2.ZERO)
fleet.add_ship(GameState.VESSEL_SHIELD)
game_state.stored_vessels = [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD]
controller._on_hangar_pressed()
elapsed = 0.0
stage = 1
return false
if stage == 1:
if elapsed < 0.2:
return false
# Available cards name the vessel and count what can be crewed.
var available_label: Label = controller.hangar_available_labels[0]
if "Interceptor" not in available_label.text or "1 available" not in available_label.text:
_fail("available interceptor card says '%s'" % available_label.text)
return true
controller._on_hangar_crew_type(0)
if fleet.ships.size() != 3 or game_state.stored_vessels.has(GameState.VESSEL_INTERCEPTOR):
_fail("tapping an available interceptor did not crew it")
return true
controller._on_hangar_store_type(1)
if fleet.ships.size() != 2 or not game_state.stored_vessels.has(GameState.VESSEL_SHIELD):
_fail("tapping the shield fleet card did not store it")
return true
# The safety rule prevents an empty deployed fleet.
controller._on_hangar_store_type(0)
controller._on_hangar_store_type(0)
if fleet.ships.size() != 1:
_fail("hangar allowed the last deployed ship to be stored")
return true
# #25: all three contextual controls have non-overlapping vertical
# rectangles when the mothership exposes Action, Transfer and Hangar.
controller.action_button.visible = true
controller.transfer_button.visible = true
controller.hangar_button.visible = true
controller._layout_context_buttons()
var buttons: Array = [controller.action_button, controller.transfer_button, controller.hangar_button]
for i in range(buttons.size() - 1):
if buttons[i].offset_bottom > buttons[i + 1].offset_top:
_fail("context buttons %d and %d overlap" % [i, i + 1])
return true
print("hangar crews and stores typed vessels; contextual buttons have separate berths")
print("TEST_HANGAR_MANAGEMENT: PASS")
quit(0)
return true
return false
func _fail(message: String) -> void:
print("TEST_HANGAR_MANAGEMENT: FAIL (%s)" % message)
quit(1)