OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_click_through.gd

224 lines · gd ref:
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)