62e48a3b0b
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>
581 lines
26 KiB
C#
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.
|
|
|
|
|
|
|
|
}
|
|
}
|