feat: Add configurable stats overlay
This commit is contained in:
1 parent
6701dd0305
commit
d68de62cf5
12 files changed
+312
-7
No files matched your search
@@ -30,6 +30,7 @@ public static class Hotkeys
|
||||
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)),
|
||||
new(InterfaceGroup, "Stats", "Stats Overlay", () => WindowManager.Toggle(WindowManager.Stats)),
|
||||
|
||||
Toggle("Player", "GodMode", "God Mode", () => Settings.GodMode, v => Settings.GodMode = v),
|
||||
Toggle("Player", "GhostMode", "Ghost Mode", () => Settings.GhostMode, v => Settings.GhostMode = v),
|
||||
|
||||
@@ -20,6 +20,9 @@ public static partial class Settings
|
||||
[nameof(GhostMode)] = "Player.GhostMode",
|
||||
[nameof(DefenseBoost)] = "Player.DefenseBoost.Enabled",
|
||||
[nameof(DefenseMult)] = "Player.DefenseBoost.Multiplier",
|
||||
[nameof(ShowStats)] = "Player.Stats.Enabled",
|
||||
[nameof(StatLines)] = "Player.Stats.Lines",
|
||||
[nameof(StatIcons)] = "Player.Stats.Icons",
|
||||
|
||||
[nameof(Fly)] = "Movement.Fly.Enabled",
|
||||
[nameof(FlySpeed)] = "Movement.Fly.Speed",
|
||||
|
||||
@@ -19,12 +19,35 @@ public enum EspBoxStyle
|
||||
Filled,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum StatLine
|
||||
{
|
||||
None = 0,
|
||||
Dps = 1 << 0,
|
||||
PeakDps = 1 << 1,
|
||||
Damage = 1 << 2,
|
||||
BestHit = 1 << 3,
|
||||
Kills = 1 << 4,
|
||||
Time = 1 << 5,
|
||||
Deaths = 1 << 6,
|
||||
Minions = 1 << 7,
|
||||
Sentries = 1 << 8,
|
||||
Defense = 1 << 9,
|
||||
DamageReduction = 1 << 10,
|
||||
Crit = 1 << 11,
|
||||
LifeRegen = 1 << 12,
|
||||
Speed = 1 << 13,
|
||||
Luck = 1 << 14,
|
||||
}
|
||||
|
||||
// Public mutable fields are persisted through Settings.Json.cs.
|
||||
public static partial class Settings
|
||||
{
|
||||
public static bool GodMode, InfHealth, InfMana, InfBreath, InfFlight, InstantRespawn;
|
||||
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 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;
|
||||
public static float FlySpeed = 12f, SpeedMult = 3f;
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using Terraria;
|
||||
|
||||
namespace TerraSharp.Features;
|
||||
|
||||
// Damage is the life the local player removed from NPCs, read from per-tick life changes.
|
||||
public static class Stats
|
||||
{
|
||||
const int DpsTicks = 180;
|
||||
|
||||
static readonly int[] LastLife = new int[Main.maxNPCs], LastType = new int[Main.maxNPCs];
|
||||
static readonly int[] Recent = new int[DpsTicks];
|
||||
static int _recentIndex, _recentSum;
|
||||
static int _world = -1;
|
||||
static bool _wasDead;
|
||||
|
||||
public static long Damage { get; private set; }
|
||||
|
||||
public static int BestHit { get; private set; }
|
||||
|
||||
public static int Kills { get; private set; }
|
||||
|
||||
public static int Deaths { get; private set; }
|
||||
|
||||
public static int Ticks { get; private set; }
|
||||
|
||||
public static float Dps => _recentSum * 60f / DpsTicks;
|
||||
|
||||
public static float PeakDps { get; private set; }
|
||||
|
||||
public static void Reset()
|
||||
{
|
||||
Array.Clear(Recent, 0, Recent.Length);
|
||||
(_recentIndex, _recentSum) = (0, 0);
|
||||
(Damage, BestHit, Kills, Deaths, Ticks, PeakDps) = (0, 0, 0, 0, 0, 0f);
|
||||
}
|
||||
|
||||
// Player.Update postfix for the local player.
|
||||
public static void Update(Player player)
|
||||
{
|
||||
if (Main.worldID != _world)
|
||||
{
|
||||
_world = Main.worldID;
|
||||
Reset();
|
||||
}
|
||||
|
||||
Ticks++;
|
||||
if (player.dead && !_wasDead)
|
||||
Deaths++;
|
||||
_wasDead = player.dead;
|
||||
|
||||
int dealt = 0;
|
||||
for (int i = 0; i < Main.maxNPCs; i++)
|
||||
{
|
||||
NPC npc = Main.npc[i];
|
||||
int life = npc.active ? Math.Max(0, npc.life) : 0;
|
||||
bool killed = !npc.active && npc.life <= 0;
|
||||
// Worm segments share one life pool owned by the head.
|
||||
bool counts = LastType[i] != 0 && npc.type == LastType[i] && npc.lastInteraction == Main.myPlayer && (npc.realLife < 0 || npc.realLife == i) && (npc.active || killed);
|
||||
if (counts && life < LastLife[i])
|
||||
{
|
||||
int hit = LastLife[i] - life;
|
||||
dealt += hit;
|
||||
BestHit = Math.Max(BestHit, hit);
|
||||
if (life == 0 && !npc.friendly && !npc.townNPC && npc.lifeMax > 5)
|
||||
Kills++;
|
||||
}
|
||||
|
||||
(LastType[i], LastLife[i]) = npc.active ? (npc.type, life) : (0, 0);
|
||||
}
|
||||
|
||||
Damage += dealt;
|
||||
_recentSum += dealt - Recent[_recentIndex];
|
||||
Recent[_recentIndex] = dealt;
|
||||
_recentIndex = (_recentIndex + 1) % DpsTicks;
|
||||
PeakDps = Math.Max(PeakDps, Dps);
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ internal static class PlayerUpdatePatch
|
||||
{
|
||||
Clock.Advance();
|
||||
PlayerFeatures.PostUpdate(player);
|
||||
Stats.Update(player);
|
||||
CombatFeatures.PostPlayerUpdate(player);
|
||||
Ballistics.Observe();
|
||||
NpcMotion.Track();
|
||||
|
||||
@@ -48,6 +48,10 @@ public sealed partial class MainWindow
|
||||
case 0: PlayerFeatures.FullHeal(); break;
|
||||
case 1: PlayerFeatures.TeleportSpawn(); break;
|
||||
}
|
||||
|
||||
Group("Stats");
|
||||
if (Toggle("Stats Overlay", ref Settings.ShowStats) && Buttons("Configure Overlay") == 0)
|
||||
WindowManager.StatsSetup.ShowBeside(Bounds);
|
||||
}
|
||||
|
||||
void MovementPage()
|
||||
|
||||
@@ -14,6 +14,8 @@ 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.",
|
||||
["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.",
|
||||
["Gravity Flip"] = "Walk on the ceiling.",
|
||||
["Lava Immune"] = "Also protects against hot blocks.",
|
||||
|
||||
@@ -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)),
|
||||
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("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)),
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Terraria;
|
||||
using TerraSharp.Core;
|
||||
using TerraSharp.Features;
|
||||
|
||||
namespace TerraSharp.UI;
|
||||
|
||||
public sealed class StatsConfig() : Window("statsConfig", "Stats Overlay", Vector2.Zero, 300, 640)
|
||||
{
|
||||
const int RowHeight = 30;
|
||||
|
||||
public override bool Popup => true;
|
||||
|
||||
public void ShowBeside(Rectangle anchor)
|
||||
{
|
||||
Pos = new Vector2(anchor.Right + 8, anchor.Y);
|
||||
WindowManager.Show(this);
|
||||
}
|
||||
|
||||
public override void DrawBody(Rectangle body)
|
||||
{
|
||||
int y = body.Y;
|
||||
Render.TextMid(Widgets.SpriteBatch, "Drag the overlay while the menu is open.", new Rectangle(body.X + 2, y, body.Width, 22), Widgets.Dim * 0.8f, 0.7f);
|
||||
y += 26;
|
||||
|
||||
Settings.StatIcons = Widgets.ToggleRow(new Rectangle(body.X, y, body.Width, 28), "Icons", Settings.StatIcons);
|
||||
y += RowHeight;
|
||||
Widgets.Separator(body.X, y + 2, body.Width);
|
||||
y += 10;
|
||||
|
||||
foreach ((StatLine line, string label, _) in StatsWindow.Lines)
|
||||
{
|
||||
bool shown = Widgets.ToggleRow(new Rectangle(body.X, y, body.Width, 28), label, (Settings.StatLines & line) != 0);
|
||||
Settings.StatLines = shown ? Settings.StatLines | line : Settings.StatLines & ~line;
|
||||
y += RowHeight;
|
||||
}
|
||||
|
||||
if (Widgets.Button(new Rectangle(body.X, body.Bottom - 32, body.Width, 32), "Reset Stats", false, 0.82f))
|
||||
Stats.Reset();
|
||||
if (Widgets.Hot(new Rectangle(body.X, body.Bottom - 32, body.Width, 32)))
|
||||
Widgets.Tooltip = "Clears damage, kills, deaths and session time.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Terraria;
|
||||
using Terraria.ID;
|
||||
using TerraSharp.Core;
|
||||
using TerraSharp.Features;
|
||||
|
||||
namespace TerraSharp.UI;
|
||||
|
||||
// Overlay without a frame; it only takes the mouse, to be dragged, while the menu is open.
|
||||
public sealed class StatsWindow() : Window("stats", "", new Vector2(20, 280), PanelWidth, RowHeight)
|
||||
{
|
||||
const int PanelWidth = 240, RowHeight = 24, IconSize = 20;
|
||||
const float TextScale = 0.8f;
|
||||
|
||||
// Main.DrawInfoAccs converts velocity to the stopwatch's miles per hour this way.
|
||||
const float MilesPerHour = 216000f / 42240f;
|
||||
|
||||
// Icons are the accessories and items that show or affect each stat in vanilla.
|
||||
public static readonly (StatLine Line, string Label, int Icon)[] Lines =
|
||||
[
|
||||
(StatLine.Dps, "DPS", ItemID.DPSMeter),
|
||||
(StatLine.PeakDps, "Peak DPS", ItemID.DestroyerEmblem),
|
||||
(StatLine.Damage, "Damage dealt", ItemID.WarriorEmblem),
|
||||
(StatLine.BestHit, "Best hit", ItemID.AvengerEmblem),
|
||||
(StatLine.Kills, "Kills", ItemID.TallyCounter),
|
||||
(StatLine.Time, "Session time", ItemID.GoldWatch),
|
||||
(StatLine.Deaths, "Deaths", ItemID.Tombstone),
|
||||
(StatLine.Minions, "Minions", ItemID.SlimeStaff),
|
||||
(StatLine.Sentries, "Sentries", ItemID.QueenSpiderStaff),
|
||||
(StatLine.Defense, "Defense", ItemID.CobaltShield),
|
||||
(StatLine.DamageReduction, "Damage reduction", ItemID.WormScarf),
|
||||
(StatLine.Crit, "Critical chance", ItemID.EyeoftheGolem),
|
||||
(StatLine.LifeRegen, "Life regen", ItemID.BandofRegeneration),
|
||||
(StatLine.Speed, "Speed", ItemID.Stopwatch),
|
||||
(StatLine.Luck, "Luck", ItemID.LuckPotion),
|
||||
];
|
||||
|
||||
public override bool Open { get => Settings.ShowStats; set => Settings.ShowStats = value; }
|
||||
|
||||
public override bool Frameless => true;
|
||||
|
||||
public override bool Interactive => WindowManager.Menu.Open && !Main.gameMenu;
|
||||
|
||||
public override void DrawBody(Rectangle body)
|
||||
{
|
||||
if (Main.gameMenu)
|
||||
return;
|
||||
|
||||
if (Interactive)
|
||||
Render.Outline(Widgets.SpriteBatch, body, Widgets.Hot(body) ? Widgets.Yellow * 0.6f : Color.White * 0.2f);
|
||||
|
||||
Player player = Main.LocalPlayer;
|
||||
int y = body.Y, shown = 0;
|
||||
foreach ((StatLine line, string label, int icon) in Lines)
|
||||
{
|
||||
if ((Settings.StatLines & line) == 0)
|
||||
continue;
|
||||
|
||||
Row(new Rectangle(body.X, y, body.Width, RowHeight), label, Value(line, player), Settings.StatIcons ? icon : null);
|
||||
y += RowHeight;
|
||||
shown++;
|
||||
}
|
||||
|
||||
Height = Math.Max(1, shown) * RowHeight;
|
||||
}
|
||||
|
||||
static string Value(StatLine line, Player player) => line switch
|
||||
{
|
||||
StatLine.Dps => $"{Stats.Dps:0}",
|
||||
StatLine.PeakDps => $"{Stats.PeakDps:0}",
|
||||
StatLine.Damage => $"{Stats.Damage:N0}",
|
||||
StatLine.BestHit => $"{Stats.BestHit:N0}",
|
||||
StatLine.Kills => $"{Stats.Kills:N0}",
|
||||
StatLine.Time => SessionTime(),
|
||||
StatLine.Deaths => $"{Stats.Deaths}",
|
||||
StatLine.Minions => $"{player.slotsMinions:0.#} / {player.maxMinions}",
|
||||
StatLine.Sentries => $"{Sentries(player)} / {player.maxTurrets}",
|
||||
StatLine.Defense => $"{(int)player.statDefense}",
|
||||
StatLine.DamageReduction => $"{player.endurance:0%}",
|
||||
StatLine.Crit => player.HeldItem.damage > 0 ? $"{player.GetWeaponCrit(player.HeldItem)}%" : "-",
|
||||
StatLine.LifeRegen => $"{player.lifeRegen / 2f:0.#}/s",
|
||||
StatLine.Speed => $"{player.velocity.Length() * MilesPerHour:0} mph",
|
||||
StatLine.Luck => $"{player.luck:0.00}",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
static string SessionTime()
|
||||
{
|
||||
TimeSpan time = TimeSpan.FromSeconds(Stats.Ticks / 60);
|
||||
return $"{(int)time.TotalHours}:{time.Minutes:00}:{time.Seconds:00}";
|
||||
}
|
||||
|
||||
static int Sentries(Player player)
|
||||
{
|
||||
int count = 0;
|
||||
for (int i = 0; i < Main.maxProjectiles; i++)
|
||||
if (Main.projectile[i] is { active: true, sentry: true } projectile && projectile.owner == player.whoAmI)
|
||||
count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
static void Row(Rectangle row, string label, string value, int? icon)
|
||||
{
|
||||
int textX = row.X;
|
||||
if (icon is { } type)
|
||||
{
|
||||
Render.Item(Widgets.SpriteBatch, type, new Rectangle(row.X, row.Y + (row.Height - IconSize) / 2, IconSize, IconSize));
|
||||
textX += IconSize + 6;
|
||||
}
|
||||
|
||||
Render.TextMid(Widgets.SpriteBatch, label, new Rectangle(textX, row.Y, row.Right - textX, row.Height), Widgets.Dim, TextScale);
|
||||
Render.TextRight(Widgets.SpriteBatch, value, row, Color.White, TextScale);
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,18 @@ public abstract class Window(string id, string title, Vector2 position, int widt
|
||||
|
||||
public int Width { get; } = width;
|
||||
|
||||
public int Height { get; } = height;
|
||||
public int Height { get; protected set; } = height;
|
||||
|
||||
public bool Open { get; set; }
|
||||
public virtual bool Open { get; set; }
|
||||
|
||||
// Draws only the body: no panel, title or close button.
|
||||
public virtual bool Frameless => false;
|
||||
|
||||
// Closes on a click outside it or when the menu closes.
|
||||
public virtual bool Popup => false;
|
||||
|
||||
// A window that is not interactive lets the mouse through to the game.
|
||||
public virtual bool Interactive => true;
|
||||
|
||||
public Rectangle Bounds => new((int)Pos.X, (int)Pos.Y, Width, Height);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using Terraria;
|
||||
@@ -17,11 +18,13 @@ public static class WindowManager
|
||||
public static readonly ItemEditor Editor = new();
|
||||
public static readonly ItemPicker SellList = new("sellList", "Auto Sell List", new Vector2(680, 130), Settings.SellList);
|
||||
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();
|
||||
|
||||
const int TitleHeight = 40;
|
||||
const int MaxInputLength = 40;
|
||||
|
||||
static readonly List<Window> Stack = [Items, Buffs, Npcs, Editor, SellList, ChestList, Menu];
|
||||
static readonly List<Window> Stack = [Items, Buffs, Npcs, Editor, SellList, ChestList, Stats, Menu, StatsSetup];
|
||||
static Window? _dragging;
|
||||
static Vector2 _dragOffset;
|
||||
|
||||
@@ -29,11 +32,11 @@ public static class WindowManager
|
||||
|
||||
public static bool MouseOver { get; private set; }
|
||||
|
||||
public static bool AnyOpen => Stack.Exists(w => w.Open);
|
||||
public static bool AnyOpen => Stack.Exists(w => w.Open && w.Interactive);
|
||||
|
||||
public static IEnumerable<Window> All => Stack;
|
||||
|
||||
public static bool Contains(Point point) => Stack.Exists(w => w.Open && w.Bounds.Contains(point));
|
||||
public static bool Contains(Point point) => Stack.Exists(w => w.Open && w.Interactive && w.Bounds.Contains(point));
|
||||
|
||||
public static void Show(Window window)
|
||||
{
|
||||
@@ -55,6 +58,10 @@ public static class WindowManager
|
||||
window.Open = false;
|
||||
Widgets.Sound(11);
|
||||
Unfocus();
|
||||
if (window == Menu)
|
||||
foreach (Window popup in Stack)
|
||||
if (popup.Popup)
|
||||
popup.Open = false;
|
||||
}
|
||||
|
||||
public static void Toggle(Window window)
|
||||
@@ -122,8 +129,10 @@ public static class WindowManager
|
||||
foreach (Window window in Stack)
|
||||
window.Pos = ClampPosition(window, window.Pos);
|
||||
|
||||
Window? top = Stack.FindLast(w => w.Open && w.Bounds.Contains(Widgets.Mouse));
|
||||
Window? top = Stack.FindLast(w => w.Open && w.Interactive && w.Bounds.Contains(Widgets.Mouse));
|
||||
MouseOver = Widgets.InputOk && top is not null;
|
||||
// Popups the click landed outside of; a button that toggles one this frame still works.
|
||||
Window[] dismissed = Widgets.Click ? [.. Stack.Where(w => w.Open && w.Popup && w != top)] : [];
|
||||
if (Widgets.Click)
|
||||
if (top is null)
|
||||
Unfocus();
|
||||
@@ -148,6 +157,8 @@ public static class WindowManager
|
||||
}
|
||||
|
||||
Widgets.Blocked = false;
|
||||
foreach (Window popup in dismissed)
|
||||
popup.Open = false;
|
||||
|
||||
if (Widgets.Tooltip is not null && MouseOver)
|
||||
Main.instance.MouseText(Widgets.Tooltip, 0, 0, -1, -1, -1, -1, 0);
|
||||
@@ -156,6 +167,21 @@ public static class WindowManager
|
||||
static void DrawFrame(Window window)
|
||||
{
|
||||
Rectangle bounds = window.Bounds;
|
||||
if (window.Frameless)
|
||||
{
|
||||
bool hot = Widgets.Hot(bounds);
|
||||
if (hot)
|
||||
Widgets.Tooltip = "Drag to move";
|
||||
if (hot && Widgets.Click)
|
||||
{
|
||||
_dragging = window;
|
||||
_dragOffset = window.Pos - new Vector2(Widgets.Mouse.X, Widgets.Mouse.Y);
|
||||
}
|
||||
|
||||
window.DrawBody(bounds);
|
||||
return;
|
||||
}
|
||||
|
||||
Render.Fill(Widgets.SpriteBatch, new Rectangle(bounds.X + 4, bounds.Y + 4, bounds.Width - 8, bounds.Height - 8), new Color(16, 20, 40) * 0.92f);
|
||||
Widgets.Panel(bounds, Widgets.WindowBg);
|
||||
|
||||
|
||||
Reference in new issue
Block a user