Files
Project-M/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs
T
kronic 62e48a3b0b LANTERN purge: delete the superseded base/expedition shell (audit H1/H3/M5)
The 2026-08-06 audit found the shipping scene was still the abandoned
co-op-Hades game with LANTERN combat bolted on, and that a third of the
codebase was live code for a direction abandoned on 2026-07-13. Operator
chose deletion over freezing: "everything is saved in source control if
needed. I want the project to be clean."

DELETED (~140 source files, Scripts 335->231, Tests 77->43):
- Enemy variants + boss (H3). ChargerAuthoring / SpitterAuthoring /
  SwarmerAuthoring were attached to ZERO prefabs, so LungeState /
  SpitterState / SwarmerTag were never baked: ~272 lines of Bursted AI
  passes, BossAISystem (261 lines) and the whole MixBands escalation
  curve could not match a single chunk at runtime, while 734 lines of
  green tests certified them. Both shipping enemy prefabs were already
  byte-identical in stats.
- Run/room lifecycle: RunDirector FSM, RunInfo/RunMap/RoomPlan/RoomTag,
  route select, portal interact, ready-check, room field/teardown.
- Meta shop, prep loadout, boons (incl. KillRewardSystem and
  DashTrailDamageSystem, which existed only to serve boon flags).
- Build palette + structures, shared storage, inventory/equipment
  (already recorded PAUSED in CLAUDE.md).
- The HUD panels driving all of the above (HudSystem 1168 -> 610).

KEPT deliberately: BaseGridMath + BaseAnchor (8 systems use PlotCenter
for spawn rings, respawn and dynamic light), the resource ledger +
StorageMath, the save system, region/relevancy. Three of these were in
the delete set until I checked their consumers — worth remembering that
the file-level manifest was wrong about them.

Also folds in audit finding M5: PlayerClass was a second, server-only
copy of the byte FrameId already replicates. It existed for the meta
shop; with that gone, FrameId is the single frame identity.

Harvest is now single-sink (ledger). HarvestMath keeps its shape so
LANTERN's carried-vs-banked cargo split lands in one place, not two.

295/295 EditMode green, zero compile errors. Subscene re-bake and Play
validation follow in the next commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 12:59:39 -07:00

581 lines
26 KiB
C#

using System.Collections.Generic;
using ProjectM.Simulation;
using Unity.Entities;
using Unity.NetCode;
using Unity.Transforms; // A6: boss-bar query reads LocalTransform (source-gen needs the using in this file)
using Unity.Mathematics; // DR-046: portal proximity math (float3/.xz/math.distance)
using UnityEngine;
using UnityEngine.UIElements;
namespace ProjectM.Client
{
/// <summary>
/// Client-only screen HUD on UI Toolkit, skinned with the curated Synty sci-fi-soldier kit
/// (<see cref="HudTheme"/>) over <see cref="MenuUi"/>'s Aether palette so it reads in one visual language with
/// the menu / pause / settings. A managed presentation <see cref="SystemBase"/> (<see cref="PresentationSystemGroup"/>)
/// that OBSERVES the local player ghost + the global cycle / ledger / goal each frame and pushes values into a
/// runtime UIDocument (shared PanelSettings, sortingOrder 50 so it sits behind the pause overlay's 100). The
/// root is <c>pickingMode = Ignore</c> so the HUD never eats world clicks — only the build-palette slots pick.
/// Layout (good-HUD spatial convention): persistent self-state hugs the corners (health bottom-left, resources
/// top-left, threat top-right, build deck bottom-center); transient mission state (phase / countdown / wave /
/// goal) lives center-top; a low-health vignette + hurt-flash + a scheme-aware build-mode control-hint bar give
/// just-in-time feedback. EVERY skinned element is null-safe: with no <see cref="HudTheme"/> it falls back to
/// the flat-colour HUD. Presentation only (client world, no simulation, no rollback double-fire).
/// </summary>
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
[UpdateInGroup(typeof(PresentationSystemGroup))]
public partial class HudSystem : SystemBase
{
// ---- palette (Aether language; Synty white skins are tinted into these) ----
static readonly Color AetherCyan = new(0.30f, 0.85f, 1f);
static readonly Color OreAmber = new(1f, 0.72f, 0.35f);
static readonly Color BioGreen = new(0.55f, 0.85f, 0.45f);
static readonly Color PanelDark = new(0.08f, 0.11f, 0.15f, 0.90f);
static readonly Color PanelWarm = new(0.16f, 0.09f, 0.09f, 0.88f);
static readonly Color PipDim = new(0.25f, 0.30f, 0.36f, 0.9f);
static readonly Color BlightRed = new(0.85f, 0.10f, 0.08f);
static readonly Color ThreatWarm = new(1f, 0.62f, 0.4f);
static readonly Color SlotIdleBg = new(0.09f, 0.11f, 0.15f, 0.92f);
static readonly Color SlotSelBg = new(0.16f, 0.26f, 0.32f, 0.95f);
static readonly Color SlotIdleBorder = new(1f, 1f, 1f, 0.08f);
const float ExpeditionRegionXMin = RegionMath.RegionBoundaryX; // camera x past this = the +1000 expedition region (DR-013)
GameObject _hudGo;
UIDocument _doc;
bool _built;
bool _themed; // HudTheme + PanelBox present (drives sprite-tint vs flat-colour retint)
// vitals
VisualElement _healthFill, _shieldRow;
Label _healthText;
// threat
VisualElement _threatPanel, _threatIcon;
Label _threatNum;
// macro: banner + location line
VisualElement _banner;
Label _phaseText, _locationText;
// Demo polish: the clickable READY panel (Staging/Launching).
VisualElement _readyPanel, _readyPipRow;
Button _readyBtn;
Label _readyTitle;
bool _readyPanelBuilt;
int _readyShownFor; // (ready, total, localReady, secs, launching) rebuild signature
// Demo polish round 2: boss presence bar, run-depth dots, outcome flash.
VisualElement _bossPanel, _bossFill;
Label _bossText;
bool _bossBarBuilt;
VisualElement _depthPanel;
int _depthShownFor;
bool _depthBuilt;
// resources
Label _aetherNum, _oreNum, _bioNum;
// build palette + hints
VisualElement _paletteRow, _hintBar, _buildDiscoveryChip;
bool _paletteBuilt, _hintBuilt;
byte _hintScheme = 255;
readonly Dictionary<byte, PaletteItem> _palette = new();
// overlays
VisualElement _vignette, _downed;
Label _downedText; // "RESPAWNING IN N" countdown (client-local: baked DelayTicks + death-edge latch)
float _downedSince = -1f;
float _prevHp, _flash;
bool _haveHp;
// personal inventory panel (read-only; toggled with I)
VisualElement _invPanel, _invList, _equipList;
bool _invOpen;
EntityQuery _huskQuery;
struct PaletteItem { public VisualElement Root; public Label Cost; public int CostAmount; public byte CostRes; public VisualElement Glow; public VisualElement Icon; }
protected override void OnCreate()
{
_huskQuery = GetEntityQuery(ComponentType.ReadOnly<EnemyTag>());
}
protected override void OnStartRunning()
{
if (_hudGo != null) return;
MenuUi.EnsureEventSystem();
_hudGo = new GameObject("~HUD");
_doc = _hudGo.AddComponent<UIDocument>();
_doc.panelSettings = MenuUi.LoadPanelSettings();
_doc.sortingOrder = 50; // behind the pause overlay (100)
}
protected override void OnDestroy()
{
if (_hudGo != null) Object.Destroy(_hudGo);
}
protected override void OnUpdate()
{
if (_doc == null) return;
if (!_built)
{
var r = _doc.rootVisualElement;
if (r == null) return; // panel not initialised yet (next frame)
BuildTree(r);
_built = true;
}
// Job-safety insurance (matches the sibling presentation systems + CLAUDE.md): finish any jobs writing
// the components we read on the main thread before reading them. No job writes these today, but this
// stays correct the day a Health/stats writer is parallelised.
EntityManager.CompleteDependencyBeforeRO<Health>();
EntityManager.CompleteDependencyBeforeRO<EffectiveCharacterStats>();
EntityManager.CompleteDependencyBeforeRO<SocketCooldown>();
EntityManager.CompleteDependencyBeforeRO<EffectiveSocketStats>();
EntityManager.CompleteDependencyBeforeRO<RespawnInvuln>();
float dt = SystemAPI.Time.DeltaTime; // wall-frame delta — correct in a presentation system
bool haveTick = SystemAPI.TryGetSingleton<NetworkTime>(out var nt);
int huskCount = _huskQuery.CalculateEntityCount();
// 2026-08-07 audit purge: the macro run banner, the location sub-line, the ready-check panel, the
// boss bar and the run-depth dots were all driven by RunInfo / ExpeditionObjective / PlayerReady —
// the superseded base/expedition FSM. All deleted; the HUD is now vitals + threat + resources + the
// ability bar (its own system).
// ---- Resources (feed palette affordability) ----
int aether = 0, ore = 0, bio = 0;
if (SystemAPI.TryGetSingletonEntity<ResourceLedger>(out var ledgerE))
{
var buf = SystemAPI.GetBuffer<StorageEntry>(ledgerE);
for (int i = 0; i < buf.Length; i++)
{
var en = buf[i];
if (en.ItemId == ResourceId.Aether) aether = en.Count;
else if (en.ItemId == ResourceId.Ore) ore = en.Count;
else if (en.ItemId == ResourceId.Biomass) bio = en.Count;
}
}
_aetherNum.text = aether.ToString();
_oreNum.text = ore.ToString();
_bioNum.text = bio.ToString();
// ---- Threat readout (top-right) — hidden entirely with zero husks; its reappearance is the cue ----
bool showThreat = huskCount > 0;
_threatPanel.style.display = showThreat ? DisplayStyle.Flex : DisplayStyle.None;
if (showThreat)
{
float intensity = Mathf.Clamp01(huskCount / 30f);
Color tc = Color.Lerp(ThreatWarm, BlightRed, intensity);
_threatNum.text = huskCount.ToString();
_threatNum.style.color = tc;
_threatIcon.style.unityBackgroundImageTintColor = tc;
RetintPanel(_threatPanel, PanelDark);
}
// ---- Per-player vitals ----
bool found = false;
float hp = 0f, maxHp = 1f;
bool dead = false, shielded = false;
foreach (var (health, effChar, cd, invuln, entity) in
SystemAPI.Query<RefRO<Health>, RefRO<EffectiveCharacterStats>,
RefRO<SocketCooldown>, RefRO<RespawnInvuln>>()
.WithAll<GhostOwnerIsLocal, PlayerTag>().WithEntityAccess())
{
found = true;
hp = health.ValueRO.Current;
maxHp = effChar.ValueRO.MaxHealth > 0f ? effChar.ValueRO.MaxHealth : health.ValueRO.Max;
dead = SystemAPI.IsComponentEnabled<Dead>(entity);
// (07-21 UI rework: socket/dash cooldown readouts moved to AbilityBarSystem.)
uint invulnUntil = invuln.ValueRO.UntilTick;
var invulnTick = new NetworkTick(invulnUntil);
shielded = haveTick && invulnUntil != 0 && invulnTick.IsValid && invulnTick.IsNewerThan(nt.ServerTick);
break;
}
_doc.rootVisualElement.style.display = found ? DisplayStyle.Flex : DisplayStyle.None;
// ---- Low-health vignette + hurt flash (full-screen) ----
_flash = HudVisualMath.DecayFlash(_flash, dt);
if (found)
{
float frac = maxHp > 0f ? Mathf.Clamp01(hp / maxHp) : 0f;
if (_haveHp && hp < _prevHp - 1f) _flash = HudVisualMath.HurtFlashKick;
_prevHp = hp; _haveHp = true;
float vigOp = dead ? 0f : HudVisualMath.CombinedOpacity(frac, _flash);
_vignette.style.opacity = vigOp;
_vignette.style.display = vigOp > 0.001f ? DisplayStyle.Flex : DisplayStyle.None;
HudUi.SetFill(_healthFill, frac);
_healthFill.style.backgroundColor = shielded
? new Color(0.45f, 0.85f, 1f)
: Color.Lerp(new Color(0.92f, 0.16f, 0.16f), new Color(0.25f, 0.9f, 0.5f), frac);
_healthText.text = Mathf.CeilToInt(Mathf.Max(0f, hp)) + " / " + Mathf.CeilToInt(maxHp);
_shieldRow.style.display = shielded ? DisplayStyle.Flex : DisplayStyle.None;
if (dead)
{
// Client-local countdown: latch the death edge; the baked (non-replicated) DelayTicks is the
// honest duration — RespawnTick itself is server-only.
if (_downedSince < 0f) _downedSince = (float)SystemAPI.Time.ElapsedTime;
int delayTicks = 180;
foreach (var rs in SystemAPI.Query<RefRO<RespawnState>>().WithAll<PlayerTag, GhostOwnerIsLocal>())
{ delayTicks = Mathf.Max(1, rs.ValueRO.DelayTicks); break; }
float left = delayTicks / 60f - ((float)SystemAPI.Time.ElapsedTime - _downedSince);
_downedText.text = left > 0.05f ? "RESPAWNING IN " + Mathf.CeilToInt(left) : "RESPAWNING...";
}
else _downedSince = -1f;
_downed.style.display = dead ? DisplayStyle.Flex : DisplayStyle.None;
}
else
{
_haveHp = false; _flash = 0f;
_vignette.style.display = DisplayStyle.None;
_downed.style.display = DisplayStyle.None;
}
}
// ---- per-frame helpers ----
void RetintPanel(VisualElement p, Color c)
{
if (_themed) p.style.unityBackgroundImageTintColor = c;
else p.style.backgroundColor = c;
}
// 2026-08-07 audit purge: AddPaletteItem built one build-palette slot (icon, cost row, selection glow,
// click-to-select). The build palette went with the structures layer.
void RebuildHints(byte scheme)
{
_hintBar.Clear();
var theme = HudTheme.Get();
bool pad = scheme == InputSchemeId.Gamepad;
AddHint(pad ? theme?.PadPlace : theme?.KbmPlace, pad ? "A" : "LMB", "PLACE");
AddHint(pad ? theme?.PadCancel : theme?.KbmCancel, pad ? "B" : "RMB", "CANCEL");
AddHint(pad ? theme?.PadExit : null, pad ? "MENU" : "ESC", "EXIT");
_hintScheme = scheme;
_hintBuilt = true;
}
VisualElement MakeChip()
{
var chip = new VisualElement();
chip.style.flexDirection = FlexDirection.Row;
chip.style.alignItems = Align.Center;
chip.style.marginLeft = 8; chip.style.marginRight = 8;
chip.pickingMode = PickingMode.Ignore;
return chip;
}
void AddHint(Sprite glyph, string fallback, string action)
{
var chip = MakeChip();
chip.Add(HudUi.Glyph(glyph, fallback, 26));
var lbl = HudUi.Text(action, 12, MenuUi.SubCol, TextAnchor.MiddleLeft);
lbl.style.marginLeft = 5;
chip.Add(lbl);
_hintBar.Add(chip);
}
// ---- UITK construction ----
void BuildTree(VisualElement root)
{
var theme = HudTheme.Get();
_themed = theme != null && theme.PanelBox != null;
root.style.position = Position.Absolute;
root.style.left = 0; root.style.right = 0; root.style.top = 0; root.style.bottom = 0;
root.pickingMode = PickingMode.Ignore; // never eat game-world clicks
BuildVignette(root);
BuildVitals(root);
BuildThreat(root);
BuildMacro(root);
BuildResources(root);
BuildDowned(root);
}
void BuildVignette(VisualElement root)
{
_vignette = new VisualElement();
_vignette.style.position = Position.Absolute;
_vignette.style.left = 0; _vignette.style.right = 0; _vignette.style.top = 0; _vignette.style.bottom = 0;
_vignette.pickingMode = PickingMode.Ignore;
var theme = HudTheme.Get();
if (theme != null && theme.Vignette != null)
{
_vignette.style.backgroundImage = new StyleBackground(Background.FromSprite(theme.Vignette));
_vignette.style.unityBackgroundImageTintColor = BlightRed;
_vignette.style.backgroundSize = new StyleBackgroundSize(new BackgroundSize(BackgroundSizeType.Cover));
}
else
{
_vignette.style.backgroundColor = new Color(BlightRed.r, BlightRed.g, BlightRed.b, 1f);
}
_vignette.style.display = DisplayStyle.None;
root.Add(_vignette);
}
void BuildVitals(VisualElement root)
{
var panel = HudUi.Panel(PanelDark);
panel.style.position = Position.Absolute;
panel.style.left = 40; panel.style.bottom = 40;
panel.style.paddingLeft = 14; panel.style.paddingRight = 14;
panel.style.paddingTop = 12; panel.style.paddingBottom = 12;
panel.style.alignItems = Align.FlexStart;
var theme = HudTheme.Get();
// shield chip (shown only while the respawn shield is active)
_shieldRow = new VisualElement();
_shieldRow.style.flexDirection = FlexDirection.Row;
_shieldRow.style.alignItems = Align.Center;
_shieldRow.style.marginBottom = 6;
_shieldRow.pickingMode = PickingMode.Ignore;
var shieldIcon = HudUi.Icon(theme != null ? theme.ShieldIcon : null, 22, AetherCyan);
shieldIcon.style.marginRight = 6;
_shieldRow.Add(shieldIcon);
_shieldRow.Add(HudUi.Text("SHIELDED", 13, new Color(0.45f, 0.85f, 1f), TextAnchor.MiddleLeft));
_shieldRow.style.display = DisplayStyle.None;
panel.Add(_shieldRow);
// 07-21 UI rework: the single socket-0 charge strip is gone — AbilityBarSystem (bottom-center)
// now shows ALL socket cooldowns + dash.
// health row: health icon + big bar with numeric overlay
var hpRow = new VisualElement();
hpRow.style.flexDirection = FlexDirection.Row;
hpRow.style.alignItems = Align.Center;
hpRow.pickingMode = PickingMode.Ignore;
var hpIcon = HudUi.Icon(theme != null ? theme.HealthIcon : null, 34, new Color(0.95f, 0.4f, 0.4f));
hpIcon.style.marginRight = 8;
hpRow.Add(hpIcon);
var hpBar = HudUi.Bar(420, 40, new Color(0.25f, 0.9f, 0.5f), out _healthFill);
_healthText = HudUi.Display("100 / 100", 24, Color.white, TextAnchor.MiddleCenter);
_healthText.style.position = Position.Absolute;
_healthText.style.left = 0; _healthText.style.right = 0; _healthText.style.top = 0; _healthText.style.bottom = 0;
hpBar.Add(_healthText);
hpRow.Add(hpBar);
panel.Add(hpRow);
root.Add(panel);
}
void BuildThreat(VisualElement root)
{
_threatPanel = HudUi.Panel(PanelDark);
_threatPanel.style.position = Position.Absolute;
_threatPanel.style.right = 40; _threatPanel.style.top = 28;
_threatPanel.style.paddingLeft = 16; _threatPanel.style.paddingRight = 16;
_threatPanel.style.paddingTop = 8; _threatPanel.style.paddingBottom = 8;
_threatPanel.style.alignItems = Align.FlexEnd;
var theme = HudTheme.Get();
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.alignItems = Align.Center;
row.pickingMode = PickingMode.Ignore;
_threatIcon = HudUi.Icon(theme != null ? theme.ThreatIcon : null, 36, ThreatWarm);
_threatIcon.style.marginRight = 8;
row.Add(_threatIcon);
_threatNum = HudUi.Display("0", 34, ThreatWarm, TextAnchor.MiddleRight);
row.Add(_threatNum);
_threatPanel.Add(row);
var caption = HudUi.Text("HUSKS", 13, MenuUi.SubCol, TextAnchor.MiddleRight);
caption.style.letterSpacing = 4;
_threatPanel.Add(caption);
_threatPanel.style.display = DisplayStyle.None;
root.Add(_threatPanel);
}
void BuildMacro(VisualElement root)
{
var macro = HudUi.Group(Align.Center);
macro.style.position = Position.Absolute;
macro.style.top = 16; macro.style.left = 0; macro.style.right = 0;
var theme = HudTheme.Get();
// banner: objective icon + phase line + cycle, phase-coloured underline
_banner = HudUi.Panel(PanelDark);
_banner.style.flexDirection = FlexDirection.Row;
_banner.style.alignItems = Align.Center;
_banner.style.paddingLeft = 22; _banner.style.paddingRight = 22;
_banner.style.paddingTop = 8; _banner.style.paddingBottom = 8;
_banner.style.borderBottomWidth = 2;
_banner.style.borderBottomColor = AetherCyan;
var bIcon = HudUi.Icon(theme != null ? theme.GoalIcon : null, 26, AetherCyan);
bIcon.style.marginRight = 10;
_banner.Add(bIcon);
_phaseText = HudUi.Display("", 30, AetherCyan, TextAnchor.MiddleCenter);
_banner.Add(_phaseText);
macro.Add(_banner);
_locationText = HudUi.Text("", 15, new Color(0.6f, 0.85f, 1f), TextAnchor.MiddleCenter);
_locationText.style.marginTop = 5;
macro.Add(_locationText);
root.Add(macro);
}
void BuildResources(VisualElement root)
{
var strip = HudUi.Panel(PanelDark);
strip.style.position = Position.Absolute;
strip.style.left = 40; strip.style.top = 28;
strip.style.flexDirection = FlexDirection.Row;
strip.style.alignItems = Align.Center;
strip.style.paddingLeft = 14; strip.style.paddingRight = 14;
strip.style.paddingTop = 8; strip.style.paddingBottom = 8;
var theme = HudTheme.Get();
strip.Add(ResourceChip(theme != null ? theme.AetherIcon : null, AetherCyan, "0", out _aetherNum, 26, 20));
strip.Add(ResourceChip(theme != null ? theme.OreIcon : null, OreAmber, "0", out _oreNum, 30, 22));
strip.Add(ResourceChip(theme != null ? theme.BioIcon : null, BioGreen, "0", out _bioNum, 26, 20));
// DR-042 C6a: the only Aether sink (ability-damage upgrade) gets a visible, clickable button (was U-key
// only). The Button element handles its own picking even though the HUD root Ignores clicks.
// (Step 11: the Aether UPGRADE-DMG button was RETIRED with AbilityUpgradeRequest — the choice-of-3
// boon modal + the base meta-shop (Step 14) replace it.)
root.Add(strip);
}
VisualElement ResourceChip(Sprite icon, Color tint, string initial, out Label num, float iconSize, int fontSize)
{
var chip = new VisualElement();
chip.style.flexDirection = FlexDirection.Row;
chip.style.alignItems = Align.Center;
chip.style.marginLeft = 9; chip.style.marginRight = 9;
chip.pickingMode = PickingMode.Ignore;
var ic = HudUi.Icon(icon, iconSize, tint);
ic.style.marginRight = 6;
chip.Add(ic);
num = HudUi.Display(initial, fontSize, tint, TextAnchor.MiddleLeft);
chip.Add(num);
return chip;
}
void BuildPaletteRow(VisualElement root)
{
_paletteRow = new VisualElement();
_paletteRow.style.position = Position.Absolute;
_paletteRow.style.bottom = 24; _paletteRow.style.left = 0; _paletteRow.style.right = 0;
_paletteRow.style.flexDirection = FlexDirection.Row;
_paletteRow.style.justifyContent = Justify.Center;
_paletteRow.pickingMode = PickingMode.Ignore; // the row passes clicks through; its slots pick
root.Add(_paletteRow);
}
void BuildHintBar(VisualElement root)
{
_hintBar = new VisualElement();
_hintBar.style.position = Position.Absolute;
_hintBar.style.bottom = 138; _hintBar.style.left = 0; _hintBar.style.right = 0;
_hintBar.style.flexDirection = FlexDirection.Row;
_hintBar.style.justifyContent = Justify.Center;
_hintBar.pickingMode = PickingMode.Ignore;
_hintBar.style.display = DisplayStyle.None;
root.Add(_hintBar);
}
void BuildDiscoveryChip(VisualElement root)
{
// Slice 1 HUD declutter: a subtle bottom-center chip teaching the build-mode toggle, shown only while
// the palette is CLOSED at base. The glyph uses the text fallback ("Tab"/"Y") — no HudTheme sprite needed.
bool pad = AimPresentation.Scheme == InputSchemeId.Gamepad;
_buildDiscoveryChip = new VisualElement();
_buildDiscoveryChip.style.position = Position.Absolute;
_buildDiscoveryChip.style.bottom = 28; _buildDiscoveryChip.style.left = 0; _buildDiscoveryChip.style.right = 0;
_buildDiscoveryChip.style.flexDirection = FlexDirection.Row;
_buildDiscoveryChip.style.justifyContent = Justify.Center;
_buildDiscoveryChip.style.alignItems = Align.Center;
_buildDiscoveryChip.pickingMode = PickingMode.Ignore;
_buildDiscoveryChip.style.opacity = 0.6f;
_buildDiscoveryChip.Add(HudUi.Glyph(null, pad ? "Y" : "Tab", 26));
var lbl = HudUi.Text("BUILD", 12, MenuUi.SubCol, TextAnchor.MiddleLeft);
lbl.style.marginLeft = 5;
_buildDiscoveryChip.Add(lbl);
_buildDiscoveryChip.style.display = DisplayStyle.None;
root.Add(_buildDiscoveryChip);
}
void BuildDowned(VisualElement root)
{
_downed = new VisualElement();
_downed.style.position = Position.Absolute;
_downed.style.left = 0; _downed.style.right = 0; _downed.style.top = 0; _downed.style.bottom = 0;
_downed.style.alignItems = Align.Center;
_downed.style.justifyContent = Justify.Center;
_downed.pickingMode = PickingMode.Ignore;
var theme = HudTheme.Get();
if (theme != null && theme.Vignette != null)
{
_downed.style.backgroundImage = new StyleBackground(Background.FromSprite(theme.Vignette));
_downed.style.unityBackgroundImageTintColor = new Color(0.45f, 0f, 0f, 0.6f);
_downed.style.backgroundSize = new StyleBackgroundSize(new BackgroundSize(BackgroundSizeType.Cover));
}
else
{
_downed.style.backgroundColor = new Color(0.35f, 0f, 0f, 0.35f);
}
var downedCol = HudUi.Group(Align.Center);
downedCol.Add(HudUi.Display("DOWNED", 52, new Color(1f, 0.45f, 0.4f), TextAnchor.MiddleCenter));
_downedText = HudUi.Text("RESPAWNING...", 24, new Color(1f, 0.75f, 0.7f), TextAnchor.MiddleCenter);
_downedText.style.marginTop = 6;
downedCol.Add(_downedText);
_downed.Add(downedCol);
_downed.style.display = DisplayStyle.None;
root.Add(_downed);
}
// 2026-08-07 audit purge: BuildInventory / AddInvRow / ItemName / ItemTint / IsEquippable / SlotName /
// AddEquipRow drove the personal-inventory + equipment strip. That layer was already PAUSED in CLAUDE.md
// and went with the shell.
static Color ResourceTint(byte resId)
=> resId == ResourceId.Aether ? AetherCyan : resId == ResourceId.Biomass ? BioGreen : OreAmber;
static Sprite ResourceSprite(HudTheme t, byte resId)
{
if (t == null) return null;
return resId == ResourceId.Aether ? t.AetherIcon : resId == ResourceId.Biomass ? t.BioIcon : t.OreIcon;
}
// 2026-08-07 audit purge: StructureName, RoomTypeLabel, the branching route map, the ready panel, the
// boss presence bar and the run-depth dots all belonged to the superseded base/expedition loop and are
// deleted along with RunInfo / PlayerReady / BossState / StructureCatalog. Recover from git if the
// roguelite spine returns.
}
}