feat: Add item editor with prefix picker

This commit is contained in:
VAC committed 2026-10-16 00:38:56 -03:00
1 parent 73998f1aaa
commit 8d6296676a
7 files changed
+282 -3

No files matched your search

+3
View File
@@ -29,6 +29,7 @@ public static class Hotkeys
new(InterfaceGroup, "ItemBrowser", "Item Browser", () => WindowManager.Toggle(WindowManager.Items)),
new(InterfaceGroup, "BuffBrowser", "Buff Browser", () => WindowManager.Toggle(WindowManager.Buffs)),
new(InterfaceGroup, "NpcSpawner", "NPC Spawner", () => WindowManager.Toggle(WindowManager.Npcs)),
new(InterfaceGroup, "ItemEditor", "Item Editor", () => WindowManager.Toggle(WindowManager.Editor)),
Toggle("Player", "GodMode", "God Mode", () => Settings.GodMode, v => Settings.GodMode = v),
Toggle("Player", "GhostMode", "Ghost Mode", () => Settings.GhostMode, v => Settings.GhostMode = v),
@@ -62,6 +63,8 @@ public static class Hotkeys
Toggle("Items", "ItemVacuum", "Item Vacuum", () => Settings.ItemVacuum, v => Settings.ItemVacuum = v),
Toggle("Items", "InstantBreak", "Instant Break", () => Settings.InstantBreak, v => Settings.InstantBreak = v),
Toggle("Items", "InfItems", "Infinite Items", () => Settings.InfItems, v => Settings.InfItems = v),
new("Items", "MaxStack", "Max Stack", ItemFeatures.MaxStack),
new("Items", "Duplicate", "Duplicate Item", ItemFeatures.Duplicate),
Toggle("Visuals", "Esp", "ESP", () => Settings.Esp, v => Settings.Esp = v),
Toggle("Visuals", "Spelunker", "Spelunker Vision", () => Settings.Spelunker, v => Settings.Spelunker = v),
+35
View File
@@ -1,5 +1,6 @@
using Microsoft.Xna.Framework;
using Terraria;
using TerraSharp.UI;
namespace TerraSharp.Features;
@@ -24,4 +25,38 @@ public static class ItemFeatures
drop.velocity = Vector2.Zero;
}
}
const int InventorySlots = 50;
// The cursor item, or the held item when the cursor is empty.
public static Item EditTarget => Main.mouseItem.type != 0 ? Main.mouseItem : Main.LocalPlayer.inventory[Main.LocalPlayer.selectedItem];
public static void MaxStack()
{
Item item = EditTarget;
if (item.type == 0 || item.stack >= item.maxStack)
return;
item.stack = item.maxStack;
Widgets.Sound(7);
}
public static void Duplicate()
{
Item item = EditTarget;
Item[] inventory = Main.LocalPlayer.inventory;
if (item.type == 0)
return;
for (int i = 0; i < InventorySlots; i++)
{
if (inventory[i].type != 0)
continue;
inventory[i] = item.Clone();
inventory[i].favorited = false;
Widgets.Sound(7);
return;
}
}
}
+222
View File
@@ -0,0 +1,222 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Terraria;
using Terraria.ID;
#if TMODLOADER
using Terraria.ModLoader;
#endif
using TerraSharp.Features;
namespace TerraSharp.UI;
// Edits the cursor item, or the held item when the cursor is empty. Only type, stack and prefix survive a save.
public sealed class ItemEditor() : Window("itemEditor", "Item Editor", new Vector2(700, 100), 480, 600)
{
const int RowHeight = 30;
readonly Scroll _scroll = new();
readonly Item _reference = new();
(int Type, int Prefix) _referenceKey = (-1, -1);
int _x, _y, _width;
bool _firstGroup;
public override void OnOpen() => Main.playerInventory = true;
public override void DrawBody(Rectangle body)
{
if (Main.gameMenu)
{
Render.TextCentered(Widgets.SpriteBatch, "Enter a world to use this.", body, Widgets.Dim, 0.9f);
return;
}
// Shows and edits base stats instead of the Damage Multiplier, Rapid Attack and Instant Break overrides.
HeldItemOverrides.Restore();
Item item = ItemFeatures.EditTarget;
DrawHeader(body, item);
int top = body.Y + 60;
var inner = new Rectangle(body.X, top, body.Width, body.Bottom - top);
Widgets.Panel(inner, Widgets.InnerBg);
if (item.type == 0)
{
Render.TextCentered(Widgets.SpriteBatch, "Hold an item or pick one up with the cursor.", inner, Widgets.Dim, 0.8f);
return;
}
var area = new Rectangle(inner.X + 12, inner.Y + 8, inner.Width - 38, inner.Height - 16);
_y = _scroll.Begin(area);
int start = _y;
(_x, _width, _firstGroup) = (area.X, area.Width, true);
Reference(item);
DrawActions(item);
DrawPrefixes(item);
DrawStats(item);
_scroll.End(area, _y - start + 4);
}
void DrawHeader(Rectangle body, Item item)
{
var slot = new Rectangle(body.X, body.Y, 52, 52);
Widgets.Slot(slot, false);
Render.Item(Widgets.SpriteBatch, item.type, new Rectangle(slot.X + 8, slot.Y + 8, slot.Width - 16, slot.Height - 16));
if (item.stack > 1)
Render.Text(Widgets.SpriteBatch, item.stack.ToString(), new Vector2(slot.X + 8, slot.Bottom - 20), Color.White, 0.7f);
string source = Main.mouseItem.type != 0 ? "Cursor item" : "Held item";
Render.Text(Widgets.SpriteBatch, item.type == 0 ? "No item" : item.AffixName(), new Vector2(slot.Right + 12, body.Y + 4), item.type == 0 ? Widgets.Dim : Color.White, 0.95f);
Render.Text(Widgets.SpriteBatch, item.type == 0 ? source : $"{source} #{item.type}", new Vector2(slot.Right + 12, body.Y + 30), Widgets.Dim, 0.72f);
}
// Defaults for the current type and prefix; right-click on a stat resets to these.
void Reference(Item item)
{
if (_referenceKey == (item.type, item.prefix))
return;
_reference.SetDefaults(item.type);
if (item.prefix > 0)
_reference.Prefix(item.prefix);
_referenceKey = (item.type, item.prefix);
}
void Group(string title)
{
if (!_firstGroup)
_y += 8;
_firstGroup = false;
Widgets.Text(title, new Vector2(_x, _y + 2), Widgets.Header, 0.88f);
float width = Render.Measure(title, 0.88f).X;
Widgets.Separator(_x + (int)width + 12, _y + 12, _width - (int)width - 12);
_y += 30;
}
void DrawActions(Item item)
{
Group("Actions");
string[] labels = ["Max Stack", "Duplicate", "Reset Stats"];
int width = (_width - 12) / labels.Length;
for (int i = 0; i < labels.Length; i++)
{
if (!Widgets.Button(new Rectangle(_x + i * (width + 6), _y + 2, width, 30), labels[i], false, 0.8f))
continue;
switch (i)
{
case 0: ItemFeatures.MaxStack(); break;
case 1: ItemFeatures.Duplicate(); break;
case 2: ResetStats(item); break;
}
}
_y += 38;
}
static void ResetStats(Item item)
{
int stack = item.stack, prefix = item.prefix;
bool favorited = item.favorited;
item.SetDefaults(item.type);
if (prefix > 0)
item.Prefix(prefix);
(item.stack, item.favorited) = (stack, favorited);
}
static IEnumerable<int> Prefixes(Item item)
{
#if TMODLOADER
return Enumerable.Range(1, PrefixLoader.PrefixCount - 1).Where(item.CanApplyPrefix);
#else
return item.GetRollablePrefixes() ?? [];
#endif
}
static string PrefixName(int prefix)
{
#if TMODLOADER
if (prefix >= PrefixID.Count)
return PrefixLoader.GetPrefix(prefix)?.DisplayName.Value ?? $"Prefix {prefix}";
#endif
return Lang.prefix[prefix].Value;
}
void DrawPrefixes(Item item)
{
int[] prefixes = [.. Prefixes(item).OrderBy(PrefixName, StringComparer.OrdinalIgnoreCase)];
if (prefixes.Length == 0)
return;
Group("Prefix");
const int Columns = 3, Gap = 6;
int width = (_width - Gap * (Columns - 1)) / Columns;
int[] options = [0, .. prefixes];
for (int i = 0; i < options.Length; i++)
{
var bounds = new Rectangle(_x + i % Columns * (width + Gap), _y + i / Columns * 34, width, 30);
if (Widgets.Button(bounds, options[i] == 0 ? "None" : PrefixName(options[i]), item.prefix == options[i], 0.76f) && item.prefix != options[i])
ApplyPrefix(item, options[i]);
}
_y += (options.Length + Columns - 1) / Columns * 34 + 4;
}
static void ApplyPrefix(Item item, int prefix)
{
int stack = item.stack;
bool favorited = item.favorited;
item.SetDefaults(item.type);
item.Prefix(prefix);
(item.stack, item.favorited) = (stack, favorited);
Widgets.Sound(2, -1, -1, 37);
}
void DrawStats(Item item)
{
Group("Stats");
bool weapon = _reference.damage > 0;
if (weapon)
{
item.damage = IntRow("Damage", item.damage, 0, Math.Max(200, _reference.damage * 10), _reference.damage);
item.crit = IntRow("Critical chance", item.crit, 0, 100, _reference.crit, "{0}%");
item.knockBack = FloatRow("Knockback", item.knockBack, 0f, 20f, _reference.knockBack, "{0:0.0}");
item.scale = FloatRow("Size", item.scale, 0.25f, 5f, _reference.scale, "{0:0.00}x");
}
if (_reference.useStyle != 0)
{
item.useTime = IntRow("Use time", item.useTime, 1, Math.Max(60, _reference.useTime), _reference.useTime, "{0} ticks");
item.useAnimation = IntRow("Use animation", item.useAnimation, 1, Math.Max(60, _reference.useAnimation), _reference.useAnimation, "{0} ticks");
}
if (_reference.shoot > 0)
item.shootSpeed = FloatRow("Shot speed", item.shootSpeed, 0f, 40f, _reference.shootSpeed, "{0:0.0}");
if (_reference.defense > 0 || _reference.accessory || _reference.headSlot >= 0 || _reference.bodySlot >= 0 || _reference.legSlot >= 0)
item.defense = IntRow("Defense", item.defense, 0, Math.Max(100, _reference.defense * 10), _reference.defense);
if (item.maxStack > 1)
item.stack = IntRow("Stack", item.stack, 1, item.maxStack, item.maxStack);
if (_reference.useStyle != 0)
item.autoReuse = BoolRow("Auto reuse", item.autoReuse);
}
float FloatRow(string label, float value, float minimum, float maximum, float fallback, string format)
{
var row = new Rectangle(_x, _y, _width, RowHeight - 2);
string id = "itemEditor|" + label;
bool hot = Widgets.Hot(row) || Widgets.ActiveSlider == id;
if (hot)
Widgets.Tooltip = "Drag to change. Right-click to reset.";
Render.TextMid(Widgets.SpriteBatch, $"{label}: {string.Format(format, value)}", row, hot ? Widgets.Yellow : Widgets.Dim, 0.85f);
float result = Widgets.ValueBar(id, row, MathHelper.Clamp(value, minimum, maximum), minimum, maximum, fallback);
_y += RowHeight;
return Widgets.ActiveSlider == id || result != MathHelper.Clamp(value, minimum, maximum) ? result : value;
}
int IntRow(string label, int value, int minimum, int maximum, int fallback, string format = "{0}") => (int)Math.Round(FloatRow(label, value, minimum, maximum, fallback, format.Replace("{0}", "{0:0}")));
bool BoolRow(string label, bool value)
{
value = Widgets.ToggleRow(new Rectangle(_x, _y, _width, 28), label, value);
_y += RowHeight;
return value;
}
}
+5 -2
View File
@@ -140,8 +140,11 @@ public sealed partial class MainWindow
Toggle("Sonar", ref Settings.Sonar);
Group("Actions");
if (Buttons(WindowManager.Items.Open ? "Close Item Browser" : "Item Browser") == 0)
WindowManager.Toggle(WindowManager.Items);
switch (Buttons(WindowManager.Items.Open ? "Close Item Browser" : "Item Browser", WindowManager.Editor.Open ? "Close Item Editor" : "Item Editor"))
{
case 0: WindowManager.Toggle(WindowManager.Items); break;
case 1: WindowManager.Toggle(WindowManager.Editor); break;
}
}
void VisualsPage()
+1
View File
@@ -37,6 +37,7 @@ public sealed partial class MainWindow
["Infinite Items"] = "Keeps inventory stacks from shrinking.",
["Item Vacuum"] = "Pulls all dropped items to you.",
["Bank Buttons"] = "Piggy Bank, Safe, Defender's Forge and Void Vault buttons left of the inventory.",
["Item Editor"] = "Change the prefix, stats and stack of the held or cursor item.",
["Auto Fishing"] = "Reels in on a bite while you hold a fishing pole.",
["Recast"] = "Casts again where you last cast by hand.",
["Catch enemies"] = "Reels in enemy catches too.",
+14
View File
@@ -227,6 +227,20 @@ public static class Widgets
return true;
}
public static bool ToggleRow(Rectangle row, string label, bool value)
{
bool hot = Hot(row);
if (hot && Click)
{
value = !value;
Sound(12);
}
Render.TextMid(SpriteBatch, label, row, hot ? Yellow : value ? On : Color.White, 0.85f);
ToggleIcon(new Rectangle(row.Right - 28, row.Y + 2, 24, 24), value, hot);
return value;
}
public static void ToggleIcon(Rectangle bounds, bool on, bool hot)
{
Render.TextRight(SpriteBatch, on ? "On" : "Off", new Rectangle(bounds.X - 44, bounds.Y, 38, bounds.Height), on ? On : Dim * 0.75f, 0.78f);
+2 -1
View File
@@ -14,11 +14,12 @@ public static class WindowManager
public static readonly ItemBrowser Items = new();
public static readonly BuffBrowser Buffs = new();
public static readonly NpcBrowser Npcs = new();
public static readonly ItemEditor Editor = new();
const int TitleHeight = 40;
const int MaxInputLength = 40;
static readonly List<Window> Stack = [Items, Buffs, Npcs, Menu];
static readonly List<Window> Stack = [Items, Buffs, Npcs, Editor, Menu];
static Window? _dragging;
static Vector2 _dragOffset;