# 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](/docs/underroot/the-talismans-hud). For the conventions all surfaces share (procedural build, CanvasLayer sizing, tooltips, UITheme tokens), see [UI Construction Conventions](/docs/underroot/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:

```gdscript
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`:

```gdscript
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. Owns `user://settings.json` and persists `hud_style` / `ledger_hold` on every save (HudShell's one-time fresh-profile merge-write is the sole other writer — see [The Talismans HUD](/docs/underroot/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_toast` queue, cap 3 (both skins).
- `scenes/ui/BuildMenu.gd` / `scenes/ui/CraftMenu.gd` — four-tab build/craft/process/village drawer (both skins).

## Related

- [The Talismans HUD](/docs/underroot/the-talismans-hud)
- [UI Construction Conventions](/docs/underroot/ui-construction-conventions)
- [EventBus and the Signal Convention](/docs/underroot/eventbus-and-the-signal-convention)
- [The Maw](/docs/underroot/the-maw)
- [Survival and Village](/docs/underroot/survival-and-village)
- [Adding Materials Tools and Recipes](/docs/underroot/adding-materials-tools-and-recipes)