#95: stop printing the bank twice in the research menu
The research table's LOCAL column was not merely equal to BANKED, it was the same store: research lives on the mothership, the mothership is also the bank, and building_inventory() redirects any bank-capable building to bank_inventory itself. ResourceBar now drops LOCAL wherever it resolves to the bank and lets the remaining two columns share the width. Anywhere else - a crafter, a lab, the transfer panel - the column is untouched. The "Research" heading went with it. It named a panel you can only reach by pressing the research button, and it cost the tree canvas 128px, which the table and the tree have now taken back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
85c9bd9d8d
commit
5f1b5a9b29
3 changed files with 205 additions and 19 deletions
159
tests/test_research_bar_columns.gd
Normal file
159
tests/test_research_bar_columns.gd
Normal file
|
|
@ -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.<instance member> 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
|
||||||
|
|
@ -25,6 +25,11 @@ const LOCAL_COLOR: Color = Color(0.45, 0.9, 0.55)
|
||||||
var _header_labels: Array[Label] = []
|
var _header_labels: Array[Label] = []
|
||||||
var _rows: Dictionary = {}
|
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:
|
func _ready() -> void:
|
||||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
|
@ -74,15 +79,38 @@ func _make_value_label(colour: Color) -> Label:
|
||||||
return 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:
|
func _layout() -> void:
|
||||||
if _header_labels.is_empty():
|
if _header_labels.is_empty():
|
||||||
return
|
return
|
||||||
|
var keys: Array = _visible_value_keys()
|
||||||
var resource_width: float = size.x * 0.25
|
var resource_width: float = size.x * 0.25
|
||||||
var value_width: float = (size.x - resource_width) / 3.0
|
var value_width: float = (size.x - resource_width) / float(keys.size())
|
||||||
var columns: Array = [0.0, resource_width, resource_width + value_width,
|
var columns: Array = [0.0]
|
||||||
resource_width + value_width * 2.0]
|
var widths: Array = [resource_width]
|
||||||
var widths: Array = [resource_width, value_width, value_width, value_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()):
|
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].position = Vector2(columns[i], 0.0)
|
||||||
_header_labels[i].size = Vector2(widths[i], HEADER_HEIGHT)
|
_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.glyph.size = Vector2(content_height, content_height)
|
||||||
row.name.position = Vector2(content_height + 10.0, y)
|
row.name.position = Vector2(content_height + 10.0, y)
|
||||||
row.name.size = Vector2(resource_width - content_height - 10.0, row_height)
|
row.name.size = Vector2(resource_width - content_height - 10.0, row_height)
|
||||||
for i in range(3):
|
for i in range(VALUE_KEYS.size()):
|
||||||
var label: Label = row[["bank", "fleet", "local"][i]]
|
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.position = Vector2(columns[i + 1], y)
|
||||||
label.size = Vector2(widths[i + 1], row_height)
|
label.size = Vector2(widths[i + 1], row_height)
|
||||||
row_index += 1
|
row_index += 1
|
||||||
|
|
@ -109,6 +140,12 @@ func _layout() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> 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():
|
for resource_type in GameState.resource_types():
|
||||||
var row: Dictionary = _rows.get(resource_type, {})
|
var row: Dictionary = _rows.get(resource_type, {})
|
||||||
if row.is_empty():
|
if row.is_empty():
|
||||||
|
|
|
||||||
|
|
@ -2060,23 +2060,13 @@ anchor_right = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
color = Color(0.03, 0.03, 0.08, 0.94)
|
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
|
mouse_filter = 2
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
offset_left = 24.0
|
offset_left = 24.0
|
||||||
offset_top = 20.0
|
offset_top = 20.0
|
||||||
offset_right = -160.0
|
offset_right = -160.0
|
||||||
offset_bottom = 82.0
|
offset_bottom = 156.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
|
|
||||||
script = ExtResource("24")
|
script = ExtResource("24")
|
||||||
|
|
||||||
[node name="Tree" type="Control" parent="UI/SkillMenu"]
|
[node name="Tree" type="Control" parent="UI/SkillMenu"]
|
||||||
|
|
@ -2085,7 +2075,7 @@ clip_contents = true
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
offset_left = 24.0
|
offset_left = 24.0
|
||||||
offset_top = 296.0
|
offset_top = 168.0
|
||||||
offset_right = -24.0
|
offset_right = -24.0
|
||||||
offset_bottom = -280.0
|
offset_bottom = -280.0
|
||||||
script = ExtResource("21")
|
script = ExtResource("21")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue