#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

@ -31,6 +31,8 @@ enum Align { LEFT, CENTER, RIGHT }
const GLYPH_SIZE: float = 26.0
const GAP: float = 5.0
const PAIR_GAP: float = 18.0
# Between stacked lines (#147).
const STACK_GAP: float = 4.0
const COST_FONT_SIZE: int = 24
# Deliberately smaller than the price. See the note above.
const HAVE_FONT_SIZE: int = 17
@ -40,6 +42,17 @@ const POINT_COLOR: Color = Color(0.72, 0.62, 1.0)
const POINT_MARK: String = "RP"
@export var alignment: int = Align.CENTER
# #147: "each resource type should go on a new line". A multi-resource price
# used to be laid out across one row whatever the tag's width, which under a
# craft button meant running off the end of it and into the vessel beside.
#
# So more than one resource means one per line, everywhere - not only where
# it would otherwise overflow. Both because that is what the ticket asks
# for, and because the overflow case was MARGINAL rather than absent: two
# resources came to 174px in a 184px box, and the box only stopped being
# wide enough once the player was holding three figures rather than one.
# A layout that is correct until you get rich is not correct.
var _stacked: bool = false
# Off for a price with nothing to compare against - a shop with no wallet in
# the same breath. Every menu in the game wants it on; it exists so a caller
# that only has a number can still use this widget rather than inventing a
@ -116,6 +129,27 @@ func is_free() -> bool:
return _entries.is_empty()
# Whether the price is currently one resource per line. For anything that
# needs to leave room for it - and for the test.
func is_stacked() -> bool:
return _stacked
# How tall this price wants to be, laid out as it will actually be laid out.
# Callers size the box from this rather than guessing a number of lines.
func wanted_height() -> float:
if _entries.size() <= 1:
return GLYPH_SIZE
return GLYPH_SIZE * float(_entries.size()) + STACK_GAP * float(_entries.size() - 1)
func _row_width() -> float:
var total: float = 0.0
for entry in _entries:
total += _entry_width(entry) + PAIR_GAP
return maxf(total - PAIR_GAP, 0.0)
# Whether anything on this tag is out of reach. The one question every caller
# used to answer for itself, in its own words.
func is_affordable() -> bool:
@ -208,28 +242,42 @@ func _entry_width(entry: Dictionary) -> float:
return GLYPH_SIZE + GAP + float(entry.have_width) + float(entry.cost_width)
func _layout() -> void:
var total: float = 0.0
for entry in _entries:
total += _entry_width(entry) + PAIR_GAP
total = maxf(total - PAIR_GAP, 0.0)
var x: float = 0.0
func _start_x(width: float) -> float:
match alignment:
Align.CENTER:
x = (size.x - total) * 0.5
return (size.x - width) * 0.5
Align.RIGHT:
x = size.x - total
var mid: float = (size.y - GLYPH_SIZE) * 0.5
return size.x - width
return 0.0
func _place(entry: Dictionary, x: float, top: float, height: float) -> float:
entry.mark.position = Vector2(x, top + (height - GLYPH_SIZE) * 0.5)
entry.mark.size = Vector2(GLYPH_SIZE, GLYPH_SIZE)
x += GLYPH_SIZE + GAP
if entry.have_label != null:
entry.have_label.position = Vector2(x, top)
entry.have_label.size = Vector2(float(entry.have_width), height)
x += float(entry.have_width)
entry.cost_label.position = Vector2(x, top)
entry.cost_label.size = Vector2(float(entry.cost_width), height)
return x + float(entry.cost_width)
func _layout() -> void:
var total: float = _row_width()
_stacked = _entries.size() > 1
if not _stacked:
var x: float = _start_x(total)
for entry in _entries:
x = _place(entry, x, 0.0, size.y) + PAIR_GAP
return
# One resource per line, the block centred in the height it was given so
# a tag with room to spare does not sit against its own top edge.
var block: float = wanted_height()
var y: float = maxf((size.y - block) * 0.5, 0.0)
for entry in _entries:
entry.mark.position = Vector2(x, mid)
entry.mark.size = Vector2(GLYPH_SIZE, GLYPH_SIZE)
x += GLYPH_SIZE + GAP
if entry.have_label != null:
entry.have_label.position = Vector2(x, 0.0)
entry.have_label.size = Vector2(float(entry.have_width), size.y)
x += float(entry.have_width)
entry.cost_label.position = Vector2(x, 0.0)
entry.cost_label.size = Vector2(float(entry.cost_width), size.y)
x += float(entry.cost_width) + PAIR_GAP
_place(entry, _start_x(_entry_width(entry)), y, GLYPH_SIZE)
y += GLYPH_SIZE + STACK_GAP