Let a dialog opened over another dialog be closed

The "no research data" explainer is the only dialog in the game that opens
ON TOP of another one - the skill tree or the ship lab, from the button that
was just refused. IconButton._blocked_by_modal() refuses input to any button
sitting behind an open panel, and it had no notion of stacking order: it saw
the lab still visible, saw it was not an ancestor of the popup's close
button, and killed the touch. Every button in the explainer was dead,
including the only way out of it. Reported from the game.

The check now compares draw order. A panel that this button's own panel is
drawn on top of is behind it, not over it, and is ignored; a panel drawn
above it still blocks, so the lab's own buttons stay dead while the popup
covers them. Buttons belonging to no panel - the HUD - are unchanged and
still blocked by anything open.

The existing test missed this because it dismissed the popup by calling
close_menu() directly, which is not what a player can do. Case D presses the
button, and asserts the reverse too. Verified in both directions: with the
fix reverted it fails with "the explainer close button is blocked by the lab
behind it - the dialog is inescapable".

172/172.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
This commit is contained in:
Claude 2026-09-08 22:31:56 +02:00
commit 693919dadb
2 changed files with 62 additions and 2 deletions

View file

@ -113,6 +113,10 @@ func _process(delta: float) -> bool:
break
if game_state.research_point_cost(String(options[controller.lab_choice].id)) <= 0:
return _fail("no lab option costs research data, so this proves nothing")
# The lab panel is open when the player presses buy - that is what
# puts the explainer on top of another dialog, which is the whole
# point of case D below.
controller.lab_menu.open_menu()
controller._on_lab_buy()
if not popup.visible:
return _fail("the lab refused for want of data with no explanation")
@ -120,6 +124,30 @@ func _process(delta: float) -> bool:
return _fail("the lab still blames the queue: '%s'" % controller.lab_menu.status.text)
print("ship lab: refused and explained, not blamed on the queue")
# --- D. and the player can get back out of it ----------------
# The explainer is the only dialog that opens ON TOP of another
# one, and IconButton refuses input to any button sitting behind
# an open panel. That used to include this popup's own close
# button, so the dialog could not be dismissed at all. Case B
# above left the ship lab open underneath, which is the real
# situation - assert that rather than arranging it.
if not controller.lab_menu.visible:
return _fail("the lab is not open underneath, so this proves nothing")
var close: Control = popup.get_node("CloseButton")
if close._blocked_by_modal():
return _fail("the explainer close button is blocked by the lab behind it - the dialog is inescapable")
close.pressed.emit()
if popup.visible:
return _fail("pressing close left the explainer open")
# The reverse must still hold: the lab's own buttons stay dead
# while a dialog covers them.
popup.open_for_shortfall(10, "anything")
var lab_close: Control = controller.lab_menu.get_node("CloseButton")
if not lab_close._blocked_by_modal():
return _fail("the lab buttons are live underneath an open dialog")
popup.close_menu()
print("close button works over the lab, and the lab stays blocked underneath")
print("TEST_RESEARCH_POINTS_EXPLAINER: PASS")
quit(0)
return true

View file

@ -130,6 +130,7 @@ func _blocked_by_modal() -> bool:
# touches through the GUI system swallows them before they ever get
# there. So the panels stay transparent to input and the buttons check
# instead.
var own_modal: Node = _own_modal()
for menu in get_tree().get_nodes_in_group("modal_menus"):
# is_visible_in_tree(), NOT visible. Hiding a parent leaves the
# child's own `visible` flag true, and the build menu's info popup is
@ -137,11 +138,42 @@ func _blocked_by_modal() -> bool:
# invisible popup still claiming to be open, and every button in the
# game was blocked from then on. That is not a state a player can
# get out of.
if menu.is_visible_in_tree() and not menu.is_ancestor_of(self):
if not menu.is_visible_in_tree():
continue
if menu.is_ancestor_of(self):
continue
# Stacking order matters, and used not to be considered at all. A
# panel that this button's own panel is drawn ON TOP OF sits behind
# it, not over it, so it must not swallow the input. The research
# shortfall popup opens over the skill tree and the ship lab, and
# without this every button in it - its close button included - was
# dead, leaving the player no way out of the dialog.
if own_modal != null and not _covers(menu, own_modal):
continue
return true
return false
# The modal panel this button lives inside, or null for a HUD button that
# belongs to no panel (those stay blocked by any open panel, as before).
func _own_modal() -> Node:
var node: Node = get_parent()
while node != null:
if node.is_in_group("modal_menus"):
return node
node = node.get_parent()
return null
func _covers(other: Node, own: Node) -> bool:
# Later siblings draw on top. Different parents means one of them is
# nested inside something else - the build menu's info popup is a child
# of the build menu - and there the conservative answer is "covered".
if other.get_parent() != own.get_parent():
return true
return other.get_index() > own.get_index()
func _gui_input(event: InputEvent) -> void:
# @tool runs this in the editor too, where a click on the canvas would
# emit `pressed` at whatever it is wired to. The glyph is what we want