2026-08-11 20:17:58 +02:00
|
|
|
extends SceneTree
|
|
|
|
|
|
|
|
|
|
# Headless gameplay test: run with
|
|
|
|
|
# godot --headless -s res://tests/test_modal_blocks_input.gd
|
|
|
|
|
# With a panel open, nothing behind it may react to a touch.
|
|
|
|
|
#
|
|
|
|
|
# Two separate leaks, because there are two separate input paths:
|
|
|
|
|
#
|
|
|
|
|
# 1. GUI clicks. The skill and formation panels cannot block these the
|
|
|
|
|
# normal way (mouse_filter = STOP), because the skill tree and the
|
|
|
|
|
# formation editor are driven from raw _unhandled_input, and a Control
|
|
|
|
|
# that consumes touches through the GUI system swallows them before
|
|
|
|
|
# they ever get there. So the panels stay transparent and the buttons
|
|
|
|
|
# check whether they are behind one.
|
|
|
|
|
# 2. Raw input. A touch landing on the panel but missing a skill node was
|
|
|
|
|
# left unhandled, so it fell through to GameController - which sits on
|
|
|
|
|
# the ROOT node and therefore sees everything its children ignore - and
|
|
|
|
|
# moved the fleet or pinch-zoomed the world behind the menu.
|
|
|
|
|
#
|
|
|
|
|
# Verifies:
|
|
|
|
|
# 1. a button outside an open panel does not fire
|
|
|
|
|
# 2. a button INSIDE the open panel still does (or the panel is unusable)
|
|
|
|
|
# 3. with every panel closed, that same outside button fires normally, so
|
|
|
|
|
# a pass cannot just mean "the button never works"
|
|
|
|
|
# 4. a touch on an open panel does not reach the world behind it
|
|
|
|
|
|
|
|
|
|
var scene: Node
|
|
|
|
|
var game_state: Node
|
|
|
|
|
var fleet: Node
|
|
|
|
|
var skill_menu: Control
|
|
|
|
|
var map_button: Control
|
|
|
|
|
var skill_close: Control
|
|
|
|
|
var stage: int = 0
|
|
|
|
|
var elapsed: float = 0.0
|
|
|
|
|
var outside_fired: bool = false
|
|
|
|
|
var inside_fired: bool = false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _initialize() -> void:
|
|
|
|
|
game_state = root.get_node("GameState")
|
|
|
|
|
game_state.buildings.clear()
|
|
|
|
|
game_state.extractors.clear()
|
|
|
|
|
game_state.transporters.clear()
|
|
|
|
|
scene = load("res://Main.tscn").instantiate()
|
|
|
|
|
root.add_child(scene)
|
|
|
|
|
fleet = scene.get_node("Fleet")
|
|
|
|
|
skill_menu = scene.get_node("UI/SkillMenu")
|
|
|
|
|
map_button = scene.get_node("UI/MapButton")
|
|
|
|
|
skill_close = scene.get_node("UI/SkillMenu/CloseButton")
|
|
|
|
|
map_button.pressed.connect(func(): outside_fired = true)
|
|
|
|
|
skill_close.pressed.connect(func(): inside_fired = true)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _tap(button: Control) -> void:
|
|
|
|
|
# Straight at the button's own _gui_input, which is where a real tap
|
|
|
|
|
# lands once Godot has routed it.
|
|
|
|
|
var centre: Vector2 = button.size / 2.0
|
|
|
|
|
for pressed in [true, false]:
|
|
|
|
|
var ev: InputEventScreenTouch = InputEventScreenTouch.new()
|
|
|
|
|
ev.pressed = pressed
|
|
|
|
|
ev.position = centre
|
|
|
|
|
button._gui_input(ev)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
# --- 3. baseline: it works with nothing open ----------------
|
|
|
|
|
outside_fired = false
|
|
|
|
|
_tap(map_button)
|
|
|
|
|
if not outside_fired:
|
|
|
|
|
print("TEST_MODAL_BLOCKS_INPUT: FAIL (the button never fires even with no panel open - the test proves nothing)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
print("no panel open -> outside button fires")
|
|
|
|
|
# That tap actually OPENED the world map, which is itself a modal
|
|
|
|
|
# panel - leave it open and it correctly blocks the research
|
|
|
|
|
# panel's own buttons, which is a true result but not the one
|
|
|
|
|
# being measured here.
|
|
|
|
|
scene.get_node("UI/WorldMap").visible = false
|
|
|
|
|
|
|
|
|
|
skill_menu.visible = true
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 1
|
|
|
|
|
return false
|
|
|
|
|
1:
|
|
|
|
|
if elapsed < 0.2:
|
|
|
|
|
return false
|
|
|
|
|
# --- 1. behind an open panel --------------------------------
|
|
|
|
|
outside_fired = false
|
|
|
|
|
_tap(map_button)
|
|
|
|
|
if outside_fired:
|
|
|
|
|
print("TEST_MODAL_BLOCKS_INPUT: FAIL (a button behind the open research panel still fired)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
print("research panel open -> button behind it is inert")
|
|
|
|
|
|
|
|
|
|
# --- 2. inside it ------------------------------------------
|
|
|
|
|
inside_fired = false
|
|
|
|
|
_tap(skill_close)
|
|
|
|
|
if not inside_fired:
|
|
|
|
|
print("TEST_MODAL_BLOCKS_INPUT: FAIL (the panel's own close button stopped working)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
print("panel's own button still works")
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 2
|
|
|
|
|
return false
|
|
|
|
|
2:
|
|
|
|
|
if elapsed < 0.2:
|
|
|
|
|
return false
|
|
|
|
|
# --- 4. raw input must not reach the world ------------------
|
|
|
|
|
skill_menu.visible = true
|
|
|
|
|
var before: Vector2 = fleet.fleet_target
|
|
|
|
|
var controller: Node = scene.get_node("BuildController")
|
|
|
|
|
var ev: InputEventScreenTouch = InputEventScreenTouch.new()
|
|
|
|
|
ev.pressed = true
|
|
|
|
|
ev.position = Vector2(360.0, 900.0)
|
|
|
|
|
controller._unhandled_input(ev)
|
|
|
|
|
if not root.get_viewport().is_input_handled():
|
|
|
|
|
print("TEST_MODAL_BLOCKS_INPUT: FAIL (a touch on the open panel was left unhandled - it falls through to the world behind)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
if fleet.fleet_target != before:
|
|
|
|
|
print("TEST_MODAL_BLOCKS_INPUT: FAIL (the fleet moved from a touch on the panel)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
print("touch on the panel consumed, fleet target unchanged")
|
Fix every button being permanently blocked after closing the build menu
The real cause of "the crafter buttons do nothing", reported three
times. IconButton._blocked_by_modal tested `menu.visible`, but hiding a
parent leaves a child's own `visible` flag true - and the build menu's
info popup is itself one of the modal panels. So opening any building's
info popup and then closing the build menu with the X left an invisible
popup still claiming to be open, and from that moment EVERY button in
the game was blocked, with no way for a player to get out of it.
It tests is_visible_in_tree() now, and closing the build menu also
closes its popup rather than leaving it half-open.
My earlier attempts kept missing it because the probes called the
handlers directly - they exercised the crafter logic, which was fine all
along, and never the input path in that state. The regression test
reproduces the exact sequence: open the build menu, show a building's
info, hide the menu, then check an unrelated button still fires.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:41:00 +02:00
|
|
|
|
|
|
|
|
# --- 5. a hidden parent must not leave a panel "open" -------
|
|
|
|
|
# The build menu's info popup is itself a modal panel. Hiding the
|
|
|
|
|
# build menu leaves the popup's own `visible` flag true, so a
|
|
|
|
|
# guard testing `visible` rather than `is_visible_in_tree()`
|
|
|
|
|
# blocked EVERY button in the game from then on - with no way for
|
|
|
|
|
# a player to get out of it. Reported three times as "the buttons
|
|
|
|
|
# do nothing".
|
|
|
|
|
skill_menu.visible = false
|
|
|
|
|
var controller2: Node = scene.get_node("BuildController")
|
|
|
|
|
controller2._on_build_pressed()
|
|
|
|
|
controller2._show_building_info(GameState.BUILDING_SHIP_CRAFTER)
|
|
|
|
|
scene.get_node("UI/BuildMenu").visible = false
|
|
|
|
|
var info: Control = scene.get_node("UI/BuildMenu/InfoPopup")
|
|
|
|
|
print("popup after hiding its parent: visible=%s in_tree=%s" % [
|
|
|
|
|
info.visible, info.is_visible_in_tree()
|
|
|
|
|
])
|
|
|
|
|
outside_fired = false
|
|
|
|
|
_tap(map_button)
|
|
|
|
|
if not outside_fired:
|
|
|
|
|
print("TEST_MODAL_BLOCKS_INPUT: FAIL (a panel hidden with its parent still blocks every button)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
print("hidden-with-parent panel no longer blocks anything")
|
2026-08-11 20:17:58 +02:00
|
|
|
print("TEST_MODAL_BLOCKS_INPUT: PASS")
|
|
|
|
|
quit(0)
|
|
|
|
|
return true
|
|
|
|
|
return false
|