diff --git a/controllers/BuildController.gd b/controllers/BuildController.gd index 9bb5ae9..539407e 100644 --- a/controllers/BuildController.gd +++ b/controllers/BuildController.gd @@ -109,7 +109,12 @@ var lab_building_id: int = 0 @onready var info_popup: Control = get_node("../UI/BuildMenu/InfoPopup") @onready var info_title: Label = get_node("../UI/BuildMenu/InfoPopup/Panel/TitleLabel") @onready var info_message: Label = get_node("../UI/BuildMenu/InfoPopup/Panel/MessageLabel") -@onready var info_cost: NeonNumber = get_node("../UI/BuildMenu/InfoPopup/Panel/CostNumber") +# Typed Control, not PriceTag, and deliberately. PriceTag reads the GameState +# autoload, and an `extends SceneTree` test script that so much as names a +# class like that fails to compile - GameState is not an identifier in that +# context - which then poisons the scene's copy of the same script. The nodes +# are duck-typed everywhere for that reason; see tests/test_menu_price_unification.gd. +@onready var info_cost: Control = get_node("../UI/BuildMenu/InfoPopup/Panel/CostNumber") @onready var info_build_button: IconButton = get_node("../UI/BuildMenu/InfoPopup/Panel/BuildButton") @onready var info_close_button: IconButton = get_node("../UI/BuildMenu/InfoPopup/Panel/CloseButton") @@ -177,7 +182,7 @@ var garrison_building_id: int = 0 @onready var skill_menu: Control = get_node("../UI/SkillMenu") @onready var skill_tree: Control = get_node("../UI/SkillMenu/Tree") @onready var skill_detail: Label = get_node("../UI/SkillMenu/DetailLabel") -@onready var skill_cost: NeonNumber = get_node("../UI/SkillMenu/CostNumber") +@onready var skill_cost: Control = get_node("../UI/SkillMenu/CostNumber") @onready var skill_status: Label = get_node("../UI/SkillMenu/StatusLabel") @onready var skill_close_button: IconButton = get_node("../UI/SkillMenu/CloseButton") @@ -268,6 +273,7 @@ func _ready() -> void: _seat_hud_columns() _layout_hangar() _build_craft_price_strips() + _build_lab_price_tags() _layout_craft_menu() build_button.pressed.connect(_on_build_pressed) build_close_button.pressed.connect(_close_build_menu) @@ -342,12 +348,12 @@ func _configure_build_menu_entries() -> void: continue var button: IconButton = get_node("../UI/BuildMenu/%s" % definition.menu_button_name) var label: Label = get_node("../UI/BuildMenu/%s" % definition.menu_label_name) - var cost_number: NeonNumber = get_node("../UI/BuildMenu/%s" % definition.menu_cost_name) + var cost_tag: Control = get_node("../UI/BuildMenu/%s" % definition.menu_cost_name) button.icon = definition.menu_icon button.pressed.connect(_show_building_info.bind(String(definition.definition_id))) label.text = definition.display_name - cost_number.amount = definition.cost - build_menu_entries.append([definition.definition_id, button, label, cost_number, definition]) + cost_tag.show_price({GameState.RESOURCE_ORE: int(definition.cost)}) + build_menu_entries.append([definition.definition_id, button, label, cost_tag, definition]) # True while a modal panel owns the shared context controls. A plain building @@ -671,25 +677,83 @@ func _planet_under_fleet() -> Node2D: # entries the grid goes to three columns instead, which takes nine into three # rows - fewer than it uses today, not more. Narrower columns mean narrower # labels, so those get a taller box to wrap into. -const MENU_ROW_TOPS: Array = [-300.0, -77.0, 146.0, 369.0] -const MENU_COL_X: float = 140.0 +# The build menu's grid. +# +# #136: rows used to be a written-down list of tops, read with +# `tops[mini(row, tops.size() - 1)]`. That clamp is the bug the ticket +# reported: with every structure researched the menu wanted a fourth row and +# the list had three, so the last entry was placed on the third row's centre +# slot - the gas burner drawn exactly on top of the transport hub. A list +# that silently stacks whatever will not fit in it is not a layout. +# +# So rows are COMPUTED from a first top and a pitch, and the pitch is +# tightened when the band is not tall enough - never below a row's own +# height, which is the guarantee that two rows can no longer occupy the same +# space whatever the catalog grows to. Columns widen for the same reason: +# a count that would need more rows than the band holds gets another column +# instead of another row. const MENU_BUTTON_HALF: float = 64.0 const MENU_BUTTON_H: float = 100.0 -const MENU_LABEL_HALF: float = 150.0 -const MENU_LABEL_H: float = 52.0 -const MENU_COST_HALF: float = 60.0 +const MENU_COST_HALF: float = 74.0 const MENU_COST_H: float = 44.0 -# Gaps from each row's own top to where the label/cost start, sized to the -# shrunk MENU_*_H values above. const MENU_LABEL_OFFSET: float = 106.0 -const MENU_COST_OFFSET: float = 162.0 -const MENU_WIDE_THRESHOLD: int = 8 -const MENU_ROW_TOPS_WIDE: Array = [-330.0, -85.0, 160.0] -const MENU_COL_X_WIDE: float = 224.0 -const MENU_LABEL_HALF_WIDE: float = 108.0 -const MENU_LABEL_H_WIDE: float = 68.0 -const MENU_COST_OFFSET_WIDE: float = 178.0 +# Per column-count geometry. The two-column figures are the ones the menu has +# always used; the three-column ones came with the wide mode; four columns is +# what ten structures now need to stay inside three rows. +const MENU_COL_X: Dictionary = {2: 140.0, 3: 224.0, 4: 258.0} +const MENU_LABEL_HALF: Dictionary = {2: 150.0, 3: 108.0, 4: 84.0} +const MENU_LABEL_H: Dictionary = {2: 52.0, 3: 68.0, 4: 68.0} +const MENU_COST_OFFSET: Dictionary = {2: 162.0, 3: 178.0, 4: 178.0} +const MENU_ROW_PITCH: Dictionary = {2: 223.0, 3: 245.0, 4: 245.0} + +# The band the grid may occupy, in the menu's own centre-anchored space: from +# under the status line (which sits below the resource bar added in #137) to +# just short of the bottom of a 1280-tall screen. +const MENU_GRID_TOP: float = -252.0 +const MENU_GRID_BOTTOM: float = 612.0 + + +# How tall one row is, end to end - the cost strip is the last thing in it. +func menu_row_height(columns: int) -> float: + return float(MENU_COST_OFFSET[columns]) + MENU_COST_H + + +# The fewest columns that keep the grid inside MENU_MAX_ROWS. Two is the shape +# the menu is meant to have; the wider ones are what a growing catalog forces. +const MENU_MAX_ROWS: int = 4 + + +func menu_columns(count: int) -> int: + for columns in [2, 3, 4]: + var rows: int = int(ceil(float(count) / float(columns))) + if rows <= MENU_MAX_ROWS and _rows_fit(rows, columns): + return columns + return 4 + + +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: + if rows <= 1: + return true + var height: float = menu_row_height(columns) + return (MENU_GRID_BOTTOM - MENU_GRID_TOP - height) / float(rows - 1) >= height + + +# Where one row starts. Top-anchored under the header, with the pitch closed +# up when the band is tight - but never below the row height, so two rows +# cannot land on each other however many structures there are (#136). +func menu_row_top(row: int, rows: int, columns: int) -> float: + var height: float = menu_row_height(columns) + var pitch: float = float(MENU_ROW_PITCH[columns]) + if rows > 1: + pitch = minf(pitch, + (MENU_GRID_BOTTOM - MENU_GRID_TOP - height) / float(rows - 1)) + return MENU_GRID_TOP + maxf(pitch, height) * float(row) func _place_entry(node: Control, cx: float, top: float, half: float, height: float) -> void: @@ -714,13 +778,12 @@ func _layout_build_menu() -> void: if visible: shown.append(entry) - var wide: bool = shown.size() > MENU_WIDE_THRESHOLD - var columns: int = 3 if wide else 2 - var tops: Array = MENU_ROW_TOPS_WIDE if wide else MENU_ROW_TOPS - var col_x: float = MENU_COL_X_WIDE if wide else MENU_COL_X - var label_half: float = MENU_LABEL_HALF_WIDE if wide else MENU_LABEL_HALF - var label_h: float = MENU_LABEL_H_WIDE if wide else MENU_LABEL_H - var cost_offset: float = MENU_COST_OFFSET_WIDE if wide else MENU_COST_OFFSET + var columns: int = menu_columns(shown.size()) + var rows: int = menu_rows(shown.size()) + var col_x: float = float(MENU_COL_X[columns]) + var label_half: float = float(MENU_LABEL_HALF[columns]) + var label_h: float = float(MENU_LABEL_H[columns]) + var cost_offset: float = float(MENU_COST_OFFSET[columns]) for i in range(shown.size()): var row: int = i / columns @@ -730,7 +793,7 @@ func _layout_build_menu() -> void: var col: int = i % columns var cx: float = (float(col) - (float(in_row) - 1.0) * 0.5) * col_x * 2.0 \ / maxf(float(columns) - 1.0, 1.0) - var top: float = tops[mini(row, tops.size() - 1)] + var top: float = menu_row_top(row, rows, columns) _place_entry(shown[i][1], cx, top, MENU_BUTTON_HALF, MENU_BUTTON_H) _place_entry(shown[i][2], cx, top + MENU_LABEL_OFFSET, label_half, label_h) _place_entry(shown[i][3], cx, top + cost_offset, MENU_COST_HALF, MENU_COST_H) @@ -871,12 +934,14 @@ func _show_building_info(type: String) -> void: # charging bank-plus-hold below would hide the build button for something # the player can in fact afford. if can_build and not GameState.can_afford(cost): - lines.append("Not enough rocks - needs %d, you have %d." % [cost, GameState.available_ore()]) + # #137: no sentence about the shortfall. The price tag below carries + # what is held over what is wanted, in red - saying it twice, in two + # different shapes, is what the ticket is about. can_build = false info_title.text = definition.display_name info_message.text = "\n\n".join(lines) - info_cost.amount = cost + info_cost.show_price({GameState.RESOURCE_ORE: cost}) info_build_button.visible = can_build pending_build_type = type if can_build else "" info_popup.visible = true @@ -967,7 +1032,7 @@ func _refresh_skill_menu(id: String = "") -> void: # Nothing chosen yet: point the player at the tree rather than # leaving the panel blank, which reads as broken. skill_detail.text = "Select a node to see what it unlocks." - skill_cost.amount = 0 + skill_cost.show_price({}) skill_status.text = "Fleet capacity %d · Research points %d" \ % [GameState.max_fleet_size(), GameState.research_points] return @@ -976,25 +1041,26 @@ func _refresh_skill_menu(id: String = "") -> void: if node.is_empty(): return var points_needed: int = GameState.skill_research_point_cost(id) + # #134: the explanation and the price no longer share a column. The text + # is the left half of the band and the tag is the right half, so a long + # description can grow without ending up on top of the number - which is + # what it used to do, the two being centred over each other. + # + # Both shortfalls it used to spell out - ore and research points - are + # now entries on the tag, held-over-needed, in red when short. One place, + # and it says WHICH one is missing without a sentence per case. skill_detail.text = "%s\n%s" % [node.name, node.desc] - if points_needed > 0: - skill_detail.text += "\nResearch points: %d/%d" % [GameState.research_points, points_needed] - skill_cost.amount = int(node.cost) + skill_cost.show_price({GameState.RESOURCE_ORE: int(node.cost)}, points_needed) if GameState.is_unlocked(id): skill_status.text = "Researched" elif not GameState.prerequisite_met(id): skill_status.text = "Needs %s first" % GameState.skill_node(node.requires).name - elif not GameState.can_afford(int(node.cost)): - # available_ore(), not the bank alone: research is paid for the same - # pooled way as everything else, so quoting the bank here would tell - # the player they cannot afford something they can. - skill_status.text = "Not enough rocks - you have %d" % GameState.available_ore() - elif GameState.research_points < points_needed: - # #63: a distinct reason from the ore one above - conflating them - # would leave the player trying to fix the wrong shortage. - skill_status.text = "Not enough research points - you have %d, need %d" \ - % [GameState.research_points, points_needed] + elif not skill_cost.is_affordable(): + # Deliberately blank. The red on the tag is the whole message; the + # line here used to repeat it in words and, being centred, repeat it + # on top of the number as well. + skill_status.text = "" else: # The button that used to appear here was the only thing saying "this # is buyable". With it gone the line has to say so itself, or an @@ -1050,7 +1116,7 @@ func _format_missing_costs(costs: Dictionary) -> String: # overlapping each other, which left "Energy Propulser" clipped and the price # 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 VesselPriceStrip). +# glyphs rather than words (see PriceTag, which every menu now shares). const CRAFT_COLUMNS: int = 3 const CRAFT_ROW_TOPS: Array = [-96.0, 104.0] const CRAFT_COL_X: float = 190.0 @@ -1064,10 +1130,14 @@ const CRAFT_PRICE_H: float = 32.0 const CRAFT_PRICE_OFFSET: float = 182.0 var craft_price_strips: Array = [] +# #137: the ship lab's own price tags, built here rather than authored in the +# scene for the same reason the craft menu's are - four identical widgets +# differing only in where they sit. +var lab_price_tags: Array = [] func _build_craft_price_strips() -> void: - var strip_script: Script = load("res://ui/VesselPriceStrip.gd") + var strip_script: Script = load("res://ui/PriceTag.gd") for i in range(craft_vessel_buttons.size()): var strip: Control = strip_script.new() strip.name = "CraftPrice%d" % i @@ -1082,6 +1152,34 @@ func _build_craft_price_strips() -> void: craft_price_strips.append(strip) +# One tag per upgrade slot, seated in the gap the caption's second line used +# to occupy. The captions above them shrink to one line to make room, so the +# option block is the same height it was and the row below it does not move. +const LAB_TAG_H: float = 38.0 +const LAB_LABEL_H: float = 40.0 + + +func _build_lab_price_tags() -> void: + var tag_script: Script = load("res://ui/PriceTag.gd") + for i in range(lab_upgrade_labels.size()): + var label: Control = lab_upgrade_labels[i] + var tag: Control = tag_script.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 + # Under the caption, which is cut to a single line here so the two + # together occupy what the two-line caption used to. + label.offset_bottom = label.offset_top + LAB_LABEL_H + tag.offset_top = label.offset_bottom + 2.0 + tag.offset_bottom = tag.offset_top + LAB_TAG_H + lab_menu.add_child(tag) + lab_price_tags.append(tag) + + func _layout_craft_menu() -> void: for i in range(craft_vessel_buttons.size()): var row: int = i / CRAFT_COLUMNS @@ -1220,19 +1318,28 @@ func _refresh_lab() -> void: var visible: bool = i < options.size() lab_upgrade_buttons[i].visible = visible lab_upgrade_labels[i].visible = visible + if i < lab_price_tags.size(): + lab_price_tags[i].visible = visible if not visible: continue var option: Dictionary = options[i] - var price_text: String = _format_costs(option.costs if bool(option.unlock) \ - else GameState.research_costs(option.id)) - var point_cost: int = GameState.research_point_cost(String(option.id)) - if point_cost > 0: - price_text += " · %d RP" % point_cost - if not bool(option.unlock) and GameState.upgrade_maxed(option.id): - price_text = "MAX" + # #137: the price is a tag under the option, the way it is under a + # build option and under a vessel in the craft menu. It used to be a + # second line of the caption - "25 ore · 10 gas · 2 RP" - which is + # the same information in a shape no other menu used. + var maxed: bool = not bool(option.unlock) and GameState.upgrade_maxed(option.id) var level_note: String = "" if bool(option.unlock) else " %d/%d" % [ GameState.upgrade_level(option.id), int(GameState.upgrade_def(option.id).max_level)] - lab_upgrade_labels[i].text = "%s%s\n%s" % [option.name, level_note, price_text] + lab_upgrade_labels[i].text = "%s%s" % [option.name, level_note] + if i < lab_price_tags.size(): + var tag: Control = lab_price_tags[i] + tag.visible = not maxed + if maxed: + tag.show_price({}) + else: + tag.show_price(option.costs if bool(option.unlock) \ + else GameState.research_costs(option.id), + GameState.research_point_cost(String(option.id))) lab_upgrade_buttons[i].icon = _upgrade_icon(option) lab_upgrade_buttons[i].glow_color = _vessel_colour(lab_vessel) lab_upgrade_buttons[i].highlighted = i == lab_choice diff --git a/tests/test_build_info_popup.gd b/tests/test_build_info_popup.gd index 28c8d86..f6c7948 100644 --- a/tests/test_build_info_popup.gd +++ b/tests/test_build_info_popup.gd @@ -20,7 +20,9 @@ var controller: Node var popup: Control var title: Label var message: Label -var cost_number: NeonNumber +# Control, not PriceTag: naming a class whose body reads the GameState +# autoload does not compile inside an `extends SceneTree` script. +var cost_number: Control var build_button: IconButton var stage: int = 0 var elapsed: float = 0.0 @@ -89,11 +91,12 @@ func _process(delta: float) -> bool: var informed: bool = popup.visible \ and title.text == option.title \ and message.text.length() > 40 \ - and cost_number.amount == expected_cost \ + and cost_number.priced(GameState.RESOURCE_ORE) == expected_cost \ and build_button.visible var untouched: bool = game_state.rocks == rocks_before and _count(type) == built_before print("%s: popup=%s title='%s' cost=%d offered=%s nothing_spent_yet=%s" % [ - type, popup.visible, title.text, cost_number.amount, + type, popup.visible, title.text, + cost_number.priced(GameState.RESOURCE_ORE), build_button.visible, untouched ]) if not (informed and untouched): diff --git a/tests/test_build_menu_hides_locked.gd b/tests/test_build_menu_hides_locked.gd index aa60234..be54698 100644 --- a/tests/test_build_menu_hides_locked.gd +++ b/tests/test_build_menu_hides_locked.gd @@ -17,6 +17,9 @@ extends SceneTree # 4. an entry alone on its row is centred, not hanging in the left column # 5. researched-but-not-usable is still shown greyed (an extractor with no # planet under the fleet is "not now", not "not yet") +# 6. #136 - with every structure researched, no two entries overlap. The +# grid used to be a fixed list of row positions read with a clamp, so +# the first row that would not fit was drawn on top of another one var game_state: Node var scene: Node @@ -45,6 +48,12 @@ func _shown() -> Array: return out +func _rect(node: Control) -> Rect2: + return Rect2(Vector2(node.offset_left, node.offset_top), + Vector2(node.offset_right - node.offset_left, + node.offset_bottom - node.offset_top)) + + func _slots() -> Array: # (row, x) of every visible button, in menu order. var out: Array = [] @@ -100,21 +109,51 @@ func _process(delta: float) -> bool: # --- 3. consecutive slots, no holes ------------------------- var slots: Array = _slots() - var rows: Array = controller.MENU_ROW_TOPS + var columns: int = controller.menu_columns(slots.size()) + var rows: int = controller.menu_rows(slots.size()) + if columns != 2: + print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (4 entries laid out in %d columns)" % columns) + quit(1) + return true for i in range(slots.size()): - var want_row: float = rows[i / 2] + var want_row: float = controller.menu_row_top(i / columns, rows, columns) if absf(slots[i][0] - want_row) > 0.01: - print("slot %d at y=%.1f, expected row %d at y=%.1f" % [i, slots[i][0], i / 2, want_row]) + print("slot %d at y=%.1f, expected row %d at y=%.1f" % [i, slots[i][0], i / columns, want_row]) print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (a gap was left where a hidden entry used to be)") quit(1) return true - var want_x: float = -controller.MENU_COL_X if i % 2 == 0 else controller.MENU_COL_X + var col_x: float = float(controller.MENU_COL_X[columns]) + var want_x: float = -col_x if i % 2 == 0 else col_x if absf(slots[i][1] - want_x) > 0.01: print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (entry %d at x=%.1f, expected %.1f)" % [i, slots[i][1], want_x]) quit(1) return true print("4 entries packed into consecutive slots, no gaps") + # --- 6. #136: nothing overlaps, with EVERY structure on offer - + # The bug the ticket reported: the row list ran out and the last + # entry was placed on top of one already there. + for node in GameState.SKILL_TREE: + game_state.unlocked.append(String(node.id)) + controller._refresh_build_status() + var all_shown: Array = _shown() + if all_shown.size() < 9: + print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (only %d structures unlocked - not the crowded case)" % all_shown.size()) + quit(1) + return true + for i in range(all_shown.size()): + for j in range(i + 1, all_shown.size()): + for part in [1, 2, 3]: + var a: Control = all_shown[i][part] + var b: Control = all_shown[j][part] + if _rect(a).intersects(_rect(b)): + print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (%s and %s overlap at %s / %s)" \ + % [all_shown[i][0], all_shown[j][0], _rect(a), _rect(b)]) + quit(1) + return true + print("%d structures on offer, in %d columns, nothing overlapping" \ + % [all_shown.size(), controller.menu_columns(all_shown.size())]) + # --- 5. researched but not usable is shown, greyed ---------- var extractor_shown: bool = false for e in shown2: diff --git a/tests/test_menu_price_unification.gd b/tests/test_menu_price_unification.gd new file mode 100644 index 0000000..82e138e --- /dev/null +++ b/tests/test_menu_price_unification.gd @@ -0,0 +1,192 @@ +extends SceneTree + +# Headless test: run with +# godot --headless -s res://tests/test_menu_price_unification.gd +# Forgejo #137: "the resources at the top are not always shown the same way, +# the price is not always put at the same place... the build menu, the +# research menu, the ship crafter menu would benefit from a more unified +# UX." And #134, which is the same complaint aimed at the skill panel: the +# explanation could land on top of the price, and a shortfall was a sentence +# instead of a number. +# +# The point of the ticket is CONSISTENCY, which is a property of several +# things at once - so this test compares the menus against each other rather +# than against written-down coordinates. A menu that moves is fine; a menu +# that moves away from the others is the bug. +# +# Covers: +# A. every buying menu has a resource bar, in the same place, the same size +# B. every price is the same widget - one class, not three presentations +# C. a price says what you HAVE as well as what it COSTS, with the held +# figure smaller than the price (#134) and red when it is short +# D. #134 - the skill panel's explanation is left of its price, not on top +# of it, and the two do not overlap at any description length +# E. the shortfall is no longer also spelled out in a sentence + +const PRICED_MENUS: Array = ["BuildMenu", "CraftMenu", "LabMenu"] +# Compared by SCRIPT PATH, as a string, and that is not fussiness. +# ResourceBar and PriceTag both read the GameState autoload, and an +# `extends SceneTree` script that so much as pulls one of them in - naming +# the class, or preloading the file - fails to compile, because GameState is +# not an identifier in this context. Worse, GDScript caches that failure, so +# the scene instantiated a few lines below would get a Control with no script +# where its resource bar should be. A string names the file without asking +# for it, and "is this the same script" is the question anyway. +const RESOURCE_BAR_PATH: String = "res://ui/ResourceBar.gd" +const PRICE_TAG_PATH: String = "res://ui/PriceTag.gd" + +var game_state: Node +var scene: Node +var controller: Node +var stage: int = 0 +var elapsed: float = 0.0 + + +func _initialize() -> void: + # GameState is an autoload: a bare GameState. does not + # compile in a SceneTree script, so the node is fetched by name instead. + game_state = root.get_node("GameState") + game_state.reset_new_game() + game_state.intro_active = false + game_state.reset_quests() + game_state.rocks = 40 + + +func _fail(message: String) -> bool: + print("TEST_MENU_PRICE_UNIFICATION: FAIL (%s)" % message) + quit(1) + return true + + +func _script_path(node: Node) -> String: + var script: Script = node.get_script() + return "" if script == null else script.resource_path + + +# The RESOLVED rectangle, not the offsets. Two Controls with different +# anchors have offsets measured from different edges, so comparing those +# compares nothing - and subtracting them produces negative sizes, which +# Rect2.intersects refuses outright. +func _rect(node: Control) -> Rect2: + return node.get_rect() + + +func _process(delta: float) -> bool: + elapsed += delta + match stage: + 0: + scene = load("res://world/Main.tscn").instantiate() + root.add_child(scene) + current_scene = scene + controller = scene.get_node("BuildController") + elapsed = 0.0 + stage = 1 + return false + 1: + if elapsed < 0.4: + return false + + # --- A. the same bar, in the same place ------------------ + var bars: Dictionary = {} + for menu in PRICED_MENUS: + var bar: Control = scene.get_node_or_null("UI/%s/Bar" % menu) + if bar == null: + return _fail("%s has no resource bar - the other menus do" % menu) + if _script_path(bar) != RESOURCE_BAR_PATH: + return _fail("%s's bar is not a ResourceBar" % menu) + bars[menu] = bar + var reference: Rect2 = _rect(bars[PRICED_MENUS[0]]) + for menu in PRICED_MENUS: + var here: Rect2 = _rect(bars[menu]) + if here != reference: + return _fail("%s's resource bar sits at %s, the others at %s" \ + % [menu, here, reference]) + if bars[menu].anchor_right != bars[PRICED_MENUS[0]].anchor_right: + return _fail("%s's resource bar is anchored differently" % menu) + print("%d menus, one resource bar geometry: %s" % [PRICED_MENUS.size(), reference]) + + # --- 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) + if tags.size() < 15: + return _fail("only %d price widgets found - the menus cannot all be covered" % tags.size()) + for tag in tags: + if _script_path(tag) != PRICE_TAG_PATH: + return _fail("a price is drawn by %s, not the shared price tag" \ + % _script_path(tag)) + print("%d prices across the build, craft, lab and skill menus, all one widget" % tags.size()) + + # --- 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 + 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") + if probe.held(GameState.RESOURCE_ORE) != 3: + return _fail("the tag reads %d held, the player has 3" \ + % probe.held(GameState.RESOURCE_ORE)) + if not probe.is_short(GameState.RESOURCE_ORE): + return _fail("3 out of 25 is not marked short") + if probe.is_affordable(): + return _fail("a tag the player cannot pay reads as affordable") + # Read off the labels the player actually sees, rather than off + # the constants behind them. + var entry: Dictionary = probe._entry_for(GameState.RESOURCE_ORE) + var have_size: int = entry.have_label.get_theme_font_size("font_size") + var cost_size: int = entry.cost_label.get_theme_font_size("font_size") + if have_size >= cost_size: + return _fail("what you hold (size %d) is not drawn smaller than the price (size %d)" \ + % [have_size, cost_size]) + var short_colour: Color = entry.have_label.get_theme_color("font_color") + if short_colour.r <= short_colour.g or short_colour.r <= short_colour.b: + return _fail("the short figure is drawn in %s, which is not a warning colour" % short_colour) + print("price tag: %d/%d ore, held figure at size %d in %s, price at size %d" \ + % [probe.held(GameState.RESOURCE_ORE), probe.priced(GameState.RESOURCE_ORE), + have_size, short_colour, cost_size]) + # ...and the affordable case is not red, or the colour says nothing. + game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 900) + probe.show_price({GameState.RESOURCE_ORE: 25}) + if probe.is_short(GameState.RESOURCE_ORE) or not probe.is_affordable(): + return _fail("900 ore does not cover a price of 25") + if probe._entry_for(GameState.RESOURCE_ORE).have_label \ + .get_theme_color("font_color") == short_colour: + return _fail("an affordable price is still drawn in the shortfall colour") + print("and an affordable price is drawn differently") + + # --- D. #134: the explanation is beside the price -------- + var longest: String = "" + for node in GameState.SKILL_TREE: + 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) + if detail.intersects(price): + return _fail("the explanation (%s) overlaps the price (%s)" % [detail, price]) + if detail.position.x >= price.position.x: + return _fail("the explanation is not left of the price") + print("skill panel: explanation at x=%.0f, price at x=%.0f, no overlap (longest of %d descriptions)" \ + % [detail.position.x, price.position.x, GameState.SKILL_TREE.size()]) + + # --- E. and the shortfall is not also a sentence --------- + game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0) + 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(): + return _fail("a node reads as affordable with no ore at all") + if controller.skill_status.text.to_lower().contains("not enough"): + return _fail("the shortfall is still written out as well: '%s'" \ + % controller.skill_status.text) + print("the shortfall is shown once, on the price, and not repeated in words") + + print("TEST_MENU_PRICE_UNIFICATION: PASS") + quit(0) + return true + return false diff --git a/tests/test_menu_price_unification.gd.uid b/tests/test_menu_price_unification.gd.uid new file mode 100644 index 0000000..a792cc7 --- /dev/null +++ b/tests/test_menu_price_unification.gd.uid @@ -0,0 +1 @@ +uid://py5ckuutwdi6 diff --git a/tests/test_research_double_tap.gd b/tests/test_research_double_tap.gd index 39216a9..3495ec7 100644 --- a/tests/test_research_double_tap.gd +++ b/tests/test_research_double_tap.gd @@ -125,9 +125,15 @@ func _process(delta: float) -> bool: if game_state.is_unlocked(second): return _fail("a node was bought with no ore at all") controller._refresh_skill_menu(second) - if controller.skill_status.text == "": - return _fail("an unaffordable node was refused silently") - print("unaffordable node refused, and says so: '%s'" % controller.skill_status.text) + # #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(): + return _fail("a node the player cannot pay for reads as affordable") + if not controller.skill_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)]) print("TEST_RESEARCH_DOUBLE_TAP: PASS") quit(0) diff --git a/ui/PriceTag.gd b/ui/PriceTag.gd new file mode 100644 index 0000000..7b4927a --- /dev/null +++ b/ui/PriceTag.gd @@ -0,0 +1,198 @@ +class_name PriceTag +extends Control + +# Forgejo #137 (and #134): one way of saying what something costs. +# +# The game had three. The build menu drew a NeonNumber under each option; the +# craft menu drew glyph-and-number strips (#111); the ship lab spelled prices +# out inside the option's caption - "25 ore · 10 gas · 2 RP" - and the skill +# tree put the number in the middle of the panel with a sentence underneath +# saying whether you could afford it. Four menus, four answers to the same +# question, and a player learning one of them learned nothing about the next. +# +# This is that answer, once: for each thing a purchase costs, its glyph, then +# what you HAVE over what it COSTS. +# +# [ore] 12/25 [gas] 40/10 +# +# #134 asks for the two numbers to be told apart by more than position, so +# the amount you have is drawn smaller than the price - a price is a fact +# about the thing, what you hold is a fact about you - and it turns red when +# it is not enough. That replaces the "Not enough rocks - you have 12" line +# the skill panel used to carry: the shortfall is now shown where the price +# is rather than described in a sentence somewhere else on the panel. +# +# Research points ride along as an entry with a written mark rather than a +# glyph, because they are not a resource - they have no inventory, no colour +# in the catalog, and nothing carries them. + +enum Align { LEFT, CENTER, RIGHT } + +const GLYPH_SIZE: float = 26.0 +const GAP: float = 5.0 +const PAIR_GAP: float = 18.0 +const COST_FONT_SIZE: int = 24 +# Deliberately smaller than the price. See the note above. +const HAVE_FONT_SIZE: int = 17 +const SHORT_COLOR: Color = Color(1.0, 0.36, 0.32) +const HAVE_COLOR: Color = Color(0.60, 0.73, 0.82) +const POINT_COLOR: Color = Color(0.72, 0.62, 1.0) +const POINT_MARK: String = "RP" + +@export var alignment: int = Align.CENTER +# 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 +# fifth way to draw one. +@export var show_available: bool = true + +var _entries: Array = [] + + +func _ready() -> void: + mouse_filter = Control.MOUSE_FILTER_IGNORE + resized.connect(_layout) + + +# costs: {resource_type: amount}. points: research points, 0 for none. +func show_price(costs: Dictionary, points: int = 0) -> void: + _clear() + for resource_type in GameState.resource_types(): + var amount: int = int(costs.get(resource_type, 0)) + if amount <= 0: + continue + _add_entry(String(resource_type), "", amount, + GameState.available_resource(String(resource_type)), + GameState.resource_color(resource_type)) + if points > 0: + _add_entry("", POINT_MARK, points, GameState.research_points, POINT_COLOR) + _layout() + + +# The old name, kept because the craft menu speaks in costs alone and reads +# better for it. +func set_costs(costs: Dictionary) -> void: + show_price(costs) + + +func is_free() -> bool: + return _entries.is_empty() + + +# 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: + for entry in _entries: + if int(entry.have) < int(entry.cost): + return false + return true + + +# What this tag says about one resource. Accessors rather than a public +# dictionary so a caller - or a test - reads the same numbers the player +# does, off the thing that drew them. +func priced(resource_type: String) -> int: + return int(_entry_for(resource_type).get("cost", 0)) + + +func held(resource_type: String) -> int: + return int(_entry_for(resource_type).get("have", 0)) + + +func is_short(resource_type: String) -> bool: + return bool(_entry_for(resource_type).get("short", false)) + + +func _entry_for(resource_type: String) -> Dictionary: + for entry in _entries: + if String(entry.resource) == resource_type: + return entry + return {} + + +func _clear() -> void: + for entry in _entries: + for node in [entry.mark, entry.have_label, entry.cost_label]: + if node != null: + node.queue_free() + _entries.clear() + + +func _add_entry(resource_type: String, mark_text: String, cost: int, have: int, + colour: Color) -> void: + var mark: Control + if resource_type != "": + var glyph := ResourceGlyph.new() + glyph.resource_type = resource_type + glyph.mouse_filter = Control.MOUSE_FILTER_IGNORE + mark = glyph + else: + mark = _make_label(mark_text, HAVE_FONT_SIZE, colour) + add_child(mark) + + var short: bool = have < cost + var have_label: Label = null + if show_available: + have_label = _make_label("%d/" % have, HAVE_FONT_SIZE, + SHORT_COLOR if short else HAVE_COLOR) + add_child(have_label) + var cost_label: Label = _make_label(str(cost), COST_FONT_SIZE, colour) + add_child(cost_label) + + _entries.append({ + "resource": resource_type, + "mark": mark, "have_label": have_label, "cost_label": cost_label, + "cost": cost, "have": have, "short": short, + "have_width": 0.0 if have_label == null else _text_width(have_label, HAVE_FONT_SIZE), + "cost_width": _text_width(cost_label, COST_FONT_SIZE), + }) + + +func _make_label(text: String, font_size: int, colour: Color) -> Label: + var label := Label.new() + label.text = text + label.mouse_filter = Control.MOUSE_FILTER_IGNORE + label.add_theme_font_size_override("font_size", font_size) + label.add_theme_color_override("font_color", colour) + label.add_theme_color_override("font_outline_color", Color(0.01, 0.01, 0.035, 0.95)) + label.add_theme_constant_override("outline_size", 2) + label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + return label + + +func _text_width(label: Label, font_size: int) -> float: + var font: Font = label.get_theme_font("font") + if font == null: + return float(label.text.length()) * float(font_size) * 0.6 + return font.get_string_size(label.text, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size).x + + +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 + match alignment: + Align.CENTER: + x = (size.x - total) * 0.5 + Align.RIGHT: + x = size.x - total + var mid: float = (size.y - GLYPH_SIZE) * 0.5 + + 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 diff --git a/ui/PriceTag.gd.uid b/ui/PriceTag.gd.uid new file mode 100644 index 0000000..6dae455 --- /dev/null +++ b/ui/PriceTag.gd.uid @@ -0,0 +1 @@ +uid://4qp8g8ogbigj diff --git a/ui/ResourceBar.gd b/ui/ResourceBar.gd index 5661a19..019e57b 100644 --- a/ui/ResourceBar.gd +++ b/ui/ResourceBar.gd @@ -26,7 +26,7 @@ var _header_labels: Array[Label] = [] var _rows: Dictionary = {} # The value columns, in the order they are drawn. LOCAL is dropped from this -# list at a building that IS the bank - see _local_is_banked(). +# list where it would only repeat another column - see _local_is_redundant(). const VALUE_KEYS: Array = ["bank", "fleet", "local"] var _local_shown: bool = true @@ -79,15 +79,22 @@ func _make_value_label(colour: Color) -> Label: return label +# Whether a LOCAL column would say anything a reader could act on. Two ways +# it would not: +# # #95: at a building that IS the bank - the mothership carries both the "bank" # and the "research" capability, so the research menu is always opened from one # - building_inventory() redirects to bank_inventory itself, and LOCAL is not # merely equal to BANKED but the very same store read twice. Printing the -# number again teaches nothing and costs a third of the table, so the column -# goes away there and the remaining two share the width. -func _local_is_banked() -> bool: +# number again teaches nothing and costs a third of the table. +# +# #137: and at no building at all, which is how the build menu opens it - it +# is reached from the fleet's own button rather than from a structure. There +# is no "here" to report on, and a column of em-dashes is worth less than the +# width it takes from the two columns that do say something. +func _local_is_redundant() -> bool: if building_id == 0: - return false + return true return GameState.building_inventory(building_id) == GameState.bank_inventory @@ -142,7 +149,7 @@ func _layout() -> void: func _process(_delta: float) -> void: # building_id is assigned by whichever panel opens, so the column count can # change between frames; re-lay out only when it actually flips. - var local_shown: bool = not _local_is_banked() + var local_shown: bool = not _local_is_redundant() if local_shown != _local_shown: _local_shown = local_shown _layout() diff --git a/ui/VesselPriceStrip.gd b/ui/VesselPriceStrip.gd deleted file mode 100644 index 293e2de..0000000 --- a/ui/VesselPriceStrip.gd +++ /dev/null @@ -1,80 +0,0 @@ -extends Control - -# #111: what a vessel costs, in glyphs. -# -# The craft menu used to spell prices out - "15 ore · 5 gas" - which is three -# words of chrome per button on a screen with six of them, and it left the -# vessel's own name fighting for the same two lines. Every resource already -# has a glyph the resource bar and the transfer list draw; this reuses it, so -# a price reads as a number beside a mark rather than as a sentence. -# -# Built in code from the costs it is given, so a vessel whose price changes - -# or a resource added to the catalog - needs nothing here. - -const GLYPH_SIZE: float = 26.0 -const GAP: float = 4.0 -const PAIR_GAP: float = 14.0 -const FONT_SIZE: int = 24 - -var _pairs: Array = [] - - -func _ready() -> void: - mouse_filter = Control.MOUSE_FILTER_IGNORE - - -func set_costs(costs: Dictionary) -> void: - for pair in _pairs: - pair.glyph.queue_free() - pair.label.queue_free() - _pairs.clear() - - for resource_type in GameState.resource_types(): - var amount: int = int(costs.get(resource_type, 0)) - if amount <= 0: - continue - var glyph := ResourceGlyph.new() - glyph.resource_type = String(resource_type) - glyph.mouse_filter = Control.MOUSE_FILTER_IGNORE - add_child(glyph) - var label := Label.new() - label.text = str(amount) - label.mouse_filter = Control.MOUSE_FILTER_IGNORE - label.add_theme_font_size_override("font_size", FONT_SIZE) - label.add_theme_color_override("font_color", GameState.resource_color(resource_type)) - label.add_theme_color_override("font_outline_color", Color(0.01, 0.01, 0.035, 0.95)) - label.add_theme_constant_override("outline_size", 2) - label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - add_child(label) - _pairs.append({"glyph": glyph, "label": label, "width": _text_width(label)}) - _layout() - - -func _text_width(label: Label) -> float: - var font: Font = label.get_theme_font("font") - if font == null: - return 20.0 - return font.get_string_size(label.text, HORIZONTAL_ALIGNMENT_LEFT, -1.0, - FONT_SIZE).x - - -func _layout() -> void: - # Centred as a block: a one-resource price and a two-resource price - # should both sit under the middle of the button they belong to. - var total: float = 0.0 - for pair in _pairs: - total += GLYPH_SIZE + GAP + float(pair.width) + PAIR_GAP - total = maxf(total - PAIR_GAP, 0.0) - var x: float = (size.x - total) * 0.5 - var mid: float = (size.y - GLYPH_SIZE) * 0.5 - for pair in _pairs: - pair.glyph.position = Vector2(x, mid) - pair.glyph.size = Vector2(GLYPH_SIZE, GLYPH_SIZE) - x += GLYPH_SIZE + GAP - pair.label.position = Vector2(x, 0.0) - pair.label.size = Vector2(float(pair.width), size.y) - x += float(pair.width) + PAIR_GAP - - -func is_free() -> bool: - return _pairs.is_empty() diff --git a/ui/VesselPriceStrip.gd.uid b/ui/VesselPriceStrip.gd.uid deleted file mode 100644 index a2db65f..0000000 --- a/ui/VesselPriceStrip.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c1xt1uhl8f023 diff --git a/world/Main.tscn b/world/Main.tscn index 99e4a6f..0cb0a8b 100644 --- a/world/Main.tscn +++ b/world/Main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=33 format=3] +[gd_scene load_steps=34 format=3] [ext_resource type="Script" path="res://world/Starfield.gd" id="1"] [ext_resource type="Script" path="res://vessels/Fleet.gd" id="2"] @@ -41,6 +41,7 @@ [ext_resource type="Script" path="res://ui/SpeedFader.gd" id="31"] [ext_resource type="Script" path="res://world/QuestDirector.gd" id="quest_dir"] [ext_resource type="Script" path="res://ui/QuestTracker.gd" id="quest_hud"] +[ext_resource type="Script" path="res://ui/PriceTag.gd" id="price_tag"] [sub_resource type="ShaderMaterial" id="1"] shader = ExtResource("5") @@ -477,19 +478,28 @@ color = Color(0.03, 0.03, 0.08, 0.94) mouse_filter = 2 anchor_right = 1.0 offset_left = 24 -offset_top = 70 +offset_top = 20 offset_right = -24 -offset_bottom = 196 +offset_bottom = 140 text = "Build" horizontal_alignment = 1 +[node name="Bar" type="Control" parent="UI/BuildMenu"] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24.0 +offset_top = 148.0 +offset_right = -24.0 +offset_bottom = 284.0 +script = ExtResource("24") + [node name="StatusLabel" type="Label" parent="UI/BuildMenu"] mouse_filter = 2 anchor_right = 1.0 offset_left = 24 -offset_top = 206 +offset_top = 292 offset_right = -24 -offset_bottom = 330 +offset_bottom = 372 theme_override_font_sizes/font_size = 40 autowrap_mode = 2 text = "" @@ -533,9 +543,7 @@ offset_left = -200 offset_right = -80 offset_top = -96 offset_bottom = -44 -script = ExtResource("19") -amount = 20 -digit_height = 40.0 +script = ExtResource("price_tag") [node name="CrafterButton" type="Control" parent="UI/BuildMenu"] anchor_left = 0.5 @@ -575,9 +583,7 @@ offset_left = 80 offset_right = 200 offset_top = -96 offset_bottom = -44 -script = ExtResource("19") -amount = 12 -digit_height = 40.0 +script = ExtResource("price_tag") [node name="CommandButton" type="Control" parent="UI/BuildMenu"] anchor_left = 0.5 @@ -617,9 +623,7 @@ offset_left = -200 offset_right = -80 offset_top = 194 offset_bottom = 246 -script = ExtResource("19") -amount = 15 -digit_height = 40.0 +script = ExtResource("price_tag") [node name="ExtractorButton" type="Control" parent="UI/BuildMenu"] anchor_left = 0.5 @@ -659,9 +663,7 @@ offset_left = 80 offset_right = 200 offset_top = 194 offset_bottom = 246 -script = ExtResource("19") -amount = 25 -digit_height = 40.0 +script = ExtResource("price_tag") [node name="TransportHubButton" type="Control" parent="UI/BuildMenu"] anchor_left = 0.5 @@ -701,9 +703,7 @@ offset_left = -60 offset_right = 60 offset_top = 484 offset_bottom = 536 -script = ExtResource("19") -amount = 30 -digit_height = 40.0 +script = ExtResource("price_tag") [node name="ShipLabButton" type="Control" parent="UI/BuildMenu"] anchor_left = 0.5 @@ -743,9 +743,7 @@ offset_left = 80 offset_right = 200 offset_top = 484 offset_bottom = 536 -script = ExtResource("19") -amount = 35 -digit_height = 40.0 +script = ExtResource("price_tag") [node name="GasBurnerButton" type="Control" parent="UI/BuildMenu"] anchor_left = 0.5 @@ -785,9 +783,7 @@ offset_left = -60 offset_right = 60 offset_top = 774 offset_bottom = 826 -script = ExtResource("19") -amount = 30 -digit_height = 40.0 +script = ExtResource("price_tag") [node name="OutpostButton" type="Control" parent="UI/BuildMenu"] anchor_left = 0.5 @@ -827,9 +823,7 @@ offset_left = -60 offset_right = 60 offset_top = 1034 offset_bottom = 1086 -script = ExtResource("19") -amount = 30 -digit_height = 40.0 +script = ExtResource("price_tag") [node name="PlanningButton" type="Control" parent="UI/BuildMenu"] anchor_left = 0.5 @@ -869,9 +863,7 @@ offset_left = -60 offset_right = 60 offset_top = 1294 offset_bottom = 1346 -script = ExtResource("19") -amount = 70 -digit_height = 40.0 +script = ExtResource("price_tag") [node name="WorkshopButton" type="Control" parent="UI/BuildMenu"] anchor_left = 0.5 @@ -911,9 +903,7 @@ offset_left = -60 offset_right = 60 offset_top = 1554 offset_bottom = 1606 -script = ExtResource("19") -amount = 55 -digit_height = 40.0 +script = ExtResource("price_tag") [node name="CloseButton" type="Control" parent="UI/BuildMenu"] anchor_left = 1.0 @@ -982,9 +972,7 @@ offset_left = -70 offset_right = 90 offset_top = -424 offset_bottom = -334 -script = ExtResource("19") -amount = 0 -digit_height = 78.0 +script = ExtResource("price_tag") [node name="BuildButton" type="Control" parent="UI/BuildMenu/InfoPopup/Panel"] anchor_left = 0.5 @@ -1850,10 +1838,11 @@ text = "Build a ship" horizontal_alignment = 1 [node name="Bar" type="Control" parent="UI/CraftMenu"] +anchor_right = 1.0 mouse_filter = 2 offset_left = 24 offset_top = 148 -offset_right = 520 +offset_right = -24 offset_bottom = 284 script = ExtResource("24") @@ -2433,25 +2422,25 @@ anchor_top = 1.0 anchor_bottom = 1.0 offset_left = 32.0 offset_top = -272.0 -offset_right = -32.0 +offset_right = -300.0 offset_bottom = -152.0 theme_override_font_sizes/font_size = 34 autowrap_mode = 2 -horizontal_alignment = 1 +horizontal_alignment = 0 text = "" [node name="CostNumber" type="Control" parent="UI/SkillMenu"] +alignment = 2 mouse_filter = 2 -anchor_left = 0.5 +anchor_left = 1.0 anchor_top = 1.0 -anchor_right = 0.5 +anchor_right = 1.0 anchor_bottom = 1.0 -offset_left = -64.0 -offset_top = -146.0 -offset_right = 76.0 -offset_bottom = -82.0 -script = ExtResource("19") -digit_height = 52.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="UI/SkillMenu"] mouse_filter = 2