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")