diff --git a/tests/test_research_bar_columns.gd b/tests/test_research_bar_columns.gd new file mode 100644 index 0000000..ff65fb1 --- /dev/null +++ b/tests/test_research_bar_columns.gd @@ -0,0 +1,159 @@ +extends SceneTree + +# Headless test: run with +# godot --headless -s res://tests/test_research_bar_columns.gd +# Forgejo #95, both halves of it. +# +# The research menu's resource table printed BANKED, FLEET and LOCAL, and +# its LOCAL column was always a copy of BANKED. That was not a coincidence: +# the research action lives on the mothership, the mothership also carries +# the "bank" capability, and GameState.building_inventory() redirects any +# bank-capable building to bank_inventory itself. The two columns were the +# same store read twice. +# +# The panel also wore a "Research" heading, which named a screen you can +# only arrive at by pressing the research button, and charged the tree +# canvas 128px for the privilege. +# +# Covers: +# A. the research bar shows BANKED and FLEET and hides LOCAL +# B. the two remaining columns tile the full width - dropping one must not +# leave a third of the table blank +# C. a building that is NOT the bank still gets its LOCAL column, so the +# fix removes a duplicate rather than the feature +# D. no heading above the table, and the tree canvas starts right under it + +const GAP_TOLERANCE: float = 48.0 + +var game_state: Node +var scene: Node +var controller: Node +var skill_menu: Control +var craft_menu: Control +var stage: int = 0 +var elapsed: float = 0.0 + + +func _initialize() -> void: + # GameState is an autoload: a bare GameState. does not + # compile in a SceneTree script, so the node is fetched by name instead. + game_state = root.get_node("GameState") + game_state.buildings.clear() + game_state.extractors.clear() + game_state.reset_progression() + game_state.reset_formation() + game_state.rocks = 500 + + scene = load("res://world/Main.tscn").instantiate() + root.add_child(scene) + controller = scene.get_node("BuildController") + skill_menu = scene.get_node("UI/SkillMenu") + craft_menu = scene.get_node("UI/CraftMenu") + + +func _building(building_type: String) -> Node2D: + for building in scene.get_node("BuildingField").spawned: + if is_instance_valid(building) and building.building_type == building_type: + return building + return null + + +func _shown_headers(bar: Control) -> Array: + var shown: Array = [] + for label in bar._header_labels: + if label.visible and label.text != "": + shown.append(label.text) + return shown + + +func _process(delta: float) -> bool: + elapsed += delta + if stage == 0: + scene.get_node("AsteroidField").set_process(false) + scene.get_node("EnemySpawner").set_process(false) + var field: Node = scene.get_node("BuildingField") + field.place(GameState.BUILDING_MOTHERSHIP, Vector2(400.0, 0.0)) + field.place(GameState.BUILDING_SHIP_CRAFTER, Vector2(-400.0, 0.0)) + elapsed = 0.0 + stage = 1 + return false + if stage != 1 or elapsed < 0.2: + return false + stage = 2 + + var mothership: Node2D = _building(GameState.BUILDING_MOTHERSHIP) + if mothership == null: + return _fail("no mothership was placed") + controller.current_building = mothership + controller._on_action_pressed() + if not skill_menu.visible: + return _fail("the mothership did not open the research menu") + + var bar: Control = scene.get_node("UI/SkillMenu/Bar") + # One frame of _process is what tells the bar its building changed. + bar._process(0.0) + + # --- A. LOCAL is gone from the research table ---------------------- + var headers: Array = _shown_headers(bar) + if headers != ["BANKED", "FLEET"]: + return _fail("the research table shows %s, not just BANKED and FLEET" % str(headers)) + for resource_type in game_state.resource_types(): + var row: Dictionary = bar._rows[resource_type] + if row.local.visible: + return _fail("the LOCAL value for %s is still drawn" % resource_type) + if not row.bank.visible or not row.fleet.visible: + return _fail("dropping LOCAL also hid a column that is not redundant") + print("the research table drops the LOCAL copy of BANKED") + + # --- B. the survivors share the freed width ------------------------- + var first_row: Dictionary = bar._rows[game_state.resource_types()[0]] + var covered: float = (first_row.fleet.position.x + first_row.fleet.size.x) \ + - first_row.bank.position.x + var expected: float = bar.size.x - bar.size.x * 0.25 + if not is_equal_approx(covered, expected): + return _fail("the two columns cover %.0fpx of %.0fpx - the dropped column left a hole" \ + % [covered, expected]) + print("BANKED and FLEET share the whole %.0fpx of value width" % covered) + + # --- C. a non-bank building keeps its LOCAL column ------------------ + skill_menu.visible = false + var crafter: Node2D = _building(GameState.BUILDING_SHIP_CRAFTER) + if crafter == null: + return _fail("no ship crafter was placed") + controller.current_building = crafter + controller._on_action_pressed() + if not craft_menu.visible: + return _fail("the crafter did not open the craft menu") + var craft_bar: Control = scene.get_node("UI/CraftMenu/Bar") + craft_bar._process(0.0) + var craft_headers: Array = _shown_headers(craft_bar) + if craft_headers != ["BANKED", "FLEET", "LOCAL"]: + return _fail("the crafter table shows %s - its LOCAL stock is not the bank" \ + % str(craft_headers)) + print("a building that is not the bank keeps LOCAL") + + # --- D. no heading, and the tree starts under the table ------------- + for child in skill_menu.get_children(): + if child is Label and String(child.text).strip_edges() == "Research": + return _fail("the \"Research\" heading is still in the panel") + var tree_view: Control = scene.get_node("UI/SkillMenu/Tree") + var bar_bottom: float = bar.position.y + bar.size.y + var head_gap: float = tree_view.position.y - bar_bottom + if bar.position.y > GAP_TOLERANCE: + return _fail("%.0fpx of empty panel above the table - the heading's hole" % bar.position.y) + if head_gap < 0.0: + return _fail("the tree canvas overlaps the table above it") + if head_gap > GAP_TOLERANCE: + return _fail("%.0fpx of unused panel between the table and the tree" % head_gap) + print("no heading: the table sits %.0fpx down and the tree is %.0fpx tall" \ + % [bar.position.y, tree_view.size.y]) + + print("TEST_RESEARCH_BAR_COLUMNS: PASS") + quit(0) + return true + + +func _fail(message: String) -> bool: + print("TEST_RESEARCH_BAR_COLUMNS: FAIL (%s)" % message) + quit(1) + return true diff --git a/ui/ResourceBar.gd b/ui/ResourceBar.gd index f47f1e1..5661a19 100644 --- a/ui/ResourceBar.gd +++ b/ui/ResourceBar.gd @@ -25,6 +25,11 @@ const LOCAL_COLOR: Color = Color(0.45, 0.9, 0.55) var _header_labels: Array[Label] = [] var _rows: Dictionary = {} +# The value columns, in the order they are drawn. LOCAL is dropped from this +# list at a building that IS the bank - see _local_is_banked(). +const VALUE_KEYS: Array = ["bank", "fleet", "local"] +var _local_shown: bool = true + func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE @@ -74,15 +79,38 @@ func _make_value_label(colour: Color) -> Label: return label +# #95: at a building that IS the bank - the mothership carries both the "bank" +# and the "research" capability, so the research menu is always opened from one +# - building_inventory() redirects to bank_inventory itself, and LOCAL is not +# merely equal to BANKED but the very same store read twice. Printing the +# number again teaches nothing and costs a third of the table, so the column +# goes away there and the remaining two share the width. +func _local_is_banked() -> bool: + if building_id == 0: + return false + return GameState.building_inventory(building_id) == GameState.bank_inventory + + +func _visible_value_keys() -> Array: + return VALUE_KEYS if _local_shown else VALUE_KEYS.slice(0, 2) + + func _layout() -> void: if _header_labels.is_empty(): return + var keys: Array = _visible_value_keys() var resource_width: float = size.x * 0.25 - var value_width: float = (size.x - resource_width) / 3.0 - var columns: Array = [0.0, resource_width, resource_width + value_width, - resource_width + value_width * 2.0] - var widths: Array = [resource_width, value_width, value_width, value_width] + var value_width: float = (size.x - resource_width) / float(keys.size()) + var columns: Array = [0.0] + var widths: Array = [resource_width] + for i in range(keys.size()): + columns.append(resource_width + value_width * float(i)) + widths.append(value_width) for i in range(_header_labels.size()): + var shown: bool = i < columns.size() + _header_labels[i].visible = shown + if not shown: + continue _header_labels[i].position = Vector2(columns[i], 0.0) _header_labels[i].size = Vector2(widths[i], HEADER_HEIGHT) @@ -100,8 +128,11 @@ func _layout() -> void: row.glyph.size = Vector2(content_height, content_height) row.name.position = Vector2(content_height + 10.0, y) row.name.size = Vector2(resource_width - content_height - 10.0, row_height) - for i in range(3): - var label: Label = row[["bank", "fleet", "local"][i]] + for i in range(VALUE_KEYS.size()): + var label: Label = row[VALUE_KEYS[i]] + label.visible = i < keys.size() + if not label.visible: + continue label.position = Vector2(columns[i + 1], y) label.size = Vector2(widths[i + 1], row_height) row_index += 1 @@ -109,6 +140,12 @@ func _layout() -> void: func _process(_delta: float) -> void: + # building_id is assigned by whichever panel opens, so the column count can + # change between frames; re-lay out only when it actually flips. + var local_shown: bool = not _local_is_banked() + if local_shown != _local_shown: + _local_shown = local_shown + _layout() for resource_type in GameState.resource_types(): var row: Dictionary = _rows.get(resource_type, {}) if row.is_empty(): diff --git a/world/Main.tscn b/world/Main.tscn index 7f9d017..0750e9b 100644 --- a/world/Main.tscn +++ b/world/Main.tscn @@ -2060,23 +2060,13 @@ anchor_right = 1.0 anchor_bottom = 1.0 color = Color(0.03, 0.03, 0.08, 0.94) -[node name="TitleLabel" type="Label" parent="UI/SkillMenu"] +[node name="Bar" type="Control" parent="UI/SkillMenu"] mouse_filter = 2 anchor_right = 1.0 offset_left = 24.0 offset_top = 20.0 offset_right = -160.0 -offset_bottom = 82.0 -text = "Research" -horizontal_alignment = 1 - -[node name="Bar" type="Control" parent="UI/SkillMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 24.0 -offset_top = 148.0 -offset_right = -24.0 -offset_bottom = 284.0 +offset_bottom = 156.0 script = ExtResource("24") [node name="Tree" type="Control" parent="UI/SkillMenu"] @@ -2085,7 +2075,7 @@ clip_contents = true anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 24.0 -offset_top = 296.0 +offset_top = 168.0 offset_right = -24.0 offset_bottom = -280.0 script = ExtResource("21")