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<AltarEquipmentTreeNode> 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<AltarEquipmentTreeNode> 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;
}
}
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<AltarEquipmentTreeNode> 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<AltarEquipmentTreeNode> 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;
}
}