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>
This commit is contained in:
2026-08-07 12:59:39 -07:00
parent 6a412fe3e7
commit 62e48a3b0b
304 changed files with 260 additions and 14591 deletions
@@ -142,134 +142,10 @@ namespace ProjectM.Client
bool haveTick = SystemAPI.TryGetSingleton<NetworkTime>(out var nt);
int huskCount = _huskQuery.CalculateEntityCount();
// ---- Macro banner: run-lifecycle header (the siege/cycle machinery is retired — LANTERN purge) ----
bool haveRun = SystemAPI.TryGetSingleton<RunInfo>(out var runInfo);
bool onRun = haveRun && runInfo.Lifecycle != RunLifecycle.Staging;
if (haveRun)
{
var col = onRun ? new Color(1f, 0.8f, 0.4f) : new Color(0.45f, 0.9f, 0.7f);
_phaseText.text = onRun ? "ON EXPEDITION" : "AT BASE";
_phaseText.style.color = col;
_banner.style.borderBottomColor = col;
RetintPanel(_banner, PanelDark);
}
else
{
_phaseText.text = "";
}
// ---- Location line (banner sub-line) — Step 14: driven by the replicated RunInfo lifecycle FSM ----
var cam = Camera.main; // camera-X region signal still feeds downstream panels (atmosphere/threat)
bool onExpedition = cam != null && cam.transform.position.x > ExpeditionRegionXMin;
SystemAPI.TryGetSingleton<ExpeditionObjective>(out var obj);
if (haveRun)
{
switch (runInfo.Lifecycle)
{
case RunLifecycle.Staging:
// The READY panel (bottom-center) owns the action + N/M count; the top line frames intent.
_locationText.text = "AT THE BASE - build defenses, buy upgrades, READY UP to launch";
_locationText.style.color = new Color(0.55f, 0.85f, 1f);
break;
case RunLifecycle.Launching:
{
// Wrap-safe countdown (post-impl review): the client's PREDICTED tick passes LaunchTick
// near zero while Lifecycle is still Launching — signed TicksSince, never raw uint math.
int secs = 0;
if (runInfo.LaunchTick != 0 && SystemAPI.TryGetSingleton<NetworkTime>(out var ntime)
&& ntime.ServerTick.IsValid)
{
int ticksLeft = new NetworkTick(runInfo.LaunchTick).TicksSince(ntime.ServerTick);
if (ticksLeft > 0) secs = ticksLeft / 60 + 1;
}
_locationText.text = "LAUNCHING IN " + secs + " - un-ready [T] to abort";
_locationText.style.color = new Color(1f, 0.9f, 0.4f);
break;
}
case RunLifecycle.InRoom:
{
string room = "ROOM " + (runInfo.CurrentRoom + 1) + "/" + runInfo.RoomCount
+ " " + RoomTypeLabel(runInfo.CurrentRoomType);
_locationText.text = obj.State == ExpeditionObjectiveState.Active
? room + " - " + obj.Remaining + " enemies remaining"
: room + " - clear it to advance";
_locationText.style.color = new Color(1f, 0.8f, 0.4f);
break;
}
case RunLifecycle.RoomReward:
_locationText.text = "ROOM CLEARED - choose your boon";
_locationText.style.color = new Color(0.5f, 1f, 0.6f);
break;
case RunLifecycle.RoomExplore: // Phase 0: without this case the RoomReward text stuck through the loot window
_locationText.text = "ROOM CLEAR - grab the loot, take the portal to move on";
_locationText.style.color = new Color(0.5f, 1f, 0.6f);
break;
case RunLifecycle.RouteSelect:
_locationText.text = "CHOOSE YOUR PATH";
_locationText.style.color = new Color(0.55f, 0.85f, 1f);
break;
case RunLifecycle.Returning:
_locationText.text = "RETURNING HOME...";
_locationText.style.color = new Color(0.7f, 0.9f, 1f);
break;
}
}
else
{
_locationText.text = "";
}
// The clickable READY panel (Staging/Launching). Counts are the replicated send-to-all PlayerReady flags.
int rTotal = 0, rReady = 0;
bool localReady = false;
int launchSecs = 0;
bool readyShow = haveRun
&& (runInfo.Lifecycle == RunLifecycle.Staging || runInfo.Lifecycle == RunLifecycle.Launching);
if (readyShow)
{
foreach (var pr in SystemAPI.Query<RefRO<PlayerReady>>().WithAll<PlayerTag>())
{
rTotal++;
if (pr.ValueRO.Value != 0) rReady++;
}
foreach (var pr in SystemAPI.Query<RefRO<PlayerReady>>().WithAll<PlayerTag, GhostOwnerIsLocal>())
localReady = pr.ValueRO.Value != 0;
if (runInfo.Lifecycle == RunLifecycle.Launching && runInfo.LaunchTick != 0
&& SystemAPI.TryGetSingleton<NetworkTime>(out var lnt) && lnt.ServerTick.IsValid)
{
int tl = new NetworkTick(runInfo.LaunchTick).TicksSince(lnt.ServerTick);
if (tl > 0) launchSecs = tl / 60 + 1;
}
}
UpdateReadyPanel(readyShow, runInfo, rTotal, rReady, localReady, launchSecs);
// Boss presence bar. The boss is a scaled Charger (EnemyTelegraph.Kind==KindCharger, baked/client-safe)
// in the EXPEDITION region — filtering on both excludes phase-two summoned swarmers AND a base-region
// siege enemy a dead teammate can see. Health.Max is now a [GhostField] (replicated x8 for the boss), so
// the fraction reads true directly.
bool bossAlive = false;
float bossHp = 0f, bossMax = 0f;
if (haveRun && runInfo.Lifecycle == RunLifecycle.InRoom && runInfo.CurrentRoomType == RoomTypeId.Boss)
{
foreach (var (bhq, tele, blt) in
SystemAPI.Query<RefRO<Health>, RefRO<EnemyTelegraph>, RefRO<LocalTransform>>().WithAll<EnemyTag>())
{
if (tele.ValueRO.Kind != ZoneEnemyMath.KindCharger) continue; // the boss is a Charger; skip summoned swarmers
if (blt.ValueRO.Position.x <= ExpeditionRegionXMin) continue; // expedition only (not a base siege enemy)
if (bhq.ValueRO.Max > bossMax)
{
bossMax = bhq.ValueRO.Max;
bossHp = bhq.ValueRO.Current;
bossAlive = bhq.ValueRO.Current > 0f;
}
}
}
UpdateBossBar(bossAlive, bossHp, bossMax);
// Run-depth dots — keeps the roguelite spine visible while fighting (the map only shows at gates).
UpdateRunDepth(haveRun ? runInfo : default, haveRun);
// 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) ----
@@ -307,23 +183,6 @@ namespace ProjectM.Client
RetintPanel(_threatPanel, PanelDark);
}
// ---- Build palette + control hints (bottom-center) ----
UpdatePalette(aether, ore, bio, onExpedition);
bool paletteOpen = BuildPaletteState.PaletteOpen && !onExpedition && _paletteBuilt;
bool buildActive = paletteOpen && BuildPaletteState.Active;
if (buildActive)
{
byte scheme = AimPresentation.Scheme;
if (!_hintBuilt || _hintScheme != scheme) RebuildHints(scheme);
_hintBar.style.display = DisplayStyle.Flex;
}
else
{
_hintBar.style.display = DisplayStyle.None;
}
// Build-mode discovery chip: a subtle "Tab/Y — BUILD" hint when the palette is hidden at base (Slice 1).
_buildDiscoveryChip.style.display = (!onExpedition && !BuildPaletteState.PaletteOpen)
? DisplayStyle.Flex : DisplayStyle.None;
// ---- Per-player vitals ----
bool found = false;
@@ -348,7 +207,7 @@ namespace ProjectM.Client
break;
}
_doc.rootVisualElement.style.display = (found || haveRun) ? DisplayStyle.Flex : DisplayStyle.None;
_doc.rootVisualElement.style.display = found ? DisplayStyle.Flex : DisplayStyle.None;
// ---- Low-health vignette + hurt flash (full-screen) ----
_flash = HudVisualMath.DecayFlash(_flash, dt);
@@ -389,51 +248,6 @@ namespace ProjectM.Client
_vignette.style.display = DisplayStyle.None;
_downed.style.display = DisplayStyle.None;
}
// ---- Personal inventory (read-only; toggle with I, deposit-all with G via InventoryDepositSendSystem) ----
var invKb = UnityEngine.InputSystem.Keyboard.current;
if (invKb != null && invKb.iKey.wasPressedThisFrame) _invOpen = !_invOpen;
if (_invOpen && found)
{
EntityManager.CompleteDependencyBeforeRO<InventorySlot>();
EntityManager.CompleteDependencyBeforeRO<EquipmentSlot>();
bool haveItemDb = SystemAPI.TryGetSingleton<ItemDatabase>(out var itemDb);
_invPanel.style.display = DisplayStyle.Flex;
_invList.Clear();
int shown = 0;
foreach (var bag in SystemAPI.Query<DynamicBuffer<InventorySlot>>()
.WithAll<GhostOwnerIsLocal, PlayerTag>())
{
for (int i = 0; i < bag.Length; i++)
{
var slot = bag[i];
if (slot.ItemId == 0 || slot.Count <= 0) continue;
AddInvRow(slot.ItemId, ItemName(haveItemDb, itemDb, slot.ItemId), ItemTint(slot.ItemId), slot.Count,
IsEquippable(haveItemDb, itemDb, slot.ItemId));
shown++;
}
break;
}
if (shown == 0)
_invList.Add(HudUi.Text("(empty)", 13, MenuUi.SubCol, TextAnchor.MiddleLeft));
_equipList.Clear();
foreach (var slots in SystemAPI.Query<DynamicBuffer<EquipmentSlot>>()
.WithAll<GhostOwnerIsLocal, PlayerTag>())
{
for (byte s = 0; s < EquipSlotId.Count && s < slots.Length; s++)
{
ushort id = slots[s].ItemId;
string label = SlotName(s) + ": " + (id == 0 ? "-" : ItemName(haveItemDb, itemDb, id));
AddEquipRow(s, label, id != 0);
}
break;
}
}
else
{
_invPanel.style.display = DisplayStyle.None;
}
}
// ---- per-frame helpers ----
@@ -446,94 +260,9 @@ namespace ProjectM.Client
// LANTERN purge: the automation buildables are deleted; Pylon stays hidden from the build palette (cosmetic-only).
static bool IsPaletteType(byte type) => type != StructureType.Pylon;
void UpdatePalette(int aether, int ore, int bio, bool onExpedition)
{
if (!_paletteBuilt && SystemAPI.TryGetSingletonEntity<StructureCatalog>(out var catE))
{
var cat = SystemAPI.GetBuffer<StructureCatalogEntry>(catE);
for (int i = 0; i < cat.Length; i++)
if (IsPaletteType(cat[i].Type))
AddPaletteItem(cat[i].Type, cat[i].CostAmount, cat[i].CostResourceId);
_paletteBuilt = true;
}
if (!_paletteBuilt) { _paletteRow.style.display = DisplayStyle.None; return; }
bool showPalette = !onExpedition && BuildPaletteState.PaletteOpen;
_paletteRow.style.display = showPalette ? DisplayStyle.Flex : DisplayStyle.None;
foreach (var kv in _palette)
{
var item = kv.Value;
int have = item.CostRes == ResourceId.Aether ? aether : item.CostRes == ResourceId.Biomass ? bio : ore;
bool affordable = have >= item.CostAmount;
bool selected = BuildPaletteState.Selected == kv.Key;
item.Root.style.opacity = affordable ? 1f : 0.5f;
item.Cost.style.color = affordable ? new Color(0.7f, 0.95f, 0.8f) : new Color(1f, 0.5f, 0.4f);
if (item.Icon != null)
item.Icon.style.unityBackgroundImageTintColor = affordable ? AetherCyan : new Color(0.5f, 0.55f, 0.6f);
MenuUi.Border(item.Root, selected ? MenuUi.Accent : SlotIdleBorder, selected ? 2 : 1);
item.Root.style.backgroundColor = selected ? SlotSelBg : SlotIdleBg;
if (item.Glow != null) item.Glow.style.opacity = selected ? 0.6f : 0f;
}
}
void AddPaletteItem(byte type, int cost, byte costRes)
{
if (type == 0 || _palette.ContainsKey(type)) return;
var theme = HudTheme.Get();
var root = new VisualElement();
root.style.width = 86;
root.style.marginLeft = 4; root.style.marginRight = 4;
root.style.paddingTop = 8; root.style.paddingBottom = 6;
root.style.alignItems = Align.Center;
root.style.backgroundColor = SlotIdleBg;
root.pickingMode = PickingMode.Position;
MenuUi.Round(root, 6);
MenuUi.Border(root, SlotIdleBorder, 1);
// selection glow: a soft Synty glow filling the slot behind everything, opacity toggled on select.
var glow = new VisualElement();
glow.style.position = Position.Absolute;
glow.style.left = 3; glow.style.right = 3; glow.style.top = 4; glow.style.bottom = 4;
glow.pickingMode = PickingMode.Ignore;
glow.style.opacity = 0f;
if (theme != null && theme.Glow != null)
{
glow.style.backgroundImage = new StyleBackground(Background.FromSprite(theme.Glow));
glow.style.unityBackgroundImageTintColor = AetherCyan;
glow.style.backgroundSize = new StyleBackgroundSize(new BackgroundSize(BackgroundSizeType.Cover));
}
root.Add(glow);
var iconEl = HudUi.Icon(theme != null ? theme.StructureIcon(type) : null, 44, AetherCyan);
root.Add(iconEl);
var nameLabel = HudUi.Text(StructureName(type), 12, MenuUi.TextCol, TextAnchor.MiddleCenter);
nameLabel.style.marginTop = 2;
root.Add(nameLabel);
var costRow = new VisualElement();
costRow.style.flexDirection = FlexDirection.Row;
costRow.style.alignItems = Align.Center;
costRow.style.marginTop = 2;
costRow.pickingMode = PickingMode.Ignore;
var costIcon = HudUi.Icon(ResourceSprite(theme, costRes), 14, ResourceTint(costRes));
costIcon.style.marginRight = 3;
costRow.Add(costIcon);
var costLabel = HudUi.Display(cost.ToString(), 13, new Color(0.7f, 0.95f, 0.8f), TextAnchor.MiddleCenter);
costRow.Add(costLabel);
root.Add(costRow);
byte t = type;
root.RegisterCallback<ClickEvent>(_ =>
BuildPaletteState.Select(BuildPaletteState.Selected == t ? (byte)0 : t));
_paletteRow.Add(root);
_palette[type] = new PaletteItem { Root = root, Cost = costLabel, CostAmount = cost, CostRes = costRes, Glow = glow, Icon = iconEl };
}
// 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)
{
@@ -583,11 +312,7 @@ namespace ProjectM.Client
BuildThreat(root);
BuildMacro(root);
BuildResources(root);
BuildPaletteRow(root);
BuildHintBar(root);
BuildDiscoveryChip(root);
BuildDowned(root);
BuildInventory(root);
}
void BuildVignette(VisualElement root)
@@ -827,119 +552,9 @@ namespace ProjectM.Client
void BuildInventory(VisualElement root)
{
_invPanel = HudUi.Panel(PanelDark);
_invPanel.style.position = Position.Absolute;
_invPanel.style.right = 40; _invPanel.style.bottom = 40;
_invPanel.style.minWidth = 224;
_invPanel.style.paddingLeft = 14; _invPanel.style.paddingRight = 14;
_invPanel.style.paddingTop = 10; _invPanel.style.paddingBottom = 10;
_invPanel.style.alignItems = Align.FlexStart;
_invPanel.pickingMode = PickingMode.Ignore;
var header = HudUi.Display("INVENTORY", 16, AetherCyan, TextAnchor.MiddleLeft);
header.style.marginBottom = 6;
_invPanel.Add(header);
_invList = new VisualElement();
_invList.pickingMode = PickingMode.Ignore;
_invPanel.Add(_invList);
var equipHeader = HudUi.Display("EQUIPMENT", 14, AetherCyan, TextAnchor.MiddleLeft);
equipHeader.style.marginTop = 8; equipHeader.style.marginBottom = 4;
_invPanel.Add(equipHeader);
_equipList = new VisualElement();
_equipList.pickingMode = PickingMode.Ignore;
_invPanel.Add(_equipList);
var hint = HudUi.Text("I close - click item=equip / slot=unequip - G deposit", 11, MenuUi.SubCol, TextAnchor.MiddleLeft);
hint.style.marginTop = 8;
_invPanel.Add(hint);
_invPanel.style.display = DisplayStyle.None;
root.Add(_invPanel);
}
void AddInvRow(ushort itemId, string name, Color tint, int count, bool equippable)
{
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.justifyContent = Justify.SpaceBetween;
row.style.minWidth = 196;
row.style.marginTop = 2;
row.Add(HudUi.Text(name + (equippable ? " (equip)" : ""), 13, tint, TextAnchor.MiddleLeft));
row.Add(HudUi.Display("x" + count, 13, Color.white, TextAnchor.MiddleRight));
if (equippable)
{
row.pickingMode = PickingMode.Position;
ushort id = itemId;
row.RegisterCallback<ClickEvent>(_ => EquipSendSystem.Equip(id));
}
else row.pickingMode = PickingMode.Ignore;
_invList.Add(row);
}
static string ItemName(bool haveDb, ItemDatabase db, ushort id)
{
if (haveDb && db.Value.IsCreated)
{
ref var blob = ref db.Value.Value;
if (blob.TryGetItem(id, out var def)) return def.Name.ToString();
}
if (id == ResourceId.Aether) return "Aether";
if (id == ResourceId.Ore) return "Ore";
if (id == ResourceId.Biomass) return "Biomass";
return "Item " + id;
}
static Color ItemTint(ushort id)
{
if (id == ResourceId.Aether) return AetherCyan;
if (id == ResourceId.Ore) return OreAmber;
if (id == ResourceId.Biomass) return BioGreen;
return new Color(0.85f, 0.85f, 0.9f);
}
static bool IsEquippable(bool haveDb, ItemDatabase db, ushort id)
{
if (!haveDb || !db.Value.IsCreated) return false;
ref var b = ref db.Value.Value;
return b.TryGetItem(id, out var def) && def.EquipSlot < EquipSlotId.Count;
}
static string SlotName(byte slot)
{
switch (slot)
{
case EquipSlotId.Weapon: return "Weapon";
case EquipSlotId.Armor: return "Armor";
case EquipSlotId.Trinket: return "Trinket";
case EquipSlotId.Tool: return "Tool";
default: return "Slot " + slot;
}
}
void AddEquipRow(byte slot, string label, bool occupied)
{
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.justifyContent = Justify.SpaceBetween;
row.style.minWidth = 196;
row.style.marginTop = 2;
row.Add(HudUi.Text(label, 13, occupied ? AetherCyan : MenuUi.SubCol, TextAnchor.MiddleLeft));
if (occupied)
{
row.pickingMode = PickingMode.Position;
byte s = slot;
row.RegisterCallback<ClickEvent>(_ => EquipSendSystem.Unequip(s));
row.Add(HudUi.Text("unequip", 11, new Color(1f, 0.6f, 0.5f), TextAnchor.MiddleRight));
}
else row.pickingMode = PickingMode.Ignore;
_equipList.Add(row);
}
// 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;
@@ -954,213 +569,10 @@ namespace ProjectM.Client
static string StructureName(byte type)
{
switch (type)
{
case StructureType.Wall: return "Wall";
case StructureType.Pylon: return "Pylon";
default: return "?";
}
}
// ==== Step 14: boon modal + route panel (lazy-built overlays; clicks -> client send statics) ====
static string RoomTypeLabel(byte roomType) => roomType == RoomTypeId.Boss ? "[BOSS]"
: roomType == RoomTypeId.Elite ? "[ELITE]"
: roomType == RoomTypeId.Reward ? "[REWARD]" : "[COMBAT]";
// ---- the drawn branching route map (Slay-the-Spire style; display regen from RunSeed, clicks bind
// to the authoritative RouteOpt* bytes) ----
// ---- the clickable READY panel (Staging: toggle + party pips; Launching: countdown + abort) ----
void UpdateReadyPanel(bool show, RunInfo runInfo, int total, int ready, bool localReady, int launchSecs)
{
if (!show)
{
if (_readyPanel != null) _readyPanel.style.display = DisplayStyle.None;
_readyShownFor = 0;
return;
}
var root = _doc != null ? _doc.rootVisualElement : null;
if (root == null) return;
if (!_readyPanelBuilt)
{
BuildReadyPanel(root);
_readyPanelBuilt = true;
}
bool launching = runInfo.Lifecycle == RunLifecycle.Launching;
int sig = 1 + ready + (total << 4) + (localReady ? 1 << 8 : 0) + (launchSecs << 9) + (launching ? 1 << 16 : 0);
if (_readyShownFor != sig)
{
_readyTitle.text = launching
? "LAUNCHING IN " + launchSecs
: "EXPEDITION — " + ready + "/" + Mathf.Max(total, 1) + " READY";
_readyTitle.style.color = launching ? new Color(1f, 0.9f, 0.4f) : new Color(0.55f, 0.85f, 1f);
_readyBtn.text = launching ? "ABORT [T]" : localReady ? "UNREADY [T]" : "READY UP [T]";
_readyPipRow.Clear();
for (int i = 0; i < total; i++)
{
var pip = new VisualElement();
pip.style.width = 14; pip.style.height = 14;
pip.style.marginLeft = 3; pip.style.marginRight = 3;
MenuUi.Round(pip, 7f);
pip.style.backgroundColor = i < ready
? new Color(0.45f, 0.95f, 0.55f) : new Color(1f, 1f, 1f, 0.15f);
_readyPipRow.Add(pip);
}
_readyShownFor = sig;
}
_readyPanel.style.display = DisplayStyle.Flex;
}
void BuildReadyPanel(VisualElement root)
{
_readyPanel = new VisualElement { pickingMode = PickingMode.Ignore };
_readyPanel.style.position = Position.Absolute;
_readyPanel.style.left = 0; _readyPanel.style.right = 0;
_readyPanel.style.bottom = 170; // clear of the build palette row + hint bar
_readyPanel.style.alignItems = Align.Center;
_readyPanel.style.display = DisplayStyle.None;
var box = new VisualElement { pickingMode = PickingMode.Position };
box.style.backgroundColor = new Color(0.07f, 0.09f, 0.12f, 0.92f);
box.style.borderTopLeftRadius = 10; box.style.borderTopRightRadius = 10;
box.style.borderBottomLeftRadius = 10; box.style.borderBottomRightRadius = 10;
box.style.paddingLeft = 18; box.style.paddingRight = 18;
box.style.paddingTop = 10; box.style.paddingBottom = 12;
box.style.alignItems = Align.Center;
_readyTitle = new Label("EXPEDITION");
_readyTitle.style.fontSize = 16;
_readyTitle.style.unityFontStyleAndWeight = FontStyle.Bold;
box.Add(_readyTitle);
_readyPipRow = new VisualElement();
_readyPipRow.style.flexDirection = FlexDirection.Row;
_readyPipRow.style.justifyContent = Justify.Center;
_readyPipRow.style.marginTop = 6; _readyPipRow.style.marginBottom = 8;
box.Add(_readyPipRow);
_readyBtn = MenuUi.Button("READY UP [T]", ReadySendSystem.ToggleReady);
box.Add(_readyBtn);
_readyPanel.Add(box);
root.Add(_readyPanel);
}
// ---- boss presence bar (Boss rooms only; red, top-center, under the macro banner) ----
void UpdateBossBar(bool alive, float hp, float max)
{
if (!alive || max <= 0f)
{
if (_bossPanel != null) _bossPanel.style.display = DisplayStyle.None;
return;
}
var root = _doc != null ? _doc.rootVisualElement : null;
if (root == null) return;
if (!_bossBarBuilt)
{
BuildBossBar(root);
_bossBarBuilt = true;
}
HudUi.SetFill(_bossFill, Mathf.Clamp01(hp / max));
_bossText.text = "ALPHA HUSK " + Mathf.CeilToInt(Mathf.Max(0f, hp)) + " / " + Mathf.CeilToInt(max);
_bossPanel.style.display = DisplayStyle.Flex;
}
void BuildBossBar(VisualElement root)
{
_bossPanel = new VisualElement { pickingMode = PickingMode.Ignore };
_bossPanel.style.position = Position.Absolute;
_bossPanel.style.left = 0; _bossPanel.style.right = 0;
_bossPanel.style.top = 168;
_bossPanel.style.alignItems = Align.Center;
_bossPanel.style.display = DisplayStyle.None;
var col = HudUi.Group(Align.Center);
_bossText = HudUi.Display("ALPHA HUSK", 22, new Color(1f, 0.35f, 0.3f), TextAnchor.MiddleCenter);
col.Add(_bossText);
var bar = HudUi.Bar(420, 12, new Color(0.92f, 0.22f, 0.18f), out _bossFill);
bar.style.marginTop = 4;
col.Add(bar);
_bossPanel.Add(col);
root.Add(_bossPanel);
}
// ---- run-depth dots (visible through the whole run; the current room pulses bigger) ----
void UpdateRunDepth(RunInfo runInfo, bool haveRun)
{
bool show = haveRun && runInfo.RoomCount > 0
&& (runInfo.Lifecycle == RunLifecycle.InRoom
|| runInfo.Lifecycle == RunLifecycle.RoomReward
|| runInfo.Lifecycle == RunLifecycle.RouteSelect);
if (!show)
{
if (_depthPanel != null) _depthPanel.style.display = DisplayStyle.None;
_depthShownFor = 0;
return;
}
var root = _doc != null ? _doc.rootVisualElement : null;
if (root == null) return;
if (!_depthBuilt)
{
_depthPanel = new VisualElement { pickingMode = PickingMode.Ignore };
_depthPanel.style.position = Position.Absolute;
_depthPanel.style.left = 0; _depthPanel.style.right = 0;
_depthPanel.style.top = 208; // below the macro cluster (goal/core) AND the boss bar (168)
_depthPanel.style.flexDirection = FlexDirection.Row;
_depthPanel.style.justifyContent = Justify.Center;
root.Add(_depthPanel);
_depthBuilt = true;
}
int sig = 1 + runInfo.CurrentRoom * 37 + runInfo.RoomCount * 3;
if (_depthShownFor != sig)
{
_depthPanel.Clear();
for (int i = 0; i < runInfo.RoomCount; i++)
{
bool current = i == runInfo.CurrentRoom;
bool done = i < runInfo.CurrentRoom;
var dot = new VisualElement { pickingMode = PickingMode.Ignore };
float size = current ? 12f : 8f;
dot.style.width = size; dot.style.height = size;
dot.style.marginLeft = 3; dot.style.marginRight = 3;
dot.style.alignSelf = Align.Center;
MenuUi.Round(dot, size * 0.5f);
dot.style.backgroundColor = current ? new Color(0.55f, 0.85f, 1f)
: done ? new Color(0.55f, 0.85f, 1f, 0.55f)
: new Color(1f, 1f, 1f, 0.16f);
_depthPanel.Add(dot);
}
_depthShownFor = sig;
}
_depthPanel.style.display = DisplayStyle.Flex;
}
// 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.