OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_areas_layout.gd

297 lines · gd ref:
extends SceneTree
## GATE — B1 of docs/editor-areas.md (hyper-games #124): the editor's areas.
##   A1  the regions are areas around ONE free rectangle R (BuildDock.view_rect()): the top
##       band and the status bar span the window; left | R | right side by side; the
##       Textures strip under R; the organizer over the right column
##   A2  the areas are opaque and R is not covered: covers() is false inside R, true on
##       every area
##   A3  a mouse drag on the left border widens the left column and shrinks R by the same
##       amount; the size is saved per player
##   A4  the same for the right border and the bottom strip's border
##   A5  collapsing an outer area (left, right, top, bottom) grows R into its space;
##       expanding restores the last size; the toggle stays reachable
##   A6  the status bar never collapses
##   A7  play mode hides every area
##
## RUN: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$GODOT" --headless --path . --script res://scenes/test/gate_areas_layout.gd

var _frame := 0
var _map: Node = null
var _dock: Node = null
var fails := 0
var checks := 0
var _step := 0


func _p(c: bool, m: String, got: Variant = "") -> void:
	checks += 1
	if not c:
		fails += 1
	print("  %-70s %s  %s" % [m, "OK" if c else "**FAIL**", str(got)])


func _mouse(pos: Vector2, button: bool, pressed: bool) -> void:
	if button:
		var b := InputEventMouseButton.new()
		b.button_index = MOUSE_BUTTON_LEFT
		b.pressed = pressed
		b.position = pos
		b.global_position = pos
		b.button_mask = MOUSE_BUTTON_MASK_LEFT if pressed else 0
		root.push_input(b)
	else:
		var m := InputEventMouseMotion.new()
		m.position = pos
		m.global_position = pos
		m.button_mask = MOUSE_BUTTON_MASK_LEFT if pressed else 0
		root.push_input(m)


## A real drag: press at `from`, move in steps to `to`, release.
func _drag(from: Vector2, to: Vector2) -> void:
	_mouse(from, false, false)
	_mouse(from, true, true)
	for i in range(1, 6):
		_mouse(from.lerp(to, i / 5.0), false, true)
	_mouse(to, true, false)


func _r() -> Rect2:
	return _dock.view_rect() if _dock.has_method("view_rect") else Rect2()


func _region(side: String) -> Control:
	var rec: Dictionary = _dock._sides.get(side, {})
	return rec.get("frame", rec.get("root")) as Control


var _r0 := Rect2()
var _w0 := 0.0


func _process(_d: float) -> bool:
	_frame += 1
	if _frame == 1:
		root.size = Vector2i(1920, 1080)      # headless is 64x64 otherwise
		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")
		root.get_node("/root/GameMode").set_mode("build")
		_map = (load("res://scenes/maps/map_flat.tscn") as PackedScene).instantiate()
		root.add_child(_map)
		_dock = root.get_node("/root/BuildDock")
		return false
	if _frame < 40 or _frame % 6 != 0:
		return false
	match _step:
		0: _a1_a2()
		1: _a3_start()
		2: _a3_check()
		3: _a4_right_start()
		4: _a4_right_check()
		5: _a4_bottom_start()
		6: _a4_bottom_check()
		7: _a5()
		8: _a5_check()
		9: _a7()
		_:
			if checks < 20:
				print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
				fails += 1
			print("\nGATE areas_layout: %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 _a1_a2() -> void:
	var win := Rect2(Vector2.ZERO, Vector2(root.size))
	var r := _r()
	_p(_dock.has_method("view_rect") and r.size.x > 600 and r.size.y > 400 and win.encloses(r),
		"A1 a free rectangle R inside the window", r)
	for side: String in ["top", "left", "right", "bottom", "organizer"]:
		_p(_dock.has_region(side) and _region(side) != null, "A1 region '%s' exists" % side)
	if _region("left") == null or _region("right") == null or _region("top") == null:
		return
	var lr := _region("left").get_global_rect()
	var rr := _region("right").get_global_rect()
	var tr := _region("top").get_global_rect()
	_p(absf(lr.end.x - r.position.x) <= 14 and absf(rr.position.x - r.end.x) <= 14,   # the split grabber is 12 px
		"A1 left | R | right side by side", [lr, r, rr])
	_p(tr.size.x >= win.size.x - 2 and tr.end.y <= r.position.y + 12, "A1 the top band spans the window, above R", tr)
	# ONE LINE PER TOP PANEL (user 2026-10-18: "the editor panel, editor should be to the
	# right so that the entire panel can be one line, same with tool"): the title button
	# sits at the RIGHT end of its panel's row, not on a line of its own above it.
	for id: String in ["topbar", "toolsettings"]:
		if not _dock.has_panel(id):
			continue
		var rec: Dictionary = _dock._panels[id]
		var hr := (rec["header"] as Control).get_global_rect()
		var br := (rec["body"] as Control).get_global_rect()
		_p(hr.position.x >= br.end.x - 2 and hr.get_center().y >= br.position.y and hr.get_center().y <= br.end.y,
			"A1 the '%s' title is at the right of its own row" % id, [hr, br])
	_p(tr.size.y <= 70, "A1 so the top band is two one-line rows (<= 70 px; spacing audit item 9)", tr.size.y)
	# NO GAP ABOVE THE TOOLS (user 2026-10-18: "there is a big gap on the left hand panel
	# above tools"): the left column used to pad itself down past the HUD readouts at the
	# window's top-left; those live in R's corner since a8aae69.
	# The pad was recomputed from wherever the column sat when the dock last re-measured,
	# so measure the cause itself, after forcing that re-measure.
	_dock.call("_clamp_to_viewport")
	var lmt: int = (_dock._sides["left"]["root"] as Control).get_theme_constant("margin_top")
	_p(lmt <= 8, "A1 the left column's panels start at its top (no HUD gap)", "margin_top %d" % lmt)
	# NO TRIANGLE ROW (spacing audit item 4): the collapse triangle sits in the area's
	# corner over the first title line, not on a row of its own above the panels.
	for side4: String in ["left", "right", "organizer"]:
		var fr4: Control = _dock._sides[side4]["frame"]
		var c4: Control = _dock._sides[side4]["scroll"]
		if fr4.is_visible_in_tree():
			_p(c4.get_global_rect().position.y - fr4.get_global_rect().position.y <= 8,
				"A1 the %s area's panels start at its top (no triangle row)" % side4,
				c4.get_global_rect().position.y - fr4.get_global_rect().position.y)
	var org := _region("organizer").get_global_rect()
	_p(org.end.y <= rr.position.y + 12 and absf(org.position.x - rr.position.x) < 2,
		"A1 the organizer sits over the right column", [org, rr])
	var outl: Variant = _dock._panels.get("outliner", {}).get("side", "")
	_p(String(outl) == "organizer", "A1 the Outliner is in the organizer area", outl)
	var bar: Control = (_dock._status_left as Control).get_parent().get_parent() as Control
	_p(bar.get_global_rect().size.x >= win.size.x - 2 and bar.get_global_rect().position.y >= r.end.y,
		"A1 the status bar spans the window, under R", bar.get_global_rect())
	# ---- A2 ----
	_p(not _dock.covers(r.get_center()), "A2 covers() is false inside R")
	var all_cover := true
	for side: String in ["top", "left", "right", "organizer"]:
		if not _dock.covers(_region(side).get_global_rect().get_center()):
			all_cover = false
	_p(all_cover, "A2 covers() is true on every area")
	var style: StyleBox = _region("left").get_theme_stylebox("panel")
	_p(style is StyleBoxFlat and is_equal_approx((style as StyleBoxFlat).bg_color.a, 1.0),
		"A2 the areas are opaque", (style as StyleBoxFlat).bg_color if style is StyleBoxFlat else style)


func _a3_start() -> void:
	_r0 = _r()
	_w0 = _region("left").size.x
	var y := _r0.get_center().y
	_drag(Vector2(_r0.position.x - 2, y), Vector2(_r0.position.x + 98, y))


func _a3_check() -> void:
	var r := _r()
	var w := _region("left").size.x
	_p(absf(w - (_w0 + 100)) <= 6 and absf(r.size.x - (_r0.size.x - 100)) <= 6,
		"A3 dragging the left border 100 px widens left, shrinks R by 100", [_w0, w, _r0.size.x, r.size.x])
	_p(absf(float(_dock.pref("area.left", 0)) - w) <= 2, "A3 the width is saved per player", _dock.pref("area.left", 0))
	# SHRINKING (user 2026-10-18: "the horizontal panel sliding does not work"): the drag
	# above only GREW the column. Narrowing must work too, down to a usable minimum.
	print("   left min width %s, right min width %s" % [_region("left").get_combined_minimum_size().x,
		(_dock._rsplit as Control).get_combined_minimum_size().x])
	var y := r.get_center().y
	_drag(Vector2(r.position.x - 2, y), Vector2(r.position.x - 202, y))
	await process_frame
	await process_frame
	var w2 := _region("left").size.x
	_p(w2 <= w - 150, "A3 dragging the left border 200 px LEFT narrows the column", [w, w2])


func _a4_right_start() -> void:
	_r0 = _r()
	_w0 = _region("right").size.x
	var y := _r0.get_center().y
	_drag(Vector2(_r0.end.x + 2, y), Vector2(_r0.end.x - 98, y))


func _a4_right_check() -> void:
	var w := _region("right").size.x
	_p(absf(w - (_w0 + 100)) <= 6 and absf(_r().size.x - (_r0.size.x - 100)) <= 6,
		"A4 the right border: right +100, R -100", [_w0, w])
	# NARROWING the right side (user: "the horizontal panel sliding does not work"): its
	# content (the outliner) is ~450 px wide, and a column could not go below its
	# content, so the default 380 sat pinned at 450 and a drag to narrow it did nothing.
	var r := _r()
	var y := r.get_center().y
	_drag(Vector2(r.end.x + 2, y), Vector2(r.end.x + 302, y))
	await process_frame
	await process_frame
	var w2: float = (_dock._rsplit as Control).size.x
	_p(w2 <= w - 250, "A4 dragging the right border 300 px RIGHT narrows the right side (below its content)", [w, w2])
	_drag(Vector2(_r().end.x + 2, y), Vector2(_r().end.x - 298, y))   # back for the next steps
	await process_frame


func _a4_bottom_start() -> void:
	_r0 = _r()
	_w0 = _region("bottom").size.y
	var x := _r0.get_center().x
	_drag(Vector2(x, _r0.end.y + 2), Vector2(x, _r0.end.y - 98))


func _a4_bottom_check() -> void:
	var h := _region("bottom").size.y
	_p(absf(h - (_w0 + 100)) <= 6 and absf(_r().size.y - (_r0.size.y - 100)) <= 6,
		"A4 the bottom strip's border: strip +100, R -100", [_w0, h])


var _before: Dictionary = {}


func _a5() -> void:
	_r0 = _r()
	for side: String in ["left", "right", "top", "bottom"]:
		_before[side] = _region(side).get_global_rect().size
		_dock.set_region_collapsed(side, true)


func _a5_check() -> void:
	var r := _r()
	var grew_x: float = r.size.x - _r0.size.x
	var grew_y: float = r.size.y - _r0.size.y
	var want_x: float = (_before["left"] as Vector2).x + (_before["right"] as Vector2).x
	var want_y: float = (_before["top"] as Vector2).y + (_before["bottom"] as Vector2).y
	_p(grew_x > want_x - 120 and grew_y > want_y - 120,
		"A5 collapsing all four outer areas grows R into their space (toggle strips left)",
		[_r0.size, r.size, want_x, want_y])
	var unreachable: Array = []
	for side: String in ["left", "right", "top", "bottom"]:
		var t: Button = _dock.region_toggle(side)
		if not (t != null and t.is_visible_in_tree() and Rect2(Vector2.ZERO, Vector2(root.size)).encloses(t.get_global_rect())):
			unreachable.append([side, t.get_global_rect() if t != null else null, t.is_visible_in_tree() if t != null else false])
	_p(unreachable.is_empty(), "A5 every toggle stays reachable on screen", unreachable)
	var bar: Control = (_dock._status_left as Control).get_parent().get_parent() as Control
	_p(bar.is_visible_in_tree(), "A6 the status bar never collapses")
	for side: String in ["left", "right", "top", "bottom"]:
		_dock.set_region_collapsed(side, false)


func _a7() -> void:
	var r := _r()
	_p(absf(r.size.x - _r0.size.x) <= 6 and absf(r.size.y - _r0.size.y) <= 6,
		"A5 expanding restores the last sizes (R as before)", [_r0.size, r.size,
		"top %s -> %s" % [(_before["top"] as Vector2).y, _region("top").size.y],
		"bottom %s -> %s" % [(_before["bottom"] as Vector2).y, _region("bottom").size.y]])
	# ---- A8: the membership / mesh readout never sits on an area ----
	# (user 2026-10-18: "membership daemon info still overwrites the ui")
	var ks: Node = root.get_node_or_null("/root/KeetService")
	var gst: Node = ks.get("game_state") if ks != null else null
	var ov: CanvasLayer = gst.get("_diag_overlay") if gst != null else null
	var lbl: Control = gst.get("_diag_label") if gst != null else null
	if ov != null and lbl != null:
		ov.visible = true
		gst.call("_process", 0.0)
		var lr := Rect2(ov.offset + lbl.position, lbl.size)
		_p(ov.offset.is_equal_approx(r.position) and r.encloses(Rect2(lr.position, Vector2(1, 1))),
			"A8 build mode: the membership readout starts inside the 3D rectangle", [ov.offset, r.position, lr])
	else:
		_p(false, "A8 the membership overlay exists (KeetService.game_state)", [ks, gst, ov])
	var gd: Script = load("res://scenes/game/game.gd")
	_p(gd != null and gd.can_instantiate(), "A8 game.gd (peer / FPS lines follow R) compiles")
	root.get_node("/root/GameMode").set_mode("play")
	_p(not _dock.visible, "A7 play mode hides every area")
	if ov != null and gst != null:
		gst.call("_process", 0.0)
		_p(ov.offset == Vector2.ZERO, "A8 play mode: the readout is back at the window corner", ov.offset)
	root.get_node("/root/GameMode").set_mode("build")