#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
|
||||
|
|
|
|||
Loading…
Reference in a new issue