#147: one resource per line, and the price back under its button

Both halves of the report had the same cause: a craft cell laid out with
more slack than its row pitch had room for.

The caption box was 62px for a one-line vessel name, which pushed the price
70px clear of the button it belonged to - "far from the button they are
about". And the price was given 184px of width in a 190px column, so a
two-resource price ran out of its own box and into its neighbour's.

Prices now put each resource on its own line. Not only where they would
otherwise overflow, which was my first attempt: two resources came to 174px
in a 184px box, so it fitted - until the player was holding three figures
rather than one, and then it did not. A layout that is correct until you get
rich is not correct, and the ticket asked for the line break outright.

The caption is one line tall now with the price directly under it, and rows
are placed off a MEASURED band rather than a written-down pair of tops - the
same fix #144 needed for the build menu, and now the same code. A
centre-anchored grid under a top-anchored header has a gap between them that
is half the panel height, so it can never be a constant.

The test pins what the ticket describes rather than the numbers behind it: a
multi-resource price is stacked, its content stays inside its own box, it
sits within 80px of its button, and no craft entry overlaps another - the
price of one row into the button of the next included, which the old
geometry did by 14px whatever was priced.
This commit is contained in:
Loïc Wisniewski 2026-08-29 13:05:46 +02:00
commit 080e3bb928
3 changed files with 181 additions and 40 deletions

View file

@ -700,7 +700,9 @@ func _planet_under_fleet() -> Node2D:
const MENU_BUTTON_HALF: float = 64.0
const MENU_BUTTON_H: float = 100.0
const MENU_COST_HALF: float = 74.0
const MENU_COST_H: float = 44.0
# #147: a price is one resource per line, so a row has to leave room for the
# most any structure asks for - three, at the workshop.
const MENU_COST_H: float = 86.0
const MENU_LABEL_OFFSET: float = 106.0
const MENU_LABEL_GAP: float = 4.0
@ -722,6 +724,29 @@ const MENU_BOTTOM_MARGIN: float = 28.0
const MENU_MAX_COLUMNS: int = 6
# A centre-anchored grid living under a top-anchored header, which is the
# shape both the build menu and the craft menu have (#144, #147). The gap
# between the two depends on the panel's HEIGHT, so it can never be written
# down as a constant - it has to be measured from the header that is
# actually there.
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 + MENU_HEADER_GAP - half, half - MENU_BOTTOM_MARGIN)
# Where one row of such a grid starts. The pitch closes up to fit the band
# and is floored at the row's own height, so rows can tighten but never land
# on each other.
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)
# The vertical band the grid may occupy, in the menu's own centre-anchored
# space: (top, bottom).
#
@ -732,11 +757,7 @@ const MENU_MAX_COLUMNS: int = 6
# height makes the answer right at every window shape, including the one the
# player is actually running.
func menu_band() -> Vector2:
var half: float = build_menu.size.y * 0.5
if half <= 0.0:
half = 640.0
var header: float = build_status.offset_bottom + MENU_HEADER_GAP
return Vector2(header - half, half - MENU_BOTTOM_MARGIN)
return grid_band(build_status.offset_bottom, build_menu.size.y)
# How wide one column is given. Never wider than looks like a grid, never
@ -795,12 +816,8 @@ func _rows_fit(rows: int, columns: int) -> bool:
# other however many structures there are, or however short the window (#136,
# #144).
func menu_row_top(row: int, rows: int, columns: int) -> float:
var height: float = menu_row_height(columns)
var band: Vector2 = menu_band()
var pitch: float = MENU_NATURAL_SPACING
if rows > 1:
pitch = minf(pitch, (band.y - band.x - height) / float(rows - 1))
return band.x + maxf(pitch, height) * float(row)
return grid_row_top(row, rows, menu_row_height(columns), menu_band(),
MENU_NATURAL_SPACING)
func _place_entry(node: Control, cx: float, top: float, half: float, height: float) -> void:
@ -1187,17 +1204,33 @@ func _format_missing_costs(costs: Dictionary) -> String:
# crushed onto a second line under it. Laid out here instead, in two rows of
# three, so a cell is wide enough for the name to breathe - and the price is
# glyphs rather than words (see PriceTag, which every menu now shares).
#
# #147: "prices are far from the button they are about, and when many
# different resources are needed the costs overlap." Both were true, and
# both were the same cause - a cell laid out with more slack than the row
# pitch had room for. The caption box was 62px for a one-line name, which
# pushed the price 70px clear of the button it belonged to; and the price
# was given 184px of width in a 190px column, so a two-resource price ran
# out of its own box and into its neighbour's.
#
# The caption is one line tall now and the price sits directly under it, and
# PriceTag puts each resource on its own line when they will not fit across
# (which in a column this narrow means whenever there is more than one).
# Rows are placed off the measured band rather than a written-down pair of
# tops, for the same reason the build menu's are - see grid_band().
const CRAFT_COLUMNS: int = 3
const CRAFT_ROW_TOPS: Array = [-96.0, 104.0]
const CRAFT_ROW_PITCH: float = 260.0
const CRAFT_COL_X: float = 190.0
const CRAFT_BUTTON_HALF: float = 56.0
const CRAFT_BUTTON_H: float = 112.0
const CRAFT_BUTTON_H: float = 100.0
const CRAFT_LABEL_HALF: float = 92.0
const CRAFT_LABEL_H: float = 62.0
const CRAFT_LABEL_OFFSET: float = 118.0
const CRAFT_LABEL_H: float = 44.0
const CRAFT_LABEL_OFFSET: float = 106.0
const CRAFT_PRICE_HALF: float = 92.0
const CRAFT_PRICE_H: float = 32.0
const CRAFT_PRICE_OFFSET: float = 182.0
# Room for the tallest price any vessel has, stacked. Read from the tag
# rather than assumed, so a vessel priced in three things still fits.
const CRAFT_PRICE_H: float = 86.0
const CRAFT_PRICE_OFFSET: float = 154.0
var craft_price_strips: Array = []
# #137: the ship lab's own price tags, built here rather than authored in the
@ -1250,12 +1283,20 @@ func _build_lab_price_tags() -> void:
lab_price_tags.append(tag)
func craft_row_height() -> float:
return CRAFT_PRICE_OFFSET + CRAFT_PRICE_H
func _layout_craft_menu() -> void:
var rows: int = maxi(int(ceil(float(craft_vessel_buttons.size())
/ float(CRAFT_COLUMNS))), 1)
var band: Vector2 = grid_band(craft_status.offset_bottom, craft_menu.size.y)
for i in range(craft_vessel_buttons.size()):
var row: int = i / CRAFT_COLUMNS
var column: int = i % CRAFT_COLUMNS
var cx: float = (float(column) - 1.0) * CRAFT_COL_X
var top: float = CRAFT_ROW_TOPS[mini(row, CRAFT_ROW_TOPS.size() - 1)]
var top: float = grid_row_top(row, rows, craft_row_height(), band,
CRAFT_ROW_PITCH)
_place_entry(craft_vessel_buttons[i], cx, top, CRAFT_BUTTON_HALF, CRAFT_BUTTON_H)
_place_entry(craft_vessel_labels[i], cx, top + CRAFT_LABEL_OFFSET,
CRAFT_LABEL_HALF, CRAFT_LABEL_H)