Refactor multi-resource UI and transfers

This commit is contained in:
Loïc Wisniewski 2026-08-14 11:54:31 +02:00
commit 8231e165ff
20 changed files with 648 additions and 435 deletions

View file

@ -1,146 +1,113 @@
class_name ResourceBar
extends Control
# The header every building menu now carries (#24): banked ore, what the
# fleet is holding, and what THIS building has. Before this, each panel
# improvised its own - the crafter spelled the same three figures into a
# run-on StatusLabel string, the research screen printed a bank total inside
# a button caption - so the same numbers looked different in every room.
#
# Three columns rather than a sentence, because the point of the bar is that
# a figure stays in the same place as the player moves between menus.
#
# Reads GameState directly every frame, like ResourceCounter and CargoCounter
# do, so it stays correct across the space <-> planet-surface scene swap with
# nobody having to push updates into it. Only `building_id` is pushed in,
# because "here" is the one thing the bar cannot work out for itself.
# Compact resource table shared by crafting, research and the ship lab.
# Rows are resources; columns are locations. Adding another material to
# GameState.RESOURCE_TYPES automatically gives it a row here.
# Which building the HERE column is reporting. 0 means none, which greys the
# column rather than showing a confident zero.
var building_id: int = 0
# The caption box has to be a full line tall, not just the font size: a Label
# renders its line height, not its point size, and the first version sat the
# digits 26px down from the top straight through the middle of the caption.
const CAPTION_HEIGHT: float = 34.0
const CAPTION_GAP: float = 4.0
const CAPTION_FONT_SIZE: int = 24
const DIGIT_HEIGHT: float = 34.0
# Bank keeps NeonNumber's own ore-teal, and the hold keeps the amber
# CargoCounter established for ore in transit. Only HERE needed a new colour;
# it borrows the mothership's green, which already reads as "landed
# somewhere and staying there" everywhere else in the game.
const BANK_GLOW: Color = Color(0.0, 0.62, 0.7)
const BANK_CORE: Color = Color(0.0, 0.12, 0.15)
const HOLD_GLOW: Color = Color(1.0, 0.78, 0.25)
const HOLD_CORE: Color = Color(0.22, 0.12, 0.0)
const HERE_GLOW: Color = Color(0.45, 0.9, 0.55)
const HERE_CORE: Color = Color(0.05, 0.16, 0.08)
const HEADER_HEIGHT: float = 22.0
const RULE_COLOR: Color = Color(0.25, 0.55, 0.7)
const FLEET_COLOR: Color = Color(1.0, 0.78, 0.25)
const LOCAL_COLOR: Color = Color(0.45, 0.9, 0.55)
var _numbers: Array[NeonNumber] = []
var _captions: Array[Label] = []
var _gas_caption: Label
var _gas_icon: IconButton
var _summary: Label
var _header_labels: Array[Label] = []
var _rows: Dictionary = {}
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE
var columns: Array = [
["BANK", BANK_GLOW, BANK_CORE],
["HOLD", HOLD_GLOW, HOLD_CORE],
["HERE", HERE_GLOW, HERE_CORE],
]
for column in columns:
var caption: Label = Label.new()
caption.text = column[0]
caption.mouse_filter = Control.MOUSE_FILTER_IGNORE
caption.add_theme_font_size_override("font_size", CAPTION_FONT_SIZE)
caption.add_theme_color_override("font_color", column[1])
add_child(caption)
_captions.append(caption)
for heading in ["", "BANKED", "FLEET", "LOCAL"]:
var label := _make_label(heading, 16, Color(0.62, 0.78, 0.86))
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
add_child(label)
_header_labels.append(label)
var number: NeonNumber = NeonNumber.new()
number.mouse_filter = Control.MOUSE_FILTER_IGNORE
number.glow_color = column[1]
number.core_color = column[2]
number.digit_height = DIGIT_HEIGHT
add_child(number)
_numbers.append(number)
for resource_type in GameState.RESOURCE_TYPES:
var glyph := ResourceGlyph.new()
glyph.resource_type = resource_type
add_child(glyph)
var name_label := _make_label(GameState.resource_name(resource_type).to_upper(), 18,
GameState.resource_color(resource_type))
add_child(name_label)
var bank_label := _make_value_label(GameState.resource_color(resource_type))
var fleet_label := _make_value_label(FLEET_COLOR)
var local_label := _make_value_label(LOCAL_COLOR)
for label in [bank_label, fleet_label, local_label]:
add_child(label)
_rows[resource_type] = {
"glyph": glyph, "name": name_label, "bank": bank_label,
"fleet": fleet_label, "local": local_label,
}
_layout()
resized.connect(_layout)
_gas_caption = Label.new()
_gas_caption.mouse_filter = Control.MOUSE_FILTER_IGNORE
_gas_caption.add_theme_font_size_override("font_size", 20)
_gas_caption.add_theme_color_override("font_color", Color(0.85, 0.52, 1.0))
add_child(_gas_caption)
_gas_icon = IconButton.new()
_gas_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
_gas_icon.icon = IconButton.Icon.GAS
_gas_icon.glow_color = Color(0.85, 0.52, 1.0)
add_child(_gas_icon)
_summary = Label.new()
_summary.mouse_filter = Control.MOUSE_FILTER_IGNORE
_summary.add_theme_font_size_override("font_size", 18)
_summary.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_summary.size = size
add_child(_summary)
_layout()
queue_redraw()
func _make_label(text_value: String, font_size: int, colour: Color) -> Label:
var label := Label.new()
label.text = text_value
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 _make_value_label(colour: Color) -> Label:
var label := _make_label("0", 19, colour)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
return label
func _layout() -> void:
var column_width: float = size.x / float(_numbers.size())
for i in _numbers.size():
var x: float = column_width * float(i)
_captions[i].position = Vector2(x, 0.0)
_captions[i].size = Vector2(column_width, CAPTION_HEIGHT)
# The hexagon and digits are drawn from the node's own origin, so the
# number is positioned rather than sized.
_numbers[i].position = Vector2(x, CAPTION_HEIGHT + CAPTION_GAP)
if _gas_caption != null:
_gas_caption.position = Vector2(size.x - 110.0, 2.0)
_gas_caption.size = Vector2(106.0, CAPTION_HEIGHT)
_gas_caption.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
if _gas_icon != null:
_gas_icon.position = Vector2(size.x - 34.0, 0.0)
_gas_icon.size = Vector2(32.0, 32.0)
_gas_caption.size.x = maxf(0.0, _gas_caption.size.x - 36.0)
if _header_labels.is_empty():
return
var resource_width: float = size.x * 0.25
var value_width: float = (size.x - resource_width) / 3.0
var columns: Array = [0.0, resource_width, resource_width + value_width,
resource_width + value_width * 2.0]
var widths: Array = [resource_width, value_width, value_width, value_width]
for i in range(_header_labels.size()):
_header_labels[i].position = Vector2(columns[i], 0.0)
_header_labels[i].size = Vector2(widths[i], HEADER_HEIGHT)
var row_height: float = (size.y - HEADER_HEIGHT - 4.0) / maxf(1.0, float(_rows.size()))
var row_index: int = 0
for resource_type in GameState.RESOURCE_TYPES:
var row: Dictionary = _rows[resource_type]
var y: float = HEADER_HEIGHT + float(row_index) * row_height
row.glyph.position = Vector2(2.0, y + 2.0)
row.glyph.size = Vector2(row_height - 4.0, row_height - 4.0)
row.name.position = Vector2(row_height, y)
row.name.size = Vector2(resource_width - row_height, row_height)
for i in range(3):
var label: Label = row[["bank", "fleet", "local"][i]]
label.position = Vector2(columns[i + 1], y)
label.size = Vector2(widths[i + 1], row_height)
row_index += 1
queue_redraw()
func _process(_delta: float) -> void:
# Resource rows scale without inventing a new menu per resource. The old
# ore-only columns remain behind this concise, explicit summary.
_summary.text = "ORE Bank %d Fleet %d Local %d\nGAS Bank %d Fleet %d Local %d" % [GameState.rocks, GameState.fleet_cargo, GameState.building_holdings(building_id), GameState.gas, GameState.fleet_gas_cargo, GameState.building_gas_holdings(building_id)]
for number in _numbers: number.visible = false
for caption in _captions: caption.visible = false
_gas_caption.visible = false
_gas_icon.visible = false
_gas_caption.text = "GAS %d / %d / %d" % [GameState.gas, GameState.fleet_gas_cargo, GameState.building_gas_holdings(building_id)]
_numbers[0].set_amount(GameState.rocks)
_numbers[1].set_amount(GameState.fleet_cargo)
# A building with no id is not a building: showing "0" there would claim
# the store is empty rather than that there is no store to look at.
var known: bool = building_id != 0
_numbers[2].visible = known
_captions[2].visible = known
if known:
_numbers[2].set_amount(GameState.building_holdings(building_id))
for resource_type in GameState.RESOURCE_TYPES:
var row: Dictionary = _rows.get(resource_type, {})
if row.is_empty():
continue
row.bank.text = str(GameState.banked_resource(resource_type))
row.fleet.text = str(GameState.fleet_resource(resource_type))
row.local.text = str(GameState.building_resource(building_id, resource_type)) \
if building_id != 0 else "—"
# The height the bar needs for a caption line and a row of digits. The scene
# sizes the node, so this is what those offsets have to add up to; anything
# less and the rule is drawn back through the digits.
static func natural_height() -> float:
return CAPTION_HEIGHT + CAPTION_GAP + DIGIT_HEIGHT + 8.0
return 84.0
func _draw() -> void:
# One rule under the whole bar, so the header reads as a band separating
# the standing figures from whatever the panel below them does.
var rule_y: float = maxf(size.y, natural_height())
NeonDraw.draw_glowing_line(
self, Vector2(0.0, rule_y), Vector2(size.x, rule_y), RULE_COLOR, 0.55, 1.0
)
NeonDraw.draw_glowing_line(self, Vector2(0.0, size.y), Vector2(size.x, size.y),
RULE_COLOR, 0.55, 1.0)