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 ReaperTreeCanvas : UIElement { private readonly ReaperProgressionPanel owner; private readonly ReaperTreeViewport viewport; private readonly IReadOnlyList nodes; public ReaperTreeCanvas(ReaperProgressionPanel owner, ReaperTreeViewport viewport, IReadOnlyList nodes) { this.owner = owner; this.viewport = viewport; this.nodes = nodes; } protected override void DrawSelf(SpriteBatch spriteBatch) { Texture2D pixel = TextureAssets.MagicPixel.Value; float scale = viewport.ContentScale; DrawRunicBackdrop(spriteBatch, pixel, scale); DrawTotem(spriteBatch, scale); DrawConnections(spriteBatch, pixel, scale); foreach (ReaperTreeNodeModel node in nodes) DrawNode(spriteBatch, pixel, node, scale); if (viewport.HoveredNode is ReaperTreeNodeModel hovered) Main.instance.MouseText(hovered.Title); } private void DrawRunicBackdrop(SpriteBatch batch, Texture2D pixel, float scale) { Color grid = new Color(72, 71, 68) * 0.09f; for (float x = 30f; x < ReaperTreeViewport.ContentWidth; x += 82f) ReaperTreeDraw.DrawLine(batch, pixel, viewport.DesignToScreen(new Vector2(x, 0f)), viewport.DesignToScreen(new Vector2(x, ReaperTreeViewport.ContentHeight)), grid, Math.Max(1f, scale)); for (float y = 26f; y < ReaperTreeViewport.ContentHeight; y += 82f) ReaperTreeDraw.DrawLine(batch, pixel, viewport.DesignToScreen(new Vector2(0f, y)), viewport.DesignToScreen(new Vector2(ReaperTreeViewport.ContentWidth, y)), grid, Math.Max(1f, scale)); Vector2 center = viewport.DesignToScreen(ReaperTreeViewport.ContentCenter); ReaperTreeDraw.DrawRing(batch, pixel, center, 123f * scale, new Color(140, 43, 51) * 0.19f, Math.Max(1f, scale), 48); ReaperTreeDraw.DrawRing(batch, pixel, center, 454f * scale, new Color(120, 109, 103) * 0.12f, Math.Max(1f, scale), 72); for (int index = 0; index < 12; index++) { float angle = MathHelper.TwoPi * index / 12f; Vector2 rune = center + new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * 123f * scale; ReaperTreeDraw.DrawDiamond(batch, pixel, rune, 3.5f * scale, index % 2 == 0 ? new Color(185, 51, 60) * 0.42f : new Color(130, 119, 111) * 0.28f); } } private void DrawTotem(SpriteBatch batch, float scale) { Texture2D texture = ReaperUIData.GetFormTexture(ReaperFormId.Base); Rectangle frame = texture.Frame(); Vector2 center = viewport.DesignToScreen(ReaperTreeViewport.ContentCenter + new Vector2(0f, -16f)); float drawScale = Math.Min(1f, 360f / Math.Max(frame.Width, frame.Height)) * scale; batch.Draw(texture, center, frame, new Color(180, 187, 179) * 0.11f, -0.55f, frame.Size() * 0.5f, drawScale, SpriteEffects.None, 0f); } private void DrawConnections(SpriteBatch batch, Texture2D pixel, float scale) { ReaperTreeNodeModel root = Find(ReaperTreeNodeKind.Base, ReaperFormId.Base, 0)!; ReaperTreeNodeModel death = Find(ReaperTreeNodeKind.Death, ReaperFormId.Death, 0)!; foreach (ReaperCommonNode common in Enum.GetValues()) { if (ReaperDefinitions.IsDeathNode(common)) continue; ReaperTreeNodeModel commonNode = FindCommon(common)!; DrawPath(batch, pixel, root, commonNode, commonNode.Lit, new Color(190, 188, 174), scale, 0.62f); } foreach (ReaperCommonNode deathUpgrade in Enum.GetValues()) { if (!ReaperDefinitions.IsDeathNode(deathUpgrade)) continue; ReaperTreeNodeModel upgradeNode = FindCommon(deathUpgrade)!; DrawPath(batch, pixel, death, upgradeNode, upgradeNode.Lit, ReaperUIData.GetPrimaryColor(ReaperFormId.Death), scale, 0.64f); } foreach (ReaperFormId form in ReaperDefinitions.BranchForms) { Color color = ReaperUIData.GetPrimaryColor(form); ReaperTreeNodeModel previousStage = root; ReaperTreeNodeModel? previousSkill = null; for (int stage = 1; stage <= 3; stage++) { ReaperTreeNodeModel stageNode = Find(ReaperTreeNodeKind.Stage, form, stage)!; ReaperTreeNodeModel skillNode = Find(ReaperTreeNodeKind.Skill, form, stage - 1)!; DrawPath(batch, pixel, previousStage, stageNode, stageNode.Lit, color, scale, 1f); DrawPath(batch, pixel, stageNode, skillNode, skillNode.Lit, color, scale, 0.78f); if (previousSkill is not null) DrawPath(batch, pixel, previousSkill, skillNode, skillNode.CurrentLevel >= ReaperDefinitions.MaximumSkillLevel, color, scale, 0.48f); previousStage = stageNode; previousSkill = skillNode; } bool mastered = owner.IsBranchMastered(form); DrawPath(batch, pixel, previousStage, death, mastered, color, scale, 0.62f); if (previousSkill is not null) DrawPath(batch, pixel, previousSkill, death, mastered, color, scale, 0.42f); } } private void DrawPath(SpriteBatch batch, Texture2D pixel, ReaperTreeNodeModel start, ReaperTreeNodeModel end, bool lit, Color color, float scale, float opacity) { Vector2 a = viewport.DesignToScreen(start.Position); Vector2 b = viewport.DesignToScreen(end.Position); Vector2 delta = b - a; if (delta.LengthSquared() < 1f) return; Vector2 direction = Vector2.Normalize(delta); a += direction * start.Radius * scale; b -= direction * end.Radius * scale; Color line = lit ? color * opacity : new Color(65, 61, 62) * (opacity * 0.58f); ReaperTreeDraw.DrawLine(batch, pixel, a, b, new Color(7, 7, 9) * Math.Min(0.94f, opacity + 0.2f), Math.Max(2f, 5f * scale)); ReaperTreeDraw.DrawLine(batch, pixel, a, b, line, Math.Max(1f, (lit ? 2.3f : 1.2f) * scale)); ReaperTreeDraw.DrawDiamond(batch, pixel, Vector2.Lerp(a, b, 0.5f), (lit ? 4.4f : 3f) * scale, line * 1.18f); } private void DrawNode(SpriteBatch batch, Texture2D pixel, ReaperTreeNodeModel node, float scale) { Vector2 center = viewport.DesignToScreen(node.Position); float radius = node.Radius * scale; Color primary = node.Kind == ReaperTreeNodeKind.Common ? new Color(196, 194, 177) : ReaperUIData.GetPrimaryColor(node.Form); bool selected = ReferenceEquals(viewport.SelectedNode, node) || viewport.SelectedNode is null && node.Kind == ReaperTreeNodeKind.Base; bool hovered = ReferenceEquals(viewport.HoveredNode, node); Color stateColor = node.Lit ? primary : node.CanRequest && node.Affordable ? primary * 0.82f : node.CanRequest ? new Color(180, 57, 65) : new Color(75, 70, 72); float pulse = 0.86f + MathF.Sin(Main.GlobalTimeWrappedHourly * 3.2f + node.Position.X * 0.01f) * 0.1f; ReaperTreeDraw.DrawDiamond(batch, pixel, center, radius * 0.88f, new Color(8, 8, 11) * 0.98f); if (node.Kind is ReaperTreeNodeKind.Base or ReaperTreeNodeKind.Death) { ReaperTreeDraw.DrawRing(batch, pixel, center, radius, stateColor * (node.Lit ? pulse : 0.52f), Math.Max(1f, 2.6f * scale), 30); ReaperTreeDraw.DrawRing(batch, pixel, center, radius - 6f * scale, new Color(104, 91, 84) * 0.56f, Math.Max(1f, scale), 24); } else if (node.Kind == ReaperTreeNodeKind.Skill) ReaperTreeDraw.DrawRing(batch, pixel, center, radius, stateColor * (node.Lit ? pulse : 0.58f), Math.Max(1f, 2f * scale), 20); else ReaperTreeDraw.DrawDiamondOutline(batch, pixel, center, radius, stateColor * (node.Lit ? pulse : 0.62f), Math.Max(1f, 2f * scale)); if (selected || hovered) ReaperTreeDraw.DrawRing(batch, pixel, center, radius + (selected ? 7f : 4f) * scale, selected ? Color.White * 0.72f : stateColor * 0.62f, Math.Max(1f, (selected ? 2f : 1f) * scale), 28); DrawNodeContent(batch, node, center, radius, primary, scale); DrawNodeLevels(batch, pixel, node, center + new Vector2(0f, radius + 9f * scale), primary, scale); if (node.Kind == ReaperTreeNodeKind.Stage && node.Index == 1) DrawBranchMarker(batch, pixel, node, center, radius, primary, scale); if (node.Kind == ReaperTreeNodeKind.Stage && node.Index == 3) { float labelScale = 0.48f * scale; string label = AltarDrawHelpers.FitText(ReaperUIData.GetFormName(node.Form), 112f * scale, labelScale); Vector2 labelSize = FontAssets.MouseText.Value.MeasureString(label) * labelScale; Utils.DrawBorderString(batch, label, center + new Vector2(-labelSize.X * 0.5f, radius + 19f * scale), primary * (node.Lit ? 1f : 0.62f), labelScale); } } private void DrawNodeContent(SpriteBatch batch, ReaperTreeNodeModel node, Vector2 center, float radius, Color color, float scale) { if (node.Kind is ReaperTreeNodeKind.Base or ReaperTreeNodeKind.Death || node.Kind == ReaperTreeNodeKind.Stage && node.Index == 3) { Texture2D texture = ReaperUIData.GetFormTexture(node.Form); Rectangle frame = texture.Frame(); float maximum = node.Kind is ReaperTreeNodeKind.Base or ReaperTreeNodeKind.Death ? radius * 1.3f : radius * 1.12f; float iconScale = Math.Min(1f, maximum / Math.Max(frame.Width, frame.Height)); Color tint = node.Lit || node.Kind == ReaperTreeNodeKind.Base ? Color.White : new Color(92, 88, 91); batch.Draw(texture, center, frame, tint, 0f, frame.Size() * 0.5f, iconScale, SpriteEffects.None, 0f); return; } Color runeColor = node.Lit ? ReaperUIData.GetSecondaryColor(node.Form) : color * 0.68f; if (node.Kind == ReaperTreeNodeKind.Skill) { ReaperTreeDraw.DrawSkillRune(batch, TextureAssets.MagicPixel.Value, center, radius, node.Form, node.Index, runeColor, scale); return; } if (node.Kind is ReaperTreeNodeKind.Common or ReaperTreeNodeKind.DeathUpgrade) { ReaperTreeDraw.DrawCommonRune(batch, TextureAssets.MagicPixel.Value, center, radius, node.CommonNode, runeColor, scale); return; } string glyph = node.Kind == ReaperTreeNodeKind.Stage ? ReaperTreeDraw.Roman(node.Index) : string.Empty; float textScale = 0.62f * scale; Vector2 size = FontAssets.MouseText.Value.MeasureString(glyph) * textScale; Utils.DrawBorderString(batch, glyph, center - size * 0.5f, runeColor, textScale); } private static void DrawBranchMarker(SpriteBatch batch, Texture2D pixel, ReaperTreeNodeModel node, Vector2 center, float radius, Color color, float scale) { Vector2 outward = (node.Position - ReaperTreeViewport.ContentCenter).SafeNormalize(Vector2.UnitY); Vector2 tangent = new(-outward.Y, outward.X); Vector2 markerCenter = center + outward * (radius + 28f * scale); float markerRadius = 12f * scale; ReaperTreeDraw.DrawDiamond(batch, pixel, markerCenter, markerRadius * 0.82f, new Color(7, 7, 9) * 0.98f); ReaperTreeDraw.DrawRing(batch, pixel, markerCenter, markerRadius, color * (node.Lit ? 0.94f : 0.58f), Math.Max(1f, 1.7f * scale), 18); Texture2D texture = ReaperUIData.GetFormTexture(node.Form); Rectangle frame = texture.Frame(); float iconScale = Math.Min(1f, 17f * scale / Math.Max(frame.Width, frame.Height)); batch.Draw(texture, markerCenter, frame, node.Lit ? Color.White : new Color(116, 109, 109), 0f, frame.Size() * 0.5f, iconScale, SpriteEffects.None, 0f); string label = ReaperUIData.GetBranchLabel(node.Form); float labelScale = 0.46f * scale; Vector2 labelCenter = markerCenter - tangent * (28f * scale); Vector2 labelSize = FontAssets.MouseText.Value.MeasureString(label) * labelScale; Utils.DrawBorderString(batch, label, labelCenter - labelSize * 0.5f, color * (node.Lit ? 1f : 0.72f), labelScale); } private static void DrawNodeLevels(SpriteBatch batch, Texture2D pixel, ReaperTreeNodeModel node, Vector2 center, Color color, float scale) { if (node.Kind is ReaperTreeNodeKind.Base or ReaperTreeNodeKind.Death) return; bool unlimited = (node.Kind is ReaperTreeNodeKind.Common or ReaperTreeNodeKind.DeathUpgrade) && ReaperDefinitions.IsUnlimitedCommonNode(node.CommonNode); int maximum = unlimited ? 8 : Math.Min(node.MaximumLevel, 10); int filledLevels = unlimited && node.CurrentLevel > 0 ? (node.CurrentLevel - 1) % maximum + 1 : node.CurrentLevel; float spacing = maximum > 5 ? 8f * scale : 10f * scale; float startX = center.X - (maximum - 1) * spacing * 0.5f; for (int level = 0; level < maximum; level++) ReaperTreeDraw.DrawDiamond(batch, pixel, new Vector2(startX + level * spacing, center.Y), (level < filledLevels ? 3.2f : 2.7f) * scale, level < filledLevels ? color : new Color(55, 52, 55)); } private ReaperTreeNodeModel? Find(ReaperTreeNodeKind kind, ReaperFormId form, int index) { foreach (ReaperTreeNodeModel node in nodes) if (node.Kind == kind && node.Form == form && node.Index == index) return node; return null; } private ReaperTreeNodeModel? FindCommon(ReaperCommonNode common) { foreach (ReaperTreeNodeModel node in nodes) if ((node.Kind is ReaperTreeNodeKind.Common or ReaperTreeNodeKind.DeathUpgrade) && node.CommonNode == common) return node; return null; } }