feat: Add player list, player inspector and party frames

This commit is contained in:
VAC committed 2026-10-16 01:37:26 -03:00
1 parent d68de62cf5
commit 9d748d679b
19 files changed
+630 -7

No files matched your search

+2
View File
@@ -31,6 +31,8 @@ public static class Hotkeys
new(InterfaceGroup, "NpcSpawner", "NPC Spawner", () => WindowManager.Toggle(WindowManager.Npcs)),
new(InterfaceGroup, "ItemEditor", "Item Editor", () => WindowManager.Toggle(WindowManager.Editor)),
new(InterfaceGroup, "Stats", "Stats Overlay", () => WindowManager.Toggle(WindowManager.Stats)),
new(InterfaceGroup, "Players", "Player List", () => WindowManager.Toggle(WindowManager.Players)),
new(InterfaceGroup, "PartyFrames", "Party Frames", () => WindowManager.Toggle(WindowManager.Party)),
Toggle("Player", "GodMode", "God Mode", () => Settings.GodMode, v => Settings.GodMode = v),
Toggle("Player", "GhostMode", "Ghost Mode", () => Settings.GhostMode, v => Settings.GhostMode = v),
+2
View File
@@ -23,6 +23,8 @@ public static partial class Settings
[nameof(ShowStats)] = "Player.Stats.Enabled",
[nameof(StatLines)] = "Player.Stats.Lines",
[nameof(StatIcons)] = "Player.Stats.Icons",
[nameof(PartyFrames)] = "Player.PartyFrames.Enabled",
[nameof(PartySelf)] = "Player.PartyFrames.Self",
[nameof(Fly)] = "Movement.Fly.Enabled",
[nameof(FlySpeed)] = "Movement.Fly.Speed",
+1
View File
@@ -47,6 +47,7 @@ public static partial class Settings
public static bool AutoHeal, AutoMana, GhostMode, DefenseBoost;
public static float AutoHealAt = 40f, AutoManaAt = 25f, BuffMinutes = 10f, DefenseMult = 3f;
public static bool ShowStats, StatIcons = true;
public static bool PartyFrames, PartySelf;
public static StatLine StatLines = StatLine.Dps | StatLine.Damage | StatLine.Kills | StatLine.Minions | StatLine.Sentries | StatLine.Defense;
public static bool Fly, SuperSpeed, HighJump, NoFallDamage, LavaImmune, WaterWalk, NoKnockback, GravityFlip, ExtendedReach;
@@ -284,6 +284,13 @@ public static class PlayerFeatures
player.fallStart = (int)(player.position.Y / 16f);
}
// Same jump as the Wormhole Potion in Player.HandleSpectatingControls, without the potion.
public static void TeleportToPlayer(Player target)
{
Player player = Main.LocalPlayer;
player.UnityTeleport(player.position + target.Bottom - player.Bottom);
}
// Matches the Magic Mirror branch of Player.ItemCheck_Inner.
public static void TeleportSpawn()
{
+46
View File
@@ -0,0 +1,46 @@
using Terraria;
namespace TerraSharp.Features;
public static class Spectate
{
public static bool Available => NpcSpawner.Multiplayer;
#if TMODLOADER
// tModLoader 1.4.4 has no spectating; SpectateCameraPatch follows this player instead.
public static int Target { get; private set; } = -1;
#else
// The client sets spectating to itself until the server confirms the target.
public static int Target => Main.LocalPlayer.spectating is var who && who != Main.myPlayer ? who : -1;
#endif
public static void Toggle(int who)
{
if (Target == who)
Stop();
else
Start(who);
}
static void Start(int who)
{
if (!Available)
return;
#if TMODLOADER
Target = who;
#else
Main.LocalPlayer.SetOrRequestSpectating(who);
#endif
}
public static void Stop()
{
#if TMODLOADER
Target = -1;
#else
if (Main.LocalPlayer.spectating >= 0)
Main.LocalPlayer.SetOrRequestSpectating(-1);
#endif
}
}
+35
View File
@@ -5,6 +5,9 @@ using MonoMod.Cil;
using Terraria;
using Terraria.Graphics.Light;
using TerraSharp.Core;
#if TMODLOADER
using TerraSharp.Features;
#endif
namespace TerraSharp.Patches;
@@ -14,6 +17,9 @@ internal static class VisionPatches
{
FullBrightPatch.Install(hooks);
ZoomPatch.Install(hooks);
#if TMODLOADER
SpectateCameraPatch.Install(hooks);
#endif
}
}
@@ -78,3 +84,32 @@ internal static class ZoomPatch
throw new MissingMethodException(context.Method.FullName, "Clamp");
}
}
#if TMODLOADER
// Centers the camera the way Player.SpectatingCameraPosition does in 1.4.5.
internal static class SpectateCameraPatch
{
internal static void Install(HookRegistry hooks) => hooks.Add(typeof(Main), "DoDraw_UpdateCameraPosition", (Action<Action>)UpdateCamera);
static void UpdateCamera(Action original)
{
original();
Guard.Run("Spectate camera", Follow);
}
static void Follow()
{
if (Spectate.Target < 0)
return;
if (!Plugin.Active || Main.gameMenu || !Spectate.Available || Main.player[Spectate.Target] is not { active: true } target)
{
Spectate.Stop();
return;
}
Vector2 center = target.Bottom + new Vector2(0f, target.gfxOffY - 21f);
Main.screenPosition = (center - new Vector2(Main.screenWidth, Main.screenHeight) / 2f).Floor();
}
}
#endif
+1 -1
View File
@@ -45,7 +45,7 @@ public sealed class BuffBrowser() : GridBrowser("buffs", "Buff Browser", new Vec
static bool IsMount(int type) => BuffID.Sets.MountType[type] >= 0;
#endif
static string FormatTime(int ticks)
public static string FormatTime(int ticks)
{
int seconds = ticks / 60;
return seconds >= 3600 ? $"{seconds / 3600}h" : seconds >= 60 ? $"{seconds / 60}m" : $"{seconds}s";
+6 -1
View File
@@ -31,6 +31,7 @@ public static class InterfaceLayers
return;
layers.RemoveAll(l => l.Name.StartsWith(Prefix, StringComparison.Ordinal));
PlayerHeads.Install();
layers.Insert(0, new LegacyGameInterfaceLayer(Prefix + "Under UI", DrawUnderUi, InterfaceScaleType.None));
@@ -45,7 +46,11 @@ public static class InterfaceLayers
layers.Insert(mouseTextIndex < 0 ? layers.Count : mouseTextIndex, new LegacyGameInterfaceLayer(Prefix + "Windows", DrawWindows, InterfaceScaleType.UI));
}
public static void Uninstall() => Layers?.RemoveAll(l => l.Name.StartsWith(Prefix, StringComparison.Ordinal));
public static void Uninstall()
{
Layers?.RemoveAll(l => l.Name.StartsWith(Prefix, StringComparison.Ordinal));
PlayerHeads.Uninstall();
}
static bool DrawUnderUi()
{
+6
View File
@@ -52,6 +52,12 @@ public sealed partial class MainWindow
Group("Stats");
if (Toggle("Stats Overlay", ref Settings.ShowStats) && Buttons("Configure Overlay") == 0)
WindowManager.StatsSetup.ShowBeside(Bounds);
Group("Party");
if (Toggle("Party Frames", ref Settings.PartyFrames))
Sub(() => Toggle("Include yourself", ref Settings.PartySelf));
if (Buttons(WindowManager.Players.Open ? "Close Player List" : "Player List") == 0)
WindowManager.Toggle(WindowManager.Players);
}
void MovementPage()
+1
View File
@@ -14,6 +14,7 @@ public sealed partial class MainWindow
["Auto Mana"] = "Uses Quick Mana below the set mana level.",
["Ghost Mode"] = "Enemies stop chasing you. Bosses and some flying enemies still attack.",
["Defense Multiplier"] = "Multiplies your total defense.",
["Party Frames"] = "Shows your teammates' health and mana on screen. Open the menu to move it.",
["Stats Overlay"] = "Shows the selected stats on screen. Damage, kills and deaths count from when you enter the world.",
["Configure Overlay"] = "Choose the lines the overlay shows.",
["Fly"] = "Move through blocks with your movement keys. Up or Jump rises; Down descends.",
+1 -1
View File
@@ -30,7 +30,7 @@ public sealed partial class MainWindow : Window
Open = true;
_pages =
[
new Page("Player", PlayerPage, () => Count(Settings.GodMode, Settings.InfHealth, Settings.InfMana, Settings.InfBreath, Settings.InfFlight, Settings.InstantRespawn, Settings.AutoHeal, Settings.AutoMana, Settings.GhostMode, Settings.DefenseBoost, Settings.ShowStats)),
new Page("Player", PlayerPage, () => Count(Settings.GodMode, Settings.InfHealth, Settings.InfMana, Settings.InfBreath, Settings.InfFlight, Settings.InstantRespawn, Settings.AutoHeal, Settings.AutoMana, Settings.GhostMode, Settings.DefenseBoost, Settings.ShowStats, Settings.PartyFrames)),
new Page("Movement", MovementPage, () => Count(Settings.Fly, Settings.SuperSpeed, Settings.HighJump, Settings.GravityFlip, Settings.NoFallDamage, Settings.LavaImmune, Settings.WaterWalk, Settings.NoKnockback, Settings.ExtendedReach)),
new Page("Combat", CombatPage, () => Count(Settings.RapidAttack, Settings.AutoSwing, Settings.DamageBoost, Settings.NoManaCost, Settings.NoSpread, Settings.Aimbot, Settings.AutoAttack, Settings.KillAura, Settings.HitMagnet, Settings.FreezeEnemies, Settings.WeakenEnemies, Settings.MagicBullets, Settings.AutoDodge)),
new Page("Items", ItemsPage, () => Count(Settings.InstantBreak, Settings.InfItems, Settings.ItemVacuum, Settings.AutoSell, Settings.AutoFish, Settings.Sonar)),
+189
View File
@@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using Terraria.ID;
using TerraSharp.Core;
using TerraSharp.Features;
namespace TerraSharp.UI;
// Teammate health (similar to what we have in MMOs)
public sealed class PartyFrames() : Window("party", "", new Vector2(20, 440), FrameWidth, RowHeight)
{
const int FrameWidth = 240, RowHeight = 40, IconSize = 16;
static readonly Color Life = new(220, 50, 50), Mana = new(70, 110, 235);
public override bool Open { get => Settings.PartyFrames; set => Settings.PartyFrames = value; }
public override bool Frameless => true;
// Bounds from the last draw; input is resolved before the body draws.
readonly List<Rectangle> _frames = [], _eyes = [];
public override bool Interactive => !Main.gameMenu && (WindowManager.Menu.Open || _frames.Exists(frame => frame.Contains(Widgets.Mouse)));
public override bool Draggable => WindowManager.Menu.Open && !_eyes.Exists(eye => eye.Contains(Widgets.Mouse));
public override void DrawBody(Rectangle body)
{
if (Main.gameMenu)
return;
_frames.Clear();
_eyes.Clear();
Player local = Main.LocalPlayer;
int y = body.Y;
if (Settings.PartySelf)
y += Frame(local, body.X, y, body.Width);
for (int i = 0; i < Main.maxPlayers && local.team > 0; i++)
if (i != Main.myPlayer && Main.player[i] is { active: true } player && player.team == local.team)
y += Frame(player, body.X, y, body.Width);
Height = Math.Max(RowHeight, y - body.Y);
if (!WindowManager.Menu.Open)
return;
if (y == body.Y)
Render.TextMid(Widgets.SpriteBatch, "Party frames: no teammates", body, Widgets.Dim, 0.75f);
Render.Outline(Widgets.SpriteBatch, body, Widgets.Hot(body) ? Widgets.Yellow * 0.6f : Color.White * 0.2f);
}
// Returns the height used, including the gap below.
int Frame(Player player, int x, int y, int width)
{
int debuffs = 0;
foreach (int t in player.buffType)
if (ShownDebuff(t))
debuffs++;
var frame = new Rectangle(x, y, width, RowHeight - 4 + (debuffs > 0 ? IconSize + 3 : 0));
_frames.Add(frame);
float fade = player.dead ? 0.5f : 1f;
Render.FillSoft(Widgets.SpriteBatch, frame, Color.Black * 0.45f);
if (Spectate.Target == player.whoAmI)
Render.Outline(Widgets.SpriteBatch, frame, Widgets.Yellow * 0.8f);
var eye = new Rectangle(frame.Right - 21, frame.Y + 18, IconSize, IconSize);
Actions(player, frame, eye);
PlayerHeads.Draw(player, new Vector2(frame.X + 18, frame.Y + (RowHeight - 4) / 2f), 0.8f);
if (!player.dead && !player.HeldItem.IsAir)
Render.Item(Widgets.SpriteBatch, player.HeldItem.type, new Rectangle(eye.X, frame.Y + 2, IconSize, IconSize));
int barX = frame.X + 38, barWidth = frame.Right - 26 - barX;
Eye(player, eye);
// The name line fills from the right: life, defense, potion sickness, then the name in what is left.
int right = barX + barWidth;
string life = player.dead ? DeathText(player) : $"{player.statLife}/{player.statLifeMax2}";
right = Stat(life, right, frame.Y, player.dead ? null : GameTextures.Get("Heart"), null, (player.dead ? Widgets.Red : Color.White) * fade, null);
if (!player.dead && GameTextures.Get("Extra", 58) is { } shield)
{
int defense = (int)player.statDefense;
right = Stat($"{defense}", right - 4, frame.Y, shield, GameTextures.Frame(shield, 3, 2, 0, 0), Color.White * fade, $"{defense} {Lang.inter[10].Value}");
}
if (player.FindBuffIndex(BuffID.PotionSickness) is var sickness and >= 0)
{
right -= IconSize + 4;
PotionSickness(player, sickness, new Rectangle(right, frame.Y + 3, IconSize, IconSize));
}
Render.Text(Widgets.SpriteBatch, Render.Ellipsize(player.name, 0.7f, right - barX - 6), new Vector2(barX, frame.Y + 2), Render.TeamColor(player) * fade, 0.7f);
Bar(new Rectangle(barX, frame.Y + 21, barWidth, 8), player.dead ? 0f : player.statLife / (float)Math.Max(1, player.statLifeMax2), Life);
Bar(new Rectangle(barX, frame.Y + 31, barWidth, 3), player.dead ? 0f : player.statMana / (float)Math.Max(1, player.statManaMax2), Mana);
if (debuffs > 0)
Debuffs(player, new Rectangle(barX, frame.Y + RowHeight - 4, barWidth, IconSize));
return frame.Height + 4;
}
// Clicks act only while the menu is closed; with it open, a click drags the frames.
static void Actions(Player player, Rectangle frame, Rectangle eye)
{
bool other = player.whoAmI != Main.myPlayer && Spectate.Available;
if (!other || !Widgets.Hot(frame) || eye.Contains(Widgets.Mouse))
return;
Widgets.Tooltip = WindowManager.Menu.Open ? "Right-click to teleport" : "Click to spectate, right-click to teleport";
if (Widgets.Click && !WindowManager.Menu.Open)
Spectate.Toggle(player.whoAmI);
if (Widgets.RightClick && !player.dead)
PlayerFeatures.TeleportToPlayer(player);
}
void Eye(Player player, Rectangle bounds)
{
_eyes.Add(bounds);
bool hot = Widgets.Hot(bounds);
bool inspecting = WindowManager.Inspector.Open && WindowManager.Inspector.Target == player.whoAmI;
Widgets.Icon(GameTextures.Get(inspecting ? "InventoryTickOff" : "InventoryTickOn"), bounds, null, 1f, hot ? Widgets.Yellow : Color.White * 0.7f);
if (!hot)
return;
Widgets.Tooltip = inspecting ? "Close inspect" : "Inspect";
if (!Widgets.Click)
return;
if (inspecting)
WindowManager.Hide(WindowManager.Inspector);
else
WindowManager.Inspector.Inspect(player.whoAmI);
}
// respawnTimer only counts down where the game updates it; otherwise just "Dead".
static string DeathText(Player player) => player.respawnTimer > 0 ? $"Dead {(player.respawnTimer + 59) / 60}s" : "Dead";
// Draws an icon and value ending at right; returns where it starts.
static int Stat(string text, int right, int y, Texture2D? icon, Rectangle? frame, Color color, string? tooltip)
{
const int statIcon = 12;
int textWidth = (int)Render.Measure(text, 0.65f).X;
Render.Text(Widgets.SpriteBatch, text, new Vector2(right - textWidth, y + 3), color, 0.65f);
if (icon is null)
return right - textWidth;
var bounds = new Rectangle(right - textWidth - statIcon - 2, y + 4, statIcon, statIcon);
Widgets.Icon(icon, bounds, frame, 1f, color);
if (tooltip is not null && Widgets.Hot(new Rectangle(bounds.X, y + 2, right - bounds.X, IconSize)))
Widgets.Tooltip = tooltip;
return bounds.X;
}
static void PotionSickness(Player player, int index, Rectangle bounds)
{
Render.Item(Widgets.SpriteBatch, ItemID.HealingPotion, bounds, Color.White * 0.55f);
Render.Line(Widgets.SpriteBatch, new Vector2(bounds.X, bounds.Bottom), new Vector2(bounds.Right, bounds.Y), Widgets.Red, 2f);
if (Widgets.Hot(bounds))
Widgets.Tooltip = player.buffTime[index] > 0 ? $"{Lang.GetBuffName(BuffID.PotionSickness)}: {BuffBrowser.FormatTime(player.buffTime[index])}" : Lang.GetBuffName(BuffID.PotionSickness);
}
// Potion sickness has its own icon next to the name.
static bool ShownDebuff(int type) => type > 0 && type != BuffID.PotionSickness && Main.debuff[type];
static void Debuffs(Player player, Rectangle row)
{
int x = row.X;
for (int i = 0; i < player.buffType.Length && x + IconSize <= row.Right; i++)
{
int type = player.buffType[i];
if (!ShownDebuff(type))
continue;
var bounds = new Rectangle(x, row.Y, IconSize, IconSize);
Widgets.Icon(GameTextures.Get("Buff", type), bounds);
if (Widgets.Hot(bounds))
Widgets.Tooltip = player.buffTime[i] > 0 ? $"{Lang.GetBuffName(type)}: {BuffBrowser.FormatTime(player.buffTime[i])}" : Lang.GetBuffName(type);
x += IconSize + 2;
}
}
static void Bar(Rectangle bounds, float fraction, Color color)
{
Render.Fill(Widgets.SpriteBatch, bounds, Color.Black * 0.6f);
Render.Fill(Widgets.SpriteBatch, new Rectangle(bounds.X, bounds.Y, (int)(bounds.Width * MathHelper.Clamp(fraction, 0f, 1f)), bounds.Height), color);
}
}
+97
View File
@@ -0,0 +1,97 @@
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using Terraria.DataStructures;
using Terraria.GameContent;
using TerraSharp.Core;
namespace TerraSharp.UI;
// MapHeadRenderer with its own targets, so heads are always facing right (lol)
public sealed class PlayerHeads : INeedRenderTargetContent
{
static readonly PlayerHeads Instance = new();
readonly PlayerHeadDrawRenderTargetContent[] _heads = new PlayerHeadDrawRenderTargetContent[Main.maxPlayers];
bool _anyRequested;
PlayerHeads()
{
for (int i = 0; i < _heads.Length; i++)
_heads[i] = new PlayerHeadDrawRenderTargetContent();
}
public static void Install()
{
Main.ContentThatNeedsRenderTargets.Remove(Instance);
Main.ContentThatNeedsRenderTargets.Add(Instance);
}
public static void Uninstall()
{
Main.ContentThatNeedsRenderTargets.Remove(Instance);
Instance.Reset();
}
// Requested every frame, appearance changes show on the next frame.
public static void Draw(Player player, Vector2 center, float scale)
{
PlayerHeadDrawRenderTargetContent head = Instance._heads[player.whoAmI];
head.UsePlayer(player);
head.UseColor(Main.GetPlayerHeadBordersColor(player));
head.Request();
Instance._anyRequested = true;
if (!head.IsReady)
return;
RenderTarget2D target = head.GetTarget();
// DrawData constructors overload on ReLogic's Asset<T>, so set the fields directly.
var data = default(DrawData);
data.texture = target;
data.position = center;
data.sourceRect = target.Bounds;
data.color = Color.White;
data.origin = new Vector2(target.Width, target.Height) / 2f;
data.scale = new Vector2(scale);
PlayerDrawHelper.SetShaderForData(player, player.cHead, ref data);
data.Draw(Widgets.SpriteBatch);
Main.pixelShader.CurrentTechnique.Passes[0].Apply();
}
public bool IsReady => !_anyRequested;
// PlayerDrawHeadSet.BoringSetup reads the direction while the target renders.
public void PrepareRenderTarget(GraphicsDevice device, SpriteBatch spriteBatch)
{
if (!_anyRequested)
return;
_anyRequested = false;
for (int i = 0; i < _heads.Length; i++)
{
Player player = Main.player[i];
int direction = player.direction;
player.direction = 1;
try
{
_heads[i].PrepareRenderTarget(device, spriteBatch);
}
catch (Exception e)
{
Log.Error("Player head target", e);
}
finally
{
player.direction = direction;
}
}
}
public void Reset()
{
_anyRequested = false;
foreach (PlayerHeadDrawRenderTargetContent head in _heads)
head.Reset();
}
}
+155
View File
@@ -0,0 +1,155 @@
using Microsoft.Xna.Framework;
using Terraria;
using Terraria.UI;
namespace TerraSharp.UI;
// Read-only view of another player.
public sealed class PlayerInspector() : Window("inspect", "Inspect Player", new Vector2(700, 170), 470, 300)
{
enum Tab { Equipment, Inventory, Buffs }
const int SlotSize = 36, SlotStep = 40, Columns = 10, ContentTop = 72, FrameHeight = 62;
// 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];
Tab _tab;
public int Target { get; private set; } = -1;
public void Inspect(int who)
{
Target = who;
WindowManager.Show(this);
}
public override void DrawBody(Rectangle body)
{
if (Target < 0 || Main.player[Target] is not { active: true } player)
{
Render.TextMid(Widgets.SpriteBatch, "Player left the world.", new Rectangle(body.X + 2, body.Y, body.Width, 26), Widgets.Dim, 0.85f);
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;
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))
_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.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)
{
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);
}
int miscY = y + 3 * SlotStep + 8;
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);
}
return 4 * SlotStep + 8;
}
// 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;
for (int i = 0; i < 8; i++)
Slot(player.inventory[50 + i], x + (i < 4 ? i : i + 2) * SlotStep, extraY);
return 6 * SlotStep + 8;
}
static int DrawBuffs(Player player, int x, int y, Rectangle body)
{
int shown = 0;
for (int i = 0; i < player.buffType.Length; i++)
{
int type = player.buffType[i];
if (type <= 0)
continue;
var bounds = new Rectangle(x + shown % Columns * SlotStep, y + shown / Columns * SlotStep, SlotSize, SlotSize);
bool hot = Widgets.Hot(bounds);
Widgets.Slot(bounds, hot);
Widgets.Icon(GameTextures.Get("Buff", type), new Rectangle(bounds.X + 4, bounds.Y + 4, 28, 28));
// Remote buff times are only known when the game syncs them.
if (player.buffTime[i] > 0 && !Main.buffNoTimeDisplay[type])
Render.TextCentered(Widgets.SpriteBatch, BuffBrowser.FormatTime(player.buffTime[i]), new Rectangle(bounds.X, bounds.Bottom - 12, bounds.Width, 14), Color.White, 0.6f);
if (hot)
Widgets.Tooltip = $"{Lang.GetBuffName(type)}\n{Lang.GetBuffDescription(type)}";
shown++;
}
if (shown == 0)
{
Render.TextMid(Widgets.SpriteBatch, "No active buffs", new Rectangle(body.X + 2, y, body.Width, SlotSize), Widgets.Dim * 0.8f, 0.8f);
return SlotStep;
}
return (shown + Columns - 1) / Columns * SlotStep;
}
static void Slot(Item item, int x, int y, string background = "InventoryBack", int emptyIcon = -1, bool selected = false)
{
var bounds = new Rectangle(x, y, SlotSize, SlotSize);
bool hot = Widgets.Hot(bounds);
Widgets.Slot(bounds, hot, selected, background);
if (item.IsAir)
{
EmptyIcon(bounds, emptyIcon);
return;
}
Render.Item(Widgets.SpriteBatch, item.type, new Rectangle(bounds.X + 5, bounds.Y + 5, bounds.Width - 10, bounds.Height - 10));
if (item.stack > 1)
Render.Text(Widgets.SpriteBatch, item.stack.ToString(), new Vector2(bounds.X + 4, bounds.Bottom - 16), Color.White, 0.6f);
if (!hot)
return;
#if TMODLOADER
ItemSlot.MouseHover(ref item, ItemSlot.Context.InventoryItem);
#else
ItemSlot.MouseHover(item, ItemSlot.Context.InventoryItem);
#endif
}
// Frames are square, so the row count follows from the sheet size; it differs between 1.4.4 and 1.4.5.
static void EmptyIcon(Rectangle bounds, int icon)
{
if (icon < 0 || GameTextures.Get("Extra", 54) is not { } sheet)
return;
int size = sheet.Width / 3;
var frame = new Rectangle(icon % 3 * size, icon / 3 * size, size - 2, size - 2);
if (frame.Bottom > sheet.Height)
return;
Widgets.Icon(sheet, new Rectangle(bounds.X + 4, bounds.Y + 4, bounds.Width - 8, bounds.Height - 8), frame, 1f, Color.White * 0.35f);
}
}
+69
View File
@@ -0,0 +1,69 @@
using Microsoft.Xna.Framework;
using Terraria;
using TerraSharp.Features;
namespace TerraSharp.UI;
public sealed class PlayerList() : Window("players", "Players", new Vector2(640, 150), 600, 440)
{
const int RowHeight = 48, ButtonWidth = 84, ButtonGap = 6;
readonly Scroll _scroll = new();
public override void DrawBody(Rectangle body)
{
int online = 0;
for (int i = 0; i < Main.maxPlayers; i++)
if (Main.player[i].active)
online++;
Render.TextMid(Widgets.SpriteBatch, $"{online} online", new Rectangle(body.X + 2, body.Y, body.Width, 26), Widgets.Dim, 0.82f);
var area = new Rectangle(body.X, body.Y + 32, body.Width - 22, body.Height - 32);
int y = _scroll.Begin(area), top = y;
for (int i = 0; i < Main.maxPlayers; i++)
{
if (!Main.player[i].active)
continue;
Row(Main.player[i], new Rectangle(area.X, y, area.Width, RowHeight - 4));
y += RowHeight;
}
_scroll.End(area, y - top);
}
static void Row(Player player, Rectangle row)
{
Widgets.Panel(row, Widgets.InnerBg);
PlayerHeads.Draw(player, new Vector2(row.X + 24, row.Y + row.Height / 2f), 1f);
int textX = row.X + 50, buttonsX = row.Right - 3 * (ButtonWidth + ButtonGap);
bool local = player.whoAmI == Main.myPlayer;
string name = Render.Ellipsize(local ? player.name + " (you)" : player.name, 0.85f, buttonsX - textX - 8);
Render.Text(Widgets.SpriteBatch, name, new Vector2(textX, row.Y + 4), Render.TeamColor(player), 0.85f);
Render.Text(Widgets.SpriteBatch, Details(player, local), new Vector2(textX, row.Y + 24), player.dead ? Widgets.Red : Widgets.Dim, 0.72f);
var button = new Rectangle(buttonsX, row.Y + (row.Height - 28) / 2, ButtonWidth, 28);
if (!local && !player.dead && Spectate.Available && Widgets.Button(button, "Teleport", false, 0.78f))
PlayerFeatures.TeleportToPlayer(player);
button.X += ButtonWidth + ButtonGap;
if (!local && Spectate.Available && Widgets.Button(button, Spectate.Target == player.whoAmI ? "Stop" : "Spectate", Spectate.Target == player.whoAmI, 0.78f))
Spectate.Toggle(player.whoAmI);
button.X += ButtonWidth + ButtonGap;
bool inspecting = WindowManager.Inspector.Open && WindowManager.Inspector.Target == player.whoAmI;
if (Widgets.Button(button, "Inspect", inspecting, 0.78f))
WindowManager.Inspector.Inspect(player.whoAmI);
}
static string Details(Player player, bool local)
{
if (player.dead)
return "Dead";
string stats = $"Life {player.statLife}/{player.statLifeMax2} Mana {player.statMana}/{player.statManaMax2}";
return local ? stats : $"{stats} {(int)(Vector2.Distance(player.Center, Main.LocalPlayer.Center) / 16f)} tiles";
}
}
+2
View File
@@ -142,5 +142,7 @@ public static class Render
spriteBatch.Draw(texture, new Vector2(bounds.X + (bounds.Width - size.X) / 2f, bounds.Y + (bounds.Height - size.Y) / 2f), frame, color ?? Color.White, 0f, Vector2.Zero, scale, SpriteEffects.None, 0f);
}
public static Color TeamColor(Player player) => player.team > 0 ? Main.teamColor[player.team] : Color.White;
public static Vector2 ToScreen(Vector2 world) => Vector2.Transform(world - Main.screenPosition, Main.GameViewMatrix.TransformationMatrix);
}
+2 -2
View File
@@ -261,9 +261,9 @@ public static class Widgets
Render.Fill(SpriteBatch, new Rectangle(bounds.X + 1, bounds.Y + 1, bounds.Width - 2, bounds.Height - 2), color);
}
public static void Slot(Rectangle bounds, bool hot, bool selected = false)
public static void Slot(Rectangle bounds, bool hot, bool selected = false, string background = "InventoryBack")
{
if (GameTextures.Get(selected ? "InventoryBack14" : hot ? "InventoryBack15" : "InventoryBack") is { } back)
if (GameTextures.Get(selected ? "InventoryBack14" : hot ? "InventoryBack15" : background) is { } back)
SpriteBatch.Draw(back, bounds, SlotTint);
else
Panel(bounds, ButtonBg);
+3
View File
@@ -25,6 +25,9 @@ public abstract class Window(string id, string title, Vector2 position, int widt
// A window that is not interactive lets the mouse through to the game.
public virtual bool Interactive => true;
// Whether a click on a frameless window starts dragging it.
public virtual bool Draggable => true;
public Rectangle Bounds => new((int)Pos.X, (int)Pos.Y, Width, Height);
public abstract void DrawBody(Rectangle body);
+5 -2
View File
@@ -20,11 +20,14 @@ public static class WindowManager
public static readonly ItemPicker ChestList = new("chestList", "Chest Finder Items", new Vector2(700, 140), Settings.ChestList);
public static readonly StatsWindow Stats = new();
public static readonly StatsConfig StatsSetup = new();
public static readonly PlayerList Players = new();
public static readonly PlayerInspector Inspector = new();
public static readonly PartyFrames Party = new();
const int TitleHeight = 40;
const int MaxInputLength = 40;
static readonly List<Window> Stack = [Items, Buffs, Npcs, Editor, SellList, ChestList, Stats, Menu, StatsSetup];
static readonly List<Window> Stack = [Party, Items, Buffs, Npcs, Editor, SellList, ChestList, Stats, Players, Inspector, Menu, StatsSetup];
static Window? _dragging;
static Vector2 _dragOffset;
@@ -169,7 +172,7 @@ public static class WindowManager
Rectangle bounds = window.Bounds;
if (window.Frameless)
{
bool hot = Widgets.Hot(bounds);
bool hot = Widgets.Hot(bounds) && window.Draggable;
if (hot)
Widgets.Tooltip = "Drag to move";
if (hot && Widgets.Click)