using SoulHarvest.Common; using SoulHarvest.Items; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using ReLogic.Content; using System; using System.Collections.Generic; using System.Text; using Terraria; using Terraria.Audio; using Terraria.GameContent; using Terraria.GameContent.UI.Elements; using Terraria.GameInput; using Terraria.ID; using Terraria.Localization; using Terraria.ModLoader; using Terraria.UI; namespace SoulHarvest.UI; internal sealed class ReaperProgressionPanel : UIPanel { private const float TopBarHeight = 52f; private const float DetailWidth = 246f; private readonly List nodes = []; private readonly ReaperTreeViewport viewport; private readonly ReaperTreeDetailPanel detailPanel; private ReaperTreeNodeModel selectedNode = null!; private ReaperProgressionState? progression; private int essenceCount; private int souls; private float layoutScale = 1f; public ReaperProgressionPanel() { SetPadding(0f); BuildPanorama(); viewport = new ReaperTreeViewport(this, nodes); Append(viewport); detailPanel = new ReaperTreeDetailPanel(this); Append(detailPanel); selectedNode = nodes.Find(node => node.Kind == ReaperTreeNodeKind.Base)!; detailPanel.SetNode(selectedNode); ApplyLayoutScale(1f); } public void ApplyLayoutScale(float scale) { layoutScale = Math.Clamp(scale, 0.4f, 1f); SetPadding(0f); 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 void Refresh(Player player, int essenceCount, int souls) { progression = player.GetModPlayer().ReaperProgression; this.essenceCount = essenceCount; this.souls = souls; foreach (ReaperTreeNodeModel node in nodes) node.Refresh(player, progression, essenceCount, souls); detailPanel.SetNode(selectedNode); detailPanel.RefreshActionState(); } public override void Recalculate() { base.Recalculate(); viewport.EnsureCameraIsValid(); } internal void CancelInteraction() { viewport.CancelInteraction(); } internal void SelectNode(ReaperTreeNodeModel node) { if (selectedNode == node) return; selectedNode = node; detailPanel.SetNode(node); detailPanel.RefreshActionState(); SoundEngine.PlaySound(SoundID.MenuTick); } internal void RequestSelectedUpgrade() { if (!selectedNode.CanRequest || !selectedNode.Affordable) return; SoundEngine.PlaySound(SoundID.MenuTick); selectedNode.RequestUpgrade(); } internal bool IsBranchMastered(ReaperFormId form) { if (progression is null || progression.GetStage(form) < ReaperStage.StageIII) return false; for (int skill = 0; skill < ReaperDefinitions.SkillsPerBranch; skill++) if (progression.GetSkillLevel(form, skill) < ReaperDefinitions.MaximumSkillLevel) return false; return true; } protected override void DrawSelf(SpriteBatch spriteBatch) { base.DrawSelf(spriteBatch); Rectangle bounds = GetDimensions().ToRectangle(); Texture2D pixel = TextureAssets.MagicPixel.Value; int scaleOne = Math.Max(1, (int)MathF.Round(layoutScale)); spriteBatch.Draw(pixel, new Rectangle(bounds.X + scaleOne, bounds.Y + scaleOne, Math.Max(1, bounds.Width - scaleOne * 2), Math.Max(1, (int)(TopBarHeight * layoutScale))), new Color(10, 9, 12) * 0.98f); spriteBatch.Draw(pixel, new Rectangle(bounds.X + (int)(8f * layoutScale), bounds.Y + (int)(TopBarHeight * layoutScale), bounds.Width - (int)(16f * layoutScale), Math.Max(1, (int)(2f * layoutScale))), new Color(140, 39, 49) * 0.72f); ReaperTreeDraw.DrawInsetBorder(spriteBatch, pixel, bounds, Math.Max(2, (int)(4f * layoutScale)), new Color(55, 51, 47), Math.Max(1, (int)(2f * layoutScale))); ReaperTreeDraw.DrawInsetBorder(spriteBatch, pixel, bounds, Math.Max(5, (int)(8f * layoutScale)), new Color(128, 43, 50) * 0.7f, 1); float titleScale = 0.82f * layoutScale; string title = Language.GetTextValue("Mods.SoulHarvest.UI.ReaperTree.EquipmentProgressionTitle"); float titleMaximumWidth = 176f * layoutScale; float measuredTitleWidth = FontAssets.MouseText.Value.MeasureString(title).X * titleScale; if (measuredTitleWidth > titleMaximumWidth && measuredTitleWidth > 0f) titleScale *= titleMaximumWidth / measuredTitleWidth; Utils.DrawBorderString(spriteBatch, title, new Vector2(bounds.X + 18f * layoutScale, bounds.Y + 15f * layoutScale), new Color(229, 218, 203), titleScale); int sealI = progression?.GetSoulSealAvailable(ReaperStage.StageI) ?? 0; int sealII = progression?.GetSoulSealAvailable(ReaperStage.StageII) ?? 0; int sealIII = progression?.GetSoulSealAvailable(ReaperStage.StageIII) ?? 0; float resourcesLeft = bounds.X + 205f * layoutScale; float resourcesRight = bounds.Right - 12f * layoutScale; float resourceWidth = Math.Max(28f * layoutScale, (resourcesRight - resourcesLeft) / 5f); DrawResourceCell(spriteBatch, resourcesLeft + resourceWidth * 0f, bounds.Y + 9f * layoutScale, resourceWidth, new Color(177, 61, 225), Language.GetTextValue("Mods.SoulHarvest.UI.ReaperTree.ResourceEssence", essenceCount)); DrawResourceCell(spriteBatch, resourcesLeft + resourceWidth * 1f, bounds.Y + 9f * layoutScale, resourceWidth, new Color(72, 214, 235), Language.GetTextValue("Mods.SoulHarvest.UI.ReaperTree.ResourceSouls", souls)); DrawResourceCell(spriteBatch, resourcesLeft + resourceWidth * 2f, bounds.Y + 9f * layoutScale, resourceWidth, new Color(116, 220, 177), Language.GetTextValue("Mods.SoulHarvest.UI.ReaperTree.ResourceSealI", sealI)); DrawResourceCell(spriteBatch, resourcesLeft + resourceWidth * 3f, bounds.Y + 9f * layoutScale, resourceWidth, new Color(241, 126, 62), Language.GetTextValue("Mods.SoulHarvest.UI.ReaperTree.ResourceSealII", sealII)); DrawResourceCell(spriteBatch, resourcesLeft + resourceWidth * 4f, bounds.Y + 9f * layoutScale, resourceWidth, new Color(229, 48, 67), Language.GetTextValue("Mods.SoulHarvest.UI.ReaperTree.ResourceSealIII", sealIII)); } 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 - 4f * layoutScale)), Math.Max(1, (int)(34f * layoutScale))); batch.Draw(pixel, cell, new Color(21, 19, 23) * 0.92f); ReaperTreeDraw.DrawBorder(batch, pixel, cell, new Color(67, 58, 58), 1); Vector2 runeCenter = new(cell.X + 12f * layoutScale, cell.Center.Y); ReaperTreeDraw.DrawDiamond(batch, pixel, runeCenter, 6f * layoutScale, color * 0.85f); ReaperTreeDraw.DrawDiamond(batch, pixel, runeCenter, 3f * layoutScale, new Color(244, 230, 214)); float textScale = 0.54f * layoutScale; string fitted = AltarDrawHelpers.FitText(text, Math.Max(6f, cell.Width - 25f * layoutScale), textScale); Utils.DrawBorderString(batch, fitted, new Vector2(cell.X + 22f * layoutScale, cell.Y + 9f * layoutScale), color, textScale); } private void BuildPanorama() { Vector2 center = ReaperTreeViewport.ContentCenter; nodes.Add(ReaperTreeNodeModel.Base(center)); ReaperCommonNode[] commonNodes = Array.FindAll( Enum.GetValues(), node => !ReaperDefinitions.IsDeathNode(node)); for (int index = 0; index < commonNodes.Length; index++) { float angle = -MathHelper.PiOver2 + MathHelper.TwoPi * index / commonNodes.Length; nodes.Add(ReaperTreeNodeModel.Common(commonNodes[index], center + new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * 91f)); } float[] angles = [-160f, -105f, -45f, 15f, 75f, 135f]; for (int branch = 0; branch < ReaperDefinitions.BranchForms.Count; branch++) { ReaperFormId form = ReaperDefinitions.BranchForms[branch]; float angle = MathHelper.ToRadians(angles[branch]); Vector2 direction = new(MathF.Cos(angle), MathF.Sin(angle)); Vector2 tangent = new(-direction.Y, direction.X); for (int stage = 1; stage <= 3; stage++) { float radius = 174f + (stage - 1) * 132f; Vector2 stagePosition = center + direction * radius; Vector2 skillPosition = stagePosition + tangent * (72f * (stage == 2 ? -1f : 1f)); nodes.Add(ReaperTreeNodeModel.Stage(form, stage, stagePosition)); nodes.Add(ReaperTreeNodeModel.Skill(form, stage - 1, skillPosition)); } } // The final form is the trunk for its own ascension tree. Keep all six // Death upgrades visibly above it so the hierarchy reads from the // branches into Death, then upward into its uncapped progression. Vector2 deathRoot = center + new Vector2(70f, -438f); nodes.Add(ReaperTreeNodeModel.Death(deathRoot)); ReaperCommonNode[] deathNodes = Array.FindAll( Enum.GetValues(), ReaperDefinitions.IsDeathNode); for (int index = 0; index < deathNodes.Length; index++) { int column = index % 3; int row = index / 3; Vector2 position = deathRoot + new Vector2((column - 1) * 154f, -105f - row * 91f); nodes.Add(ReaperTreeNodeModel.DeathUpgrade(deathNodes[index], position)); } } }