Refactor gameplay menus into owned scenes

This commit is contained in:
Loïc Wisniewski 2026-08-31 02:18:08 +02:00
commit a17fe88197
50 changed files with 3449 additions and 3619 deletions

View file

@ -9,6 +9,7 @@ script = ExtResource("3")
input_buffers = {
"gas": 40
}
collected_outputs = PackedStringArray("energy")
conversion_input = "gas"
conversion_output = "energy"
conversion_rate = 1.0
@ -27,6 +28,7 @@ build_resource_costs = {
action_icon = 33
menu_icon = 33
visual_scene = ExtResource("2")
capabilities = PackedStringArray("gas_burner")
logistics = SubResource("Resource_logistics")
map_color = Color(0.95, 0.35, 0.15, 1)
map_sides = 5

File diff suppressed because it is too large Load diff

View file

@ -46,9 +46,10 @@ func _initialize() -> void:
func _shown() -> Array:
var out: Array = []
for entry in controller.build_menu_entries:
if entry[1].visible:
out.append(entry)
for entry in controller.build_menu.entries:
if entry.button.visible:
out.append([entry.definition.definition_id, entry.button,
entry.label, entry.cost, entry.definition])
return out

View file

@ -105,7 +105,7 @@ func _process(delta: float) -> bool:
# --- D. locked state in the crafter UI -------------------------
controller.craft_building_id = crafter_id
controller._refresh_craft_status()
if controller.craft_vessel_buttons[FLEET_UI_INDEX].enabled:
if controller.craft_menu.vessel_buttons[FLEET_UI_INDEX].enabled:
return _fail("a locked carrier shows enabled in the crafter menu")
stage = 1
@ -126,8 +126,8 @@ func _process(delta: float) -> bool:
# --- D. unlocked state in the crafter UI ------------------------
controller._refresh_craft_status()
var craft_button: IconButton = controller.craft_vessel_buttons[FLEET_UI_INDEX]
var craft_label: Label = controller.craft_vessel_labels[FLEET_UI_INDEX]
var craft_button: IconButton = controller.craft_menu.vessel_buttons[FLEET_UI_INDEX]
var craft_label: Label = controller.craft_menu.vessel_labels[FLEET_UI_INDEX]
if not craft_button.enabled:
return _fail("an unlocked carrier still shows disabled in the crafter menu")
if craft_button.icon != definition.icon or craft_button.glow_color != definition.glow_color:
@ -148,8 +148,8 @@ func _process(delta: float) -> bool:
# --- D. hangar display -------------------------------------------
controller._refresh_hangar()
var available_slot: IconButton = controller.hangar_available_slots[FLEET_UI_INDEX]
var available_label: Label = controller.hangar_available_labels[FLEET_UI_INDEX]
var available_slot: IconButton = controller.hangar_menu.available_slots[FLEET_UI_INDEX]
var available_label: Label = controller.hangar_menu.available_labels[FLEET_UI_INDEX]
if available_slot.icon != definition.icon or available_slot.glow_color != definition.glow_color:
return _fail("hangar carrier slot does not match its definition's icon/colour")
var expected_available: String = "%s\n1 available" % definition.display_name
@ -177,8 +177,8 @@ func _process(delta: float) -> bool:
# --- D. ship lab display -------------------------------------------
controller.lab_vessel = GameState.VESSEL_CARRIER
controller._refresh_lab()
var lab_button: IconButton = controller.lab_vessel_buttons[LAB_UI_INDEX]
var lab_label: Label = controller.lab_vessel_labels[LAB_UI_INDEX]
var lab_button: IconButton = controller.lab_menu.vessel_buttons[LAB_UI_INDEX]
var lab_label: Label = controller.lab_menu.vessel_labels[LAB_UI_INDEX]
if lab_button.icon != definition.icon or lab_button.glow_color != definition.glow_color:
return _fail("ship lab carrier slot does not match its definition's icon/colour")
if lab_label.text != definition.display_name:

View file

@ -67,7 +67,7 @@ func _tap(button: Control) -> void:
func _first_line() -> String:
return controller.craft_status.text.split("\n")[0]
return controller.craft_menu.status.text.split("\n")[0]
func _process(delta: float) -> bool:

View file

@ -55,7 +55,7 @@ func _process(delta: float) -> bool:
or controller.landing_button.visible:
return _fail("landing was not folded into the shared action control")
controller._on_transfer_pressed()
if not controller.transfer_menu.visible or controller.transfer_list.extractor_index != extractor_index:
if not controller.transfer_menu.visible or controller.transfer_menu.resource_list.extractor_index != extractor_index:
return _fail("transfer panel did not bind to the extractor")
var before: int = state.fleet_resource(resource_type)
controller._on_resource_transfer(resource_type, false, 5)

View file

@ -45,7 +45,7 @@ func _process(delta: float) -> bool:
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]
var available_label: Label = controller.hangar_menu.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

View file

@ -69,34 +69,34 @@ func _process(delta: float) -> bool:
# Gas Bomber is named after a resource and always will be, and
# what #111 removed was the PRICE spelled out underneath.
var fleet_defs: Array = VesselDefinitions.fleet()
for i in range(mini(controller.craft_vessel_labels.size(), fleet_defs.size())):
var caption: String = String(controller.craft_vessel_labels[i].text)
for i in range(mini(controller.craft_menu.vessel_labels.size(), fleet_defs.size())):
var caption: String = String(controller.craft_menu.vessel_labels[i].text)
var expected: String = String(fleet_defs[i].display_name)
if caption != expected:
return _fail("a craft caption reads \"%s\", expected just \"%s\"" \
% [caption, expected])
# Captions must not sit on top of one another, which is what
# made the old single row unreadable.
for i in range(controller.craft_vessel_labels.size()):
for j in range(i + 1, controller.craft_vessel_labels.size()):
var a: Control = controller.craft_vessel_labels[i]
var b: Control = controller.craft_vessel_labels[j]
for i in range(controller.craft_menu.vessel_labels.size()):
for j in range(i + 1, controller.craft_menu.vessel_labels.size()):
var a: Control = controller.craft_menu.vessel_labels[i]
var b: Control = controller.craft_menu.vessel_labels[j]
if Rect2(a.position, a.size).intersects(Rect2(b.position, b.size)):
return _fail("craft captions %d and %d overlap" % [i, j])
print("craft captions: %d, one line each, none overlapping" \
% controller.craft_vessel_labels.size())
% controller.craft_menu.vessel_labels.size())
# --- B. and the price is really there ---------------------
if controller.craft_price_strips.size() != controller.craft_vessel_buttons.size():
if controller.craft_menu.price_tags.size() != controller.craft_menu.vessel_buttons.size():
return _fail("%d price strips for %d vessels" \
% [controller.craft_price_strips.size(),
controller.craft_vessel_buttons.size()])
% [controller.craft_menu.price_tags.size(),
controller.craft_menu.vessel_buttons.size()])
var priced: int = 0
var stacked: int = 0
for i in range(controller.craft_price_strips.size()):
for i in range(controller.craft_menu.price_tags.size()):
var definition: Resource = VesselDefinitions.fleet()[i]
var costs: Dictionary = game_state.product_costs(definition.definition_id)
var strip: Control = controller.craft_price_strips[i]
var strip: Control = controller.craft_menu.price_tags[i]
if costs.is_empty():
continue
if strip.is_free():
@ -122,7 +122,7 @@ func _process(delta: float) -> bool:
% [definition.definition_id, box.position.x, box.end.x, strip.size.x])
# ...and the price sits with the button it is about, not
# adrift below the caption.
var button: Control = controller.craft_vessel_buttons[i]
var button: Control = controller.craft_menu.vessel_buttons[i]
var gap: float = strip.position.y - (button.position.y + button.size.y)
if gap > 80.0:
return _fail("%s's price sits %.0fpx below its button" \
@ -135,10 +135,10 @@ func _process(delta: float) -> bool:
% [priced, stacked])
# --- F. #147 - and the rows do not run into each other -------
for i in range(controller.craft_vessel_buttons.size()):
for j in range(i + 1, controller.craft_vessel_buttons.size()):
for part in [controller.craft_vessel_buttons,
controller.craft_vessel_labels, controller.craft_price_strips]:
for i in range(controller.craft_menu.vessel_buttons.size()):
for j in range(i + 1, controller.craft_menu.vessel_buttons.size()):
for part in [controller.craft_menu.vessel_buttons,
controller.craft_menu.vessel_labels, controller.craft_menu.price_tags]:
if i >= part.size() or j >= part.size():
continue
var a: Control = part[i]
@ -146,9 +146,9 @@ func _process(delta: float) -> bool:
if Rect2(a.position, a.size).intersects(Rect2(b.position, b.size)):
return _fail("craft entries %d and %d overlap" % [i, j])
# A price must not reach the button of the row below it.
var mine: Control = controller.craft_price_strips[i]
for k in range(controller.craft_vessel_buttons.size()):
var other: Control = controller.craft_vessel_buttons[k]
var mine: Control = controller.craft_menu.price_tags[i]
for k in range(controller.craft_menu.vessel_buttons.size()):
var other: Control = controller.craft_menu.vessel_buttons[k]
if other.position.y > mine.position.y \
and Rect2(mine.position, mine.size).intersects(
Rect2(other.position, other.size)):
@ -157,26 +157,26 @@ func _process(delta: float) -> bool:
# --- C. the hangar ---------------------------------------
controller._layout_hangar()
if controller.hangar_divider_y <= 0.0:
if controller.hangar_menu.divider_y <= 0.0:
return _fail("the hangar laid out with no divider between its halves")
var available_bottom: float = 0.0
var fleet_top: float = 1.0e9
for label in controller.hangar_available_labels:
for label in controller.hangar_menu.available_labels:
available_bottom = maxf(available_bottom, label.position.y + label.size.y)
for slot in controller.hangar_fleet_slots:
for slot in controller.hangar_menu.fleet_slots:
fleet_top = minf(fleet_top, slot.position.y)
if not (controller.hangar_divider_y > available_bottom - 40.0
and controller.hangar_divider_y < fleet_top):
if not (controller.hangar_menu.divider_y > available_bottom - 40.0
and controller.hangar_menu.divider_y < fleet_top):
return _fail("the divider at %.0f is not between the two grids (%.0f / %.0f)" \
% [controller.hangar_divider_y, available_bottom, fleet_top])
% [controller.hangar_menu.divider_y, available_bottom, fleet_top])
# Two lines of caption. The font is 24-ish, so anything under
# about 44 cannot show a second line at all.
var caption_height: float = controller.hangar_available_labels[0].size.y
var caption_height: float = controller.hangar_menu.available_labels[0].size.y
if caption_height < 44.0:
return _fail("hangar captions are %.0fpx tall - a two-word name is clipped" \
% caption_height)
print("hangar: %.0fpx captions, divider at %.0f between %.0f and %.0f" \
% [caption_height, controller.hangar_divider_y, available_bottom, fleet_top])
% [caption_height, controller.hangar_menu.divider_y, available_bottom, fleet_top])
# --- D. #143 - and the caption stays in its box --------------
# The caption is two lines before autowrap and three after it in
@ -185,8 +185,8 @@ func _process(delta: float) -> bool:
# the box now, and the box clips, so nothing can reach the next
# row however the vessel names grow.
controller._refresh_hangar()
for pair in [[controller.hangar_available_labels, controller.hangar_available_slots],
[controller.hangar_fleet_labels, controller.hangar_fleet_slots]]:
for pair in [[controller.hangar_menu.available_labels, controller.hangar_menu.available_slots],
[controller.hangar_menu.fleet_labels, controller.hangar_menu.fleet_slots]]:
for i in range(pair[0].size()):
var caption: Label = pair[0][i]
if not caption.clip_text:
@ -205,7 +205,7 @@ func _process(delta: float) -> bool:
and Rect2(caption.position, caption.size).intersects(below):
return _fail("caption \"%s\" runs into the icon below it" \
% caption.text.replace("\n", " / "))
var probe: Label = controller.hangar_available_labels[0]
var probe: Label = controller.hangar_menu.available_labels[0]
print("captions fitted to %dpx and clipped; \"%s\" fits its %.0fpx box" \
% [probe.get_theme_font_size("font_size"),
probe.text.replace("\n", " / "), probe.size.y])

View file

@ -107,12 +107,12 @@ func _process(delta: float) -> bool:
# --- B. one price widget everywhere ----------------------
var tags: Array = []
for entry in controller.build_menu_entries:
tags.append(entry[3])
tags.append_array(controller.craft_price_strips)
tags.append_array(controller.lab_price_tags)
tags.append(controller.info_cost)
tags.append(controller.skill_cost)
for entry in controller.build_menu.entries:
tags.append(entry.cost)
tags.append_array(controller.craft_menu.price_tags)
tags.append_array(controller.lab_menu.price_tags)
tags.append(controller.build_menu.info_cost)
tags.append(controller.skill_menu.cost)
if tags.size() < 15:
return _fail("only %d price widgets found - the menus cannot all be covered" % tags.size())
for tag in tags:
@ -124,7 +124,7 @@ func _process(delta: float) -> bool:
# --- C. have over cost, smaller, red when short ----------
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 3)
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 0)
var probe: Control = controller.info_cost
var probe: Control = controller.build_menu.info_cost
probe.show_price({GameState.RESOURCE_ORE: 25})
if probe.priced(GameState.RESOURCE_ORE) != 25:
return _fail("the tag does not carry the price it was given")
@ -165,8 +165,8 @@ func _process(delta: float) -> bool:
if String(node.desc).length() > longest.length():
longest = String(node.desc)
controller._refresh_skill_menu(String(node.id))
var detail: Rect2 = _rect(controller.skill_detail)
var price: Rect2 = _rect(controller.skill_cost)
var detail: Rect2 = _rect(controller.skill_menu.detail)
var price: Rect2 = _rect(controller.skill_menu.cost)
if detail.intersects(price):
return _fail("the explanation (%s) overlaps the price (%s)" % [detail, price])
if detail.position.x >= price.position.x:
@ -179,11 +179,11 @@ func _process(delta: float) -> bool:
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 0)
game_state.unlocked.clear()
controller._refresh_skill_menu("shipyards")
if controller.skill_cost.is_affordable():
if controller.skill_menu.cost.is_affordable():
return _fail("a node reads as affordable with no ore at all")
if controller.skill_status.text.to_lower().contains("not enough"):
if controller.skill_menu.status.text.to_lower().contains("not enough"):
return _fail("the shortfall is still written out as well: '%s'" \
% controller.skill_status.text)
% controller.skill_menu.status.text)
print("the shortfall is shown once, on the price, and not repeated in words")
print("TEST_MENU_PRICE_UNIFICATION: PASS")

View file

@ -0,0 +1,42 @@
extends SceneTree
const MENUS: Array[String] = [
"BuildMenu", "CraftMenu", "LabMenu", "TransferMenu", "HangarMenu",
"FormationMenu", "SkillMenu", "PlanningMenu", "GarrisonMenu",
]
func _initialize() -> void:
call_deferred("_verify")
func _verify() -> void:
var packed: PackedScene = load("res://world/Main.tscn")
if packed == null:
return _fail("Main.tscn did not load")
var main: Node = packed.instantiate()
root.add_child(main)
for menu_name in MENUS:
var menu: Node = main.get_node_or_null("UI/%s" % menu_name)
if menu == null:
return _fail("%s is missing" % menu_name)
var expected: String = "res://ui/menus/%s.tscn" % menu_name
if menu.scene_file_path != expected:
return _fail("%s is inline in Main.tscn instead of instancing %s" \
% [menu_name, expected])
if menu.get_script() == null:
return _fail("%s has no owning script" % menu_name)
var source: String = FileAccess.get_file_as_string(
"res://controllers/BuildController.gd")
for menu_name in MENUS:
if "../UI/%s/" % menu_name in source:
return _fail("BuildController reaches into %s children" % menu_name)
print("%d menu scenes own their controls; BuildController uses root APIs" % MENUS.size())
print("TEST_MENU_SCENE_BOUNDARIES: PASS")
quit(0)
func _fail(message: String) -> void:
print("TEST_MENU_SCENE_BOUNDARIES: FAIL (%s)" % message)
quit(1)

View file

@ -0,0 +1 @@
uid://bufrmk7en8qxd

View file

@ -253,7 +253,7 @@ func _process(delta: float) -> bool:
if game_state.buildings.size() != before + 1:
return _fail("building a mothership from the fleet's own hold did not place one (%d -> %d): %s" \
% [before, game_state.buildings.size(), build_controller.build_status.text])
% [before, game_state.buildings.size(), build_controller.build_menu.status.text])
# Checked as a TOTAL, not as the hold alone: a mothership standing
# where the fleet is starts taking delivery of the hold on the
# very next tick, so ore legitimately moves from hold to bank

View file

@ -154,13 +154,13 @@ func _process(delta: float) -> bool:
controller._refresh_skill_menu(second)
# #134: the refusal is shown on the price rather than written out in a
# sentence below it - what you hold over what it costs, in red.
if controller.skill_cost.is_affordable():
if controller.skill_menu.cost.is_affordable():
return _fail("a node the player cannot pay for reads as affordable")
if not controller.skill_cost.is_short(GameState.RESOURCE_ORE):
if not controller.skill_menu.cost.is_short(GameState.RESOURCE_ORE):
return _fail("the ore the player does not have is not marked short")
print("unaffordable node refused, and its price says so: %d/%d ore" \
% [controller.skill_cost.held(GameState.RESOURCE_ORE),
controller.skill_cost.priced(GameState.RESOURCE_ORE)])
% [controller.skill_menu.cost.held(GameState.RESOURCE_ORE),
controller.skill_menu.cost.priced(GameState.RESOURCE_ORE)])
print("TEST_RESEARCH_DOUBLE_TAP: PASS")
quit(0)

View file

@ -98,9 +98,9 @@ func _process(delta: float) -> bool:
controller._confirm_build()
if game_state.extractors.size() != before:
return _fail("an extractor was built on a research_lab planet")
if not ("nothing to extract" in controller.build_status.text):
return _fail("refusal did not explain itself, status was '%s'" % controller.build_status.text)
print("extractor refused on a research_lab planet, explained: '%s'" % controller.build_status.text)
if not ("nothing to extract" in controller.build_menu.status.text):
return _fail("refusal did not explain itself, status was '%s'" % controller.build_menu.status.text)
print("extractor refused on a research_lab planet, explained: '%s'" % controller.build_menu.status.text)
print("TEST_RESEARCH_LAB_EXTRACTOR_GATE: PASS")
quit(0)

View file

@ -127,16 +127,14 @@ func _process(delta: float) -> bool:
% [controller.transfer_building_id, crafter_id])
# --- 6. the shared bar follows the panel --------------------
for bar in controller.resource_bars:
_expect(bar.building_id == crafter_id,
"a resource bar is reporting building %d, not the crafter %d"
% [bar.building_id, crafter_id])
_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_status.text,
"unloading an empty hold said: %s" % controller.transfer_status.text)
_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

View file

@ -125,19 +125,19 @@ func _process(delta: float) -> bool:
return true
# The tree is a tall, vertically scrolling canvas. A branch keeps a
# stable lane as it rises, and dragging exposes higher depths.
var freight: Vector2 = controller.skill_tree.node_screen_pos(game_state.skill_node("freight"))
var yards: Vector2 = controller.skill_tree.node_screen_pos(game_state.skill_node("freight_yards"))
var freight: Vector2 = controller.skill_menu.tree.node_screen_pos(game_state.skill_node("freight"))
var yards: Vector2 = controller.skill_menu.tree.node_screen_pos(game_state.skill_node("freight_yards"))
if absf(freight.x - yards.x) > 1.0 or yards.y >= freight.y:
print("TEST_SKILL_TREE: FAIL (freight branch does not rise vertically)")
quit(1)
return true
var before_scroll: float = controller.skill_tree.scroll_y
var before_scroll: float = controller.skill_menu.tree.scroll_y
var drag: InputEventScreenDrag = InputEventScreenDrag.new()
# Start at the root at the bottom; pull the canvas down to reveal
# the branches rising above it.
drag.relative = Vector2(0.0, 180.0)
controller.skill_tree.handle_drag(drag)
if controller.skill_tree.scroll_y >= before_scroll:
controller.skill_menu.tree.handle_drag(drag)
if controller.skill_menu.tree.scroll_y >= before_scroll:
print("TEST_SKILL_TREE: FAIL (tree did not scroll upward)")
quit(1)
return true

View file

@ -1,20 +0,0 @@
extends Control
# #120: the rule between the hangar's two halves.
#
# The panel itself draws it rather than a separate node, because the y comes
# from BuildController._layout_hangar() - which is also what places the two
# grids - and a divider that had to be positioned independently would drift
# away from the sections it divides the first time either grid changed size.
const RULE_COLOR: Color = Color(0.45, 0.9, 0.55)
@onready var controller: Node = get_node("../../BuildController")
func _draw() -> void:
if controller == null or controller.hangar_divider_y <= 0.0:
return
var y: float = controller.hangar_divider_y
NeonDraw.draw_glowing_line(self, Vector2(24.0, y), Vector2(size.x - 24.0, y),
RULE_COLOR, 0.5, 0.4)

View file

@ -1 +0,0 @@
uid://bwqdhbksv0xgf

216
ui/menus/BuildMenu.gd Normal file
View file

@ -0,0 +1,216 @@
extends "res://ui/menus/ModalMenu.gd"
signal building_selected(type: String)
signal build_confirmed
const BUILDING_DEFINITIONS = preload("res://buildings/BuildingDefinitions.gd")
const MENU_LAYOUT = preload("res://ui/menus/MenuLayout.gd")
const BUTTON_HALF: float = 64.0
const BUTTON_HEIGHT: float = 100.0
const COST_HALF: float = 74.0
const COST_HEIGHT: float = 86.0
const LABEL_OFFSET: float = 106.0
const LABEL_GAP: float = 4.0
const NATURAL_SPACING: float = 280.0
const MIN_SPACING: float = BUTTON_HALF * 2.0 + 12.0
const LABEL_SHARE: float = 0.46
const LABEL_HEIGHT: float = 52.0
const LABEL_PADDING: float = 10.0
const COST_PADDING: float = 12.0
const SIDE_MARGIN: float = 24.0
const MAX_COLUMNS: int = 6
@onready var status: Label = $StatusLabel
@onready var info_popup: Control = $InfoPopup
@onready var info_title: Label = $InfoPopup/Panel/TitleLabel
@onready var info_message: Label = $InfoPopup/Panel/MessageLabel
@onready var info_cost: Control = $InfoPopup/Panel/CostNumber
@onready var info_build_button: Control = $InfoPopup/Panel/BuildButton
@onready var info_close_button: Control = $InfoPopup/Panel/CloseButton
var entries: Array = []
func _ready() -> void:
super._ready()
_configure_entries()
info_close_button.pressed.connect(close_info)
info_build_button.pressed.connect(func(): build_confirmed.emit())
info_popup.visible = false
func _configure_entries() -> void:
entries.clear()
for definition in BUILDING_DEFINITIONS.all():
if definition.menu_button_name == "":
continue
var button: Control = get_node(String(definition.menu_button_name))
var label: Label = get_node(String(definition.menu_label_name))
var cost_tag: Control = get_node(String(definition.menu_cost_name))
button.icon = definition.menu_icon
button.pressed.connect(
func(): building_selected.emit(String(definition.definition_id)))
label.text = definition.display_name
cost_tag.show_price(GameState.building_costs(String(definition.definition_id)))
entries.append({"definition": definition, "button": button,
"label": label, "cost": cost_tag})
func open_menu() -> void:
super.open_menu()
close_info()
func close_menu() -> void:
close_info()
super.close_menu()
func close_info() -> void:
info_popup.visible = false
func show_info(title: String, message: String, costs: Dictionary,
can_build: bool) -> void:
info_title.text = title
info_message.text = message
info_cost.show_price(costs)
info_build_button.visible = can_build
info_popup.visible = true
func set_status(message: String) -> void:
status.text = message
func refresh(planet: Node2D) -> void:
_layout_entries()
refresh_option_states(planet)
if planet == null:
status.text = ""
elif not GameState.is_planet_conquered(planet.planet_id):
status.text = "Over %s - capture it before building an extractor" % planet.planet_name
else:
status.text = "Over %s - an extractor can be built here" % planet.planet_name
func refresh_option_states(planet: Node2D) -> void:
for entry in entries:
var definition: Resource = entry.definition
entry.cost.show_price(GameState.building_costs(String(definition.definition_id)))
var enabled: bool = GameState.is_building_unlocked(definition.definition_id)
if definition.placement_mode == "planet":
enabled = enabled and planet != null \
and GameState.is_planet_conquered(planet.planet_id)
entry.button.enabled = enabled
entry.button.queue_redraw()
func menu_band() -> Vector2:
return MENU_LAYOUT.grid_band(status.offset_bottom, size.y)
func menu_column_spacing(columns: int) -> float:
var room: float = size.x - SIDE_MARGIN * 2.0
if room <= 0.0:
room = 672.0
return maxf(MIN_SPACING, minf(NATURAL_SPACING, room / float(columns)))
func menu_label_half(columns: int) -> float:
return menu_column_spacing(columns) * LABEL_SHARE
func measured_label_height(label: Label, text: String, columns: int) -> float:
var font: Font = label.get_theme_font("font")
var font_size: int = label.get_theme_font_size("font_size")
if font == null or font_size <= 0 or text == "":
return LABEL_HEIGHT
var height: float = font.get_multiline_string_size(text,
HORIZONTAL_ALIGNMENT_CENTER, menu_label_half(columns) * 2.0, font_size).y
return maxf(LABEL_HEIGHT, height + LABEL_PADDING)
func menu_label_height(columns: int) -> float:
var tallest: float = LABEL_HEIGHT
for entry in entries:
if entry.label.visible:
tallest = maxf(tallest,
measured_label_height(entry.label, entry.label.text, columns))
return tallest
func menu_cost_height() -> float:
var tallest: float = COST_HEIGHT
for entry in entries:
if entry.cost.visible and entry.cost.has_method("wanted_height"):
tallest = maxf(tallest, float(entry.cost.wanted_height()) + COST_PADDING)
return tallest
func menu_cost_offset(columns: int) -> float:
return LABEL_OFFSET + menu_label_height(columns) + LABEL_GAP
func menu_row_height(columns: int) -> float:
return menu_cost_offset(columns) + menu_cost_height()
func menu_columns(count: int) -> int:
var widest: int = maxi(mini(MAX_COLUMNS,
int(floor((size.x - SIDE_MARGIN * 2.0) / MIN_SPACING))), 2)
for columns in range(2, widest + 1):
if _rows_fit(int(ceil(float(count) / float(columns))), columns):
return columns
return widest
func menu_rows(count: int) -> int:
var columns: int = menu_columns(count)
return maxi(int(ceil(float(count) / float(columns))), 1)
func _rows_fit(rows: int, columns: int) -> bool:
var height: float = menu_row_height(columns)
var band: Vector2 = menu_band()
if rows <= 1:
return band.y - band.x >= height
return (band.y - band.x - height) / float(rows - 1) >= height
func menu_row_top(row: int, rows: int, columns: int) -> float:
return MENU_LAYOUT.grid_row_top(row, rows, menu_row_height(columns),
menu_band(), NATURAL_SPACING)
func _layout_entries() -> void:
var shown: Array = []
for entry in entries:
var definition: Resource = entry.definition
var entry_visible: bool = GameState.is_building_unlocked(definition.definition_id)
entry.button.visible = entry_visible
entry.label.visible = entry_visible
entry.cost.visible = entry_visible
if entry_visible:
entry.cost.show_price(GameState.building_costs(String(definition.definition_id)))
shown.append(entry)
var columns: int = menu_columns(shown.size())
var rows: int = menu_rows(shown.size())
var spacing: float = menu_column_spacing(columns)
var label_half: float = menu_label_half(columns)
var label_height: float = menu_label_height(columns)
var cost_offset: float = menu_cost_offset(columns)
var cost_height: float = menu_cost_height()
for i in range(shown.size()):
var row: int = i / columns
var in_row: int = mini(shown.size() - row * columns, columns)
var column: int = i % columns
var cx: float = (float(column) - (float(in_row) - 1.0) * 0.5) * spacing
var top: float = menu_row_top(row, rows, columns)
MENU_LAYOUT.place_entry(shown[i].button, cx, top, BUTTON_HALF, BUTTON_HEIGHT)
MENU_LAYOUT.place_entry(shown[i].label, cx, top + LABEL_OFFSET,
label_half, label_height)
MENU_LAYOUT.place_entry(shown[i].cost, cx, top + cost_offset,
COST_HALF, cost_height)

View file

@ -0,0 +1 @@
uid://b4fmal0r6so5

544
ui/menus/BuildMenu.tscn Normal file
View file

@ -0,0 +1,544 @@
[gd_scene load_steps=6 format=3]
[ext_resource type="Script" path="res://ui/IconButton.gd" id="10"]
[ext_resource type="Script" path="res://ui/NeonPanel.gd" id="20"]
[ext_resource type="Script" path="res://ui/ResourceBar.gd" id="24"]
[ext_resource type="Script" path="res://ui/PriceTag.gd" id="price_tag"]
[ext_resource type="Script" path="res://ui/menus/BuildMenu.gd" id="menu_script"]
[node name="BuildMenu" type="Control" groups=["modal_menus"]]
visible = false
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("menu_script")
[node name="Backdrop" type="ColorRect" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.03, 0.03, 0.08, 0.94)
[node name="TitleLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24
offset_top = 20
offset_right = -24
offset_bottom = 140
text = "Build"
horizontal_alignment = 1
[node name="Bar" type="Control" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24.0
offset_top = 148
offset_right = -24.0
offset_bottom = 330
script = ExtResource("24")
[node name="StatusLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24
offset_top = 338
offset_right = -24
offset_bottom = 418
theme_override_font_sizes/font_size = 40
autowrap_mode = 2
text = ""
horizontal_alignment = 1
[node name="MothershipButton" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -204
offset_right = -76
offset_top = -300
offset_bottom = -172
script = ExtResource("10")
icon = 8
glow_color = Color(0.45, 0.9, 0.55, 1)
[node name="MothershipLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -290
offset_right = 10
offset_top = -166
offset_bottom = -100
theme_override_font_sizes/font_size = 32
autowrap_mode = 2
text = "Mothership"
horizontal_alignment = 1
[node name="MothershipCost" type="Control" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -200
offset_right = -80
offset_top = -96
offset_bottom = -44
script = ExtResource("price_tag")
[node name="CrafterButton" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = 76
offset_right = 204
offset_top = -300
offset_bottom = -172
script = ExtResource("10")
icon = 9
glow_color = Color(1, 0.72, 0.25, 1)
[node name="CrafterLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -10
offset_right = 290
offset_top = -166
offset_bottom = -100
theme_override_font_sizes/font_size = 32
autowrap_mode = 2
text = "Ship Crafter"
horizontal_alignment = 1
[node name="CrafterCost" type="Control" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = 80
offset_right = 200
offset_top = -96
offset_bottom = -44
script = ExtResource("price_tag")
[node name="CommandButton" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -204
offset_right = -76
offset_top = -10
offset_bottom = 118
script = ExtResource("10")
icon = 10
glow_color = Color(0.55, 0.6, 1, 1)
[node name="CommandLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -290
offset_right = 10
offset_top = 124
offset_bottom = 190
theme_override_font_sizes/font_size = 32
autowrap_mode = 2
text = "Command Center"
horizontal_alignment = 1
[node name="CommandCost" type="Control" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -200
offset_right = -80
offset_top = 194
offset_bottom = 246
script = ExtResource("price_tag")
[node name="ExtractorButton" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = 76
offset_right = 204
offset_top = -10
offset_bottom = 118
script = ExtResource("10")
icon = 11
glow_color = Color(0, 0.62, 0.7, 1)
[node name="ExtractorLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -10
offset_right = 290
offset_top = 124
offset_bottom = 190
theme_override_font_sizes/font_size = 32
autowrap_mode = 2
text = "Extractor"
horizontal_alignment = 1
[node name="ExtractorCost" type="Control" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = 80
offset_right = 200
offset_top = 194
offset_bottom = 246
script = ExtResource("price_tag")
[node name="TransportHubButton" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -64
offset_right = 64
offset_top = 280
offset_bottom = 408
script = ExtResource("10")
icon = 12
glow_color = Color(0.95, 0.55, 0.85, 1)
[node name="TransportHubLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -150
offset_right = 150
offset_top = 414
offset_bottom = 480
theme_override_font_sizes/font_size = 32
autowrap_mode = 2
text = "Transport Hub"
horizontal_alignment = 1
[node name="TransportHubCost" type="Control" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -60
offset_right = 60
offset_top = 484
offset_bottom = 536
script = ExtResource("price_tag")
[node name="ShipLabButton" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = 76
offset_right = 204
offset_top = 280
offset_bottom = 408
script = ExtResource("10")
icon = 13
glow_color = Color(0.55, 0.95, 0.9, 1)
[node name="ShipLabLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -10
offset_right = 290
offset_top = 414
offset_bottom = 480
theme_override_font_sizes/font_size = 32
autowrap_mode = 2
text = "Ship Lab"
horizontal_alignment = 1
[node name="ShipLabCost" type="Control" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = 80
offset_right = 200
offset_top = 484
offset_bottom = 536
script = ExtResource("price_tag")
[node name="GasBurnerButton" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -64
offset_right = 64
offset_top = 570
offset_bottom = 698
script = ExtResource("10")
icon = 33
glow_color = Color(0.95, 0.35, 0.15, 1)
[node name="GasBurnerLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -150
offset_right = 150
offset_top = 704
offset_bottom = 770
theme_override_font_sizes/font_size = 32
autowrap_mode = 2
text = "Gas Burner"
horizontal_alignment = 1
[node name="GasBurnerCost" type="Control" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -60
offset_right = 60
offset_top = 774
offset_bottom = 826
script = ExtResource("price_tag")
[node name="OutpostButton" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -64
offset_right = 64
offset_top = 830
offset_bottom = 958
script = ExtResource("10")
icon = 40
glow_color = Color(0.72, 0.86, 0.45, 1)
[node name="OutpostLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -150
offset_right = 150
offset_top = 964
offset_bottom = 1030
theme_override_font_sizes/font_size = 32
autowrap_mode = 2
text = "Outpost"
horizontal_alignment = 1
[node name="OutpostCost" type="Control" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -60
offset_right = 60
offset_top = 1034
offset_bottom = 1086
script = ExtResource("price_tag")
[node name="PlanningButton" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -64
offset_right = 64
offset_top = 1090
offset_bottom = 1218
script = ExtResource("10")
icon = 41
glow_color = Color(0.62, 0.74, 0.92, 1)
[node name="PlanningLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -150
offset_right = 150
offset_top = 1224
offset_bottom = 1290
theme_override_font_sizes/font_size = 32
autowrap_mode = 2
text = "Planning Center"
horizontal_alignment = 1
[node name="PlanningCost" type="Control" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -60
offset_right = 60
offset_top = 1294
offset_bottom = 1346
script = ExtResource("price_tag")
[node name="WorkshopButton" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -64
offset_right = 64
offset_top = 1350
offset_bottom = 1478
script = ExtResource("10")
icon = 42
glow_color = Color(0.5, 0.95, 0.8, 1)
[node name="WorkshopLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -150
offset_right = 150
offset_top = 1484
offset_bottom = 1550
theme_override_font_sizes/font_size = 32
autowrap_mode = 2
text = "Spacial Workshop"
horizontal_alignment = 1
[node name="WorkshopCost" type="Control" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -60
offset_right = 60
offset_top = 1554
offset_bottom = 1606
script = ExtResource("price_tag")
[node name="CloseButton" type="Control" parent="."]
anchor_left = 1.0
anchor_right = 1.0
offset_left = -172
offset_right = -24
offset_top = 24
offset_bottom = 172
script = ExtResource("10")
icon = 1
[node name="InfoPopup" type="Control" parent="." groups=["modal_menus"]]
visible = false
anchor_right = 1.0
anchor_bottom = 1.0
[node name="Dim" type="ColorRect" parent="InfoPopup"]
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.01, 0.01, 0.03, 0.55)
[node name="Panel" type="Control" parent="InfoPopup"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -352
offset_right = 352
offset_top = -600
offset_bottom = 600
script = ExtResource("20")
glow_color = Color(0, 0.62, 0.7, 1)
[node name="TitleLabel" type="Label" parent="InfoPopup/Panel"]
mouse_filter = 2
anchor_right = 1.0
offset_left = 44
offset_right = -44
offset_top = 30
offset_bottom = 150
text = ""
horizontal_alignment = 1
[node name="MessageLabel" type="Label" parent="InfoPopup/Panel"]
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 44
offset_right = -44
offset_top = 160
offset_bottom = -440
theme_override_font_sizes/font_size = 48
text = ""
horizontal_alignment = 1
vertical_alignment = 1
autowrap_mode = 2
[node name="CostNumber" type="Control" parent="InfoPopup/Panel"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = -70
offset_right = 90
offset_top = -424
offset_bottom = -334
script = ExtResource("price_tag")
[node name="BuildButton" type="Control" parent="InfoPopup/Panel"]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = -200
offset_right = -40
offset_top = -300
offset_bottom = -140
script = ExtResource("10")
icon = 5
glow_color = Color(0.45, 0.9, 0.55, 1)
[node name="CloseButton" type="Control" parent="InfoPopup/Panel"]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 40
offset_right = 200
offset_top = -300
offset_bottom = -140
script = ExtResource("10")
icon = 1
glow_color = Color(0, 0.62, 0.7, 1)

144
ui/menus/CraftMenu.gd Normal file
View file

@ -0,0 +1,144 @@
extends "res://ui/menus/ModalMenu.gd"
signal craft_requested(vessel_type: String)
signal queue_cancel_requested(index: int)
const VESSEL_DEFINITIONS = preload("res://vessels/VesselDefinitions.gd")
const MENU_LAYOUT = preload("res://ui/menus/MenuLayout.gd")
const PRICE_TAG = preload("res://ui/PriceTag.gd")
const COLUMNS := 3
const ROW_PITCH := 260.0
const COLUMN_X := 190.0
const BUTTON_HALF := 56.0
const BUTTON_HEIGHT := 100.0
const LABEL_HALF := 92.0
const LABEL_HEIGHT := 44.0
const LABEL_OFFSET := 106.0
const PRICE_HALF := 92.0
const PRICE_HEIGHT := 86.0
const PRICE_OFFSET := 154.0
@onready var status: Label = $StatusLabel
@onready var queue_detail: Label = $QueueDetail
@onready var resource_bar: Control = $Bar
@onready var vessel_buttons: Array = [$InterceptorButton, $ShieldButton,
$GasBomberButton, $CarrierButton, $LaserButton, $DistorderButton]
@onready var vessel_labels: Array = [$InterceptorLabel, $ShieldLabel,
$GasBomberLabel, $CarrierLabel, $LaserLabel, $DistorderLabel]
@onready var queue_slots: Array = [$QueueSlot0, $QueueSlot1, $QueueSlot2, $QueueSlot3]
var building_id: int = 0
var message: String = ""
var message_until: int = 0
var price_tags: Array = []
func _ready() -> void:
super._ready()
_build_price_tags()
for i in range(mini(vessel_buttons.size(), VESSEL_DEFINITIONS.fleet().size())):
var definition: Resource = VESSEL_DEFINITIONS.fleet()[i]
vessel_buttons[i].pressed.connect(
func(): craft_requested.emit(String(definition.definition_id)))
for i in range(queue_slots.size()):
queue_slots[i].pressed.connect(func(): queue_cancel_requested.emit(i))
_layout_entries()
func open_for(next_building_id: int) -> void:
building_id = next_building_id
resource_bar.building_id = building_id
open_menu()
refresh()
func close_menu() -> void:
super.close_menu()
building_id = 0
func say(next_message: String) -> void:
message = next_message
message_until = Time.get_ticks_msec() + 3000
refresh()
func refresh() -> void:
status.text = ""
var definitions: Array = VESSEL_DEFINITIONS.fleet()
for i in range(mini(definitions.size(), vessel_buttons.size())):
var definition: Resource = definitions[i]
var button: Control = vessel_buttons[i]
vessel_labels[i].text = String(definition.display_name)
if i < price_tags.size():
price_tags[i].set_costs(GameState.product_costs(definition.definition_id))
button.icon = definition.icon
button.glow_color = definition.glow_color
button.enabled = GameState.vessel_is_unlocked(definition.definition_id)
button.queue_redraw()
refresh_queue()
if message != "":
if Time.get_ticks_msec() < message_until:
status.text = "%s\n%s" % [message, status.text]
else:
message = ""
func refresh_queue() -> void:
var orders: Array = GameState.order_list(building_id)
for i in range(queue_slots.size()):
var slot: Control = queue_slots[i]
slot.visible = i < orders.size()
if not slot.visible:
continue
var order: Dictionary = orders[i]
var definition: Resource = VESSEL_DEFINITIONS.get_definition(String(order.product))
slot.icon = definition.icon
slot.glow_color = vessel_colour(String(order.product))
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_name(String(head.product)),
int(round(float(head.progress) * 100.0))]
else:
queue_detail.text = "%s waiting for resources\nTap to cancel" \
% GameState.vessel_name(String(head.product))
func vessel_colour(vessel_type: String) -> Color:
var definition: Resource = VESSEL_DEFINITIONS.get_definition(vessel_type)
return definition.glow_color if definition != null else Color(0.25, 0.85, 1.0)
func _build_price_tags() -> void:
for i in range(vessel_buttons.size()):
var tag: Control = PRICE_TAG.new()
tag.name = "CraftPrice%d" % i
tag.set_anchors_preset(Control.PRESET_CENTER)
add_child(tag)
price_tags.append(tag)
func row_height() -> float:
return PRICE_OFFSET + PRICE_HEIGHT
func _layout_entries() -> void:
var rows: int = maxi(int(ceil(float(vessel_buttons.size()) / float(COLUMNS))), 1)
var band: Vector2 = MENU_LAYOUT.grid_band(status.offset_bottom, size.y)
for i in range(vessel_buttons.size()):
var row: int = i / COLUMNS
var column: int = i % COLUMNS
var cx: float = (float(column) - 1.0) * COLUMN_X
var top: float = MENU_LAYOUT.grid_row_top(row, rows, row_height(), band, ROW_PITCH)
MENU_LAYOUT.place_entry(vessel_buttons[i], cx, top, BUTTON_HALF, BUTTON_HEIGHT)
MENU_LAYOUT.place_entry(vessel_labels[i], cx, top + LABEL_OFFSET,
LABEL_HALF, LABEL_HEIGHT)
if i < price_tags.size():
MENU_LAYOUT.place_entry(price_tags[i], cx, top + PRICE_OFFSET,
PRICE_HALF, PRICE_HEIGHT)

View file

@ -0,0 +1 @@
uid://5oywglpqem62

308
ui/menus/CraftMenu.tscn Normal file
View file

@ -0,0 +1,308 @@
[gd_scene load_steps=4 format=3]
[ext_resource type="Script" path="res://ui/IconButton.gd" id="10"]
[ext_resource type="Script" path="res://ui/ResourceBar.gd" id="24"]
[ext_resource type="Script" path="res://ui/menus/CraftMenu.gd" id="menu_script"]
[node name="CraftMenu" type="Control" groups=["modal_menus"]]
visible = false
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("menu_script")
[node name="Backdrop" type="ColorRect" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.03, 0.03, 0.08, 0.94)
[node name="TitleLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24
offset_top = 20
offset_right = -200
offset_bottom = 82
text = "Build a ship"
horizontal_alignment = 1
[node name="Bar" type="Control" parent="."]
anchor_right = 1.0
mouse_filter = 2
offset_left = 24
offset_top = 148
offset_right = -24
offset_bottom = 330
script = ExtResource("24")
[node name="StatusLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24
offset_top = 344
offset_right = -200
offset_bottom = 490
theme_override_font_sizes/font_size = 36
autowrap_mode = 2
text = ""
horizontal_alignment = 1
[node name="InterceptorButton" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -310
offset_right = -245
offset_top = -100
offset_bottom = 48
script = ExtResource("10")
icon = 2
[node name="InterceptorLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -330
offset_right = -225
offset_top = 56
offset_bottom = 136
theme_override_font_sizes/font_size = 24
text = "Interceptor"
horizontal_alignment = 1
[node name="ShieldButton" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -221
offset_right = -156
offset_top = -100
offset_bottom = 48
script = ExtResource("10")
icon = 2
glow_color = Color(0.45, 0.8, 1, 1)
[node name="ShieldLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -241
offset_right = -136
offset_top = 56
offset_bottom = 136
theme_override_font_sizes/font_size = 24
autowrap_mode = 2
text = "Shield Vessel"
horizontal_alignment = 1
[node name="GasBomberButton" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -132
offset_right = -67
offset_top = -100
offset_bottom = 28
script = ExtResource("10")
icon = 31
glow_color = Color(0.96, 0.32, 0.78, 1)
[node name="GasBomberLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -152
offset_right = -47
offset_top = 56
offset_bottom = 150
theme_override_font_sizes/font_size = 24
autowrap_mode = 2
text = "Gas Bomber"
horizontal_alignment = 1
[node name="CarrierButton" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -43
offset_right = 22
offset_top = -100
offset_bottom = 48
script = ExtResource("10")
icon = 34
glow_color = Color(1, 0.78, 0.3, 1)
[node name="CarrierLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -63
offset_right = 42
offset_top = 56
offset_bottom = 136
theme_override_font_sizes/font_size = 24
autowrap_mode = 2
text = "Carrier"
horizontal_alignment = 1
[node name="LaserButton" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = 46
offset_right = 111
offset_top = -100
offset_bottom = 48
script = ExtResource("10")
icon = 35
glow_color = Color(0.3, 0.9, 1, 1)
[node name="LaserLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = 26
offset_right = 131
offset_top = 56
offset_bottom = 136
theme_override_font_sizes/font_size = 24
autowrap_mode = 2
text = "Laser Ship"
horizontal_alignment = 1
[node name="DistorderButton" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = 135
offset_right = 200
offset_top = -100
offset_bottom = 48
script = ExtResource("10")
icon = 37
glow_color = Color(0.7, 0.3, 1, 1)
[node name="DistorderLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = 115
offset_right = 220
offset_top = 56
offset_bottom = 136
theme_override_font_sizes/font_size = 24
autowrap_mode = 2
text = "Distorder"
horizontal_alignment = 1
[node name="QueueLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 1.0
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
offset_left = -200
offset_right = -10
offset_top = -450
offset_bottom = -390
theme_override_font_sizes/font_size = 26
autowrap_mode = 2
horizontal_alignment = 1
text = "Queue"
[node name="QueueDetail" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 24
offset_right = -24
offset_top = -150
offset_bottom = -30
theme_override_font_sizes/font_size = 26
autowrap_mode = 2
horizontal_alignment = 1
text = ""
[node name="QueueSlot0" type="Control" parent="."]
visible = false
anchor_left = 1.0
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
offset_left = -140
offset_right = -30
offset_top = -380
offset_bottom = -270
script = ExtResource("10")
icon = 2
glow_color = Color(0.25, 0.85, 1, 1)
[node name="QueueSlot1" type="Control" parent="."]
visible = false
anchor_left = 1.0
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
offset_left = -140
offset_right = -30
offset_top = -262
offset_bottom = -152
script = ExtResource("10")
icon = 2
glow_color = Color(0.25, 0.85, 1, 1)
[node name="QueueSlot2" type="Control" parent="."]
visible = false
anchor_left = 1.0
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
offset_left = -140
offset_right = -30
offset_top = -144
offset_bottom = -34
script = ExtResource("10")
icon = 2
glow_color = Color(0.25, 0.85, 1, 1)
[node name="QueueSlot3" type="Control" parent="."]
visible = false
anchor_left = 1.0
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
offset_left = -140
offset_right = -30
offset_top = -26
offset_bottom = 84
script = ExtResource("10")
icon = 2
glow_color = Color(0.25, 0.85, 1, 1)
[node name="CloseButton" type="Control" parent="."]
anchor_left = 1.0
anchor_right = 1.0
offset_left = -140
offset_right = -24
offset_top = 20
offset_bottom = 136
script = ExtResource("10")
icon = 1

36
ui/menus/FormationMenu.gd Normal file
View file

@ -0,0 +1,36 @@
extends "res://ui/menus/ModalMenu.gd"
signal formation_changed
signal reset_requested
@onready var editor: Control = $Editor
@onready var reset_button: Control = $ResetButton
func _ready() -> void:
super._ready()
reset_button.pressed.connect(func(): reset_requested.emit())
editor.formation_changed.connect(func(): formation_changed.emit())
func open_menu() -> void:
super.open_menu()
editor.queue_redraw()
func redraw() -> void:
editor.queue_redraw()
func handle_unhandled_input(event: InputEvent) -> bool:
if not visible:
return false
if event is InputEventScreenTouch:
editor.handle_touch(event)
elif event is InputEventScreenDrag:
editor.handle_drag(event)
elif event is InputEventMouseButton:
editor.handle_mouse_button(event)
elif event is InputEventMouseMotion:
editor.handle_mouse_motion(event)
return true

View file

@ -0,0 +1 @@
uid://bigv0ubmyddob

View file

@ -0,0 +1,90 @@
[gd_scene load_steps=4 format=3]
[ext_resource type="Script" path="res://ui/IconButton.gd" id="10"]
[ext_resource type="Script" path="res://ui/FormationEditor.gd" id="17"]
[ext_resource type="Script" path="res://ui/menus/FormationMenu.gd" id="menu_script"]
[node name="FormationMenu" type="Control" groups=["modal_menus"]]
visible = false
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("menu_script")
[node name="Backdrop" type="ColorRect" parent="."]
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.03, 0.03, 0.08, 0.94)
[node name="TitleLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24
offset_top = 70
offset_right = -24
offset_bottom = 220
theme_override_font_sizes/font_size = 44
autowrap_mode = 2
text = "Fleet formation - drag to arrange"
horizontal_alignment = 1
[node name="Editor" type="Control" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -300
offset_right = 300
offset_top = -300
offset_bottom = 300
script = ExtResource("17")
[node name="ResetButton" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = -74
offset_right = 74
offset_top = -270
offset_bottom = -122
script = ExtResource("10")
icon = 3
[node name="ResetLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = -180
offset_right = 180
offset_top = -114
offset_bottom = -8
theme_override_font_sizes/font_size = 60
text = "Reset"
horizontal_alignment = 1
[node name="CloseButton" type="Control" parent="."]
anchor_left = 1.0
anchor_right = 1.0
offset_left = -172
offset_right = -24
offset_top = 24
offset_bottom = 172
script = ExtResource("10")
icon = 1
[node name="FadeOverlay" type="CanvasLayer" parent="."]
layer = 10
[node name="FadeRect" type="ColorRect" parent="FadeOverlay"]
mouse_filter = 2
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 0)

39
ui/menus/GarrisonMenu.gd Normal file
View file

@ -0,0 +1,39 @@
extends "res://ui/menus/ModalMenu.gd"
@onready var panel: Control = $Panel
@onready var status: Label = $StatusLabel
var building_id: int = 0
func _ready() -> void:
super._ready()
panel.changed.connect(refresh)
func open_for(next_building_id: int) -> void:
building_id = next_building_id
panel.building_id = building_id
open_menu()
refresh()
func close_menu() -> void:
super.close_menu()
building_id = 0
panel.building_id = 0
func refresh() -> void:
if building_id == 0:
return
panel.building_id = building_id
panel.refresh()
var ordered: int = GameState.garrison_desired_total(building_id)
var crewed: int = GameState.garrison_crew(building_id).size()
if ordered == 0:
status.text = "No vessels ordered. Only armed vessels can hold a position."
elif crewed >= ordered:
status.text = "Garrison at full strength"
else:
status.text = "%d of %d berths crewed - vessels are sent from the mothership as they become available" % [crewed, ordered]

View file

@ -0,0 +1 @@
uid://cdfkcdqvwdsh3

View file

@ -0,0 +1,62 @@
[gd_scene load_steps=4 format=3]
[ext_resource type="Script" path="res://ui/IconButton.gd" id="10"]
[ext_resource type="Script" path="res://ui/GarrisonPanel.gd" id="33"]
[ext_resource type="Script" path="res://ui/menus/GarrisonMenu.gd" id="menu_script"]
[node name="GarrisonMenu" type="Control" groups=["modal_menus"]]
visible = false
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("menu_script")
[node name="Backdrop" type="ColorRect" parent="."]
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.03, 0.03, 0.08, 0.94)
[node name="TitleLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24.0
offset_top = 20.0
offset_right = -200.0
offset_bottom = 82.0
text = "Outpost Garrison"
horizontal_alignment = 1
[node name="Panel" type="Control" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 40.0
offset_top = 120.0
offset_right = -40.0
offset_bottom = 620.0
script = ExtResource("33")
[node name="StatusLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 32.0
offset_top = -160.0
offset_right = -32.0
offset_bottom = -24.0
theme_override_font_sizes/font_size = 30
autowrap_mode = 2
horizontal_alignment = 1
text = ""
[node name="CloseButton" type="Control" parent="."]
anchor_left = 1.0
anchor_right = 1.0
offset_left = -140.0
offset_top = 20.0
offset_right = -24.0
offset_bottom = 136.0
script = ExtResource("10")
icon = 1
glow_color = Color(0.72, 0.86, 0.45, 1)

138
ui/menus/HangarMenu.gd Normal file
View file

@ -0,0 +1,138 @@
extends "res://ui/menus/ModalMenu.gd"
signal crew_requested(vessel_type: String)
signal store_requested(vessel_type: String)
const VESSEL_DEFINITIONS = preload("res://vessels/VesselDefinitions.gd")
const RULE_COLOR := Color(0.45, 0.9, 0.55)
const COLUMNS := 3
const LABEL_GAP := 2.0
const DIVIDER_GAP := 10.0
const CAPTION_LINES := 3.0
const CAPTION_MIN := 13
const CAPTION_MAX := 22
const LINE_RATIO := 1.25
@onready var status: Label = $StatusLabel
@onready var available_title: Label = $AvailableTitle
@onready var fleet_title: Label = $FleetTitle
@onready var available_slots: Array = [$AvailableInterceptor, $AvailableShield,
$AvailableGasBomber, $AvailableCarrier, $AvailableLaser, $AvailableDistorder]
@onready var available_labels: Array = [$AvailableInterceptorLabel, $AvailableShieldLabel,
$AvailableGasBomberLabel, $AvailableCarrierLabel, $AvailableLaserLabel,
$AvailableDistorderLabel]
@onready var fleet_slots: Array = [$FleetInterceptor, $FleetShield, $FleetGasBomber,
$FleetCarrier, $FleetLaser, $FleetDistorder]
@onready var fleet_labels: Array = [$FleetInterceptorLabel, $FleetShieldLabel,
$FleetGasBomberLabel, $FleetCarrierLabel, $FleetLaserLabel, $FleetDistorderLabel]
var divider_y: float = 0.0
func _ready() -> void:
super._ready()
for i in range(available_slots.size()):
available_slots[i].pressed.connect(
func(): crew_requested.emit(_vessel_types()[i]))
fleet_slots[i].pressed.connect(
func(): store_requested.emit(_vessel_types()[i]))
_layout_sections()
func open_for(fleet: Node) -> void:
status.text = ""
open_menu()
refresh(fleet)
func set_status(message: String) -> void:
status.text = message
func refresh(fleet: Node) -> void:
var types: Array = _vessel_types()
for i in range(mini(types.size(), available_slots.size())):
var vessel_type: String = types[i]
var definition: Resource = VESSEL_DEFINITIONS.get_definition(vessel_type)
var available: int = GameState.stored_vessels.count(vessel_type)
var deployed: int = 0
for ship in fleet.ships:
if is_instance_valid(ship) and String(ship.vessel_type) == vessel_type:
deployed += 1
available_labels[i].text = "%s\n%d available" % [definition.display_name, available]
fleet_labels[i].text = "%s\n%d in fleet" % [definition.display_name, deployed]
for slot in [available_slots[i], fleet_slots[i]]:
slot.icon = definition.icon
slot.glow_color = definition.glow_color
available_slots[i].enabled = available > 0 \
and fleet.ships.size() < GameState.max_fleet_size()
fleet_slots[i].enabled = deployed > 0 and fleet.ships.size() > 1
available_slots[i].queue_redraw()
fleet_slots[i].queue_redraw()
func _vessel_types() -> Array:
var types: Array = []
for definition in VESSEL_DEFINITIONS.fleet():
types.append(String(definition.definition_id))
return types
func _layout_sections() -> void:
var count: int = available_slots.size()
if count == 0:
return
var columns: int = mini(COLUMNS, count)
var rows: int = int(ceil(float(count) / float(columns)))
var top: float = available_title.position.y + available_title.size.y + 8.0
var bottom: float = status.position.y - 12.0
var title_height: float = fleet_title.size.y
var section_height: float = (bottom - top - title_height - 16.0) * 0.5
if section_height <= 0.0:
return
var cell_width: float = size.x / float(columns)
var cell_height: float = section_height / float(rows)
var slot_size: float = minf(cell_height * 0.46, cell_width * 0.40)
var label_height: float = maxf(cell_height - slot_size - LABEL_GAP * 2.0, 1.0)
_layout_section(available_slots, available_labels, top, columns, cell_width,
cell_height, slot_size, label_height)
var available_bottom: float = top + section_height
divider_y = available_bottom + DIVIDER_GAP
fleet_title.position.y = divider_y + DIVIDER_GAP
var fleet_top: float = fleet_title.position.y + title_height + 8.0
_layout_section(fleet_slots, fleet_labels, fleet_top, columns, cell_width,
cell_height, slot_size, label_height)
queue_redraw()
func _layout_section(slots: Array, labels: Array, top: float, columns: int,
cell_width: float, cell_height: float, slot_size: float,
label_height: float) -> void:
if columns <= 0:
return
for i in range(slots.size()):
var column: int = i % columns
var row: int = i / columns
var cell_x: float = float(column) * cell_width
var cell_y: float = top + float(row) * cell_height
slots[i].position = Vector2(cell_x + (cell_width - slot_size) * 0.5, cell_y)
slots[i].size = Vector2(slot_size, slot_size)
labels[i].position = Vector2(cell_x, cell_y + slot_size + LABEL_GAP)
labels[i].size = Vector2(cell_width, label_height)
labels[i].horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
labels[i].vertical_alignment = VERTICAL_ALIGNMENT_TOP
labels[i].autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
labels[i].add_theme_font_size_override("font_size", caption_size(label_height))
labels[i].clip_text = true
func caption_size(label_height: float) -> int:
return clampi(int(label_height / (CAPTION_LINES * LINE_RATIO)),
CAPTION_MIN, CAPTION_MAX)
func _draw() -> void:
if divider_y <= 0.0:
return
NeonDraw.draw_glowing_line(self, Vector2(24.0, divider_y),
Vector2(size.x - 24.0, divider_y), RULE_COLOR, 0.5, 0.4)

View file

@ -0,0 +1 @@
uid://c386w72vtqmdw

388
ui/menus/HangarMenu.tscn Normal file
View file

@ -0,0 +1,388 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="Script" path="res://ui/IconButton.gd" id="10"]
[ext_resource type="Script" path="res://ui/menus/HangarMenu.gd" id="39"]
[node name="HangarMenu" type="Control" groups=["modal_menus"]]
visible = false
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("39")
[node name="Backdrop" type="ColorRect" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.03, 0.03, 0.08, 0.94)
[node name="TitleLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24
offset_top = 60
offset_right = -210
offset_bottom = 170
text = "Hangar"
horizontal_alignment = 1
[node name="ListLabel" type="Label" parent="."]
visible = false
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 32
offset_top = 200
offset_right = -32
offset_bottom = -420
theme_override_font_sizes/font_size = 30
autowrap_mode = 2
horizontal_alignment = 1
text = ""
[node name="StatusLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 32
offset_top = -410
offset_right = -32
offset_bottom = -300
theme_override_font_sizes/font_size = 30
autowrap_mode = 2
horizontal_alignment = 1
text = ""
[node name="NextButton" type="Control" parent="."]
visible = false
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -230
offset_right = -110
offset_top = -280
offset_bottom = -160
script = ExtResource("10")
icon = 6
glow_color = Color(0.55, 0.95, 0.9, 1)
[node name="CrewButton" type="Control" parent="."]
visible = false
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = 110
offset_right = 230
offset_top = -280
offset_bottom = -160
script = ExtResource("10")
icon = 2
glow_color = Color(0.45, 0.9, 0.55, 1)
[node name="HintLabel" type="Label" parent="."]
visible = false
mouse_filter = 2
anchor_right = 1.0
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 24
offset_top = -150
offset_right = -24
offset_bottom = -60
theme_override_font_sizes/font_size = 28
autowrap_mode = 2
horizontal_alignment = 1
text = "Next Crew"
[node name="AvailableTitle" type="Label" parent="."]
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="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -290
offset_right = -215
offset_top = 255
offset_bottom = 375
script = ExtResource("10")
icon = 2
[node name="AvailableInterceptorLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -330
offset_right = -175
offset_top = 380
offset_bottom = 460
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="AvailableShield" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -189
offset_right = -114
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="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -229
offset_right = -74
offset_top = 380
offset_bottom = 460
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="AvailableGasBomber" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -88
offset_right = -13
offset_top = 255
offset_bottom = 375
script = ExtResource("10")
icon = 31
glow_color = Color(0.96, 0.32, 0.78, 1)
[node name="AvailableGasBomberLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -128
offset_right = 27
offset_top = 380
offset_bottom = 460
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="AvailableCarrier" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 13
offset_right = 88
offset_top = 255
offset_bottom = 375
script = ExtResource("10")
icon = 34
glow_color = Color(1, 0.78, 0.3, 1)
[node name="AvailableCarrierLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -27
offset_right = 128
offset_top = 380
offset_bottom = 460
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="AvailableLaser" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 114
offset_right = 189
offset_top = 255
offset_bottom = 375
script = ExtResource("10")
icon = 35
glow_color = Color(0.3, 0.9, 1, 1)
[node name="AvailableLaserLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 74
offset_right = 229
offset_top = 380
offset_bottom = 460
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="AvailableDistorder" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 215
offset_right = 290
offset_top = 255
offset_bottom = 375
script = ExtResource("10")
icon = 37
glow_color = Color(0.7, 0.3, 1, 1)
[node name="AvailableDistorderLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 175
offset_right = 330
offset_top = 380
offset_bottom = 460
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="FleetTitle" type="Label" parent="."]
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="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -290
offset_right = -215
offset_top = 565
offset_bottom = 685
script = ExtResource("10")
icon = 2
[node name="FleetInterceptorLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -330
offset_right = -175
offset_top = 690
offset_bottom = 770
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="FleetShield" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -189
offset_right = -114
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="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -229
offset_right = -74
offset_top = 690
offset_bottom = 770
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="FleetGasBomber" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -88
offset_right = -13
offset_top = 565
offset_bottom = 685
script = ExtResource("10")
icon = 31
glow_color = Color(0.96, 0.32, 0.78, 1)
[node name="FleetGasBomberLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -128
offset_right = 27
offset_top = 690
offset_bottom = 770
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="FleetCarrier" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 13
offset_right = 88
offset_top = 565
offset_bottom = 685
script = ExtResource("10")
icon = 34
glow_color = Color(1, 0.78, 0.3, 1)
[node name="FleetCarrierLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -27
offset_right = 128
offset_top = 690
offset_bottom = 770
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="FleetLaser" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 114
offset_right = 189
offset_top = 565
offset_bottom = 685
script = ExtResource("10")
icon = 35
glow_color = Color(0.3, 0.9, 1, 1)
[node name="FleetLaserLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 74
offset_right = 229
offset_top = 690
offset_bottom = 770
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="FleetDistorder" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 215
offset_right = 290
offset_top = 565
offset_bottom = 685
script = ExtResource("10")
icon = 37
glow_color = Color(0.7, 0.3, 1, 1)
[node name="FleetDistorderLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 175
offset_right = 330
offset_top = 690
offset_bottom = 770
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="CloseButton" type="Control" parent="."]
anchor_left = 1.0
anchor_right = 1.0
offset_left = -172
offset_top = 24
offset_right = -24
offset_bottom = 172
script = ExtResource("10")
icon = 1
glow_color = Color(0.45, 0.9, 0.55, 1)

157
ui/menus/LabMenu.gd Normal file
View file

@ -0,0 +1,157 @@
extends "res://ui/menus/ModalMenu.gd"
signal vessel_selected(index: int)
signal upgrade_selected(index: int)
const PRICE_TAG = preload("res://ui/PriceTag.gd")
const TAG_HEIGHT := 38.0
const LABEL_HEIGHT := 40.0
const TAG_PADDING := 8.0
const ROW_GAP := 18.0
const VESSEL_HALF := 30.0
const VESSEL_SPACING := 90.0
const VESSEL_MARGIN := 24.0
@onready var vessel_buttons: Array = [$VesselA, $VesselB, $VesselC, $VesselD,
$VesselE, $VesselF, $VesselG, $VesselH]
@onready var vessel_labels: Array = [$VesselALabel, $VesselBLabel, $VesselCLabel,
$VesselDLabel, $VesselELabel, $VesselFLabel, $VesselGLabel, $VesselHLabel]
@onready var upgrade_buttons: Array = [$Upgrade0, $Upgrade1, $Upgrade2, $Upgrade3]
@onready var upgrade_labels: Array = [$Upgrade0Label, $Upgrade1Label,
$Upgrade2Label, $Upgrade3Label]
@onready var vessel_heading: Label = $VesselLabel
@onready var detail: Label = $DetailLabel
@onready var status: Label = $StatusLabel
@onready var resource_bar: Control = $Bar
var building_id: int = 0
var price_tags: Array = []
func _ready() -> void:
super._ready()
_build_price_tags()
for i in range(vessel_buttons.size()):
vessel_buttons[i].pressed.connect(func(): vessel_selected.emit(i))
for i in range(upgrade_buttons.size()):
upgrade_buttons[i].pressed.connect(func(): upgrade_selected.emit(i))
func open_for(next_building_id: int) -> void:
building_id = next_building_id
resource_bar.building_id = building_id
open_menu()
func close_menu() -> void:
super.close_menu()
building_id = 0
func render_vessels(definitions: Array, selected_type: String) -> void:
for i in range(mini(vessel_buttons.size(), definitions.size())):
var definition: Resource = definitions[i]
vessel_buttons[i].enabled = true
vessel_buttons[i].icon = definition.icon
vessel_buttons[i].glow_color = definition.glow_color
vessel_buttons[i].highlighted = String(definition.definition_id) == selected_type
vessel_labels[i].text = definition.display_name
vessel_buttons[i].queue_redraw()
_layout_vessels()
func render_options(options: Array, selected: int, vessel_colour: Color) -> void:
for i in range(upgrade_buttons.size()):
var shown: bool = i < options.size()
upgrade_buttons[i].visible = shown
upgrade_labels[i].visible = shown
price_tags[i].visible = shown
if not shown:
continue
var option: Dictionary = options[i]
var maxed: bool = bool(option.get("maxed", false))
upgrade_labels[i].text = String(option.label)
price_tags[i].visible = not maxed
price_tags[i].show_price({} if maxed else option.costs,
0 if maxed else int(option.points))
upgrade_buttons[i].icon = int(option.icon)
upgrade_buttons[i].glow_color = vessel_colour
upgrade_buttons[i].highlighted = i == selected
upgrade_buttons[i].enabled = true
upgrade_buttons[i].queue_redraw()
_resize_price_tags()
func show_detail(heading: String, description: String, points: int) -> void:
detail.text = "%s\n%s" % [heading, description]
if points > 0:
detail.text += "\nResearch points: %d/%d" % [GameState.research_points, points]
func clear_detail() -> void:
detail.text = ""
func set_status(message: String) -> void:
status.text = message
func _build_price_tags() -> void:
for i in range(upgrade_labels.size()):
var label: Control = upgrade_labels[i]
var tag: Control = PRICE_TAG.new()
tag.name = "LabPrice%d" % i
tag.anchor_left = label.anchor_left
tag.anchor_right = label.anchor_right
tag.anchor_top = label.anchor_top
tag.anchor_bottom = label.anchor_bottom
tag.offset_left = label.offset_left
tag.offset_right = label.offset_right
label.offset_bottom = label.offset_top + LABEL_HEIGHT
tag.offset_top = label.offset_bottom + 2.0
tag.offset_bottom = tag.offset_top + tag_height()
add_child(tag)
price_tags.append(tag)
func tag_height() -> float:
var tallest: float = TAG_HEIGHT
for tag in price_tags:
if tag != null and tag.has_method("wanted_height"):
tallest = maxf(tallest, float(tag.wanted_height()) + TAG_PADDING)
return tallest
func _layout_vessels() -> void:
var shown: Array = []
for i in range(vessel_buttons.size()):
if vessel_buttons[i].visible:
shown.append(i)
if shown.is_empty():
return
var room: float = size.x - VESSEL_MARGIN * 2.0
if room <= 0.0:
room = 672.0
var spacing: float = minf(VESSEL_SPACING, room / float(shown.size()))
for slot in range(shown.size()):
var i: int = shown[slot]
var cx: float = (float(slot) - (float(shown.size()) - 1.0) * 0.5) * spacing
vessel_buttons[i].offset_left = cx - VESSEL_HALF
vessel_buttons[i].offset_right = cx + VESSEL_HALF
vessel_labels[i].offset_left = cx - spacing * 0.5
vessel_labels[i].offset_right = cx + spacing * 0.5
func _resize_price_tags() -> void:
var height: float = tag_height()
for tag in price_tags:
tag.offset_bottom = tag.offset_top + height
if price_tags.size() < 3:
return
var shift: float = price_tags[0].offset_bottom + ROW_GAP - upgrade_buttons[2].offset_top
if absf(shift) < 0.5:
return
for i in range(2, upgrade_buttons.size()):
for node in [upgrade_buttons[i], upgrade_labels[i], price_tags[i]]:
node.offset_top += shift
node.offset_bottom += shift

1
ui/menus/LabMenu.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://dq44uh8vf0qp0

426
ui/menus/LabMenu.tscn Normal file
View file

@ -0,0 +1,426 @@
[gd_scene load_steps=4 format=3]
[ext_resource type="Script" path="res://ui/IconButton.gd" id="10"]
[ext_resource type="Script" path="res://ui/ResourceBar.gd" id="24"]
[ext_resource type="Script" path="res://ui/menus/LabMenu.gd" id="menu_script"]
[node name="LabMenu" type="Control" groups=["modal_menus"]]
visible = false
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("menu_script")
[node name="Backdrop" type="ColorRect" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.03, 0.03, 0.08, 0.94)
[node name="TitleLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24
offset_top = 20
offset_right = -200
offset_bottom = 82
text = "Ship Lab"
horizontal_alignment = 1
[node name="Bar" type="Control" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24
offset_top = 148
offset_right = -24
offset_bottom = 330
script = ExtResource("24")
[node name="VesselA" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -300
offset_right = -240
offset_top = 346
offset_bottom = 466
script = ExtResource("10")
icon = 2
glow_color = Color(0.25, 0.85, 1, 1)
[node name="VesselB" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -210
offset_right = -150
offset_top = 346
offset_bottom = 466
script = ExtResource("10")
icon = 2
glow_color = Color(0.45, 0.8, 1, 1)
[node name="VesselC" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -120
offset_right = -60
offset_top = 346
offset_bottom = 466
script = ExtResource("10")
icon = 31
glow_color = Color(0.96, 0.32, 0.78, 1)
[node name="VesselD" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -30
offset_right = 30
offset_top = 346
offset_bottom = 466
script = ExtResource("10")
icon = 12
glow_color = Color(0.95, 0.55, 0.85, 1)
[node name="VesselE" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 60
offset_right = 120
offset_top = 346
offset_bottom = 466
script = ExtResource("10")
icon = 34
glow_color = Color(1, 0.78, 0.3, 1)
[node name="VesselF" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 150
offset_right = 210
offset_top = 346
offset_bottom = 466
script = ExtResource("10")
icon = 35
glow_color = Color(0.3, 0.9, 1, 1)
[node name="VesselG" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 240
offset_right = 300
offset_top = 346
offset_bottom = 466
script = ExtResource("10")
icon = 37
glow_color = Color(0.7, 0.3, 1, 1)
[node name="VesselH" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 320
offset_right = 380
offset_top = 346
offset_bottom = 466
script = ExtResource("10")
icon = 43
glow_color = Color(0.5, 0.95, 0.8, 1)
[node name="VesselLabel" type="Label" parent="."]
visible = false
mouse_filter = 2
anchor_right = 1.0
offset_left = 24
offset_top = 470
offset_right = -24
offset_bottom = 528
theme_override_font_sizes/font_size = 34
horizontal_alignment = 1
text = ""
[node name="VesselALabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -310
offset_right = -230
offset_top = 470
offset_bottom = 542
theme_override_font_sizes/font_size = 15
autowrap_mode = 2
horizontal_alignment = 1
text = "Interceptor"
[node name="VesselBLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -220
offset_right = -140
offset_top = 470
offset_bottom = 542
theme_override_font_sizes/font_size = 15
autowrap_mode = 2
horizontal_alignment = 1
text = "Shield"
[node name="VesselCLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -130
offset_right = -50
offset_top = 470
offset_bottom = 542
theme_override_font_sizes/font_size = 15
autowrap_mode = 2
horizontal_alignment = 1
text = "Gas Bomber"
[node name="VesselDLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -40
offset_right = 40
offset_top = 470
offset_bottom = 542
theme_override_font_sizes/font_size = 15
autowrap_mode = 2
horizontal_alignment = 1
text = "Transporter"
[node name="VesselELabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 50
offset_right = 130
offset_top = 470
offset_bottom = 542
theme_override_font_sizes/font_size = 15
autowrap_mode = 2
horizontal_alignment = 1
text = "Carrier"
[node name="VesselFLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 140
offset_right = 220
offset_top = 470
offset_bottom = 542
theme_override_font_sizes/font_size = 15
autowrap_mode = 2
horizontal_alignment = 1
text = "Laser Ship"
[node name="VesselGLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 230
offset_right = 310
offset_top = 470
offset_bottom = 542
theme_override_font_sizes/font_size = 15
autowrap_mode = 2
horizontal_alignment = 1
text = "Distorder"
[node name="VesselHLabel" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 310
offset_right = 390
offset_top = 470
offset_bottom = 542
theme_override_font_sizes/font_size = 15
autowrap_mode = 2
horizontal_alignment = 1
text = "Repair Drone"
[node name="VesselUpgradeSeparator" type="ColorRect" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 48
offset_top = 550
offset_right = -48
offset_bottom = 554
color = Color(0.25, 0.85, 1, 0.6)
[node name="UpgradeList" type="Label" parent="."]
visible = false
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 32
offset_top = 562
offset_right = -32
offset_bottom = -390
theme_override_font_sizes/font_size = 30
autowrap_mode = 2
text = ""
[node name="DetailLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 32
offset_top = -300
offset_right = -32
offset_bottom = -180
theme_override_font_sizes/font_size = 32
autowrap_mode = 2
horizontal_alignment = 1
text = ""
[node name="StatusLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 32
offset_top = -174
offset_right = -32
offset_bottom = -120
theme_override_font_sizes/font_size = 30
autowrap_mode = 2
horizontal_alignment = 1
text = ""
[node name="NextButton" type="Control" parent="."]
visible = false
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -230
offset_right = -110
offset_top = -112
offset_bottom = -22
script = ExtResource("10")
icon = 6
glow_color = Color(0.55, 0.95, 0.9, 1)
[node name="HintLabel" type="Label" parent="."]
visible = false
mouse_filter = 2
anchor_right = 1.0
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 24
offset_top = -102
offset_right = -24
offset_bottom = -22
theme_override_font_sizes/font_size = 28
autowrap_mode = 2
horizontal_alignment = 1
text = "Next upgrade Buy"
[node name="Upgrade0" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -240
offset_right = -120
offset_top = 562
offset_bottom = 672
script = ExtResource("10")
icon = 13
glow_color = Color(0.25, 0.85, 1, 1)
[node name="Upgrade0Label" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -310
offset_right = -50
offset_top = 676
offset_bottom = 746
theme_override_font_sizes/font_size = 22
autowrap_mode = 2
horizontal_alignment = 1
text = ""
[node name="Upgrade1" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 120
offset_right = 240
offset_top = 562
offset_bottom = 672
script = ExtResource("10")
icon = 13
glow_color = Color(0.25, 0.85, 1, 1)
[node name="Upgrade1Label" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 50
offset_right = 310
offset_top = 676
offset_bottom = 746
theme_override_font_sizes/font_size = 22
autowrap_mode = 2
horizontal_alignment = 1
text = ""
[node name="Upgrade2" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -240
offset_right = -120
offset_top = 790
offset_bottom = 900
script = ExtResource("10")
icon = 13
glow_color = Color(0.25, 0.85, 1, 1)
[node name="Upgrade2Label" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -310
offset_right = -50
offset_top = 904
offset_bottom = 974
theme_override_font_sizes/font_size = 22
autowrap_mode = 2
horizontal_alignment = 1
text = ""
[node name="Upgrade3" type="Control" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 120
offset_right = 240
offset_top = 790
offset_bottom = 900
script = ExtResource("10")
icon = 13
glow_color = Color(0.25, 0.85, 1, 1)
[node name="Upgrade3Label" type="Label" parent="."]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 50
offset_right = 310
offset_top = 904
offset_bottom = 974
theme_override_font_sizes/font_size = 22
autowrap_mode = 2
horizontal_alignment = 1
text = ""
[node name="CloseButton" type="Control" parent="."]
anchor_left = 1.0
anchor_right = 1.0
offset_left = -140
offset_top = 20
offset_right = -24
offset_bottom = 136
script = ExtResource("10")
icon = 1
glow_color = Color(0.55, 0.95, 0.9, 1)

27
ui/menus/MenuLayout.gd Normal file
View file

@ -0,0 +1,27 @@
extends RefCounted
const HEADER_GAP: float = 16.0
const BOTTOM_MARGIN: float = 28.0
static func grid_band(header_bottom: float, panel_height: float) -> Vector2:
var half: float = panel_height * 0.5
if half <= 0.0:
half = 640.0
return Vector2(header_bottom + HEADER_GAP - half, half - BOTTOM_MARGIN)
static func grid_row_top(row: int, rows: int, row_height: float, band: Vector2,
natural_pitch: float) -> float:
var pitch: float = natural_pitch
if rows > 1:
pitch = minf(pitch, (band.y - band.x - row_height) / float(rows - 1))
return band.x + maxf(pitch, row_height) * float(row)
static func place_entry(node: Control, cx: float, top: float, half: float,
height: float) -> void:
node.offset_left = cx - half
node.offset_right = cx + half
node.offset_top = top
node.offset_bottom = top + height

View file

@ -0,0 +1 @@
uid://mhjg8jhbmy6f

24
ui/menus/ModalMenu.gd Normal file
View file

@ -0,0 +1,24 @@
extends Control
signal closed
@onready var close_button: Control = get_node_or_null("CloseButton")
func _ready() -> void:
visible = false
if close_button != null and close_button.has_signal("pressed"):
close_button.pressed.connect(close_menu)
func open_menu() -> void:
visible = true
func close_menu() -> void:
visible = false
closed.emit()
func handle_unhandled_input(_event: InputEvent) -> bool:
return visible

View file

@ -0,0 +1 @@
uid://dkpcfwj4unbbf

25
ui/menus/PlanningMenu.gd Normal file
View file

@ -0,0 +1,25 @@
extends "res://ui/menus/ModalMenu.gd"
@onready var panel: Control = $Panel
@onready var status: Label = $StatusLabel
func _ready() -> void:
super._ready()
panel.changed.connect(refresh)
func open_menu() -> void:
super.open_menu()
refresh()
func refresh() -> void:
panel.refresh()
var short: PackedStringArray = PackedStringArray()
for vessel_type in GameState.planned_vessel_types():
var missing: int = GameState.vessel_shortfall(String(vessel_type))
if missing > 0:
short.append("%d %s" % [missing, GameState.vessel_name(String(vessel_type))])
status.text = "Every standing order is met or already being built" if short.is_empty() \
else "Short: %s" % ", ".join(short)

View file

@ -0,0 +1 @@
uid://b80lmxhmmqw8a

View file

@ -0,0 +1,63 @@
[gd_scene load_steps=4 format=3]
[ext_resource type="Script" path="res://ui/IconButton.gd" id="10"]
[ext_resource type="Script" path="res://ui/PlanningPanel.gd" id="35"]
[ext_resource type="Script" path="res://ui/menus/PlanningMenu.gd" id="menu_script"]
[node name="PlanningMenu" type="Control" groups=["modal_menus"]]
visible = false
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("menu_script")
[node name="Backdrop" type="ColorRect" parent="."]
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.03, 0.03, 0.08, 0.94)
[node name="TitleLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24.0
offset_top = 20.0
offset_right = -200.0
offset_bottom = 82.0
text = "Planning Center"
horizontal_alignment = 1
[node name="Panel" type="Control" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 40.0
offset_top = 120.0
offset_right = -40.0
offset_bottom = -180.0
anchor_bottom = 1.0
script = ExtResource("35")
[node name="StatusLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 32.0
offset_top = -160.0
offset_right = -32.0
offset_bottom = -24.0
theme_override_font_sizes/font_size = 28
autowrap_mode = 2
horizontal_alignment = 1
text = ""
[node name="CloseButton" type="Control" parent="."]
anchor_left = 1.0
anchor_right = 1.0
offset_left = -140.0
offset_top = 20.0
offset_right = -24.0
offset_bottom = 136.0
script = ExtResource("10")
icon = 1
glow_color = Color(0.62, 0.74, 0.92, 1)

70
ui/menus/SkillMenu.gd Normal file
View file

@ -0,0 +1,70 @@
extends "res://ui/menus/ModalMenu.gd"
signal research_requested(id: String)
@onready var tree: Control = $Tree
@onready var detail: Label = $DetailLabel
@onready var cost: Control = $CostNumber
@onready var status: Label = $StatusLabel
@onready var resource_bar: Control = $Bar
func _ready() -> void:
super._ready()
tree.node_picked.connect(refresh)
tree.node_confirmed.connect(func(id: String): research_requested.emit(id))
func open_for(building_id: int) -> void:
resource_bar.building_id = building_id
open_menu()
tree.reset_scroll()
refresh()
func selected_id() -> String:
return String(tree.selected_id)
func research_completed(id: String) -> void:
tree.queue_redraw()
refresh(id)
func refresh(id: String = "") -> void:
if id == "":
id = String(tree.selected_id)
if id == "":
detail.text = "Select a node to see what it unlocks."
cost.show_price({})
status.text = "Fleet capacity %d · Research points %d" \
% [GameState.max_fleet_size(), GameState.research_points]
return
var node: Dictionary = GameState.skill_node(id)
if node.is_empty():
return
detail.text = "%s\n%s" % [node.name, node.desc]
cost.show_price(GameState.skill_costs(id), GameState.skill_research_point_cost(id))
if GameState.is_unlocked(id):
status.text = "Researched"
elif not GameState.prerequisite_met(id):
status.text = "Needs %s first" % GameState.skill_node(node.requires).name
elif not cost.is_affordable():
status.text = ""
else:
status.text = "Tap again to research"
func handle_unhandled_input(event: InputEvent) -> bool:
if not visible:
return false
if event is InputEventScreenTouch:
tree.handle_touch(event)
elif event is InputEventScreenDrag:
tree.handle_drag(event)
elif event is InputEventMouseButton:
tree.handle_mouse_button(event)
elif event is InputEventMouseMotion:
tree.handle_mouse_motion(event)
return true

View file

@ -0,0 +1 @@
uid://bhgf1risnbb03

92
ui/menus/SkillMenu.tscn Normal file
View file

@ -0,0 +1,92 @@
[gd_scene load_steps=6 format=3]
[ext_resource type="Script" path="res://ui/IconButton.gd" id="10"]
[ext_resource type="Script" path="res://ui/SkillTreeView.gd" id="21"]
[ext_resource type="Script" path="res://ui/ResourceBar.gd" id="24"]
[ext_resource type="Script" path="res://ui/PriceTag.gd" id="price_tag"]
[ext_resource type="Script" path="res://ui/menus/SkillMenu.gd" id="menu_script"]
[node name="SkillMenu" type="Control" groups=["modal_menus"]]
visible = false
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("menu_script")
[node name="Backdrop" type="ColorRect" parent="."]
mouse_filter = 2
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.03, 0.03, 0.08, 0.94)
[node name="Bar" type="Control" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24.0
offset_top = 20
offset_right = -160.0
offset_bottom = 202
script = ExtResource("24")
[node name="Tree" type="Control" parent="."]
mouse_filter = 2
clip_contents = true
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 24.0
offset_top = 214
offset_right = -24.0
offset_bottom = -280.0
script = ExtResource("21")
[node name="DetailLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 32.0
offset_top = -272.0
offset_right = -300.0
offset_bottom = -152.0
theme_override_font_sizes/font_size = 34
autowrap_mode = 2
horizontal_alignment = 0
text = ""
[node name="CostNumber" type="Control" parent="."]
alignment = 2
mouse_filter = 2
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -288.0
offset_top = -272.0
offset_right = -32.0
offset_bottom = -152.0
script = ExtResource("price_tag")
[node name="StatusLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 32.0
offset_top = -76.0
offset_right = -32.0
offset_bottom = -24.0
theme_override_font_sizes/font_size = 32
autowrap_mode = 2
horizontal_alignment = 1
text = ""
[node name="CloseButton" type="Control" parent="."]
anchor_left = 1.0
anchor_right = 1.0
offset_left = -140.0
offset_top = 20.0
offset_right = -24.0
offset_bottom = 136.0
script = ExtResource("10")
icon = 1
glow_color = Color(0.55, 0.6, 1, 1)

56
ui/menus/TransferMenu.gd Normal file
View file

@ -0,0 +1,56 @@
extends "res://ui/menus/ModalMenu.gd"
signal transfer_requested(resource_type: String, to_building: bool, amount: int)
@onready var status: Label = $StatusLabel
@onready var resource_list: Control = $ResourceList
var building_id: int = 0
var extractor_index: int = -1
func _ready() -> void:
super._ready()
resource_list.transfer_requested.connect(
func(resource_type: String, to_building: bool, amount: int):
transfer_requested.emit(resource_type, to_building, amount))
func open_destination(next_building_id: int, next_extractor_index: int) -> void:
building_id = next_building_id
extractor_index = next_extractor_index
status.text = ""
open_menu()
refresh()
func close_menu() -> void:
super.close_menu()
building_id = 0
extractor_index = -1
func refresh() -> void:
if extractor_index >= 0:
resource_list.set_extractor_destination(extractor_index, "Planet extractor")
return
var building: Dictionary = GameState.find_building(building_id)
var heading: String = "This building"
if GameState.building_has_capability(building, "bank"):
heading = "Mothership bank"
resource_list.set_destination(building_id, heading)
func place_name() -> String:
if extractor_index >= 0:
return "the extractor"
var building: Dictionary = GameState.find_building(building_id)
if building.is_empty():
return "this building"
if GameState.building_has_capability(building, "bank"):
return "the bank"
return "this building"
func show_status(message: String) -> void:
status.text = message

View file

@ -0,0 +1 @@
uid://bw1aiy8jt817c

View file

@ -0,0 +1,73 @@
[gd_scene load_steps=4 format=3]
[ext_resource type="Script" path="res://ui/IconButton.gd" id="10"]
[ext_resource type="Script" path="res://ui/ResourceTransferList.gd" id="27"]
[ext_resource type="Script" path="res://ui/menus/TransferMenu.gd" id="menu_script"]
[node name="TransferMenu" type="Control" groups=["modal_menus"]]
visible = false
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("menu_script")
[node name="Backdrop" type="ColorRect" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.03, 0.03, 0.08, 0.94)
[node name="TitleLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24
offset_top = 36
offset_right = -200
offset_bottom = 166
text = "Transfer cargo"
horizontal_alignment = 1
vertical_alignment = 1
autowrap_mode = 2
theme_override_font_sizes/font_size = 44
[node name="StatusLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24
offset_top = 184
offset_right = -24
offset_bottom = 284
theme_override_font_sizes/font_size = 32
autowrap_mode = 2
text = ""
horizontal_alignment = 1
[node name="ResourceList" type="Control" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 16
offset_top = 294
offset_right = -16
offset_bottom = 1080
script = ExtResource("27")
[node name="CloseButton" type="Control" parent="."]
anchor_left = 1.0
anchor_right = 1.0
offset_left = -172
offset_right = -24
offset_top = 24
offset_bottom = 172
script = ExtResource("10")
icon = 1
glow_color = Color(1, 0.78, 0.25, 1)
[node name="HintLabel" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24
offset_top = 1092
offset_right = -24
offset_bottom = 1150
text = "Arrows move the selected amount from that side"
horizontal_alignment = 1
theme_override_colors/font_color = Color(0.55, 0.7, 0.78, 1)
theme_override_font_sizes/font_size = 23

File diff suppressed because it is too large Load diff