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[1].visible: out.append(entry) 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() 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 (4 entries laid out in %d columns)" % columns) quit(1) return true for i in range(slots.size()): var want_row: float = controller.menu_row_top(i / columns, 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], i / columns, want_row]) print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (a gap was left where a hidden entry used to be)") quit(1) return true var spacing: float = controller.menu_column_spacing(columns) var want_x: float = -spacing * 0.5 if i % 2 == 0 else spacing * 0.5 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("4 entries packed into consecutive slots, no gaps") # --- 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