extends SceneTree ## Dev tool: build a PlayerHUD against a stand-in player, let it lay out, and ## print where every element actually landed. ## ## godot --headless --path . -s res://debug/hud_layout_check.gd ## ## A HUD element that lands at (0, 0) with zero size is the single most common ## thing to get wrong here, and it is invisible in a headless smoke test and ## easy to miss in a screenshot. This prints the rects so the answer is a number. var _frames := 0 var _hud: PlayerHUD func _initialize() -> void: var stand_in := CharacterBody3D.new() stand_in.name = "StandInPlayer" root.add_child(stand_in) _hud = PlayerHUD.new() _hud.player = stand_in root.add_child(_hud) func _process(_delta: float) -> bool: _frames += 1 # A hidden Control keeps its last rect and contributes nothing to its # container's minimum size, so measuring the ammo card while the stand-in # player holds no weapon reads a stale position from a collapsed column and # reports a layout bug that is not there. Force the states that matter to be # visible, then give the containers a frame to re-sort before measuring. if _frames == 4: # Freeze the HUD first. Its own `_process` hides the ammo card whenever # the player holds no weapon, so anything forced visible here is undone # again before the measurement and what gets read is the stale rect of a # hidden control inside a collapsed column. _hud.set_process(false) _hud._ammo_card.visible = true _hud._ammo_num.text = "30" _hud._ammo_max.text = "/ 30" _hud._weapon_name.text = "DOUBLE BARREL SHOTGUN" _hud._chain_row.visible = true for pip in _hud._grenade_row.get_children(): (pip as Control).visible = true if _frames < 8: return false print("viewport = ", root.get_visible_rect().size) if OS.has_environment("HUD_TREE"): _dump(_hud, 0) var fails := 0 for node in _hud.find_children("*", "Control", true, false): var c := node as Control # Only the elements that are supposed to occupy space. Containers that # legitimately shrink to their content are not interesting. if c.name in ["Crosshair", "ReloadRing", "DeathScreen"]: var r := c.get_global_rect() # GLOBAL: get_rect() is parent-relative and lies about where a nested element really sits print(" %-12s pos=%s size=%s" % [c.name, r.position, r.size]) if r.size.x < 100.0 or r.size.y < 100.0: print(" FAIL: expected to fill the viewport") fails += 1 # The ammo card, which must hug the RIGHT edge inside its margin. It sat # flush against the left instead, because a VBoxContainer hands each child # its own horizontal placement and the alignment flag was on the column. var vr := root.get_visible_rect().size var card := _find_by_class("PanelContainer") if card == null: print(" MISSING ammo card") fails += 1 else: var r := card.get_global_rect() var right_gap := vr.x - (r.position.x + r.size.x) print(" %-12s pos=%s size=%s right_gap=%.0f" % ["ammo", r.position, r.size, right_gap]) if r.size.x < 60.0: print(" FAIL: collapsed to nothing") fails += 1 elif right_gap < 0.0: print(" FAIL: runs off the right edge of the screen") fails += 1 elif right_gap > 120.0: print(" FAIL: not anchored to the right edge (gap %.0f)" % right_gap) fails += 1 print("HUD LAYOUT: %s" % ("PASS" if fails == 0 else "FAIL (%d)" % fails)) quit(1 if fails > 0 else 0) return true ## The whole Control tree with GLOBAL rects. `HUD_TREE=1` to switch on — when a ## container collapses, the answer is always visible in its parent chain and ## never in the leaf you noticed the problem on. func _dump(node: Node, depth: int) -> void: for child in node.get_children(): if child is Control: var c := child as Control var r := c.get_global_rect() print("%s%s [%s] pos=%s size=%s vis=%s" % [" ".repeat(depth + 1), c.name, c.get_class(), r.position, r.size, c.visible]) _dump(child, depth + 1) func _find_by_class(cls: String) -> Control: for n in _hud.find_children("*", cls, true, false): return n as Control return null