HUD and Bars Reference
The on-screen game UI is a set of self-managing nodes, each owning its own screen-space position, its own polling, and its own signal subscriptions. Underroot ships two HUD skins; this page covers the classic skin's surfaces and the chrome both skins share (BuildMenu/CraftMenu, DepthRuler, ToastQueue, HelpOverlay, SettingsPanel). The talisman skin's instruments — medallion, crest, charm, rail, ledger, speech bubbles — are documented in The Talismans HUD. For the conventions all surfaces share (procedural build, CanvasLayer sizing, tooltips, UITheme tokens), see UI Construction Conventions.
Two HUD skins
HudShell (scripts/ui/HudShell.gd, static class) owns which skin is active — "classic" or "talismans" — persisted as hud_style in user://settings.json. Since the Phase 4 default flip, fresh profiles default to the talisman skin (persisted immediately); profiles that predate the skin stay classic until they opt in. The Settings > GAMEPLAY row is "Legacy UI" (ON = classic HUD), hidden during a first playthrough and always visible while classic is active.
A flip emits EventBus.hud_style_changed; HUD.gd responds by swapping the threat panel (MawBar ↔ MawMedallion) and the bottom chrome (BottomBar + VillageBar ↔ HearthCrest + DiggerCharm + SpeechBubbleLayer + LedgerOverlay + MaterialRail). VillageBar is hidden and processing-disabled in the talisman skin, never freed. The tutorial (TutorialOverlay/TutorialNudges) and HelpOverlay are skin-aware — copy resolves {ui_*} placeholders against the live skin and anchors resolve against whichever provider is active, so neither forces a particular skin.
scenes/ui/HUD.gd is the parent: its _ready() instantiates the shared sub-components (DepthRuler, ToastQueue, HelpOverlay, SettingsPanel), spawns the active skin's threat panel and bottom chrome, and builds the left panel (the classic sidebar; a hidden journal drawer under the talisman skin). Most surfaces re-derive their position from get_viewport().size_changed and poll live state on a ~1 s timer.
HUD sidebar (classic skin)
scenes/ui/HUD.gd (extends CanvasLayer) draws the left-edge sidebar: the materials/inventory panel, the journal tab, tools, and machine status.
What it shows. A two-tab panel — ITEMS and JOURNAL. The ITEMS tab lists inventory in collapsible categories built from constant material arrays: INV_TERRAIN, INV_EXOTIC, INV_REFINED, INV_GLAZED (the same tables the talisman skin's MaterialRail and LedgerOverlay read). Each category header shows a [held/total] · qty summary via _cat_header_text(). Exotic rows stay hidden until the material has been seen (CraftingUnlockManager.seen_materials). Below the materials come the active tools with durability bars and a live MACHINES section. A pinned ▼ Dig Front button and clickable material rows teleport the camera to the relevant layer (EventBus.player_teleport_requested). The JOURNAL tab holds the active-tasks list (from ProjectManager) and a narrative log.
Talisman skin. The sidebar becomes a hidden-by-default journal drawer (_apply_sidebar_skin()): the tab bar is hidden and the ITEMS tab never renders — materials live on the TAB ledger and the left rail instead. The drawer opens to the journal via the [I] hotkey, the crest's tasks button, or EventBus.open_journal_tab. Task fulfil/deny/collect logic is identical in both skins.
Positioning and polling. _ready() connects get_viewport().size_changed to _reposition() and defers _initial_refresh. A _poll_timer in _process() fires every 1.0 s to refresh machine status and journal projects; a separate _pulse_timer throttles the unread-journal tab pulse to ~20 fps (add_theme_color_override invalidates the theme, so per-frame pulsing is wasteful). Collapsing the sidebar or switching tabs calls _left_panel.reset_size() so the PanelContainer shrinks synchronously.
Key constants.
| Constant | Value | Role |
|---|---|---|
DRAWER_W |
280.0 |
sidebar width (matches the BuildMenu drawer) |
BOTTOM_BAR_H |
44.0 |
reserved bottom strip |
VILLAGE_BAR_H |
36.0 |
must match VillageBar.BAR_H |
SIDEBAR_CHROME_H |
72.0 |
expanded chrome height for layout math |
Fold state per category persists per-save through the UIFolds accessor (UIFolds.is_open("hud.terrain", true) etc.). HUD.get_bottom_bar_y() is skin-aware: classic returns the bottom bar's top, talisman the higher of the crest/charm tops, so consumers (DepthRuler, VillageBar) clear whichever chrome is live.
MawBar (classic skin)
scenes/ui/MawBar.gd (extends CanvasLayer) is the classic skin's top-right threat panel. The talisman skin replaces it with MawMedallion, which joins the same maw_bar group, exposes the same bottom_y(), and shares the same fold key, so consumers don't care which is live.
What it shows. Line 1: breach ETA and total standing wall HP. Line 2: the idle ETA, a combined Hunger ×N escalation readout, and (during a surge) a ⚡ Surging ×N — Ns countdown. A ▸/▾ chevron expands a per-column queue (up to MAX_QUEUE_ROWS = 4 rows) showing each wall the Maw must chew — the active column marked ▶, each with material, HP, a QueueBar, and a per-column survival time. With the Two Fronts challenge active it reports both fronts' ETAs and labels the queue East → / ← West. A surge-song switcher button appears while a surge track plays, after the post-3rd-ritual unlock.
Positioning and polling. _reposition() calls _panel.reset_size() to force synchronous size-to-content, then pins the panel to Vector2(vp.x - panel_w - 8.0, 3.0). A _poll_timer refreshes the readout every 1.0 s (_refresh_eta()), reading MawController.fronts, AstrolabeManager multipliers, and per-front resonance_remaining. It exposes bottom_y() so the BuildMenu drawer can pin itself just below the panel as the queue expands and collapses. The panel joins the maw_bar group so the tutorial can anchor a ring beside it. Expanded-fold state persists via UIFolds.set_open("maw_bar.expanded", …), self-healed off the first poll that sees run state (_sync_fold_once()) to survive the build-once run-state race.
Key constants. MAX_QUEUE_ROWS = 4; _FOLD_KEY = "maw_bar.expanded" (shared with MawMedallion so the fold survives skin flips); _SONG_CYCLE / _SONG_LABELS drive the surge-song switcher. The QueueBar inner class draws the HP bar with draw_rect (no block glyphs — those are absent from the bundled web font subset), green→amber→red as it empties.
VillageBar (classic skin)
scenes/ui/VillageBar.gd (extends CanvasLayer) is the village support strip, sitting directly above the bottom bar. Under the talisman skin it is hidden and processing-disabled (its role moves to the HearthCrest and the SpeechBubbleLayer); flipping back to classic re-enables it in place.
What it shows. Population plus three support metrics — food, shelter, safety — each as Score/Pop text with a thin colour-coded level bar. A task-tally button (N tasks) opens the journal tab. A 🔔 call button summons Wren (VillageManager.call_villager()), pulsing gold while a visit is available. A scrolling ticker marquees active-task flavour text and, at intervals, a clickable donate line. A collapsed mode shrinks the metrics to icon+value cells. A pending building-upgrade ("village bell") button appears while a bell decision is outstanding.
Positioning and polling. _ready() sets layer = 2. _reposition() finds the HUD, calls get_bottom_bar_y() to sit flush above the bottom bar, then — per the CanvasLayer sizing rule — assigns both custom_minimum_size and .size explicitly before setting position. There is no fixed poll timer; _process(delta) drives the task-button colour ramp, the call-button pulse, the donate timer, and the ticker marquee every frame, while metric values update from EventBus signals (population_changed, food_changed, water_changed, building_completed, project signals).
Key constants.
| Constant | Value | Role |
|---|---|---|
BAR_H |
44.0 |
strip height |
FONT_SIZE |
11 |
cell font |
TICK_INTERVAL |
5.0 |
metric refresh cadence reference |
DONATE_INTERVAL |
1800.0 |
seconds between donate ticker lines |
DONATE_DWELL |
14.0 |
display time for a non-scrolling donate line |
Metric colour thresholds live in _metric_color(): green at ratio ≥ 1.0 (score ≥ population), amber ≥ 0.8, orange ≥ 0.5, red below — the same rule the tooltip text and the tutorial's elder gates use. VillageBar and HearthCrest expose the same tutorial provider methods (task_button_screen_pos(), get_population_label_screen_pos(), metric_screen_pos(id)), so tutorial anchors resolve identically on either skin.
BottomBar (classic skin)
scenes/ui/BottomBar.gd (extends PanelContainer — a child managed by the HUD, not its own CanvasLayer) is the food/water supply bar and stats strip. Under the talisman skin its content splits across the HearthCrest (supply bars, ETAs, idle line) and the DiggerCharm (run stats, songbook, chips, help/settings) and the BottomBar is not built.
What it shows. Player Food and Water progress bars with percentages, plus optional sub-bars for stored supply (Silo, Well, Tower) that appear only when the corresponding building is owned. A supply-ETA readout (food ~8m · water ~5m) with an idle-window second line. A run-stats label (Day N • best Nm • N tiles • 🪙 N). A ♪ Songs village-songbook button (hidden until the first song unlocks), an active-challenge chip, and ? help / ⚙︎ settings buttons that emit help_requested / settings_requested for the HUD to handle.
Positioning and polling. It emits layout_changed only when a sub-bar row appears or disappears (never on a value tick), and the HUD relayouts in response. A _poll_timer refreshes the supply ETA and the challenge chip every 1.0 s; food/water bars update from EventBus.food_changed / water_changed / storage_changed / well_changed. Idle-window urgency colours the idle line dim / amber / pulsing-red by how long stores feed everyone, gated on both storage buildings existing.
Key constants. IDLE_AMBER_S = 16 h, IDLE_RED_S = 8 h (idle-line urgency thresholds — HearthCrest uses the same values); the SONGBOOK array defines the six earnable songs and their unlock hints (duplicated on DiggerCharm for the talisman skin). The stats label converts depth tiles to metres at the game-wide ×1.5 convention.
DepthRuler (both skins)
scenes/ui/DepthRuler.gd (extends Control) is the right-edge "where am I underground" indicator, shared by both skins (with a small talisman-skin positioning branch so it clears the carved bar).
What it shows. A vertical strip of layer-coloured bands mapping the visible world-Y span, fogged below the deepest explored layer with a single desaturated ??? teaser band. Meter ticks every TICK_EVERY_TILES tiles, a live player caret with a depth label, a dashed best record marker at depth_reached, and discovered-only inclusion dots (round, natural) and Astrolabe-seeded exotic markers (diamond). Band tooltips describe the layer, its main material, and discovered inclusions.
Positioning and polling. _ready() sets mouse_filter = MOUSE_FILTER_PASS so hover/tooltips work but clicks fall through to world input. It fades in/out (FADE_SPEED) — hidden on the surface (SURFACE_HIDE_TILES) and while the BuildMenu drawer is open. Redraws are dirty-flagged: _process() calls queue_redraw() only when the camera or player drifts past REDRAW_EPSILON px, and throttles to LOW_PERF_REDRAW (10 fps) under Performance Mode. World↔ruler mapping reads get_viewport().get_camera_2d(), mirroring TerrainDisplay's cull-rect math.
Key constants.
| Constant | Value | Role |
|---|---|---|
M_PER_TILE |
1.5 |
game-wide depth display convention (tiles → metres) |
STRIP_W |
16.0 |
band strip width |
CONTROL_W |
56.0 |
label gutter + strip |
TOP_MARGIN |
170.0 |
clears the threat panel |
TICK_EVERY_TILES |
10 |
tick + label spacing (15 m) |
REDRAW_EPSILON |
4.0 |
px drift that forces a redraw |
hud and build_menu references are injected by the HUD to provide get_bottom_bar_y() and is_open().
ToastQueue (both skins)
scenes/ui/ToastQueue.gd (extends CanvasLayer) is the notification toast system, shared by both skins.
What it shows. One toast at a time — a centered PanelContainer with a coloured label at UITheme.T_TOAST (15) — pulled from a FIFO queue. Any code emits EventBus.show_toast.emit(text, color, duration) and the queue displays it; there is no API surface the HUD must call.
Positioning and polling. _ready() sets layer = 4 (above VillageBar at layer 2 and the threat panel) and connects EventBus.show_toast. _process() counts down the current toast's _timer, hides it, then pops the next queued entry. _reposition() centers the panel horizontally and stacks it above the bottom chrome — the classic branch uses BOTTOM_BAR_H and VILLAGE_BAR_H, with a talisman branch that clears the crest/charm instead.
Key constant — queue cap. _on_show_toast drops any toast that arrives when the queue already holds 3:
func _on_show_toast(text: String, color: Color, duration: float) -> void:
if _queue.size() >= 3:
return
_queue.append({"text": text, "color": color, "duration": duration})
So the effective cap is 3 queued messages; default duration when unspecified by callers is 4.0 s.
BuildMenu and CraftMenu (both skins)
scenes/ui/BuildMenu.gd (class_name BuildMenu, extends CanvasLayer) is the right-edge slide-out drawer; scenes/ui/CraftMenu.gd (class_name CraftMenu, extends VBoxContainer) is the crafting list rendered inside it.
What it shows. A four-tab drawer — BUILD, CRAFT, PROCESS, VILLAGE (the Tab enum). BUILD lists wall material cards grouped by WALL_TIERS (RAW / REFINED / GLAZED / DEEP), plus BIND and REMOVE wall actions. CRAFT hosts the CraftMenu node. PROCESS shows processor and extractor machine rows with live progress and fuel bars. VILLAGE homes storage structures, gatherer buildings, the read-only Elder-building cards, and the "Your Digger" cosmetics customizer. An always-visible 🧰 pull-tab reopens the drawer (to CRAFT, which does not move the camera) when it is closed.
Positioning and polling. Hotkeys B / C / P / V toggle tabs via _unhandled_input; Esc closes. _reposition() pins the drawer's right edge to the screen and its top just below the threat panel by reading bottom_y() off the maw_bar group node (_maw_bottom() — works for MawBar and MawMedallion alike), so the two never overlap; a talisman-skin branch adjusts the pin for the carved bar. _process() re-pins when the canvas transform changes (camera move) or the threat panel height changes, and drives live machine progress/fuel bars while PROCESS or VILLAGE is open. Because a tab's content can be wider than DRAWER_W, _on_panel_resized re-pins the right edge whenever the PanelContainer grows.
CraftMenu category rule. CraftMenu renders only the categories in CATEGORY_ORDER:
const CATEGORY_ORDER: Array = ["tools", "structures", "machines", "components"]
A recipe whose category is anything other than these four is silently invisible in the menu. Tier families (pickaxe / axe / drill / excavator) fold older tiers behind an "older tiers" toggle, showing the best unlocked tier forward.
Key constants.
| Constant | Value | Role |
|---|---|---|
DRAWER_W |
280.0 (BuildMenu) |
drawer width |
BOTTOM_BAR_H |
80.0 (BuildMenu) |
reserved bottom band for drawer height |
WALL_TIERS |
4 tiers | wall material grouping in the BUILD tab |
MACHINE_MAX_OWNED |
GameConstants.MACHINE_DEFAULT_MAX_PLACED |
placement cap fallback (CraftMenu) |
BuildController reads BuildMenu.selected_material directly; card selection emits material_selected.
HelpOverlay, SettingsPanel, and the tutorial
scenes/ui/HelpOverlay.gd— the full-screen how-to-play panel now describes the talisman skin as primary: a "YOUR INSTRUMENTS" section covers the medallion / crest / charm / rail / ledger,[TAB]and[I]are listed as standard keys, and classic locations are noted inline as "(legacy UI: …)".scenes/ui/SettingsPanel.gd— audio, low-perf toggle (web defaults ON first run), the Legacy UI skin toggle, offline-speed slider (donate-gated), save export. Ownsuser://settings.jsonand persistshud_style/ledger_holdon every save (HudShell's one-time fresh-profile merge-write is the sole other writer — see The Talismans HUD).scenes/ui/TutorialOverlay.gd/scripts/core/TutorialNudges.gd— skin-aware since the Phase 4 port: copy resolves{ui_*}placeholders at display time, anchors resolve through_provider_pos()against the active skin's provider, and a talisman-only spine beat teaches the TAB ledger (auto-skipped on classic).
Key files
scenes/ui/HUD.gd— parent CanvasLayer; classic sidebar / talisman journal drawer, skin flip handling,get_bottom_bar_y().scripts/ui/HudShell.gd— skin state (hud_style), defaults,ledger_hold.scenes/ui/MawBar.gd— classic threat panel; breach/idle ETA, Hunger, wall-column queue.scenes/ui/VillageBar.gd— classic village strip; metrics, task button, call-Wren bell, ticker.scenes/ui/BottomBar.gd— classic food/water bars, supply ETA, run stats, songbook, help/settings.scenes/ui/DepthRuler.gd— right-edge depth ruler; layer bands, caret, record marker (both skins).scenes/ui/ToastQueue.gd—EventBus.show_toastqueue, cap 3 (both skins).scenes/ui/BuildMenu.gd/scenes/ui/CraftMenu.gd— four-tab build/craft/process/village drawer (both skins).