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<AltarEquipmentTreeNode> 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<DeathAltarUpgradeType> types, out float contentWidth, out float contentHeight)
{
contentWidth = 1320f;
Vector2 rootPosition = new(contentWidth * 0.5f, 112f);
nodes.Add(AltarEquipmentTreeNode.Root(rootPosition));
List<DeathAltarUpgradeType> regular = [];
List<DeathAltarUpgradeType> 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<DeathAltarUpgradeType> 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<DeathAltarUpgradeType>()
};
}
private static bool IsConvergenceNode(DeathAltarUpgradeType type)
{
return type is DeathAltarUpgradeType.WingInfiniteFlight
or DeathAltarUpgradeType.NecklaceSovereignty;
}
}
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<AltarEquipmentTreeNode> 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<DeathAltarUpgradeType> types, out float contentWidth, out float contentHeight)
{
contentWidth = 1320f;
Vector2 rootPosition = new(contentWidth * 0.5f, 112f);
nodes.Add(AltarEquipmentTreeNode.Root(rootPosition));
List<DeathAltarUpgradeType> regular = [];
List<DeathAltarUpgradeType> 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<DeathAltarUpgradeType> 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<DeathAltarUpgradeType>()
};
}
private static bool IsConvergenceNode(DeathAltarUpgradeType type)
{
return type is DeathAltarUpgradeType.WingInfiniteFlight
or DeathAltarUpgradeType.NecklaceSovereignty;
}
}