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