#136 - the gas burner drawn on top of the transport hub. The build menu's rows were a written-down list of tops read with `tops[mini(row, size - 1)]`, and with every structure researched the menu wanted a fourth row where the list had three, so the last entry was placed on the third row's centre slot. A list that silently stacks whatever will not fit in it is not a layout. Rows are computed now, from a first top and a pitch that tightens to fit and is floored at the row's own height - so two rows cannot occupy the same space however far the catalog grows - and the column count widens rather than adding a row the band cannot hold. Ten structures now sit in four columns of three rows. The test unlocks the whole tree and checks every pair of entries for overlap, which is the shape of the bug rather than a coordinate that happens to be right today. #137 - the game had three answers to "what does this cost". The build menu drew a NeonNumber under each option, the craft menu drew glyph strips, and the ship lab spelled prices out inside the caption. PriceTag is that answer once: the resource's glyph, then what you HOLD over what it COSTS. It replaces all three, plus the info popup and the skill panel, and VesselPriceStrip - which was the same idea for one menu - goes with them. The build menu also gains the resource bar the other two already had, in the same place and at the same size, and the craft menu's - the only one not running the full width of its panel - now matches. The bar drops its LOCAL column where there is no building to report on, for the same reason #95 dropped it where it only repeated BANKED. #134 - the skill panel's explanation and its price were both centred in the same band, so a long description landed on the number. The text is the left of the band now and the tag is the right. The shortfall it used to spell out in a sentence - one per case, ore or research points - is the red on the tag instead, and research points ride along as an entry of their own, so the tag says WHICH thing is missing without a line of prose per possibility. One trap worth recording: PriceTag and ResourceBar read the GameState autoload, and an `extends SceneTree` test script that so much as names such a class - or preloads the file - fails to compile, because GameState is not an identifier in that context. GDScript then caches the failure, so the scene the test instantiates gets a Control with no script where its resource bar should be. The tests compare script PATHS, as strings.
This commit is contained in:
parent
710dcbf230
commit
ea25318201
12 changed files with 661 additions and 199 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
192
tests/test_menu_price_unification.gd
Normal file
192
tests/test_menu_price_unification.gd
Normal file
|
|
@ -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.<instance member> 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
|
||||
1
tests/test_menu_price_unification.gd.uid
Normal file
1
tests/test_menu_price_unification.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://py5ckuutwdi6
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
198
ui/PriceTag.gd
Normal file
198
ui/PriceTag.gd
Normal file
|
|
@ -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
|
||||
1
ui/PriceTag.gd.uid
Normal file
1
ui/PriceTag.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://4qp8g8ogbigj
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://c1xt1uhl8f023
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue