Derive the context action row from the build button, not a fixed y
Seating the HUD columns below the resource readout moved the build button
from the scene-authored 160 to 288. _layout_context_buttons() then wrote the
action row straight back to a hardcoded 324 on the next context change - and
it runs on the first frame, so the action button overlapped the build button
immediately.
Same shape of bug as the overlap that seating fixed: a literal coordinate
that meant "directly below the thing above me" and silently stopped being
true when that thing moved. It now measures instead:
row_top = build_button.position.y + build_button.size.y + HUD_ROW_GAP
Both the action button and the expanded context row derive from that, so
wherever the build button ends up, the row below follows.
tests/test_hud_layout.gd asserts the RELATIONSHIP rather than any
coordinate, so a freshly hardcoded y fails whatever value someone picks:
every HUD button clears the readout's real bottom, and no two
simultaneously-usable buttons overlap - checked at load, collapsed and
expanded, which is the path that broke. BuildingActionButton and
HangarButton are excluded as a deliberately exclusive pair.
Verified to fail against the previous code with the reported symptom:
build_button y288..436 overlapping action_button y324.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
75b7603f04
commit
6a62b6f5f8
3 changed files with 146 additions and 4 deletions
130
tests/test_hud_layout.gd
Normal file
130
tests/test_hud_layout.gd
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
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
|
||||
1
tests/test_hud_layout.gd.uid
Normal file
1
tests/test_hud_layout.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://65cagevj7qbj
|
||||
Loading…
Reference in a new issue