Compare commits
10
Commits
d68de62cf5
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d9de7dd02 | ||
|
|
118eb96429 | ||
|
|
cfe56356c4 | ||
|
|
c5ff9a75fb | ||
|
|
23ac33cb93 | ||
|
|
249787187d | ||
|
|
aae755913d | ||
|
|
9de8112ce9 | ||
|
|
4e95719c2f | ||
|
|
9d748d679b |
No files matched your search
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>0.1.0</Version>
|
||||
<Version>0.2.0</Version>
|
||||
<TargetFrameworks>net48;net8.0</TargetFrameworks>
|
||||
<PlatformTarget Condition="'$(TargetFramework)' == 'net48'">x86</PlatformTarget>
|
||||
<PlatformTarget Condition="'$(TargetFramework)' == 'net8.0'">x64</PlatformTarget>
|
||||
|
||||
@@ -31,6 +31,8 @@ public static class Hotkeys
|
||||
new(InterfaceGroup, "NpcSpawner", "NPC Spawner", () => WindowManager.Toggle(WindowManager.Npcs)),
|
||||
new(InterfaceGroup, "ItemEditor", "Item Editor", () => WindowManager.Toggle(WindowManager.Editor)),
|
||||
new(InterfaceGroup, "Stats", "Stats Overlay", () => WindowManager.Toggle(WindowManager.Stats)),
|
||||
new(InterfaceGroup, "Players", "Player List", () => WindowManager.Toggle(WindowManager.Players)),
|
||||
new(InterfaceGroup, "PartyFrames", "Party Frames", () => WindowManager.Toggle(WindowManager.Party)),
|
||||
|
||||
Toggle("Player", "GodMode", "God Mode", () => Settings.GodMode, v => Settings.GodMode = v),
|
||||
Toggle("Player", "GhostMode", "Ghost Mode", () => Settings.GhostMode, v => Settings.GhostMode = v),
|
||||
@@ -67,6 +69,7 @@ public static class Hotkeys
|
||||
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),
|
||||
Toggle("Items", "AutoDeposit", "Auto Deposit", () => Settings.AutoDeposit, v => Settings.AutoDeposit = v),
|
||||
new("Items", "MaxStack", "Max Stack", ItemFeatures.MaxStack),
|
||||
new("Items", "Duplicate", "Duplicate Item", ItemFeatures.Duplicate),
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ public static partial class Settings
|
||||
[nameof(ShowStats)] = "Player.Stats.Enabled",
|
||||
[nameof(StatLines)] = "Player.Stats.Lines",
|
||||
[nameof(StatIcons)] = "Player.Stats.Icons",
|
||||
[nameof(PartyFrames)] = "Player.PartyFrames.Enabled",
|
||||
[nameof(PartySelf)] = "Player.PartyFrames.Self",
|
||||
|
||||
[nameof(Fly)] = "Movement.Fly.Enabled",
|
||||
[nameof(FlySpeed)] = "Movement.Fly.Speed",
|
||||
@@ -68,6 +70,7 @@ public static partial class Settings
|
||||
[nameof(InfItems)] = "Items.InfiniteItems",
|
||||
[nameof(ItemVacuum)] = "Items.Vacuum",
|
||||
[nameof(AutoSell)] = "Items.AutoSell.Enabled",
|
||||
[nameof(AutoDeposit)] = "Items.AutoDeposit",
|
||||
[nameof(BankButtons)] = "Items.BankButtons",
|
||||
|
||||
[nameof(AutoFish)] = "Fishing.Enabled",
|
||||
|
||||
@@ -47,6 +47,7 @@ public static partial class Settings
|
||||
public static bool AutoHeal, AutoMana, GhostMode, DefenseBoost;
|
||||
public static float AutoHealAt = 40f, AutoManaAt = 25f, BuffMinutes = 10f, DefenseMult = 3f;
|
||||
public static bool ShowStats, StatIcons = true;
|
||||
public static bool PartyFrames, PartySelf;
|
||||
public static StatLine StatLines = StatLine.Dps | StatLine.Damage | StatLine.Kills | StatLine.Minions | StatLine.Sentries | StatLine.Defense;
|
||||
|
||||
public static bool Fly, SuperSpeed, HighJump, NoFallDamage, LavaImmune, WaterWalk, NoKnockback, GravityFlip, ExtendedReach;
|
||||
@@ -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 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 bool AutoFish, AutoRecast = true, FishEnemies, Sonar;
|
||||
|
||||
|
||||
@@ -2,31 +2,53 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Terraria;
|
||||
using Terraria.ID;
|
||||
using TerraSharp.Core;
|
||||
using TerraSharp.Simulation;
|
||||
|
||||
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
|
||||
{
|
||||
const int Horizon = 24;
|
||||
const int ContactHorizon = 12;
|
||||
const int MaxThreats = 48;
|
||||
const float ScanRange = 60f * 16f;
|
||||
const float Margin = 6f;
|
||||
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
|
||||
{
|
||||
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 List<Threat> Threats = [];
|
||||
static readonly Vector2[] Trail = new Vector2[Horizon + 1];
|
||||
static int _lastJump = -JumpCooldown, _lastTeleport = -TeleportCooldown;
|
||||
static int _hitDamage;
|
||||
|
||||
static AutoDodge()
|
||||
{
|
||||
@@ -34,10 +56,34 @@ public static class AutoDodge
|
||||
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)
|
||||
{
|
||||
if (!Settings.AutoDodge || player.dead)
|
||||
if (!Settings.AutoDodge || Settings.GodMode || player.dead)
|
||||
return;
|
||||
|
||||
CollectThreats(player);
|
||||
@@ -45,68 +91,222 @@ public static class AutoDodge
|
||||
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)
|
||||
int bestHit = FirstHit(player, null, Stay, speed, false);
|
||||
if (bestHit > Horizon)
|
||||
return;
|
||||
|
||||
Move best = Moves[0];
|
||||
int bestHit = baseline;
|
||||
int incoming = _hitDamage;
|
||||
Move best = Stay;
|
||||
bool canRise = CanRise(player), canFall = !Grounded(player);
|
||||
foreach (Move move in Moves)
|
||||
{
|
||||
if (move == Moves[0])
|
||||
if ((move.Vertical < 0 && !canRise) || (move.Vertical > 0 && !canFall))
|
||||
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);
|
||||
int hit = FirstHit(player, null, move, speed, true);
|
||||
if (hit > bestHit)
|
||||
(best, bestHit) = (move, hit);
|
||||
}
|
||||
|
||||
if (best == Moves[0])
|
||||
if (bestHit <= Horizon && CanDash(player))
|
||||
foreach (Move move in Dashes)
|
||||
{
|
||||
int hit = FirstHit(player, null, move, speed, true);
|
||||
if (hit > bestHit)
|
||||
(best, bestHit) = (move, hit);
|
||||
}
|
||||
|
||||
if (bestHit <= TeleportWithin && TryTeleport(player, speed, incoming))
|
||||
return;
|
||||
|
||||
Apply(player, best, speed, jump);
|
||||
if (best != Stay)
|
||||
Apply(player, best, speed);
|
||||
}
|
||||
|
||||
static void Apply(Player player, Move move, float speed, float jump)
|
||||
static void Apply(Player player, Move move, float speed)
|
||||
{
|
||||
player.controlLeft = 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)
|
||||
player.velocity.X = move.Horizontal * speed;
|
||||
|
||||
player.controlUp = move.Vertical < 0;
|
||||
player.controlUp = move.Vertical < 0 && Settings.Fly;
|
||||
player.controlDown = move.Vertical > 0;
|
||||
if (move.Vertical != 0)
|
||||
player.velocity.Y = move.Vertical * jump * player.gravDir;
|
||||
if (move.Vertical > 0)
|
||||
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.
|
||||
static int FirstHit(Player player, Vector2 velocity, bool checkWalls)
|
||||
// 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 bool TryTeleport(Player player, float speed, int incoming)
|
||||
{
|
||||
Vector2 size = new(player.width, player.height);
|
||||
if (checkWalls && TileCollision.Sweep(player.Center, player.Center + velocity * WallCheckTicks, player.width, player.height))
|
||||
return -1;
|
||||
if (Clock.Tick - _lastTeleport < TeleportCooldown || FindRod(player) is not { } rod)
|
||||
return false;
|
||||
|
||||
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++)
|
||||
{
|
||||
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));
|
||||
if (move.Vertical < 0)
|
||||
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)
|
||||
{
|
||||
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;
|
||||
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 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)
|
||||
{
|
||||
Threats.Clear();
|
||||
@@ -122,20 +322,21 @@ public static class AutoDodge
|
||||
|
||||
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));
|
||||
// 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);
|
||||
}
|
||||
|
||||
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)
|
||||
if (!Hostility.IsHostile(npc) || npc.damage <= 0 || Vector2.DistanceSquared(npc.Center, player.Center) > rangeSq || KnockedAway(player, npc))
|
||||
continue;
|
||||
|
||||
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.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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Terraria;
|
||||
using Terraria.ID;
|
||||
using TerraSharp.Core;
|
||||
using TerraSharp.UI;
|
||||
|
||||
@@ -81,4 +84,64 @@ public static class ItemFeatures
|
||||
if (sold)
|
||||
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);
|
||||
if (Settings.AutoSell)
|
||||
ItemFeatures.AutoSell(player);
|
||||
if (Settings.AutoDeposit)
|
||||
ItemFeatures.DepositCoins(player);
|
||||
}
|
||||
|
||||
if (Settings.InfFlight)
|
||||
@@ -284,6 +286,13 @@ public static class PlayerFeatures
|
||||
player.fallStart = (int)(player.position.Y / 16f);
|
||||
}
|
||||
|
||||
// Same jump as the Wormhole Potion in Player.HandleSpectatingControls, without the potion.
|
||||
public static void TeleportToPlayer(Player target)
|
||||
{
|
||||
Player player = Main.LocalPlayer;
|
||||
player.UnityTeleport(player.position + target.Bottom - player.Bottom);
|
||||
}
|
||||
|
||||
// Matches the Magic Mirror branch of Player.ItemCheck_Inner.
|
||||
public static void TeleportSpawn()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ using MonoMod.Cil;
|
||||
using Terraria;
|
||||
using Terraria.Graphics.Light;
|
||||
using TerraSharp.Core;
|
||||
#if TMODLOADER
|
||||
using TerraSharp.Features;
|
||||
#endif
|
||||
|
||||
namespace TerraSharp.Patches;
|
||||
|
||||
@@ -14,6 +17,9 @@ internal static class VisionPatches
|
||||
{
|
||||
FullBrightPatch.Install(hooks);
|
||||
ZoomPatch.Install(hooks);
|
||||
#if TMODLOADER
|
||||
SpectateCameraPatch.Install(hooks);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,3 +84,32 @@ internal static class ZoomPatch
|
||||
throw new MissingMethodException(context.Method.FullName, "Clamp");
|
||||
}
|
||||
}
|
||||
|
||||
#if TMODLOADER
|
||||
// Centers the camera the way Player.SpectatingCameraPosition does in 1.4.5.
|
||||
internal static class SpectateCameraPatch
|
||||
{
|
||||
internal static void Install(HookRegistry hooks) => hooks.Add(typeof(Main), "DoDraw_UpdateCameraPosition", (Action<Action>)UpdateCamera);
|
||||
|
||||
static void UpdateCamera(Action original)
|
||||
{
|
||||
original();
|
||||
Guard.Run("Spectate camera", Follow);
|
||||
}
|
||||
|
||||
static void Follow()
|
||||
{
|
||||
if (Spectate.Target < 0)
|
||||
return;
|
||||
|
||||
if (!Plugin.Active || Main.gameMenu || !Spectate.Available || Main.player[Spectate.Target] is not { active: true } target)
|
||||
{
|
||||
Spectate.Stop();
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 center = target.Bottom + new Vector2(0f, target.gfxOffY - 21f);
|
||||
Main.screenPosition = (center - new Vector2(Main.screenWidth, Main.screenHeight) / 2f).Floor();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -11,7 +11,7 @@ using TerraSharp.Features;
|
||||
|
||||
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";
|
||||
|
||||
@@ -45,7 +45,11 @@ public sealed class BuffBrowser() : GridBrowser("buffs", "Buff Browser", new Vec
|
||||
static bool IsMount(int type) => BuffID.Sets.MountType[type] >= 0;
|
||||
#endif
|
||||
|
||||
static string FormatTime(int ticks)
|
||||
#if TMODLOADER
|
||||
protected override string? ModOf(int type) => BuffLoader.GetBuff(type)?.Mod.DisplayNameClean;
|
||||
#endif
|
||||
|
||||
public static string FormatTime(int ticks)
|
||||
{
|
||||
int seconds = ticks / 60;
|
||||
return seconds >= 3600 ? $"{seconds / 3600}h" : seconds >= 60 ? $"{seconds / 60}m" : $"{seconds}s";
|
||||
|
||||
@@ -10,19 +10,50 @@ namespace TerraSharp.UI;
|
||||
// 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 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)
|
||||
{
|
||||
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 Scroll _scroll = new();
|
||||
readonly Scroll _menuScroll = new();
|
||||
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 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;
|
||||
|
||||
public override bool Open
|
||||
{
|
||||
get => base.Open;
|
||||
set
|
||||
{
|
||||
base.Open = value;
|
||||
_menu = Menu.None;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed override void DrawBody(Rectangle body)
|
||||
{
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
// An open menu takes all input until it closes.
|
||||
bool blocked = Widgets.Blocked;
|
||||
if (_menu != Menu.None)
|
||||
Widgets.Blocked = true;
|
||||
DrawContent(body);
|
||||
Widgets.Blocked = blocked;
|
||||
if (_menu != Menu.None)
|
||||
DrawMenu(body);
|
||||
}
|
||||
|
||||
protected abstract void DrawContent(Rectangle body);
|
||||
|
||||
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)
|
||||
{
|
||||
var field = new Rectangle(body.X, top, body.Width - 110, 36);
|
||||
Widgets.Field(_search, field);
|
||||
int gridTop = DrawCategories(body, top + 42);
|
||||
Index.Filter(_search.Text, _subTabs[_category] is { } subTabs ? subTabs[_subcategory].Fits : categories[_category].Fits);
|
||||
LoadSources();
|
||||
Widgets.Field(_search, new Rectangle(body.X, top, body.Width, 36));
|
||||
int toolbarY = top + 42;
|
||||
int gridTop = DrawCategories(body, DrawToolbar(body, toolbarY));
|
||||
Index.Filter(_search.Text, CurrentFits(), _sorts[_sort].Order);
|
||||
if (_search.Text != _lastQuery)
|
||||
{
|
||||
_lastQuery = _search.Text;
|
||||
_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);
|
||||
Widgets.Panel(inner, Widgets.InnerBg);
|
||||
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)
|
||||
{
|
||||
if (DrawTabs(body, categories, ref _category, y, TabHeight, 0.78f))
|
||||
@@ -91,17 +261,15 @@ public abstract class GridBrowser(string id, string title, Vector2 position, int
|
||||
changed = true;
|
||||
_scroll.Reset();
|
||||
}
|
||||
if (!iconTab)
|
||||
continue;
|
||||
if (icons is not null)
|
||||
{
|
||||
// Journey icon frames include a 2px separator on the right.
|
||||
Rectangle frame = GameTextures.Frame(icons, 11, 1, tab.Icon, 0);
|
||||
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);
|
||||
}
|
||||
if (Widgets.Hot(bounds))
|
||||
Widgets.Tooltip = tab.Name;
|
||||
Widgets.Tooltip = $"{tab.Name}: {CountOf(tab)} {noun}";
|
||||
if (!iconTab || icons is null)
|
||||
continue;
|
||||
|
||||
// Journey icon frames include a 2px separator on the right.
|
||||
Rectangle frame = GameTextures.Frame(icons, 11, 1, tab.Icon, 0);
|
||||
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);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,12 @@ public sealed class IdIndex(Func<int> count, Func<int, string?> name)
|
||||
string[]? _names;
|
||||
string? _lastQuery;
|
||||
Func<int, bool>? _lastFits;
|
||||
Comparison<int>? _lastOrder;
|
||||
|
||||
public List<int> Ids { get; } = [];
|
||||
|
||||
public int Version { get; private set; }
|
||||
|
||||
string[] Names
|
||||
{
|
||||
get
|
||||
@@ -27,31 +30,57 @@ 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 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();
|
||||
if (query == _lastQuery && fits == _lastFits)
|
||||
if (query == _lastQuery && fits == _lastFits && order == _lastOrder)
|
||||
return;
|
||||
(_lastQuery, _lastFits) = (query, fits);
|
||||
(_lastQuery, _lastFits, _lastOrder) = (query, fits, order);
|
||||
string[] names = Names;
|
||||
Ids.Clear();
|
||||
if (query.Length == 0)
|
||||
{
|
||||
for (int i = 1; i < names.Length; i++)
|
||||
if (names[i].Length > 0 && fits(i))
|
||||
foreach (int i in All())
|
||||
if (fits(i))
|
||||
Ids.Add(i);
|
||||
return;
|
||||
}
|
||||
if (int.TryParse(query, out int id) && id > 0 && id < names.Length && names[id].Length > 0 && fits(id))
|
||||
Ids.Add(id);
|
||||
foreach (bool prefixPass in (bool[])[true, false])
|
||||
for (int i = 1; i < names.Length; i++)
|
||||
{
|
||||
int at = names[i].IndexOf(query, StringComparison.OrdinalIgnoreCase);
|
||||
if ((prefixPass ? at == 0 : at > 0) && i != id && fits(i))
|
||||
Ids.Add(i);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (int.TryParse(query, out int id) && id > 0 && id < names.Length && names[id].Length > 0 && fits(id))
|
||||
Ids.Add(id);
|
||||
foreach (bool prefixPass in (bool[])[true, false])
|
||||
for (int i = 1; i < names.Length; i++)
|
||||
{
|
||||
int at = names[i].IndexOf(query, StringComparison.OrdinalIgnoreCase);
|
||||
if ((prefixPass ? at == 0 : at > 0) && i != id && fits(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));
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ public static class InterfaceLayers
|
||||
return;
|
||||
|
||||
layers.RemoveAll(l => l.Name.StartsWith(Prefix, StringComparison.Ordinal));
|
||||
PlayerHeads.Install();
|
||||
|
||||
layers.Insert(0, new LegacyGameInterfaceLayer(Prefix + "Under UI", DrawUnderUi, InterfaceScaleType.None));
|
||||
|
||||
@@ -45,7 +46,11 @@ public static class InterfaceLayers
|
||||
layers.Insert(mouseTextIndex < 0 ? layers.Count : mouseTextIndex, new LegacyGameInterfaceLayer(Prefix + "Windows", DrawWindows, InterfaceScaleType.UI));
|
||||
}
|
||||
|
||||
public static void Uninstall() => Layers?.RemoveAll(l => l.Name.StartsWith(Prefix, StringComparison.Ordinal));
|
||||
public static void Uninstall()
|
||||
{
|
||||
Layers?.RemoveAll(l => l.Name.StartsWith(Prefix, StringComparison.Ordinal));
|
||||
PlayerHeads.Uninstall();
|
||||
}
|
||||
|
||||
static bool DrawUnderUi()
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using Terraria;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Terraria;
|
||||
using Terraria.GameContent.UI;
|
||||
using Terraria.ID;
|
||||
#if TMODLOADER
|
||||
using Terraria.ModLoader;
|
||||
@@ -19,6 +20,8 @@ public sealed class ItemEditor() : Window("itemEditor", "Item Editor", new Vecto
|
||||
readonly Scroll _scroll = new();
|
||||
readonly Item _reference = new();
|
||||
(int Type, int Prefix) _referenceKey = (-1, -1);
|
||||
(int Prefix, int Value, int Rarity)[] _prefixOptions = [];
|
||||
int _prefixType = -1;
|
||||
int _x, _y, _width;
|
||||
bool _firstGroup;
|
||||
|
||||
@@ -141,21 +144,43 @@ public sealed class ItemEditor() : Window("itemEditor", "Item Editor", new Vecto
|
||||
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)
|
||||
{
|
||||
int[] prefixes = [.. Prefixes(item).OrderBy(PrefixName, StringComparer.OrdinalIgnoreCase)];
|
||||
if (prefixes.Length == 0)
|
||||
(int Prefix, int Value, int Rarity)[] options = PrefixOptions(item);
|
||||
if (options.Length <= 1)
|
||||
return;
|
||||
|
||||
Group("Prefix");
|
||||
const int Columns = 3, Gap = 6;
|
||||
const float Scale = 0.76f;
|
||||
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]);
|
||||
int prefix = options[i].Prefix;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ using Terraria.ModLoader;
|
||||
|
||||
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
|
||||
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();
|
||||
|
||||
// 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)];
|
||||
// Journey duplication filters paired with Infinite_Icons frame indices; a part without a test holds the rest.
|
||||
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() =>
|
||||
[
|
||||
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)),
|
||||
.. 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),
|
||||
];
|
||||
|
||||
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;
|
||||
|
||||
// Whips must be classified as summon weapons before checking melee.
|
||||
|
||||
@@ -4,7 +4,7 @@ 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)])
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -52,6 +52,12 @@ public sealed partial class MainWindow
|
||||
Group("Stats");
|
||||
if (Toggle("Stats Overlay", ref Settings.ShowStats) && Buttons("Configure Overlay") == 0)
|
||||
WindowManager.StatsSetup.ShowBeside(Bounds);
|
||||
|
||||
Group("Party");
|
||||
if (Toggle("Party Frames", ref Settings.PartyFrames))
|
||||
Sub(() => Toggle("Include yourself", ref Settings.PartySelf));
|
||||
if (Buttons(WindowManager.Players.Open ? "Close Player List" : "Player List") == 0)
|
||||
WindowManager.Toggle(WindowManager.Players);
|
||||
}
|
||||
|
||||
void MovementPage()
|
||||
@@ -138,6 +144,7 @@ public sealed partial class MainWindow
|
||||
if (Buttons(WindowManager.SellList.Open ? "Close Sell List" : "Edit Sell List") == 0)
|
||||
WindowManager.Toggle(WindowManager.SellList);
|
||||
});
|
||||
Toggle("Auto Deposit", ref Settings.AutoDeposit);
|
||||
Toggle("Bank Buttons", ref Settings.BankButtons);
|
||||
Toggle("Infinite Items", ref Settings.InfItems);
|
||||
Toggle("Instant Break", ref Settings.InstantBreak);
|
||||
|
||||
@@ -14,6 +14,7 @@ public sealed partial class MainWindow
|
||||
["Auto Mana"] = "Uses Quick Mana below the set mana level.",
|
||||
["Ghost Mode"] = "Enemies stop chasing you. Bosses and some flying enemies still attack.",
|
||||
["Defense Multiplier"] = "Multiplies your total defense.",
|
||||
["Party Frames"] = "Shows your teammates' health and mana on screen. Open the menu to move it.",
|
||||
["Stats Overlay"] = "Shows the selected stats on screen. Damage, kills and deaths count from when you enter the world.",
|
||||
["Configure Overlay"] = "Choose the lines the overlay shows.",
|
||||
["Fly"] = "Move through blocks with your movement keys. Up or Jump rises; Down descends.",
|
||||
@@ -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.",
|
||||
["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 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.",
|
||||
["Show target"] = "Shows the cursor radius, target brackets and aim point.",
|
||||
["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.",
|
||||
["Infinite Items"] = "Keeps inventory stacks from shrinking.",
|
||||
["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.",
|
||||
["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.",
|
||||
|
||||
@@ -30,10 +30,10 @@ public sealed partial class MainWindow : Window
|
||||
Open = true;
|
||||
_pages =
|
||||
[
|
||||
new Page("Player", PlayerPage, () => Count(Settings.GodMode, Settings.InfHealth, Settings.InfMana, Settings.InfBreath, Settings.InfFlight, Settings.InstantRespawn, Settings.AutoHeal, Settings.AutoMana, Settings.GhostMode, Settings.DefenseBoost, Settings.ShowStats)),
|
||||
new Page("Player", PlayerPage, () => Count(Settings.GodMode, Settings.InfHealth, Settings.InfMana, Settings.InfBreath, Settings.InfFlight, Settings.InstantRespawn, Settings.AutoHeal, Settings.AutoMana, Settings.GhostMode, Settings.DefenseBoost, Settings.ShowStats, Settings.PartyFrames)),
|
||||
new Page("Movement", MovementPage, () => Count(Settings.Fly, Settings.SuperSpeed, Settings.HighJump, Settings.GravityFlip, Settings.NoFallDamage, Settings.LavaImmune, Settings.WaterWalk, Settings.NoKnockback, Settings.ExtendedReach)),
|
||||
new Page("Combat", CombatPage, () => Count(Settings.RapidAttack, Settings.AutoSwing, Settings.DamageBoost, Settings.NoManaCost, Settings.NoSpread, Settings.Aimbot, Settings.AutoAttack, Settings.KillAura, Settings.HitMagnet, Settings.FreezeEnemies, Settings.WeakenEnemies, Settings.MagicBullets, Settings.AutoDodge)),
|
||||
new Page("Items", ItemsPage, () => Count(Settings.InstantBreak, Settings.InfItems, Settings.ItemVacuum, Settings.AutoSell, Settings.AutoFish, Settings.Sonar)),
|
||||
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("World", WorldPage, () => Count(Settings.FreezeTime, Settings.SpawnRateBoost, Settings.MapTeleport)),
|
||||
new Page("Keys", KeysPage, () => 0),
|
||||
|
||||
@@ -9,7 +9,7 @@ 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)
|
||||
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;
|
||||
|
||||
@@ -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 BrowserSort[] Sorts = [new("Life", (a, b) => (Sample(b)?.lifeMax ?? 0).CompareTo(Sample(a)?.lifeMax ?? 0))];
|
||||
|
||||
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 bool IsBoss(int type) => Sample(type) is { boss: true };
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -142,5 +142,7 @@ public static class Render
|
||||
spriteBatch.Draw(texture, new Vector2(bounds.X + (bounds.Width - size.X) / 2f, bounds.Y + (bounds.Height - size.Y) / 2f), frame, color ?? Color.White, 0f, Vector2.Zero, scale, SpriteEffects.None, 0f);
|
||||
}
|
||||
|
||||
public static Color TeamColor(Player player) => player.team > 0 ? Main.teamColor[player.team] : Color.White;
|
||||
|
||||
public static Vector2 ToScreen(Vector2 world) => Vector2.Transform(world - Main.screenPosition, Main.GameViewMatrix.TransformationMatrix);
|
||||
}
|
||||
@@ -261,9 +261,9 @@ public static class Widgets
|
||||
Render.Fill(SpriteBatch, new Rectangle(bounds.X + 1, bounds.Y + 1, bounds.Width - 2, bounds.Height - 2), color);
|
||||
}
|
||||
|
||||
public static void Slot(Rectangle bounds, bool hot, bool selected = false)
|
||||
public static void Slot(Rectangle bounds, bool hot, bool selected = false, string background = "InventoryBack")
|
||||
{
|
||||
if (GameTextures.Get(selected ? "InventoryBack14" : hot ? "InventoryBack15" : "InventoryBack") is { } back)
|
||||
if (GameTextures.Get(selected ? "InventoryBack14" : hot ? "InventoryBack15" : background) is { } back)
|
||||
SpriteBatch.Draw(back, bounds, SlotTint);
|
||||
else
|
||||
Panel(bounds, ButtonBg);
|
||||
|
||||
@@ -6,7 +6,7 @@ public abstract class Window(string id, string title, Vector2 position, int widt
|
||||
{
|
||||
public string Id { get; } = id;
|
||||
|
||||
public string Title { get; } = title;
|
||||
public virtual string Title { get; } = title;
|
||||
|
||||
public Vector2 Pos { get; set; } = position;
|
||||
|
||||
@@ -25,6 +25,9 @@ public abstract class Window(string id, string title, Vector2 position, int widt
|
||||
// A window that is not interactive lets the mouse through to the game.
|
||||
public virtual bool Interactive => true;
|
||||
|
||||
// Whether a click on a frameless window starts dragging it.
|
||||
public virtual bool Draggable => true;
|
||||
|
||||
public Rectangle Bounds => new((int)Pos.X, (int)Pos.Y, Width, Height);
|
||||
|
||||
public abstract void DrawBody(Rectangle body);
|
||||
|
||||
@@ -20,11 +20,14 @@ public static class WindowManager
|
||||
public static readonly ItemPicker ChestList = new("chestList", "Chest Finder Items", new Vector2(700, 140), Settings.ChestList);
|
||||
public static readonly StatsWindow Stats = new();
|
||||
public static readonly StatsConfig StatsSetup = new();
|
||||
public static readonly PlayerList Players = new();
|
||||
public static readonly PlayerInspector Inspector = new();
|
||||
public static readonly PartyFrames Party = new();
|
||||
|
||||
const int TitleHeight = 40;
|
||||
const int MaxInputLength = 40;
|
||||
|
||||
static readonly List<Window> Stack = [Items, Buffs, Npcs, Editor, SellList, ChestList, Stats, Menu, StatsSetup];
|
||||
static readonly List<Window> Stack = [Party, Items, Buffs, Npcs, Editor, SellList, ChestList, Stats, Players, Inspector, Menu, StatsSetup];
|
||||
static Window? _dragging;
|
||||
static Vector2 _dragOffset;
|
||||
|
||||
@@ -169,7 +172,7 @@ public static class WindowManager
|
||||
Rectangle bounds = window.Bounds;
|
||||
if (window.Frameless)
|
||||
{
|
||||
bool hot = Widgets.Hot(bounds);
|
||||
bool hot = Widgets.Hot(bounds) && window.Draggable;
|
||||
if (hot)
|
||||
Widgets.Tooltip = "Drag to move";
|
||||
if (hot && Widgets.Click)
|
||||
@@ -191,7 +194,7 @@ public static class WindowManager
|
||||
var title = new Rectangle(bounds.X, bounds.Y, bounds.Width, TitleHeight);
|
||||
if (hasTitle)
|
||||
{
|
||||
Render.TextMid(Widgets.SpriteBatch, window.Title, new Rectangle(bounds.X + 16, bounds.Y + 2, bounds.Width, TitleHeight), Color.White, 1.0f);
|
||||
Render.TextMid(Widgets.SpriteBatch, Render.Ellipsize(window.Title, 1.0f, bounds.Width - 64), new Rectangle(bounds.X + 16, bounds.Y + 2, bounds.Width, TitleHeight), Color.White, 1.0f);
|
||||
Widgets.Separator(bounds.X + 10, bounds.Y + TitleHeight, bounds.Width - 20);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user