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 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 col_x: float = float(controller.MENU_COL_X[columns]) var want_x: float = -col_x if i % 2 == 0 else col_x 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())]) # --- 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