XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

公开
关注 0 Fork 0 Star 0
UTF-8
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;
    }
}