extends SceneTree
## GATE — a click on a dock panel never selects what is behind it (hyper-games #109,
## 2026-10-12: "mouse should not select things that are underneath a panel window").
##
## Click-to-select lives in map_dc_level._unhandled_input, which a click reaches when no
## Control under the pointer STOPS it. A brush wall fills the view, so any click that
## falls through selects it.
##
## C0 control: a click in the open (no Control under it) selects the wall
## C for EVERY visible panel: a click on its padding, its centre and a Label in it
## selects nothing; nor does a click in the gap between two stacked panels (the
## column's separation — what fell through: 5 gaps red before BuildDock.covers)
##
## Targets are found AT CLICK TIME: the dock re-lays out as the selection changes, and
## points computed up front landed beside it (three false failures on the first run).
## The window is set to 1920x1080 — headless is 64x64 and every panel sat off screen.
##
## RUN: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$GODOT" --headless --path . --script res://scenes/test/gate_click_through.gd
var _frame := 0
var _map: Node = null
var _cam: Camera3D = null
var _step := 0
var fails := 0
var checks := 0
var _label_at := Vector2.ZERO
var _pad_at := Vector2.ZERO
var _open_at := Vector2.ZERO
## A screen point no visible, mouse-taking Control covers (the headless window is small
## and the dock fills much of it; its centre is a status label).
func _find_open() -> Vector2:
var ctrls: Array[Control] = []
for n: Node in root.find_children("*", "Control", true, false):
var c := n as Control
if c.is_visible_in_tree() and c.mouse_filter != Control.MOUSE_FILTER_IGNORE:
ctrls.append(c)
var sz := root.get_visible_rect().size
for gy in range(1, 10):
for gx in range(1, 10):
var pt := Vector2(sz.x * gx / 10.0, sz.y * gy / 10.0)
var covered := false
for c: Control in ctrls:
if c.get_global_rect().has_point(pt):
covered = true
break
if not covered:
return pt
return sz * 0.5
func _p(c: bool, m: String, got: Variant = "") -> void:
checks += 1
if not c:
fails += 1
print(" %-66s %s %s" % [m, "OK" if c else "**FAIL**", str(got)])
func _box(lo: Vector3, hi: Vector3) -> Array:
return [1, 0, 0, hi.x, -1, 0, 0, -lo.x, 0, 1, 0, hi.y, 0, -1, 0, -lo.y, 0, 0, 1, hi.z, 0, 0, -1, -lo.z]
func _click(at: Vector2) -> void:
for pressed: bool in [true, false]:
var e := InputEventMouseButton.new()
e.button_index = MOUSE_BUTTON_LEFT
e.pressed = pressed
e.position = at
e.global_position = at
root.push_input(e)
## Anything selected — a click on a brush wall selects a FACE first (element mode), so
## counting _sel_brushes alone misreads it.
func _selected() -> int:
return 0 if String(_map.get("_sel_kind")) == "" else 1
func _hover(at: Vector2) -> void:
var m := InputEventMouseMotion.new()
m.position = at
m.global_position = at
root.push_input(m)
func _process(_d: float) -> bool:
_frame += 1
if _frame == 1:
# HEADLESS IS 64x64 and ignores --resolution: every dock panel would sit outside
# the window, where no Control can stop a click and the gate would "reproduce"
# nothing real. A real window size first.
root.size = Vector2i(1920, 1080)
root.get_node("/root/GameMode").set_mode("build")
var keet: Node = root.get_node_or_null("/root/KeetService")
if keet != null:
keet.current_member_id = "gate-author"
var gs: Node = root.get_node_or_null("/root/GameState")
if gs != null:
gs.route("world:level:*", 1, "peer")
_map = (load("res://scenes/maps/map_flat.tscn") as PackedScene).instantiate()
root.add_child(_map)
return false
if _frame < 30 or _frame % 4 != 0:
return false
match _step:
0:
_setup()
_open_at = _find_open()
_hover(_open_at)
1:
# Precondition: nothing of the UI under the control click.
var hc: Control = root.gui_get_hovered_control()
if hc != null:
print(" [diag] window %s; hovered %s rect %s filter %d canvas %s" % [root.get_visible_rect().size,
hc.get_path(), hc.get_global_rect(), hc.mouse_filter, str(hc.get_canvas_layer_node())])
_p(root.gui_get_hovered_control() == null, "C0 precondition: no Control under %s" % _open_at,
str(root.gui_get_hovered_control()))
_click(_open_at)
2:
_p(_selected() > 0, "C0 control: a click in the open selects the wall", _selected())
_map.select_brushes(PackedInt32Array())
_next_target()
_:
if _ti <= _targets.size():
var t: Dictionary = _targets[_ti - 1]
if t["at"] == Vector2(-1, -1):
print(" skip %s (gone or off screen at click time)" % String(t["name"]))
else:
# A click on a ROW of a panel's Tree (the outliner) selects that row's
# object: the panel's own selection, not a click through it. Since the
# areas (#124 B1) the organizer is tall enough that its centre is a row.
# (The map logs it as "outliner selected …": the Tree's row-selection path.)
var ol: Node = _map.get("_outliner")
var tr: Tree = ol.get("_tree") if ol != null else null
var by_row: bool = String(t["name"]).begins_with("outliner") and tr != null \
and tr.get_next_selected(null) != null
_p(_selected() == 0 or by_row, "C %s" % String(t["name"]), "at %s: %d%s"
% [t["at"], _selected(), " (a Tree row: the panel's own pick)" if by_row and _selected() > 0 else ""])
_map.select_brushes(PackedInt32Array())
if _ti < _targets.size():
_next_target()
_step += 1
return false
_ti += 1
_step += 1
return false
if checks < 4:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE click_through: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
quit(0 if fails == 0 else 1)
return true
_step += 1
return false
func _setup() -> void:
_cam = Camera3D.new()
root.add_child(_cam)
_cam.global_position = Vector3(0, 300, 0)
_cam.make_current()
var persist: Node = _map.get("_persist")
persist.record("brush.create", {"planes": _box(Vector3(-600, -300, -40), Vector3(600, 900, -30)),
"proj": [], "material_id": 0, "level_id": 0, "sky": false})
persist.refold()
# EVERY visible panel: its padding, its centre, a Label in it; and the gap between
# consecutive frames in each column (the column's separation).
var dock: Node = root.get_node("/root/BuildDock")
var panels: Dictionary = dock.get("_panels")
var frames: Array[Control] = []
for id: String in panels:
var section: Control = (panels[id] as Dictionary)["section"]
var frame := section.get_parent() as Control
if frame == null or not frame.is_visible_in_tree() or frame.size.x < 20 or frame.size.y < 10:
continue
frames.append(frame)
_targets.append({"name": "%s: padding" % id, "kind": "padding", "a": frame})
_targets.append({"name": "%s: centre" % id, "kind": "centre", "a": frame})
for l: Node in frame.find_children("*", "Label", true, false):
if (l as Label).is_visible_in_tree() and (l as Label).size.x > 10:
_targets.append({"name": "%s: label '%s'" % [id, (l as Label).text.left(16)], "kind": "centre", "a": l})
break
for a: Control in frames:
for b: Control in frames:
if a.get_parent() == b.get_parent() and a.get_index() + 1 == b.get_index():
_targets.append({"name": "gap below %s" % a.name, "kind": "gap", "a": a, "b": b})
print(" [setup] %d click targets over %d visible panels" % [_targets.size(), frames.size()])
var _targets: Array = []
var _ti := 0
## WHERE THE TARGET IS NOW. The dock re-lays out as the selection changes (the tool bar
## grows and shrinks), so a point computed at setup can land beside the dock by the time
## it is clicked — that read as three false failures.
func _target_at(t: Dictionary) -> Variant:
var a := t["a"] as Control
if a == null or not is_instance_valid(a) or not a.is_visible_in_tree():
return null
var at := Vector2.ZERO
match String(t["kind"]):
"padding":
at = a.global_position + Vector2(3, a.size.y * 0.5)
"centre":
at = a.global_position + a.size * 0.5
"gap":
var b := t["b"] as Control
var gap_top := a.global_position.y + a.size.y
if b == null or not b.is_visible_in_tree() or b.global_position.y - gap_top < 2.0:
return null
at = Vector2(a.global_position.x + a.size.x * 0.5, (gap_top + b.global_position.y) * 0.5)
return at if root.get_visible_rect().has_point(at) else null
func _next_target() -> void:
var t: Dictionary = _targets[_ti]
_ti += 1
var at: Variant = _target_at(t)
t["at"] = at if at != null else Vector2(-1, -1)
if at != null:
_click(at as Vector2)