spaceship/tests/test_fleet_upkeep_sheet.gd

150 lines
5.6 KiB
GDScript3
Raw Permalink Normal View History

#231, #230: a key per ability, and an upkeep sheet that adds up #231 - the abilities were hard to use on a keyboard. "The currently selected weapons" names a real mechanism: MouseCombatController (#80) has number keys that SELECT which weapon a click fires. That is two acts for one shot, and the selection is invisible unless you look at the buttons. Q, E and space now fire the gas bomb, the laser and the dash outright, wherever the mouse already points. It goes through fire_at(), the same public entry the click path uses, so every guard comes with it - no hull ready, cannot afford the shot, a panel over the button. The number keys stay: they are not what this ticket is about, and removing them would take the click path's abilities with them. Q and E were already taken - they pivot the formation - so the pivot pair moved to Z and C, one row under the movement keys and still left-hand. That is a change to a control the ticket did not mention, and the one thing here worth arguing with. test_keyboard_input spelled Q and E out and so reported a broken pivot when the pivot was fine. It reads KEY_PIVOT_CW/CCW off the controller now, and additionally asserts the pair is not bound to an ability key - the failure that would actually matter. #230 - the upkeep menu never added anything up. Two faults, and the second is invisible from the code. There was no TOTAL, so the bill had to be done in the player's head. And the rows were one Label padded with %-16s, which aligns only in a monospace font - the theme's is CarroisGothicSC, which is proportional - with autowrap on top, so a long row folded and the table lost its shape. A real GridContainer now: header, a row per paying berth, and a TOTAL. The headline carries both figures, because they answer different questions - the per-charge bill is what the player watches happen, the per-minute rate is what they plan against. Amounts are listed in the game's resource order rather than the dictionary's. The screenshot showed "2 ore 1 energy" on one row above "2 gas 2 ore" on the next, which defeats the point of a column. The sheet is rebuilt only when the numbers change. BuildController refreshes this menu every frame while it is open, and rebuilding a dozen Labels per frame for figures that move every 25 seconds is what #6 and #10 are about. The test refreshes twice and asserts the same cell objects come back. Every figure still comes from fleet_upkeep_breakdown(), the calculation the charging loop itself runs - the new total sums those same per-berth costs rather than recomputing them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-15 22:27:00 -04:00
extends SceneTree
# Forgejo #230: the upkeep menu shows what the fleet costs IN TOTAL, laid out
# as a table.
#
# It listed each berth and never added them up, so the bill had to be done in
# the player's head. The rows were also one Label padded with %-16s, which
# lines up only in a monospace font - the theme's is CarroisGothicSC, which
# is proportional, so the "columns" never actually aligned. Worse, that Label
# had autowrap on, so a long row folded and the table lost its shape.
#
# Covers:
# A. a row per paying berth, in a real grid
# B. a TOTAL row, and it equals the sum of the rows
# C. the headline states both the per-charge bill and the per-minute rate
# D. an unpaid berth is marked
# E. with nothing to charge, the table gives way to an explanation
# F. the sheet is not rebuilt on every frame - the menu refreshes at frame
# rate and these numbers move every 25 seconds
var game_state: Node
var scene: Node
var menu: Control
var elapsed: float = 0.0
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.reset_progression()
game_state.reset_new_game()
game_state.reset_quests()
game_state.intro_active = false
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
current_scene = scene
menu = scene.get_node("UI/FleetUpkeepMenu")
func _rows() -> Array:
# Grid cells come out in reading order; COLUMNS wide makes one row.
var width: int = menu.sheet.columns
var cells: Array = []
for child in menu.sheet.get_children():
cells.append(String((child as Label).text))
var rows: Array = []
for i in range(0, cells.size(), width):
rows.append(cells.slice(i, i + width))
return rows
func _process(delta: float) -> bool:
elapsed += delta
if elapsed < 0.4:
return false
# --- E. nothing to charge -------------------------------------------
menu.open_menu()
menu.refresh()
if not menu.empty_label.visible or menu.sheet.visible:
return _fail("with no berths the table is shown instead of an explanation")
if not menu.total_label.text.contains("NOTHING"):
return _fail("with no berths the headline reads '%s'" % menu.total_label.text)
print("with nothing to charge: '%s'" % menu.total_label.text)
# Two offices, two crewed berths - the second costs more than the first.
game_state.add_building(GameState.BUILDING_FLEET_OFFICE, Vector2.ZERO)
game_state.add_building(GameState.BUILDING_FLEET_OFFICE, Vector2(600.0, 0.0))
game_state.fleet_vessel_types = PackedStringArray([
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_INTERCEPTOR,
GameState.VESSEL_INTERCEPTOR])
menu.refresh()
var breakdown: Dictionary = game_state.fleet_upkeep_breakdown()
if breakdown.berths.size() < 2:
return _fail("only %d berth(s) are being charged - that proves nothing about a total" \
% breakdown.berths.size())
# --- A. a row per berth, plus header and total ------------------------
var rows: Array = _rows()
if not menu.sheet.visible or menu.empty_label.visible:
return _fail("the table is hidden while berths are being charged")
if rows.size() != breakdown.berths.size() + 2:
return _fail("%d rows for %d berths - expected a header and a total too" \
% [rows.size(), breakdown.berths.size()])
if String(rows[0][0]) != "BERTH":
return _fail("the first row is not a header: %s" % str(rows[0]))
print("%d berths -> %d rows (header, %d berths, total)" \
% [breakdown.berths.size(), rows.size(), breakdown.berths.size()])
# --- B. and the total is the sum of the rows --------------------------
var total_row: Array = rows[rows.size() - 1]
if String(total_row[0]) != "TOTAL":
return _fail("the last row is '%s', not a total" % total_row[0])
var summed: int = 0
for berth in breakdown.berths:
for resource_type in berth.costs:
summed += int(berth.costs[resource_type])
var shown: int = 0
for word in String(total_row[2]).split(" "):
if word.is_valid_int():
shown += int(word)
if shown != summed:
return _fail("the total row reads '%s' (%d), but the berths add to %d" \
% [total_row[2], shown, summed])
if summed <= 0:
return _fail("the berths cost nothing, so a total proves nothing")
print("total row '%s' matches the %d the berths add to" % [total_row[2], summed])
# --- C. the headline carries both figures -----------------------------
var headline: String = menu.total_label.text
if not headline.contains("PER MINUTE") or not headline.contains("EVERY"):
return _fail("the headline does not state both the bill and the rate: '%s'" % headline)
print("headline: %s" % headline)
# --- F. and it is not rebuilt every frame -----------------------------
var first: Array = menu.sheet.get_children()
menu.refresh()
menu.refresh()
var again: Array = menu.sheet.get_children()
if first.size() != again.size():
return _fail("the row count changed across a refresh with no change behind it")
for i in range(first.size()):
if first[i] != again[i]:
return _fail("refreshing rebuilt the table though nothing changed")
print("refreshing twice reuses the same %d cells" % again.size())
# --- D. an unpaid berth is called out ---------------------------------
for building in game_state.buildings:
if String(building.type) == GameState.BUILDING_FLEET_OFFICE:
building["starved"] = true
menu.refresh()
var marked: bool = false
for row in _rows():
if String(row[4]) == "UNPAID":
marked = true
if not marked:
return _fail("a starved berth is not marked unpaid")
print("a starved berth is marked UNPAID")
print("TEST_FLEET_UPKEEP_SHEET: PASS")
quit(0)
return true
func _fail(message: String) -> bool:
print("TEST_FLEET_UPKEEP_SHEET: FAIL (%s)" % message)
quit(1)
return true