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:
- Fresh profile (
SaveManager.has_played_before()false, no stored key) →"talismans", and the choice is persisted immediately via_persist_style(). Without that write, finishing the tutorial (which flipsplayed_beforetrue) would silently drop the player back to classic on the next boot. - Pre-skin profile (played before, no stored key) →
"classic"until they opt in.
_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():
_spawn_threat_panel()instantiates the active skin's threat panel — classicMawBaror talismanMawMedallion._spawn_bottom_chrome()frees the previous set, then: talisman — hides the scene-declared VillageBar (_set_village_bar_active(false); hidden and processing-disabled, never freed) and adds HearthCrest, DiggerCharm, SpeechBubbleLayer, LedgerOverlay, SkillTreeOverlay, LineageOverlay, MaterialRail as HUD children; classic — re-enables VillageBar in place and builds a fresh BottomBar._apply_sidebar_skin()reshapes the left panel (see Journal drawer below).
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:
- Joins the
maw_bargroup and exposesbottom_y(), exactly like MawBar — the tutorial ring and the BuildMenu clamp find whichever panel is live through the group. - Same fold key as MawBar (
_FOLD_KEY = "maw_bar.expanded"), so the expanded state survives skin flips. - Surface-compact mode: above ground the top-right corner is where the Maw actually is, so the bar sheds its carved bulk near the surface and regains it underground, with a hysteresis band so panning never flaps it.
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.
- Two TAB modes via
HudShell.ledger_hold(): press-to-toggle (default — outside-click on the dim closes) or hold-to-peek. Neither blocks world input; WASD stays alive. A mode chip inside the overlay switches between them. _show()emitsEventBus.ledger_opened— the tutorial's ledger beat listens for it.- Three sections open a fuller view through a shared
_open_btn()helper:View Skill Mastery >(EventBus.skill_tree_requested),Read the Chronicle >(EventBus.black_hollow_chronicle_requested), andThe Line >(EventBus.lineage_requested). - Machine rows aggregate by name+status and are sorted by urgency. The three
no_fuellandmarks state their effect instead of a fuel figure — Mead Hall+45 support, Trading Post+N🪙/day(scales with population), BulwarkGuarding. A machine that is genuinely running rounds its fuel up, so nothing active ever reads~0s fuel. - Live refresh while open updates values in place; the machines block structurally rebuilds only when grouping changes, never under the cursor, and re-runs the viewport clamp afterwards because the column just changed height.
- Reads the HUD's category tables via
get_parent()at runtime — preloadingHUD.gdhere would be a cyclic preload, since HUD preloads this script.
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:
- A work bar, tiles dug, scaled against the hardest-working generation in the line.
- Ritual diamonds, one per ritual that generation fired (8 drawn, then a count).
- A depth note only where it was earned —
pushed to 398mappears solely on the generation that extended the record. - Days held, cause of death, their calling, and
└ succeeded by their daughterdrawn as a real connector down the spine. The relation comes fromGameManager.RELATION_POOL, which is not only family (neighbor, best friend, hunting partner, old rival) — the line is a chain of succession, not a family tree.
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 grave — GameConstants.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:
- Bottom chrome:
HUD.get_bottom_bar_y()is skin-aware — classic returns the bottom bar's top; talisman returns the higher of the crest/charm tops, so the DepthRuler and other consumers clear whichever is taller. - Threat panel bottom: the
maw_bargroup duck-typing — both MawBar and MawMedallion join the group and exposebottom_y(); BuildMenu's_maw_bottom()resolves through it, with a talisman branch for drawer pinning.
Tutorial and help integration
The tutorial is fully skin-aware (Phase 4 port) — it no longer forces or assumes classic:
{ui_*}placeholders. Spine and nudge copy uses terms like{ui_bell},{ui_threat},{ui_tasks},{ui_wallqueue},{ui_digfront},{ui_journal_open}.TutorialOverlay._fill_ui_terms()resolves them againstHudShell.style()at display time (applied insidepresent()), so a mid-lineage skin flip reads correctly. BothTutorialOverlay.gdandTutorialNudges.gdcopy use them.- Provider anchors.
TutorialOverlay._provider_pos(method)queries whichever node answers village questions in the active skin — the HearthCrest (talisman, viaHUD._crest) or the VillageBar (classic). Both expose the same method names (task_button_screen_pos(),get_population_label_screen_pos(),metric_screen_pos(id)). - The ledger beat. A talisman-only spine beat teaches TAB and the rail's
⋯. Its trigger"ledger_open"arms onEventBus.ledger_opened; on classic the trigger auto-skips the beat entirely. Completing it marks theledger_tabnudge so the HUD's toast never repeats the lesson. - Mode-aware key copy. The spine line and both one-time toasts read the live mode rather than hardcoding a verb —
"Hold TAB"whenHudShell.ledger_hold()is true,"Press TAB"otherwise (the current default). The rail's tab tooltip saysFull ledger: [TAB]. - The skip path. A new player who skips the tutorial gets the same keys the short way: the skip handler fires the ledger/journal toast (talisman skin, tips enabled, nudge unseen).
- HUD's own one-time toast (
_apply_sidebar_skin()) fires only forhas_played_before()profiles — brand-new players learn TAB from the spine or the skip path; firing it during the intro would be lost.
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 |