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 ReaperTreeViewport : UIElement { public const float ContentWidth = 1580f; public const float ContentHeight = 1320f; public static readonly Vector2 ContentCenter = new(790f, 768f); private const float EdgeMargin = 96f; private const float DragThreshold = 5f; private const float MinimumZoom = 0.55f; private const float MaximumZoom = 1.75f; private const float ZoomStep = 1.12f; private readonly ReaperProgressionPanel owner; private readonly IReadOnlyList nodes; private readonly ReaperTreeButton resetButton; private ReaperViewportDragState dragState; private ReaperTreeNodeModel? pressedNode; private ReaperTreeNodeModel? hoveredNode; private Vector2 camera; private Vector2 dragStartMouse; private Vector2 dragStartCamera; private float layoutScale = 1f; private float contentZoom = 1f; private bool previousLeft; private bool previousMiddle; private bool cameraInitialized; internal float LayoutScale => layoutScale; internal float ContentScale => layoutScale * contentZoom; internal ReaperTreeNodeModel? HoveredNode => hoveredNode; internal ReaperTreeNodeModel? SelectedNode { get; private set; } public ReaperTreeViewport(ReaperProgressionPanel owner, IReadOnlyList nodes) { this.owner = owner; this.nodes = nodes; OverflowHidden = true; SetPadding(0f); ReaperTreeCanvas canvasLayer = new(owner, this, nodes) { IgnoresMouseInteraction = true }; canvasLayer.Width.Set(0f, 1f); canvasLayer.Height.Set(0f, 1f); Append(canvasLayer); ReaperTreeViewportOverlay overlayLayer = new(this) { IgnoresMouseInteraction = true }; overlayLayer.Width.Set(0f, 1f); overlayLayer.Height.Set(0f, 1f); Append(overlayLayer); // Appended last so it remains both visible and interactive above the // decorative overlay and the panning canvas. resetButton = new ReaperTreeButton(ResetCamera, new Color(45, 38, 38), new Color(99, 45, 48), new Color(208, 76, 78)); Append(resetButton); } public void ApplyLayoutScale(float scale) { layoutScale = Math.Clamp(scale, 0.4f, 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 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 resetHovered = resetButton.IsMouseHovering; int wheelDelta = PlayerInput.ScrollWheelDeltaForUI; if (inside && !resetHovered && dragState == ReaperViewportDragState.None && wheelDelta != 0) { ZoomAt(mouse, wheelDelta); PlayerInput.ScrollWheelDeltaForUI = 0; } if (inside || dragState != ReaperViewportDragState.None) Main.LocalPlayer.mouseInterface = true; hoveredNode = inside && !resetHovered && (dragState is ReaperViewportDragState.None or ReaperViewportDragState.PendingLeft) ? HitTest(mouse) : null; if (middlePressed && inside && !resetHovered) { dragState = ReaperViewportDragState.DraggingMiddle; pressedNode = null; dragStartMouse = mouse; dragStartCamera = camera; } else if (leftPressed && inside && dragState == ReaperViewportDragState.None && !resetHovered) { dragState = ReaperViewportDragState.PendingLeft; pressedNode = HitTest(mouse); dragStartMouse = mouse; dragStartCamera = camera; } if (dragState == ReaperViewportDragState.PendingLeft && left) { if (ExceededDragThreshold(mouse)) dragState = ReaperViewportDragState.DraggingLeft; } if (dragState == ReaperViewportDragState.DraggingLeft) { if (left) camera = ClampCamera(dragStartCamera + (mouse - dragStartMouse) / ContentScale); else EndDrag(); } else if (dragState == ReaperViewportDragState.DraggingMiddle) { if (middle) camera = ClampCamera(dragStartCamera + (mouse - dragStartMouse) / ContentScale); else EndDrag(); } else if (dragState == ReaperViewportDragState.PendingLeft && leftReleased) { ReaperTreeNodeModel? releasedNode = inside ? HitTest(mouse) : null; if (!ExceededDragThreshold(mouse) && pressedNode is not null && ReferenceEquals(pressedNode, releasedNode)) { 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, 10, 13) * 0.99f); } internal Vector2 DesignToScreen(Vector2 designPosition) => GetDimensions().Position() + (designPosition + camera) * ContentScale; internal void CancelInteraction() { EndDrag(); hoveredNode = null; previousLeft = Main.mouseLeft; previousMiddle = Main.mouseMiddle; } private ReaperTreeNodeModel? HitTest(Vector2 mouse) { if (!ContainsPoint(mouse)) return null; Vector2 design = (mouse - GetDimensions().Position()) / ContentScale - camera; for (int index = nodes.Count - 1; index >= 0; index--) { ReaperTreeNodeModel node = nodes[index]; float radius = node.Radius + 7f; if (Vector2.DistanceSquared(design, node.Position) <= radius * radius) return node; } return null; } private bool ExceededDragThreshold(Vector2 mouse) { return Vector2.DistanceSquared(mouse, dragStartMouse) >= DragThreshold * DragThreshold; } private void ResetCamera() => ResetCamera(playSound: true); private void ResetCamera(bool playSound) { CalculatedStyle bounds = GetDimensions(); if (bounds.Width <= 1f || bounds.Height <= 1f) return; contentZoom = 1f; Vector2 viewportSize = new(bounds.Width / ContentScale, bounds.Height / ContentScale); camera = ClampCamera(viewportSize * 0.5f - ContentCenter); 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 / ContentScale), Math.Max(1f, bounds.Height / ContentScale)); 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 ZoomAt(Vector2 mouse, int wheelDelta) { float oldScale = ContentScale; Vector2 viewportOrigin = GetDimensions().Position(); Vector2 anchoredDesignPosition = (mouse - viewportOrigin) / oldScale - camera; float factor = wheelDelta > 0 ? ZoomStep : 1f / ZoomStep; float nextZoom = Math.Clamp(contentZoom * factor, MinimumZoom, MaximumZoom); if (Math.Abs(nextZoom - contentZoom) <= 0.0001f) return; contentZoom = nextZoom; camera = (mouse - viewportOrigin) / ContentScale - anchoredDesignPosition; camera = ClampCamera(camera); } private void EndDrag() { dragState = ReaperViewportDragState.None; pressedNode = null; } }