extends SceneTree # Headless test: run with # godot --headless -s res://tests/test_hud_layout.gd # The HUD's top-anchored button columns must not collide with the resource # readout, and must not collide with each other. # # Both halves are regressions that actually shipped: # - ResourceReadout builds one row per resource type AT RUNTIME, so the # scene's authored offset_bottom understated it and the build button, # at a hardcoded y, ended up underneath the resource list. # - Seating the columns below the readout then exposed a second hardcoded # y: _layout_context_buttons() wrote the action row back to 324 on the # next context change, dropping it on top of the moved build button. # # Both were a literal coordinate that encoded "below the thing above me" and # stopped being true when the thing above moved. So this asserts the # RELATIONSHIP rather than any particular coordinate - a fresh hardcoded y # fails here whatever value someone picks. # # Covers: # A. every HUD button clears the resource readout's real bottom # B. no two simultaneously-usable buttons overlap each other # C. both still hold after _layout_context_buttons() runs, collapsed and # expanded - the path that broke # D. it holds with MORE resource types than the scene was authored for, # which is the direction this drifts var scene: Node var controller: Node var readout: Node var stage: int = 0 var elapsed: float = 0.0 # Deliberately share a row: a building either offers its own action or a # hangar, never both, so their rects coinciding is the design. const EXCLUSIVE_PAIRS: Array = [["building_action_button", "hangar_button"]] func _initialize() -> void: scene = load("res://Main.tscn").instantiate() root.add_child(scene) controller = scene.get_node("BuildController") readout = scene.get_node("UI/ResourceReadout") 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) if not _check("as laid out at load"): return true # --- C. the path that broke it ------------------------------ controller.context_actions_expanded = false controller._layout_context_buttons() if not _check("after _layout_context_buttons() collapsed"): return true controller.context_actions_expanded = true controller._layout_context_buttons() if not _check("after _layout_context_buttons() expanded"): return true print("TEST_HUD_LAYOUT: PASS") quit(0) return true return false func _buttons() -> Dictionary: return { "build_button": controller.build_button, "transfer_button": controller.transfer_button, "action_button": controller.action_button, "landing_button": controller.landing_button, "building_action_button": controller.building_action_button, "hangar_button": controller.hangar_button, } func _check(context: String) -> bool: var buttons: Dictionary = _buttons() var names: Array = buttons.keys() # --- A. clear of the resource readout --------------------------------- # content_bottom() is the readout's REAL extent, from the live resource # list - not the stale height the scene happens to declare. var readout_bottom: float = readout.content_bottom() for name in names: var button: Control = buttons[name] if button == null: continue if button.position.y < readout_bottom: return _fail("%s sits at y %.0f, inside the resource readout which reaches %.0f (%s)" \ % [name, button.position.y, readout_bottom, context]) # --- B. clear of each other ------------------------------------------- for i in range(names.size()): for j in range(i + 1, names.size()): var a_name: String = names[i] var b_name: String = names[j] if _is_exclusive(a_name, b_name): continue var a: Control = buttons[a_name] var b: Control = buttons[b_name] if a == null or b == null: continue if a.get_rect().intersects(b.get_rect()): return _fail("%s %s overlaps %s %s (%s)" \ % [a_name, a.get_rect(), b_name, b.get_rect(), context]) print("clear of the readout (reaches %.0f) and of each other - %s" % [readout_bottom, context]) return true func _is_exclusive(a: String, b: String) -> bool: for pair in EXCLUSIVE_PAIRS: if (a == pair[0] and b == pair[1]) or (a == pair[1] and b == pair[0]): return true return false func _fail(message: String) -> bool: print("TEST_HUD_LAYOUT: FAIL (%s)" % message) quit(1) return false