Compare commits

...
10 Commits
30 changed files with 1348 additions and 81 deletions

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>0.1.0</Version> <Version>0.2.0</Version>
<TargetFrameworks>net48;net8.0</TargetFrameworks> <TargetFrameworks>net48;net8.0</TargetFrameworks>
<PlatformTarget Condition="'$(TargetFramework)' == 'net48'">x86</PlatformTarget> <PlatformTarget Condition="'$(TargetFramework)' == 'net48'">x86</PlatformTarget>
<PlatformTarget Condition="'$(TargetFramework)' == 'net8.0'">x64</PlatformTarget> <PlatformTarget Condition="'$(TargetFramework)' == 'net8.0'">x64</PlatformTarget>
+3
View File
@@ -31,6 +31,8 @@ public static class Hotkeys
new(InterfaceGroup, "NpcSpawner", "NPC Spawner", () => WindowManager.Toggle(WindowManager.Npcs)), new(InterfaceGroup, "NpcSpawner", "NPC Spawner", () => WindowManager.Toggle(WindowManager.Npcs)),
new(InterfaceGroup, "ItemEditor", "Item Editor", () => WindowManager.Toggle(WindowManager.Editor)), new(InterfaceGroup, "ItemEditor", "Item Editor", () => WindowManager.Toggle(WindowManager.Editor)),
new(InterfaceGroup, "Stats", "Stats Overlay", () => WindowManager.Toggle(WindowManager.Stats)), new(InterfaceGroup, "Stats", "Stats Overlay", () => WindowManager.Toggle(WindowManager.Stats)),
new(InterfaceGroup, "Players", "Player List", () => WindowManager.Toggle(WindowManager.Players)),
new(InterfaceGroup, "PartyFrames", "Party Frames", () => WindowManager.Toggle(WindowManager.Party)),
Toggle("Player", "GodMode", "God Mode", () => Settings.GodMode, v => Settings.GodMode = v), Toggle("Player", "GodMode", "God Mode", () => Settings.GodMode, v => Settings.GodMode = v),
Toggle("Player", "GhostMode", "Ghost Mode", () => Settings.GhostMode, v => Settings.GhostMode = v), Toggle("Player", "GhostMode", "Ghost Mode", () => Settings.GhostMode, v => Settings.GhostMode = v),
@@ -67,6 +69,7 @@ public static class Hotkeys
Toggle("Items", "InstantBreak", "Instant Break", () => Settings.InstantBreak, v => Settings.InstantBreak = 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", "InfItems", "Infinite Items", () => Settings.InfItems, v => Settings.InfItems = v),
Toggle("Items", "AutoSell", "Auto Sell", () => Settings.AutoSell, v => Settings.AutoSell = v), Toggle("Items", "AutoSell", "Auto Sell", () => Settings.AutoSell, v => Settings.AutoSell = v),
Toggle("Items", "AutoDeposit", "Auto Deposit", () => Settings.AutoDeposit, v => Settings.AutoDeposit = v),
new("Items", "MaxStack", "Max Stack", ItemFeatures.MaxStack), new("Items", "MaxStack", "Max Stack", ItemFeatures.MaxStack),
new("Items", "Duplicate", "Duplicate Item", ItemFeatures.Duplicate), new("Items", "Duplicate", "Duplicate Item", ItemFeatures.Duplicate),
+3
View File
@@ -23,6 +23,8 @@ public static partial class Settings
[nameof(ShowStats)] = "Player.Stats.Enabled", [nameof(ShowStats)] = "Player.Stats.Enabled",
[nameof(StatLines)] = "Player.Stats.Lines", [nameof(StatLines)] = "Player.Stats.Lines",
[nameof(StatIcons)] = "Player.Stats.Icons", [nameof(StatIcons)] = "Player.Stats.Icons",
[nameof(PartyFrames)] = "Player.PartyFrames.Enabled",
[nameof(PartySelf)] = "Player.PartyFrames.Self",
[nameof(Fly)] = "Movement.Fly.Enabled", [nameof(Fly)] = "Movement.Fly.Enabled",
[nameof(FlySpeed)] = "Movement.Fly.Speed", [nameof(FlySpeed)] = "Movement.Fly.Speed",
@@ -68,6 +70,7 @@ public static partial class Settings
[nameof(InfItems)] = "Items.InfiniteItems", [nameof(InfItems)] = "Items.InfiniteItems",
[nameof(ItemVacuum)] = "Items.Vacuum", [nameof(ItemVacuum)] = "Items.Vacuum",
[nameof(AutoSell)] = "Items.AutoSell.Enabled", [nameof(AutoSell)] = "Items.AutoSell.Enabled",
[nameof(AutoDeposit)] = "Items.AutoDeposit",
[nameof(BankButtons)] = "Items.BankButtons", [nameof(BankButtons)] = "Items.BankButtons",
[nameof(AutoFish)] = "Fishing.Enabled", [nameof(AutoFish)] = "Fishing.Enabled",
+2 -1
View File
@@ -47,6 +47,7 @@ public static partial class Settings
public static bool AutoHeal, AutoMana, GhostMode, DefenseBoost; public static bool AutoHeal, AutoMana, GhostMode, DefenseBoost;
public static float AutoHealAt = 40f, AutoManaAt = 25f, BuffMinutes = 10f, DefenseMult = 3f; public static float AutoHealAt = 40f, AutoManaAt = 25f, BuffMinutes = 10f, DefenseMult = 3f;
public static bool ShowStats, StatIcons = true; public static bool ShowStats, StatIcons = true;
public static bool PartyFrames, PartySelf;
public static StatLine StatLines = StatLine.Dps | StatLine.Damage | StatLine.Kills | StatLine.Minions | StatLine.Sentries | StatLine.Defense; public static 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 bool Fly, SuperSpeed, HighJump, NoFallDamage, LavaImmune, WaterWalk, NoKnockback, GravityFlip, ExtendedReach;
@@ -61,7 +62,7 @@ public static partial class Settings
public static bool Aimbot, AimPrediction = true, AimDrawTarget = true, AutoAttack, AimExact = true, ShotPreview, MagicBullets, AutoDodge; public static bool Aimbot, AimPrediction = true, AimDrawTarget = true, AutoAttack, AimExact = true, ShotPreview, MagicBullets, AutoDodge;
public static float AimFov = 250f, AimRange = 60f; public static float AimFov = 250f, AimRange = 60f;
public static bool InstantBreak, InfItems, ItemVacuum, AutoSell, BankButtons = true; public static bool InstantBreak, InfItems, ItemVacuum, AutoSell, AutoDeposit, BankButtons = true;
public static readonly HashSet<int> SellList = []; public static readonly HashSet<int> SellList = [];
public static bool AutoFish, AutoRecast = true, FishEnemies, Sonar; public static bool AutoFish, AutoRecast = true, FishEnemies, Sonar;
+235 -34
View File
@@ -2,31 +2,53 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Terraria; using Terraria;
using Terraria.ID;
using TerraSharp.Core; using TerraSharp.Core;
using TerraSharp.Simulation; using TerraSharp.Simulation;
namespace TerraSharp.Features.Combat; namespace TerraSharp.Features.Combat;
// Steers the local player out of predicted projectile and contact hits. // Steers the local player out of predicted projectile and contact hits, using dashes and teleport rods when running and jumping are not enough.
public static class AutoDodge public static class AutoDodge
{ {
const int Horizon = 24; const int Horizon = 24;
const int ContactHorizon = 12;
const int MaxThreats = 48; const int MaxThreats = 48;
const float ScanRange = 60f * 16f; const float ScanRange = 60f * 16f;
const float Margin = 6f; const float Margin = 6f;
const int WallCheckTicks = 6; const int WallCheckTicks = 6;
const int JumpCooldown = 12;
const float Gravity = 0.4f, MaxFall = 10f, FastFall = 8f;
// Slowest dash in Player.DashMovement (Tabi, Master Ninja Gear).
const float DashSpeed = 14.5f;
// Horizontal speed away from the player that counts as being knocked back, not drifting.
const float KnockbackSpeed = 1f;
// Teleport only for hits this close; a later run or jump may still avoid farther ones.
const int TeleportWithin = 8;
const int TeleportCooldown = 30;
static readonly float[] TeleportRadii = [6f, 10f, 14f, 18f];
const int TeleportDirections = 16;
const int TempleWall = 87;
sealed class Threat sealed class Threat
{ {
public readonly Vector2[] Path = new Vector2[Horizon + 1]; public readonly Vector2[] Path = new Vector2[Horizon + 1];
public int Count, Width, Height; public int Count, Width, Height, Damage;
} }
readonly record struct Move(int Horizontal, int Vertical); readonly record struct Move(int Horizontal, int Vertical, bool Dash = false);
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 Move Stay = new(0, 0);
static readonly Move[] Moves = [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 Move[] Dashes = [new(-1, 0, true), new(1, 0, true)];
static readonly Threat[] Pool = new Threat[MaxThreats]; static readonly Threat[] Pool = new Threat[MaxThreats];
static readonly List<Threat> Threats = []; static readonly List<Threat> Threats = [];
static readonly Vector2[] Trail = new Vector2[Horizon + 1];
static int _lastJump = -JumpCooldown, _lastTeleport = -TeleportCooldown;
static int _hitDamage;
static AutoDodge() static AutoDodge()
{ {
@@ -34,10 +56,34 @@ public static class AutoDodge
Pool[i] = new Threat(); Pool[i] = new Threat();
} }
// Player.HorizontalMovement prefix: controls and velocity set here drive this tick's movement. static bool Grounded(Player player) => player.velocity.Y == 0f || player.sliding;
static bool Flying(Player player) => Settings.Fly || player.wingTime > 0f || player.rocketTime > 0 || MountFlies(player);
// Mirrors the jump conditions in Player.JumpMovement; the game itself performs the jump.
static bool CanRise(Player player) => Settings.Fly || (Clock.Tick - _lastJump >= JumpCooldown && (Grounded(player) || Flying(player) || HasExtraJump(player)));
// Player.DashMovement starts a dash only with a dash accessory, off a mount, once dashDelay is back to 0.
static bool CanDash(Player player) => player.dashType > 0 && player.dashDelay == 0 && !player.mount.Active && !Settings.Fly;
#if TMODLOADER
static bool HasExtraJump(Player player) => player.AnyExtraJumpUsable();
static bool MountFlies(Player player) => player.mount.Active && player.mount.CanFly();
static int Wall(int x, int y) => Main.tile[x, y].WallType;
#else
static bool MountFlies(Player player) => player.mount.Active && player.mount.CanFly(player);
static bool HasExtraJump(Player player) => player.canJumpAgain_Cloud || player.canJumpAgain_Sandstorm || player.canJumpAgain_Blizzard || player.canJumpAgain_Fart || player.canJumpAgain_Sail || player.canJumpAgain_Unicorn || player.canJumpAgain_Santank || player.canJumpAgain_WallOfFleshGoat || player.canJumpAgain_Basilisk;
static int Wall(int x, int y) => Main.tile[x, y]?.wall ?? 0;
#endif
// Player.HorizontalMovement prefix: DashMovement, JumpMovement and WingMovement run later this tick and read the controls set here.
public static void Steer(Player player) public static void Steer(Player player)
{ {
if (!Settings.AutoDodge || player.dead) if (!Settings.AutoDodge || Settings.GodMode || player.dead)
return; return;
CollectThreats(player); CollectThreats(player);
@@ -45,61 +91,188 @@ public static class AutoDodge
return; return;
float speed = Settings.Fly ? Settings.FlySpeed : Math.Max(Math.Max(player.maxRunSpeed, player.accRunSpeed), 3f); float speed = Settings.Fly ? Settings.FlySpeed : Math.Max(Math.Max(player.maxRunSpeed, player.accRunSpeed), 3f);
float jump = Player.jumpSpeed * 1.5f; int bestHit = FirstHit(player, null, Stay, speed, false);
int baseline = FirstHit(player, player.velocity, false); if (bestHit > Horizon)
if (baseline > Horizon)
return; return;
Move best = Moves[0]; int incoming = _hitDamage;
int bestHit = baseline; Move best = Stay;
bool canRise = CanRise(player), canFall = !Grounded(player);
foreach (Move move in Moves) foreach (Move move in Moves)
{ {
if (move == Moves[0]) if ((move.Vertical < 0 && !canRise) || (move.Vertical > 0 && !canFall))
continue; continue;
var velocity = new Vector2(move.Horizontal * speed, move.Vertical == 0 ? player.velocity.Y : move.Vertical * jump * player.gravDir); int hit = FirstHit(player, null, move, speed, true);
int hit = FirstHit(player, velocity, true);
if (hit > bestHit) if (hit > bestHit)
(best, bestHit) = (move, hit); (best, bestHit) = (move, hit);
} }
if (best == Moves[0]) if (bestHit <= Horizon && CanDash(player))
return; foreach (Move move in Dashes)
{
Apply(player, best, speed, jump); int hit = FirstHit(player, null, move, speed, true);
if (hit > bestHit)
(best, bestHit) = (move, hit);
} }
static void Apply(Player player, Move move, float speed, float jump) if (bestHit <= TeleportWithin && TryTeleport(player, speed, incoming))
return;
if (best != Stay)
Apply(player, best, speed);
}
static void Apply(Player player, Move move, float speed)
{ {
player.controlLeft = move.Horizontal < 0; player.controlLeft = move.Horizontal < 0;
player.controlRight = move.Horizontal > 0; player.controlRight = move.Horizontal > 0;
if (move.Dash)
{
// DoCommonDashHandle reads a fresh press with dashTime already set as the second tap of a double tap.
player.dashTime = move.Horizontal * 15;
(player.releaseLeft, player.releaseRight) = (move.Horizontal < 0, move.Horizontal > 0);
return;
}
if (move.Horizontal != 0) if (move.Horizontal != 0)
player.velocity.X = move.Horizontal * speed; player.velocity.X = move.Horizontal * speed;
player.controlUp = move.Vertical < 0; player.controlUp = move.Vertical < 0 && Settings.Fly;
player.controlDown = move.Vertical > 0; player.controlDown = move.Vertical > 0;
if (move.Vertical != 0) if (move.Vertical > 0)
player.velocity.Y = move.Vertical * jump * player.gravDir; player.velocity.Y = Math.Max(player.velocity.Y * player.gravDir, FastFall) * player.gravDir;
if (move.Vertical >= 0)
return;
player.controlJump = true;
if (Grounded(player) || !Flying(player))
{
player.releaseJump = true;
_lastJump = Clock.Tick;
}
} }
// First tick a threat overlaps the player moving at this velocity, or Horizon + 1 when none do. // Rod of Harmony is free; Rod of Discord costs a seventh of max life under Chaos State, so it must cost less than the hit.
static int FirstHit(Player player, Vector2 velocity, bool checkWalls) static bool TryTeleport(Player player, float speed, int incoming)
{ {
Vector2 size = new(player.width, player.height); if (Clock.Tick - _lastTeleport < TeleportCooldown || FindRod(player) is not { } rod)
if (checkWalls && TileCollision.Sweep(player.Center, player.Center + velocity * WallCheckTicks, player.width, player.height)) return false;
return -1;
int cost = rod == ItemID.RodofDiscord && player.chaosState ? player.statLifeMax2 / 7 : 0;
if (cost > 0 && (cost >= incoming || cost >= player.statLife))
return false;
if (SafeSpot(player, speed) is not { } spot)
return false;
Teleport(player, spot, rod);
return true;
}
static int? FindRod(Player player)
{
int? found = null;
for (int i = 0; i < 58; i++)
{
int type = player.inventory[i].type;
if (type == ItemID.RodOfHarmony)
return type;
if (type == ItemID.RodofDiscord)
found = type;
}
return found;
}
// Nearest spot that Player.ItemCheck_UseTeleportRod accepts and no threat reaches.
static Vector2? SafeSpot(Player player, float speed)
{
foreach (float radius in TeleportRadii)
for (int i = 0; i < TeleportDirections; i++)
{
float angle = MathHelper.TwoPi * i / TeleportDirections;
Vector2 spot = player.position + new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * radius * 16f;
if (spot.X <= 50f || spot.X >= Main.maxTilesX * 16 - 50 || spot.Y <= 50f || spot.Y >= Main.maxTilesY * 16 - 50)
continue;
if (Collision.SolidCollision(spot, player.width, player.height))
continue;
if (Wall((int)(spot.X / 16f), (int)(spot.Y / 16f)) == TempleWall && !NPC.downedPlantBoss)
continue;
if (FirstHit(player, spot, Stay, speed, false) > Horizon)
return spot;
}
return null;
}
// The rod's effect from Player.ItemCheck_UseTeleportRod, without holding or swinging it.
static void Teleport(Player player, Vector2 spot, int rod)
{
player.Teleport(spot, 1, 0);
NetMessage.SendData(MessageID.TeleportEntity, -1, -1, null, 0, player.whoAmI, spot.X, spot.Y, 1);
_lastTeleport = Clock.Tick;
if (rod != ItemID.RodofDiscord)
return;
if (player.chaosState)
{
player.statLife -= player.statLifeMax2 / 7;
player.lifeRegenCount = 0;
player.lifeRegenTime = 0f;
}
player.AddBuff(BuffID.ChaosState, 360, true);
}
// Player positions from the current position, or from a teleport spot: jumps rise at Player.jumpSpeed for Player.jumpHeight ticks, dashes keep DashSpeed, everything else falls.
static void Predict(Player player, Vector2? spot, Move move, float speed)
{
bool moving = spot is null;
bool grounded = moving && Grounded(player);
bool fly = Settings.Fly || (move.Vertical < 0 && Flying(player) && !grounded);
float horizontal = move.Dash ? move.Horizontal * DashSpeed : move.Horizontal != 0 ? move.Horizontal * speed : moving ? player.velocity.X : 0f;
float vertical = moving ? player.velocity.Y * player.gravDir : 0f;
if (move.Vertical > 0)
vertical = Math.Max(vertical, FastFall);
Vector2 position = spot ?? player.position;
Trail[0] = position;
for (int t = 1; t <= Horizon; t++) for (int t = 1; t <= Horizon; t++)
{ {
Vector2 position = player.position + velocity * t; if (move.Vertical < 0)
var bounds = new Rectangle((int)(position.X - Margin), (int)(position.Y - Margin), (int)(size.X + Margin * 2), (int)(size.Y + Margin * 2)); vertical = fly || t <= Player.jumpHeight ? -Player.jumpSpeed : Math.Min(vertical + Gravity, MaxFall);
else if (!grounded || move.Vertical > 0)
vertical = Math.Min(vertical + Gravity, MaxFall);
if (!moving && TileCollision.BoxSolid(position + new Vector2(player.width / 2f, player.height / 2f + vertical), player.width, player.height))
vertical = 0f;
position += new Vector2(horizontal, vertical * player.gravDir);
Trail[t] = position;
}
}
// First tick a threat overlaps the player, or Horizon + 1 when none do; immunity frames count as safe. Sets _hitDamage.
static int FirstHit(Player player, Vector2? spot, Move move, float speed, bool checkWalls)
{
_hitDamage = 0;
Predict(player, spot, move, speed);
if (checkWalls && TileCollision.Sweep(player.Center, Trail[WallCheckTicks] + new Vector2(player.width / 2f, player.height / 2f), player.width, player.height))
return -1;
int safeUntil = player.immune ? player.immuneTime : 0;
for (int t = Math.Max(1, safeUntil + 1); t <= Horizon; t++)
{
Vector2 position = Trail[t];
var bounds = new Rectangle((int)(position.X - Margin), (int)(position.Y - Margin), (int)(player.width + Margin * 2), (int)(player.height + Margin * 2));
foreach (Threat threat in Threats) foreach (Threat threat in Threats)
{ {
if (t >= threat.Count) if (t >= threat.Count)
continue; continue;
Vector2 center = threat.Path[t]; 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))) if (!bounds.Intersects(new Rectangle((int)(center.X - threat.Width / 2f), (int)(center.Y - threat.Height / 2f), threat.Width, threat.Height)))
continue;
_hitDamage = threat.Damage;
return t; return t;
} }
} }
@@ -107,6 +280,33 @@ public static class AutoDodge
return Horizon + 1; return Horizon + 1;
} }
// Main.CalculateDamagePlayersTake from 1.4.5 (missing in tModLoader), then damage reduction.
static int Taken(Player player, int damage)
{
int defense = player.statDefense;
float armor = Main.masterMode ? 1f : Main.expertMode ? 0.75f : 0.5f;
return (int)(Math.Max(1f, damage - defense * armor) * (1f - player.endurance));
}
// Enemies the current swing will knock back, or that a hit is already pushing away, are not contact threats.
static bool KnockedAway(Player player, NPC npc)
{
if (npc.knockBackResist <= 0f)
return false;
int away = Math.Sign(npc.Center.X - player.Center.X);
if (npc.immune[player.whoAmI] > 0 && npc.velocity.X * away > KnockbackSpeed)
return true;
Item item = player.HeldItem;
if (player.itemAnimation <= 0 || item.damage <= 0 || item.noMelee || away != player.direction)
return false;
float reach = Math.Max(item.width, item.height) * item.scale + 8f;
float gap = Math.Abs(npc.Center.X - player.Center.X) - (npc.width + player.width) / 2f;
return gap <= reach && Math.Abs(npc.Center.Y - player.Center.Y) <= (npc.height + player.height) / 2f + reach;
}
static void CollectThreats(Player player) static void CollectThreats(Player player)
{ {
Threats.Clear(); Threats.Clear();
@@ -122,20 +322,21 @@ public static class AutoDodge
Threat threat = Pool[Threats.Count]; Threat threat = Pool[Threats.Count];
threat.Count = Ballistics.Path(Ballistics.Get(projectile.type), projectile.Center, velocity, age, Horizon, threat.Path); 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)); // Projectile.Damage doubles hostile projectile damage against players.
(threat.Width, threat.Height, threat.Damage) = (Math.Max(4, projectile.width), Math.Max(4, projectile.height), Taken(player, projectile.damage * 2));
Threats.Add(threat); Threats.Add(threat);
} }
for (int i = 0; i < Main.maxNPCs && Threats.Count < MaxThreats; i++) for (int i = 0; i < Main.maxNPCs && Threats.Count < MaxThreats; i++)
{ {
NPC npc = Main.npc[i]; NPC npc = Main.npc[i];
if (!Hostility.IsHostile(npc) || npc.damage <= 0 || Vector2.DistanceSquared(npc.Center, player.Center) > rangeSq) if (!Hostility.IsHostile(npc) || npc.damage <= 0 || Vector2.DistanceSquared(npc.Center, player.Center) > rangeSq || KnockedAway(player, npc))
continue; continue;
Threat threat = Pool[Threats.Count]; Threat threat = Pool[Threats.Count];
for (int t = 0; t <= Horizon; t++) for (int t = 0; t <= ContactHorizon; t++)
threat.Path[t] = npc.Center + NpcMotion.Offset(npc, t); threat.Path[t] = npc.Center + NpcMotion.Offset(npc, t);
(threat.Count, threat.Width, threat.Height) = (Horizon + 1, npc.width, npc.height); (threat.Count, threat.Width, threat.Height, threat.Damage) = (ContactHorizon + 1, npc.width, npc.height, Taken(player, npc.damage));
Threats.Add(threat); Threats.Add(threat);
} }
} }
+63
View File
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Terraria; using Terraria;
using Terraria.ID;
using TerraSharp.Core; using TerraSharp.Core;
using TerraSharp.UI; using TerraSharp.UI;
@@ -81,4 +84,64 @@ public static class ItemFeatures
if (sold) if (sold)
Widgets.Sound(CoinSound); Widgets.Sound(CoinSound);
} }
const int CarriedSlots = 58;
static readonly int[] Coins = [ItemID.PlatinumCoin, ItemID.GoldCoin, ItemID.SilverCoin, ItemID.CopperCoin];
static long CoinValue(int type) => type switch
{
ItemID.CopperCoin => 1,
ItemID.SilverCoin => 100,
ItemID.GoldCoin => 10_000,
ItemID.PlatinumCoin => 1_000_000,
_ => 0,
};
public static void DepositCoins(Player player)
{
if (player.HeldItem.useAmmo == AmmoID.Coin)
return;
Item[] inventory = player.inventory;
long total = 0;
for (int i = 0; i < CarriedSlots; i++)
if (!inventory[i].favorited)
total += CoinValue(inventory[i].type) * inventory[i].stack;
if (total == 0)
return;
Item[] bank = player.bank.item;
var slots = new List<int>();
for (int i = 0; i < bank.Length; i++)
{
long value = CoinValue(bank[i].type);
if (value > 0 || bank[i].type == 0)
slots.Add(i);
total += value * bank[i].stack;
}
var stacks = new List<(int Type, int Stack)>();
foreach (int coin in Coins)
{
long count = total / CoinValue(coin);
total %= CoinValue(coin);
int maxStack = ContentSamples.ItemsByType[coin].maxStack;
for (; count > 0; count -= maxStack)
stacks.Add((coin, (int)Math.Min(count, maxStack)));
}
if (stacks.Count > slots.Count)
return;
for (int i = 0; i < CarriedSlots; i++)
if (!inventory[i].favorited && CoinValue(inventory[i].type) > 0)
inventory[i].TurnToAir();
foreach (int slot in slots)
bank[slot].TurnToAir();
for (int i = 0; i < stacks.Count; i++)
{
bank[slots[i]].SetDefaults(stacks[i].Type);
bank[slots[i]].stack = stacks[i].Stack;
}
}
} }
@@ -147,6 +147,8 @@ public static class PlayerFeatures
AutoPotions(player); AutoPotions(player);
if (Settings.AutoSell) if (Settings.AutoSell)
ItemFeatures.AutoSell(player); ItemFeatures.AutoSell(player);
if (Settings.AutoDeposit)
ItemFeatures.DepositCoins(player);
} }
if (Settings.InfFlight) if (Settings.InfFlight)
@@ -284,6 +286,13 @@ public static class PlayerFeatures
player.fallStart = (int)(player.position.Y / 16f); player.fallStart = (int)(player.position.Y / 16f);
} }
// Same jump as the Wormhole Potion in Player.HandleSpectatingControls, without the potion.
public static void TeleportToPlayer(Player target)
{
Player player = Main.LocalPlayer;
player.UnityTeleport(player.position + target.Bottom - player.Bottom);
}
// Matches the Magic Mirror branch of Player.ItemCheck_Inner. // Matches the Magic Mirror branch of Player.ItemCheck_Inner.
public static void TeleportSpawn() public static void TeleportSpawn()
{ {
+46
View File
@@ -0,0 +1,46 @@
using Terraria;
namespace TerraSharp.Features;
public static class Spectate
{
public static bool Available => NpcSpawner.Multiplayer;
#if TMODLOADER
// tModLoader 1.4.4 has no spectating; SpectateCameraPatch follows this player instead.
public static int Target { get; private set; } = -1;
#else
// The client sets spectating to itself until the server confirms the target.
public static int Target => Main.LocalPlayer.spectating is var who && who != Main.myPlayer ? who : -1;
#endif
public static void Toggle(int who)
{
if (Target == who)
Stop();
else
Start(who);
}
static void Start(int who)
{
if (!Available)
return;
#if TMODLOADER
Target = who;
#else
Main.LocalPlayer.SetOrRequestSpectating(who);
#endif
}
public static void Stop()
{
#if TMODLOADER
Target = -1;
#else
if (Main.LocalPlayer.spectating >= 0)
Main.LocalPlayer.SetOrRequestSpectating(-1);
#endif
}
}
+35
View File
@@ -5,6 +5,9 @@ using MonoMod.Cil;
using Terraria; using Terraria;
using Terraria.Graphics.Light; using Terraria.Graphics.Light;
using TerraSharp.Core; using TerraSharp.Core;
#if TMODLOADER
using TerraSharp.Features;
#endif
namespace TerraSharp.Patches; namespace TerraSharp.Patches;
@@ -14,6 +17,9 @@ internal static class VisionPatches
{ {
FullBrightPatch.Install(hooks); FullBrightPatch.Install(hooks);
ZoomPatch.Install(hooks); ZoomPatch.Install(hooks);
#if TMODLOADER
SpectateCameraPatch.Install(hooks);
#endif
} }
} }
@@ -78,3 +84,32 @@ internal static class ZoomPatch
throw new MissingMethodException(context.Method.FullName, "Clamp"); throw new MissingMethodException(context.Method.FullName, "Clamp");
} }
} }
#if TMODLOADER
// Centers the camera the way Player.SpectatingCameraPosition does in 1.4.5.
internal static class SpectateCameraPatch
{
internal static void Install(HookRegistry hooks) => hooks.Add(typeof(Main), "DoDraw_UpdateCameraPosition", (Action<Action>)UpdateCamera);
static void UpdateCamera(Action original)
{
original();
Guard.Run("Spectate camera", Follow);
}
static void Follow()
{
if (Spectate.Target < 0)
return;
if (!Plugin.Active || Main.gameMenu || !Spectate.Available || Main.player[Spectate.Target] is not { active: true } target)
{
Spectate.Stop();
return;
}
Vector2 center = target.Bottom + new Vector2(0f, target.gfxOffY - 21f);
Main.screenPosition = (center - new Vector2(Main.screenWidth, Main.screenHeight) / 2f).Floor();
}
}
#endif
+6 -2
View File
@@ -11,7 +11,7 @@ using TerraSharp.Features;
namespace TerraSharp.UI; namespace TerraSharp.UI;
public sealed class BuffBrowser() : GridBrowser("buffs", "Buff Browser", new Vector2(660, 160), 480, 540, "Search buffs by name or ID", "buffs", new IdIndex(() => BuffCount, Lang.GetBuffName), Categories) public sealed class BuffBrowser() : GridBrowser("buffs", "Buff Browser", new Vector2(660, 160), 480, 574, "Search buffs by name or ID", "buffs", new IdIndex(() => BuffCount, Lang.GetBuffName), Categories, [])
{ {
const string DurationSlider = "buffDuration"; const string DurationSlider = "buffDuration";
@@ -45,7 +45,11 @@ public sealed class BuffBrowser() : GridBrowser("buffs", "Buff Browser", new Vec
static bool IsMount(int type) => BuffID.Sets.MountType[type] >= 0; static bool IsMount(int type) => BuffID.Sets.MountType[type] >= 0;
#endif #endif
static string FormatTime(int ticks) #if TMODLOADER
protected override string? ModOf(int type) => BuffLoader.GetBuff(type)?.Mod.DisplayNameClean;
#endif
public static string FormatTime(int ticks)
{ {
int seconds = ticks / 60; int seconds = ticks / 60;
return seconds >= 3600 ? $"{seconds / 3600}h" : seconds >= 60 ? $"{seconds / 60}m" : $"{seconds}s"; return seconds >= 3600 ? $"{seconds / 3600}h" : seconds >= 60 ? $"{seconds / 60}m" : $"{seconds}s";
+182 -14
View File
@@ -10,19 +10,50 @@ namespace TerraSharp.UI;
// Icon is a frame of Images/UI/Creative/Infinite_Icons; -1 draws the name instead. // Icon is a frame of Images/UI/Creative/Infinite_Icons; -1 draws the name instead.
public sealed record BrowserCategory(string Name, Func<int, bool> Fits, int Icon = -1, BrowserCategory[]? Subcategories = null); public sealed record BrowserCategory(string Name, Func<int, bool> Fits, int Icon = -1, BrowserCategory[]? Subcategories = null);
public abstract class GridBrowser(string id, string title, Vector2 position, int width, int height, string placeholder, string noun, IdIndex index, BrowserCategory[] categories) // A null order keeps ID order, with name-prefix matches first while searching.
public sealed record BrowserSort(string Name, Comparison<int>? Order);
public abstract class GridBrowser(string id, string title, Vector2 position, int width, int height, string placeholder, string noun, IdIndex index, BrowserCategory[] categories, BrowserSort[] sorts)
: Window(id, title, position, width, height) : Window(id, title, position, width, height)
{ {
const int SlotSize = 44, Gap = 4, Step = SlotSize + Gap, TabHeight = 32, SubTabHeight = 24, IconTabWidth = 38; enum Menu { None, Mod, Sort }
const int SlotSize = 44, Gap = 4, Step = SlotSize + Gap, TabHeight = 32, SubTabHeight = 24, IconTabWidth = 38, ToolbarHeight = 28, MenuRowHeight = 26, MenuRows = 10;
const float MenuScale = 0.78f;
readonly TextField _search = new(placeholder); readonly TextField _search = new(placeholder);
readonly Scroll _scroll = new(); readonly Scroll _scroll = new();
readonly Scroll _menuScroll = new();
string _lastQuery = ""; string _lastQuery = "";
int _category, _subcategory; int _category, _subcategory, _mod, _sort;
readonly BrowserCategory[]?[] _subTabs = categories.Select(category => category.Subcategories?.Prepend(new BrowserCategory("All", category.Fits)).ToArray()).ToArray(); readonly BrowserCategory[]?[] _subTabs = categories.Select(category => category.Subcategories?.Prepend(new BrowserCategory("All", category.Fits)).ToArray()).ToArray();
readonly BrowserSort[] _sorts = [new("ID", null), new("Name", (a, b) => string.Compare(index.Name(a), index.Name(b), StringComparison.OrdinalIgnoreCase)), .. sorts];
// Mod display name per id, null for vanilla.
string?[]? _sources;
(string? Source, string Name, int Count)[] _mods = [];
Func<int, bool>? _fits;
(int Category, int Subcategory, int Mod) _fitsKey;
readonly Dictionary<BrowserCategory, int> _counts = [];
(int Version, int Mod) _countsKey = (-1, -1);
Menu _menu;
Rectangle _menuAnchor;
bool _menuOpened;
protected IdIndex Index { get; } = index; protected IdIndex Index { get; } = index;
public override bool Open
{
get => base.Open;
set
{
base.Open = value;
_menu = Menu.None;
}
}
public sealed override void DrawBody(Rectangle body) public sealed override void DrawBody(Rectangle body)
{ {
if (Main.gameMenu) if (Main.gameMenu)
@@ -30,31 +61,170 @@ public abstract class GridBrowser(string id, string title, Vector2 position, int
Render.TextCentered(Widgets.SpriteBatch, "Enter a world to use this.", body, Widgets.Dim, 0.9f); Render.TextCentered(Widgets.SpriteBatch, "Enter a world to use this.", body, Widgets.Dim, 0.9f);
return; return;
} }
// An open menu takes all input until it closes.
bool blocked = Widgets.Blocked;
if (_menu != Menu.None)
Widgets.Blocked = true;
DrawContent(body); DrawContent(body);
Widgets.Blocked = blocked;
if (_menu != Menu.None)
DrawMenu(body);
} }
protected abstract void DrawContent(Rectangle body); protected abstract void DrawContent(Rectangle body);
protected abstract void DrawSlot(int id, Rectangle bounds, bool hot); protected abstract void DrawSlot(int id, Rectangle bounds, bool hot);
protected virtual string? ModOf(int id) => null;
protected void DrawSearchGrid(Rectangle body, int top, int bottom) protected void DrawSearchGrid(Rectangle body, int top, int bottom)
{ {
var field = new Rectangle(body.X, top, body.Width - 110, 36); LoadSources();
Widgets.Field(_search, field); Widgets.Field(_search, new Rectangle(body.X, top, body.Width, 36));
int gridTop = DrawCategories(body, top + 42); int toolbarY = top + 42;
Index.Filter(_search.Text, _subTabs[_category] is { } subTabs ? subTabs[_subcategory].Fits : categories[_category].Fits); int gridTop = DrawCategories(body, DrawToolbar(body, toolbarY));
Index.Filter(_search.Text, CurrentFits(), _sorts[_sort].Order);
if (_search.Text != _lastQuery) if (_search.Text != _lastQuery)
{ {
_lastQuery = _search.Text; _lastQuery = _search.Text;
_scroll.Reset(); _scroll.Reset();
} }
Render.TextRight(Widgets.SpriteBatch, $"{Index.Ids.Count} {noun}", new Rectangle(field.Right, top, 106, 36), Widgets.Dim, 0.8f); Render.TextRight(Widgets.SpriteBatch, $"{Index.Ids.Count} {noun}", new Rectangle(body.X, toolbarY, body.Width - 2, ToolbarHeight), Widgets.Dim, 0.8f);
var inner = new Rectangle(body.X, gridTop, body.Width, bottom - gridTop); var inner = new Rectangle(body.X, gridTop, body.Width, bottom - gridTop);
Widgets.Panel(inner, Widgets.InnerBg); Widgets.Panel(inner, Widgets.InnerBg);
DrawGrid(Index.Ids, new Rectangle(inner.X + 10, inner.Y + 10, inner.Width - 34, inner.Height - 20)); DrawGrid(Index.Ids, new Rectangle(inner.X + 10, inner.Y + 10, inner.Width - 34, inner.Height - 20));
} }
void LoadSources()
{
if (_sources is not null)
return;
int[] ids = [.. Index.All()];
_sources = new string?[ids.Length == 0 ? 0 : ids[ids.Length - 1] + 1];
Dictionary<string, int> counts = [];
foreach (int id in ids)
if (ModOf(id) is { } mod)
{
_sources[id] = mod;
counts[mod] = counts.TryGetValue(mod, out int count) ? count + 1 : 1;
}
if (counts.Count == 0)
return;
_mods = [(null, "All mods", ids.Length), (null, "Terraria", ids.Length - counts.Values.Sum()), .. counts.OrderBy(pair => pair.Key, StringComparer.OrdinalIgnoreCase).Select(pair => ((string?)pair.Key, pair.Key, pair.Value))];
}
string? Source(int id) => id < _sources!.Length ? _sources[id] : null;
Func<int, bool> WithMod(Func<int, bool> fits)
{
if (_mod == 0)
return fits;
string? source = _mods[_mod].Source;
return id => Source(id) == source && fits(id);
}
// IdIndex.Filter compares delegates by reference, so reuse one per selection.
Func<int, bool> CurrentFits()
{
if (_fits is not null && _fitsKey == (_category, _subcategory, _mod))
return _fits;
_fitsKey = (_category, _subcategory, _mod);
BrowserCategory selected = _subTabs[_category] is { } subTabs ? subTabs[_subcategory] : categories[_category];
return _fits = WithMod(selected.Fits);
}
int CountOf(BrowserCategory tab)
{
if (_countsKey != (Index.Version, _mod))
{
_counts.Clear();
_countsKey = (Index.Version, _mod);
}
if (!_counts.TryGetValue(tab, out int count))
_counts[tab] = count = Index.Count(WithMod(tab.Fits));
return count;
}
int DrawToolbar(Rectangle body, int y)
{
int x = body.X;
if (_mods.Length > 0)
x = MenuButton(new Rectangle(x, y, 190, ToolbarHeight), Menu.Mod, "Images/UI/Bestiary/Button_Filtering", _mods[_mod].Name) + 6;
MenuButton(new Rectangle(x, y, 150, ToolbarHeight), Menu.Sort, "Images/UI/Bestiary/Button_Sorting", $"Sort: {_sorts[_sort].Name}");
return y + ToolbarHeight + 6;
}
int MenuButton(Rectangle bounds, Menu menu, string icon, string label)
{
bool open = _menu == menu;
if (Widgets.Button(bounds, "", open, MenuScale))
{
(_menu, _menuOpened, open) = (menu, true, true);
_menuScroll.Reset();
}
if (open)
_menuAnchor = bounds;
Widgets.Icon(GameTextures.Request(icon), new Rectangle(bounds.X + 5, bounds.Y + 4, bounds.Height - 8, bounds.Height - 8));
var text = new Rectangle(bounds.X + bounds.Height + 2, bounds.Y, bounds.Width - bounds.Height - 8, bounds.Height);
Render.TextMid(Widgets.SpriteBatch, Render.Ellipsize(label, MenuScale, text.Width), text, open || Widgets.Hot(bounds) ? Widgets.Yellow : Color.White, MenuScale);
return bounds.Right;
}
void DrawMenu(Rectangle body)
{
(string Name, string Detail)[] entries = _menu == Menu.Mod ? [.. _mods.Select(mod => (mod.Name, mod.Count.ToString()))] : [.. _sorts.Select(sort => (sort.Name, ""))];
int selected = _menu == Menu.Mod ? _mod : _sort;
int widest = (int)entries.Max(entry => Render.Measure(entry.Name, MenuScale).X + Render.Measure(entry.Detail, MenuScale).X);
int contentHeight = entries.Length * MenuRowHeight;
var popup = new Rectangle(_menuAnchor.X, _menuAnchor.Bottom + 2, Math.Max(_menuAnchor.Width, widest + 56), Math.Min(Math.Min(entries.Length, MenuRows) * MenuRowHeight + 8, body.Bottom - _menuAnchor.Bottom - 2));
bool scrolls = contentHeight > popup.Height - 8;
Widgets.Panel(popup, Widgets.WindowBg);
var area = new Rectangle(popup.X + 4, popup.Y + 4, popup.Width - 8 - (scrolls ? 20 : 0), popup.Height - 8);
int y0 = _menuScroll.Begin(area);
int picked = -1;
for (int i = 0; i < entries.Length; i++)
{
var row = new Rectangle(area.X, y0 + i * MenuRowHeight, area.Width, MenuRowHeight);
if (row.Bottom < area.Y || row.Y > area.Bottom)
continue;
bool hot = Widgets.Hot(row);
if (hot)
Render.Fill(Widgets.SpriteBatch, row, Widgets.ButtonHot);
var text = new Rectangle(row.X + 6, row.Y, row.Width - 12, row.Height);
Render.TextMid(Widgets.SpriteBatch, entries[i].Name, text, hot || i == selected ? Widgets.Yellow : Color.White, MenuScale);
Render.TextRight(Widgets.SpriteBatch, entries[i].Detail, text, Widgets.Dim, MenuScale);
if (hot && Widgets.Click)
picked = i;
}
_menuScroll.End(area, contentHeight);
if (picked >= 0)
{
if (_menu == Menu.Mod)
_mod = picked;
else
_sort = picked;
_menu = Menu.None;
_scroll.Reset();
Widgets.Sound(12);
}
else if (Widgets.Click && !_menuOpened && !Widgets.Hot(popup))
{
_menu = Menu.None;
}
_menuOpened = false;
}
int DrawCategories(Rectangle body, int y) int DrawCategories(Rectangle body, int y)
{ {
if (DrawTabs(body, categories, ref _category, y, TabHeight, 0.78f)) if (DrawTabs(body, categories, ref _category, y, TabHeight, 0.78f))
@@ -91,18 +261,16 @@ public abstract class GridBrowser(string id, string title, Vector2 position, int
changed = true; changed = true;
_scroll.Reset(); _scroll.Reset();
} }
if (!iconTab) if (Widgets.Hot(bounds))
Widgets.Tooltip = $"{tab.Name}: {CountOf(tab)} {noun}";
if (!iconTab || icons is null)
continue; continue;
if (icons is not null)
{
// Journey icon frames include a 2px separator on the right. // Journey icon frames include a 2px separator on the right.
Rectangle frame = GameTextures.Frame(icons, 11, 1, tab.Icon, 0); Rectangle frame = GameTextures.Frame(icons, 11, 1, tab.Icon, 0);
frame.Width -= 2; frame.Width -= 2;
Widgets.Icon(icons, new Rectangle(bounds.X + 4, bounds.Y + 4, bounds.Width - 8, bounds.Height - 8), frame, 1f, i == selected || Widgets.Hot(bounds) ? Color.White : Color.White * 0.7f); Widgets.Icon(icons, new Rectangle(bounds.X + 4, bounds.Y + 4, bounds.Width - 8, bounds.Height - 8), frame, 1f, i == selected || Widgets.Hot(bounds) ? Color.White : Color.White * 0.7f);
} }
if (Widgets.Hot(bounds))
Widgets.Tooltip = tab.Name;
}
return changed; return changed;
} }
+36 -7
View File
@@ -8,9 +8,12 @@ public sealed class IdIndex(Func<int> count, Func<int, string?> name)
string[]? _names; string[]? _names;
string? _lastQuery; string? _lastQuery;
Func<int, bool>? _lastFits; Func<int, bool>? _lastFits;
Comparison<int>? _lastOrder;
public List<int> Ids { get; } = []; public List<int> Ids { get; } = [];
public int Version { get; private set; }
string[] Names string[] Names
{ {
get get
@@ -27,23 +30,45 @@ public sealed class IdIndex(Func<int> count, Func<int, string?> name)
public string Name(int id) => id > 0 && id < Names.Length ? Names[id] : ""; public string Name(int id) => id > 0 && id < Names.Length ? Names[id] : "";
public void Refresh() => _lastQuery = null; public IEnumerable<int> All()
{
string[] names = Names;
for (int i = 1; i < names.Length; i++)
if (names[i].Length > 0)
yield return i;
}
public void Filter(string query, Func<int, bool> fits) public int Count(Func<int, bool> fits)
{
int total = 0;
foreach (int id in All())
if (fits(id))
total++;
return total;
}
public void Refresh()
{
_lastQuery = null;
Version++;
}
public void Filter(string query, Func<int, bool> fits, Comparison<int>? order)
{ {
query = query.Trim(); query = query.Trim();
if (query == _lastQuery && fits == _lastFits) if (query == _lastQuery && fits == _lastFits && order == _lastOrder)
return; return;
(_lastQuery, _lastFits) = (query, fits); (_lastQuery, _lastFits, _lastOrder) = (query, fits, order);
string[] names = Names; string[] names = Names;
Ids.Clear(); Ids.Clear();
if (query.Length == 0) if (query.Length == 0)
{ {
for (int i = 1; i < names.Length; i++) foreach (int i in All())
if (names[i].Length > 0 && fits(i)) if (fits(i))
Ids.Add(i); Ids.Add(i);
return;
} }
else
{
if (int.TryParse(query, out int id) && id > 0 && id < names.Length && names[id].Length > 0 && fits(id)) if (int.TryParse(query, out int id) && id > 0 && id < names.Length && names[id].Length > 0 && fits(id))
Ids.Add(id); Ids.Add(id);
foreach (bool prefixPass in (bool[])[true, false]) foreach (bool prefixPass in (bool[])[true, false])
@@ -54,4 +79,8 @@ public sealed class IdIndex(Func<int> count, Func<int, string?> name)
Ids.Add(i); Ids.Add(i);
} }
} }
if (order is not null)
Ids.Sort((a, b) => order(a, b) is var result and not 0 ? result : a.CompareTo(b));
}
} }
+6 -1
View File
@@ -31,6 +31,7 @@ public static class InterfaceLayers
return; return;
layers.RemoveAll(l => l.Name.StartsWith(Prefix, StringComparison.Ordinal)); layers.RemoveAll(l => l.Name.StartsWith(Prefix, StringComparison.Ordinal));
PlayerHeads.Install();
layers.Insert(0, new LegacyGameInterfaceLayer(Prefix + "Under UI", DrawUnderUi, InterfaceScaleType.None)); layers.Insert(0, new LegacyGameInterfaceLayer(Prefix + "Under UI", DrawUnderUi, InterfaceScaleType.None));
@@ -45,7 +46,11 @@ public static class InterfaceLayers
layers.Insert(mouseTextIndex < 0 ? layers.Count : mouseTextIndex, new LegacyGameInterfaceLayer(Prefix + "Windows", DrawWindows, InterfaceScaleType.UI)); layers.Insert(mouseTextIndex < 0 ? layers.Count : mouseTextIndex, new LegacyGameInterfaceLayer(Prefix + "Windows", DrawWindows, InterfaceScaleType.UI));
} }
public static void Uninstall() => Layers?.RemoveAll(l => l.Name.StartsWith(Prefix, StringComparison.Ordinal)); public static void Uninstall()
{
Layers?.RemoveAll(l => l.Name.StartsWith(Prefix, StringComparison.Ordinal));
PlayerHeads.Uninstall();
}
static bool DrawUnderUi() static bool DrawUnderUi()
{ {
+1 -1
View File
@@ -3,7 +3,7 @@ using Terraria;
namespace TerraSharp.UI; namespace TerraSharp.UI;
public sealed class ItemBrowser() : ItemGrid("items", "Item Browser", new Vector2(660, 120), 534, 520, []) public sealed class ItemBrowser() : ItemGrid("items", "Item Browser", new Vector2(660, 120), 534, 554, [])
{ {
const int InventorySlots = 50; const int InventorySlots = 50;
+30 -5
View File
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Terraria; using Terraria;
using Terraria.GameContent.UI;
using Terraria.ID; using Terraria.ID;
#if TMODLOADER #if TMODLOADER
using Terraria.ModLoader; using Terraria.ModLoader;
@@ -19,6 +20,8 @@ public sealed class ItemEditor() : Window("itemEditor", "Item Editor", new Vecto
readonly Scroll _scroll = new(); readonly Scroll _scroll = new();
readonly Item _reference = new(); readonly Item _reference = new();
(int Type, int Prefix) _referenceKey = (-1, -1); (int Type, int Prefix) _referenceKey = (-1, -1);
(int Prefix, int Value, int Rarity)[] _prefixOptions = [];
int _prefixType = -1;
int _x, _y, _width; int _x, _y, _width;
bool _firstGroup; bool _firstGroup;
@@ -141,21 +144,43 @@ public sealed class ItemEditor() : Window("itemEditor", "Item Editor", new Vecto
return Lang.prefix[prefix].Value; return Lang.prefix[prefix].Value;
} }
// Ranked by the value each prefix gives, like reforge tiers.
(int Prefix, int Value, int Rarity)[] PrefixOptions(Item item)
{
if (_prefixType == item.type)
return _prefixOptions;
var sample = new Item();
_prefixOptions = [.. Prefixes(item).Prepend(0).Select(prefix =>
{
sample.SetDefaults(item.type);
if (prefix > 0)
sample.Prefix(prefix);
return (prefix, sample.value, sample.rare);
}).OrderBy(option => option.prefix != 0).ThenByDescending(option => option.value).ThenBy(option => PrefixName(option.prefix), StringComparer.OrdinalIgnoreCase)];
_prefixType = item.type;
return _prefixOptions;
}
void DrawPrefixes(Item item) void DrawPrefixes(Item item)
{ {
int[] prefixes = [.. Prefixes(item).OrderBy(PrefixName, StringComparer.OrdinalIgnoreCase)]; (int Prefix, int Value, int Rarity)[] options = PrefixOptions(item);
if (prefixes.Length == 0) if (options.Length <= 1)
return; return;
Group("Prefix"); Group("Prefix");
const int Columns = 3, Gap = 6; const int Columns = 3, Gap = 6;
const float Scale = 0.76f;
int width = (_width - Gap * (Columns - 1)) / Columns; int width = (_width - Gap * (Columns - 1)) / Columns;
int[] options = [0, .. prefixes];
for (int i = 0; i < options.Length; i++) for (int i = 0; i < options.Length; i++)
{ {
var bounds = new Rectangle(_x + i % Columns * (width + Gap), _y + i / Columns * 34, width, 30); 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]) int prefix = options[i].Prefix;
ApplyPrefix(item, options[i]); if (Widgets.Button(bounds, "", item.prefix == prefix, Scale) && item.prefix != prefix)
ApplyPrefix(item, prefix);
string name = Render.Ellipsize(prefix == 0 ? "None" : PrefixName(prefix), Scale, bounds.Width - 12);
Render.TextCentered(Widgets.SpriteBatch, name, bounds, ItemRarity.GetColor(options[i].Rarity), Scale);
} }
_y += (options.Length + Columns - 1) / Columns * 34 + 4; _y += (options.Length + Columns - 1) / Columns * 34 + 4;
} }
+32 -4
View File
@@ -13,7 +13,7 @@ using Terraria.ModLoader;
namespace TerraSharp.UI; 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]) 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], Sorts)
{ {
#if TMODLOADER #if TMODLOADER
static int ItemCount => ItemLoader.ItemCount; static int ItemCount => ItemLoader.ItemCount;
@@ -25,17 +25,45 @@ public abstract class ItemGrid(string id, string title, Vector2 position, int wi
static readonly IItemEntryFilter Weapon = new ItemFilters.Weapon(); static readonly IItemEntryFilter Weapon = new ItemFilters.Weapon();
// Journey duplication filters paired with Infinite_Icons frame indices. // Journey duplication filters paired with Infinite_Icons frame indices; a part without a test holds the rest.
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 readonly (IItemEntryFilter Filter, int Icon, (string Name, Func<Item, bool>? Fits)[]? Parts)[] Filters =
[
(new ItemFilters.Tools(), 6, [("Pickaxes", item => item.pick > 0), ("Axes", item => item.axe > 0), ("Hammers", item => item.hammer > 0), ("Fishing", item => item.fishingPole > 0), ("Other", null)]),
(new ItemFilters.Armor(), 2, null),
(new ItemFilters.Vanity(), 8, null),
(new ItemFilters.Accessories(), 1, [("Wings", item => item.wingSlot > 0), ("Boots", item => item.shoeSlot > 0), ("Shields", item => item.shieldSlot > 0), ("Other", null)]),
(new ItemFilters.MiscAccessories(), 9, [("Pets", item => item.buffType > 0 && Main.vanityPet[item.buffType]), ("Light Pets", item => item.buffType > 0 && Main.lightPet[item.buffType]), ("Minecarts", item => item.mountType >= 0 && MountID.Sets.Cart[item.mountType]), ("Mounts", item => item.mountType >= 0 && !MountID.Sets.Cart[item.mountType]), ("Hooks", item => Main.projHook[item.shoot])]),
(new ItemFilters.Consumables(), 3, null),
(new ItemFilters.BuildingBlock(), 4, null),
(new ItemFilters.Furniture(), 7, null),
(new ItemFilters.Materials(), 10, null),
];
static BrowserCategory[] Categories() => static BrowserCategory[] Categories() =>
[ [
new("All", _ => true), 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 })]), 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)), .. Filters.Select(entry => new BrowserCategory(Language.GetTextValue(entry.Filter.GetDisplayNameKey()), type => Fits(entry.Filter, type), entry.Icon, entry.Parts is null ? null : Split(entry.Filter, entry.Parts))),
new(Language.GetTextValue("CreativePowers.TabMisc"), type => !Fits(Weapon, type) && !Filters.Any(entry => Fits(entry.Filter, type)), 5), new(Language.GetTextValue("CreativePowers.TabMisc"), type => !Fits(Weapon, type) && !Filters.Any(entry => Fits(entry.Filter, type)), 5),
]; ];
static BrowserCategory[] Split(IItemEntryFilter filter, (string Name, Func<Item, bool>? Fits)[] parts) => [.. parts.Select(part => new BrowserCategory(part.Name, type => ContentSample(type) is { } item && filter.FitsFilter(item) && (part.Fits?.Invoke(item) ?? !parts.Any(other => other.Fits?.Invoke(item) ?? false))))];
static readonly BrowserSort[] Sorts = [new("Rarity", (a, b) => RarityRank(b).CompareTo(RarityRank(a))), new("Value", (a, b) => (ContentSample(b)?.value ?? 0).CompareTo(ContentSample(a)?.value ?? 0))];
// Expert (-12) and Master (-13) rank above Purple (11); Quest and Gray stay at the bottom.
static int RarityRank(int type) => ContentSample(type)?.rare switch
{
-13 => 13,
-12 => 12,
{ } rare => rare,
null => int.MinValue,
};
#if TMODLOADER
protected override string? ModOf(int type) => ItemLoader.GetItem(type)?.Mod.DisplayNameClean;
#endif
static Item? ContentSample(int type) => ContentSamples.ItemsByType.TryGetValue(type, out Item? item) ? item : null; static Item? ContentSample(int type) => ContentSamples.ItemsByType.TryGetValue(type, out Item? item) ? item : null;
// Whips must be classified as summon weapons before checking melee. // Whips must be classified as summon weapons before checking melee.
+1 -1
View File
@@ -4,7 +4,7 @@ using Microsoft.Xna.Framework;
namespace TerraSharp.UI; namespace TerraSharp.UI;
// Edits a persisted set of item types, such as the auto sell list. // 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)]) public sealed class ItemPicker(string id, string title, Vector2 position, HashSet<int> list) : ItemGrid(id, title, position, 534, 554, [new("Selected", list.Contains)])
{ {
protected override void DrawContent(Rectangle body) protected override void DrawContent(Rectangle body)
{ {
+7
View File
@@ -52,6 +52,12 @@ public sealed partial class MainWindow
Group("Stats"); Group("Stats");
if (Toggle("Stats Overlay", ref Settings.ShowStats) && Buttons("Configure Overlay") == 0) if (Toggle("Stats Overlay", ref Settings.ShowStats) && Buttons("Configure Overlay") == 0)
WindowManager.StatsSetup.ShowBeside(Bounds); WindowManager.StatsSetup.ShowBeside(Bounds);
Group("Party");
if (Toggle("Party Frames", ref Settings.PartyFrames))
Sub(() => Toggle("Include yourself", ref Settings.PartySelf));
if (Buttons(WindowManager.Players.Open ? "Close Player List" : "Player List") == 0)
WindowManager.Toggle(WindowManager.Players);
} }
void MovementPage() void MovementPage()
@@ -138,6 +144,7 @@ public sealed partial class MainWindow
if (Buttons(WindowManager.SellList.Open ? "Close Sell List" : "Edit Sell List") == 0) if (Buttons(WindowManager.SellList.Open ? "Close Sell List" : "Edit Sell List") == 0)
WindowManager.Toggle(WindowManager.SellList); WindowManager.Toggle(WindowManager.SellList);
}); });
Toggle("Auto Deposit", ref Settings.AutoDeposit);
Toggle("Bank Buttons", ref Settings.BankButtons); Toggle("Bank Buttons", ref Settings.BankButtons);
Toggle("Infinite Items", ref Settings.InfItems); Toggle("Infinite Items", ref Settings.InfItems);
Toggle("Instant Break", ref Settings.InstantBreak); Toggle("Instant Break", ref Settings.InstantBreak);
+3 -1
View File
@@ -14,6 +14,7 @@ public sealed partial class MainWindow
["Auto Mana"] = "Uses Quick Mana below the set mana level.", ["Auto Mana"] = "Uses Quick Mana below the set mana level.",
["Ghost Mode"] = "Enemies stop chasing you. Bosses and some flying enemies still attack.", ["Ghost Mode"] = "Enemies stop chasing you. Bosses and some flying enemies still attack.",
["Defense Multiplier"] = "Multiplies your total defense.", ["Defense Multiplier"] = "Multiplies your total defense.",
["Party Frames"] = "Shows your teammates' health and mana on screen. Open the menu to move it.",
["Stats Overlay"] = "Shows the selected stats on screen. Damage, kills and deaths count from when you enter the world.", ["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.", ["Configure Overlay"] = "Choose the lines the overlay shows.",
["Fly"] = "Move through blocks with your movement keys. Up or Jump rises; Down descends.", ["Fly"] = "Move through blocks with your movement keys. Up or Jump rises; Down descends.",
@@ -31,7 +32,7 @@ public sealed partial class MainWindow
["Exact check"] = "Checks and corrects aim before firing. Auto Attack skips known misses. Orange brackets mean an unconfirmed shot.", ["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.", ["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.", ["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 Dodge"] = "Moves you out of the way of incoming projectiles and enemies. Uses jumps, wings, dashes and Rod of Harmony or Discord when needed.",
["Auto Attack"] = "Swings at enemies in reach and shoots at enemies your weapon can hit.", ["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.", ["Show target"] = "Shows the cursor radius, target brackets and aim point.",
["Kill Aura"] = "Kills enemies in range and drops their loot.", ["Kill Aura"] = "Kills enemies in range and drops their loot.",
@@ -40,6 +41,7 @@ public sealed partial class MainWindow
["Instant Break"] = "Break blocks in one hit with your held pickaxe, axe or hammer.", ["Instant Break"] = "Break blocks in one hit with your held pickaxe, axe or hammer.",
["Infinite Items"] = "Keeps inventory stacks from shrinking.", ["Infinite Items"] = "Keeps inventory stacks from shrinking.",
["Item Vacuum"] = "Pulls all dropped items to you.", ["Item Vacuum"] = "Pulls all dropped items to you.",
["Auto Deposit"] = "Sends all coins to the Piggy Bank.",
["Bank Buttons"] = "Piggy Bank, Safe, Defender's Forge and Void Vault buttons left of the inventory.", ["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.", ["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.", ["Item Editor"] = "Change the prefix, stats and stack of the held or cursor item.",
+2 -2
View File
@@ -30,10 +30,10 @@ public sealed partial class MainWindow : Window
Open = true; Open = true;
_pages = _pages =
[ [
new Page("Player", PlayerPage, () => Count(Settings.GodMode, Settings.InfHealth, Settings.InfMana, Settings.InfBreath, Settings.InfFlight, Settings.InstantRespawn, Settings.AutoHeal, Settings.AutoMana, Settings.GhostMode, Settings.DefenseBoost, Settings.ShowStats)), new Page("Player", PlayerPage, () => Count(Settings.GodMode, Settings.InfHealth, Settings.InfMana, Settings.InfBreath, Settings.InfFlight, Settings.InstantRespawn, Settings.AutoHeal, Settings.AutoMana, Settings.GhostMode, Settings.DefenseBoost, Settings.ShowStats, Settings.PartyFrames)),
new Page("Movement", MovementPage, () => Count(Settings.Fly, Settings.SuperSpeed, Settings.HighJump, Settings.GravityFlip, Settings.NoFallDamage, Settings.LavaImmune, Settings.WaterWalk, Settings.NoKnockback, Settings.ExtendedReach)), new Page("Movement", MovementPage, () => Count(Settings.Fly, Settings.SuperSpeed, Settings.HighJump, Settings.GravityFlip, Settings.NoFallDamage, Settings.LavaImmune, Settings.WaterWalk, Settings.NoKnockback, Settings.ExtendedReach)),
new Page("Combat", CombatPage, () => Count(Settings.RapidAttack, Settings.AutoSwing, Settings.DamageBoost, Settings.NoManaCost, Settings.NoSpread, Settings.Aimbot, Settings.AutoAttack, Settings.KillAura, Settings.HitMagnet, Settings.FreezeEnemies, Settings.WeakenEnemies, Settings.MagicBullets, Settings.AutoDodge)), new Page("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("Items", ItemsPage, () => Count(Settings.InstantBreak, Settings.InfItems, Settings.ItemVacuum, Settings.AutoSell, Settings.AutoDeposit, 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("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("World", WorldPage, () => Count(Settings.FreezeTime, Settings.SpawnRateBoost, Settings.MapTeleport)),
new Page("Keys", KeysPage, () => 0), new Page("Keys", KeysPage, () => 0),
+7 -1
View File
@@ -9,7 +9,7 @@ using TerraSharp.Features;
namespace TerraSharp.UI; 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) public sealed class NpcBrowser() : GridBrowser("npcs", "NPC Spawner", new Vector2(660, 140), 532, 554, "Search NPCs by name or ID", "NPCs", new IdIndex(() => NpcCount, Lang.GetNPCNameValue), Categories, Sorts)
{ {
const int Batch = 10; const int Batch = 10;
@@ -21,8 +21,14 @@ public sealed class NpcBrowser() : GridBrowser("npcs", "NPC Spawner", new Vector
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))]; 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))];
static readonly BrowserSort[] Sorts = [new("Life", (a, b) => (Sample(b)?.lifeMax ?? 0).CompareTo(Sample(a)?.lifeMax ?? 0))];
bool _multiplayer; bool _multiplayer;
#if TMODLOADER
protected override string? ModOf(int type) => NPCLoader.GetNPC(type)?.Mod.DisplayNameClean;
#endif
static NPC? Sample(int type) => ContentSamples.NpcsByNetId.TryGetValue(type, out NPC? npc) ? npc : null; 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 IsBoss(int type) => Sample(type) is { boss: true };
+189
View File
@@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using Terraria.ID;
using TerraSharp.Core;
using TerraSharp.Features;
namespace TerraSharp.UI;
// Teammate health (similar to what we have in MMOs)
public sealed class PartyFrames() : Window("party", "", new Vector2(20, 440), FrameWidth, RowHeight)
{
const int FrameWidth = 240, RowHeight = 40, IconSize = 16;
static readonly Color Life = new(220, 50, 50), Mana = new(70, 110, 235);
public override bool Open { get => Settings.PartyFrames; set => Settings.PartyFrames = value; }
public override bool Frameless => true;
// Bounds from the last draw; input is resolved before the body draws.
readonly List<Rectangle> _frames = [], _eyes = [];
public override bool Interactive => !Main.gameMenu && (WindowManager.Menu.Open || _frames.Exists(frame => frame.Contains(Widgets.Mouse)));
public override bool Draggable => WindowManager.Menu.Open && !_eyes.Exists(eye => eye.Contains(Widgets.Mouse));
public override void DrawBody(Rectangle body)
{
if (Main.gameMenu)
return;
_frames.Clear();
_eyes.Clear();
Player local = Main.LocalPlayer;
int y = body.Y;
if (Settings.PartySelf)
y += Frame(local, body.X, y, body.Width);
for (int i = 0; i < Main.maxPlayers && local.team > 0; i++)
if (i != Main.myPlayer && Main.player[i] is { active: true } player && player.team == local.team)
y += Frame(player, body.X, y, body.Width);
Height = Math.Max(RowHeight, y - body.Y);
if (!WindowManager.Menu.Open)
return;
if (y == body.Y)
Render.TextMid(Widgets.SpriteBatch, "Party frames: no teammates", body, Widgets.Dim, 0.75f);
Render.Outline(Widgets.SpriteBatch, body, Widgets.Hot(body) ? Widgets.Yellow * 0.6f : Color.White * 0.2f);
}
// Returns the height used, including the gap below.
int Frame(Player player, int x, int y, int width)
{
int debuffs = 0;
foreach (int t in player.buffType)
if (ShownDebuff(t))
debuffs++;
var frame = new Rectangle(x, y, width, RowHeight - 4 + (debuffs > 0 ? IconSize + 3 : 0));
_frames.Add(frame);
float fade = player.dead ? 0.5f : 1f;
Render.FillSoft(Widgets.SpriteBatch, frame, Color.Black * 0.45f);
if (Spectate.Target == player.whoAmI)
Render.Outline(Widgets.SpriteBatch, frame, Widgets.Yellow * 0.8f);
var eye = new Rectangle(frame.Right - 21, frame.Y + 18, IconSize, IconSize);
Actions(player, frame, eye);
PlayerHeads.Draw(player, new Vector2(frame.X + 18, frame.Y + (RowHeight - 4) / 2f), 0.8f);
if (!player.dead && !player.HeldItem.IsAir)
Render.Item(Widgets.SpriteBatch, player.HeldItem.type, new Rectangle(eye.X, frame.Y + 2, IconSize, IconSize));
int barX = frame.X + 38, barWidth = frame.Right - 26 - barX;
Eye(player, eye);
// The name line fills from the right: life, defense, potion sickness, then the name in what is left.
int right = barX + barWidth;
string life = player.dead ? DeathText(player) : $"{player.statLife}/{player.statLifeMax2}";
right = Stat(life, right, frame.Y, player.dead ? null : GameTextures.Get("Heart"), null, (player.dead ? Widgets.Red : Color.White) * fade, null);
if (!player.dead && GameTextures.Get("Extra", 58) is { } shield)
{
int defense = (int)player.statDefense;
right = Stat($"{defense}", right - 4, frame.Y, shield, GameTextures.Frame(shield, 3, 2, 0, 0), Color.White * fade, $"{defense} {Lang.inter[10].Value}");
}
if (player.FindBuffIndex(BuffID.PotionSickness) is var sickness and >= 0)
{
right -= IconSize + 4;
PotionSickness(player, sickness, new Rectangle(right, frame.Y + 3, IconSize, IconSize));
}
Render.Text(Widgets.SpriteBatch, Render.Ellipsize(player.name, 0.7f, right - barX - 6), new Vector2(barX, frame.Y + 2), Render.TeamColor(player) * fade, 0.7f);
Bar(new Rectangle(barX, frame.Y + 21, barWidth, 8), player.dead ? 0f : player.statLife / (float)Math.Max(1, player.statLifeMax2), Life);
Bar(new Rectangle(barX, frame.Y + 31, barWidth, 3), player.dead ? 0f : player.statMana / (float)Math.Max(1, player.statManaMax2), Mana);
if (debuffs > 0)
Debuffs(player, new Rectangle(barX, frame.Y + RowHeight - 4, barWidth, IconSize));
return frame.Height + 4;
}
// Clicks act only while the menu is closed; with it open, a click drags the frames.
static void Actions(Player player, Rectangle frame, Rectangle eye)
{
bool other = player.whoAmI != Main.myPlayer && Spectate.Available;
if (!other || !Widgets.Hot(frame) || eye.Contains(Widgets.Mouse))
return;
Widgets.Tooltip = WindowManager.Menu.Open ? "Right-click to teleport" : "Click to spectate, right-click to teleport";
if (Widgets.Click && !WindowManager.Menu.Open)
Spectate.Toggle(player.whoAmI);
if (Widgets.RightClick && !player.dead)
PlayerFeatures.TeleportToPlayer(player);
}
void Eye(Player player, Rectangle bounds)
{
_eyes.Add(bounds);
bool hot = Widgets.Hot(bounds);
bool inspecting = WindowManager.Inspector.Open && WindowManager.Inspector.Target == player.whoAmI;
Widgets.Icon(GameTextures.Get(inspecting ? "InventoryTickOff" : "InventoryTickOn"), bounds, null, 1f, hot ? Widgets.Yellow : Color.White * 0.7f);
if (!hot)
return;
Widgets.Tooltip = inspecting ? "Close inspect" : "Inspect";
if (!Widgets.Click)
return;
if (inspecting)
WindowManager.Hide(WindowManager.Inspector);
else
WindowManager.Inspector.Inspect(player.whoAmI);
}
// respawnTimer only counts down where the game updates it; otherwise just "Dead".
static string DeathText(Player player) => player.respawnTimer > 0 ? $"Dead {(player.respawnTimer + 59) / 60}s" : "Dead";
// Draws an icon and value ending at right; returns where it starts.
static int Stat(string text, int right, int y, Texture2D? icon, Rectangle? frame, Color color, string? tooltip)
{
const int statIcon = 12;
int textWidth = (int)Render.Measure(text, 0.65f).X;
Render.Text(Widgets.SpriteBatch, text, new Vector2(right - textWidth, y + 3), color, 0.65f);
if (icon is null)
return right - textWidth;
var bounds = new Rectangle(right - textWidth - statIcon - 2, y + 4, statIcon, statIcon);
Widgets.Icon(icon, bounds, frame, 1f, color);
if (tooltip is not null && Widgets.Hot(new Rectangle(bounds.X, y + 2, right - bounds.X, IconSize)))
Widgets.Tooltip = tooltip;
return bounds.X;
}
static void PotionSickness(Player player, int index, Rectangle bounds)
{
Render.Item(Widgets.SpriteBatch, ItemID.HealingPotion, bounds, Color.White * 0.55f);
Render.Line(Widgets.SpriteBatch, new Vector2(bounds.X, bounds.Bottom), new Vector2(bounds.Right, bounds.Y), Widgets.Red, 2f);
if (Widgets.Hot(bounds))
Widgets.Tooltip = player.buffTime[index] > 0 ? $"{Lang.GetBuffName(BuffID.PotionSickness)}: {BuffBrowser.FormatTime(player.buffTime[index])}" : Lang.GetBuffName(BuffID.PotionSickness);
}
// Potion sickness has its own icon next to the name.
static bool ShownDebuff(int type) => type > 0 && type != BuffID.PotionSickness && Main.debuff[type];
static void Debuffs(Player player, Rectangle row)
{
int x = row.X;
for (int i = 0; i < player.buffType.Length && x + IconSize <= row.Right; i++)
{
int type = player.buffType[i];
if (!ShownDebuff(type))
continue;
var bounds = new Rectangle(x, row.Y, IconSize, IconSize);
Widgets.Icon(GameTextures.Get("Buff", type), bounds);
if (Widgets.Hot(bounds))
Widgets.Tooltip = player.buffTime[i] > 0 ? $"{Lang.GetBuffName(type)}: {BuffBrowser.FormatTime(player.buffTime[i])}" : Lang.GetBuffName(type);
x += IconSize + 2;
}
}
static void Bar(Rectangle bounds, float fraction, Color color)
{
Render.Fill(Widgets.SpriteBatch, bounds, Color.Black * 0.6f);
Render.Fill(Widgets.SpriteBatch, new Rectangle(bounds.X, bounds.Y, (int)(bounds.Width * MathHelper.Clamp(fraction, 0f, 1f)), bounds.Height), color);
}
}
+97
View File
@@ -0,0 +1,97 @@
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using Terraria.DataStructures;
using Terraria.GameContent;
using TerraSharp.Core;
namespace TerraSharp.UI;
// MapHeadRenderer with its own targets, so heads are always facing right (lol)
public sealed class PlayerHeads : INeedRenderTargetContent
{
static readonly PlayerHeads Instance = new();
readonly PlayerHeadDrawRenderTargetContent[] _heads = new PlayerHeadDrawRenderTargetContent[Main.maxPlayers];
bool _anyRequested;
PlayerHeads()
{
for (int i = 0; i < _heads.Length; i++)
_heads[i] = new PlayerHeadDrawRenderTargetContent();
}
public static void Install()
{
Main.ContentThatNeedsRenderTargets.Remove(Instance);
Main.ContentThatNeedsRenderTargets.Add(Instance);
}
public static void Uninstall()
{
Main.ContentThatNeedsRenderTargets.Remove(Instance);
Instance.Reset();
}
// Requested every frame, appearance changes show on the next frame.
public static void Draw(Player player, Vector2 center, float scale)
{
PlayerHeadDrawRenderTargetContent head = Instance._heads[player.whoAmI];
head.UsePlayer(player);
head.UseColor(Main.GetPlayerHeadBordersColor(player));
head.Request();
Instance._anyRequested = true;
if (!head.IsReady)
return;
RenderTarget2D target = head.GetTarget();
// DrawData constructors overload on ReLogic's Asset<T>, so set the fields directly.
var data = default(DrawData);
data.texture = target;
data.position = center;
data.sourceRect = target.Bounds;
data.color = Color.White;
data.origin = new Vector2(target.Width, target.Height) / 2f;
data.scale = new Vector2(scale);
PlayerDrawHelper.SetShaderForData(player, player.cHead, ref data);
data.Draw(Widgets.SpriteBatch);
Main.pixelShader.CurrentTechnique.Passes[0].Apply();
}
public bool IsReady => !_anyRequested;
// PlayerDrawHeadSet.BoringSetup reads the direction while the target renders.
public void PrepareRenderTarget(GraphicsDevice device, SpriteBatch spriteBatch)
{
if (!_anyRequested)
return;
_anyRequested = false;
for (int i = 0; i < _heads.Length; i++)
{
Player player = Main.player[i];
int direction = player.direction;
player.direction = 1;
try
{
_heads[i].PrepareRenderTarget(device, spriteBatch);
}
catch (Exception e)
{
Log.Error("Player head target", e);
}
finally
{
player.direction = direction;
}
}
}
public void Reset()
{
_anyRequested = false;
foreach (PlayerHeadDrawRenderTargetContent head in _heads)
head.Reset();
}
}
+195
View File
@@ -0,0 +1,195 @@
using System;
using Microsoft.Xna.Framework;
using Terraria;
using Terraria.UI;
namespace TerraSharp.UI;
// Read-only view of another player.
public sealed class PlayerInspector() : Window("inspect", "Inspect Player", new Vector2(700, 170), 470, 300)
{
enum Tab { Equipment, Inventory, Buffs }
const int SlotSize = 36, SlotStep = 40, Columns = 10, ContentTop = 40, FrameHeight = 62, LabelHeight = 20, HeaderHeight = 20;
// Empty-slot icons are frames of TextureAssets.Extra[54], as ItemSlot.Draw picks them per context.
const int AccessoryIcon = 11, VanityAccessoryIcon = 2, DyeIcon = 1;
static readonly int[] ArmorIcons = [0, 6, 12], VanityArmorIcons = [3, 9, 15], MiscIcons = [4, 13, 7, 10, 17];
static readonly string[] ArmorLabels = ["Head", "Body", "Legs"], MiscLabels = ["Pet", "Light Pet", "Minecart", "Mount", "Hook"];
Tab _tab;
public int Target { get; private set; } = -1;
public override string Title => Target >= 0 && Main.player[Target] is { active: true } player ? $"Inspecting Player: {player.name}" : base.Title;
public void Inspect(int who)
{
Target = who;
WindowManager.Show(this);
}
public override void DrawBody(Rectangle body)
{
if (Target < 0 || Main.player[Target] is not { active: true } player)
{
Render.TextMid(Widgets.SpriteBatch, "Player left the world.", new Rectangle(body.X + 2, body.Y, body.Width, 26), Widgets.Dim, 0.85f);
return;
}
int tabWidth = (body.Width - 12) / 3;
for (int i = 0; i < 3; i++)
if (Widgets.Button(new Rectangle(body.X + i * (tabWidth + 6), body.Y, tabWidth, 30), ((Tab)i).ToString(), _tab == (Tab)i, 0.82f))
_tab = (Tab)i;
int x = body.X + (body.Width - (Columns * SlotStep - (SlotStep - SlotSize))) / 2, y = body.Y + ContentTop;
int height = _tab switch
{
Tab.Equipment => DrawEquipment(player, body, y),
Tab.Inventory => DrawInventory(player, x, y),
_ => DrawBuffs(player, x, y, body),
};
Height = FrameHeight + ContentTop + height;
}
// Same column order as the vanilla equipment page.
static int DrawEquipment(Player player, Rectangle body, int y)
{
const int ArmorLabelWidth = 48, MiscLabelWidth = 64, BlockGap = 40;
int armorWidth = ArmorLabelWidth + 3 * SlotStep - (SlotStep - SlotSize), miscWidth = MiscLabelWidth + 2 * SlotStep - (SlotStep - SlotSize);
int x = body.X + (body.Width - armorWidth - BlockGap - miscWidth) / 2, miscX = x + armorWidth + BlockGap;
int rowsY = y + HeaderHeight;
ColumnHeaders(x + ArmorLabelWidth, y, "Dye", "Vanity", "Equip");
int rows = 0;
for (int i = 0; i < Columns; i++)
{
// Slots 8 and 9 need the Demon Heart and Master Mode.
if (!player.IsItemSlotUnlockedAndUsable(i))
continue;
int rowY = rowsY + rows * SlotStep;
rows++;
RowLabel(i < 3 ? ArmorLabels[i] : $"Acc {rows - 3}", x, rowY, ArmorLabelWidth);
Slot(player.dye[i], x + ArmorLabelWidth, rowY, "InventoryBack12", DyeIcon);
Slot(player.armor[i + Columns], x + ArmorLabelWidth + SlotStep, rowY, "InventoryBack8", i < 3 ? VanityArmorIcons[i] : VanityAccessoryIcon);
Slot(player.armor[i], x + ArmorLabelWidth + 2 * SlotStep, rowY, "InventoryBack3", i < 3 ? ArmorIcons[i] : AccessoryIcon);
}
ColumnHeaders(miscX + MiscLabelWidth, y, "Dye", "Equip");
for (int i = 0; i < player.miscEquips.Length; i++)
{
int rowY = rowsY + i * SlotStep;
RowLabel(MiscLabels[i], miscX, rowY, MiscLabelWidth);
Slot(player.miscDyes[i], miscX + MiscLabelWidth, rowY, "InventoryBack12", DyeIcon);
Slot(player.miscEquips[i], miscX + MiscLabelWidth + SlotStep, rowY, "InventoryBack3", MiscIcons[i]);
}
const int PortraitWidth = 96, PortraitMinHeight = 110;
int portraitY = rowsY + player.miscEquips.Length * SlotStep + 4;
var portrait = new Rectangle(miscX, portraitY, PortraitWidth, Math.Max(PortraitMinHeight, rowsY + rows * SlotStep - SlotStep + SlotSize - portraitY));
Widgets.Panel(portrait, Widgets.InnerBg);
PlayerPortrait.Draw(player, portrait);
int heldX = miscX + miscWidth - SlotSize;
Render.TextCenteredAt(Widgets.SpriteBatch, "Held", heldX + SlotSize / 2f, portrait.Bottom - SlotSize - HeaderHeight, Widgets.Dim, 0.7f);
Slot(player.HeldItem, heldX, portrait.Bottom - SlotSize, selected: true);
return Math.Max(HeaderHeight + rows * SlotStep, portrait.Bottom - y);
}
static void ColumnHeaders(int x, int y, params string[] titles)
{
for (int i = 0; i < titles.Length; i++)
Render.TextCenteredAt(Widgets.SpriteBatch, titles[i], x + i * SlotStep + SlotSize / 2f, y, Widgets.Dim, 0.7f);
}
static void RowLabel(string text, int x, int y, int width) => Render.TextMid(Widgets.SpriteBatch, text, new Rectangle(x, y, width, SlotSize), Widgets.Header, 0.75f);
// Slots 0-49 are the main inventory, 50-53 coins and 54-57 ammo.
static int DrawInventory(Player player, int x, int y)
{
for (int i = 0; i < 50; i++)
Slot(player.inventory[i], x + i % Columns * SlotStep, y + i / Columns * SlotStep, selected: i == player.selectedItem);
// Vanilla labels the hotbar 1-0 whatever the key bindings are.
for (int i = 0; i < Columns; i++)
Render.Text(Widgets.SpriteBatch, ((i + 1) % 10).ToString(), new Vector2(x + i * SlotStep + 5, y + 2), Color.White, 0.6f);
int labelY = y + 5 * SlotStep + 6, extraY = labelY + LabelHeight;
int groupWidth = 4 * SlotStep - (SlotStep - SlotSize);
Render.TextCenteredAt(Widgets.SpriteBatch, Lang.inter[26].Value, x + groupWidth / 2f, labelY, Widgets.Header, 0.75f);
Render.TextCenteredAt(Widgets.SpriteBatch, Lang.inter[27].Value, x + 6 * SlotStep + groupWidth / 2f, labelY, Widgets.Header, 0.75f);
for (int i = 0; i < 8; i++)
Slot(player.inventory[50 + i], x + (i < 4 ? i : i + 2) * SlotStep, extraY);
return 6 * SlotStep + 6 + LabelHeight;
}
static int DrawBuffs(Player player, int x, int y, Rectangle body)
{
int shown = 0;
for (int i = 0; i < player.buffType.Length; i++)
{
int type = player.buffType[i];
if (type <= 0)
continue;
var bounds = new Rectangle(x + shown % Columns * SlotStep, y + shown / Columns * SlotStep, SlotSize, SlotSize);
bool hot = Widgets.Hot(bounds);
Widgets.Slot(bounds, hot);
Widgets.Icon(GameTextures.Get("Buff", type), new Rectangle(bounds.X + 4, bounds.Y + 4, 28, 28));
// Remote buff times are only known when the game syncs them.
if (player.buffTime[i] > 0 && !Main.buffNoTimeDisplay[type])
Render.TextCentered(Widgets.SpriteBatch, BuffBrowser.FormatTime(player.buffTime[i]), new Rectangle(bounds.X, bounds.Bottom - 12, bounds.Width, 14), Color.White, 0.6f);
if (hot)
Widgets.Tooltip = $"{Lang.GetBuffName(type)}\n{Lang.GetBuffDescription(type)}";
shown++;
}
if (shown == 0)
{
Render.TextMid(Widgets.SpriteBatch, "No active buffs", new Rectangle(body.X + 2, y, body.Width, SlotSize), Widgets.Dim * 0.8f, 0.8f);
return SlotStep;
}
return (shown + Columns - 1) / Columns * SlotStep;
}
static void Slot(Item item, int x, int y, string background = "InventoryBack", int emptyIcon = -1, bool selected = false)
{
var bounds = new Rectangle(x, y, SlotSize, SlotSize);
bool hot = Widgets.Hot(bounds);
Widgets.Slot(bounds, hot, selected, background);
if (item.IsAir)
{
EmptyIcon(bounds, emptyIcon);
return;
}
Render.Item(Widgets.SpriteBatch, item.type, new Rectangle(bounds.X + 5, bounds.Y + 5, bounds.Width - 10, bounds.Height - 10));
if (item.stack > 1)
Render.Text(Widgets.SpriteBatch, item.stack.ToString(), new Vector2(bounds.X + 4, bounds.Bottom - 16), Color.White, 0.6f);
if (!hot)
return;
#if TMODLOADER
ItemSlot.MouseHover(ref item, ItemSlot.Context.InventoryItem);
#else
ItemSlot.MouseHover(item, ItemSlot.Context.InventoryItem);
#endif
}
// Frames are square, so the row count follows from the sheet size; it differs between 1.4.4 and 1.4.5.
static void EmptyIcon(Rectangle bounds, int icon)
{
if (icon < 0 || GameTextures.Get("Extra", 54) is not { } sheet)
return;
int size = sheet.Width / 3;
var frame = new Rectangle(icon % 3 * size, icon / 3 * size, size - 2, size - 2);
if (frame.Bottom > sheet.Height)
return;
Widgets.Icon(sheet, new Rectangle(bounds.X + 4, bounds.Y + 4, bounds.Width - 8, bounds.Height - 8), frame, 1f, Color.White * 0.35f);
}
}
+69
View File
@@ -0,0 +1,69 @@
using Microsoft.Xna.Framework;
using Terraria;
using TerraSharp.Features;
namespace TerraSharp.UI;
public sealed class PlayerList() : Window("players", "Players", new Vector2(640, 150), 600, 440)
{
const int RowHeight = 48, ButtonWidth = 84, ButtonGap = 6;
readonly Scroll _scroll = new();
public override void DrawBody(Rectangle body)
{
int online = 0;
for (int i = 0; i < Main.maxPlayers; i++)
if (Main.player[i].active)
online++;
Render.TextMid(Widgets.SpriteBatch, $"{online} online", new Rectangle(body.X + 2, body.Y, body.Width, 26), Widgets.Dim, 0.82f);
var area = new Rectangle(body.X, body.Y + 32, body.Width - 22, body.Height - 32);
int y = _scroll.Begin(area), top = y;
for (int i = 0; i < Main.maxPlayers; i++)
{
if (!Main.player[i].active)
continue;
Row(Main.player[i], new Rectangle(area.X, y, area.Width, RowHeight - 4));
y += RowHeight;
}
_scroll.End(area, y - top);
}
static void Row(Player player, Rectangle row)
{
Widgets.Panel(row, Widgets.InnerBg);
PlayerHeads.Draw(player, new Vector2(row.X + 24, row.Y + row.Height / 2f), 1f);
int textX = row.X + 50, buttonsX = row.Right - 3 * (ButtonWidth + ButtonGap);
bool local = player.whoAmI == Main.myPlayer;
string name = Render.Ellipsize(local ? player.name + " (you)" : player.name, 0.85f, buttonsX - textX - 8);
Render.Text(Widgets.SpriteBatch, name, new Vector2(textX, row.Y + 4), Render.TeamColor(player), 0.85f);
Render.Text(Widgets.SpriteBatch, Details(player, local), new Vector2(textX, row.Y + 24), player.dead ? Widgets.Red : Widgets.Dim, 0.72f);
var button = new Rectangle(buttonsX, row.Y + (row.Height - 28) / 2, ButtonWidth, 28);
if (!local && !player.dead && Spectate.Available && Widgets.Button(button, "Teleport", false, 0.78f))
PlayerFeatures.TeleportToPlayer(player);
button.X += ButtonWidth + ButtonGap;
if (!local && Spectate.Available && Widgets.Button(button, Spectate.Target == player.whoAmI ? "Stop" : "Spectate", Spectate.Target == player.whoAmI, 0.78f))
Spectate.Toggle(player.whoAmI);
button.X += ButtonWidth + ButtonGap;
bool inspecting = WindowManager.Inspector.Open && WindowManager.Inspector.Target == player.whoAmI;
if (Widgets.Button(button, "Inspect", inspecting, 0.78f))
WindowManager.Inspector.Inspect(player.whoAmI);
}
static string Details(Player player, bool local)
{
if (player.dead)
return "Dead";
string stats = $"Life {player.statLife}/{player.statLifeMax2} Mana {player.statMana}/{player.statManaMax2}";
return local ? stats : $"{stats} {(int)(Vector2.Distance(player.Center, Main.LocalPlayer.Center) / 16f)} tiles";
}
}
+74
View File
@@ -0,0 +1,74 @@
using System;
using System.Reflection;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using TerraSharp.Core;
namespace TerraSharp.UI;
// Draws a player standing still and facing right, without the held item.
public static class PlayerPortrait
{
const float Scale = 1.5f;
static readonly Item Blank = new();
// Public on tModLoader, private on vanilla.
const BindingFlags AnyInstance = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
static readonly FieldInfo? MountFrame = typeof(Mount).GetField("_frame", AnyInstance);
static readonly FieldInfo? MountData = typeof(Mount).GetField("_data", AnyInstance);
static readonly FieldInfo? StandingFrame = MountData?.FieldType.GetField("standingFrameStart", AnyInstance);
// Main.gameMenu makes BoringSetup skip world lighting and Dust.NewDust spawn nothing at the UI position.
public static void Draw(Player player, Rectangle bounds)
{
SpriteBatch spriteBatch = Widgets.SpriteBatch;
bool gameMenu = Main.gameMenu;
(int direction, int immuneAlpha, int wingFrame) = (player.direction, player.immuneAlpha, player.wingFrame);
(Rectangle bodyFrame, Rectangle legFrame, Rectangle headFrame) = (player.bodyFrame, player.legFrame, player.headFrame);
int selected = player.selectedItem, itemAnimation = player.itemAnimation;
(Item held, Item visualized) = (player.inventory[selected], player.lastVisualizedSelectedItem);
(Player.CompositeArmData frontArm, Player.CompositeArmData backArm) = (player.compositeFrontArm, player.compositeBackArm);
bool mounted = player.mount.Active;
object? mountFrame = mounted ? MountFrame?.GetValue(player.mount) : null;
try
{
// Dye shaders apply per draw, so the batch must be immediate.
spriteBatch.End();
spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, DepthStencilState.None, RasterizerState.CullNone, null, Main.UIScaleMatrix);
Main.gameMenu = true;
(player.direction, player.immuneAlpha, player.wingFrame) = (1, 0, 0);
// Mounted legs hold the seated pose; reset only the mount and the attacking arm.
if (mounted && MountData?.GetValue(player.mount) is { } data && StandingFrame?.GetValue(data) is int standing)
MountFrame?.SetValue(player.mount, standing);
if (mounted && itemAnimation > 0)
player.bodyFrame.Y = 0;
else if (!mounted)
player.bodyFrame.Y = player.legFrame.Y = player.headFrame.Y = 0;
// PlayerDrawSet takes the held item from lastVisualizedSelectedItem and the swing pose from the composite arms.
(player.inventory[selected], player.lastVisualizedSelectedItem, player.itemAnimation) = (Blank, Blank, 0);
player.compositeFrontArm.enabled = player.compositeBackArm.enabled = false;
var position = new Vector2(bounds.Center.X - player.width / 2f, bounds.Center.Y - player.height / 2f);
Main.PlayerRenderer.DrawPlayer(Main.Camera, player, position + Main.screenPosition, 0f, Vector2.Zero, 0f, Scale);
}
catch (Exception e)
{
Log.Error("Player portrait", e);
}
finally
{
Main.gameMenu = gameMenu;
(player.direction, player.immuneAlpha, player.wingFrame) = (direction, immuneAlpha, wingFrame);
(player.bodyFrame, player.legFrame, player.headFrame) = (bodyFrame, legFrame, headFrame);
(player.inventory[selected], player.lastVisualizedSelectedItem, player.itemAnimation) = (held, visualized, itemAnimation);
(player.compositeFrontArm, player.compositeBackArm) = (frontArm, backArm);
if (mountFrame is not null)
MountFrame!.SetValue(player.mount, mountFrame);
}
Widgets.Recover(spriteBatch, Main.UIScaleMatrix);
}
}
+2
View File
@@ -142,5 +142,7 @@ public static class Render
spriteBatch.Draw(texture, new Vector2(bounds.X + (bounds.Width - size.X) / 2f, bounds.Y + (bounds.Height - size.Y) / 2f), frame, color ?? Color.White, 0f, Vector2.Zero, scale, SpriteEffects.None, 0f); spriteBatch.Draw(texture, new Vector2(bounds.X + (bounds.Width - size.X) / 2f, bounds.Y + (bounds.Height - size.Y) / 2f), frame, color ?? Color.White, 0f, Vector2.Zero, scale, SpriteEffects.None, 0f);
} }
public static Color TeamColor(Player player) => player.team > 0 ? Main.teamColor[player.team] : Color.White;
public static Vector2 ToScreen(Vector2 world) => Vector2.Transform(world - Main.screenPosition, Main.GameViewMatrix.TransformationMatrix); public static Vector2 ToScreen(Vector2 world) => Vector2.Transform(world - Main.screenPosition, Main.GameViewMatrix.TransformationMatrix);
} }
+2 -2
View File
@@ -261,9 +261,9 @@ public static class Widgets
Render.Fill(SpriteBatch, new Rectangle(bounds.X + 1, bounds.Y + 1, bounds.Width - 2, bounds.Height - 2), color); Render.Fill(SpriteBatch, new Rectangle(bounds.X + 1, bounds.Y + 1, bounds.Width - 2, bounds.Height - 2), color);
} }
public static void Slot(Rectangle bounds, bool hot, bool selected = false) public static void Slot(Rectangle bounds, bool hot, bool selected = false, string background = "InventoryBack")
{ {
if (GameTextures.Get(selected ? "InventoryBack14" : hot ? "InventoryBack15" : "InventoryBack") is { } back) if (GameTextures.Get(selected ? "InventoryBack14" : hot ? "InventoryBack15" : background) is { } back)
SpriteBatch.Draw(back, bounds, SlotTint); SpriteBatch.Draw(back, bounds, SlotTint);
else else
Panel(bounds, ButtonBg); Panel(bounds, ButtonBg);
+4 -1
View File
@@ -6,7 +6,7 @@ public abstract class Window(string id, string title, Vector2 position, int widt
{ {
public string Id { get; } = id; public string Id { get; } = id;
public string Title { get; } = title; public virtual string Title { get; } = title;
public Vector2 Pos { get; set; } = position; public Vector2 Pos { get; set; } = position;
@@ -25,6 +25,9 @@ public abstract class Window(string id, string title, Vector2 position, int widt
// A window that is not interactive lets the mouse through to the game. // A window that is not interactive lets the mouse through to the game.
public virtual bool Interactive => true; public virtual bool Interactive => true;
// Whether a click on a frameless window starts dragging it.
public virtual bool Draggable => true;
public Rectangle Bounds => new((int)Pos.X, (int)Pos.Y, Width, Height); public Rectangle Bounds => new((int)Pos.X, (int)Pos.Y, Width, Height);
public abstract void DrawBody(Rectangle body); public abstract void DrawBody(Rectangle body);
+6 -3
View File
@@ -20,11 +20,14 @@ public static class WindowManager
public static readonly ItemPicker ChestList = new("chestList", "Chest Finder Items", new Vector2(700, 140), Settings.ChestList); public static readonly ItemPicker ChestList = new("chestList", "Chest Finder Items", new Vector2(700, 140), Settings.ChestList);
public static readonly StatsWindow Stats = new(); public static readonly StatsWindow Stats = new();
public static readonly StatsConfig StatsSetup = new(); public static readonly StatsConfig StatsSetup = new();
public static readonly PlayerList Players = new();
public static readonly PlayerInspector Inspector = new();
public static readonly PartyFrames Party = new();
const int TitleHeight = 40; const int TitleHeight = 40;
const int MaxInputLength = 40; const int MaxInputLength = 40;
static readonly List<Window> Stack = [Items, Buffs, Npcs, Editor, SellList, ChestList, Stats, Menu, StatsSetup]; static readonly List<Window> Stack = [Party, Items, Buffs, Npcs, Editor, SellList, ChestList, Stats, Players, Inspector, Menu, StatsSetup];
static Window? _dragging; static Window? _dragging;
static Vector2 _dragOffset; static Vector2 _dragOffset;
@@ -169,7 +172,7 @@ public static class WindowManager
Rectangle bounds = window.Bounds; Rectangle bounds = window.Bounds;
if (window.Frameless) if (window.Frameless)
{ {
bool hot = Widgets.Hot(bounds); bool hot = Widgets.Hot(bounds) && window.Draggable;
if (hot) if (hot)
Widgets.Tooltip = "Drag to move"; Widgets.Tooltip = "Drag to move";
if (hot && Widgets.Click) if (hot && Widgets.Click)
@@ -191,7 +194,7 @@ public static class WindowManager
var title = new Rectangle(bounds.X, bounds.Y, bounds.Width, TitleHeight); var title = new Rectangle(bounds.X, bounds.Y, bounds.Width, TitleHeight);
if (hasTitle) if (hasTitle)
{ {
Render.TextMid(Widgets.SpriteBatch, window.Title, new Rectangle(bounds.X + 16, bounds.Y + 2, bounds.Width, TitleHeight), Color.White, 1.0f); Render.TextMid(Widgets.SpriteBatch, Render.Ellipsize(window.Title, 1.0f, bounds.Width - 64), new Rectangle(bounds.X + 16, bounds.Y + 2, bounds.Width, TitleHeight), Color.White, 1.0f);
Widgets.Separator(bounds.X + 10, bounds.Y + TitleHeight, bounds.Width - 20); Widgets.Separator(bounds.X + 10, bounds.Y + TitleHeight, bounds.Width - 20);
} }