feat: Rework player inspector with vertical equipment, portrait and slot labels
This commit is contained in:
1 parent
23ac33cb93
commit
c5ff9a75fb
4 files changed
+135
-21
No files matched your search
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Terraria;
|
||||
using Terraria.UI;
|
||||
@@ -9,16 +10,19 @@ public sealed class PlayerInspector() : Window("inspect", "Inspect Player", new
|
||||
{
|
||||
enum Tab { Equipment, Inventory, Buffs }
|
||||
|
||||
const int SlotSize = 36, SlotStep = 40, Columns = 10, ContentTop = 72, FrameHeight = 62;
|
||||
const int SlotSize = 36, SlotStep = 40, Columns = 10, ContentTop = 40, FrameHeight = 62, LabelHeight = 20, HeaderHeight = 20;
|
||||
|
||||
// Empty-slot icons are frames of TextureAssets.Extra[54], as ItemSlot.Draw picks them per context.
|
||||
const int AccessoryIcon = 11, VanityAccessoryIcon = 2, DyeIcon = 1;
|
||||
static readonly int[] ArmorIcons = [0, 6, 12], VanityArmorIcons = [3, 9, 15], MiscIcons = [4, 13, 7, 10, 17];
|
||||
static readonly string[] ArmorLabels = ["Head", "Body", "Legs"], MiscLabels = ["Pet", "Light Pet", "Minecart", "Mount", "Hook"];
|
||||
|
||||
Tab _tab;
|
||||
|
||||
public int Target { get; private set; } = -1;
|
||||
|
||||
public override string Title => Target >= 0 && Main.player[Target] is { active: true } player ? $"Inspecting Player: {player.name}" : base.Title;
|
||||
|
||||
public void Inspect(int who)
|
||||
{
|
||||
Target = who;
|
||||
@@ -33,56 +37,92 @@ public sealed class PlayerInspector() : Window("inspect", "Inspect Player", new
|
||||
return;
|
||||
}
|
||||
|
||||
Render.TextMid(Widgets.SpriteBatch, Render.Ellipsize(player.name, 0.9f, body.Width), new Rectangle(body.X + 2, body.Y, body.Width, 26), Render.TeamColor(player), 0.9f);
|
||||
|
||||
// The held item sits right of the tabs, on the hotbar's selected slot background.
|
||||
int tabWidth = (body.Width - 12 - SlotStep) / 3;
|
||||
int tabWidth = (body.Width - 12) / 3;
|
||||
for (int i = 0; i < 3; i++)
|
||||
if (Widgets.Button(new Rectangle(body.X + i * (tabWidth + 6), body.Y + 32, tabWidth, 30), ((Tab)i).ToString(), _tab == (Tab)i, 0.82f))
|
||||
if (Widgets.Button(new Rectangle(body.X + i * (tabWidth + 6), body.Y, tabWidth, 30), ((Tab)i).ToString(), _tab == (Tab)i, 0.82f))
|
||||
_tab = (Tab)i;
|
||||
Slot(player.HeldItem, body.Right - SlotSize, body.Y + 29, selected: true);
|
||||
|
||||
int x = body.X + (body.Width - (Columns * SlotStep - (SlotStep - SlotSize))) / 2, y = body.Y + ContentTop;
|
||||
int height = _tab switch
|
||||
{
|
||||
Tab.Equipment => DrawEquipment(player, x, y),
|
||||
Tab.Equipment => DrawEquipment(player, body, y),
|
||||
Tab.Inventory => DrawInventory(player, x, y),
|
||||
_ => DrawBuffs(player, x, y, body),
|
||||
};
|
||||
Height = FrameHeight + ContentTop + height;
|
||||
}
|
||||
|
||||
// Rows follow the vanilla equipment page: armor and accessories, vanity, dyes, then misc equips and their dyes.
|
||||
static int DrawEquipment(Player player, int x, int y)
|
||||
// Same column order as the vanilla equipment page.
|
||||
static int DrawEquipment(Player player, Rectangle body, int y)
|
||||
{
|
||||
const int ArmorLabelWidth = 48, MiscLabelWidth = 64, BlockGap = 40;
|
||||
int armorWidth = ArmorLabelWidth + 3 * SlotStep - (SlotStep - SlotSize), miscWidth = MiscLabelWidth + 2 * SlotStep - (SlotStep - SlotSize);
|
||||
int x = body.X + (body.Width - armorWidth - BlockGap - miscWidth) / 2, miscX = x + armorWidth + BlockGap;
|
||||
int rowsY = y + HeaderHeight;
|
||||
|
||||
ColumnHeaders(x + ArmorLabelWidth, y, "Dye", "Vanity", "Equip");
|
||||
int rows = 0;
|
||||
for (int i = 0; i < Columns; i++)
|
||||
{
|
||||
Slot(player.armor[i], x + i * SlotStep, y, "InventoryBack3", i < 3 ? ArmorIcons[i] : AccessoryIcon);
|
||||
Slot(player.armor[i + Columns], x + i * SlotStep, y + SlotStep, "InventoryBack8", i < 3 ? VanityArmorIcons[i] : VanityAccessoryIcon);
|
||||
Slot(player.dye[i], x + i * SlotStep, y + 2 * SlotStep, "InventoryBack12", DyeIcon);
|
||||
// Slots 8 and 9 need the Demon Heart and Master Mode.
|
||||
if (!player.IsItemSlotUnlockedAndUsable(i))
|
||||
continue;
|
||||
|
||||
int rowY = rowsY + rows * SlotStep;
|
||||
rows++;
|
||||
RowLabel(i < 3 ? ArmorLabels[i] : $"Acc {rows - 3}", x, rowY, ArmorLabelWidth);
|
||||
Slot(player.dye[i], x + ArmorLabelWidth, rowY, "InventoryBack12", DyeIcon);
|
||||
Slot(player.armor[i + Columns], x + ArmorLabelWidth + SlotStep, rowY, "InventoryBack8", i < 3 ? VanityArmorIcons[i] : VanityAccessoryIcon);
|
||||
Slot(player.armor[i], x + ArmorLabelWidth + 2 * SlotStep, rowY, "InventoryBack3", i < 3 ? ArmorIcons[i] : AccessoryIcon);
|
||||
}
|
||||
|
||||
int miscY = y + 3 * SlotStep + 8;
|
||||
ColumnHeaders(miscX + MiscLabelWidth, y, "Dye", "Equip");
|
||||
for (int i = 0; i < player.miscEquips.Length; i++)
|
||||
{
|
||||
Slot(player.miscEquips[i], x + i * SlotStep, miscY, "InventoryBack3", MiscIcons[i]);
|
||||
Slot(player.miscDyes[i], x + (i + player.miscEquips.Length) * SlotStep, miscY, "InventoryBack12", DyeIcon);
|
||||
int rowY = rowsY + i * SlotStep;
|
||||
RowLabel(MiscLabels[i], miscX, rowY, MiscLabelWidth);
|
||||
Slot(player.miscDyes[i], miscX + MiscLabelWidth, rowY, "InventoryBack12", DyeIcon);
|
||||
Slot(player.miscEquips[i], miscX + MiscLabelWidth + SlotStep, rowY, "InventoryBack3", MiscIcons[i]);
|
||||
}
|
||||
|
||||
return 4 * SlotStep + 8;
|
||||
const int PortraitWidth = 96, PortraitMinHeight = 110;
|
||||
int portraitY = rowsY + player.miscEquips.Length * SlotStep + 4;
|
||||
var portrait = new Rectangle(miscX, portraitY, PortraitWidth, Math.Max(PortraitMinHeight, rowsY + rows * SlotStep - SlotStep + SlotSize - portraitY));
|
||||
Widgets.Panel(portrait, Widgets.InnerBg);
|
||||
PlayerPortrait.Draw(player, portrait);
|
||||
int heldX = miscX + miscWidth - SlotSize;
|
||||
Render.TextCenteredAt(Widgets.SpriteBatch, "Held", heldX + SlotSize / 2f, portrait.Bottom - SlotSize - HeaderHeight, Widgets.Dim, 0.7f);
|
||||
Slot(player.HeldItem, heldX, portrait.Bottom - SlotSize, selected: true);
|
||||
|
||||
return Math.Max(HeaderHeight + rows * SlotStep, portrait.Bottom - y);
|
||||
}
|
||||
|
||||
static void ColumnHeaders(int x, int y, params string[] titles)
|
||||
{
|
||||
for (int i = 0; i < titles.Length; i++)
|
||||
Render.TextCenteredAt(Widgets.SpriteBatch, titles[i], x + i * SlotStep + SlotSize / 2f, y, Widgets.Dim, 0.7f);
|
||||
}
|
||||
|
||||
static void RowLabel(string text, int x, int y, int width) => Render.TextMid(Widgets.SpriteBatch, text, new Rectangle(x, y, width, SlotSize), Widgets.Header, 0.75f);
|
||||
|
||||
// Slots 0-49 are the main inventory, 50-53 coins and 54-57 ammo.
|
||||
static int DrawInventory(Player player, int x, int y)
|
||||
{
|
||||
for (int i = 0; i < 50; i++)
|
||||
Slot(player.inventory[i], x + i % Columns * SlotStep, y + i / Columns * SlotStep, selected: i == player.selectedItem);
|
||||
|
||||
int extraY = y + 5 * SlotStep + 8;
|
||||
// Vanilla labels the hotbar 1-0 whatever the key bindings are.
|
||||
for (int i = 0; i < Columns; i++)
|
||||
Render.Text(Widgets.SpriteBatch, ((i + 1) % 10).ToString(), new Vector2(x + i * SlotStep + 5, y + 2), Color.White, 0.6f);
|
||||
|
||||
int labelY = y + 5 * SlotStep + 6, extraY = labelY + LabelHeight;
|
||||
int groupWidth = 4 * SlotStep - (SlotStep - SlotSize);
|
||||
Render.TextCenteredAt(Widgets.SpriteBatch, Lang.inter[26].Value, x + groupWidth / 2f, labelY, Widgets.Header, 0.75f);
|
||||
Render.TextCenteredAt(Widgets.SpriteBatch, Lang.inter[27].Value, x + 6 * SlotStep + groupWidth / 2f, labelY, Widgets.Header, 0.75f);
|
||||
for (int i = 0; i < 8; i++)
|
||||
Slot(player.inventory[50 + i], x + (i < 4 ? i : i + 2) * SlotStep, extraY);
|
||||
|
||||
return 6 * SlotStep + 8;
|
||||
return 6 * SlotStep + 6 + LabelHeight;
|
||||
}
|
||||
|
||||
static int DrawBuffs(Player player, int x, int y, Rectangle body)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using Terraria;
|
||||
using TerraSharp.Core;
|
||||
|
||||
namespace TerraSharp.UI;
|
||||
|
||||
// Draws a player standing still and facing right, without the held item.
|
||||
public static class PlayerPortrait
|
||||
{
|
||||
const float Scale = 1.5f;
|
||||
|
||||
static readonly Item Blank = new();
|
||||
|
||||
// Public on tModLoader, private on vanilla.
|
||||
const BindingFlags AnyInstance = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
static readonly FieldInfo? MountFrame = typeof(Mount).GetField("_frame", AnyInstance);
|
||||
static readonly FieldInfo? MountData = typeof(Mount).GetField("_data", AnyInstance);
|
||||
static readonly FieldInfo? StandingFrame = MountData?.FieldType.GetField("standingFrameStart", AnyInstance);
|
||||
|
||||
// Main.gameMenu makes BoringSetup skip world lighting and Dust.NewDust spawn nothing at the UI position.
|
||||
public static void Draw(Player player, Rectangle bounds)
|
||||
{
|
||||
SpriteBatch spriteBatch = Widgets.SpriteBatch;
|
||||
bool gameMenu = Main.gameMenu;
|
||||
(int direction, int immuneAlpha, int wingFrame) = (player.direction, player.immuneAlpha, player.wingFrame);
|
||||
(Rectangle bodyFrame, Rectangle legFrame, Rectangle headFrame) = (player.bodyFrame, player.legFrame, player.headFrame);
|
||||
int selected = player.selectedItem, itemAnimation = player.itemAnimation;
|
||||
(Item held, Item visualized) = (player.inventory[selected], player.lastVisualizedSelectedItem);
|
||||
(Player.CompositeArmData frontArm, Player.CompositeArmData backArm) = (player.compositeFrontArm, player.compositeBackArm);
|
||||
bool mounted = player.mount.Active;
|
||||
object? mountFrame = mounted ? MountFrame?.GetValue(player.mount) : null;
|
||||
try
|
||||
{
|
||||
// Dye shaders apply per draw, so the batch must be immediate.
|
||||
spriteBatch.End();
|
||||
spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, DepthStencilState.None, RasterizerState.CullNone, null, Main.UIScaleMatrix);
|
||||
|
||||
Main.gameMenu = true;
|
||||
(player.direction, player.immuneAlpha, player.wingFrame) = (1, 0, 0);
|
||||
// Mounted legs hold the seated pose; reset only the mount and the attacking arm.
|
||||
if (mounted && MountData?.GetValue(player.mount) is { } data && StandingFrame?.GetValue(data) is int standing)
|
||||
MountFrame?.SetValue(player.mount, standing);
|
||||
if (mounted && itemAnimation > 0)
|
||||
player.bodyFrame.Y = 0;
|
||||
else if (!mounted)
|
||||
player.bodyFrame.Y = player.legFrame.Y = player.headFrame.Y = 0;
|
||||
// PlayerDrawSet takes the held item from lastVisualizedSelectedItem and the swing pose from the composite arms.
|
||||
(player.inventory[selected], player.lastVisualizedSelectedItem, player.itemAnimation) = (Blank, Blank, 0);
|
||||
player.compositeFrontArm.enabled = player.compositeBackArm.enabled = false;
|
||||
|
||||
var position = new Vector2(bounds.Center.X - player.width / 2f, bounds.Center.Y - player.height / 2f);
|
||||
Main.PlayerRenderer.DrawPlayer(Main.Camera, player, position + Main.screenPosition, 0f, Vector2.Zero, 0f, Scale);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("Player portrait", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Main.gameMenu = gameMenu;
|
||||
(player.direction, player.immuneAlpha, player.wingFrame) = (direction, immuneAlpha, wingFrame);
|
||||
(player.bodyFrame, player.legFrame, player.headFrame) = (bodyFrame, legFrame, headFrame);
|
||||
(player.inventory[selected], player.lastVisualizedSelectedItem, player.itemAnimation) = (held, visualized, itemAnimation);
|
||||
(player.compositeFrontArm, player.compositeBackArm) = (frontArm, backArm);
|
||||
if (mountFrame is not null)
|
||||
MountFrame!.SetValue(player.mount, mountFrame);
|
||||
}
|
||||
|
||||
Widgets.Recover(spriteBatch, Main.UIScaleMatrix);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ public abstract class Window(string id, string title, Vector2 position, int widt
|
||||
{
|
||||
public string Id { get; } = id;
|
||||
|
||||
public string Title { get; } = title;
|
||||
public virtual string Title { get; } = title;
|
||||
|
||||
public Vector2 Pos { get; set; } = position;
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ public static class WindowManager
|
||||
var title = new Rectangle(bounds.X, bounds.Y, bounds.Width, TitleHeight);
|
||||
if (hasTitle)
|
||||
{
|
||||
Render.TextMid(Widgets.SpriteBatch, window.Title, new Rectangle(bounds.X + 16, bounds.Y + 2, bounds.Width, TitleHeight), Color.White, 1.0f);
|
||||
Render.TextMid(Widgets.SpriteBatch, Render.Ellipsize(window.Title, 1.0f, bounds.Width - 64), new Rectangle(bounds.X + 16, bounds.Y + 2, bounds.Width, TitleHeight), Color.White, 1.0f);
|
||||
Widgets.Separator(bounds.X + 10, bounds.Y + TitleHeight, bounds.Width - 20);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user