diff --git a/controllers/BuildController.gd b/controllers/BuildController.gd index bf067ac..c0c3571 100644 --- a/controllers/BuildController.gd +++ b/controllers/BuildController.gd @@ -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) diff --git a/tests/test_menu_polish.gd b/tests/test_menu_polish.gd index b2640a0..be4cc1d 100644 --- a/tests/test_menu_polish.gd +++ b/tests/test_menu_polish.gd @@ -12,6 +12,9 @@ extends SceneTree # B. #111 - and a price strip really carries the vessel's cost # D. #143 - a hangar caption fits the box it was given and cannot draw # outside it, so it never lands on the icon of the row below +# E. #147 - a price with several resources goes one per line and stays +# inside its own box, close to the button it is about +# F. #147 - and no craft entry overlaps another # C. #120 - the hangar's captions have room for two lines, and there is a # rule between what is parked and what is flying # D. #112 - every skill node has a mark, and a node that unlocks a @@ -89,6 +92,7 @@ func _process(delta: float) -> bool: % [controller.craft_price_strips.size(), controller.craft_vessel_buttons.size()]) var priced: int = 0 + var stacked: int = 0 for i in range(controller.craft_price_strips.size()): var definition: Resource = VesselDefinitions.fleet()[i] var costs: Dictionary = game_state.product_costs(definition.definition_id) @@ -99,9 +103,57 @@ func _process(delta: float) -> bool: return _fail("%s costs %s and its strip is empty" \ % [definition.definition_id, str(costs)]) priced += 1 + # --- E. #147 - a price stays inside its own box ---------- + # A two-resource price used to be laid out across a strip + # narrower than it needed, so it ran off the end and into the + # vessel beside it. + if costs.size() > 1 and not strip.is_stacked(): + return _fail("%s is priced in %d resources on one line - they will not fit %.0fpx" \ + % [definition.definition_id, costs.size(), strip.size.x]) + if costs.size() > 1: + stacked += 1 + if strip.wanted_height() > strip.size.y + 0.5: + return _fail("%s's price needs %.0fpx and its box is %.0f" \ + % [definition.definition_id, strip.wanted_height(), strip.size.y]) + for child in strip.get_children(): + var box: Rect2 = Rect2((child as Control).position, (child as Control).size) + if box.position.x < -0.5 or box.end.x > strip.size.x + 0.5: + return _fail("%s's price runs from %.0f to %.0f in a %.0fpx strip" \ + % [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 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" \ + % [definition.definition_id, gap]) if priced == 0: return _fail("no vessel showed a price at all") - print("%d vessels show their price as glyphs" % priced) + if stacked == 0: + return _fail("no vessel costs more than one resource - E proves nothing") + print("%d vessels show their price as glyphs, %d of them stacked one per line" \ + % [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]: + if i >= part.size() or j >= part.size(): + continue + var a: Control = part[i] + var b: Control = part[j] + 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] + if other.position.y > mine.position.y \ + and Rect2(mine.position, mine.size).intersects( + Rect2(other.position, other.size)): + return _fail("the price under vessel %d runs into vessel %d's button" % [i, k]) + print("craft grid: nothing overlapping, prices under their own buttons") # --- C. the hangar --------------------------------------- controller._layout_hangar() diff --git a/ui/PriceTag.gd b/ui/PriceTag.gd index 64856be..ce14a1d 100644 --- a/ui/PriceTag.gd +++ b/ui/PriceTag.gd @@ -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