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 SoulHarvest.Projectiles;
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.GameInput;
using Terraria.ID;
using Terraria.ModLoader;
using Terraria.UI;

namespace SoulHarvest.UI;

[Autoload(Side = ModSide.Client)]
public sealed class ReaperFormWheelUISystem : ModSystem
{
    private readonly List<ReaperFormId> wheelForms = [];
    private bool visible;
    private bool keyWasHeld;
    private ReaperFormId? hoveredForm;

    /// <summary>
    /// Lets the combat-input layer suppress overlapping keybinds while the radial
    /// selector owns the mouse. The wheel itself also clears item/tile use below.
    /// </summary>
    public static bool IsWheelOpen => ModContent.GetInstance<ReaperFormWheelUISystem>().visible;

    /// <summary>
    /// Shared client-side guard for special/ultimate trigger handling. Keeping the
    /// check public prevents rebound combat keys from firing through the wheel,
    /// inventory, chat, altar, map, NPC dialogue or another vanilla UI state.
    /// </summary>
    public static bool ShouldBlockReaperCombatInput(Player player)
    {
        return !Main.dedServ && (IsWheelOpen || HasBlockingInterface(player));
    }

    public override void Load()
    {
        wheelForms.Clear();
        wheelForms.AddRange(ReaperUIData.BranchForms);
    }

    public override void Unload()
    {
        wheelForms.Clear();
        visible = false;
        keyWasHeld = false;
        hoveredForm = null;
    }

    public override void UpdateUI(GameTime gameTime)
    {
#if DEBUG
        // A freshly isolated test profile can spend its first world tick before
        // tML has repopulated mod trigger keys. Do not let that transient test
        // setup state abort the entire update loop.
        bool keyHeld = PlayerInput.Triggers.Current.KeyStatus.TryGetValue(
            "SoulHarvest/FormWheel", out bool debugKeyHeld) && debugKeyHeld;
#else
        bool keyHeld = SoulHarvest.FormWheelKeybind?.Current == true;
#endif
        Player player = Main.LocalPlayer;
        bool validContext = !Main.gameMenu
            && player.active
            && !player.dead
            && player.HeldItem.ModItem is NormalSickle;
        bool interfaceBlocked = HasBlockingInterface(player);

        if (!validContext || interfaceBlocked && !visible)
        {
            visible = false;
            hoveredForm = null;
            keyWasHeld = keyHeld;
            return;
        }

        if (visible && interfaceBlocked)
        {
            visible = false;
            hoveredForm = null;
            keyWasHeld = keyHeld;
            SoundEngine.PlaySound(SoundID.MenuClose);
            return;
        }

        if (keyHeld && !keyWasHeld)
        {
            visible = true;
            hoveredForm = null;
            SoundEngine.PlaySound(SoundID.MenuOpen);
        }

        if (visible && keyHeld)
        {
            player.mouseInterface = true;
            player.controlUseItem = false;
            player.controlUseTile = false;
            UpdateHoveredForm(player.GetModPlayer<MyPlayer>().ReaperProgression);
        }

        if (visible && !keyHeld && keyWasHeld)
        {
            // Sample the cursor again on the release frame. Previously the hover
            // state was only refreshed while the key was held, so a cursor move
            // and key release in the same frame could commit the segment that was
            // highlighted one tick earlier instead of the segment under the mouse.
            UpdateHoveredForm(player.GetModPlayer<MyPlayer>().ReaperProgression);
            CommitSelection(player, player.GetModPlayer<MyPlayer>().ReaperProgression);
            visible = false;
            hoveredForm = null;
        }

        keyWasHeld = keyHeld;
    }

    public override void ModifyInterfaceLayers(List<GameInterfaceLayer> layers)
    {
        int mouseTextIndex = layers.FindIndex(layer => layer.Name == "Vanilla: Mouse Text");
        if (mouseTextIndex < 0)
            return;

        layers.Insert(mouseTextIndex, new LegacyGameInterfaceLayer(
            "SoulHarvest: Reaper Form Wheel",
            DrawWheel,
            InterfaceScaleType.UI));
    }

    private void UpdateHoveredForm(ReaperProgressionState progression)
    {
        hoveredForm = null;
        if (wheelForms.Count == 0)
            return;

        Vector2 center = new(Main.screenWidth * 0.5f, Main.screenHeight * 0.5f);
        Vector2 mouse = Main.MouseScreen;
        Vector2 fromCenter = mouse - center;
        if (fromCenter.LengthSquared() < 54f * 54f)
        {
            hoveredForm = ReaperFormId.Base;
            return;
        }

        if (progression.DeathFormUnlocked && GetDeathButtonBounds(center, GetWheelRadius()).Contains(mouse.ToPoint()))
        {
            hoveredForm = ReaperFormId.Death;
            return;
        }

        if (fromCenter.LengthSquared() > 225f * 225f)
            return;

        float radius = GetWheelRadius();
        float bestDistance = float.MaxValue;
        foreach ((ReaperFormId form, Vector2 position) in EnumeratePositions(center, radius))
        {
            float distance = Vector2.DistanceSquared(mouse, position);
            if (distance >= bestDistance)
                continue;
            bestDistance = distance;
            hoveredForm = form;
        }

        if (bestDistance > 58f * 58f)
            hoveredForm = null;
    }

    private void CommitSelection(Player player, ReaperProgressionState progression)
    {
        if (hoveredForm is not ReaperFormId form || !ReaperUIData.IsUnlocked(progression, form))
        {
            SoundEngine.PlaySound(SoundID.MenuClose);
            return;
        }

        if (progression.CurrentForm != form)
        {
            if (IsReaperActionInProgress(player))
            {
                Main.NewText(Terraria.Localization.Language.GetTextValue(
                    "Mods.SoulHarvest.UI.ReaperWheel.SwitchQueued",
                    ReaperUIData.GetFormName(form)), ReaperUIData.GetPrimaryColor(form));
            }
            SoulHarvest.RequestReaperFormSwitch(form);
        }
        SoundEngine.PlaySound(SoundID.MenuTick);
    }

    private static bool IsReaperActionInProgress(Player player)
    {
        return player.itemAnimation > 0
            || player.itemTime > 0
            || player.ownedProjectileCounts[ModContent.ProjectileType<SickleSwingProjectile>()] > 0
            || player.ownedProjectileCounts[ModContent.ProjectileType<ReaperActionControllerProjectile>()] > 0
            || player.GetModPlayer<MyPlayer>().IsReaperFormAssemblyActive();
    }

    private static bool HasBlockingInterface(Player player)
    {
        return Main.playerInventory
            || Main.drawingPlayerChat
            || Main.editSign
            || Main.editChest
            || Main.mapFullscreen
            || Main.ingameOptionsWindow
            || player.talkNPC >= 0
            || player.mouseInterface
            || Main.InGameUI.IsVisible
            || ModContent.GetInstance<DeathAltarUISystem>().IsVisible;
    }

    private bool DrawWheel()
    {
        if (!visible || Main.gameMenu || wheelForms.Count == 0)
            return true;

        Player player = Main.LocalPlayer;
        MyPlayer modPlayer = player.GetModPlayer<MyPlayer>();
        ReaperProgressionState progression = modPlayer.ReaperProgression;
        Texture2D pixel = TextureAssets.MagicPixel.Value;
        Vector2 center = new(Main.screenWidth * 0.5f, Main.screenHeight * 0.5f);
        float radius = GetWheelRadius();

        DrawBackdrop(Main.spriteBatch, pixel, center, radius, progression.DeathFormUnlocked);
        foreach ((ReaperFormId form, Vector2 position) in EnumeratePositions(center, radius))
            DrawForm(Main.spriteBatch, pixel, modPlayer, progression, form, position);
        DrawCenter(Main.spriteBatch, pixel, progression, center);
        if (progression.DeathFormUnlocked)
            DrawDeathTerminal(Main.spriteBatch, pixel, modPlayer, progression, center, radius);
        return true;
    }

    private void DrawBackdrop(SpriteBatch spriteBatch, Texture2D pixel, Vector2 center, float radius, bool showDeathTerminal)
    {
        int width = (int)MathF.Round(radius * 2f + 104f);
        int top = (int)MathF.Round(center.Y - radius - 52f);
        int bottom = (int)MathF.Round(center.Y + radius + 52f);
        if (showDeathTerminal)
            bottom = GetDeathButtonBounds(center, radius).Bottom + 18;
        Rectangle shade = new((int)center.X - width / 2, top, width, Math.Max(1, bottom - top));
        spriteBatch.Draw(pixel, shade, new Color(7, 5, 16) * 0.58f);
        AltarDrawHelpers.DrawBorder(spriteBatch, pixel, shade, new Color(103, 58, 137) * 0.75f, 2);

        foreach ((ReaperFormId form, Vector2 position) in EnumeratePositions(center, radius))
        {
            Vector2 delta = position - center;
            Color color = ReaperUIData.GetPrimaryColor(form) * (hoveredForm == form ? 0.62f : 0.25f);
            spriteBatch.Draw(pixel, center + delta.SafeNormalize(Vector2.UnitX) * 48f, null, color, delta.ToRotation(), Vector2.Zero,
                new Vector2(Math.Max(1f, delta.Length() - 78f) / pixel.Width, (hoveredForm == form ? 3f : 1.5f) / pixel.Height), SpriteEffects.None, 0f);
        }
    }

    private void DrawForm(SpriteBatch spriteBatch, Texture2D pixel, MyPlayer modPlayer, ReaperProgressionState progression, ReaperFormId form, Vector2 center)
    {
        bool unlocked = ReaperUIData.IsUnlocked(progression, form);
        bool selected = hoveredForm == form;
        bool active = progression.CurrentForm == form;
        Color primary = ReaperUIData.GetPrimaryColor(form);
        float pulse = active ? 0.82f + (float)Math.Sin(Main.GlobalTimeWrappedHourly * 4f) * 0.16f : 0.72f;
        float slotSize = selected ? 68f : 60f;
        Rectangle bounds = new((int)(center.X - slotSize * 0.5f), (int)(center.Y - slotSize * 0.5f), (int)slotSize, (int)slotSize);
        spriteBatch.Draw(pixel, bounds, (unlocked ? Color.Lerp(new Color(24, 18, 38), primary, selected ? 0.30f : 0.14f) : new Color(24, 22, 31)) * 0.98f);
        AltarDrawHelpers.DrawBorder(spriteBatch, pixel, bounds, unlocked ? primary * (selected ? 1f : pulse) : new Color(74, 69, 82), selected || active ? 3 : 1);

        DrawFormIcon(
            spriteBatch,
            form,
            center,
            selected ? 48f : 42f,
            unlocked ? Color.White : new Color(80, 80, 80));

        string stageText = form == ReaperFormId.Death
            ? (unlocked ? "III+" : "—")
            : unlocked ? $"{(int)progression.GetStage(form)}/3" : "—";
        Utils.DrawBorderString(spriteBatch, stageText, center + new Vector2(15f, 20f), unlocked ? Color.White : new Color(130, 124, 137), 0.50f);

        bool ultimateUnlocked = form == ReaperFormId.Death
            ? progression.DeathFormUnlocked
            : progression.GetStage(form) >= ReaperStage.StageIII;
        string energyText = unlocked
            ? $"{(int)MathF.Floor(modPlayer.GetReaperEnergy(form))}%"
            : "--";
        Utils.DrawBorderString(spriteBatch, energyText, center + new Vector2(-27f, 20f),
            ultimateUnlocked ? ReaperUIData.GetSecondaryColor(form) : new Color(116, 108, 126), 0.47f);

        if (selected)
        {
            string name = ReaperUIData.GetFormName(form);
            Vector2 size = FontAssets.MouseText.Value.MeasureString(name) * 0.66f;
            Utils.DrawBorderString(spriteBatch, name, new Vector2(center.X - size.X * 0.5f, bounds.Bottom + 7f), unlocked ? primary : new Color(150, 142, 157), 0.66f);
        }
    }

    private void DrawCenter(SpriteBatch spriteBatch, Texture2D pixel, ReaperProgressionState progression, Vector2 center)
    {
        bool baseSelected = hoveredForm == ReaperFormId.Base;
        bool active = progression.CurrentForm == ReaperFormId.Base;
        Color primary = ReaperUIData.GetPrimaryColor(ReaperFormId.Base);
        const int centerSize = 94;
        Rectangle bounds = new((int)center.X - centerSize / 2, (int)center.Y - centerSize / 2, centerSize, centerSize);
        Color centerBackground = baseSelected
            ? Color.Lerp(new Color(15, 11, 27), primary, 0.28f)
            : new Color(15, 11, 27);
        spriteBatch.Draw(pixel, bounds, centerBackground * 0.99f);
        float pulse = active ? 0.78f + (float)Math.Sin(Main.GlobalTimeWrappedHourly * 4f) * 0.17f : 0.62f;
        AltarDrawHelpers.DrawBorder(spriteBatch, pixel, bounds, primary * (baseSelected ? 1f : pulse), baseSelected || active ? 3 : 2);

        DrawFormIcon(
            spriteBatch,
            ReaperFormId.Base,
            center - Vector2.UnitY * 8f,
            baseSelected ? 56f : 50f,
            Color.White);

        string name = ReaperUIData.GetFormName(ReaperFormId.Base);
        float nameScale = 0.58f;
        string fitted = AltarDrawHelpers.FitText(name, bounds.Width - 10f, nameScale);
        Vector2 nameSize = FontAssets.MouseText.Value.MeasureString(fitted) * nameScale;
        Utils.DrawBorderString(spriteBatch, fitted, new Vector2(center.X - nameSize.X * 0.5f, bounds.Bottom - 23f), Color.White, nameScale);

        string hint = Terraria.Localization.Language.GetTextValue("Mods.SoulHarvest.UI.ReaperWheel.ReleaseHint");
        float hintScale = 0.48f;
        Vector2 hintSize = FontAssets.MouseText.Value.MeasureString(hint) * hintScale;
        Utils.DrawBorderString(spriteBatch, hint, new Vector2(center.X - hintSize.X * 0.5f, bounds.Bottom + 15f), new Color(190, 178, 207), hintScale);
    }

    private void DrawDeathTerminal(
        SpriteBatch spriteBatch,
        Texture2D pixel,
        MyPlayer modPlayer,
        ReaperProgressionState progression,
        Vector2 wheelCenter,
        float radius)
    {
        Rectangle bounds = GetDeathButtonBounds(wheelCenter, radius);
        bool selected = hoveredForm == ReaperFormId.Death;
        bool active = progression.CurrentForm == ReaperFormId.Death;
        Color primary = ReaperUIData.GetPrimaryColor(ReaperFormId.Death);
        Color secondary = ReaperUIData.GetSecondaryColor(ReaperFormId.Death);
        float pulse = active ? 0.76f + (float)Math.Sin(Main.GlobalTimeWrappedHourly * 5f) * 0.18f : 0.66f;

        Rectangle shadow = bounds;
        shadow.Inflate(selected ? 5 : 3, selected ? 4 : 2);
        spriteBatch.Draw(pixel, shadow, new Color(70, 3, 18) * 0.70f);
        spriteBatch.Draw(pixel, bounds, Color.Lerp(new Color(31, 7, 18), primary, selected ? 0.34f : 0.18f));
        AltarDrawHelpers.DrawBorder(spriteBatch, pixel, bounds, primary * (selected ? 1f : pulse), selected || active ? 3 : 2);

        DrawFormIcon(
            spriteBatch,
            ReaperFormId.Death,
            new Vector2(bounds.Left + 29f, bounds.Center.Y),
            selected ? 45f : 40f,
            Color.White);

        string name = ReaperUIData.GetFormName(ReaperFormId.Death);
        const float nameScale = 0.61f;
        string fitted = AltarDrawHelpers.FitText(name, bounds.Width - 66f, nameScale);
        Utils.DrawBorderString(spriteBatch, fitted, new Vector2(bounds.Left + 57f, bounds.Top + 8f), Color.White, nameScale);

        string energy = $"III+  ·  {(int)MathF.Floor(modPlayer.GetReaperEnergy(ReaperFormId.Death))}%";
        Utils.DrawBorderString(spriteBatch, energy, new Vector2(bounds.Left + 58f, bounds.Top + 30f), secondary, 0.49f);
    }

    private IEnumerable<(ReaperFormId Form, Vector2 Position)> EnumeratePositions(Vector2 center, float radius)
    {
        for (int index = 0; index < wheelForms.Count; index++)
        {
            float angle = -MathHelper.PiOver2 + index * MathHelper.TwoPi / wheelForms.Count;
            yield return (wheelForms[index], center + angle.ToRotationVector2() * radius);
        }
    }

    private static void DrawFormIcon(
        SpriteBatch spriteBatch,
        ReaperFormId form,
        Vector2 center,
        float maximumSize,
        Color color)
    {
        // The legacy form ModItems deliberately share NormalSickle.Texture so old
        // saves retain one physical shell. Constructing those items therefore
        // always drew the base icon. Form presentation must address the retained
        // artwork directly instead of going through TextureAssets.Item.
        Texture2D texture = ReaperUIData.GetFormTexture(form);
        Rectangle frame = texture.Frame();
        float scale = Math.Min(1f, maximumSize / Math.Max(frame.Width, frame.Height));
        spriteBatch.Draw(
            texture,
            center,
            frame,
            color,
            0f,
            frame.Size() * 0.5f,
            scale,
            SpriteEffects.None,
            0f);
    }

    private static Rectangle GetDeathButtonBounds(Vector2 center, float radius)
    {
        const int width = 166;
        const int height = 56;
        int buttonCenterY = (int)MathF.Round(center.Y + radius + 92f);
        return new Rectangle((int)MathF.Round(center.X) - width / 2, buttonCenterY - height / 2, width, height);
    }

    private static float GetWheelRadius() => Math.Clamp(Math.Min(Main.screenWidth, Main.screenHeight) * 0.19f, 118f, 166f);
}