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
160 lines
6.4 KiB
GDScript
160 lines
6.4 KiB
GDScript
extends SceneTree
|
|
|
|
# A player who is short of research data is told where it comes from.
|
|
#
|
|
# It is the one currency with a non-obvious source: ore and gas are visibly
|
|
# mined, but data comes off conquered worlds and nothing said so at the point
|
|
# it mattered. The skill tree refused the node in SILENCE, and the ship lab
|
|
# refused with "Research is already queued or complete" - which is not why it
|
|
# refused, and sends the player looking at the wrong thing.
|
|
#
|
|
# Covers:
|
|
# A. a doctrine that costs data the player has not got opens the explainer,
|
|
# naming what was wanted and what they hold
|
|
# B. the same from the ship lab, instead of blaming the queue
|
|
# C. with the data in hand, the research goes through and no popup appears
|
|
|
|
var game_state: Node
|
|
var scene: Node
|
|
var controller: Node
|
|
var popup: Control
|
|
var elapsed: float = 0.0
|
|
var stage: int = 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
game_state = root.get_node("GameState")
|
|
game_state.buildings.clear()
|
|
game_state.extractors.clear()
|
|
game_state.reset_progression()
|
|
game_state.reset_new_game()
|
|
game_state.intro_active = false
|
|
game_state.reset_quests()
|
|
game_state.rocks = 5000
|
|
game_state.bank_inventory.set_amount(GameState.RESOURCE_GAS, 5000)
|
|
game_state.bank_inventory.set_amount(GameState.RESOURCE_ENERGY, 5000)
|
|
game_state.research_points = 0
|
|
game_state.unlocked = ["shipyards"]
|
|
scene = load("res://world/Main.tscn").instantiate()
|
|
root.add_child(scene)
|
|
current_scene = scene
|
|
controller = scene.get_node("BuildController")
|
|
popup = scene.get_node("UI/ResearchPointsMenu")
|
|
|
|
|
|
func _costly_doctrine() -> String:
|
|
for node in GameState.SKILL_TREE:
|
|
if int(node.get("research_points", 0)) > 0:
|
|
return String(node.id)
|
|
return ""
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
elapsed += delta
|
|
if elapsed < 0.4:
|
|
return false
|
|
match stage:
|
|
0:
|
|
var doctrine: String = _costly_doctrine()
|
|
if doctrine == "":
|
|
return _fail("no doctrine costs research data, so this proves nothing")
|
|
# Everything but the data: the prerequisite met and the ore paid,
|
|
# so data is unambiguously the thing that is missing.
|
|
game_state.unlocked = ["shipyards"]
|
|
var requires: String = String(game_state.skill_node(doctrine).get("requires", ""))
|
|
if requires != "" and not game_state.is_unlocked(requires):
|
|
game_state.unlocked.append(requires)
|
|
game_state.research_points = 0
|
|
|
|
var mothership_id: int = game_state.add_building(
|
|
GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
|
controller.skill_menu.building_id = mothership_id
|
|
if popup.visible:
|
|
return _fail("the explainer was open before anything was refused")
|
|
controller._on_unlock_requested(doctrine)
|
|
if not popup.visible:
|
|
return _fail("a doctrine costing data was refused with no explanation")
|
|
if game_state.is_unlocked(doctrine):
|
|
return _fail("the doctrine was researched with no data at all")
|
|
var said: String = popup.have_label.text
|
|
if not said.contains("YOU HAVE 0"):
|
|
return _fail("the explainer does not say what the player holds: '%s'" % said)
|
|
if not popup.detail_label.text.to_lower().contains("scanner"):
|
|
return _fail("the explainer never mentions how to find a lab world")
|
|
print("skill tree: refused and explained - '%s'" % said)
|
|
popup.close_menu()
|
|
|
|
# --- C. with the data, it goes through, quietly ---------------
|
|
game_state.research_points = 20
|
|
controller._on_unlock_requested(doctrine)
|
|
if not game_state.is_unlocked(doctrine):
|
|
return _fail("the doctrine was refused with data in hand")
|
|
if popup.visible:
|
|
return _fail("the explainer opened on a research that succeeded")
|
|
print("with data in hand it goes through, and says nothing")
|
|
elapsed = 0.0
|
|
stage = 1
|
|
return false
|
|
1:
|
|
# --- B. the lab explains too, rather than blaming the queue ---
|
|
game_state.research_points = 0
|
|
var lab_id: int = game_state.add_building(GameState.BUILDING_SHIP_LAB,
|
|
Vector2(600.0, 0.0))
|
|
controller.lab_building_id = lab_id
|
|
controller.lab_vessel = GameState.VESSEL_INTERCEPTOR
|
|
controller.lab_choice = 0
|
|
var options: Array = controller._lab_options()
|
|
if options.is_empty():
|
|
return _fail("the lab offers nothing, so this proves nothing")
|
|
# Pick an option that actually costs data.
|
|
for i in range(options.size()):
|
|
if game_state.research_point_cost(String(options[i].id)) > 0:
|
|
controller.lab_choice = i
|
|
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")
|
|
if controller.lab_menu.status.text.contains("already queued"):
|
|
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
|
|
return false
|
|
|
|
|
|
func _fail(message: String) -> bool:
|
|
print("TEST_RESEARCH_POINTS_EXPLAINER: FAIL (%s)" % message)
|
|
quit(1)
|
|
return true
|