# 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](/docs/underroot/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:

```gdscript
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 flips `played_before` true) 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 — classic `MawBar` or talisman `MawMedallion`.
- `_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 `CanvasLayer`s built procedurally, signal-driven plus a ~1 s poll, styled through the `UITheme` tokens (`T_CARVED` for face numerals — see [UI Construction Conventions](/docs/underroot/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_bar` group and exposes `bottom_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()` emits **`EventBus.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`), and `The Line >` (`EventBus.lineage_requested`).
- Machine rows aggregate by name+status and are sorted by urgency. The three `no_fuel` landmarks state their effect instead of a fuel figure — Mead Hall `+45 support`, Trading Post `+N🪙/day` (scales with population), Bulwark `Guarding`. 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 — preloading `HUD.gd` here 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 398m` appears solely on the generation that extended the record.
- Days held, cause of death, their calling, and `└ succeeded by their daughter` drawn as a real connector down the spine. The relation comes from `GameManager.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](/docs/underroot/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_bar` group duck-typing — both MawBar and MawMedallion join the group and expose `bottom_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 against `HudShell.style()` **at display time** (applied inside `present()`), so a mid-lineage skin flip reads correctly. Both `TutorialOverlay.gd` and `TutorialNudges.gd` copy use them.
- **Provider anchors.** `TutorialOverlay._provider_pos(method)` queries whichever node answers village questions in the active skin — the HearthCrest (talisman, via `HUD._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 on `EventBus.ledger_opened`; on classic the trigger auto-skips the beat entirely. Completing it marks the `ledger_tab` nudge 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"` when `HudShell.ledger_hold()` is true, `"Press TAB"` otherwise (the current default). The rail's tab tooltip says `Full 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 for `has_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` |

## Related

- [HUD and Bars Reference](/docs/underroot/hud-and-bars-reference)
- [UI Construction Conventions](/docs/underroot/ui-construction-conventions)
- [EventBus and the Signal Convention](/docs/underroot/eventbus-and-the-signal-convention)
- [GameManager and the Run Lifecycle](/docs/underroot/gamemanager-and-the-run-lifecycle)
- [Save System and Migration](/docs/underroot/save-system-and-migration)
- [The Maw](/docs/underroot/the-maw)