using SoulHarvest.Common; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using Terraria; using Terraria.Audio; using Terraria.GameContent; using Terraria.ID; using Terraria.Localization; using Terraria.ModLoader; using Terraria.UI; namespace SoulHarvest.UI; internal sealed class AltarEquipmentProgressionPanel : UIElement { private const float TopBarHeight = 52f; private const float DetailWidth = 246f; private readonly DeathAltarUIState owner; private readonly List nodes = []; private readonly AltarEquipmentTreeDetailPanel detailPanel; private AltarEquipmentTreeViewport? viewport; private AltarEquipmentTreeNode? selectedNode; private DeathAltarItemReference? boundReference; private IDeathAltarUpgradeable? upgradeable; private bool boundCharacterDeathDomain; private int itemType; private string itemName = string.Empty; private int essenceCount; private int souls; private float layoutScale = 1f; public AltarEquipmentProgressionPanel(DeathAltarUIState owner) { this.owner = owner; SetPadding(0f); detailPanel = new AltarEquipmentTreeDetailPanel(this); Append(detailPanel); } public bool IsBoundTo(AltarEquipmentEntry entry) { return !entry.IsCharacterSickle && entry.ItemReference is DeathAltarItemReference reference && boundReference is DeathAltarItemReference current && current.Storage == reference.Storage && current.Slot == reference.Slot && current.ExpectedItemType == reference.ExpectedItemType; } public void Bind(AltarEquipmentEntry entry, Item item, IDeathAltarUpgradeable target) { if (entry.ItemReference is not DeathAltarItemReference reference) return; upgradeable = target; boundCharacterDeathDomain = entry.IsCharacterDeathDomain; itemType = item.type; itemName = item.Name; if (IsBoundTo(entry)) return; DeactivateInteraction(); boundReference = reference; if (viewport is not null && viewport.Parent is not null) RemoveChild(viewport); nodes.Clear(); BuildTree(target.AltarUpgradeTypes, out float contentWidth, out float contentHeight); viewport = new AltarEquipmentTreeViewport(this, nodes, contentWidth, contentHeight); Append(viewport); if (detailPanel.Parent is not null) RemoveChild(detailPanel); Append(detailPanel); selectedNode = nodes.Count > 0 ? nodes[0] : null; viewport.SetSelectedNode(selectedNode); detailPanel.SetNode(selectedNode); ApplyLayoutScale(layoutScale); } public void Refresh(Item item, IDeathAltarUpgradeable target, int essenceCount, int souls) { itemType = item.type; itemName = item.Name; upgradeable = target; this.essenceCount = essenceCount; this.souls = souls; foreach (AltarEquipmentTreeNode node in nodes) node.Refresh(itemName, target, essenceCount, souls); detailPanel.SetNode(selectedNode); detailPanel.RefreshActionState(); } public void ApplyLayoutScale(float scale) { layoutScale = Math.Clamp(scale, 0.25f, 1f); SetPadding(0f); if (viewport is not null) { viewport.Left.Set(8f * layoutScale, 0f); viewport.Top.Set((TopBarHeight + 6f) * layoutScale, 0f); viewport.Width.Set(-(DetailWidth + 22f) * layoutScale, 1f); viewport.Height.Set(-(TopBarHeight + 14f) * layoutScale, 1f); viewport.ApplyLayoutScale(layoutScale); } detailPanel.Left.Set(-(DetailWidth + 8f) * layoutScale, 1f); detailPanel.Top.Set((TopBarHeight + 6f) * layoutScale, 0f); detailPanel.Width.Set(DetailWidth * layoutScale, 0f); detailPanel.Height.Set(-(TopBarHeight + 14f) * layoutScale, 1f); detailPanel.ApplyLayoutScale(layoutScale); } public override void Recalculate() { base.Recalculate(); viewport?.EnsureCameraIsValid(); } public void DeactivateInteraction() => viewport?.CancelInteraction(); internal void SelectNode(AltarEquipmentTreeNode node) { if (ReferenceEquals(selectedNode, node)) return; selectedNode = node; detailPanel.SetNode(node); detailPanel.RefreshActionState(); SoundEngine.PlaySound(SoundID.MenuTick); } internal void RequestSelectedUpgrade() { if (selectedNode?.UpgradeType is not DeathAltarUpgradeType type || !selectedNode.CanRequest || !selectedNode.Affordable) { return; } owner.RequestUpgrade(type); } protected override void DrawSelf(SpriteBatch spriteBatch) { Rectangle bounds = GetDimensions().ToRectangle(); Texture2D pixel = TextureAssets.MagicPixel.Value; spriteBatch.Draw(pixel, bounds, new Color(12, 11, 13) * 0.995f); ReaperTreeDraw.DrawBorder(spriteBatch, pixel, bounds, new Color(91, 62, 61), Math.Max(1, (int)(2f * layoutScale))); ReaperTreeDraw.DrawInsetBorder(spriteBatch, pixel, bounds, Math.Max(3, (int)(7f * layoutScale)), new Color(137, 42, 50) * 0.72f, 1); spriteBatch.Draw(pixel, new Rectangle(bounds.X + Math.Max(1, (int)(8f * layoutScale)), bounds.Y + Math.Max(1, (int)(TopBarHeight * layoutScale)), Math.Max(1, bounds.Width - Math.Max(2, (int)(16f * layoutScale))), Math.Max(1, (int)(2f * layoutScale))), new Color(139, 42, 50) * 0.72f); if (itemType > ItemID.None && itemType < ItemLoader.ItemCount) { Item display = new(); display.SetDefaults(itemType); Vector2 iconCenter = new(bounds.X + 31f * layoutScale, bounds.Y + 27f * layoutScale); AltarDrawHelpers.DrawItemSlot(spriteBatch, pixel, iconCenter, 38f * layoutScale, new Color(28, 23, 25), new Color(143, 52, 57)); if (boundCharacterDeathDomain) { int iconSize = Math.Max(1, (int)MathF.Round(29f * layoutScale)); DeathDomainBackdropTextureSystem.DrawUiIcon( spriteBatch, new Rectangle( (int)MathF.Round(iconCenter.X - iconSize * 0.5f), (int)MathF.Round(iconCenter.Y - iconSize * 0.5f), iconSize, iconSize), Main.MouseScreen); } else { AltarDrawHelpers.DrawItemIcon(spriteBatch, display, iconCenter, 31f * layoutScale); } } float titleScale = 0.76f * layoutScale; string title = Language.GetTextValue("Mods.SoulHarvest.UI.EquipmentTree.ProgressionTitle", itemName); title = AltarDrawHelpers.FitText(title, 255f * layoutScale, titleScale); Utils.DrawBorderString(spriteBatch, title, new Vector2(bounds.X + 56f * layoutScale, bounds.Y + 15f * layoutScale), new Color(232, 219, 202), titleScale); float resourcesLeft = bounds.X + 326f * layoutScale; float resourcesRight = bounds.Right - 12f * layoutScale; float cellWidth = Math.Max(34f * layoutScale, (resourcesRight - resourcesLeft) * 0.5f); DrawResourceCell(spriteBatch, resourcesLeft, bounds.Y + 9f * layoutScale, cellWidth, new Color(201, 68, 76), Language.GetTextValue("Mods.SoulHarvest.UI.ReaperTree.ResourceEssence", essenceCount)); DrawResourceCell(spriteBatch, resourcesLeft + cellWidth, bounds.Y + 9f * layoutScale, cellWidth, new Color(196, 151, 113), Language.GetTextValue("Mods.SoulHarvest.UI.ReaperTree.ResourceSouls", souls)); } private void DrawResourceCell(SpriteBatch batch, float x, float y, float width, Color color, string text) { Texture2D pixel = TextureAssets.MagicPixel.Value; Rectangle cell = new((int)x, (int)y, Math.Max(1, (int)(width - 5f * layoutScale)), Math.Max(1, (int)(34f * layoutScale))); batch.Draw(pixel, cell, new Color(21, 19, 22) * 0.94f); ReaperTreeDraw.DrawBorder(batch, pixel, cell, new Color(72, 57, 57), 1); Vector2 rune = new(cell.X + 13f * layoutScale, cell.Center.Y); ReaperTreeDraw.DrawDiamond(batch, pixel, rune, 6f * layoutScale, color * 0.86f); float textScale = 0.55f * layoutScale; string fitted = AltarDrawHelpers.FitText(text, Math.Max(6f, cell.Width - 27f * layoutScale), textScale); Utils.DrawBorderString(batch, fitted, new Vector2(cell.X + 24f * layoutScale, cell.Y + 9f * layoutScale), color, textScale); } private void BuildTree(IReadOnlyList types, out float contentWidth, out float contentHeight) { contentWidth = 1320f; Vector2 rootPosition = new(contentWidth * 0.5f, 112f); nodes.Add(AltarEquipmentTreeNode.Root(rootPosition)); List regular = []; List convergence = []; foreach (DeathAltarUpgradeType type in types) { if (IsConvergenceNode(type)) convergence.Add(type); else regular.Add(type); } int columns = Math.Clamp(regular.Count, 1, 5); const float spacingX = 214f; const float spacingY = 158f; const float firstRowY = 310f; int rows = Math.Max(1, (regular.Count + columns - 1) / columns); for (int index = 0; index < regular.Count; index++) { int row = index / columns; int rowStart = row * columns; int rowCount = Math.Min(columns, regular.Count - rowStart); int column = index - rowStart; float startX = contentWidth * 0.5f - (rowCount - 1) * spacingX * 0.5f; nodes.Add(AltarEquipmentTreeNode.Upgrade(regular[index], new Vector2(startX + column * spacingX, firstRowY + row * spacingY), convergence: false)); } float convergenceY = firstRowY + rows * spacingY + 42f; for (int index = 0; index < convergence.Count; index++) { float startX = contentWidth * 0.5f - (convergence.Count - 1) * spacingX * 0.5f; nodes.Add(AltarEquipmentTreeNode.Upgrade(convergence[index], new Vector2(startX + index * spacingX, convergenceY), convergence: true)); } float lastY = convergence.Count > 0 ? convergenceY : firstRowY + (rows - 1) * spacingY; contentHeight = Math.Max(760f, lastY + 190f); } internal static IReadOnlyList GetPrerequisites(DeathAltarUpgradeType type) { return type switch { DeathAltarUpgradeType.FatedMaxStacks or DeathAltarUpgradeType.FatedStacksPerHit or DeathAltarUpgradeType.FatedDuration => [DeathAltarUpgradeType.FatedUnlock], DeathAltarUpgradeType.WingInfiniteFlight => [DeathAltarUpgradeType.WingFlightTime, DeathAltarUpgradeType.WingHorizontalSpeed, DeathAltarUpgradeType.WingAcceleration, DeathAltarUpgradeType.WingVerticalPower], DeathAltarUpgradeType.NecklaceSovereignty => [DeathAltarUpgradeType.NecklaceRadius, DeathAltarUpgradeType.NecklaceAbsorbAll], _ => Array.Empty() }; } private static bool IsConvergenceNode(DeathAltarUpgradeType type) { return type is DeathAltarUpgradeType.WingInfiniteFlight or DeathAltarUpgradeType.NecklaceSovereignty; } }