diff --git a/src/TerraSharp/UI/PlayerInspector.cs b/src/TerraSharp/UI/PlayerInspector.cs index 2ce1125..62f55db 100644 --- a/src/TerraSharp/UI/PlayerInspector.cs +++ b/src/TerraSharp/UI/PlayerInspector.cs @@ -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) diff --git a/src/TerraSharp/UI/PlayerPortrait.cs b/src/TerraSharp/UI/PlayerPortrait.cs new file mode 100644 index 0000000..dc47b88 --- /dev/null +++ b/src/TerraSharp/UI/PlayerPortrait.cs @@ -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); + } +} diff --git a/src/TerraSharp/UI/Window.cs b/src/TerraSharp/UI/Window.cs index 3189580..883f248 100644 --- a/src/TerraSharp/UI/Window.cs +++ b/src/TerraSharp/UI/Window.cs @@ -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; diff --git a/src/TerraSharp/UI/WindowManager.cs b/src/TerraSharp/UI/WindowManager.cs index 62f7014..9f5f0b5 100644 --- a/src/TerraSharp/UI/WindowManager.cs +++ b/src/TerraSharp/UI/WindowManager.cs @@ -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); }