Compare commits

..
20 Commits
Author SHA1 Message Date
VAC d68de62cf5 feat: Add configurable stats overlay 2026-10-16 00:39:04 -03:00
VAC 6701dd0305 feat: Add auto dodge 2026-10-16 00:39:02 -03:00
VAC afb8235c08 feat: Add magic bullets 2026-10-16 00:39:01 -03:00
VAC f84ad4faeb feat: Add chest finder ESP 2026-10-16 00:38:59 -03:00
VAC 7b768fde2c feat: Add auto sell list 2026-10-16 00:38:57 -03:00
VAC 8d6296676a feat: Add item editor with prefix picker 2026-10-16 00:38:56 -03:00
VAC 73998f1aaa feat: Add map teleport 2026-10-16 00:38:54 -03:00
VAC 9df9f0a322 feat: Add map reveal 2026-10-16 00:38:53 -03:00
VAC 2692887684 feat: Add extended zoom 2026-10-16 00:38:51 -03:00
VAC a9081d28d9 feat: Add full bright 2026-10-16 00:38:50 -03:00
VAC 73a90f47f1 feat: Add defense multiplier 2026-10-16 00:38:48 -03:00
VAC 7d5115ded4 feat: Add ghost mode 2026-10-16 00:38:47 -03:00
VAC 6b3dcccc7c refactor: Split item grid out of ItemBrowser 2026-10-16 00:38:45 -03:00
VAC 3195e7ba92 feat: Color damage numbers by damage class 2026-10-16 00:30:44 -03:00
VAC 746ad799a0 feat: Add boss cursor for off-screen bosses 2026-10-16 00:30:40 -03:00
VAC bcf4e71558 fix: Browser tabs cut off text labels 2026-10-16 00:29:39 -03:00
VAC be429a9448 fix: Window clamp applies the UI scale twice 2026-10-16 00:18:05 -03:00
VAC 993b91a46b feat: Add bank buttons under builder toggles 2026-10-16 00:08:37 -03:00
VAC 55150f3c47 feat: Add NPC spawner and spawn rate multiplier 2026-10-15 21:47:06 -03:00
VAC 5bf19308dc build: Set initial release version to 0.1.0 2026-10-14 23:46:11 -03:00
39 changed files with 1932 additions and 113 deletions

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>0.1.1</Version>
<Version>0.1.0</Version>
<TargetFrameworks>net48;net8.0</TargetFrameworks>
<PlatformTarget Condition="'$(TargetFramework)' == 'net48'">x86</PlatformTarget>
<PlatformTarget Condition="'$(TargetFramework)' == 'net8.0'">x64</PlatformTarget>
+16
View File
@@ -28,8 +28,13 @@ public static class Hotkeys
new(InterfaceGroup, "Menu", "Open / close menu", () => WindowManager.Toggle(WindowManager.Menu)),
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)),
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),
Toggle("Player", "DefenseBoost", "Defense Multiplier", () => Settings.DefenseBoost, v => Settings.DefenseBoost = v),
Toggle("Player", "AutoHeal", "Auto Heal", () => Settings.AutoHeal, v => Settings.AutoHeal = v),
Toggle("Player", "AutoMana", "Auto Mana", () => Settings.AutoMana, v => Settings.AutoMana = v),
new("Player", "FullHeal", "Full Heal", PlayerFeatures.FullHeal),
@@ -52,6 +57,8 @@ public static class Hotkeys
Toggle("Combat", "FreezeEnemies", "Freeze Enemies", () => Settings.FreezeEnemies, v => Settings.FreezeEnemies = v),
Toggle("Combat", "WeakenEnemies", "Weaken Enemies", () => Settings.WeakenEnemies, v => Settings.WeakenEnemies = v),
Toggle("Combat", "RapidAttack", "Rapid Attack", () => Settings.RapidAttack, v => Settings.RapidAttack = v),
Toggle("Combat", "MagicBullets", "Magic Bullets", () => Settings.MagicBullets, v => Settings.MagicBullets = v),
Toggle("Combat", "AutoDodge", "Auto Dodge", () => Settings.AutoDodge, v => Settings.AutoDodge = v),
new("Combat", "GatherAll", "Gather All Enemies", CombatFeatures.GatherAll),
new("Combat", "KillAll", "Kill All Enemies", CombatFeatures.KillAll),
@@ -59,6 +66,9 @@ 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),
Toggle("Items", "AutoSell", "Auto Sell", () => Settings.AutoSell, v => Settings.AutoSell = 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),
@@ -67,9 +77,15 @@ public static class Hotkeys
Toggle("Visuals", "BiomeSight", "Biome Sight", () => Settings.BiomeSight, v => Settings.BiomeSight = v),
Toggle("Visuals", "NightOwl", "Night Owl", () => Settings.NightOwl, v => Settings.NightOwl = v),
Toggle("Visuals", "Shine", "Shine", () => Settings.Shine, v => Settings.Shine = v),
Toggle("Visuals", "FullBright", "Full Bright", () => Settings.FullBright, v => Settings.FullBright = v),
Toggle("Visuals", "ExtendedZoom", "Extended Zoom", () => Settings.ExtendedZoom, v => Settings.ExtendedZoom = v),
Toggle("Visuals", "ChestFinder", "Chest Finder", () => Settings.EspChests, v => Settings.EspChests = v),
Toggle("World", "FreezeTime", "Freeze Time", () => Settings.FreezeTime, v => Settings.FreezeTime = v),
Toggle("World", "SpawnRate", "Spawn Rate", () => Settings.SpawnRateBoost, v => Settings.SpawnRateBoost = v),
new("World", "SavePosition", "Save Current Position", Waypoints.AddHere),
Toggle("World", "MapTeleport", "Map Teleport", () => Settings.MapTeleport, v => Settings.MapTeleport = v),
new("World", "RevealMap", "Reveal Map", MapFeatures.StartReveal),
];
// Windows virtual-key codes for mouse buttons; XNA's Keys leaves them unassigned.
+30
View File
@@ -17,6 +17,12 @@ public static partial class Settings
[nameof(AutoMana)] = "Player.AutoMana.Enabled",
[nameof(AutoManaAt)] = "Player.AutoMana.Threshold",
[nameof(BuffMinutes)] = "Player.Buffs.Minutes",
[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",
@@ -55,10 +61,14 @@ public static partial class Settings
[nameof(ShotPreview)] = "Combat.Aim.ShotPreview",
[nameof(AimFov)] = "Combat.Aim.CursorRadius",
[nameof(AimRange)] = "Combat.Aim.Range",
[nameof(MagicBullets)] = "Combat.MagicBullets",
[nameof(AutoDodge)] = "Combat.AutoDodge",
[nameof(InstantBreak)] = "Items.InstantBreak",
[nameof(InfItems)] = "Items.InfiniteItems",
[nameof(ItemVacuum)] = "Items.Vacuum",
[nameof(AutoSell)] = "Items.AutoSell.Enabled",
[nameof(BankButtons)] = "Items.BankButtons",
[nameof(AutoFish)] = "Fishing.Enabled",
[nameof(AutoRecast)] = "Fishing.Recast",
@@ -87,6 +97,7 @@ public static partial class Settings
[nameof(ColTown)] = "Esp.TownNpcs.Color",
[nameof(EspBossRainbow)] = "Esp.Bosses.Rainbow",
[nameof(ColBoss)] = "Esp.Bosses.Color",
[nameof(EspBossCursor)] = "Esp.Bosses.Cursor",
[nameof(EspItems)] = "Esp.Items.Enabled",
[nameof(ColItems)] = "Esp.Items.Color",
[nameof(EspProjHostile)] = "Esp.HostileProjectiles.Enabled",
@@ -95,6 +106,18 @@ public static partial class Settings
[nameof(ColProjFriendly)] = "Esp.FriendlyProjectiles.Color",
[nameof(EspWaypoints)] = "Esp.Waypoints.Enabled",
[nameof(ColWaypoint)] = "Esp.Waypoints.Color",
[nameof(EspChests)] = "Esp.Chests.Enabled",
[nameof(ColChests)] = "Esp.Chests.Color",
[nameof(EspChestsListed)] = "Esp.Chests.OnlyListed",
[nameof(DamageMelee)] = "DamageNumbers.Melee.Enabled",
[nameof(ColDamageMelee)] = "DamageNumbers.Melee.Color",
[nameof(DamageRanged)] = "DamageNumbers.Ranged.Enabled",
[nameof(ColDamageRanged)] = "DamageNumbers.Ranged.Color",
[nameof(DamageMagic)] = "DamageNumbers.Magic.Enabled",
[nameof(ColDamageMagic)] = "DamageNumbers.Magic.Color",
[nameof(DamageSummon)] = "DamageNumbers.Summon.Enabled",
[nameof(ColDamageSummon)] = "DamageNumbers.Summon.Color",
[nameof(Spelunker)] = "Vision.Spelunker",
[nameof(Hunter)] = "Vision.Hunter",
@@ -102,12 +125,19 @@ public static partial class Settings
[nameof(NightOwl)] = "Vision.NightOwl",
[nameof(DangerSense)] = "Vision.DangerSense",
[nameof(BiomeSight)] = "Vision.BiomeSight",
[nameof(FullBright)] = "Vision.FullBright",
[nameof(ExtendedZoom)] = "Vision.ExtendedZoom",
[nameof(FreezeTime)] = "World.FreezeTime",
[nameof(SpawnRateBoost)] = "World.SpawnRate.Enabled",
[nameof(SpawnRateMult)] = "World.SpawnRate.Multiplier",
[nameof(MapTeleport)] = "World.MapTeleport",
[nameof(LogBallistics)] = "Ballistics.Log",
};
static readonly string[] ListPaths = ["Items.AutoSell.Items", "Esp.Chests.Items"];
static void SetValue(Dictionary<string, object> document, string path, object value)
{
string[] parts = path.Split('.');
+58 -8
View File
@@ -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;
public static float AutoHealAt = 40f, AutoManaAt = 25f, BuffMinutes = 10f;
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;
@@ -35,20 +58,25 @@ public static partial class Settings
public static bool KillAura, HitMagnet, FreezeEnemies, WeakenEnemies;
public static float KillAuraRange = 20f, HitMagnetRange = 30f;
public static int KillAuraInterval = 10;
public static bool Aimbot, AimPrediction = true, AimDrawTarget = true, AutoAttack, AimExact = true, ShotPreview;
public static bool Aimbot, AimPrediction = true, AimDrawTarget = true, AutoAttack, AimExact = true, ShotPreview, MagicBullets, AutoDodge;
public static float AimFov = 250f, AimRange = 60f;
public static bool InstantBreak, InfItems, ItemVacuum;
public static bool InstantBreak, InfItems, ItemVacuum, AutoSell, BankButtons = true;
public static readonly HashSet<int> SellList = [];
public static bool AutoFish, AutoRecast = true, FishEnemies, Sonar;
public static bool Esp, EspNames = true, EspHealth = true, EspHealthText, EspDistance, EspLines, EspProjPath = true;
public static bool EspLocal, EspPlayers = true, EspHostile = true, EspCritters, EspTown, EspBossRainbow = true, EspItems, EspProjHostile, EspProjFriendly, EspWaypoints = true;
public static bool EspLocal, EspPlayers = true, EspHostile = true, EspCritters, EspTown, EspBossRainbow = true, EspBossCursor = true, EspItems, EspProjHostile, EspProjFriendly, EspWaypoints = true, EspChests, EspChestsListed;
public static readonly HashSet<int> ChestList = [];
public static EspBoxStyle EspStyle;
public static float EspThickness = 1f, EspMaxDistance; // tiles, 0 = unlimited
public static Color ColLocal = Color.White, ColPlayers = new(70, 200, 255), ColHostile = new(235, 60, 60), ColCritters = new(200, 170, 120), ColTown = new(80, 220, 110), ColBoss = new(255, 120, 40), ColItems = new(255, 210, 70), ColProjHostile = new(255, 80, 200), ColProjFriendly = new(120, 160, 255), ColWaypoint = new(220, 90, 255);
public static bool Spelunker, Hunter, Shine, NightOwl, DangerSense, BiomeSight;
public static Color ColLocal = Color.White, ColPlayers = new(70, 200, 255), ColHostile = new(235, 60, 60), ColCritters = new(200, 170, 120), ColTown = new(80, 220, 110), ColBoss = new(255, 120, 40), ColItems = new(255, 210, 70), ColProjHostile = new(255, 80, 200), ColProjFriendly = new(120, 160, 255), ColWaypoint = new(220, 90, 255), ColChests = new(240, 170, 60);
public static bool DamageMelee, DamageRanged, DamageMagic, DamageSummon;
public static Color ColDamageMelee = new(255, 110, 60), ColDamageRanged = new(120, 230, 90), ColDamageMagic = new(90, 150, 255), ColDamageSummon = new(200, 110, 255);
public static bool Spelunker, Hunter, Shine, NightOwl, DangerSense, BiomeSight, FullBright, ExtendedZoom;
public static bool FreezeTime;
public static bool FreezeTime, SpawnRateBoost, MapTeleport = true;
public static float SpawnRateMult = 5f;
public static bool LogBallistics = true;
@@ -92,6 +120,9 @@ public static partial class Settings
binds[bind.Key] = bind.Value.ToString();
document["Binds"] = binds;
SetValue(document, ListPaths[0], SortedList(SellList));
SetValue(document, ListPaths[1], SortedList(ChestList));
var windows = new Dictionary<string, int[]>();
foreach (Window window in WindowManager.All)
windows[window.Id] = [(int)window.Pos.X, (int)window.Pos.Y];
@@ -121,6 +152,9 @@ public static partial class Settings
if (TryGetValue(document, setting.Value, out object? value))
TryLoad(setting.Value, () => Apply(setting.Key, Convert.ToString(value, CultureInfo.InvariantCulture) ?? ""));
LoadList(document, ListPaths[0], SellList);
LoadList(document, ListPaths[1], ChestList);
foreach (KeyValuePair<string, object> bind in Section(document, "Binds"))
TryLoad("Binds." + bind.Key, () => Binds[bind.Key] = (Keys)Enum.Parse(typeof(Keys), (string)bind.Value));
@@ -149,6 +183,20 @@ public static partial class Settings
}
}
static int[] SortedList(HashSet<int> list)
{
int[] ids = [.. list];
Array.Sort(ids);
return ids;
}
static void LoadList(Dictionary<string, object> document, string path, HashSet<int> list)
{
list.Clear();
if (TryGetValue(document, path, out object? value))
TryLoad(path, () => list.UnionWith(Json.ConvertTo<int[]>(value!)));
}
static void TryLoad(string key, Action load)
{
try
@@ -208,6 +256,8 @@ public static partial class Settings
{
foreach (KeyValuePair<string, object> pair in Defaults)
typeof(Settings).GetField(pair.Key)!.SetValue(null, pair.Value);
SellList.Clear();
ChestList.Clear();
}
public static string Hex(Color color) => $"{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
+142
View File
@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Terraria;
using TerraSharp.Core;
using TerraSharp.Simulation;
namespace TerraSharp.Features.Combat;
// Steers the local player out of predicted projectile and contact hits.
public static class AutoDodge
{
const int Horizon = 24;
const int MaxThreats = 48;
const float ScanRange = 60f * 16f;
const float Margin = 6f;
const int WallCheckTicks = 6;
sealed class Threat
{
public readonly Vector2[] Path = new Vector2[Horizon + 1];
public int Count, Width, Height;
}
readonly record struct Move(int Horizontal, int Vertical);
static readonly Move[] Moves = [new(0, 0), new(-1, 0), new(1, 0), new(0, -1), new(-1, -1), new(1, -1), new(0, 1), new(-1, 1), new(1, 1)];
static readonly Threat[] Pool = new Threat[MaxThreats];
static readonly List<Threat> Threats = [];
static AutoDodge()
{
for (int i = 0; i < Pool.Length; i++)
Pool[i] = new Threat();
}
// Player.HorizontalMovement prefix: controls and velocity set here drive this tick's movement.
public static void Steer(Player player)
{
if (!Settings.AutoDodge || player.dead)
return;
CollectThreats(player);
if (Threats.Count == 0)
return;
float speed = Settings.Fly ? Settings.FlySpeed : Math.Max(Math.Max(player.maxRunSpeed, player.accRunSpeed), 3f);
float jump = Player.jumpSpeed * 1.5f;
int baseline = FirstHit(player, player.velocity, false);
if (baseline > Horizon)
return;
Move best = Moves[0];
int bestHit = baseline;
foreach (Move move in Moves)
{
if (move == Moves[0])
continue;
var velocity = new Vector2(move.Horizontal * speed, move.Vertical == 0 ? player.velocity.Y : move.Vertical * jump * player.gravDir);
int hit = FirstHit(player, velocity, true);
if (hit > bestHit)
(best, bestHit) = (move, hit);
}
if (best == Moves[0])
return;
Apply(player, best, speed, jump);
}
static void Apply(Player player, Move move, float speed, float jump)
{
player.controlLeft = move.Horizontal < 0;
player.controlRight = move.Horizontal > 0;
if (move.Horizontal != 0)
player.velocity.X = move.Horizontal * speed;
player.controlUp = move.Vertical < 0;
player.controlDown = move.Vertical > 0;
if (move.Vertical != 0)
player.velocity.Y = move.Vertical * jump * player.gravDir;
}
// First tick a threat overlaps the player moving at this velocity, or Horizon + 1 when none do.
static int FirstHit(Player player, Vector2 velocity, bool checkWalls)
{
Vector2 size = new(player.width, player.height);
if (checkWalls && TileCollision.Sweep(player.Center, player.Center + velocity * WallCheckTicks, player.width, player.height))
return -1;
for (int t = 1; t <= Horizon; t++)
{
Vector2 position = player.position + velocity * t;
var bounds = new Rectangle((int)(position.X - Margin), (int)(position.Y - Margin), (int)(size.X + Margin * 2), (int)(size.Y + Margin * 2));
foreach (Threat threat in Threats)
{
if (t >= threat.Count)
continue;
Vector2 center = threat.Path[t];
if (bounds.Intersects(new Rectangle((int)(center.X - threat.Width / 2f), (int)(center.Y - threat.Height / 2f), threat.Width, threat.Height)))
return t;
}
}
return Horizon + 1;
}
static void CollectThreats(Player player)
{
Threats.Clear();
float rangeSq = ScanRange * ScanRange;
for (int i = 0; i < Main.maxProjectiles && Threats.Count < MaxThreats; i++)
{
Projectile projectile = Main.projectile[i];
if (!projectile.active || !projectile.hostile || projectile.damage <= 0 || Vector2.DistanceSquared(projectile.Center, player.Center) > rangeSq)
continue;
if (!Ballistics.Current(i, out Vector2 velocity, out int age))
(velocity, age) = (projectile.velocity * (1 + projectile.extraUpdates), 0);
Threat threat = Pool[Threats.Count];
threat.Count = Ballistics.Path(Ballistics.Get(projectile.type), projectile.Center, velocity, age, Horizon, threat.Path);
(threat.Width, threat.Height) = (Math.Max(4, projectile.width), Math.Max(4, projectile.height));
Threats.Add(threat);
}
for (int i = 0; i < Main.maxNPCs && Threats.Count < MaxThreats; i++)
{
NPC npc = Main.npc[i];
if (!Hostility.IsHostile(npc) || npc.damage <= 0 || Vector2.DistanceSquared(npc.Center, player.Center) > rangeSq)
continue;
Threat threat = Pool[Threats.Count];
for (int t = 0; t <= Horizon; t++)
threat.Path[t] = npc.Center + NpcMotion.Offset(npc, t);
(threat.Count, threat.Width, threat.Height) = (Horizon + 1, npc.width, npc.height);
Threats.Add(threat);
}
}
}
@@ -0,0 +1,69 @@
using System;
using Microsoft.Xna.Framework;
using Terraria;
using Terraria.ID;
using TerraSharp.Core;
using TerraSharp.Simulation;
namespace TerraSharp.Features.Combat;
public static class MagicBullets
{
const float MinSpeed = 8f;
const int GrappleStyle = 7, YoyoStyle = 99, FlailStyle = 15;
// Projectile type per slot while steered, 0 otherwise.
static readonly int[] Steered = new int[Main.maxProjectiles];
public static bool IsSteered(int slot) => Steered[slot] != 0 && Steered[slot] == Main.projectile[slot].type;
static bool Eligible(Projectile projectile) => projectile is { friendly: true, hostile: false, minion: false, sentry: false, bobber: false, npcProj: false, damage: > 0 } && projectile.aiStyle is not (GrappleStyle or YoyoStyle or FlailStyle) && !Main.projPet[projectile.type] && !ProjectileID.Sets.IsAWhip[projectile.type] && !Sandbox.Held(projectile);
// Projectile.AI postfix for the local player's projectiles.
public static void PostAI(Projectile projectile)
{
int slot = projectile.whoAmI;
NPC? target = Eligible(projectile) ? TargetFor(projectile) : null;
if (target is null)
{
if (IsSteered(slot) && ContentSamples.ProjectilesByType.TryGetValue(projectile.type, out Projectile? sample))
projectile.tileCollide = sample.tileCollide;
Steered[slot] = 0;
return;
}
Vector2 offset = target.Center - projectile.Center;
if (offset.LengthSquared() < 1f)
return;
projectile.velocity = Vector2.Normalize(offset) * Math.Max(projectile.velocity.Length(), MinSpeed);
projectile.tileCollide = false;
Steered[slot] = projectile.type;
}
static NPC? TargetFor(Projectile projectile)
{
if (Aimbot.Target is { } locked && Hostility.IsDamageable(locked))
return locked;
Player player = Main.player[projectile.owner];
float range = Settings.AimRange * 16f, rangeSq = range * range;
NPC? best = null;
float bestDistance = float.MaxValue;
for (int i = 0; i < Main.maxNPCs; i++)
{
NPC npc = Main.npc[i];
if (!Hostility.IsDamageable(npc) || Vector2.DistanceSquared(npc.Center, player.Center) > rangeSq)
continue;
float distance = Vector2.DistanceSquared(npc.Center, projectile.Center);
if (distance < bestDistance)
{
bestDistance = distance;
best = npc;
}
}
return best;
}
}
+76
View File
@@ -0,0 +1,76 @@
using Microsoft.Xna.Framework;
using Terraria;
using Terraria.ID;
using TerraSharp.Core;
#if TMODLOADER
using Terraria.ModLoader;
#endif
namespace TerraSharp.Features;
public enum DamageKind { None, Melee, Ranged, Magic, Summon }
// Hit paths set Current around StrikeNPC; CombatText.NewText reads it.
public static class DamageNumbers
{
public static DamageKind Current;
public static Color? Recolor(Color color)
{
bool crit = color == CombatText.DamagedHostileCrit;
if (!crit && color != CombatText.DamagedHostile)
return null;
(bool enabled, Color chosen) = Current switch
{
DamageKind.Melee => (Settings.DamageMelee, Settings.ColDamageMelee),
DamageKind.Ranged => (Settings.DamageRanged, Settings.ColDamageRanged),
DamageKind.Magic => (Settings.DamageMagic, Settings.ColDamageMagic),
DamageKind.Summon => (Settings.DamageSummon, Settings.ColDamageSummon),
_ => (false, default),
};
if (!enabled)
return null;
var opaque = new Color(chosen.R, chosen.G, chosen.B);
return crit ? Color.Lerp(opaque, Color.White, 0.4f) : opaque;
}
#if TMODLOADER
// Whips count as melee too, so summon is checked first.
public static DamageKind Of(DamageClass? damageClass)
{
if (damageClass is null)
return DamageKind.None;
if (damageClass.CountsAsClass(DamageClass.Summon))
return DamageKind.Summon;
if (damageClass.CountsAsClass(DamageClass.Magic))
return DamageKind.Magic;
if (damageClass.CountsAsClass(DamageClass.Ranged))
return DamageKind.Ranged;
return damageClass.CountsAsClass(DamageClass.Melee) ? DamageKind.Melee : DamageKind.None;
}
#else
public static DamageKind Of(Projectile projectile)
{
if (projectile.minion || projectile.sentry || ProjectileID.Sets.IsAWhip[projectile.type] || ProjectileID.Sets.MinionShot[projectile.type] || ProjectileID.Sets.SentryShot[projectile.type])
return DamageKind.Summon;
if (projectile.magic)
return DamageKind.Magic;
if (projectile.ranged)
return DamageKind.Ranged;
return projectile.melee ? DamageKind.Melee : DamageKind.None;
}
public static DamageKind Of(Item item)
{
if (item.summon || item.sentry)
return DamageKind.Summon;
if (item.magic)
return DamageKind.Magic;
if (item.ranged)
return DamageKind.Ranged;
return item.melee ? DamageKind.Melee : DamageKind.None;
}
#endif
}
+57
View File
@@ -1,5 +1,7 @@
using Microsoft.Xna.Framework;
using Terraria;
using TerraSharp.Core;
using TerraSharp.UI;
namespace TerraSharp.Features;
@@ -24,4 +26,59 @@ public static class ItemFeatures
drop.velocity = Vector2.Zero;
}
}
const int InventorySlots = 50;
const int CoinSound = 18;
// 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;
}
}
// Items worth nothing are deleted; items worth something stay when no slot can take the coins.
public static void AutoSell(Player player)
{
bool sold = false;
for (int i = 0; i < InventorySlots; i++)
{
Item item = player.inventory[i];
if (item.type == 0 || item.favorited || !Settings.SellList.Contains(item.type) || (i == player.selectedItem && player.itemAnimation > 0))
continue;
if (!player.SellItem(item) && item.value > 0)
continue;
item.TurnToAir();
sold = true;
}
if (sold)
Widgets.Sound(CoinSound);
}
}
+74
View File
@@ -0,0 +1,74 @@
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Terraria;
using TerraSharp.Core;
using TerraSharp.UI;
namespace TerraSharp.Features;
public static class MapFeatures
{
const int ColumnsPerTick = 24;
const float WorldEdge = 42f;
// MapHelper.GetBackgroundType samples map tiles up to 36 away; WorldMap.UnlockMapSection keeps the same margin.
const int Margin = 40;
static int _revealColumn = -1;
static bool _rightWasDown;
public static bool Revealing => _revealColumn >= 0;
public static float RevealProgress => Revealing ? (_revealColumn - Margin) / (float)(Main.maxTilesX - 2 * Margin) : 0f;
public static void StartReveal() => _revealColumn = Margin;
// Main.DoUpdate postfix.
public static void Update()
{
if (Main.gameMenu)
{
_revealColumn = -1;
return;
}
RevealStep();
if (Settings.MapTeleport)
TeleportOnRightClick();
}
// A large world is about 20 million map tiles, so the reveal is spread over ticks.
static void RevealStep()
{
if (_revealColumn < 0)
return;
int start = _revealColumn, end = Math.Min(Main.maxTilesX - Margin, start + ColumnsPerTick);
_revealColumn = end < Main.maxTilesX - Margin ? end : -1;
for (int x = start; x < end; x++)
for (int y = Margin; y < Main.maxTilesY - Margin; y++)
Main.Map.Update(x, y, byte.MaxValue);
if (_revealColumn < 0)
Main.refreshMap = true;
}
// Main.DrawMap puts tile mapFullscreenPos at the screen center, scaled by mapFullscreenScale.
static void TeleportOnRightClick()
{
MouseState mouse = Mouse.GetState();
bool down = mouse.RightButton == ButtonState.Pressed;
bool clicked = down && !_rightWasDown;
_rightWasDown = down;
if (!clicked || !Main.mapFullscreen || !Main.instance.IsActive || WindowManager.MouseOver)
return;
Vector2 tile = Main.mapFullscreenPos + (new Vector2(mouse.X, mouse.Y) - new Vector2(Main.screenWidth, Main.screenHeight) / 2f) / Main.mapFullscreenScale;
Vector2 feet = new Vector2(MathHelper.Clamp(tile.X, WorldEdge, Main.maxTilesX - WorldEdge), MathHelper.Clamp(tile.Y, WorldEdge, Main.maxTilesY - WorldEdge)) * 16f;
Player player = Main.LocalPlayer;
PlayerFeatures.TeleportTo(feet - new Vector2(player.width / 2f, player.height));
Main.mapFullscreen = false;
Widgets.Sound(2, -1, -1, 6);
}
}
+59
View File
@@ -0,0 +1,59 @@
using System;
using Terraria;
using Terraria.DataStructures;
using Terraria.ID;
using TerraSharp.Core;
namespace TerraSharp.Features;
public static class NpcSpawner
{
const float SpawnDistance = 10 * 16f;
const int MultiplayerClient = 1;
#if TMODLOADER
static readonly EntitySource_DebugCommand Source = new(nameof(NpcSpawner));
#else
static readonly EntitySource_DebugCommand Source = new();
#endif
public static bool Multiplayer => Main.netMode == MultiplayerClient;
// The server only honors summon requests for these types.
public static bool CanSummon(int type) => type >= 0 && type < NPCID.Sets.MPAllowedEnemies.Length && NPCID.Sets.MPAllowedEnemies[type];
public static bool Available(int type) => !Multiplayer || CanSummon(type);
public static void Spawn(int type, int count)
{
if (Multiplayer)
{
if (CanSummon(type))
NetMessage.SendData(MessageID.SpawnBossUseLicenseStartEvent, -1, -1, null, Main.myPlayer, type);
return;
}
Player player = Main.LocalPlayer;
int x = (int)(player.Center.X + player.direction * SpawnDistance);
int y = (int)player.Bottom.Y;
for (int i = 0; i < count; i++)
if (NPC.NewNPC(Source, x, y, type) >= Main.maxNPCs)
break;
}
// Spawner.GetSpawnRate postfix; spawnRate is the 1-in-N chance per tick.
public static void ScaleSpawnRate(ref int spawnRate, ref int maxSpawns)
{
if (!Settings.SpawnRateBoost)
return;
if (Settings.SpawnRateMult <= 0f)
{
maxSpawns = 0;
return;
}
spawnRate = Math.Max(1, (int)(spawnRate / Settings.SpawnRateMult));
maxSpawns = (int)Math.Ceiling(maxSpawns * Settings.SpawnRateMult);
}
}
+22
View File
@@ -2,6 +2,7 @@ using System;
using Microsoft.Xna.Framework;
using Terraria;
using TerraSharp.Core;
using TerraSharp.Features.Combat;
using TerraSharp.UI;
namespace TerraSharp.Features;
@@ -92,7 +93,26 @@ public static class PlayerFeatures
Player.tileRangeY = Settings.ReachY;
}
if (Settings.DefenseBoost)
#if TMODLOADER
player.statDefense.FinalMultiplier *= Settings.DefenseMult;
#else
player.statDefense = (int)(player.statDefense * Settings.DefenseMult);
#endif
if (Settings.GhostMode)
ApplyGhost(player);
ApplyVision(player);
AutoDodge.Steer(player);
}
// ResetEffects clears both every tick; NPC.TargetClosest and SetTargetTrackingValues read them.
static void ApplyGhost(Player player)
{
player.aggro = -10000;
bool[] passive = player.npcTypeNoAggro;
for (int i = 0; i < passive.Length; i++)
passive[i] = true;
}
static void ApplyVision(Player player)
@@ -125,6 +145,8 @@ public static class PlayerFeatures
if (Settings.InfBreath)
player.breath = player.breathMax;
AutoPotions(player);
if (Settings.AutoSell)
ItemFeatures.AutoSell(player);
}
if (Settings.InfFlight)
+78
View File
@@ -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);
}
}
@@ -0,0 +1,103 @@
using System;
using Microsoft.Xna.Framework;
using Terraria;
using TerraSharp.Core;
using TerraSharp.Features;
#if !TMODLOADER
using Terraria.GameContent;
using Terraria.ID;
#endif
namespace TerraSharp.Patches;
internal static class DamageNumberPatches
{
delegate int NewTextOriginal(Rectangle location, Color color, string text, bool dramatic, bool dot);
internal static void Install(HookRegistry hooks)
{
hooks.Add(HookRegistry.Method(typeof(CombatText), nameof(CombatText.NewText), [typeof(Rectangle), typeof(Color), typeof(string), typeof(bool), typeof(bool)]), (Func<NewTextOriginal, Rectangle, Color, string, bool, bool, int>)NewText);
#if TMODLOADER
hooks.Add(HookRegistry.Method(typeof(NPC), nameof(NPC.StrikeNPC), [typeof(NPC.HitInfo), typeof(bool), typeof(bool)]), (Func<StrikeOriginal, NPC, NPC.HitInfo, bool, bool, int>)Strike);
#else
hooks.Add(typeof(Projectile), nameof(Projectile.Damage), (Action<ProjectileDamageOriginal, Projectile>)ProjectileDamage);
hooks.Add(typeof(Player), "ProcessHitAgainstNPC", (Action<MeleeHitOriginal, Player, Item, Rectangle, int, float, int>)MeleeHit);
hooks.Add(HookRegistry.Method(typeof(Player), nameof(Player.ApplyDamageToNPC), [typeof(NPC), typeof(int), typeof(float), typeof(int), typeof(bool), typeof(PlayerNPCHitSource)]), (Action<ApplyDamageOriginal, Player, NPC, int, float, int, bool, PlayerNPCHitSource>)ApplyDamage);
#endif
}
static int NewText(NewTextOriginal original, Rectangle location, Color color, string text, bool dramatic, bool dot)
{
if (Plugin.Active && DamageNumbers.Current != DamageKind.None && DamageNumbers.Recolor(color) is { } recolored)
color = recolored;
return original(location, color, text, dramatic, dot);
}
#if TMODLOADER
delegate int StrikeOriginal(NPC self, NPC.HitInfo hit, bool fromNet, bool noPlayerInteraction);
static int Strike(StrikeOriginal original, NPC self, NPC.HitInfo hit, bool fromNet, bool noPlayerInteraction)
{
DamageKind previous = DamageNumbers.Current;
DamageNumbers.Current = DamageNumbers.Of(hit.DamageType);
try
{
return original(self, hit, fromNet, noPlayerInteraction);
}
finally
{
DamageNumbers.Current = previous;
}
}
#else
delegate void ProjectileDamageOriginal(Projectile self);
delegate void MeleeHitOriginal(Player self, Item item, Rectangle itemRectangle, int originalDamage, float knockBack, int npcIndex);
delegate void ApplyDamageOriginal(Player self, NPC npc, int damage, float knockback, int direction, bool crit, PlayerNPCHitSource source);
static void ProjectileDamage(ProjectileDamageOriginal original, Projectile self)
{
DamageKind previous = DamageNumbers.Current;
DamageNumbers.Current = DamageNumbers.Of(self);
try
{
original(self);
}
finally
{
DamageNumbers.Current = previous;
}
}
static void MeleeHit(MeleeHitOriginal original, Player self, Item item, Rectangle itemRectangle, int originalDamage, float knockBack, int npcIndex)
{
DamageKind previous = DamageNumbers.Current;
DamageNumbers.Current = DamageNumbers.Of(item);
try
{
original(self, item, itemRectangle, originalDamage, knockBack, npcIndex);
}
finally
{
DamageNumbers.Current = previous;
}
}
// Dashes, buffs and contact damage pass the item or projectile that caused the hit.
static void ApplyDamage(ApplyDamageOriginal original, Player self, NPC npc, int damage, float knockback, int direction, bool crit, PlayerNPCHitSource source)
{
DamageKind previous = DamageNumbers.Current;
if (source.ProjType > 0 && ContentSamples.ProjectilesByType.TryGetValue(source.ProjType, out Projectile? projectile))
DamageNumbers.Current = DamageNumbers.Of(projectile);
else if (source.ItemType > 0 && ContentSamples.ItemsByType.TryGetValue(source.ItemType, out Item? item))
DamageNumbers.Current = DamageNumbers.Of(item);
try
{
original(self, npc, damage, knockback, direction, crit, source);
}
finally
{
DamageNumbers.Current = previous;
}
}
#endif
}
+4
View File
@@ -21,6 +21,10 @@ internal sealed class HookRegistry : IDisposable
MainPatches.Install(hooks);
PlayerPatches.Install(hooks);
NpcPatches.Install(hooks);
ProjectilePatches.Install(hooks);
DamageNumberPatches.Install(hooks);
VisionPatches.Install(hooks);
SpawnRatePatch.Install(hooks);
ItemCheckShootPatch.Install(hooks);
SandboxGuards.Install(hooks);
return hooks;
+1
View File
@@ -44,6 +44,7 @@ internal static class MainUpdatePatch
Hotkeys.Update();
Sandbox.NewTick();
WorldFeatures.Update();
MapFeatures.Update();
Settings.AutoSave();
Profiler.EndTick();
}
+18
View File
@@ -15,6 +15,7 @@ internal static class PlayerPatches
PlayerUpdatePatch.Install(hooks);
PlayerItemCheckPatch.Install(hooks);
PlayerMovementPatch.Install(hooks);
ChestRangePatch.Install(hooks);
}
}
@@ -48,6 +49,7 @@ internal static class PlayerUpdatePatch
{
Clock.Advance();
PlayerFeatures.PostUpdate(player);
Stats.Update(player);
CombatFeatures.PostPlayerUpdate(player);
Ballistics.Observe();
NpcMotion.Track();
@@ -99,3 +101,19 @@ internal static class PlayerMovementPatch
original(self);
}
}
// Closes any open chest or bank once the player leaves range of chestX/chestY.
internal static class ChestRangePatch
{
delegate void Original(Player self);
internal static void Install(HookRegistry hooks) => hooks.Add(typeof(Player), "HandleBeingInChestRange", (Action<Original, Player>)HandleRange);
static void HandleRange(Original original, Player self)
{
if (Plugin.Active && self.whoAmI == Main.myPlayer && BankButtons.KeepsOpen(self))
return;
original(self);
}
}
@@ -0,0 +1,22 @@
using System;
using Terraria;
using TerraSharp.Core;
using TerraSharp.Features.Combat;
using TerraSharp.Simulation;
namespace TerraSharp.Patches;
// After the AI chose its velocity, before Projectile.Update moves it.
internal static class ProjectilePatches
{
delegate void Original(Projectile self);
internal static void Install(HookRegistry hooks) => hooks.Add(typeof(Projectile), nameof(Projectile.AI), (Action<Original, Projectile>)AI);
static void AI(Original original, Projectile self)
{
original(self);
if (Plugin.Active && Settings.MagicBullets && !Sandbox.Active && self.owner == Main.myPlayer)
Guard.Run("Projectile.AI postfix", MagicBullets.PostAI, self);
}
}
+53
View File
@@ -0,0 +1,53 @@
using System;
using Terraria;
using TerraSharp.Core;
using TerraSharp.Features;
#if TMODLOADER
using Terraria.ModLoader;
#endif
namespace TerraSharp.Patches;
// Natural spawns run only in single player and on the server.
internal static class SpawnRatePatch
{
#if TMODLOADER
// tModLoader's NPC.SpawnNPC passes the final rate through NPCLoader.EditSpawnRate.
delegate void Original(Player player, ref int spawnRate, ref int maxSpawns);
delegate void Hooked(Original original, Player player, ref int spawnRate, ref int maxSpawns);
internal static void Install(HookRegistry hooks) => hooks.Add(typeof(NPCLoader), nameof(NPCLoader.EditSpawnRate), (Hooked)EditSpawnRate);
static void EditSpawnRate(Original original, Player player, ref int spawnRate, ref int maxSpawns)
{
original(player, ref spawnRate, ref maxSpawns);
Scale(ref spawnRate, ref maxSpawns);
}
#else
delegate void Original(NPC.Spawner self, Player player, out int spawnRate, out int maxSpawns);
delegate void Hooked(Original original, NPC.Spawner self, Player player, out int spawnRate, out int maxSpawns);
internal static void Install(HookRegistry hooks) => hooks.Add(typeof(NPC.Spawner), "GetSpawnRate", (Hooked)GetSpawnRate);
static void GetSpawnRate(Original original, NPC.Spawner self, Player player, out int spawnRate, out int maxSpawns)
{
original(self, player, out spawnRate, out maxSpawns);
Scale(ref spawnRate, ref maxSpawns);
}
#endif
static void Scale(ref int spawnRate, ref int maxSpawns)
{
if (!Plugin.Active)
return;
try
{
NpcSpawner.ScaleSpawnRate(ref spawnRate, ref maxSpawns);
}
catch (Exception e)
{
Log.Error("Spawn rate", e);
}
}
}
+80
View File
@@ -0,0 +1,80 @@
using System;
using Microsoft.Xna.Framework;
using Mono.Cecil.Cil;
using MonoMod.Cil;
using Terraria;
using Terraria.Graphics.Light;
using TerraSharp.Core;
namespace TerraSharp.Patches;
internal static class VisionPatches
{
internal static void Install(HookRegistry hooks)
{
FullBrightPatch.Install(hooks);
ZoomPatch.Install(hooks);
}
}
// Every Lighting query reaches the active engine's GetColor, millions of times per frame, so the check is a bare field load.
internal static class FullBrightPatch
{
internal static void Install(HookRegistry hooks)
{
hooks.Modify(HookRegistry.Method(typeof(LightingEngine), nameof(LightingEngine.GetColor)), ReturnWhite);
hooks.Modify(HookRegistry.Method(typeof(LegacyLighting), nameof(LegacyLighting.GetColor)), ReturnWhite);
}
static void ReturnWhite(ILContext context)
{
var cursor = new ILCursor(context);
ILLabel body = cursor.DefineLabel();
cursor.Emit(OpCodes.Ldsfld, typeof(Settings).GetField(nameof(Settings.FullBright))!);
cursor.Emit(OpCodes.Brfalse, body);
cursor.Emit(OpCodes.Call, typeof(Vector3).GetProperty(nameof(Vector3.One))!.GetGetMethod()!);
cursor.Emit(OpCodes.Ret);
cursor.Emit(OpCodes.Nop);
body.Target = cursor.Prev;
}
}
// Main.DoDraw and Main.UpdateViewZoomKeys clamp Main.GameZoomTarget to [1, 2].
internal static class ZoomPatch
{
public const float MaxZoom = 8f;
const int SearchWindow = 8;
// Tile and wall targets cover the screen plus Main.offScreenRange on each side.
public static float MinZoom => Math.Max(Main.screenWidth / (float)(Main.screenWidth + 2 * Main.offScreenRange), Main.screenHeight / (float)(Main.screenHeight + 2 * Main.offScreenRange));
internal static void Install(HookRegistry hooks)
{
hooks.Modify(HookRegistry.Method(typeof(Main), "DoDraw"), ReplaceClamps);
hooks.Modify(HookRegistry.Method(typeof(Main), nameof(Main.UpdateViewZoomKeys)), ReplaceClamps);
}
public static float Clamp(float value, float minimum, float maximum) => Settings.ExtendedZoom && Plugin.Active ? MathHelper.Clamp(value, MinZoom, MaxZoom) : MathHelper.Clamp(value, minimum, maximum);
static void ReplaceClamps(ILContext context)
{
var cursor = new ILCursor(context);
int replaced = 0;
while (cursor.TryGotoNext(MoveType.After, i => i.MatchLdsfld(typeof(Main), nameof(Main.GameZoomTarget))))
{
Instruction? instruction = cursor.Next;
for (int i = 0; i < SearchWindow && instruction is not null; i++, instruction = instruction.Next)
{
if (instruction.OpCode != OpCodes.Call || instruction.Operand is not Mono.Cecil.MethodReference { Name: "Clamp" })
continue;
instruction.Operand = context.Import(typeof(ZoomPatch).GetMethod(nameof(Clamp))!);
replaced++;
break;
}
}
if (replaced == 0)
throw new MissingMethodException(context.Method.FullName, "Clamp");
}
}
+1 -1
View File
@@ -133,7 +133,7 @@ public static class Ballistics
track.Age++;
// Tile hits, bounces and homing turns show up as sharp changes.
if (!track.Spoiled && track.Velocities.Count > 0 && (velocity - track.LastVelocity).Length() > Math.Max(4f, track.LastVelocity.Length() * 0.5f))
if (!track.Spoiled && (MagicBullets.IsSteered(i) || (track.Velocities.Count > 0 && (velocity - track.LastVelocity).Length() > Math.Max(4f, track.LastVelocity.Length() * 0.5f))))
track.Spoiled = true;
if (!track.Spoiled && track.Velocities.Count < MaxSamples)
track.Velocities.Add(velocity);
+141
View File
@@ -0,0 +1,141 @@
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using Terraria.GameInput;
using Terraria.ID;
using Terraria.Localization;
using TerraSharp.Core;
using TerraSharp.Patches;
#if TMODLOADER
using Terraria.ModLoader;
#endif
namespace TerraSharp.UI;
public enum Bank
{
PiggyBank = -2,
Safe = -3,
DefendersForge = -4,
VoidVault = -5,
}
// Bank icons stacked under the builder accessory toggles, left of the inventory.
public static class BankButtons
{
const int Size = 18;
const int Spacing = 24;
const float ColumnX = 10f;
static readonly (Bank Bank, int Item)[] Banks = [(Bank.PiggyBank, ItemID.PiggyBank), (Bank.Safe, ItemID.Safe), (Bank.DefendersForge, ItemID.DefendersForge), (Bank.VoidVault, ItemID.VoidVault)];
static readonly Action<Player, int, int, int> OpenChest = (Action<Player, int, int, int>)Delegate.CreateDelegate(typeof(Action<Player, int, int, int>), HookRegistry.Method(typeof(Player), "OpenChest", [typeof(int), typeof(int), typeof(int)]));
// Bank opened by a button; HandleBeingInChestRange would close it for being out of range.
public static Bank? Remote { get; private set; }
public static bool KeepsOpen(Player player)
{
if (Settings.BankButtons && Remote is { } bank && player.chest == (int)bank)
return true;
Remote = null;
return false;
}
public static void Draw(SpriteBatch spriteBatch)
{
Player player = Main.LocalPlayer;
if (!Main.playerInventory || player.sign >= 0)
return;
float y = FirstSlotY(player);
var mouse = new Point(Main.mouseX, Main.mouseY);
foreach ((Bank bank, int item) in Banks)
{
var center = new Vector2(ColumnX, y);
y += Spacing;
Rectangle bounds = Utils.CenteredRectangle(center, new Vector2(Size));
bool hover = bounds.Contains(mouse) && !PlayerInput.IgnoreMouseInterface;
bool open = player.chest == (int)bank;
if (hover)
bounds.Inflate(2, 2);
Render.Item(spriteBatch, item, bounds, hover || open ? Color.White : new Color(180, 180, 180));
if (!hover)
continue;
player.mouseInterface = true;
Main.instance.MouseText(Lang.GetItemNameValue(item), 0, 0, -1, -1, -1, -1, 0);
Main.mouseText = true;
if (Main.mouseLeft && Main.mouseLeftRelease)
{
Main.mouseLeftRelease = false;
Toggle(player, bank);
}
}
}
// Center of the slot after the last builder toggle; mirrors Main.DrawBuilderAccToggles.
static float FirstSlotY(Player player)
{
#if TMODLOADER
int count = BuilderToggleLoader.ActiveBuilderToggles(), page = BuilderToggleLoader.BuilderTogglePage;
bool shiftHotbarLock = count / 12 != page || count % 12 >= 10;
int start = 0, end = count;
bool previousArrow = false, nextArrow = false;
if (count > 12)
{
start = 12 * page;
end = count - start < 12 ? count : start == 0 ? start + 12 : start + 11;
previousArrow = page != 0;
nextArrow = page != count / 12;
}
return 77f - (shiftHotbarLock ? 42 : 21) + Spacing * (end - start + previousArrow.ToInt() + nextArrow.ToInt());
#else
int torchIcons = player.unlockedBiomeTorches.ToInt();
int toggles = player.InfoAccMechShowWires.ToInt() * 6 + player.rulerLine.ToInt() + player.rulerGrid.ToInt() + player.autoActuator.ToInt() + player.autoPaint.ToInt();
bool pushUp = toggles + 1 + torchIcons >= 10;
return 77f + Spacing * (torchIcons + toggles - pushUp.ToInt());
#endif
}
static void Toggle(Player player, Bank bank)
{
if (player.chest == (int)bank)
{
player.chest = -1;
Remote = null;
Widgets.Sound(11);
return;
}
bool wasClosed = player.chest == -1;
player.SetTalkNPC(-1);
Main.npcChatCornerItem = 0;
Main.npcChatText = "";
if (Main.editChest)
{
Widgets.Sound(12);
Main.editChest = false;
}
if (player.editedChestName && player.chest >= 0)
{
NetMessage.SendData(MessageID.ChestName, -1, -1, NetworkText.FromLiteral(Main.chest[player.chest].name), player.chest, 1f);
player.editedChestName = false;
}
player.piggyBankProjTracker.Clear();
player.voidLensChest.Clear();
Main.stackSplit = 600;
var tile = new Point((int)(player.Center.X / 16f), (int)(player.Center.Y / 16f));
OpenChest(player, tile.X, tile.Y, (int)bank);
Remote = bank;
Widgets.Sound(wasClosed ? 10 : 12);
}
}
+6 -2
View File
@@ -71,7 +71,11 @@ public abstract class GridBrowser(string id, string title, Vector2 position, int
const int TabGap = 4;
int iconTabs = tabs.Count(tab => tab.Icon >= 0);
int textTabs = tabs.Length - iconTabs;
int textWidth = textTabs == 0 ? 0 : (body.Width - TabGap * (tabs.Length - 1) - iconTabs * IconTabWidth) / textTabs;
int available = body.Width - TabGap * (tabs.Length - 1);
int LabelWidth(BrowserCategory tab) => (int)Math.Ceiling(Render.Measure(tab.Name, scale).X) + 16;
int labels = tabs.Where(tab => tab.Icon < 0).Sum(LabelWidth);
int iconWidth = iconTabs == 0 ? 0 : Math.Max(24, Math.Min(IconTabWidth, (available - labels) / iconTabs));
int spare = textTabs == 0 ? 0 : (available - labels - iconTabs * iconWidth) / textTabs;
Texture2D? icons = iconTabs > 0 ? GameTextures.Request("Images/UI/Creative/Infinite_Icons") : null;
bool changed = false;
int x = body.X;
@@ -79,7 +83,7 @@ public abstract class GridBrowser(string id, string title, Vector2 position, int
{
BrowserCategory tab = tabs[i];
bool iconTab = tab.Icon >= 0;
var bounds = new Rectangle(x, y, iconTab ? IconTabWidth : textWidth, height);
var bounds = new Rectangle(x, y, iconTab ? iconWidth : LabelWidth(tab) + spare, height);
x += bounds.Width + TabGap;
if (Widgets.Button(bounds, iconTab ? "" : tab.Name, i == selected, scale) && i != selected)
{
+2
View File
@@ -27,6 +27,8 @@ public sealed class IdIndex(Func<int> count, Func<int, string?> name)
public string Name(int id) => id > 0 && id < Names.Length ? Names[id] : "";
public void Refresh() => _lastQuery = null;
public void Filter(string query, Func<int, bool> fits)
{
query = query.Trim();
+20
View File
@@ -37,6 +37,10 @@ public static class InterfaceLayers
int healthBarIndex = layers.FindIndex(l => l.Name == "Vanilla: Entity Health Bars");
layers.Insert(healthBarIndex < 0 ? 1 : healthBarIndex + 1, new LegacyGameInterfaceLayer(Prefix + "Health Bars", DrawHealthBars, InterfaceScaleType.Game));
int builderIndex = layers.FindIndex(l => l.Name == "Vanilla: Builder Accessories Bar");
if (builderIndex >= 0)
layers.Insert(builderIndex + 1, new LegacyGameInterfaceLayer(Prefix + "Bank Buttons", DrawBankButtons, InterfaceScaleType.UI));
int mouseTextIndex = layers.FindIndex(l => l.Name == "Vanilla: Mouse Text");
layers.Insert(mouseTextIndex < 0 ? layers.Count : mouseTextIndex, new LegacyGameInterfaceLayer(Prefix + "Windows", DrawWindows, InterfaceScaleType.UI));
}
@@ -81,6 +85,22 @@ public static class InterfaceLayers
return true;
}
static bool DrawBankButtons()
{
try
{
if (Plugin.Active && !Main.gameMenu && Settings.BankButtons)
BankButtons.Draw(Main.spriteBatch);
}
catch (Exception e)
{
Log.Error("Bank buttons layer", e);
Widgets.Recover(Main.spriteBatch, Main.UIScaleMatrix);
}
return true;
}
static bool DrawWindows()
{
Unblock();
+2 -85
View File
@@ -1,89 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Terraria;
using Terraria.GameContent.Creative;
using Terraria.ID;
using Terraria.Localization;
using Terraria.UI;
#if TMODLOADER
using Terraria.ModLoader;
#endif
namespace TerraSharp.UI;
public sealed class ItemBrowser() : GridBrowser("items", "Item Browser", new Vector2(660, 120), 532, 520, "Search items by name or ID", "items", new IdIndex(() => ItemCount, Lang.GetItemNameValue), Categories())
public sealed class ItemBrowser() : ItemGrid("items", "Item Browser", new Vector2(660, 120), 534, 520, [])
{
const int InventorySlots = 50;
#if TMODLOADER
static int ItemCount => ItemLoader.ItemCount;
#else
static int ItemCount => ItemID.Count;
#endif
enum WeaponClass { Melee, Ranged, Magic, Summon, Other }
static readonly IItemEntryFilter Weapon = new ItemFilters.Weapon();
// Journey duplication filters paired with Infinite_Icons frame indices.
static readonly (IItemEntryFilter Filter, int Icon)[] Filters = [(new ItemFilters.Tools(), 6), (new ItemFilters.Armor(), 2), (new ItemFilters.Vanity(), 8), (new ItemFilters.Accessories(), 1), (new ItemFilters.MiscAccessories(), 9), (new ItemFilters.Consumables(), 3), (new ItemFilters.BuildingBlock(), 4), (new ItemFilters.Furniture(), 7), (new ItemFilters.Materials(), 10)];
static BrowserCategory[] Categories() =>
[
new("All", _ => true),
new(Language.GetTextValue(Weapon.GetDisplayNameKey()), type => Fits(Weapon, type), 0, [.. Enum.GetValues(typeof(WeaponClass)).Cast<WeaponClass>().Select(weaponClass => new BrowserCategory(weaponClass.ToString(), type => ContentSample(type) is { } item && ClassOf(item) == weaponClass)), new("Ammo", type => ContentSample(type) is { ammo: > 0 })]),
.. Filters.Select(entry => new BrowserCategory(Language.GetTextValue(entry.Filter.GetDisplayNameKey()), type => Fits(entry.Filter, type), entry.Icon)),
new(Language.GetTextValue("CreativePowers.TabMisc"), type => !Fits(Weapon, type) && !Filters.Any(entry => Fits(entry.Filter, type)), 5),
];
static Item? ContentSample(int type) => ContentSamples.ItemsByType.TryGetValue(type, out Item? item) ? item : null;
// Whips must be classified as summon weapons before checking melee.
static WeaponClass? ClassOf(Item item)
{
if (!Weapon.FitsFilter(item) || item.pick > 0 || item.axe > 0 || item.hammer > 0 || item.ammo > 0)
return null;
#if TMODLOADER
if (item.CountsAsClass(DamageClass.Summon))
return WeaponClass.Summon;
if (item.CountsAsClass(DamageClass.Magic))
return WeaponClass.Magic;
if (item.CountsAsClass(DamageClass.Ranged))
return WeaponClass.Ranged;
if (item.CountsAsClass(DamageClass.Melee))
return WeaponClass.Melee;
#else
if (item.summon)
return WeaponClass.Summon;
if (item.magic)
return WeaponClass.Magic;
if (item.ranged)
return WeaponClass.Ranged;
if (item.melee)
return WeaponClass.Melee;
#endif
return WeaponClass.Other;
}
static bool Fits(IItemEntryFilter filter, int type) => ContentSample(type) is { } item && filter.FitsFilter(item);
readonly Dictionary<int, Item> _samples = [];
public override void OnOpen() => Main.playerInventory = true;
Item Sample(int type)
{
if (!_samples.TryGetValue(type, out Item? item))
{
item = new Item();
item.SetDefaults(type);
_samples[type] = item;
}
return item;
}
protected override void DrawContent(Rectangle body)
{
DrawSearchGrid(body, body.Y, body.Bottom - 30);
@@ -92,18 +17,10 @@ public sealed class ItemBrowser() : GridBrowser("items", "Item Browser", new Vec
protected override void DrawSlot(int type, Rectangle bounds, bool hot)
{
Widgets.Slot(bounds, hot);
Render.Item(Widgets.SpriteBatch, type, new Rectangle(bounds.X + 6, bounds.Y + 6, bounds.Width - 12, bounds.Height - 12));
DrawItem(type, bounds, hot);
if (!hot)
return;
#if TMODLOADER
Item sample = Sample(type);
ItemSlot.MouseHover(ref sample, ItemSlot.Context.CreativeInfinite);
#else
ItemSlot.MouseHover(Sample(type), ItemSlot.Context.CreativeInfinite);
#endif
if (Widgets.Click)
Give(type, Widgets.Shift);
else if (Widgets.RightClick)
+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;
}
}
+97
View File
@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Terraria;
using Terraria.GameContent.Creative;
using Terraria.ID;
using Terraria.Localization;
using Terraria.UI;
#if TMODLOADER
using Terraria.ModLoader;
#endif
namespace TerraSharp.UI;
public abstract class ItemGrid(string id, string title, Vector2 position, int width, int height, BrowserCategory[] extra) : GridBrowser(id, title, position, width, height, "Search items by name or ID", "items", new IdIndex(() => ItemCount, Lang.GetItemNameValue), [.. Categories(), .. extra])
{
#if TMODLOADER
static int ItemCount => ItemLoader.ItemCount;
#else
static int ItemCount => ItemID.Count;
#endif
enum WeaponClass { Melee, Ranged, Magic, Summon, Other }
static readonly IItemEntryFilter Weapon = new ItemFilters.Weapon();
// Journey duplication filters paired with Infinite_Icons frame indices.
static readonly (IItemEntryFilter Filter, int Icon)[] Filters = [(new ItemFilters.Tools(), 6), (new ItemFilters.Armor(), 2), (new ItemFilters.Vanity(), 8), (new ItemFilters.Accessories(), 1), (new ItemFilters.MiscAccessories(), 9), (new ItemFilters.Consumables(), 3), (new ItemFilters.BuildingBlock(), 4), (new ItemFilters.Furniture(), 7), (new ItemFilters.Materials(), 10)];
static BrowserCategory[] Categories() =>
[
new("All", _ => true),
new(Language.GetTextValue(Weapon.GetDisplayNameKey()), type => Fits(Weapon, type), 0, [.. Enum.GetValues(typeof(WeaponClass)).Cast<WeaponClass>().Select(weaponClass => new BrowserCategory(weaponClass.ToString(), type => ContentSample(type) is { } item && ClassOf(item) == weaponClass)), new("Ammo", type => ContentSample(type) is { ammo: > 0 })]),
.. Filters.Select(entry => new BrowserCategory(Language.GetTextValue(entry.Filter.GetDisplayNameKey()), type => Fits(entry.Filter, type), entry.Icon)),
new(Language.GetTextValue("CreativePowers.TabMisc"), type => !Fits(Weapon, type) && !Filters.Any(entry => Fits(entry.Filter, type)), 5),
];
static Item? ContentSample(int type) => ContentSamples.ItemsByType.TryGetValue(type, out Item? item) ? item : null;
// Whips must be classified as summon weapons before checking melee.
static WeaponClass? ClassOf(Item item)
{
if (!Weapon.FitsFilter(item) || item.pick > 0 || item.axe > 0 || item.hammer > 0 || item.ammo > 0)
return null;
#if TMODLOADER
if (item.CountsAsClass(DamageClass.Summon))
return WeaponClass.Summon;
if (item.CountsAsClass(DamageClass.Magic))
return WeaponClass.Magic;
if (item.CountsAsClass(DamageClass.Ranged))
return WeaponClass.Ranged;
if (item.CountsAsClass(DamageClass.Melee))
return WeaponClass.Melee;
#else
if (item.summon)
return WeaponClass.Summon;
if (item.magic)
return WeaponClass.Magic;
if (item.ranged)
return WeaponClass.Ranged;
if (item.melee)
return WeaponClass.Melee;
#endif
return WeaponClass.Other;
}
static bool Fits(IItemEntryFilter filter, int type) => ContentSample(type) is { } item && filter.FitsFilter(item);
readonly Dictionary<int, Item> _samples = [];
Item Sample(int type)
{
if (!_samples.TryGetValue(type, out Item? item))
{
item = new Item();
item.SetDefaults(type);
_samples[type] = item;
}
return item;
}
protected void DrawItem(int type, Rectangle bounds, bool hot, bool selected = false)
{
Widgets.Slot(bounds, hot, selected);
Render.Item(Widgets.SpriteBatch, type, new Rectangle(bounds.X + 6, bounds.Y + 6, bounds.Width - 12, bounds.Height - 12));
if (!hot)
return;
#if TMODLOADER
Item sample = Sample(type);
ItemSlot.MouseHover(ref sample, ItemSlot.Context.CreativeInfinite);
#else
ItemSlot.MouseHover(Sample(type), ItemSlot.Context.CreativeInfinite);
#endif
}
}
+32
View File
@@ -0,0 +1,32 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
namespace TerraSharp.UI;
// Edits a persisted set of item types, such as the auto sell list.
public sealed class ItemPicker(string id, string title, Vector2 position, HashSet<int> list) : ItemGrid(id, title, position, 534, 520, [new("Selected", list.Contains)])
{
protected override void DrawContent(Rectangle body)
{
DrawSearchGrid(body, body.Y, body.Bottom - 40);
var footer = new Rectangle(body.X + 4, body.Bottom - 34, body.Width - 4, 32);
Render.TextMid(Widgets.SpriteBatch, $"{list.Count} selected Click: add or remove", footer, Widgets.Dim, 0.72f);
if (!Widgets.Button(new Rectangle(footer.Right - 100, footer.Y + 1, 100, 30), "Clear", false, 0.8f))
return;
list.Clear();
Index.Refresh();
}
protected override void DrawSlot(int type, Rectangle bounds, bool hot)
{
DrawItem(type, bounds, hot, list.Contains(type));
if (!hot || !Widgets.Click)
return;
if (!list.Remove(type))
list.Add(type);
Index.Refresh();
Widgets.Sound(12);
}
}
+62 -2
View File
@@ -7,6 +7,7 @@ using Terraria.GameContent.Events;
using TerraSharp.Core;
using TerraSharp.Features;
using TerraSharp.Features.Combat;
using TerraSharp.Patches;
using TerraSharp.Simulation;
namespace TerraSharp.UI;
@@ -19,6 +20,9 @@ public sealed partial class MainWindow
{
Group("Survival");
Toggle("God Mode", ref Settings.GodMode);
Toggle("Ghost Mode", ref Settings.GhostMode);
if (Toggle("Defense Multiplier", ref Settings.DefenseBoost))
Sub(() => Slider("Multiplier", ref Settings.DefenseMult, 1f, 20f, 3f, "x{0:0.0}"));
Toggle("Infinite Breath", ref Settings.InfBreath);
Toggle("Infinite Flight", ref Settings.InfFlight);
Toggle("Infinite Health", ref Settings.InfHealth);
@@ -44,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()
@@ -97,6 +105,8 @@ public sealed partial class MainWindow
Slider("Max range", ref Settings.AimRange, 5f, 150f, 60f, "{0:0} tiles");
Toggle("Show target", ref Settings.AimDrawTarget);
});
Toggle("Auto Dodge", ref Settings.AutoDodge);
Toggle("Magic Bullets", ref Settings.MagicBullets);
Toggle("Shot Preview", ref Settings.ShotPreview);
Group("Enemies");
@@ -121,6 +131,14 @@ public sealed partial class MainWindow
void ItemsPage()
{
Group("Items");
if (Toggle("Auto Sell", ref Settings.AutoSell))
Sub(() =>
{
Note($"{Settings.SellList.Count} items listed. Favorited items are never sold.");
if (Buttons(WindowManager.SellList.Open ? "Close Sell List" : "Edit Sell List") == 0)
WindowManager.Toggle(WindowManager.SellList);
});
Toggle("Bank Buttons", ref Settings.BankButtons);
Toggle("Infinite Items", ref Settings.InfItems);
Toggle("Instant Break", ref Settings.InstantBreak);
Toggle("Item Vacuum", ref Settings.ItemVacuum);
@@ -135,8 +153,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()
@@ -154,7 +175,17 @@ public sealed partial class MainWindow
Toggle("Tracers", ref Settings.EspLines);
Group("Targets");
Toggle("Boss cursor", ref Settings.EspBossCursor);
ToggleColor("Bosses (Rainbow)", ref Settings.EspBossRainbow, ref Settings.ColBoss, nameof(Settings.ColBoss));
ToggleColor("Chests", ref Settings.EspChests, ref Settings.ColChests, nameof(Settings.ColChests));
if (Settings.EspChests)
Sub(() =>
{
if (Toggle("Only with listed items", ref Settings.EspChestsListed))
Note($"{Settings.ChestList.Count} items listed. Multiplayer: only opened chests.");
if (Buttons(WindowManager.ChestList.Open ? "Close Item List" : "Edit Item List") == 0)
WindowManager.Toggle(WindowManager.ChestList);
});
ToggleColor("Critters", ref Settings.EspCritters, ref Settings.ColCritters, nameof(Settings.ColCritters));
ToggleColor("Friendly projectiles", ref Settings.EspProjFriendly, ref Settings.ColProjFriendly, nameof(Settings.ColProjFriendly));
ToggleColor("Ground items", ref Settings.EspItems, ref Settings.ColItems, nameof(Settings.ColItems));
@@ -173,6 +204,23 @@ public sealed partial class MainWindow
Toggle("Night Owl", ref Settings.NightOwl);
Toggle("Shine", ref Settings.Shine);
Toggle("Spelunker", ref Settings.Spelunker);
Group("Damage Numbers");
Note("Colors your hits by damage class. Crits are lighter.");
ToggleColor("Magic", ref Settings.DamageMagic, ref Settings.ColDamageMagic, nameof(Settings.ColDamageMagic));
ToggleColor("Melee", ref Settings.DamageMelee, ref Settings.ColDamageMelee, nameof(Settings.ColDamageMelee));
ToggleColor("Ranged", ref Settings.DamageRanged, ref Settings.ColDamageRanged, nameof(Settings.ColDamageRanged));
ToggleColor("Summon", ref Settings.DamageSummon, ref Settings.ColDamageSummon, nameof(Settings.ColDamageSummon));
Group("View");
Toggle("Full Bright", ref Settings.FullBright);
if (Toggle("Extended Zoom", ref Settings.ExtendedZoom))
Sub(() =>
{
float zoom = Main.GameZoomTarget;
Slider("Zoom", ref zoom, ZoomPatch.MinZoom, ZoomPatch.MaxZoom, 1f, "{0:0.00}x");
Main.GameZoomTarget = zoom;
});
}
void WorldPage()
@@ -228,6 +276,18 @@ public sealed partial class MainWindow
if (WorldFeatures.InvasionName is { } active && Buttons($"Stop {active}") == 0)
WorldFeatures.StopInvasion();
Group("Map");
Toggle("Map Teleport", ref Settings.MapTeleport);
if (Buttons(MapFeatures.Revealing ? $"Revealing... {MapFeatures.RevealProgress:0%}" : "Reveal Map") == 0 && !MapFeatures.Revealing)
MapFeatures.StartReveal();
Group("Spawning");
Note(NpcSpawner.Multiplayer ? "Multiplayer: boss summons only, spawn rate is ignored." : "Spawn rate only affects single player.");
if (Toggle("Spawn Rate", ref Settings.SpawnRateBoost))
Sub(() => Slider("Multiplier", ref Settings.SpawnRateMult, 0f, 20f, 5f, "x{0:0.0}", v => v < 0.05f ? "No spawns" : null));
if (Buttons(WindowManager.Npcs.Open ? "Close NPC Spawner" : "NPC Spawner") == 0)
WindowManager.Toggle(WindowManager.Npcs);
Group("Waypoints");
Note("Saved per world.");
if (Buttons("Save Current Position") == 0)
+16
View File
@@ -12,6 +12,10 @@ public sealed partial class MainWindow
["Infinite Flight"] = "Unlimited wing and rocket boot flight time.",
["Auto Heal"] = "Uses Quick Heal below the set health level. Waits for potion sickness to end.",
["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.",
@@ -26,6 +30,8 @@ public sealed partial class MainWindow
["Prediction"] = "Leads moving enemies and accounts for projectile drop.",
["Exact check"] = "Checks and corrects aim before firing. Auto Attack skips known misses. Orange brackets mean an unconfirmed shot.",
["Shot Preview"] = "Shows your next shot's path, including spread and homing.",
["Magic Bullets"] = "Your projectiles home in on the aimbot target or the nearest enemy, through blocks.",
["Auto Dodge"] = "Moves you out of the way of incoming projectiles and enemies.",
["Auto Attack"] = "Swings at enemies in reach and shoots at enemies your weapon can hit.",
["Show target"] = "Shows the cursor radius, target brackets and aim point.",
["Kill Aura"] = "Kills enemies in range and drops their loot.",
@@ -34,6 +40,9 @@ public sealed partial class MainWindow
["Instant Break"] = "Break blocks in one hit with your held pickaxe, axe or hammer.",
["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.",
["Auto Sell"] = "Turns listed items in your inventory into coins. Items worth nothing are deleted.",
["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.",
@@ -44,6 +53,7 @@ public sealed partial class MainWindow
["Distance"] = "Shown in feet. 1 tile = 2 ft.",
["Tracers"] = "Draws lines from you to each target.",
["Projectile paths"] = "Shows projectile paths until they hit a block or expire.",
["Boss cursor"] = "Shows off-screen bosses at the screen edge with their map head.",
["Bosses (Rainbow)"] = "Cycles boss colors. Turn off to use the chosen color.",
["Spelunker"] = "Makes ores, gems and treasure glow.",
["Hunter"] = "Makes enemies glow.",
@@ -51,6 +61,12 @@ public sealed partial class MainWindow
["Night Owl"] = "Brightens dark areas.",
["Danger Sense"] = "Makes traps and hazards glow.",
["Biome Sight"] = "Makes Corruption, Crimson and Hallow blocks glow.",
["Full Bright"] = "Lights everything at full brightness.",
["Extended Zoom"] = "Zoom in up to 8x and out until the edge of what the game draws.",
["Chests"] = "Marks chests. Their contents can be filtered with an item list.",
["Only with listed items"] = "Only marks chests holding an item from the list.",
["Map Teleport"] = "Right-click the fullscreen map to teleport there.",
["Reveal Map"] = "Reveals the whole map. Multiplayer only reveals areas the server has sent.",
["Full Heal"] = "Refills health and mana.",
["TP to Spawn"] = "Recalls like a Magic Mirror: to your bed if set, otherwise the world's spawn point.",
["Reset Learned Ballistics"] = "Clears learned projectile data. Learns it again as you shoot.",
+5 -5
View File
@@ -30,12 +30,12 @@ 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)),
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)),
new Page("Items", ItemsPage, () => Count(Settings.InstantBreak, Settings.InfItems, Settings.ItemVacuum, Settings.AutoFish, Settings.Sonar)),
new Page("Visuals", VisualsPage, () => Count(Settings.Esp, Settings.Spelunker, Settings.Hunter, Settings.Shine, Settings.NightOwl, Settings.DangerSense, Settings.BiomeSight)),
new Page("World", WorldPage, () => Count(Settings.FreezeTime)),
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)),
new Page("Visuals", VisualsPage, () => Count(Settings.Esp, Settings.Spelunker, Settings.Hunter, Settings.Shine, Settings.NightOwl, Settings.DangerSense, Settings.BiomeSight, Settings.FullBright, Settings.ExtendedZoom)),
new Page("World", WorldPage, () => Count(Settings.FreezeTime, Settings.SpawnRateBoost, Settings.MapTeleport)),
new Page("Keys", KeysPage, () => 0),
];
+62
View File
@@ -0,0 +1,62 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using Terraria.ID;
#if TMODLOADER
using Terraria.ModLoader;
#endif
using TerraSharp.Features;
namespace TerraSharp.UI;
public sealed class NpcBrowser() : GridBrowser("npcs", "NPC Spawner", new Vector2(660, 140), 532, 520, "Search NPCs by name or ID", "NPCs", new IdIndex(() => NpcCount, Lang.GetNPCNameValue), Categories)
{
const int Batch = 10;
#if TMODLOADER
static int NpcCount => NPCLoader.NPCCount;
#else
static int NpcCount => NPCID.Count;
#endif
static readonly BrowserCategory[] Categories = [new("All", NpcSpawner.Available), new("Bosses", type => NpcSpawner.Available(type) && IsBoss(type)), new("Enemies", type => NpcSpawner.Available(type) && Sample(type) is { friendly: false, townNPC: false } && !IsBoss(type) && !IsCritter(type)), new("Town", type => NpcSpawner.Available(type) && Sample(type) is { townNPC: true }), new("Critters", type => NpcSpawner.Available(type) && IsCritter(type))];
bool _multiplayer;
static NPC? Sample(int type) => ContentSamples.NpcsByNetId.TryGetValue(type, out NPC? npc) ? npc : null;
static bool IsBoss(int type) => Sample(type) is { boss: true };
static bool IsCritter(int type) => NPCID.Sets.CountsAsCritter[type] || Main.npcCatchable[type];
protected override void DrawContent(Rectangle body)
{
if (_multiplayer != NpcSpawner.Multiplayer)
{
_multiplayer = NpcSpawner.Multiplayer;
Index.Refresh();
}
DrawSearchGrid(body, body.Y, body.Bottom - 30);
string hint = _multiplayer ? "Multiplayer: only boss summons the server accepts" : $"Click: spawn in front of you Shift click: spawn {Batch}";
Render.TextMid(Widgets.SpriteBatch, hint, new Rectangle(body.X + 4, body.Bottom - 26, body.Width, 26), Widgets.Dim, 0.72f);
}
protected override void DrawSlot(int type, Rectangle bounds, bool hot)
{
Widgets.Slot(bounds, hot);
Main.instance.LoadNPC(type);
if (GameTextures.Get("Npc", type) is { } texture)
Widgets.Icon(texture, new Rectangle(bounds.X + 4, bounds.Y + 4, bounds.Width - 8, bounds.Height - 8), GameTextures.Frame(texture, 1, Main.npcFrameCount[type], 0, 0));
if (!hot)
return;
string action = _multiplayer ? "Click to summon" : "Click to spawn";
Widgets.Tooltip = $"{Index.Name(type)}\n{(IsBoss(type) ? "Boss" : "NPC")} #{type} {action}";
if (!Widgets.Click)
return;
NpcSpawner.Spawn(type, Widgets.Shift ? Batch : 1);
Widgets.Sound(12);
}
}
+2 -2
View File
@@ -127,7 +127,7 @@ public static class Render
return text + "...";
}
public static void Item(SpriteBatch spriteBatch, int type, Rectangle bounds)
public static void Item(SpriteBatch spriteBatch, int type, Rectangle bounds, Color? color = null)
{
if (type <= 0)
return;
@@ -139,7 +139,7 @@ public static class Render
float scale = Math.Min(1f, Math.Min(bounds.Width / (float)frame.Width, bounds.Height / (float)frame.Height));
Vector2 size = new Vector2(frame.Width, frame.Height) * scale;
spriteBatch.Draw(texture, new Vector2(bounds.X + (bounds.Width - size.X) / 2f, bounds.Y + (bounds.Height - size.Y) / 2f), frame, Color.White, 0f, Vector2.Zero, scale, SpriteEffects.None, 0f);
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 Vector2 ToScreen(Vector2 world) => Vector2.Transform(world - Main.screenPosition, Main.GameViewMatrix.TransformationMatrix);
+43
View File
@@ -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.";
}
}
+115
View File
@@ -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);
}
}
+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);
+11 -2
View File
@@ -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);
+36 -5
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
@@ -13,11 +14,17 @@ public static class WindowManager
public static readonly MainWindow Menu = new();
public static readonly ItemBrowser Items = new();
public static readonly BuffBrowser Buffs = new();
public static readonly NpcBrowser Npcs = new();
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, Menu];
static readonly List<Window> Stack = [Items, Buffs, Npcs, Editor, SellList, ChestList, Stats, Menu, StatsSetup];
static Window? _dragging;
static Vector2 _dragOffset;
@@ -25,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)
{
@@ -51,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)
@@ -107,7 +118,8 @@ public static class WindowManager
Unfocus();
}
static Vector2 ClampPosition(Window window, Vector2 position) => new(MathHelper.Clamp(position.X, 0f, Math.Max(0f, Main.screenWidth / Main.UIScale - window.Width)), MathHelper.Clamp(position.Y, 0f, Math.Max(0f, Main.screenHeight / Main.UIScale - window.Height)));
// PlayerInput.SetZoom_UI already divides Main.screenWidth/Height by the UI scale inside UI layers.
static Vector2 ClampPosition(Window window, Vector2 position) => new(MathHelper.Clamp(position.X, 0f, Math.Max(0f, PlayerInput.RealScreenWidth / Main.UIScale - window.Width)), MathHelper.Clamp(position.Y, 0f, Math.Max(0f, PlayerInput.RealScreenHeight / Main.UIScale - window.Height)));
public static void Draw(SpriteBatch spriteBatch)
{
@@ -117,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();
@@ -143,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);
@@ -151,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);
+80
View File
@@ -36,9 +36,51 @@ public static class Esp
DrawNpcs(spriteBatch);
if (Settings.EspItems)
DrawItems(spriteBatch);
if (Settings.EspChests)
DrawChests(spriteBatch);
if (Settings.EspWaypoints)
foreach (Waypoint waypoint in Waypoints.Current)
Box(spriteBatch, waypoint.Position, _player.width, _player.height, Premultiplied(Settings.ColWaypoint), waypoint.Name, -1, -1, true, true);
if (Settings.EspBossCursor)
DrawBossCursors(spriteBatch);
}
// Off-screen bosses get their map head and an arrow at the screen edge.
static void DrawBossCursors(SpriteBatch spriteBatch)
{
const float Margin = 40f;
var center = new Vector2(Main.screenWidth / 2f, Main.screenHeight / 2f);
Color color = Premultiplied(Settings.EspBossRainbow ? Rainbow() : Settings.ColBoss);
for (int i = 0; i < Main.maxNPCs; i++)
{
NPC npc = Main.npc[i];
if (!npc.active)
continue;
int head = npc.GetBossHeadTextureIndex();
if (head < 0 && !npc.boss)
continue;
Vector2 topLeft = Render.ToScreen(npc.position), bottomRight = Render.ToScreen(npc.position + new Vector2(npc.width, npc.height));
if (bottomRight.X >= 0 && bottomRight.Y >= 0 && topLeft.X <= Main.screenWidth && topLeft.Y <= Main.screenHeight)
continue;
Vector2 offset = (topLeft + bottomRight) / 2f - center;
float scale = Math.Min((center.X - Margin) / Math.Abs(offset.X), (center.Y - Margin) / Math.Abs(offset.Y));
Vector2 position = center + offset * scale, direction = Vector2.Normalize(offset), side = new(-direction.Y, direction.X);
Vector2 tip = position + direction * 28f, back = tip - direction * 10f;
Render.Line(spriteBatch, back + side * 8f, tip, color, 2f);
Render.Line(spriteBatch, back - side * 8f, tip, color, 2f);
if (GameTextures.Get("NpcHeadBoss", head) is { } texture)
spriteBatch.Draw(texture, position, null, Color.White, npc.GetBossHeadRotation(), new Vector2(texture.Width, texture.Height) / 2f, 1f, npc.GetBossHeadSpriteEffects(), 0f);
else
Render.Fill(spriteBatch, new Rectangle((int)position.X - 6, (int)position.Y - 6, 12, 12), color);
if (Settings.EspDistance)
Render.TextCenteredAt(spriteBatch, $"{(int)(Vector2.Distance(npc.Center, _player.Center) / 8f)} ft", position.X, position.Y + 18f, color, 0.7f);
}
}
static void DrawProjectiles(SpriteBatch spriteBatch)
@@ -116,6 +158,44 @@ public static class Esp
}
}
// Multiplayer clients only know the contents of chests they have opened.
static void DrawChests(SpriteBatch spriteBatch)
{
Color color = Premultiplied(Settings.ColChests);
for (int i = 0; i < Main.maxChests; i++)
{
Chest? chest = Main.chest[i];
if (chest is not null && ChestLabel(chest) is { } label)
Box(spriteBatch, new Vector2(chest.x * 16, chest.y * 16), 32, 32, color, label, -1, -1, true, true);
}
}
static string? ChestLabel(Chest chest)
{
string name = string.IsNullOrEmpty(chest.name) ? "Chest" : chest.name;
if (!Settings.EspChestsListed)
return name;
if (chest.item is null)
return null;
Item? first = null;
int matches = 0;
foreach (Item? item in chest.item)
{
if (item is null || item.type == 0 || !Settings.ChestList.Contains(item.type))
continue;
first ??= item;
matches++;
}
if (first is null)
return null;
string found = first.stack > 1 ? $"{first.Name} x{first.stack}" : first.Name;
return matches > 1 ? $"{name}: {found} +{matches - 1}" : $"{name}: {found}";
}
// Anchors and scale match Main.DrawInterface_14_EntityHealthBars.
static (Vector2 Position, float Scale) NpcBar(NPC npc)
{