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