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 AltarEquipmentTreeViewport : UIElement { private const float EdgeMargin = 96f; private const float DragThresholdScreenPixels = 5f; private readonly AltarEquipmentProgressionPanel owner; private readonly IReadOnlyList nodes; private readonly float contentWidth; private readonly float contentHeight; private readonly ReaperTreeButton resetButton; private readonly AltarEquipmentTreeViewportOverlay overlay; private AltarEquipmentDragState dragState; private AltarEquipmentTreeNode? pressedNode; private AltarEquipmentTreeNode? hoveredNode; private Vector2 camera; private Vector2 dragStartMouse; private Vector2 dragStartCamera; private float layoutScale = 1f; private bool previousLeft; private bool previousMiddle; private bool cameraInitialized; internal float LayoutScale => layoutScale; internal AltarEquipmentTreeNode? HoveredNode => hoveredNode; internal AltarEquipmentTreeNode? SelectedNode { get; private set; } public AltarEquipmentTreeViewport(AltarEquipmentProgressionPanel owner, IReadOnlyList nodes, float contentWidth, float contentHeight) { this.owner = owner; this.nodes = nodes; this.contentWidth = contentWidth; this.contentHeight = contentHeight; OverflowHidden = true; SetPadding(0f); AltarEquipmentTreeCanvas canvas = new(this, nodes, contentWidth, contentHeight) { IgnoresMouseInteraction = true }; canvas.Width.Set(0f, 1f); canvas.Height.Set(0f, 1f); Append(canvas); overlay = new AltarEquipmentTreeViewportOverlay(this) { IgnoresMouseInteraction = true }; overlay.Width.Set(0f, 1f); overlay.Height.Set(0f, 1f); Append(overlay); resetButton = new ReaperTreeButton(ResetCamera, new Color(45, 37, 38), new Color(99, 42, 47), new Color(207, 70, 77)); Append(resetButton); CancelInteraction(); } public void SetSelectedNode(AltarEquipmentTreeNode? node) => SelectedNode = node; public void ApplyLayoutScale(float scale) { layoutScale = Math.Clamp(scale, 0.25f, 1f); resetButton.Left.Set(-91f * layoutScale, 1f); resetButton.Top.Set(8f * layoutScale, 0f); resetButton.Width.Set(82f * layoutScale, 0f); resetButton.Height.Set(27f * layoutScale, 0f); resetButton.ApplyLayoutScale(layoutScale, 0.52f); resetButton.SetText(Language.GetTextValue("Mods.SoulHarvest.UI.ReaperTree.ResetView")); } public override void Recalculate() { base.Recalculate(); EnsureCameraIsValid(); } public void EnsureCameraIsValid() { CalculatedStyle bounds = GetDimensions(); if (bounds.Width <= 1f || bounds.Height <= 1f) return; if (!cameraInitialized) ResetCamera(playSound: false); else camera = ClampCamera(camera); } public void CancelInteraction() { dragState = AltarEquipmentDragState.None; pressedNode = null; hoveredNode = null; previousLeft = Main.mouseLeft; previousMiddle = Main.mouseMiddle; } public override void Update(GameTime gameTime) { base.Update(gameTime); resetButton.SetText(Language.GetTextValue("Mods.SoulHarvest.UI.ReaperTree.ResetView")); Vector2 mouse = Main.MouseScreen; bool inside = ContainsPoint(mouse); bool left = Main.mouseLeft; bool middle = Main.mouseMiddle; bool leftPressed = left && !previousLeft; bool leftReleased = !left && previousLeft; bool middlePressed = middle && !previousMiddle; bool fixedOverlayHovered = IsFixedOverlayHovered(mouse); if (inside || dragState != AltarEquipmentDragState.None) Main.LocalPlayer.mouseInterface = true; hoveredNode = inside && !fixedOverlayHovered && dragState is AltarEquipmentDragState.None or AltarEquipmentDragState.PendingLeft ? HitTest(mouse) : null; if (middlePressed && inside && !fixedOverlayHovered) { dragState = AltarEquipmentDragState.DraggingMiddle; pressedNode = null; dragStartMouse = mouse; dragStartCamera = camera; } else if (leftPressed && inside && dragState == AltarEquipmentDragState.None && !fixedOverlayHovered) { dragState = AltarEquipmentDragState.PendingLeft; pressedNode = HitTest(mouse); dragStartMouse = mouse; dragStartCamera = camera; } if (dragState == AltarEquipmentDragState.PendingLeft && left && Vector2.DistanceSquared(mouse, dragStartMouse) >= DragThresholdScreenPixels * DragThresholdScreenPixels) { dragState = AltarEquipmentDragState.DraggingLeft; } if (dragState == AltarEquipmentDragState.DraggingLeft) { if (left) camera = ClampCamera(dragStartCamera + (mouse - dragStartMouse) / layoutScale); else EndDrag(); } else if (dragState == AltarEquipmentDragState.DraggingMiddle) { if (middle) camera = ClampCamera(dragStartCamera + (mouse - dragStartMouse) / layoutScale); else EndDrag(); } else if (dragState == AltarEquipmentDragState.PendingLeft && leftReleased) { AltarEquipmentTreeNode? released = inside ? HitTest(mouse) : null; bool stayedWithinClickThreshold = Vector2.DistanceSquared(mouse, dragStartMouse) < DragThresholdScreenPixels * DragThresholdScreenPixels; if (stayedWithinClickThreshold && !fixedOverlayHovered && pressedNode is not null && ReferenceEquals(pressedNode, released)) { SelectedNode = pressedNode; owner.SelectNode(pressedNode); } EndDrag(); } previousLeft = left; previousMiddle = middle; } protected override void DrawSelf(SpriteBatch spriteBatch) { Rectangle bounds = GetDimensions().ToRectangle(); Texture2D pixel = TextureAssets.MagicPixel.Value; spriteBatch.Draw(pixel, bounds, new Color(8, 9, 11) * 0.995f); } internal Vector2 DesignToScreen(Vector2 design) => GetDimensions().Position() + (design + camera) * layoutScale; internal Rectangle GetHintStripBounds() { Rectangle bounds = GetDimensions().ToRectangle(); return new Rectangle( bounds.X + Math.Max(2, (int)(8f * layoutScale)), bounds.Bottom - Math.Max(8, (int)(28f * layoutScale)), Math.Max(1, bounds.Width - Math.Max(4, (int)(16f * layoutScale))), Math.Max(6, (int)(20f * layoutScale))); } private bool IsFixedOverlayHovered(Vector2 mouse) { return resetButton.ContainsPoint(mouse) || GetHintStripBounds().Contains(mouse.ToPoint()); } private AltarEquipmentTreeNode? HitTest(Vector2 mouse) { if (!ContainsPoint(mouse)) return null; Vector2 design = (mouse - GetDimensions().Position()) / layoutScale - camera; for (int index = nodes.Count - 1; index >= 0; index--) { AltarEquipmentTreeNode node = nodes[index]; float radius = node.Radius + 7f; if (Vector2.DistanceSquared(design, node.Position) <= radius * radius) return node; } return null; } private void ResetCamera() => ResetCamera(playSound: true); private void ResetCamera(bool playSound) { CalculatedStyle bounds = GetDimensions(); if (bounds.Width <= 1f || bounds.Height <= 1f) return; Vector2 viewportSize = new(bounds.Width / layoutScale, bounds.Height / layoutScale); Vector2 focus = new(contentWidth * 0.5f, Math.Min(contentHeight * 0.5f, 420f)); camera = ClampCamera(viewportSize * 0.5f - focus); cameraInitialized = true; EndDrag(); if (playSound) SoundEngine.PlaySound(SoundID.MenuTick); } private Vector2 ClampCamera(Vector2 value) { CalculatedStyle bounds = GetDimensions(); Vector2 viewportSize = new(Math.Max(1f, bounds.Width / layoutScale), Math.Max(1f, bounds.Height / layoutScale)); float minX = viewportSize.X - EdgeMargin - contentWidth; float maxX = EdgeMargin; float minY = viewportSize.Y - EdgeMargin - contentHeight; float maxY = EdgeMargin; value.X = minX > maxX ? (viewportSize.X - contentWidth) * 0.5f : Math.Clamp(value.X, minX, maxX); value.Y = minY > maxY ? (viewportSize.Y - contentHeight) * 0.5f : Math.Clamp(value.Y, minY, maxY); return value; } private void EndDrag() { dragState = AltarEquipmentDragState.None; pressedNode = null; } }