spaceship/tests/test_build_menu_hides_locked.gd
Loïc Wisniewski cc71811d89 Research the Fleet Office, and give it one hull
Fleet Offices sit behind a new Berth Authority doctrine. It costs ore and no
research points, deliberately: points come only from conquering a planet,
conquering needs a fleet, and the fleet caps at its opening hull until an
office exists - so pricing the fleet's own gate in them would lock a player
behind a fight that one vessel has to win first. The research unlocks the
STRUCTURE; capacity is still the built, supplied thing #219 made it.

The tree gains a sixth lane for it, so the branch that fleet size vacated is
occupied again rather than reading as a hole.

Also fixes a shape #216 exposed: a stationed transporter drew a hull written
into its own script while a delivered one drew the definition's, and the
transporter's definition declared none at all - so one in flight was an
empty polygon. Both vessels now draw the definition, and a test pins that
the two paths agree. The repair drone had the same fault, off by two units.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-05 21:57:53 +02:00

231 lines
9.4 KiB
GDScript

extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_build_menu_hides_locked.gd
# Forgejo #21: a structure the player has not researched should not be in the
# build menu at all, rather than sitting there greyed out.
#
# The menu is a fixed grid in Main.tscn, so simply hiding entries would leave
# holes where they used to be - it is repacked from whatever is researched.
#
# Verifies:
# 1. with nothing researched only the mothership is offered (it is
# deliberately never gated - it is where research happens)
# 2. researching a structure adds it, and it is placed, not left at
# whatever offsets it happened to have in the scene file
# 3. no gaps: what is shown occupies consecutive grid slots
# 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
# 7. #144 - and that holds at any window shape. The header is anchored to
# the top of the panel and the grid to its centre, so the gap between
# them is half the panel height - it was a constant measured at 1280,
# and at 1080 the grid started above the status line and drew through it
var game_state: Node
var scene: Node
var controller: Node
var stage: int = 0
var elapsed: float = 0.0
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.transporters.clear()
game_state.reset_progression()
game_state.rocks = 500
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
controller = scene.get_node("BuildController")
func _shown() -> Array:
var out: Array = []
for entry in controller.build_menu.entries:
if entry.button.visible:
out.append([entry.definition.definition_id, entry.button,
entry.label, entry.cost, entry.definition])
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 = []
for entry in _shown():
var b: Control = entry[1]
out.append([b.offset_top, b.offset_left + (b.offset_right - b.offset_left) / 2.0])
return out
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
if elapsed < 0.2:
return false
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
# --- 1. nothing researched ----------------------------------
controller._refresh_build_status()
# The Fleet Office sits behind Berth Authority, so the opening is
# the mothership alone again - the fallback that is always there.
var shown: Array = _shown()
if shown.size() != 1 or shown[0][0] != GameState.BUILDING_MOTHERSHIP:
var names: Array = []
for e in shown:
names.append(e[0])
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (unresearched structures on offer: %s)" % [names])
quit(1)
return true
# --- 4. and it is centred, being alone on its row -----------
var only: Array = _slots()[0]
if absf(only[1]) > 0.01:
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (the only entry sits at x=%.1f instead of centred)" % only[1])
quit(1)
return true
print("nothing researched -> only the mothership, centred")
# --- 2. research three more ---------------------------------
game_state.unlocked = ["shipyards", "logistics", "coordination"]
controller._refresh_build_status()
stage = 1
return false
1:
var shown2: Array = _shown()
if shown2.size() != 4:
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (researched 3, expected 4 entries, got %d)" % shown2.size())
quit(1)
return true
for e in shown2:
if e[0] == GameState.BUILDING_TRANSPORT_HUB:
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (the unresearched transport hub is still listed)")
quit(1)
return true
# --- 3. consecutive slots, no holes -------------------------
var slots: Array = _slots()
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 (%d entries laid out in %d columns)" \
% [slots.size(), columns])
quit(1)
return true
for i in range(slots.size()):
var row: int = i / columns
var want_row: float = controller.menu_row_top(row, 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], row, want_row])
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (a gap was left where a hidden entry used to be)")
quit(1)
return true
# Each ROW is centred on however many it holds, so a final row
# with one entry sits in the middle rather than in column 0.
# Written the way the layout writes it, so an odd count is not
# a special case here that is ordinary there.
var in_row: int = mini(slots.size() - row * columns, columns)
var spacing: float = controller.menu_column_spacing(columns)
var want_x: float = (float(i % columns) - (float(in_row) - 1.0) * 0.5) * spacing
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("%d entries packed into consecutive slots, no gaps" % slots.size())
# --- 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())])
# --- 7. #144: and at any window shape ------------------------
# The grid is anchored to the panel's CENTRE and the status line
# above it to the panel's TOP, so the gap between them is half
# the panel height. It used to be a written-down constant, right
# only at 1280 - at 1080 the grid started above the status line
# and drew through it, which is what the ticket reported.
var menu: Control = scene.get_node("UI/BuildMenu")
var status: Control = scene.get_node("UI/BuildMenu/StatusLabel")
var restore: float = menu.offset_bottom
for panel_height in [900.0, 1080.0, 1280.0, 1600.0]:
menu.anchor_bottom = 0.0
menu.offset_top = 0.0
menu.offset_bottom = panel_height
controller._refresh_build_status()
var half: float = menu.size.y * 0.5
# The status line's own bottom, in the grid's centre-anchored
# frame - which is the comparison that was never made.
var header_bottom: float = status.offset_bottom - half
var placed: Array = _shown()
for a in range(placed.size()):
for part in [1, 2, 3]:
var box: Rect2 = _rect(placed[a][part])
if box.position.y < header_bottom:
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (at %.0fpx tall, %s is drawn at y=%.0f, above the status line at %.0f)" \
% [panel_height, placed[a][0], box.position.y, header_bottom])
quit(1)
return true
for b in range(a + 1, placed.size()):
for part2 in [1, 2, 3]:
if _rect(placed[a][part2]).intersects(_rect(placed[b][part2])):
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (at %.0fpx tall, %s and %s overlap)" \
% [panel_height, placed[a][0], placed[b][0]])
quit(1)
return true
print(" %.0fpx tall: %d columns, top row at %.0f, clear of the status line at %.0f" \
% [panel_height, controller.menu_columns(placed.size()),
_rect(placed[0][1]).position.y, header_bottom])
menu.anchor_bottom = 1.0
menu.offset_top = 0.0
menu.offset_bottom = restore
controller._refresh_build_status()
print("the grid clears the header at every window shape")
# --- 5. researched but not usable is shown, greyed ----------
var extractor_shown: bool = false
for e in shown2:
if e[0] == GameState.BUILDING_EXTRACTOR:
extractor_shown = true
if e[1].enabled:
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (extractor offered as usable with no planet under the fleet)")
quit(1)
return true
if not extractor_shown:
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (researched extractor hidden - 'not now' is not 'not yet')")
quit(1)
return true
print("researched extractor shown but greyed, with no planet in reach")
print("TEST_BUILD_MENU_HIDES_LOCKED: PASS")
quit(0)
return true
return false