The Talismans HUD

The talisman skin is Underroot's second — and now default — HUD: art-framed instruments in the four corners of the screen plus an on-demand TAB ledger, replacing the always-on left sidebar and the classic bar stack. Both skins ship; HudShell decides which one is live, and everything flips at runtime. The classic skin and the chrome shared by both are documented in HUD and Bars Reference.

Design record: docs/superpowers/specs/2026-07-27-talismans-hud-design.md in the repo (plus the phase plans beside it). Note two places where the shipped code deliberately diverges from that spec: the geometry broker functions (HudShell.bottom_reserved_y() / threat_bottom_y()) were never built — see Geometry below — and the medallion shipped as v2, a carved bar rather than a disc.

HudShell: who owns the skin

scripts/ui/HudShell.gd (class_name HudShell extends RefCounted) is a static holder, not an autoload — same pattern as GameConstants. It owns two settings-backed values:

HudShell.style()        # "classic" | "talismans"
HudShell.set_style(s)   # validates, stores, emits EventBus.hud_style_changed
HudShell.ledger_hold()  # true = hold-TAB-to-peek, false = TAB toggles (default)
HudShell.set_ledger_hold(hold)

Both persist in user://settings.json (hud_style, ledger_hold). HudShell reads the file directly because HUD._ready() builds the threat panel before SettingsPanel._load_settings() has run; SettingsPanel remains the file's single writer — with one exception, below.

Defaults (the Phase 4 flip)

_read_style() resolves a missing hud_style key by profile age:

_persist_style() is a merge-write — it re-reads the file, sets only hud_style, and writes the merged dictionary back, so any keys SettingsPanel already stored survive. This is the one exception to SettingsPanel being the file's single writer.

Defaults: ledger_hold flipped to Toggle

ledger_hold now defaults to false — TAB toggles the ledger open and closed. It shipped as hold-to-peek, which was right when the ledger was a read-only glance. It no longer is: it scrolls, and it is full of things to click (material travel, Dig Front, Skill Mastery, the Chronicle, The Line, machine rows). Holding a key through all of that is the wrong grammar.

The flip is safe for existing players because SettingsPanel.save_settings() writes ledger_hold on every save — anyone who has ever saved a setting has an explicit stored value and keeps it. Only profiles that never chose see the new default. The mode chip inside the ledger switches back either way.

The Legacy UI toggle

scenes/ui/SettingsPanel.gd renders a "Legacy UI" row in the GAMEPLAY section (ON = classic HUD). Visibility rule: SaveManager.has_played_before() or HudShell.style() == STYLE_CLASSIC — hidden during a first playthrough so the tutorial runs on one skin, but always shown while classic is active so a profile whose config lost played_before is never stuck on the old HUD with no way forward.

The flip flow

set_style() emits EventBus.hud_style_changed(style). HUD.gd handles it by calling _spawn_threat_panel() and _spawn_bottom_chrome():

Safe mid-run: every panel self-heals from autoload state on its poll tick (the build-once run-state race rule), so a flip never needs a reload.

The six components

Component File Corner Replaces
MawMedallion scenes/ui/MawMedallion.gd top right MawBar
HearthCrest scenes/ui/HearthCrest.gd bottom left VillageBar + BottomBar's supply half
DiggerCharm scenes/ui/DiggerCharm.gd bottom right BottomBar's stats/buttons half + sidebar TOOLS
MaterialRail scenes/ui/MaterialRail.gd left edge the collapsed sidebar's swatch rail
LedgerOverlay scenes/ui/LedgerOverlay.gd TAB overlay the sidebar ITEMS tab
SpeechBubbleLayer scenes/ui/SpeechBubbleLayer.gd world/anchored VillageBar's ticker

All are CanvasLayers built procedurally, signal-driven plus a ~1 s poll, styled through the UITheme tokens (T_CARVED for face numerals — see UI Construction Conventions).

MawMedallion (the carved bar)

v2 after the v1 disc failed playtest (occluded the Maw region, hover-gated the queue). It keeps MawBar's proven instrument shape: compact top-right panel with the breach countdown at T_CARVED (BREACH / WAKES IN / OPEN GROUND word label), the wall line as a clickable toggle, a persistent chevron dropdown of the column queue (MAX_QUEUE_ROWS = 4, QueueBar rows via UITheme.timer_color), hunger/idle/surge lines, and the surge-song switcher. Details that matter:

HearthCrest

Bottom-left, one coherent block (v2 — compacted after playtest): population column, the three support metrics (food / shelter / safety) with level bars and formula tooltips, and the two stock rows (food, water) with percentage bars, inline supply ETAs, and store numbers (Silo 1204, Well 43, Tower 605 — the 6px micro-bars were retired; caps live in the ledger). Idle-window urgency uses BottomBar's exact thresholds (IDLE_AMBER_S 16 h, IDLE_RED_S 8 h). Carries the tasks button and the 🔔 call-Wren button. Layout contracts: docs/superpowers/specs/2026-07-27-bottom-chrome-contracts.md.

The Two-Foods rule from the contracts doc: 🍗 577/340 is village support capacity; the Food bar is the digger's stomach. Different facts, never conflated.

DiggerCharm

Bottom-right plaque: Day and gold at T_TITLE, best/tiles as captions, the /🪓 tool buttons with durability percentages (tooltip names the tool), a 🔧 flux repair/temper button visible while Wavellite Flux is held, the Harrow and challenge chips, the songbook (the SONGBOOK array moved here from BottomBar), and ? / buttons. Help/settings are member buttons firing injected callables (help_action / settings_action, set by HUD at build time) — fixing the classic bar's local-variable button oddity.

MaterialRail

Left-edge compact material strip (playtest ask: a peek overlay doesn't serve quick repeated layer jumps). Folded behind a slim pull-tab by default (RAIL_W = 34, fold persists per save via UIFolds key rail.open); hidden while the journal drawer is open (they share the left edge). Rows are swatch+count per seen material with category dividers, tooltips name the material (and add "Click to travel to its layer." while it is jumpable), click travels to its layer, and the at the top opens the full ledger. Shares the HUD's INV_TERRAIN / INV_EXOTIC / INV_REFINED / INV_GLAZED category tables.

LedgerOverlay

The TAB bookkeeping overlay (layer = 6): all material categories with counts and click-to-jump, a refined/glazed detail card (recipe with have/need marks, machine build cost), tools with durability, machines with statuses and placement hints, the run-stats block, skills, Black Hollow, and the line of diggers — the sidebar's content promoted to an on-demand moment at readable sizes.

Column layout — stock → stock → stock → state. Each stable section pairs with a material column by theme; only the last column's height moves:

Column Contents
1 — the dig TERRAIN · Dig Front · THE RUN (day, best depth, tiles dug, gold) · LINEAGE
2 — the strange EXOTIC · GLAZED · BLACK HOLLOW
3 — the workshop REFINED · TOOLS
4 — your works SKILLS · VILLAGE BUILDINGS · MACHINES

The first three columns are fixed by the material tables and can never change height, so column 4 is the only one that can drive the scroll. Every column's first header shares one eye-line — that alignment is what makes four columns read as a single table, and it is why TOOLS sits under REFINED rather than above it.

Fitting the viewport. The panel is clamped to the live viewport rect on both axes and the column grid scrolls inside a ScrollContainer; the header and TAB-mode chip stay pinned. _reposition() measures the panel around the grid at its natural size to learn the chrome (borders, margins, header), then hands the grid whatever the screen actually leaves. PANEL_MAX_W = 1200 is a genuine width cap (it was previously used only in the centring maths, so a wide panel hung off the right edge). A vertical scrollbar's width is paid for up front so it cannot induce a spurious horizontal one, and the header hint gains · scroll for more when clipped, since the skin's scrollbars are deliberately quiet. The scrollbar rides the panel border (content_margin_right = 8) while the content keeps a GUTTER_W = 14 gutter of its own.

LineageOverlay — "The Line"

scenes/ui/LineageOverlay.gd (layer = 12, HUD-owned like SkillTreeOverlay, opened from the ledger's LINEAGE section via EventBus.lineage_requested). It was a tab in HelpOverlay, which framed the line of diggers as reference material; it is the opposite — the most run-specific thing a player owns. The help menu is back to six how-to-play tabs.

The axis is work, not depth. Depth is capped and the first generation usually banks it, so a depth chart would be a flat line. What separates one digger from the next is ground moved and rituals fired — the labour and the damage. Each generation draws as:

Portraits are DiggerTile figures drawn through the shared DiggerRenderer: the fallen modulated down to stone on a slab, the living digger in full colour inside a breathing glow. Sourcing is covered in GameManager and the Run Lifecycle.

Clicking a fallen digger travels to their graveGameConstants.grave_slot_x(gen) already maps a generation to a real headstone on the graveyard hill, so the screen is a place rather than a table.

Markers are drawn vector shapes, not glyphs: the game font carries only a hand-subset emoji fallback, so a headstone dingbat would tofu on Web (the same lesson as SkillTreeOverlay's node markers).

SpeechBubbleLayer

Replaces the ticker. One queue (QUEUE_CAP = 3, DWELL = 7.0 s), two presentations: village on-screen → a parchment bubble anchored at the speaker's building (Maren→farm, Bjorn→longhouse, Siv→watchtower, healer/generic→bell tower, Wren as the default voice); village off-screen (player deeper than CARD_DEPTH_TILES = 11) → a compact voice card below the threat panel. Narrative events (population_narrative) ride both; the ambient project-flavour rotation (FLAVOUR_EVERY = 75 s) stays village-anchored only. The donate rotation moved here from the crest (verbatim VillageBar timing: DONATE_INTERVAL = 1800 s).

LayerNav: shared click-to-travel

scripts/ui/LayerNav.gd (class_name LayerNav extends RefCounted, static — also not an autoload) is one copy of the sidebar's jump logic serving both the rail and the ledger: jumpable(mat_id), layer_for_material, jump_dig_front() (returns to current_run.last_dig_x/y, falling back to the deepest open tile). Astrolabe-seeded exotics resolve their depth ranges from AstrolabeManager and stay discovery-gated (an unseen exotic gives away nothing, matching the DepthRuler's diamonds). The classic sidebar keeps its own copy untouched.

The journal drawer and [I]

In the talisman skin the left sidebar becomes a hidden-by-default journal drawer (_apply_sidebar_skin()): the tab bar is hidden, the ITEMS tab never shows (materials live on TAB), and the drawer opens to the journal via the [I] hotkey (HUD._unhandled_input — I and Z were the only free letters), the crest's tasks button, or EventBus.open_journal_tab. Task cards render in the drawer; fulfil/deny/collect logic is untouched.

Geometry between panels

The spec's HudShell.bottom_reserved_y() / threat_bottom_y() broker functions were not implemented. What ships:

Tutorial and help integration

The tutorial is fully skin-aware (Phase 4 port) — it no longer forces or assumes classic:

scenes/ui/HelpOverlay.gd now describes the talisman skin as primary: a "YOUR INSTRUMENTS" section covers the four corners plus rail and ledger, [TAB] / [I] are listed as standard keys, and legacy locations are noted inline as "(legacy UI: …)" plus a dedicated "Legacy UI" row. Its tab set is six — CONTROLS, WORLD, MACHINES, MATS, SURVIVAL, VILLAGE. The former LINEAGE tab moved to the ledger (see LineageOverlay above).

Persistence recap

Key (user://settings.json) Values Default
hud_style "classic" / "talismans" talismans for fresh profiles (persisted immediately by HudShell's merge-write); classic for profiles that predate the skin
ledger_hold bool false — TAB toggles. SettingsPanel writes this key on every save, so anyone who has ever saved a setting keeps their explicit choice

The medallion's fold shares the classic maw_bar.expanded UIFolds key; the rail adds rail.open. No save-format change from the HUD work itself.

Key files

File Role
scripts/ui/HudShell.gd Skin state, defaults, hud_style_changed, ledger_hold, the one merge-write
scenes/ui/MawMedallion.gd Talisman threat instrument (carved bar)
scenes/ui/HearthCrest.gd Bottom-left village/supply instrument
scenes/ui/DiggerCharm.gd Bottom-right digger plaque
scenes/ui/MaterialRail.gd Left-edge jump rail
scenes/ui/LedgerOverlay.gd TAB ledger; emits ledger_opened; hosts the three > buttons
scenes/ui/SkillTreeOverlay.gd Skill Mastery tree (lineage_requested's sibling, skill_tree_requested)
scenes/ui/LineageOverlay.gd The Line — generations, work bars, ritual diamonds, portraits
scenes/ui/SpeechBubbleLayer.gd Ticker replacement (bubbles + voice cards)
scripts/ui/LayerNav.gd Shared click-to-travel for rail + ledger
scenes/ui/HUD.gd Skin flip handling, journal drawer, [I], get_bottom_bar_y()
scenes/ui/SettingsPanel.gd Legacy UI toggle; persists hud_style / ledger_hold