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

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)