using SoulHarvest.Common;
using SoulHarvest.Items;
using SoulHarvest.Tiles;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.Audio;
using Terraria.GameContent;
using Terraria.GameContent.UI.Elements;
using Terraria.ID;
using Terraria.Localization;
using Terraria.ModLoader;
using Terraria.UI;
namespace SoulHarvest.UI;
internal class DeathAltarUIState : UIState
{
private const float BasePanelWidth = 1180f;
private const float BasePanelHeight = 700f;
private const float TreeLeft = 228f;
private readonly List<AltarEquipmentEntry> availableEquipment = [];
private DeathAltarFramePanel? panel;
private UIPanel? selectorPanel;
private UIList? itemList;
private UIScrollbar? itemScrollbar;
private UIText? titleText;
private UIText? selectorTitleText;
private UIText? itemText;
private UIText? resourceText;
private AltarTextButton? closeButton;
private AltarTextButton? recipeHelpButton;
private ReaperProgressionPanel? characterProgressionPanel;
private AltarEquipmentProgressionPanel? equipmentProgressionPanel;
private UIPanel? recipeHelpPanel;
private UIList? recipeList;
private UIScrollbar? recipeScrollbar;
private UIText? recipeHelpTitle;
private UIText? noRecipesText;
private bool recipeHelpVisible;
private AltarEquipmentEntry? selectedEquipment;
private int tileX;
private int tileY;
private int itemSignature;
private float layoutScale = 1f;
private int layoutWidth = -1;
private int layoutHeight = -1;
public override void OnInitialize()
{
panel = new DeathAltarFramePanel();
panel.BackgroundColor = new Color(10, 9, 11) * 0.985f;
panel.BorderColor = new Color(116, 34, 43);
Append(panel);
titleText = new UIText(string.Empty, 1.08f, true)
{
DynamicallyScaleDownToWidth = true,
TextOriginX = 0.5f,
TextColor = new Color(232, 219, 204)
};
panel.Append(titleText);
closeButton = new AltarTextButton(
"×",
1f,
new Color(90, 35, 55),
new Color(142, 49, 72),
new Color(180, 82, 105));
closeButton.OnLeftClick += (_, _) =>
{
SoundEngine.PlaySound(SoundID.MenuClose);
ModContent.GetInstance<DeathAltarUISystem>().Hide();
};
panel.Append(closeButton);
recipeHelpButton = new AltarTextButton(
string.Empty,
0.72f,
new Color(42, 35, 39),
new Color(70, 42, 46),
new Color(151, 54, 63));
recipeHelpButton.OnLeftClick += (_, _) => ToggleRecipeHelp();
panel.Append(recipeHelpButton);
selectorTitleText = new UIText(string.Empty, 0.86f, true)
{
DynamicallyScaleDownToWidth = true,
TextColor = new Color(218, 203, 190)
};
panel.Append(selectorTitleText);
selectorPanel = new UIPanel();
selectorPanel.BackgroundColor = new Color(15, 14, 17) * 0.94f;
selectorPanel.BorderColor = new Color(83, 48, 51);
panel.Append(selectorPanel);
itemList = new UIList();
selectorPanel.Append(itemList);
itemScrollbar = new UIScrollbar();
selectorPanel.Append(itemScrollbar);
itemList.SetScrollbar(itemScrollbar);
itemText = new UIText(string.Empty, 0.85f, true)
{
DynamicallyScaleDownToWidth = true,
TextColor = new Color(237, 222, 208)
};
panel.Append(itemText);
resourceText = new UIText(string.Empty, 0.78f)
{
DynamicallyScaleDownToWidth = true,
TextColor = new Color(202, 96, 104)
};
panel.Append(resourceText);
BuildRecipeHelpPanel();
BuildCharacterProgressionPanel();
BuildEquipmentProgressionPanel();
ApplyResponsiveLayout(UserInterface.ActiveInstance.GetDimensions());
}
public override void OnDeactivate()
{
CancelTransientInteraction();
base.OnDeactivate();
}
public override void Recalculate()
{
ApplyResponsiveLayout(UserInterface.ActiveInstance.GetDimensions());
base.Recalculate();
}
private void ApplyResponsiveLayout(CalculatedStyle viewport)
{
if (panel is null)
return;
int viewportWidth = Math.Max(1, (int)MathF.Round(viewport.Width));
int viewportHeight = Math.Max(1, (int)MathF.Round(viewport.Height));
if (viewportWidth == layoutWidth && viewportHeight == layoutHeight)
return;
layoutWidth = viewportWidth;
layoutHeight = viewportHeight;
float widthScale = Math.Max(0.01f, (viewport.Width - 24f) / BasePanelWidth);
float heightScale = Math.Max(0.01f, (viewport.Height - 24f) / BasePanelHeight);
// Active UI dimensions can be as small as 400x300 when the game is run at
// 800x600 with 200% UI scale. Scale against both axes and preserve a small
// safety margin instead of forcing a minimum that can leave the frame off-screen.
layoutScale = Math.Clamp(Math.Min(1f, Math.Min(widthScale, heightScale)), 0.25f, 1f);
float panelWidth = BasePanelWidth * layoutScale;
float panelHeight = BasePanelHeight * layoutScale;
panel.Left.Set(-panelWidth * 0.5f, 0.5f);
panel.Top.Set(-panelHeight * 0.5f, 0.5f);
panel.Width.Set(panelWidth, 0f);
panel.Height.Set(panelHeight, 0f);
panel.SetPadding(14f * layoutScale);
panel.LayoutScale = layoutScale;
panel.SidebarDividerX = 218f * layoutScale;
if (titleText is not null)
{
titleText.Left.Set(240f * layoutScale, 0f);
titleText.Top.Set(4f * layoutScale, 0f);
titleText.Width.Set(-480f * layoutScale, 1f);
titleText.Height.Set(30f * layoutScale, 0f);
SetConstrainedText(titleText, titleText.Text, 1.08f, large: true);
}
LayoutButton(closeButton, -34f, 1f, 0f, 30f, 30f, 1f);
LayoutButton(recipeHelpButton, -156f, 1f, 0f, 116f, 30f, 0.72f);
if (selectorTitleText is not null)
{
selectorTitleText.Left.Set(10f * layoutScale, 0f);
selectorTitleText.Top.Set(49f * layoutScale, 0f);
selectorTitleText.Width.Set(198f * layoutScale, 0f);
selectorTitleText.Height.Set(24f * layoutScale, 0f);
SetConstrainedText(selectorTitleText, selectorTitleText.Text, 0.86f, large: true);
}
if (selectorPanel is not null)
{
selectorPanel.Left.Set(4f * layoutScale, 0f);
selectorPanel.Top.Set(78f * layoutScale, 0f);
selectorPanel.Width.Set(206f * layoutScale, 0f);
selectorPanel.Height.Set(564f * layoutScale, 0f);
selectorPanel.SetPadding(6f * layoutScale);
}
if (itemList is not null)
{
itemList.Width.Set(-18f * layoutScale, 1f);
itemList.Height.Set(0f, 1f);
itemList.ListPadding = 5f * layoutScale;
}
if (itemScrollbar is not null)
{
itemScrollbar.Left.Set(-14f * layoutScale, 1f);
itemScrollbar.Width.Set(14f * layoutScale, 0f);
itemScrollbar.Height.Set(0f, 1f);
}
LayoutTreeHeaderText(itemText, 48f, 0.85f, large: true);
LayoutTreeHeaderText(resourceText, 75f, 0.78f, large: false);
LayoutRecipeHelpPanel();
LayoutCharacterProgressionPanel();
LayoutEquipmentProgressionPanel();
if (availableEquipment.Count > 0)
RebuildSelectors();
if (recipeHelpVisible)
RefreshRecipeHelp();
SyncSelectedContentLayer();
}
private void LayoutTreeHeaderText(UIText? text, float top, float textScale, bool large)
{
if (text is null)
return;
text.Left.Set(TreeLeft * layoutScale, 0f);
text.Top.Set(top * layoutScale, 0f);
text.Width.Set(-(TreeLeft + 8f) * layoutScale, 1f);
text.Height.Set(24f * layoutScale, 0f);
SetConstrainedText(text, text.Text, textScale, large);
}
private void LayoutButton(AltarTextButton? button, float left, float leftPercent, float top, float width, float height, float textScale)
{
if (button is null)
return;
button.Left.Set(left * layoutScale, leftPercent);
button.Top.Set(top * layoutScale, 0f);
button.Width.Set(width * layoutScale, 0f);
button.Height.Set(height * layoutScale, 0f);
button.SetPadding(0f);
button.TextScale = textScale * layoutScale;
}
private void SetConstrainedText(UIText text, string value, float textScale, bool large)
{
text.SetText(value, textScale * layoutScale, large);
text.MinWidth.Set(0f, 0f);
}
public override void Update(GameTime gameTime)
{
base.Update(gameTime);
if (panel?.ContainsPoint(Main.MouseScreen) == true)
Main.LocalPlayer.mouseInterface = true;
Player player = Main.LocalPlayer;
RefreshAvailableEquipment(player);
IDeathAltarUpgradeable? upgradeable = null;
Item? selected = null;
bool characterSickleSelected = selectedEquipment is AltarEquipmentEntry selection && selection.IsCharacterSickle;
if (selectedEquipment is AltarEquipmentEntry selectedEntry && selectedEntry.TryGetDisplayItem(player, out Item resolved))
{
selected = resolved;
if (selectedEntry.IsCharacterDeathDomain)
upgradeable = player.GetModPlayer<MyPlayer>().DeathDomainProgression;
else if (!selectedEntry.IsCharacterSickle)
upgradeable = resolved.ModItem as IDeathAltarUpgradeable;
}
int essenceCount = player.CountItem(ModContent.ItemType<Soul>());
int souls = player.GetModPlayer<MyPlayer>().Souls;
if (!characterSickleSelected
&& selectedEquipment is AltarEquipmentEntry equipmentEntry
&& selected is not null
&& upgradeable is not null
&& equipmentProgressionPanel is not null)
{
equipmentProgressionPanel.Bind(equipmentEntry, selected, upgradeable);
equipmentProgressionPanel.Refresh(selected, upgradeable, essenceCount, souls);
}
if (titleText is not null)
SetConstrainedText(titleText, Language.GetTextValue("Mods.SoulHarvest.UI.AltarTitle"), 1.08f, large: true);
if (selectorTitleText is not null)
SetConstrainedText(selectorTitleText, Language.GetTextValue("Mods.SoulHarvest.UI.SelectEquipment"), 0.86f, large: true);
if (itemText is not null)
SetConstrainedText(itemText, selected is null
? Language.GetTextValue("Mods.SoulHarvest.UI.NoUpgradeableItems")
: Language.GetTextValue("Mods.SoulHarvest.UI.CurrentItem", selected.Name), 0.85f, large: true);
if (resourceText is not null)
SetConstrainedText(resourceText, Language.GetTextValue("Mods.SoulHarvest.UI.AltarResources", essenceCount, souls), 0.78f, large: false);
recipeHelpButton?.SetText(Language.GetTextValue(recipeHelpVisible
? "Mods.SoulHarvest.UI.BackToSkillTree"
: "Mods.SoulHarvest.UI.RecipeHelpButton"));
if (characterSickleSelected)
characterProgressionPanel?.Refresh(player, essenceCount, souls);
SyncSelectedContentLayer();
}
public void SetAltarPosition(int x, int y)
{
CancelTransientInteraction();
tileX = x;
tileY = y;
itemSignature = 0;
selectedEquipment = AltarEquipmentEntry.CreateCharacterSickle();
SetRecipeHelpVisible(false, playSound: false);
SyncSelectedContentLayer();
}
internal void CancelTransientInteraction()
{
characterProgressionPanel?.CancelInteraction();
equipmentProgressionPanel?.DeactivateInteraction();
}
public bool IsPlayerNearAltar()
{
Tile tile = Framing.GetTileSafely(tileX, tileY);
if (!tile.HasTile || tile.TileType != ModContent.TileType<DeathAltarTile>())
return false;
Vector2 altar = new((tileX + 0.5f) * 16f, (tileY + 0.5f) * 16f);
return Vector2.Distance(Main.LocalPlayer.Center, altar) <= 12f * 16f;
}
internal void SelectEquipment(AltarEquipmentEntry entry)
{
if (selectedEquipment is AltarEquipmentEntry current && IsSameEquipmentEntry(current, entry))
return;
CancelTransientInteraction();
selectedEquipment = entry;
SetRecipeHelpVisible(false, playSound: false);
RebuildSelectors();
SyncSelectedContentLayer();
SoundEngine.PlaySound(SoundID.MenuTick);
}
internal void RequestUpgrade(DeathAltarUpgradeType type)
{
if (selectedEquipment?.ItemReference is not DeathAltarItemReference reference)
return;
SoundEngine.PlaySound(SoundID.MenuTick);
ModContent.GetInstance<DeathAltarUISystem>().RequestUpgrade(type, reference, tileX, tileY);
}
private void RefreshAvailableEquipment(Player player)
{
AltarEquipmentEntry characterSickle = AltarEquipmentEntry.CreateCharacterSickle();
AltarEquipmentEntry characterDeathDomain = AltarEquipmentEntry.CreateCharacterDeathDomain();
List<AltarEquipmentEntry> found = [characterSickle, characterDeathDomain];
int signature = HashCode.Combine(17, (int)characterSickle.Kind, characterSickle.ItemType);
signature = HashCode.Combine(signature, (int)characterDeathDomain.Kind, characterDeathDomain.ItemType);
ScanContainer(player.inventory, DeathAltarItemStorage.Inventory, Math.Min(58, player.inventory.Length), found, ref signature);
ScanContainer(player.armor, DeathAltarItemStorage.Armor, player.armor.Length, found, ref signature);
if (signature == itemSignature && found.Count == availableEquipment.Count)
return;
CancelTransientInteraction();
AltarEquipmentEntry? previous = selectedEquipment;
availableEquipment.Clear();
availableEquipment.AddRange(found);
selectedEquipment = previous is AltarEquipmentEntry old ? FindSameEquipmentEntry(old) : null;
selectedEquipment ??= characterSickle;
itemSignature = signature;
RebuildSelectors();
SyncSelectedContentLayer();
}
private static void ScanContainer(Item[] container, DeathAltarItemStorage storage, int accessibleSlots, List<AltarEquipmentEntry> found, ref int signature)
{
for (int slot = 0; slot < accessibleSlots && slot <= byte.MaxValue; slot++)
{
Item item = container[slot];
if (item.IsAir || item.ModItem is not IDeathAltarUpgradeable)
continue;
DeathAltarItemReference reference = new(storage, (byte)slot, item.type);
found.Add(AltarEquipmentEntry.CreateItem(reference));
signature = HashCode.Combine(signature, (int)storage, slot, item.type);
}
}
private AltarEquipmentEntry? FindSameEquipmentEntry(AltarEquipmentEntry old)
{
foreach (AltarEquipmentEntry candidate in availableEquipment)
if (IsSameEquipmentEntry(candidate, old))
return candidate;
return null;
}
private static bool IsSameEquipmentEntry(AltarEquipmentEntry left, AltarEquipmentEntry right)
{
if (left.Kind != right.Kind)
return false;
if (left.IsCharacterProgression)
return true;
return left.ItemReference is DeathAltarItemReference leftReference
&& right.ItemReference is DeathAltarItemReference rightReference
&& leftReference.Storage == rightReference.Storage
&& leftReference.Slot == rightReference.Slot;
}
private void RebuildSelectors()
{
if (itemList is null)
return;
itemList.Clear();
foreach (AltarEquipmentEntry entry in availableEquipment)
{
if (!entry.TryGetDisplayItem(Main.LocalPlayer, out Item item))
continue;
bool isSelected = selectedEquipment is AltarEquipmentEntry current && IsSameEquipmentEntry(current, entry);
itemList.Add(new AltarItemSelector(this, entry, item, isSelected, layoutScale));
}
}
private void BuildRecipeHelpPanel()
{
recipeHelpPanel = new UIPanel();
recipeHelpPanel.BackgroundColor = new Color(13, 12, 14) * 0.995f;
recipeHelpPanel.BorderColor = new Color(111, 42, 49);
recipeHelpTitle = new UIText(string.Empty, 0.9f, true) { HAlign = 0.5f };
recipeHelpTitle.DynamicallyScaleDownToWidth = true;
recipeHelpPanel.Append(recipeHelpTitle);
recipeList = new UIList();
recipeHelpPanel.Append(recipeList);
recipeScrollbar = new UIScrollbar();
recipeHelpPanel.Append(recipeScrollbar);
recipeList.SetScrollbar(recipeScrollbar);
noRecipesText = new UIText(string.Empty, 0.72f) { HAlign = 0.5f };
noRecipesText.DynamicallyScaleDownToWidth = true;
LayoutRecipeHelpPanel();
}
private void BuildCharacterProgressionPanel()
{
characterProgressionPanel = new ReaperProgressionPanel();
characterProgressionPanel.BackgroundColor = new Color(13, 11, 14) * 0.995f;
characterProgressionPanel.BorderColor = new Color(126, 39, 49);
LayoutCharacterProgressionPanel();
}
private void BuildEquipmentProgressionPanel()
{
equipmentProgressionPanel = new AltarEquipmentProgressionPanel(this);
LayoutEquipmentProgressionPanel();
}
private void LayoutCharacterProgressionPanel()
{
if (characterProgressionPanel is null)
return;
characterProgressionPanel.Left.Set((TreeLeft - 6f) * layoutScale, 0f);
characterProgressionPanel.Top.Set(42f * layoutScale, 0f);
characterProgressionPanel.Width.Set(-(TreeLeft - 2f) * layoutScale, 1f);
characterProgressionPanel.Height.Set(-46f * layoutScale, 1f);
characterProgressionPanel.SetPadding(10f * layoutScale);
characterProgressionPanel.ApplyLayoutScale(layoutScale);
}
private void LayoutEquipmentProgressionPanel()
{
if (equipmentProgressionPanel is null)
return;
equipmentProgressionPanel.Left.Set((TreeLeft - 6f) * layoutScale, 0f);
equipmentProgressionPanel.Top.Set(42f * layoutScale, 0f);
equipmentProgressionPanel.Width.Set(-(TreeLeft - 2f) * layoutScale, 1f);
equipmentProgressionPanel.Height.Set(-46f * layoutScale, 1f);
equipmentProgressionPanel.SetPadding(0f);
equipmentProgressionPanel.ApplyLayoutScale(layoutScale);
}
private void LayoutRecipeHelpPanel()
{
if (recipeHelpPanel is null)
return;
recipeHelpPanel.Left.Set((TreeLeft - 6f) * layoutScale, 0f);
recipeHelpPanel.Top.Set(42f * layoutScale, 0f);
recipeHelpPanel.Width.Set(-(TreeLeft - 2f) * layoutScale, 1f);
recipeHelpPanel.Height.Set(-42f * layoutScale, 1f);
recipeHelpPanel.SetPadding(10f * layoutScale);
if (recipeHelpTitle is not null)
{
recipeHelpTitle.HAlign = 0f;
recipeHelpTitle.Top.Set(2f * layoutScale, 0f);
recipeHelpTitle.Width.Set(0f, 1f);
recipeHelpTitle.Height.Set(28f * layoutScale, 0f);
recipeHelpTitle.TextOriginX = 0.5f;
SetConstrainedText(recipeHelpTitle, recipeHelpTitle.Text, 0.9f, large: true);
}
if (recipeList is not null)
{
recipeList.Top.Set(38f * layoutScale, 0f);
recipeList.Width.Set(-20f * layoutScale, 1f);
recipeList.Height.Set(-42f * layoutScale, 1f);
recipeList.ListPadding = 8f * layoutScale;
}
if (recipeScrollbar is not null)
{
recipeScrollbar.Left.Set(-15f * layoutScale, 1f);
recipeScrollbar.Top.Set(38f * layoutScale, 0f);
recipeScrollbar.Width.Set(14f * layoutScale, 0f);
recipeScrollbar.Height.Set(-42f * layoutScale, 1f);
}
if (noRecipesText is not null)
{
noRecipesText.HAlign = 0f;
noRecipesText.Left.Set(10f * layoutScale, 0f);
noRecipesText.Top.Set(82f * layoutScale, 0f);
noRecipesText.Width.Set(-20f * layoutScale, 1f);
noRecipesText.Height.Set(22f * layoutScale, 0f);
noRecipesText.TextOriginX = 0.5f;
SetConstrainedText(noRecipesText, noRecipesText.Text, 0.72f, large: false);
}
}
private void ToggleRecipeHelp()
{
SetRecipeHelpVisible(!recipeHelpVisible, playSound: true);
}
private bool IsCharacterSickleSelected()
{
return selectedEquipment is AltarEquipmentEntry entry && entry.IsCharacterSickle;
}
private void SyncSelectedContentLayer()
{
if (panel is null || characterProgressionPanel is null || equipmentProgressionPanel is null)
return;
bool showCharacterTree = IsCharacterSickleSelected() && !recipeHelpVisible;
bool showEquipmentTree = !IsCharacterSickleSelected()
&& selectedEquipment is not null
&& !recipeHelpVisible;
if (showCharacterTree)
{
if (characterProgressionPanel.Parent is null)
{
characterProgressionPanel.CancelInteraction();
panel.Append(characterProgressionPanel);
}
}
else if (characterProgressionPanel.Parent is not null)
{
characterProgressionPanel.CancelInteraction();
panel.RemoveChild(characterProgressionPanel);
}
if (showEquipmentTree)
{
if (equipmentProgressionPanel.Parent is null)
{
equipmentProgressionPanel.DeactivateInteraction();
panel.Append(equipmentProgressionPanel);
}
}
else if (equipmentProgressionPanel.Parent is not null)
{
equipmentProgressionPanel.DeactivateInteraction();
panel.RemoveChild(equipmentProgressionPanel);
}
}
private void SetRecipeHelpVisible(bool visible, bool playSound)
{
if (visible)
CancelTransientInteraction();
recipeHelpVisible = visible;
if (panel is null || recipeHelpPanel is null)
return;
if (visible)
{
SyncSelectedContentLayer();
RefreshRecipeHelp();
BringRecipeHelpToFront();
}
else if (recipeHelpPanel.Parent is not null)
{
panel.RemoveChild(recipeHelpPanel);
}
if (!visible)
SyncSelectedContentLayer();
recipeHelpButton?.SetText(Language.GetTextValue(visible
? "Mods.SoulHarvest.UI.BackToSkillTree"
: "Mods.SoulHarvest.UI.RecipeHelpButton"));
if (playSound)
SoundEngine.PlaySound(SoundID.MenuTick);
}
private void BringRecipeHelpToFront()
{
if (!recipeHelpVisible || panel is null || recipeHelpPanel is null)
return;
if (recipeHelpPanel.Parent is not null)
panel.RemoveChild(recipeHelpPanel);
panel.Append(recipeHelpPanel);
}
private void RefreshRecipeHelp()
{
if (recipeList is null || recipeHelpPanel is null)
return;
recipeList.Clear();
int recipeCount = 0;
Mod deathMod = ModContent.GetInstance<SoulHarvest>();
for (int index = 0; index < Main.recipe.Length; index++)
{
Recipe recipe = Main.recipe[index];
if (recipe is null
|| recipe.Disabled
|| recipe.createItem.IsAir
|| recipe.createItem.ModItem?.Mod != deathMod)
{
continue;
}
recipeList.Add(new AltarRecipeEntry(recipe, layoutScale));
recipeCount++;
}
if (recipeHelpTitle is not null)
SetConstrainedText(recipeHelpTitle, Language.GetTextValue("Mods.SoulHarvest.UI.RecipeHelpTitle", recipeCount), 0.9f, large: true);
if (noRecipesText is not null)
SetConstrainedText(noRecipesText, Language.GetTextValue("Mods.SoulHarvest.UI.NoModRecipes"), 0.72f, large: false);
if (noRecipesText is not null)
{
if (recipeCount == 0 && noRecipesText.Parent is null)
recipeHelpPanel.Append(noRecipesText);
else if (recipeCount > 0 && noRecipesText.Parent is not null)
recipeHelpPanel.RemoveChild(noRecipesText);
}
}
internal static string FormatPrice(DeathAltarPrice price)
{
string key = price.Currency switch
{
DeathAltarCurrency.Souls => "Souls",
DeathAltarCurrency.SoulEssenceRefund => "SoulEssenceRefund",
_ => "SoulEssence"
};
return Language.GetTextValue($"Mods.SoulHarvest.UI.UpgradeCosts.{key}", price.Amount);
}
}
using SoulHarvest.Common;
using SoulHarvest.Items;
using SoulHarvest.Tiles;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.Audio;
using Terraria.GameContent;
using Terraria.GameContent.UI.Elements;
using Terraria.ID;
using Terraria.Localization;
using Terraria.ModLoader;
using Terraria.UI;
namespace SoulHarvest.UI;
internal class DeathAltarUIState : UIState
{
private const float BasePanelWidth = 1180f;
private const float BasePanelHeight = 700f;
private const float TreeLeft = 228f;
private readonly List<AltarEquipmentEntry> availableEquipment = [];
private DeathAltarFramePanel? panel;
private UIPanel? selectorPanel;
private UIList? itemList;
private UIScrollbar? itemScrollbar;
private UIText? titleText;
private UIText? selectorTitleText;
private UIText? itemText;
private UIText? resourceText;
private AltarTextButton? closeButton;
private AltarTextButton? recipeHelpButton;
private ReaperProgressionPanel? characterProgressionPanel;
private AltarEquipmentProgressionPanel? equipmentProgressionPanel;
private UIPanel? recipeHelpPanel;
private UIList? recipeList;
private UIScrollbar? recipeScrollbar;
private UIText? recipeHelpTitle;
private UIText? noRecipesText;
private bool recipeHelpVisible;
private AltarEquipmentEntry? selectedEquipment;
private int tileX;
private int tileY;
private int itemSignature;
private float layoutScale = 1f;
private int layoutWidth = -1;
private int layoutHeight = -1;
public override void OnInitialize()
{
panel = new DeathAltarFramePanel();
panel.BackgroundColor = new Color(10, 9, 11) * 0.985f;
panel.BorderColor = new Color(116, 34, 43);
Append(panel);
titleText = new UIText(string.Empty, 1.08f, true)
{
DynamicallyScaleDownToWidth = true,
TextOriginX = 0.5f,
TextColor = new Color(232, 219, 204)
};
panel.Append(titleText);
closeButton = new AltarTextButton(
"×",
1f,
new Color(90, 35, 55),
new Color(142, 49, 72),
new Color(180, 82, 105));
closeButton.OnLeftClick += (_, _) =>
{
SoundEngine.PlaySound(SoundID.MenuClose);
ModContent.GetInstance<DeathAltarUISystem>().Hide();
};
panel.Append(closeButton);
recipeHelpButton = new AltarTextButton(
string.Empty,
0.72f,
new Color(42, 35, 39),
new Color(70, 42, 46),
new Color(151, 54, 63));
recipeHelpButton.OnLeftClick += (_, _) => ToggleRecipeHelp();
panel.Append(recipeHelpButton);
selectorTitleText = new UIText(string.Empty, 0.86f, true)
{
DynamicallyScaleDownToWidth = true,
TextColor = new Color(218, 203, 190)
};
panel.Append(selectorTitleText);
selectorPanel = new UIPanel();
selectorPanel.BackgroundColor = new Color(15, 14, 17) * 0.94f;
selectorPanel.BorderColor = new Color(83, 48, 51);
panel.Append(selectorPanel);
itemList = new UIList();
selectorPanel.Append(itemList);
itemScrollbar = new UIScrollbar();
selectorPanel.Append(itemScrollbar);
itemList.SetScrollbar(itemScrollbar);
itemText = new UIText(string.Empty, 0.85f, true)
{
DynamicallyScaleDownToWidth = true,
TextColor = new Color(237, 222, 208)
};
panel.Append(itemText);
resourceText = new UIText(string.Empty, 0.78f)
{
DynamicallyScaleDownToWidth = true,
TextColor = new Color(202, 96, 104)
};
panel.Append(resourceText);
BuildRecipeHelpPanel();
BuildCharacterProgressionPanel();
BuildEquipmentProgressionPanel();
ApplyResponsiveLayout(UserInterface.ActiveInstance.GetDimensions());
}
public override void OnDeactivate()
{
CancelTransientInteraction();
base.OnDeactivate();
}
public override void Recalculate()
{
ApplyResponsiveLayout(UserInterface.ActiveInstance.GetDimensions());
base.Recalculate();
}
private void ApplyResponsiveLayout(CalculatedStyle viewport)
{
if (panel is null)
return;
int viewportWidth = Math.Max(1, (int)MathF.Round(viewport.Width));
int viewportHeight = Math.Max(1, (int)MathF.Round(viewport.Height));
if (viewportWidth == layoutWidth && viewportHeight == layoutHeight)
return;
layoutWidth = viewportWidth;
layoutHeight = viewportHeight;
float widthScale = Math.Max(0.01f, (viewport.Width - 24f) / BasePanelWidth);
float heightScale = Math.Max(0.01f, (viewport.Height - 24f) / BasePanelHeight);
// Active UI dimensions can be as small as 400x300 when the game is run at
// 800x600 with 200% UI scale. Scale against both axes and preserve a small
// safety margin instead of forcing a minimum that can leave the frame off-screen.
layoutScale = Math.Clamp(Math.Min(1f, Math.Min(widthScale, heightScale)), 0.25f, 1f);
float panelWidth = BasePanelWidth * layoutScale;
float panelHeight = BasePanelHeight * layoutScale;
panel.Left.Set(-panelWidth * 0.5f, 0.5f);
panel.Top.Set(-panelHeight * 0.5f, 0.5f);
panel.Width.Set(panelWidth, 0f);
panel.Height.Set(panelHeight, 0f);
panel.SetPadding(14f * layoutScale);
panel.LayoutScale = layoutScale;
panel.SidebarDividerX = 218f * layoutScale;
if (titleText is not null)
{
titleText.Left.Set(240f * layoutScale, 0f);
titleText.Top.Set(4f * layoutScale, 0f);
titleText.Width.Set(-480f * layoutScale, 1f);
titleText.Height.Set(30f * layoutScale, 0f);
SetConstrainedText(titleText, titleText.Text, 1.08f, large: true);
}
LayoutButton(closeButton, -34f, 1f, 0f, 30f, 30f, 1f);
LayoutButton(recipeHelpButton, -156f, 1f, 0f, 116f, 30f, 0.72f);
if (selectorTitleText is not null)
{
selectorTitleText.Left.Set(10f * layoutScale, 0f);
selectorTitleText.Top.Set(49f * layoutScale, 0f);
selectorTitleText.Width.Set(198f * layoutScale, 0f);
selectorTitleText.Height.Set(24f * layoutScale, 0f);
SetConstrainedText(selectorTitleText, selectorTitleText.Text, 0.86f, large: true);
}
if (selectorPanel is not null)
{
selectorPanel.Left.Set(4f * layoutScale, 0f);
selectorPanel.Top.Set(78f * layoutScale, 0f);
selectorPanel.Width.Set(206f * layoutScale, 0f);
selectorPanel.Height.Set(564f * layoutScale, 0f);
selectorPanel.SetPadding(6f * layoutScale);
}
if (itemList is not null)
{
itemList.Width.Set(-18f * layoutScale, 1f);
itemList.Height.Set(0f, 1f);
itemList.ListPadding = 5f * layoutScale;
}
if (itemScrollbar is not null)
{
itemScrollbar.Left.Set(-14f * layoutScale, 1f);
itemScrollbar.Width.Set(14f * layoutScale, 0f);
itemScrollbar.Height.Set(0f, 1f);
}
LayoutTreeHeaderText(itemText, 48f, 0.85f, large: true);
LayoutTreeHeaderText(resourceText, 75f, 0.78f, large: false);
LayoutRecipeHelpPanel();
LayoutCharacterProgressionPanel();
LayoutEquipmentProgressionPanel();
if (availableEquipment.Count > 0)
RebuildSelectors();
if (recipeHelpVisible)
RefreshRecipeHelp();
SyncSelectedContentLayer();
}
private void LayoutTreeHeaderText(UIText? text, float top, float textScale, bool large)
{
if (text is null)
return;
text.Left.Set(TreeLeft * layoutScale, 0f);
text.Top.Set(top * layoutScale, 0f);
text.Width.Set(-(TreeLeft + 8f) * layoutScale, 1f);
text.Height.Set(24f * layoutScale, 0f);
SetConstrainedText(text, text.Text, textScale, large);
}
private void LayoutButton(AltarTextButton? button, float left, float leftPercent, float top, float width, float height, float textScale)
{
if (button is null)
return;
button.Left.Set(left * layoutScale, leftPercent);
button.Top.Set(top * layoutScale, 0f);
button.Width.Set(width * layoutScale, 0f);
button.Height.Set(height * layoutScale, 0f);
button.SetPadding(0f);
button.TextScale = textScale * layoutScale;
}
private void SetConstrainedText(UIText text, string value, float textScale, bool large)
{
text.SetText(value, textScale * layoutScale, large);
text.MinWidth.Set(0f, 0f);
}
public override void Update(GameTime gameTime)
{
base.Update(gameTime);
if (panel?.ContainsPoint(Main.MouseScreen) == true)
Main.LocalPlayer.mouseInterface = true;
Player player = Main.LocalPlayer;
RefreshAvailableEquipment(player);
IDeathAltarUpgradeable? upgradeable = null;
Item? selected = null;
bool characterSickleSelected = selectedEquipment is AltarEquipmentEntry selection && selection.IsCharacterSickle;
if (selectedEquipment is AltarEquipmentEntry selectedEntry && selectedEntry.TryGetDisplayItem(player, out Item resolved))
{
selected = resolved;
if (selectedEntry.IsCharacterDeathDomain)
upgradeable = player.GetModPlayer<MyPlayer>().DeathDomainProgression;
else if (!selectedEntry.IsCharacterSickle)
upgradeable = resolved.ModItem as IDeathAltarUpgradeable;
}
int essenceCount = player.CountItem(ModContent.ItemType<Soul>());
int souls = player.GetModPlayer<MyPlayer>().Souls;
if (!characterSickleSelected
&& selectedEquipment is AltarEquipmentEntry equipmentEntry
&& selected is not null
&& upgradeable is not null
&& equipmentProgressionPanel is not null)
{
equipmentProgressionPanel.Bind(equipmentEntry, selected, upgradeable);
equipmentProgressionPanel.Refresh(selected, upgradeable, essenceCount, souls);
}
if (titleText is not null)
SetConstrainedText(titleText, Language.GetTextValue("Mods.SoulHarvest.UI.AltarTitle"), 1.08f, large: true);
if (selectorTitleText is not null)
SetConstrainedText(selectorTitleText, Language.GetTextValue("Mods.SoulHarvest.UI.SelectEquipment"), 0.86f, large: true);
if (itemText is not null)
SetConstrainedText(itemText, selected is null
? Language.GetTextValue("Mods.SoulHarvest.UI.NoUpgradeableItems")
: Language.GetTextValue("Mods.SoulHarvest.UI.CurrentItem", selected.Name), 0.85f, large: true);
if (resourceText is not null)
SetConstrainedText(resourceText, Language.GetTextValue("Mods.SoulHarvest.UI.AltarResources", essenceCount, souls), 0.78f, large: false);
recipeHelpButton?.SetText(Language.GetTextValue(recipeHelpVisible
? "Mods.SoulHarvest.UI.BackToSkillTree"
: "Mods.SoulHarvest.UI.RecipeHelpButton"));
if (characterSickleSelected)
characterProgressionPanel?.Refresh(player, essenceCount, souls);
SyncSelectedContentLayer();
}
public void SetAltarPosition(int x, int y)
{
CancelTransientInteraction();
tileX = x;
tileY = y;
itemSignature = 0;
selectedEquipment = AltarEquipmentEntry.CreateCharacterSickle();
SetRecipeHelpVisible(false, playSound: false);
SyncSelectedContentLayer();
}
internal void CancelTransientInteraction()
{
characterProgressionPanel?.CancelInteraction();
equipmentProgressionPanel?.DeactivateInteraction();
}
public bool IsPlayerNearAltar()
{
Tile tile = Framing.GetTileSafely(tileX, tileY);
if (!tile.HasTile || tile.TileType != ModContent.TileType<DeathAltarTile>())
return false;
Vector2 altar = new((tileX + 0.5f) * 16f, (tileY + 0.5f) * 16f);
return Vector2.Distance(Main.LocalPlayer.Center, altar) <= 12f * 16f;
}
internal void SelectEquipment(AltarEquipmentEntry entry)
{
if (selectedEquipment is AltarEquipmentEntry current && IsSameEquipmentEntry(current, entry))
return;
CancelTransientInteraction();
selectedEquipment = entry;
SetRecipeHelpVisible(false, playSound: false);
RebuildSelectors();
SyncSelectedContentLayer();
SoundEngine.PlaySound(SoundID.MenuTick);
}
internal void RequestUpgrade(DeathAltarUpgradeType type)
{
if (selectedEquipment?.ItemReference is not DeathAltarItemReference reference)
return;
SoundEngine.PlaySound(SoundID.MenuTick);
ModContent.GetInstance<DeathAltarUISystem>().RequestUpgrade(type, reference, tileX, tileY);
}
private void RefreshAvailableEquipment(Player player)
{
AltarEquipmentEntry characterSickle = AltarEquipmentEntry.CreateCharacterSickle();
AltarEquipmentEntry characterDeathDomain = AltarEquipmentEntry.CreateCharacterDeathDomain();
List<AltarEquipmentEntry> found = [characterSickle, characterDeathDomain];
int signature = HashCode.Combine(17, (int)characterSickle.Kind, characterSickle.ItemType);
signature = HashCode.Combine(signature, (int)characterDeathDomain.Kind, characterDeathDomain.ItemType);
ScanContainer(player.inventory, DeathAltarItemStorage.Inventory, Math.Min(58, player.inventory.Length), found, ref signature);
ScanContainer(player.armor, DeathAltarItemStorage.Armor, player.armor.Length, found, ref signature);
if (signature == itemSignature && found.Count == availableEquipment.Count)
return;
CancelTransientInteraction();
AltarEquipmentEntry? previous = selectedEquipment;
availableEquipment.Clear();
availableEquipment.AddRange(found);
selectedEquipment = previous is AltarEquipmentEntry old ? FindSameEquipmentEntry(old) : null;
selectedEquipment ??= characterSickle;
itemSignature = signature;
RebuildSelectors();
SyncSelectedContentLayer();
}
private static void ScanContainer(Item[] container, DeathAltarItemStorage storage, int accessibleSlots, List<AltarEquipmentEntry> found, ref int signature)
{
for (int slot = 0; slot < accessibleSlots && slot <= byte.MaxValue; slot++)
{
Item item = container[slot];
if (item.IsAir || item.ModItem is not IDeathAltarUpgradeable)
continue;
DeathAltarItemReference reference = new(storage, (byte)slot, item.type);
found.Add(AltarEquipmentEntry.CreateItem(reference));
signature = HashCode.Combine(signature, (int)storage, slot, item.type);
}
}
private AltarEquipmentEntry? FindSameEquipmentEntry(AltarEquipmentEntry old)
{
foreach (AltarEquipmentEntry candidate in availableEquipment)
if (IsSameEquipmentEntry(candidate, old))
return candidate;
return null;
}
private static bool IsSameEquipmentEntry(AltarEquipmentEntry left, AltarEquipmentEntry right)
{
if (left.Kind != right.Kind)
return false;
if (left.IsCharacterProgression)
return true;
return left.ItemReference is DeathAltarItemReference leftReference
&& right.ItemReference is DeathAltarItemReference rightReference
&& leftReference.Storage == rightReference.Storage
&& leftReference.Slot == rightReference.Slot;
}
private void RebuildSelectors()
{
if (itemList is null)
return;
itemList.Clear();
foreach (AltarEquipmentEntry entry in availableEquipment)
{
if (!entry.TryGetDisplayItem(Main.LocalPlayer, out Item item))
continue;
bool isSelected = selectedEquipment is AltarEquipmentEntry current && IsSameEquipmentEntry(current, entry);
itemList.Add(new AltarItemSelector(this, entry, item, isSelected, layoutScale));
}
}
private void BuildRecipeHelpPanel()
{
recipeHelpPanel = new UIPanel();
recipeHelpPanel.BackgroundColor = new Color(13, 12, 14) * 0.995f;
recipeHelpPanel.BorderColor = new Color(111, 42, 49);
recipeHelpTitle = new UIText(string.Empty, 0.9f, true) { HAlign = 0.5f };
recipeHelpTitle.DynamicallyScaleDownToWidth = true;
recipeHelpPanel.Append(recipeHelpTitle);
recipeList = new UIList();
recipeHelpPanel.Append(recipeList);
recipeScrollbar = new UIScrollbar();
recipeHelpPanel.Append(recipeScrollbar);
recipeList.SetScrollbar(recipeScrollbar);
noRecipesText = new UIText(string.Empty, 0.72f) { HAlign = 0.5f };
noRecipesText.DynamicallyScaleDownToWidth = true;
LayoutRecipeHelpPanel();
}
private void BuildCharacterProgressionPanel()
{
characterProgressionPanel = new ReaperProgressionPanel();
characterProgressionPanel.BackgroundColor = new Color(13, 11, 14) * 0.995f;
characterProgressionPanel.BorderColor = new Color(126, 39, 49);
LayoutCharacterProgressionPanel();
}
private void BuildEquipmentProgressionPanel()
{
equipmentProgressionPanel = new AltarEquipmentProgressionPanel(this);
LayoutEquipmentProgressionPanel();
}
private void LayoutCharacterProgressionPanel()
{
if (characterProgressionPanel is null)
return;
characterProgressionPanel.Left.Set((TreeLeft - 6f) * layoutScale, 0f);
characterProgressionPanel.Top.Set(42f * layoutScale, 0f);
characterProgressionPanel.Width.Set(-(TreeLeft - 2f) * layoutScale, 1f);
characterProgressionPanel.Height.Set(-46f * layoutScale, 1f);
characterProgressionPanel.SetPadding(10f * layoutScale);
characterProgressionPanel.ApplyLayoutScale(layoutScale);
}
private void LayoutEquipmentProgressionPanel()
{
if (equipmentProgressionPanel is null)
return;
equipmentProgressionPanel.Left.Set((TreeLeft - 6f) * layoutScale, 0f);
equipmentProgressionPanel.Top.Set(42f * layoutScale, 0f);
equipmentProgressionPanel.Width.Set(-(TreeLeft - 2f) * layoutScale, 1f);
equipmentProgressionPanel.Height.Set(-46f * layoutScale, 1f);
equipmentProgressionPanel.SetPadding(0f);
equipmentProgressionPanel.ApplyLayoutScale(layoutScale);
}
private void LayoutRecipeHelpPanel()
{
if (recipeHelpPanel is null)
return;
recipeHelpPanel.Left.Set((TreeLeft - 6f) * layoutScale, 0f);
recipeHelpPanel.Top.Set(42f * layoutScale, 0f);
recipeHelpPanel.Width.Set(-(TreeLeft - 2f) * layoutScale, 1f);
recipeHelpPanel.Height.Set(-42f * layoutScale, 1f);
recipeHelpPanel.SetPadding(10f * layoutScale);
if (recipeHelpTitle is not null)
{
recipeHelpTitle.HAlign = 0f;
recipeHelpTitle.Top.Set(2f * layoutScale, 0f);
recipeHelpTitle.Width.Set(0f, 1f);
recipeHelpTitle.Height.Set(28f * layoutScale, 0f);
recipeHelpTitle.TextOriginX = 0.5f;
SetConstrainedText(recipeHelpTitle, recipeHelpTitle.Text, 0.9f, large: true);
}
if (recipeList is not null)
{
recipeList.Top.Set(38f * layoutScale, 0f);
recipeList.Width.Set(-20f * layoutScale, 1f);
recipeList.Height.Set(-42f * layoutScale, 1f);
recipeList.ListPadding = 8f * layoutScale;
}
if (recipeScrollbar is not null)
{
recipeScrollbar.Left.Set(-15f * layoutScale, 1f);
recipeScrollbar.Top.Set(38f * layoutScale, 0f);
recipeScrollbar.Width.Set(14f * layoutScale, 0f);
recipeScrollbar.Height.Set(-42f * layoutScale, 1f);
}
if (noRecipesText is not null)
{
noRecipesText.HAlign = 0f;
noRecipesText.Left.Set(10f * layoutScale, 0f);
noRecipesText.Top.Set(82f * layoutScale, 0f);
noRecipesText.Width.Set(-20f * layoutScale, 1f);
noRecipesText.Height.Set(22f * layoutScale, 0f);
noRecipesText.TextOriginX = 0.5f;
SetConstrainedText(noRecipesText, noRecipesText.Text, 0.72f, large: false);
}
}
private void ToggleRecipeHelp()
{
SetRecipeHelpVisible(!recipeHelpVisible, playSound: true);
}
private bool IsCharacterSickleSelected()
{
return selectedEquipment is AltarEquipmentEntry entry && entry.IsCharacterSickle;
}
private void SyncSelectedContentLayer()
{
if (panel is null || characterProgressionPanel is null || equipmentProgressionPanel is null)
return;
bool showCharacterTree = IsCharacterSickleSelected() && !recipeHelpVisible;
bool showEquipmentTree = !IsCharacterSickleSelected()
&& selectedEquipment is not null
&& !recipeHelpVisible;
if (showCharacterTree)
{
if (characterProgressionPanel.Parent is null)
{
characterProgressionPanel.CancelInteraction();
panel.Append(characterProgressionPanel);
}
}
else if (characterProgressionPanel.Parent is not null)
{
characterProgressionPanel.CancelInteraction();
panel.RemoveChild(characterProgressionPanel);
}
if (showEquipmentTree)
{
if (equipmentProgressionPanel.Parent is null)
{
equipmentProgressionPanel.DeactivateInteraction();
panel.Append(equipmentProgressionPanel);
}
}
else if (equipmentProgressionPanel.Parent is not null)
{
equipmentProgressionPanel.DeactivateInteraction();
panel.RemoveChild(equipmentProgressionPanel);
}
}
private void SetRecipeHelpVisible(bool visible, bool playSound)
{
if (visible)
CancelTransientInteraction();
recipeHelpVisible = visible;
if (panel is null || recipeHelpPanel is null)
return;
if (visible)
{
SyncSelectedContentLayer();
RefreshRecipeHelp();
BringRecipeHelpToFront();
}
else if (recipeHelpPanel.Parent is not null)
{
panel.RemoveChild(recipeHelpPanel);
}
if (!visible)
SyncSelectedContentLayer();
recipeHelpButton?.SetText(Language.GetTextValue(visible
? "Mods.SoulHarvest.UI.BackToSkillTree"
: "Mods.SoulHarvest.UI.RecipeHelpButton"));
if (playSound)
SoundEngine.PlaySound(SoundID.MenuTick);
}
private void BringRecipeHelpToFront()
{
if (!recipeHelpVisible || panel is null || recipeHelpPanel is null)
return;
if (recipeHelpPanel.Parent is not null)
panel.RemoveChild(recipeHelpPanel);
panel.Append(recipeHelpPanel);
}
private void RefreshRecipeHelp()
{
if (recipeList is null || recipeHelpPanel is null)
return;
recipeList.Clear();
int recipeCount = 0;
Mod deathMod = ModContent.GetInstance<SoulHarvest>();
for (int index = 0; index < Main.recipe.Length; index++)
{
Recipe recipe = Main.recipe[index];
if (recipe is null
|| recipe.Disabled
|| recipe.createItem.IsAir
|| recipe.createItem.ModItem?.Mod != deathMod)
{
continue;
}
recipeList.Add(new AltarRecipeEntry(recipe, layoutScale));
recipeCount++;
}
if (recipeHelpTitle is not null)
SetConstrainedText(recipeHelpTitle, Language.GetTextValue("Mods.SoulHarvest.UI.RecipeHelpTitle", recipeCount), 0.9f, large: true);
if (noRecipesText is not null)
SetConstrainedText(noRecipesText, Language.GetTextValue("Mods.SoulHarvest.UI.NoModRecipes"), 0.72f, large: false);
if (noRecipesText is not null)
{
if (recipeCount == 0 && noRecipesText.Parent is null)
recipeHelpPanel.Append(noRecipesText);
else if (recipeCount > 0 && noRecipesText.Parent is not null)
recipeHelpPanel.RemoveChild(noRecipesText);
}
}
internal static string FormatPrice(DeathAltarPrice price)
{
string key = price.Currency switch
{
DeathAltarCurrency.Souls => "Souls",
DeathAltarCurrency.SoulEssenceRefund => "SoulEssenceRefund",
_ => "SoulEssence"
};
return Language.GetTextValue($"Mods.SoulHarvest.UI.UpgradeCosts.{key}", price.Amount);
}
}