extends "res://ui/menus/ModalMenu.gd" # Forgejo #220: what the fleet costs to keep, and WHY it costs that. # # A Fleet Office charges for the vessel in its berth every 25 seconds, and # each berth costs more than the one before it. Neither of those is visible # from the resource bar - the player watches a number fall and cannot tell # which of their own decisions is draining it - so this states the bill and # shows the arithmetic that produced it, line by line. # # Every number here comes from GameState.fleet_upkeep_breakdown(), which is # the same calculation the charging loop runs. A readout that did its own sum # could disagree with what is actually being taken, which is exactly the kind # of "the game is lying to me" the ticket is about. @onready var total_label: Label = $TotalLabel @onready var sheet: GridContainer = $Sheet @onready var empty_label: Label = $EmptyLabel @onready var formula_label: Label = $FormulaLabel const HEADER_COLOR: Color = Color(0.55, 0.78, 0.86) const CELL_COLOR: Color = Color(0.72, 0.88, 0.96) const TOTAL_COLOR: Color = Color(1.0, 0.85, 0.35) const UNPAID_COLOR: Color = Color(1.0, 0.45, 0.4) const COLUMNS: Array = ["BERTH", "VESSEL", "PER CHARGE", "RATE", ""] var building_id: int = 0 # What the sheet was last built from. The menu is refreshed every frame while # it is open (see BuildController), and rebuilding a dozen Labels per frame to # show numbers that change every 25 seconds is the thing issues #6 and #10 # are about. var _built_from: String = "" func open_for(next_building_id: int) -> void: building_id = next_building_id open_menu() refresh() func close_menu() -> void: super.close_menu() building_id = 0 func refresh() -> void: if not visible: return var breakdown: Dictionary = GameState.fleet_upkeep_breakdown() var berths: Array = breakdown.berths var per_minute: Dictionary = breakdown.per_minute # The bill, both ways round: what leaves the stores at each charge, and # what that adds up to over a minute. The per-charge figure is the one # the player watches happen; the rate is the one they plan against. var totals: Dictionary = {} for berth in berths: for resource_type in berth.costs: totals[resource_type] = int(totals.get(resource_type, 0)) \ + int(berth.costs[resource_type]) if per_minute.is_empty(): total_label.text = "NOTHING TO KEEP" else: total_label.text = "%s EVERY %dS ยท %s PER MINUTE" % [ _amounts(totals), int(breakdown.charge_seconds), _rates(per_minute)] var signature: String = "%s|%s" % [str(berths), str(totals)] if signature != _built_from: _built_from = signature _build_sheet(berths, totals) empty_label.visible = berths.is_empty() sheet.visible = not berths.is_empty() empty_label.text = "No berth is being charged. Build a Fleet Office and put a vessel in it." formula_label.text = ("Each berth is charged every %ds: the vessel\'s build price " + "divided by %d, then multiplied by %.2f for every berth past the first " + "paid one. The first vessel is free.") % [ int(breakdown.charge_seconds), int(breakdown.divisor), float(breakdown.growth)] # One row per paying berth, plus a totals row - a berth standing empty is # charged nothing and is left out rather than listed at zero. func _build_sheet(berths: Array, totals: Dictionary) -> void: for child in sheet.get_children(): child.free() if berths.is_empty(): return for column in COLUMNS: _cell(String(column), HEADER_COLOR) for berth in berths: var starved: bool = bool(berth.starved) var row_color: Color = UNPAID_COLOR if starved else CELL_COLOR _cell(str(int(berth.slot)), row_color) _cell(GameState.vessel_name(String(berth.vessel_type)), row_color) _cell(_amounts(berth.costs), row_color) _cell("x%.2f" % float(berth.scale), row_color) _cell("UNPAID" if starved else "", UNPAID_COLOR) # The totals row is what #230 asked for: the per-berth lines never added # up to anything on screen, so the bill had to be done in the head. _cell("TOTAL", TOTAL_COLOR) _cell("", TOTAL_COLOR) _cell(_amounts(totals), TOTAL_COLOR) _cell("", TOTAL_COLOR) _cell("", TOTAL_COLOR) func _cell(text: String, colour: Color) -> void: var label := Label.new() label.text = text label.add_theme_font_size_override("font_size", 28) label.add_theme_color_override("font_color", colour) label.mouse_filter = Control.MOUSE_FILTER_IGNORE sheet.add_child(label) # In the game's own resource order, not the dictionary's. Iterating the costs # directly put "2 ore 1 energy" on one row and "2 gas 2 ore" on the next, # which is most of what a column is for - you cannot read down a table whose # entries are in a different order on every line. func _ordered(costs: Dictionary) -> Array: var out: Array = [] for resource_type in GameState.resource_types(): if costs.has(resource_type): out.append(resource_type) # Anything the catalog does not know about still gets shown rather than # silently dropped. for resource_type in costs: if not out.has(resource_type): out.append(resource_type) return out func _amounts(costs: Dictionary) -> String: var parts: PackedStringArray = PackedStringArray() for resource_type in _ordered(costs): parts.append("%d %s" % [int(costs[resource_type]), GameState.resource_name(String(resource_type)).to_lower()]) return " ".join(parts) if not parts.is_empty() else "-" func _rates(per_minute: Dictionary) -> String: var parts: PackedStringArray = PackedStringArray() for resource_type in _ordered(per_minute): parts.append("%.1f %s" % [float(per_minute[resource_type]), GameState.resource_name(String(resource_type)).to_lower()]) return " ".join(parts)