Compare commits
29
Commits
v0.1.0
...
0d9de7dd02
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d9de7dd02 | ||
|
|
118eb96429 | ||
|
|
cfe56356c4 | ||
|
|
c5ff9a75fb | ||
|
|
23ac33cb93 | ||
|
|
249787187d | ||
|
|
aae755913d | ||
|
|
9de8112ce9 | ||
|
|
4e95719c2f | ||
|
|
9d748d679b | ||
|
|
d68de62cf5 | ||
|
|
6701dd0305 | ||
|
|
afb8235c08 | ||
|
|
f84ad4faeb | ||
|
|
7b768fde2c | ||
|
|
8d6296676a | ||
|
|
73998f1aaa | ||
|
|
9df9f0a322 | ||
|
|
2692887684 | ||
|
|
a9081d28d9 | ||
|
|
73a90f47f1 | ||
|
|
7d5115ded4 | ||
|
|
6b3dcccc7c | ||
|
|
3195e7ba92 | ||
|
|
746ad799a0 | ||
|
|
bcf4e71558 | ||
|
|
be429a9448 | ||
|
|
993b91a46b | ||
|
|
55150f3c47 |
No files matched your search
@@ -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>
|
||||||
|
|||||||
@@ -28,8 +28,15 @@ public static class Hotkeys
|
|||||||
new(InterfaceGroup, "Menu", "Open / close menu", () => WindowManager.Toggle(WindowManager.Menu)),
|
new(InterfaceGroup, "Menu", "Open / close menu", () => WindowManager.Toggle(WindowManager.Menu)),
|
||||||
new(InterfaceGroup, "ItemBrowser", "Item Browser", () => WindowManager.Toggle(WindowManager.Items)),
|
new(InterfaceGroup, "ItemBrowser", "Item Browser", () => WindowManager.Toggle(WindowManager.Items)),
|
||||||
new(InterfaceGroup, "BuffBrowser", "Buff Browser", () => WindowManager.Toggle(WindowManager.Buffs)),
|
new(InterfaceGroup, "BuffBrowser", "Buff Browser", () => WindowManager.Toggle(WindowManager.Buffs)),
|
||||||
|
new(InterfaceGroup, "NpcSpawner", "NPC Spawner", () => WindowManager.Toggle(WindowManager.Npcs)),
|
||||||
|
new(InterfaceGroup, "ItemEditor", "Item Editor", () => WindowManager.Toggle(WindowManager.Editor)),
|
||||||
|
new(InterfaceGroup, "Stats", "Stats Overlay", () => WindowManager.Toggle(WindowManager.Stats)),
|
||||||
|
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", "DefenseBoost", "Defense Multiplier", () => Settings.DefenseBoost, v => Settings.DefenseBoost = v),
|
||||||
Toggle("Player", "AutoHeal", "Auto Heal", () => Settings.AutoHeal, v => Settings.AutoHeal = v),
|
Toggle("Player", "AutoHeal", "Auto Heal", () => Settings.AutoHeal, v => Settings.AutoHeal = v),
|
||||||
Toggle("Player", "AutoMana", "Auto Mana", () => Settings.AutoMana, v => Settings.AutoMana = v),
|
Toggle("Player", "AutoMana", "Auto Mana", () => Settings.AutoMana, v => Settings.AutoMana = v),
|
||||||
new("Player", "FullHeal", "Full Heal", PlayerFeatures.FullHeal),
|
new("Player", "FullHeal", "Full Heal", PlayerFeatures.FullHeal),
|
||||||
@@ -52,6 +59,8 @@ public static class Hotkeys
|
|||||||
Toggle("Combat", "FreezeEnemies", "Freeze Enemies", () => Settings.FreezeEnemies, v => Settings.FreezeEnemies = v),
|
Toggle("Combat", "FreezeEnemies", "Freeze Enemies", () => Settings.FreezeEnemies, v => Settings.FreezeEnemies = v),
|
||||||
Toggle("Combat", "WeakenEnemies", "Weaken Enemies", () => Settings.WeakenEnemies, v => Settings.WeakenEnemies = v),
|
Toggle("Combat", "WeakenEnemies", "Weaken Enemies", () => Settings.WeakenEnemies, v => Settings.WeakenEnemies = v),
|
||||||
Toggle("Combat", "RapidAttack", "Rapid Attack", () => Settings.RapidAttack, v => Settings.RapidAttack = v),
|
Toggle("Combat", "RapidAttack", "Rapid Attack", () => Settings.RapidAttack, v => Settings.RapidAttack = v),
|
||||||
|
Toggle("Combat", "MagicBullets", "Magic Bullets", () => Settings.MagicBullets, v => Settings.MagicBullets = v),
|
||||||
|
Toggle("Combat", "AutoDodge", "Auto Dodge", () => Settings.AutoDodge, v => Settings.AutoDodge = v),
|
||||||
new("Combat", "GatherAll", "Gather All Enemies", CombatFeatures.GatherAll),
|
new("Combat", "GatherAll", "Gather All Enemies", CombatFeatures.GatherAll),
|
||||||
new("Combat", "KillAll", "Kill All Enemies", CombatFeatures.KillAll),
|
new("Combat", "KillAll", "Kill All Enemies", CombatFeatures.KillAll),
|
||||||
|
|
||||||
@@ -59,6 +68,10 @@ public static class Hotkeys
|
|||||||
Toggle("Items", "ItemVacuum", "Item Vacuum", () => Settings.ItemVacuum, v => Settings.ItemVacuum = v),
|
Toggle("Items", "ItemVacuum", "Item Vacuum", () => Settings.ItemVacuum, v => Settings.ItemVacuum = v),
|
||||||
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", "AutoDeposit", "Auto Deposit", () => Settings.AutoDeposit, v => Settings.AutoDeposit = v),
|
||||||
|
new("Items", "MaxStack", "Max Stack", ItemFeatures.MaxStack),
|
||||||
|
new("Items", "Duplicate", "Duplicate Item", ItemFeatures.Duplicate),
|
||||||
|
|
||||||
Toggle("Visuals", "Esp", "ESP", () => Settings.Esp, v => Settings.Esp = v),
|
Toggle("Visuals", "Esp", "ESP", () => Settings.Esp, v => Settings.Esp = v),
|
||||||
Toggle("Visuals", "Spelunker", "Spelunker Vision", () => Settings.Spelunker, v => Settings.Spelunker = v),
|
Toggle("Visuals", "Spelunker", "Spelunker Vision", () => Settings.Spelunker, v => Settings.Spelunker = v),
|
||||||
@@ -67,9 +80,15 @@ public static class Hotkeys
|
|||||||
Toggle("Visuals", "BiomeSight", "Biome Sight", () => Settings.BiomeSight, v => Settings.BiomeSight = v),
|
Toggle("Visuals", "BiomeSight", "Biome Sight", () => Settings.BiomeSight, v => Settings.BiomeSight = v),
|
||||||
Toggle("Visuals", "NightOwl", "Night Owl", () => Settings.NightOwl, v => Settings.NightOwl = v),
|
Toggle("Visuals", "NightOwl", "Night Owl", () => Settings.NightOwl, v => Settings.NightOwl = v),
|
||||||
Toggle("Visuals", "Shine", "Shine", () => Settings.Shine, v => Settings.Shine = v),
|
Toggle("Visuals", "Shine", "Shine", () => Settings.Shine, v => Settings.Shine = v),
|
||||||
|
Toggle("Visuals", "FullBright", "Full Bright", () => Settings.FullBright, v => Settings.FullBright = v),
|
||||||
|
Toggle("Visuals", "ExtendedZoom", "Extended Zoom", () => Settings.ExtendedZoom, v => Settings.ExtendedZoom = v),
|
||||||
|
Toggle("Visuals", "ChestFinder", "Chest Finder", () => Settings.EspChests, v => Settings.EspChests = v),
|
||||||
|
|
||||||
Toggle("World", "FreezeTime", "Freeze Time", () => Settings.FreezeTime, v => Settings.FreezeTime = v),
|
Toggle("World", "FreezeTime", "Freeze Time", () => Settings.FreezeTime, v => Settings.FreezeTime = v),
|
||||||
|
Toggle("World", "SpawnRate", "Spawn Rate", () => Settings.SpawnRateBoost, v => Settings.SpawnRateBoost = v),
|
||||||
new("World", "SavePosition", "Save Current Position", Waypoints.AddHere),
|
new("World", "SavePosition", "Save Current Position", Waypoints.AddHere),
|
||||||
|
Toggle("World", "MapTeleport", "Map Teleport", () => Settings.MapTeleport, v => Settings.MapTeleport = v),
|
||||||
|
new("World", "RevealMap", "Reveal Map", MapFeatures.StartReveal),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Windows virtual-key codes for mouse buttons; XNA's Keys leaves them unassigned.
|
// Windows virtual-key codes for mouse buttons; XNA's Keys leaves them unassigned.
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ public static partial class Settings
|
|||||||
[nameof(AutoMana)] = "Player.AutoMana.Enabled",
|
[nameof(AutoMana)] = "Player.AutoMana.Enabled",
|
||||||
[nameof(AutoManaAt)] = "Player.AutoMana.Threshold",
|
[nameof(AutoManaAt)] = "Player.AutoMana.Threshold",
|
||||||
[nameof(BuffMinutes)] = "Player.Buffs.Minutes",
|
[nameof(BuffMinutes)] = "Player.Buffs.Minutes",
|
||||||
|
[nameof(GhostMode)] = "Player.GhostMode",
|
||||||
|
[nameof(DefenseBoost)] = "Player.DefenseBoost.Enabled",
|
||||||
|
[nameof(DefenseMult)] = "Player.DefenseBoost.Multiplier",
|
||||||
|
[nameof(ShowStats)] = "Player.Stats.Enabled",
|
||||||
|
[nameof(StatLines)] = "Player.Stats.Lines",
|
||||||
|
[nameof(StatIcons)] = "Player.Stats.Icons",
|
||||||
|
[nameof(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",
|
||||||
@@ -55,10 +63,15 @@ public static partial class Settings
|
|||||||
[nameof(ShotPreview)] = "Combat.Aim.ShotPreview",
|
[nameof(ShotPreview)] = "Combat.Aim.ShotPreview",
|
||||||
[nameof(AimFov)] = "Combat.Aim.CursorRadius",
|
[nameof(AimFov)] = "Combat.Aim.CursorRadius",
|
||||||
[nameof(AimRange)] = "Combat.Aim.Range",
|
[nameof(AimRange)] = "Combat.Aim.Range",
|
||||||
|
[nameof(MagicBullets)] = "Combat.MagicBullets",
|
||||||
|
[nameof(AutoDodge)] = "Combat.AutoDodge",
|
||||||
|
|
||||||
[nameof(InstantBreak)] = "Items.InstantBreak",
|
[nameof(InstantBreak)] = "Items.InstantBreak",
|
||||||
[nameof(InfItems)] = "Items.InfiniteItems",
|
[nameof(InfItems)] = "Items.InfiniteItems",
|
||||||
[nameof(ItemVacuum)] = "Items.Vacuum",
|
[nameof(ItemVacuum)] = "Items.Vacuum",
|
||||||
|
[nameof(AutoSell)] = "Items.AutoSell.Enabled",
|
||||||
|
[nameof(AutoDeposit)] = "Items.AutoDeposit",
|
||||||
|
[nameof(BankButtons)] = "Items.BankButtons",
|
||||||
|
|
||||||
[nameof(AutoFish)] = "Fishing.Enabled",
|
[nameof(AutoFish)] = "Fishing.Enabled",
|
||||||
[nameof(AutoRecast)] = "Fishing.Recast",
|
[nameof(AutoRecast)] = "Fishing.Recast",
|
||||||
@@ -87,6 +100,7 @@ public static partial class Settings
|
|||||||
[nameof(ColTown)] = "Esp.TownNpcs.Color",
|
[nameof(ColTown)] = "Esp.TownNpcs.Color",
|
||||||
[nameof(EspBossRainbow)] = "Esp.Bosses.Rainbow",
|
[nameof(EspBossRainbow)] = "Esp.Bosses.Rainbow",
|
||||||
[nameof(ColBoss)] = "Esp.Bosses.Color",
|
[nameof(ColBoss)] = "Esp.Bosses.Color",
|
||||||
|
[nameof(EspBossCursor)] = "Esp.Bosses.Cursor",
|
||||||
[nameof(EspItems)] = "Esp.Items.Enabled",
|
[nameof(EspItems)] = "Esp.Items.Enabled",
|
||||||
[nameof(ColItems)] = "Esp.Items.Color",
|
[nameof(ColItems)] = "Esp.Items.Color",
|
||||||
[nameof(EspProjHostile)] = "Esp.HostileProjectiles.Enabled",
|
[nameof(EspProjHostile)] = "Esp.HostileProjectiles.Enabled",
|
||||||
@@ -95,6 +109,18 @@ public static partial class Settings
|
|||||||
[nameof(ColProjFriendly)] = "Esp.FriendlyProjectiles.Color",
|
[nameof(ColProjFriendly)] = "Esp.FriendlyProjectiles.Color",
|
||||||
[nameof(EspWaypoints)] = "Esp.Waypoints.Enabled",
|
[nameof(EspWaypoints)] = "Esp.Waypoints.Enabled",
|
||||||
[nameof(ColWaypoint)] = "Esp.Waypoints.Color",
|
[nameof(ColWaypoint)] = "Esp.Waypoints.Color",
|
||||||
|
[nameof(EspChests)] = "Esp.Chests.Enabled",
|
||||||
|
[nameof(ColChests)] = "Esp.Chests.Color",
|
||||||
|
[nameof(EspChestsListed)] = "Esp.Chests.OnlyListed",
|
||||||
|
|
||||||
|
[nameof(DamageMelee)] = "DamageNumbers.Melee.Enabled",
|
||||||
|
[nameof(ColDamageMelee)] = "DamageNumbers.Melee.Color",
|
||||||
|
[nameof(DamageRanged)] = "DamageNumbers.Ranged.Enabled",
|
||||||
|
[nameof(ColDamageRanged)] = "DamageNumbers.Ranged.Color",
|
||||||
|
[nameof(DamageMagic)] = "DamageNumbers.Magic.Enabled",
|
||||||
|
[nameof(ColDamageMagic)] = "DamageNumbers.Magic.Color",
|
||||||
|
[nameof(DamageSummon)] = "DamageNumbers.Summon.Enabled",
|
||||||
|
[nameof(ColDamageSummon)] = "DamageNumbers.Summon.Color",
|
||||||
|
|
||||||
[nameof(Spelunker)] = "Vision.Spelunker",
|
[nameof(Spelunker)] = "Vision.Spelunker",
|
||||||
[nameof(Hunter)] = "Vision.Hunter",
|
[nameof(Hunter)] = "Vision.Hunter",
|
||||||
@@ -102,12 +128,19 @@ public static partial class Settings
|
|||||||
[nameof(NightOwl)] = "Vision.NightOwl",
|
[nameof(NightOwl)] = "Vision.NightOwl",
|
||||||
[nameof(DangerSense)] = "Vision.DangerSense",
|
[nameof(DangerSense)] = "Vision.DangerSense",
|
||||||
[nameof(BiomeSight)] = "Vision.BiomeSight",
|
[nameof(BiomeSight)] = "Vision.BiomeSight",
|
||||||
|
[nameof(FullBright)] = "Vision.FullBright",
|
||||||
|
[nameof(ExtendedZoom)] = "Vision.ExtendedZoom",
|
||||||
|
|
||||||
[nameof(FreezeTime)] = "World.FreezeTime",
|
[nameof(FreezeTime)] = "World.FreezeTime",
|
||||||
|
[nameof(SpawnRateBoost)] = "World.SpawnRate.Enabled",
|
||||||
|
[nameof(SpawnRateMult)] = "World.SpawnRate.Multiplier",
|
||||||
|
[nameof(MapTeleport)] = "World.MapTeleport",
|
||||||
|
|
||||||
[nameof(LogBallistics)] = "Ballistics.Log",
|
[nameof(LogBallistics)] = "Ballistics.Log",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static readonly string[] ListPaths = ["Items.AutoSell.Items", "Esp.Chests.Items"];
|
||||||
|
|
||||||
static void SetValue(Dictionary<string, object> document, string path, object value)
|
static void SetValue(Dictionary<string, object> document, string path, object value)
|
||||||
{
|
{
|
||||||
string[] parts = path.Split('.');
|
string[] parts = path.Split('.');
|
||||||
|
|||||||
@@ -19,12 +19,36 @@ public enum EspBoxStyle
|
|||||||
Filled,
|
Filled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum StatLine
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Dps = 1 << 0,
|
||||||
|
PeakDps = 1 << 1,
|
||||||
|
Damage = 1 << 2,
|
||||||
|
BestHit = 1 << 3,
|
||||||
|
Kills = 1 << 4,
|
||||||
|
Time = 1 << 5,
|
||||||
|
Deaths = 1 << 6,
|
||||||
|
Minions = 1 << 7,
|
||||||
|
Sentries = 1 << 8,
|
||||||
|
Defense = 1 << 9,
|
||||||
|
DamageReduction = 1 << 10,
|
||||||
|
Crit = 1 << 11,
|
||||||
|
LifeRegen = 1 << 12,
|
||||||
|
Speed = 1 << 13,
|
||||||
|
Luck = 1 << 14,
|
||||||
|
}
|
||||||
|
|
||||||
// Public mutable fields are persisted through Settings.Json.cs.
|
// Public mutable fields are persisted through Settings.Json.cs.
|
||||||
public static partial class Settings
|
public static partial class Settings
|
||||||
{
|
{
|
||||||
public static bool GodMode, InfHealth, InfMana, InfBreath, InfFlight, InstantRespawn;
|
public static bool GodMode, InfHealth, InfMana, InfBreath, InfFlight, InstantRespawn;
|
||||||
public static bool AutoHeal, AutoMana;
|
public static bool AutoHeal, AutoMana, GhostMode, DefenseBoost;
|
||||||
public static float AutoHealAt = 40f, AutoManaAt = 25f, BuffMinutes = 10f;
|
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;
|
public static bool Fly, SuperSpeed, HighJump, NoFallDamage, LavaImmune, WaterWalk, NoKnockback, GravityFlip, ExtendedReach;
|
||||||
public static float FlySpeed = 12f, SpeedMult = 3f;
|
public static float FlySpeed = 12f, SpeedMult = 3f;
|
||||||
@@ -35,20 +59,25 @@ public static partial class Settings
|
|||||||
public static bool KillAura, HitMagnet, FreezeEnemies, WeakenEnemies;
|
public static bool KillAura, HitMagnet, FreezeEnemies, WeakenEnemies;
|
||||||
public static float KillAuraRange = 20f, HitMagnetRange = 30f;
|
public static float KillAuraRange = 20f, HitMagnetRange = 30f;
|
||||||
public static int KillAuraInterval = 10;
|
public static int KillAuraInterval = 10;
|
||||||
public static bool Aimbot, AimPrediction = true, AimDrawTarget = true, AutoAttack, AimExact = true, ShotPreview;
|
public static bool Aimbot, AimPrediction = true, AimDrawTarget = true, AutoAttack, AimExact = true, ShotPreview, MagicBullets, AutoDodge;
|
||||||
public static float AimFov = 250f, AimRange = 60f;
|
public static float AimFov = 250f, AimRange = 60f;
|
||||||
|
|
||||||
public static bool InstantBreak, InfItems, ItemVacuum;
|
public static bool InstantBreak, InfItems, ItemVacuum, AutoSell, AutoDeposit, BankButtons = true;
|
||||||
|
public static readonly HashSet<int> SellList = [];
|
||||||
public static bool AutoFish, AutoRecast = true, FishEnemies, Sonar;
|
public static bool AutoFish, AutoRecast = true, FishEnemies, Sonar;
|
||||||
|
|
||||||
public static bool Esp, EspNames = true, EspHealth = true, EspHealthText, EspDistance, EspLines, EspProjPath = true;
|
public static bool Esp, EspNames = true, EspHealth = true, EspHealthText, EspDistance, EspLines, EspProjPath = true;
|
||||||
public static bool EspLocal, EspPlayers = true, EspHostile = true, EspCritters, EspTown, EspBossRainbow = true, EspItems, EspProjHostile, EspProjFriendly, EspWaypoints = true;
|
public static bool EspLocal, EspPlayers = true, EspHostile = true, EspCritters, EspTown, EspBossRainbow = true, EspBossCursor = true, EspItems, EspProjHostile, EspProjFriendly, EspWaypoints = true, EspChests, EspChestsListed;
|
||||||
|
public static readonly HashSet<int> ChestList = [];
|
||||||
public static EspBoxStyle EspStyle;
|
public static EspBoxStyle EspStyle;
|
||||||
public static float EspThickness = 1f, EspMaxDistance; // tiles, 0 = unlimited
|
public static float EspThickness = 1f, EspMaxDistance; // tiles, 0 = unlimited
|
||||||
public static Color ColLocal = Color.White, ColPlayers = new(70, 200, 255), ColHostile = new(235, 60, 60), ColCritters = new(200, 170, 120), ColTown = new(80, 220, 110), ColBoss = new(255, 120, 40), ColItems = new(255, 210, 70), ColProjHostile = new(255, 80, 200), ColProjFriendly = new(120, 160, 255), ColWaypoint = new(220, 90, 255);
|
public static Color ColLocal = Color.White, ColPlayers = new(70, 200, 255), ColHostile = new(235, 60, 60), ColCritters = new(200, 170, 120), ColTown = new(80, 220, 110), ColBoss = new(255, 120, 40), ColItems = new(255, 210, 70), ColProjHostile = new(255, 80, 200), ColProjFriendly = new(120, 160, 255), ColWaypoint = new(220, 90, 255), ColChests = new(240, 170, 60);
|
||||||
public static bool Spelunker, Hunter, Shine, NightOwl, DangerSense, BiomeSight;
|
public static bool DamageMelee, DamageRanged, DamageMagic, DamageSummon;
|
||||||
|
public static Color ColDamageMelee = new(255, 110, 60), ColDamageRanged = new(120, 230, 90), ColDamageMagic = new(90, 150, 255), ColDamageSummon = new(200, 110, 255);
|
||||||
|
public static bool Spelunker, Hunter, Shine, NightOwl, DangerSense, BiomeSight, FullBright, ExtendedZoom;
|
||||||
|
|
||||||
public static bool FreezeTime;
|
public static bool FreezeTime, SpawnRateBoost, MapTeleport = true;
|
||||||
|
public static float SpawnRateMult = 5f;
|
||||||
|
|
||||||
public static bool LogBallistics = true;
|
public static bool LogBallistics = true;
|
||||||
|
|
||||||
@@ -92,6 +121,9 @@ public static partial class Settings
|
|||||||
binds[bind.Key] = bind.Value.ToString();
|
binds[bind.Key] = bind.Value.ToString();
|
||||||
document["Binds"] = binds;
|
document["Binds"] = binds;
|
||||||
|
|
||||||
|
SetValue(document, ListPaths[0], SortedList(SellList));
|
||||||
|
SetValue(document, ListPaths[1], SortedList(ChestList));
|
||||||
|
|
||||||
var windows = new Dictionary<string, int[]>();
|
var windows = new Dictionary<string, int[]>();
|
||||||
foreach (Window window in WindowManager.All)
|
foreach (Window window in WindowManager.All)
|
||||||
windows[window.Id] = [(int)window.Pos.X, (int)window.Pos.Y];
|
windows[window.Id] = [(int)window.Pos.X, (int)window.Pos.Y];
|
||||||
@@ -121,6 +153,9 @@ public static partial class Settings
|
|||||||
if (TryGetValue(document, setting.Value, out object? value))
|
if (TryGetValue(document, setting.Value, out object? value))
|
||||||
TryLoad(setting.Value, () => Apply(setting.Key, Convert.ToString(value, CultureInfo.InvariantCulture) ?? ""));
|
TryLoad(setting.Value, () => Apply(setting.Key, Convert.ToString(value, CultureInfo.InvariantCulture) ?? ""));
|
||||||
|
|
||||||
|
LoadList(document, ListPaths[0], SellList);
|
||||||
|
LoadList(document, ListPaths[1], ChestList);
|
||||||
|
|
||||||
foreach (KeyValuePair<string, object> bind in Section(document, "Binds"))
|
foreach (KeyValuePair<string, object> bind in Section(document, "Binds"))
|
||||||
TryLoad("Binds." + bind.Key, () => Binds[bind.Key] = (Keys)Enum.Parse(typeof(Keys), (string)bind.Value));
|
TryLoad("Binds." + bind.Key, () => Binds[bind.Key] = (Keys)Enum.Parse(typeof(Keys), (string)bind.Value));
|
||||||
|
|
||||||
@@ -149,6 +184,20 @@ public static partial class Settings
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int[] SortedList(HashSet<int> list)
|
||||||
|
{
|
||||||
|
int[] ids = [.. list];
|
||||||
|
Array.Sort(ids);
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void LoadList(Dictionary<string, object> document, string path, HashSet<int> list)
|
||||||
|
{
|
||||||
|
list.Clear();
|
||||||
|
if (TryGetValue(document, path, out object? value))
|
||||||
|
TryLoad(path, () => list.UnionWith(Json.ConvertTo<int[]>(value!)));
|
||||||
|
}
|
||||||
|
|
||||||
static void TryLoad(string key, Action load)
|
static void TryLoad(string key, Action load)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -208,6 +257,8 @@ public static partial class Settings
|
|||||||
{
|
{
|
||||||
foreach (KeyValuePair<string, object> pair in Defaults)
|
foreach (KeyValuePair<string, object> pair in Defaults)
|
||||||
typeof(Settings).GetField(pair.Key)!.SetValue(null, pair.Value);
|
typeof(Settings).GetField(pair.Key)!.SetValue(null, pair.Value);
|
||||||
|
SellList.Clear();
|
||||||
|
ChestList.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string Hex(Color color) => $"{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
|
public static string Hex(Color color) => $"{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
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, 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, Damage;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly record struct Move(int Horizontal, int Vertical, bool Dash = false);
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < Pool.Length; i++)
|
||||||
|
Pool[i] = new Threat();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 || Settings.GodMode || player.dead)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CollectThreats(player);
|
||||||
|
if (Threats.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float speed = Settings.Fly ? Settings.FlySpeed : Math.Max(Math.Max(player.maxRunSpeed, player.accRunSpeed), 3f);
|
||||||
|
int bestHit = FirstHit(player, null, Stay, speed, false);
|
||||||
|
if (bestHit > Horizon)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int incoming = _hitDamage;
|
||||||
|
Move best = Stay;
|
||||||
|
bool canRise = CanRise(player), canFall = !Grounded(player);
|
||||||
|
foreach (Move move in Moves)
|
||||||
|
{
|
||||||
|
if ((move.Vertical < 0 && !canRise) || (move.Vertical > 0 && !canFall))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int hit = FirstHit(player, null, move, speed, true);
|
||||||
|
if (hit > bestHit)
|
||||||
|
(best, bestHit) = (move, hit);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (best != Stay)
|
||||||
|
Apply(player, best, speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 && Settings.Fly;
|
||||||
|
player.controlDown = move.Vertical > 0;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
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++)
|
||||||
|
{
|
||||||
|
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)))
|
||||||
|
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();
|
||||||
|
float rangeSq = ScanRange * ScanRange;
|
||||||
|
for (int i = 0; i < Main.maxProjectiles && Threats.Count < MaxThreats; i++)
|
||||||
|
{
|
||||||
|
Projectile projectile = Main.projectile[i];
|
||||||
|
if (!projectile.active || !projectile.hostile || projectile.damage <= 0 || Vector2.DistanceSquared(projectile.Center, player.Center) > rangeSq)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!Ballistics.Current(i, out Vector2 velocity, out int age))
|
||||||
|
(velocity, age) = (projectile.velocity * (1 + projectile.extraUpdates), 0);
|
||||||
|
|
||||||
|
Threat threat = Pool[Threats.Count];
|
||||||
|
threat.Count = Ballistics.Path(Ballistics.Get(projectile.type), projectile.Center, velocity, age, Horizon, threat.Path);
|
||||||
|
// 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 || KnockedAway(player, npc))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Threat threat = Pool[Threats.Count];
|
||||||
|
for (int t = 0; t <= ContactHorizon; t++)
|
||||||
|
threat.Path[t] = npc.Center + NpcMotion.Offset(npc, t);
|
||||||
|
(threat.Count, threat.Width, threat.Height, threat.Damage) = (ContactHorizon + 1, npc.width, npc.height, Taken(player, npc.damage));
|
||||||
|
Threats.Add(threat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Terraria;
|
||||||
|
using Terraria.ID;
|
||||||
|
using TerraSharp.Core;
|
||||||
|
using TerraSharp.Simulation;
|
||||||
|
|
||||||
|
namespace TerraSharp.Features.Combat;
|
||||||
|
|
||||||
|
public static class MagicBullets
|
||||||
|
{
|
||||||
|
const float MinSpeed = 8f;
|
||||||
|
const int GrappleStyle = 7, YoyoStyle = 99, FlailStyle = 15;
|
||||||
|
|
||||||
|
// Projectile type per slot while steered, 0 otherwise.
|
||||||
|
static readonly int[] Steered = new int[Main.maxProjectiles];
|
||||||
|
|
||||||
|
public static bool IsSteered(int slot) => Steered[slot] != 0 && Steered[slot] == Main.projectile[slot].type;
|
||||||
|
|
||||||
|
static bool Eligible(Projectile projectile) => projectile is { friendly: true, hostile: false, minion: false, sentry: false, bobber: false, npcProj: false, damage: > 0 } && projectile.aiStyle is not (GrappleStyle or YoyoStyle or FlailStyle) && !Main.projPet[projectile.type] && !ProjectileID.Sets.IsAWhip[projectile.type] && !Sandbox.Held(projectile);
|
||||||
|
|
||||||
|
// Projectile.AI postfix for the local player's projectiles.
|
||||||
|
public static void PostAI(Projectile projectile)
|
||||||
|
{
|
||||||
|
int slot = projectile.whoAmI;
|
||||||
|
NPC? target = Eligible(projectile) ? TargetFor(projectile) : null;
|
||||||
|
if (target is null)
|
||||||
|
{
|
||||||
|
if (IsSteered(slot) && ContentSamples.ProjectilesByType.TryGetValue(projectile.type, out Projectile? sample))
|
||||||
|
projectile.tileCollide = sample.tileCollide;
|
||||||
|
Steered[slot] = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 offset = target.Center - projectile.Center;
|
||||||
|
if (offset.LengthSquared() < 1f)
|
||||||
|
return;
|
||||||
|
|
||||||
|
projectile.velocity = Vector2.Normalize(offset) * Math.Max(projectile.velocity.Length(), MinSpeed);
|
||||||
|
projectile.tileCollide = false;
|
||||||
|
Steered[slot] = projectile.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NPC? TargetFor(Projectile projectile)
|
||||||
|
{
|
||||||
|
if (Aimbot.Target is { } locked && Hostility.IsDamageable(locked))
|
||||||
|
return locked;
|
||||||
|
|
||||||
|
Player player = Main.player[projectile.owner];
|
||||||
|
float range = Settings.AimRange * 16f, rangeSq = range * range;
|
||||||
|
NPC? best = null;
|
||||||
|
float bestDistance = float.MaxValue;
|
||||||
|
for (int i = 0; i < Main.maxNPCs; i++)
|
||||||
|
{
|
||||||
|
NPC npc = Main.npc[i];
|
||||||
|
if (!Hostility.IsDamageable(npc) || Vector2.DistanceSquared(npc.Center, player.Center) > rangeSq)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
float distance = Vector2.DistanceSquared(npc.Center, projectile.Center);
|
||||||
|
if (distance < bestDistance)
|
||||||
|
{
|
||||||
|
bestDistance = distance;
|
||||||
|
best = npc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Terraria;
|
||||||
|
using Terraria.ID;
|
||||||
|
using TerraSharp.Core;
|
||||||
|
#if TMODLOADER
|
||||||
|
using Terraria.ModLoader;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace TerraSharp.Features;
|
||||||
|
|
||||||
|
public enum DamageKind { None, Melee, Ranged, Magic, Summon }
|
||||||
|
|
||||||
|
// Hit paths set Current around StrikeNPC; CombatText.NewText reads it.
|
||||||
|
public static class DamageNumbers
|
||||||
|
{
|
||||||
|
public static DamageKind Current;
|
||||||
|
|
||||||
|
public static Color? Recolor(Color color)
|
||||||
|
{
|
||||||
|
bool crit = color == CombatText.DamagedHostileCrit;
|
||||||
|
if (!crit && color != CombatText.DamagedHostile)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
(bool enabled, Color chosen) = Current switch
|
||||||
|
{
|
||||||
|
DamageKind.Melee => (Settings.DamageMelee, Settings.ColDamageMelee),
|
||||||
|
DamageKind.Ranged => (Settings.DamageRanged, Settings.ColDamageRanged),
|
||||||
|
DamageKind.Magic => (Settings.DamageMagic, Settings.ColDamageMagic),
|
||||||
|
DamageKind.Summon => (Settings.DamageSummon, Settings.ColDamageSummon),
|
||||||
|
_ => (false, default),
|
||||||
|
};
|
||||||
|
if (!enabled)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var opaque = new Color(chosen.R, chosen.G, chosen.B);
|
||||||
|
return crit ? Color.Lerp(opaque, Color.White, 0.4f) : opaque;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if TMODLOADER
|
||||||
|
// Whips count as melee too, so summon is checked first.
|
||||||
|
public static DamageKind Of(DamageClass? damageClass)
|
||||||
|
{
|
||||||
|
if (damageClass is null)
|
||||||
|
return DamageKind.None;
|
||||||
|
if (damageClass.CountsAsClass(DamageClass.Summon))
|
||||||
|
return DamageKind.Summon;
|
||||||
|
if (damageClass.CountsAsClass(DamageClass.Magic))
|
||||||
|
return DamageKind.Magic;
|
||||||
|
if (damageClass.CountsAsClass(DamageClass.Ranged))
|
||||||
|
return DamageKind.Ranged;
|
||||||
|
return damageClass.CountsAsClass(DamageClass.Melee) ? DamageKind.Melee : DamageKind.None;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
public static DamageKind Of(Projectile projectile)
|
||||||
|
{
|
||||||
|
if (projectile.minion || projectile.sentry || ProjectileID.Sets.IsAWhip[projectile.type] || ProjectileID.Sets.MinionShot[projectile.type] || ProjectileID.Sets.SentryShot[projectile.type])
|
||||||
|
return DamageKind.Summon;
|
||||||
|
if (projectile.magic)
|
||||||
|
return DamageKind.Magic;
|
||||||
|
if (projectile.ranged)
|
||||||
|
return DamageKind.Ranged;
|
||||||
|
return projectile.melee ? DamageKind.Melee : DamageKind.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DamageKind Of(Item item)
|
||||||
|
{
|
||||||
|
if (item.summon || item.sentry)
|
||||||
|
return DamageKind.Summon;
|
||||||
|
if (item.magic)
|
||||||
|
return DamageKind.Magic;
|
||||||
|
if (item.ranged)
|
||||||
|
return DamageKind.Ranged;
|
||||||
|
return item.melee ? DamageKind.Melee : DamageKind.None;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
|
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.UI;
|
||||||
|
|
||||||
namespace TerraSharp.Features;
|
namespace TerraSharp.Features;
|
||||||
|
|
||||||
@@ -24,4 +29,119 @@ public static class ItemFeatures
|
|||||||
drop.velocity = Vector2.Zero;
|
drop.velocity = Vector2.Zero;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const int InventorySlots = 50;
|
||||||
|
const int CoinSound = 18;
|
||||||
|
|
||||||
|
// The cursor item, or the held item when the cursor is empty.
|
||||||
|
public static Item EditTarget => Main.mouseItem.type != 0 ? Main.mouseItem : Main.LocalPlayer.inventory[Main.LocalPlayer.selectedItem];
|
||||||
|
|
||||||
|
public static void MaxStack()
|
||||||
|
{
|
||||||
|
Item item = EditTarget;
|
||||||
|
if (item.type == 0 || item.stack >= item.maxStack)
|
||||||
|
return;
|
||||||
|
|
||||||
|
item.stack = item.maxStack;
|
||||||
|
Widgets.Sound(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Duplicate()
|
||||||
|
{
|
||||||
|
Item item = EditTarget;
|
||||||
|
Item[] inventory = Main.LocalPlayer.inventory;
|
||||||
|
if (item.type == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (int i = 0; i < InventorySlots; i++)
|
||||||
|
{
|
||||||
|
if (inventory[i].type != 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
inventory[i] = item.Clone();
|
||||||
|
inventory[i].favorited = false;
|
||||||
|
Widgets.Sound(7);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Items worth nothing are deleted; items worth something stay when no slot can take the coins.
|
||||||
|
public static void AutoSell(Player player)
|
||||||
|
{
|
||||||
|
bool sold = false;
|
||||||
|
for (int i = 0; i < InventorySlots; i++)
|
||||||
|
{
|
||||||
|
Item item = player.inventory[i];
|
||||||
|
if (item.type == 0 || item.favorited || !Settings.SellList.Contains(item.type) || (i == player.selectedItem && player.itemAnimation > 0))
|
||||||
|
continue;
|
||||||
|
if (!player.SellItem(item) && item.value > 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
item.TurnToAir();
|
||||||
|
sold = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sold)
|
||||||
|
Widgets.Sound(CoinSound);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
using Terraria;
|
||||||
|
using TerraSharp.Core;
|
||||||
|
using TerraSharp.UI;
|
||||||
|
|
||||||
|
namespace TerraSharp.Features;
|
||||||
|
|
||||||
|
public static class MapFeatures
|
||||||
|
{
|
||||||
|
const int ColumnsPerTick = 24;
|
||||||
|
const float WorldEdge = 42f;
|
||||||
|
|
||||||
|
// MapHelper.GetBackgroundType samples map tiles up to 36 away; WorldMap.UnlockMapSection keeps the same margin.
|
||||||
|
const int Margin = 40;
|
||||||
|
|
||||||
|
static int _revealColumn = -1;
|
||||||
|
static bool _rightWasDown;
|
||||||
|
|
||||||
|
public static bool Revealing => _revealColumn >= 0;
|
||||||
|
|
||||||
|
public static float RevealProgress => Revealing ? (_revealColumn - Margin) / (float)(Main.maxTilesX - 2 * Margin) : 0f;
|
||||||
|
|
||||||
|
public static void StartReveal() => _revealColumn = Margin;
|
||||||
|
|
||||||
|
// Main.DoUpdate postfix.
|
||||||
|
public static void Update()
|
||||||
|
{
|
||||||
|
if (Main.gameMenu)
|
||||||
|
{
|
||||||
|
_revealColumn = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RevealStep();
|
||||||
|
if (Settings.MapTeleport)
|
||||||
|
TeleportOnRightClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A large world is about 20 million map tiles, so the reveal is spread over ticks.
|
||||||
|
static void RevealStep()
|
||||||
|
{
|
||||||
|
if (_revealColumn < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int start = _revealColumn, end = Math.Min(Main.maxTilesX - Margin, start + ColumnsPerTick);
|
||||||
|
_revealColumn = end < Main.maxTilesX - Margin ? end : -1;
|
||||||
|
for (int x = start; x < end; x++)
|
||||||
|
for (int y = Margin; y < Main.maxTilesY - Margin; y++)
|
||||||
|
Main.Map.Update(x, y, byte.MaxValue);
|
||||||
|
|
||||||
|
if (_revealColumn < 0)
|
||||||
|
Main.refreshMap = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main.DrawMap puts tile mapFullscreenPos at the screen center, scaled by mapFullscreenScale.
|
||||||
|
static void TeleportOnRightClick()
|
||||||
|
{
|
||||||
|
MouseState mouse = Mouse.GetState();
|
||||||
|
bool down = mouse.RightButton == ButtonState.Pressed;
|
||||||
|
bool clicked = down && !_rightWasDown;
|
||||||
|
_rightWasDown = down;
|
||||||
|
if (!clicked || !Main.mapFullscreen || !Main.instance.IsActive || WindowManager.MouseOver)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Vector2 tile = Main.mapFullscreenPos + (new Vector2(mouse.X, mouse.Y) - new Vector2(Main.screenWidth, Main.screenHeight) / 2f) / Main.mapFullscreenScale;
|
||||||
|
Vector2 feet = new Vector2(MathHelper.Clamp(tile.X, WorldEdge, Main.maxTilesX - WorldEdge), MathHelper.Clamp(tile.Y, WorldEdge, Main.maxTilesY - WorldEdge)) * 16f;
|
||||||
|
Player player = Main.LocalPlayer;
|
||||||
|
PlayerFeatures.TeleportTo(feet - new Vector2(player.width / 2f, player.height));
|
||||||
|
Main.mapFullscreen = false;
|
||||||
|
Widgets.Sound(2, -1, -1, 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
using Terraria;
|
||||||
|
using Terraria.DataStructures;
|
||||||
|
using Terraria.ID;
|
||||||
|
using TerraSharp.Core;
|
||||||
|
|
||||||
|
namespace TerraSharp.Features;
|
||||||
|
|
||||||
|
public static class NpcSpawner
|
||||||
|
{
|
||||||
|
const float SpawnDistance = 10 * 16f;
|
||||||
|
const int MultiplayerClient = 1;
|
||||||
|
|
||||||
|
#if TMODLOADER
|
||||||
|
static readonly EntitySource_DebugCommand Source = new(nameof(NpcSpawner));
|
||||||
|
#else
|
||||||
|
static readonly EntitySource_DebugCommand Source = new();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public static bool Multiplayer => Main.netMode == MultiplayerClient;
|
||||||
|
|
||||||
|
// The server only honors summon requests for these types.
|
||||||
|
public static bool CanSummon(int type) => type >= 0 && type < NPCID.Sets.MPAllowedEnemies.Length && NPCID.Sets.MPAllowedEnemies[type];
|
||||||
|
|
||||||
|
public static bool Available(int type) => !Multiplayer || CanSummon(type);
|
||||||
|
|
||||||
|
public static void Spawn(int type, int count)
|
||||||
|
{
|
||||||
|
if (Multiplayer)
|
||||||
|
{
|
||||||
|
if (CanSummon(type))
|
||||||
|
NetMessage.SendData(MessageID.SpawnBossUseLicenseStartEvent, -1, -1, null, Main.myPlayer, type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player player = Main.LocalPlayer;
|
||||||
|
int x = (int)(player.Center.X + player.direction * SpawnDistance);
|
||||||
|
int y = (int)player.Bottom.Y;
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
if (NPC.NewNPC(Source, x, y, type) >= Main.maxNPCs)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawner.GetSpawnRate postfix; spawnRate is the 1-in-N chance per tick.
|
||||||
|
public static void ScaleSpawnRate(ref int spawnRate, ref int maxSpawns)
|
||||||
|
{
|
||||||
|
if (!Settings.SpawnRateBoost)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Settings.SpawnRateMult <= 0f)
|
||||||
|
{
|
||||||
|
maxSpawns = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnRate = Math.Max(1, (int)(spawnRate / Settings.SpawnRateMult));
|
||||||
|
maxSpawns = (int)Math.Ceiling(maxSpawns * Settings.SpawnRateMult);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Terraria;
|
using Terraria;
|
||||||
using TerraSharp.Core;
|
using TerraSharp.Core;
|
||||||
|
using TerraSharp.Features.Combat;
|
||||||
using TerraSharp.UI;
|
using TerraSharp.UI;
|
||||||
|
|
||||||
namespace TerraSharp.Features;
|
namespace TerraSharp.Features;
|
||||||
@@ -92,7 +93,26 @@ public static class PlayerFeatures
|
|||||||
Player.tileRangeY = Settings.ReachY;
|
Player.tileRangeY = Settings.ReachY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Settings.DefenseBoost)
|
||||||
|
#if TMODLOADER
|
||||||
|
player.statDefense.FinalMultiplier *= Settings.DefenseMult;
|
||||||
|
#else
|
||||||
|
player.statDefense = (int)(player.statDefense * Settings.DefenseMult);
|
||||||
|
#endif
|
||||||
|
if (Settings.GhostMode)
|
||||||
|
ApplyGhost(player);
|
||||||
|
|
||||||
ApplyVision(player);
|
ApplyVision(player);
|
||||||
|
AutoDodge.Steer(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetEffects clears both every tick; NPC.TargetClosest and SetTargetTrackingValues read them.
|
||||||
|
static void ApplyGhost(Player player)
|
||||||
|
{
|
||||||
|
player.aggro = -10000;
|
||||||
|
bool[] passive = player.npcTypeNoAggro;
|
||||||
|
for (int i = 0; i < passive.Length; i++)
|
||||||
|
passive[i] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ApplyVision(Player player)
|
static void ApplyVision(Player player)
|
||||||
@@ -125,6 +145,10 @@ public static class PlayerFeatures
|
|||||||
if (Settings.InfBreath)
|
if (Settings.InfBreath)
|
||||||
player.breath = player.breathMax;
|
player.breath = player.breathMax;
|
||||||
AutoPotions(player);
|
AutoPotions(player);
|
||||||
|
if (Settings.AutoSell)
|
||||||
|
ItemFeatures.AutoSell(player);
|
||||||
|
if (Settings.AutoDeposit)
|
||||||
|
ItemFeatures.DepositCoins(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Settings.InfFlight)
|
if (Settings.InfFlight)
|
||||||
@@ -262,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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using System;
|
||||||
|
using Terraria;
|
||||||
|
|
||||||
|
namespace TerraSharp.Features;
|
||||||
|
|
||||||
|
// Damage is the life the local player removed from NPCs, read from per-tick life changes.
|
||||||
|
public static class Stats
|
||||||
|
{
|
||||||
|
const int DpsTicks = 180;
|
||||||
|
|
||||||
|
static readonly int[] LastLife = new int[Main.maxNPCs], LastType = new int[Main.maxNPCs];
|
||||||
|
static readonly int[] Recent = new int[DpsTicks];
|
||||||
|
static int _recentIndex, _recentSum;
|
||||||
|
static int _world = -1;
|
||||||
|
static bool _wasDead;
|
||||||
|
|
||||||
|
public static long Damage { get; private set; }
|
||||||
|
|
||||||
|
public static int BestHit { get; private set; }
|
||||||
|
|
||||||
|
public static int Kills { get; private set; }
|
||||||
|
|
||||||
|
public static int Deaths { get; private set; }
|
||||||
|
|
||||||
|
public static int Ticks { get; private set; }
|
||||||
|
|
||||||
|
public static float Dps => _recentSum * 60f / DpsTicks;
|
||||||
|
|
||||||
|
public static float PeakDps { get; private set; }
|
||||||
|
|
||||||
|
public static void Reset()
|
||||||
|
{
|
||||||
|
Array.Clear(Recent, 0, Recent.Length);
|
||||||
|
(_recentIndex, _recentSum) = (0, 0);
|
||||||
|
(Damage, BestHit, Kills, Deaths, Ticks, PeakDps) = (0, 0, 0, 0, 0, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player.Update postfix for the local player.
|
||||||
|
public static void Update(Player player)
|
||||||
|
{
|
||||||
|
if (Main.worldID != _world)
|
||||||
|
{
|
||||||
|
_world = Main.worldID;
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ticks++;
|
||||||
|
if (player.dead && !_wasDead)
|
||||||
|
Deaths++;
|
||||||
|
_wasDead = player.dead;
|
||||||
|
|
||||||
|
int dealt = 0;
|
||||||
|
for (int i = 0; i < Main.maxNPCs; i++)
|
||||||
|
{
|
||||||
|
NPC npc = Main.npc[i];
|
||||||
|
int life = npc.active ? Math.Max(0, npc.life) : 0;
|
||||||
|
bool killed = !npc.active && npc.life <= 0;
|
||||||
|
// Worm segments share one life pool owned by the head.
|
||||||
|
bool counts = LastType[i] != 0 && npc.type == LastType[i] && npc.lastInteraction == Main.myPlayer && (npc.realLife < 0 || npc.realLife == i) && (npc.active || killed);
|
||||||
|
if (counts && life < LastLife[i])
|
||||||
|
{
|
||||||
|
int hit = LastLife[i] - life;
|
||||||
|
dealt += hit;
|
||||||
|
BestHit = Math.Max(BestHit, hit);
|
||||||
|
if (life == 0 && !npc.friendly && !npc.townNPC && npc.lifeMax > 5)
|
||||||
|
Kills++;
|
||||||
|
}
|
||||||
|
|
||||||
|
(LastType[i], LastLife[i]) = npc.active ? (npc.type, life) : (0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Damage += dealt;
|
||||||
|
_recentSum += dealt - Recent[_recentIndex];
|
||||||
|
Recent[_recentIndex] = dealt;
|
||||||
|
_recentIndex = (_recentIndex + 1) % DpsTicks;
|
||||||
|
PeakDps = Math.Max(PeakDps, Dps);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Terraria;
|
||||||
|
using TerraSharp.Core;
|
||||||
|
using TerraSharp.Features;
|
||||||
|
#if !TMODLOADER
|
||||||
|
using Terraria.GameContent;
|
||||||
|
using Terraria.ID;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace TerraSharp.Patches;
|
||||||
|
|
||||||
|
internal static class DamageNumberPatches
|
||||||
|
{
|
||||||
|
delegate int NewTextOriginal(Rectangle location, Color color, string text, bool dramatic, bool dot);
|
||||||
|
|
||||||
|
internal static void Install(HookRegistry hooks)
|
||||||
|
{
|
||||||
|
hooks.Add(HookRegistry.Method(typeof(CombatText), nameof(CombatText.NewText), [typeof(Rectangle), typeof(Color), typeof(string), typeof(bool), typeof(bool)]), (Func<NewTextOriginal, Rectangle, Color, string, bool, bool, int>)NewText);
|
||||||
|
#if TMODLOADER
|
||||||
|
hooks.Add(HookRegistry.Method(typeof(NPC), nameof(NPC.StrikeNPC), [typeof(NPC.HitInfo), typeof(bool), typeof(bool)]), (Func<StrikeOriginal, NPC, NPC.HitInfo, bool, bool, int>)Strike);
|
||||||
|
#else
|
||||||
|
hooks.Add(typeof(Projectile), nameof(Projectile.Damage), (Action<ProjectileDamageOriginal, Projectile>)ProjectileDamage);
|
||||||
|
hooks.Add(typeof(Player), "ProcessHitAgainstNPC", (Action<MeleeHitOriginal, Player, Item, Rectangle, int, float, int>)MeleeHit);
|
||||||
|
hooks.Add(HookRegistry.Method(typeof(Player), nameof(Player.ApplyDamageToNPC), [typeof(NPC), typeof(int), typeof(float), typeof(int), typeof(bool), typeof(PlayerNPCHitSource)]), (Action<ApplyDamageOriginal, Player, NPC, int, float, int, bool, PlayerNPCHitSource>)ApplyDamage);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static int NewText(NewTextOriginal original, Rectangle location, Color color, string text, bool dramatic, bool dot)
|
||||||
|
{
|
||||||
|
if (Plugin.Active && DamageNumbers.Current != DamageKind.None && DamageNumbers.Recolor(color) is { } recolored)
|
||||||
|
color = recolored;
|
||||||
|
return original(location, color, text, dramatic, dot);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if TMODLOADER
|
||||||
|
delegate int StrikeOriginal(NPC self, NPC.HitInfo hit, bool fromNet, bool noPlayerInteraction);
|
||||||
|
|
||||||
|
static int Strike(StrikeOriginal original, NPC self, NPC.HitInfo hit, bool fromNet, bool noPlayerInteraction)
|
||||||
|
{
|
||||||
|
DamageKind previous = DamageNumbers.Current;
|
||||||
|
DamageNumbers.Current = DamageNumbers.Of(hit.DamageType);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return original(self, hit, fromNet, noPlayerInteraction);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DamageNumbers.Current = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
delegate void ProjectileDamageOriginal(Projectile self);
|
||||||
|
delegate void MeleeHitOriginal(Player self, Item item, Rectangle itemRectangle, int originalDamage, float knockBack, int npcIndex);
|
||||||
|
delegate void ApplyDamageOriginal(Player self, NPC npc, int damage, float knockback, int direction, bool crit, PlayerNPCHitSource source);
|
||||||
|
|
||||||
|
static void ProjectileDamage(ProjectileDamageOriginal original, Projectile self)
|
||||||
|
{
|
||||||
|
DamageKind previous = DamageNumbers.Current;
|
||||||
|
DamageNumbers.Current = DamageNumbers.Of(self);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
original(self);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DamageNumbers.Current = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void MeleeHit(MeleeHitOriginal original, Player self, Item item, Rectangle itemRectangle, int originalDamage, float knockBack, int npcIndex)
|
||||||
|
{
|
||||||
|
DamageKind previous = DamageNumbers.Current;
|
||||||
|
DamageNumbers.Current = DamageNumbers.Of(item);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
original(self, item, itemRectangle, originalDamage, knockBack, npcIndex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DamageNumbers.Current = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dashes, buffs and contact damage pass the item or projectile that caused the hit.
|
||||||
|
static void ApplyDamage(ApplyDamageOriginal original, Player self, NPC npc, int damage, float knockback, int direction, bool crit, PlayerNPCHitSource source)
|
||||||
|
{
|
||||||
|
DamageKind previous = DamageNumbers.Current;
|
||||||
|
if (source.ProjType > 0 && ContentSamples.ProjectilesByType.TryGetValue(source.ProjType, out Projectile? projectile))
|
||||||
|
DamageNumbers.Current = DamageNumbers.Of(projectile);
|
||||||
|
else if (source.ItemType > 0 && ContentSamples.ItemsByType.TryGetValue(source.ItemType, out Item? item))
|
||||||
|
DamageNumbers.Current = DamageNumbers.Of(item);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
original(self, npc, damage, knockback, direction, crit, source);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DamageNumbers.Current = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -21,6 +21,10 @@ internal sealed class HookRegistry : IDisposable
|
|||||||
MainPatches.Install(hooks);
|
MainPatches.Install(hooks);
|
||||||
PlayerPatches.Install(hooks);
|
PlayerPatches.Install(hooks);
|
||||||
NpcPatches.Install(hooks);
|
NpcPatches.Install(hooks);
|
||||||
|
ProjectilePatches.Install(hooks);
|
||||||
|
DamageNumberPatches.Install(hooks);
|
||||||
|
VisionPatches.Install(hooks);
|
||||||
|
SpawnRatePatch.Install(hooks);
|
||||||
ItemCheckShootPatch.Install(hooks);
|
ItemCheckShootPatch.Install(hooks);
|
||||||
SandboxGuards.Install(hooks);
|
SandboxGuards.Install(hooks);
|
||||||
return hooks;
|
return hooks;
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ internal static class MainUpdatePatch
|
|||||||
Hotkeys.Update();
|
Hotkeys.Update();
|
||||||
Sandbox.NewTick();
|
Sandbox.NewTick();
|
||||||
WorldFeatures.Update();
|
WorldFeatures.Update();
|
||||||
|
MapFeatures.Update();
|
||||||
Settings.AutoSave();
|
Settings.AutoSave();
|
||||||
Profiler.EndTick();
|
Profiler.EndTick();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ internal static class PlayerPatches
|
|||||||
PlayerUpdatePatch.Install(hooks);
|
PlayerUpdatePatch.Install(hooks);
|
||||||
PlayerItemCheckPatch.Install(hooks);
|
PlayerItemCheckPatch.Install(hooks);
|
||||||
PlayerMovementPatch.Install(hooks);
|
PlayerMovementPatch.Install(hooks);
|
||||||
|
ChestRangePatch.Install(hooks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ internal static class PlayerUpdatePatch
|
|||||||
{
|
{
|
||||||
Clock.Advance();
|
Clock.Advance();
|
||||||
PlayerFeatures.PostUpdate(player);
|
PlayerFeatures.PostUpdate(player);
|
||||||
|
Stats.Update(player);
|
||||||
CombatFeatures.PostPlayerUpdate(player);
|
CombatFeatures.PostPlayerUpdate(player);
|
||||||
Ballistics.Observe();
|
Ballistics.Observe();
|
||||||
NpcMotion.Track();
|
NpcMotion.Track();
|
||||||
@@ -99,3 +101,19 @@ internal static class PlayerMovementPatch
|
|||||||
original(self);
|
original(self);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Closes any open chest or bank once the player leaves range of chestX/chestY.
|
||||||
|
internal static class ChestRangePatch
|
||||||
|
{
|
||||||
|
delegate void Original(Player self);
|
||||||
|
|
||||||
|
internal static void Install(HookRegistry hooks) => hooks.Add(typeof(Player), "HandleBeingInChestRange", (Action<Original, Player>)HandleRange);
|
||||||
|
|
||||||
|
static void HandleRange(Original original, Player self)
|
||||||
|
{
|
||||||
|
if (Plugin.Active && self.whoAmI == Main.myPlayer && BankButtons.KeepsOpen(self))
|
||||||
|
return;
|
||||||
|
|
||||||
|
original(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using Terraria;
|
||||||
|
using TerraSharp.Core;
|
||||||
|
using TerraSharp.Features.Combat;
|
||||||
|
using TerraSharp.Simulation;
|
||||||
|
|
||||||
|
namespace TerraSharp.Patches;
|
||||||
|
|
||||||
|
// After the AI chose its velocity, before Projectile.Update moves it.
|
||||||
|
internal static class ProjectilePatches
|
||||||
|
{
|
||||||
|
delegate void Original(Projectile self);
|
||||||
|
|
||||||
|
internal static void Install(HookRegistry hooks) => hooks.Add(typeof(Projectile), nameof(Projectile.AI), (Action<Original, Projectile>)AI);
|
||||||
|
|
||||||
|
static void AI(Original original, Projectile self)
|
||||||
|
{
|
||||||
|
original(self);
|
||||||
|
if (Plugin.Active && Settings.MagicBullets && !Sandbox.Active && self.owner == Main.myPlayer)
|
||||||
|
Guard.Run("Projectile.AI postfix", MagicBullets.PostAI, self);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using Terraria;
|
||||||
|
using TerraSharp.Core;
|
||||||
|
using TerraSharp.Features;
|
||||||
|
#if TMODLOADER
|
||||||
|
using Terraria.ModLoader;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace TerraSharp.Patches;
|
||||||
|
|
||||||
|
// Natural spawns run only in single player and on the server.
|
||||||
|
internal static class SpawnRatePatch
|
||||||
|
{
|
||||||
|
#if TMODLOADER
|
||||||
|
// tModLoader's NPC.SpawnNPC passes the final rate through NPCLoader.EditSpawnRate.
|
||||||
|
delegate void Original(Player player, ref int spawnRate, ref int maxSpawns);
|
||||||
|
delegate void Hooked(Original original, Player player, ref int spawnRate, ref int maxSpawns);
|
||||||
|
|
||||||
|
internal static void Install(HookRegistry hooks) => hooks.Add(typeof(NPCLoader), nameof(NPCLoader.EditSpawnRate), (Hooked)EditSpawnRate);
|
||||||
|
|
||||||
|
static void EditSpawnRate(Original original, Player player, ref int spawnRate, ref int maxSpawns)
|
||||||
|
{
|
||||||
|
original(player, ref spawnRate, ref maxSpawns);
|
||||||
|
Scale(ref spawnRate, ref maxSpawns);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
delegate void Original(NPC.Spawner self, Player player, out int spawnRate, out int maxSpawns);
|
||||||
|
delegate void Hooked(Original original, NPC.Spawner self, Player player, out int spawnRate, out int maxSpawns);
|
||||||
|
|
||||||
|
internal static void Install(HookRegistry hooks) => hooks.Add(typeof(NPC.Spawner), "GetSpawnRate", (Hooked)GetSpawnRate);
|
||||||
|
|
||||||
|
static void GetSpawnRate(Original original, NPC.Spawner self, Player player, out int spawnRate, out int maxSpawns)
|
||||||
|
{
|
||||||
|
original(self, player, out spawnRate, out maxSpawns);
|
||||||
|
Scale(ref spawnRate, ref maxSpawns);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static void Scale(ref int spawnRate, ref int maxSpawns)
|
||||||
|
{
|
||||||
|
if (!Plugin.Active)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
NpcSpawner.ScaleSpawnRate(ref spawnRate, ref maxSpawns);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error("Spawn rate", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Mono.Cecil.Cil;
|
||||||
|
using MonoMod.Cil;
|
||||||
|
using Terraria;
|
||||||
|
using Terraria.Graphics.Light;
|
||||||
|
using TerraSharp.Core;
|
||||||
|
#if TMODLOADER
|
||||||
|
using TerraSharp.Features;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace TerraSharp.Patches;
|
||||||
|
|
||||||
|
internal static class VisionPatches
|
||||||
|
{
|
||||||
|
internal static void Install(HookRegistry hooks)
|
||||||
|
{
|
||||||
|
FullBrightPatch.Install(hooks);
|
||||||
|
ZoomPatch.Install(hooks);
|
||||||
|
#if TMODLOADER
|
||||||
|
SpectateCameraPatch.Install(hooks);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every Lighting query reaches the active engine's GetColor, millions of times per frame, so the check is a bare field load.
|
||||||
|
internal static class FullBrightPatch
|
||||||
|
{
|
||||||
|
internal static void Install(HookRegistry hooks)
|
||||||
|
{
|
||||||
|
hooks.Modify(HookRegistry.Method(typeof(LightingEngine), nameof(LightingEngine.GetColor)), ReturnWhite);
|
||||||
|
hooks.Modify(HookRegistry.Method(typeof(LegacyLighting), nameof(LegacyLighting.GetColor)), ReturnWhite);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ReturnWhite(ILContext context)
|
||||||
|
{
|
||||||
|
var cursor = new ILCursor(context);
|
||||||
|
ILLabel body = cursor.DefineLabel();
|
||||||
|
cursor.Emit(OpCodes.Ldsfld, typeof(Settings).GetField(nameof(Settings.FullBright))!);
|
||||||
|
cursor.Emit(OpCodes.Brfalse, body);
|
||||||
|
cursor.Emit(OpCodes.Call, typeof(Vector3).GetProperty(nameof(Vector3.One))!.GetGetMethod()!);
|
||||||
|
cursor.Emit(OpCodes.Ret);
|
||||||
|
cursor.Emit(OpCodes.Nop);
|
||||||
|
body.Target = cursor.Prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main.DoDraw and Main.UpdateViewZoomKeys clamp Main.GameZoomTarget to [1, 2].
|
||||||
|
internal static class ZoomPatch
|
||||||
|
{
|
||||||
|
public const float MaxZoom = 8f;
|
||||||
|
const int SearchWindow = 8;
|
||||||
|
|
||||||
|
// Tile and wall targets cover the screen plus Main.offScreenRange on each side.
|
||||||
|
public static float MinZoom => Math.Max(Main.screenWidth / (float)(Main.screenWidth + 2 * Main.offScreenRange), Main.screenHeight / (float)(Main.screenHeight + 2 * Main.offScreenRange));
|
||||||
|
|
||||||
|
internal static void Install(HookRegistry hooks)
|
||||||
|
{
|
||||||
|
hooks.Modify(HookRegistry.Method(typeof(Main), "DoDraw"), ReplaceClamps);
|
||||||
|
hooks.Modify(HookRegistry.Method(typeof(Main), nameof(Main.UpdateViewZoomKeys)), ReplaceClamps);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float Clamp(float value, float minimum, float maximum) => Settings.ExtendedZoom && Plugin.Active ? MathHelper.Clamp(value, MinZoom, MaxZoom) : MathHelper.Clamp(value, minimum, maximum);
|
||||||
|
|
||||||
|
static void ReplaceClamps(ILContext context)
|
||||||
|
{
|
||||||
|
var cursor = new ILCursor(context);
|
||||||
|
int replaced = 0;
|
||||||
|
while (cursor.TryGotoNext(MoveType.After, i => i.MatchLdsfld(typeof(Main), nameof(Main.GameZoomTarget))))
|
||||||
|
{
|
||||||
|
Instruction? instruction = cursor.Next;
|
||||||
|
for (int i = 0; i < SearchWindow && instruction is not null; i++, instruction = instruction.Next)
|
||||||
|
{
|
||||||
|
if (instruction.OpCode != OpCodes.Call || instruction.Operand is not Mono.Cecil.MethodReference { Name: "Clamp" })
|
||||||
|
continue;
|
||||||
|
|
||||||
|
instruction.Operand = context.Import(typeof(ZoomPatch).GetMethod(nameof(Clamp))!);
|
||||||
|
replaced++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replaced == 0)
|
||||||
|
throw new MissingMethodException(context.Method.FullName, "Clamp");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#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
|
||||||
@@ -133,7 +133,7 @@ public static class Ballistics
|
|||||||
track.Age++;
|
track.Age++;
|
||||||
|
|
||||||
// Tile hits, bounces and homing turns show up as sharp changes.
|
// Tile hits, bounces and homing turns show up as sharp changes.
|
||||||
if (!track.Spoiled && track.Velocities.Count > 0 && (velocity - track.LastVelocity).Length() > Math.Max(4f, track.LastVelocity.Length() * 0.5f))
|
if (!track.Spoiled && (MagicBullets.IsSteered(i) || (track.Velocities.Count > 0 && (velocity - track.LastVelocity).Length() > Math.Max(4f, track.LastVelocity.Length() * 0.5f))))
|
||||||
track.Spoiled = true;
|
track.Spoiled = true;
|
||||||
if (!track.Spoiled && track.Velocities.Count < MaxSamples)
|
if (!track.Spoiled && track.Velocities.Count < MaxSamples)
|
||||||
track.Velocities.Add(velocity);
|
track.Velocities.Add(velocity);
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using Terraria;
|
||||||
|
using Terraria.GameInput;
|
||||||
|
using Terraria.ID;
|
||||||
|
using Terraria.Localization;
|
||||||
|
using TerraSharp.Core;
|
||||||
|
using TerraSharp.Patches;
|
||||||
|
#if TMODLOADER
|
||||||
|
using Terraria.ModLoader;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace TerraSharp.UI;
|
||||||
|
|
||||||
|
public enum Bank
|
||||||
|
{
|
||||||
|
PiggyBank = -2,
|
||||||
|
Safe = -3,
|
||||||
|
DefendersForge = -4,
|
||||||
|
VoidVault = -5,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bank icons stacked under the builder accessory toggles, left of the inventory.
|
||||||
|
public static class BankButtons
|
||||||
|
{
|
||||||
|
const int Size = 18;
|
||||||
|
const int Spacing = 24;
|
||||||
|
const float ColumnX = 10f;
|
||||||
|
|
||||||
|
static readonly (Bank Bank, int Item)[] Banks = [(Bank.PiggyBank, ItemID.PiggyBank), (Bank.Safe, ItemID.Safe), (Bank.DefendersForge, ItemID.DefendersForge), (Bank.VoidVault, ItemID.VoidVault)];
|
||||||
|
static readonly Action<Player, int, int, int> OpenChest = (Action<Player, int, int, int>)Delegate.CreateDelegate(typeof(Action<Player, int, int, int>), HookRegistry.Method(typeof(Player), "OpenChest", [typeof(int), typeof(int), typeof(int)]));
|
||||||
|
|
||||||
|
// Bank opened by a button; HandleBeingInChestRange would close it for being out of range.
|
||||||
|
public static Bank? Remote { get; private set; }
|
||||||
|
|
||||||
|
public static bool KeepsOpen(Player player)
|
||||||
|
{
|
||||||
|
if (Settings.BankButtons && Remote is { } bank && player.chest == (int)bank)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
Remote = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Draw(SpriteBatch spriteBatch)
|
||||||
|
{
|
||||||
|
Player player = Main.LocalPlayer;
|
||||||
|
if (!Main.playerInventory || player.sign >= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float y = FirstSlotY(player);
|
||||||
|
var mouse = new Point(Main.mouseX, Main.mouseY);
|
||||||
|
foreach ((Bank bank, int item) in Banks)
|
||||||
|
{
|
||||||
|
var center = new Vector2(ColumnX, y);
|
||||||
|
y += Spacing;
|
||||||
|
|
||||||
|
Rectangle bounds = Utils.CenteredRectangle(center, new Vector2(Size));
|
||||||
|
bool hover = bounds.Contains(mouse) && !PlayerInput.IgnoreMouseInterface;
|
||||||
|
bool open = player.chest == (int)bank;
|
||||||
|
if (hover)
|
||||||
|
bounds.Inflate(2, 2);
|
||||||
|
Render.Item(spriteBatch, item, bounds, hover || open ? Color.White : new Color(180, 180, 180));
|
||||||
|
|
||||||
|
if (!hover)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
player.mouseInterface = true;
|
||||||
|
Main.instance.MouseText(Lang.GetItemNameValue(item), 0, 0, -1, -1, -1, -1, 0);
|
||||||
|
Main.mouseText = true;
|
||||||
|
|
||||||
|
if (Main.mouseLeft && Main.mouseLeftRelease)
|
||||||
|
{
|
||||||
|
Main.mouseLeftRelease = false;
|
||||||
|
Toggle(player, bank);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center of the slot after the last builder toggle; mirrors Main.DrawBuilderAccToggles.
|
||||||
|
static float FirstSlotY(Player player)
|
||||||
|
{
|
||||||
|
#if TMODLOADER
|
||||||
|
int count = BuilderToggleLoader.ActiveBuilderToggles(), page = BuilderToggleLoader.BuilderTogglePage;
|
||||||
|
bool shiftHotbarLock = count / 12 != page || count % 12 >= 10;
|
||||||
|
int start = 0, end = count;
|
||||||
|
bool previousArrow = false, nextArrow = false;
|
||||||
|
if (count > 12)
|
||||||
|
{
|
||||||
|
start = 12 * page;
|
||||||
|
end = count - start < 12 ? count : start == 0 ? start + 12 : start + 11;
|
||||||
|
previousArrow = page != 0;
|
||||||
|
nextArrow = page != count / 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 77f - (shiftHotbarLock ? 42 : 21) + Spacing * (end - start + previousArrow.ToInt() + nextArrow.ToInt());
|
||||||
|
#else
|
||||||
|
int torchIcons = player.unlockedBiomeTorches.ToInt();
|
||||||
|
int toggles = player.InfoAccMechShowWires.ToInt() * 6 + player.rulerLine.ToInt() + player.rulerGrid.ToInt() + player.autoActuator.ToInt() + player.autoPaint.ToInt();
|
||||||
|
bool pushUp = toggles + 1 + torchIcons >= 10;
|
||||||
|
return 77f + Spacing * (torchIcons + toggles - pushUp.ToInt());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Toggle(Player player, Bank bank)
|
||||||
|
{
|
||||||
|
if (player.chest == (int)bank)
|
||||||
|
{
|
||||||
|
player.chest = -1;
|
||||||
|
Remote = null;
|
||||||
|
Widgets.Sound(11);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool wasClosed = player.chest == -1;
|
||||||
|
player.SetTalkNPC(-1);
|
||||||
|
Main.npcChatCornerItem = 0;
|
||||||
|
Main.npcChatText = "";
|
||||||
|
if (Main.editChest)
|
||||||
|
{
|
||||||
|
Widgets.Sound(12);
|
||||||
|
Main.editChest = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player.editedChestName && player.chest >= 0)
|
||||||
|
{
|
||||||
|
NetMessage.SendData(MessageID.ChestName, -1, -1, NetworkText.FromLiteral(Main.chest[player.chest].name), player.chest, 1f);
|
||||||
|
player.editedChestName = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.piggyBankProjTracker.Clear();
|
||||||
|
player.voidLensChest.Clear();
|
||||||
|
Main.stackSplit = 600;
|
||||||
|
|
||||||
|
var tile = new Point((int)(player.Center.X / 16f), (int)(player.Center.Y / 16f));
|
||||||
|
OpenChest(player, tile.X, tile.Y, (int)bank);
|
||||||
|
Remote = bank;
|
||||||
|
Widgets.Sound(wasClosed ? 10 : 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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))
|
||||||
@@ -71,7 +241,11 @@ public abstract class GridBrowser(string id, string title, Vector2 position, int
|
|||||||
const int TabGap = 4;
|
const int TabGap = 4;
|
||||||
int iconTabs = tabs.Count(tab => tab.Icon >= 0);
|
int iconTabs = tabs.Count(tab => tab.Icon >= 0);
|
||||||
int textTabs = tabs.Length - iconTabs;
|
int textTabs = tabs.Length - iconTabs;
|
||||||
int textWidth = textTabs == 0 ? 0 : (body.Width - TabGap * (tabs.Length - 1) - iconTabs * IconTabWidth) / textTabs;
|
int available = body.Width - TabGap * (tabs.Length - 1);
|
||||||
|
int LabelWidth(BrowserCategory tab) => (int)Math.Ceiling(Render.Measure(tab.Name, scale).X) + 16;
|
||||||
|
int labels = tabs.Where(tab => tab.Icon < 0).Sum(LabelWidth);
|
||||||
|
int iconWidth = iconTabs == 0 ? 0 : Math.Max(24, Math.Min(IconTabWidth, (available - labels) / iconTabs));
|
||||||
|
int spare = textTabs == 0 ? 0 : (available - labels - iconTabs * iconWidth) / textTabs;
|
||||||
Texture2D? icons = iconTabs > 0 ? GameTextures.Request("Images/UI/Creative/Infinite_Icons") : null;
|
Texture2D? icons = iconTabs > 0 ? GameTextures.Request("Images/UI/Creative/Infinite_Icons") : null;
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
int x = body.X;
|
int x = body.X;
|
||||||
@@ -79,7 +253,7 @@ public abstract class GridBrowser(string id, string title, Vector2 position, int
|
|||||||
{
|
{
|
||||||
BrowserCategory tab = tabs[i];
|
BrowserCategory tab = tabs[i];
|
||||||
bool iconTab = tab.Icon >= 0;
|
bool iconTab = tab.Icon >= 0;
|
||||||
var bounds = new Rectangle(x, y, iconTab ? IconTabWidth : textWidth, height);
|
var bounds = new Rectangle(x, y, iconTab ? iconWidth : LabelWidth(tab) + spare, height);
|
||||||
x += bounds.Width + TabGap;
|
x += bounds.Width + TabGap;
|
||||||
if (Widgets.Button(bounds, iconTab ? "" : tab.Name, i == selected, scale) && i != selected)
|
if (Widgets.Button(bounds, iconTab ? "" : tab.Name, i == selected, scale) && i != selected)
|
||||||
{
|
{
|
||||||
@@ -87,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,21 +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 Filter(string query, Func<int, bool> fits)
|
public IEnumerable<int> All()
|
||||||
|
{
|
||||||
|
string[] names = Names;
|
||||||
|
for (int i = 1; i < names.Length; i++)
|
||||||
|
if (names[i].Length > 0)
|
||||||
|
yield return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
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])
|
||||||
@@ -52,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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -31,17 +31,26 @@ 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));
|
||||||
|
|
||||||
int healthBarIndex = layers.FindIndex(l => l.Name == "Vanilla: Entity Health Bars");
|
int healthBarIndex = layers.FindIndex(l => l.Name == "Vanilla: Entity Health Bars");
|
||||||
layers.Insert(healthBarIndex < 0 ? 1 : healthBarIndex + 1, new LegacyGameInterfaceLayer(Prefix + "Health Bars", DrawHealthBars, InterfaceScaleType.Game));
|
layers.Insert(healthBarIndex < 0 ? 1 : healthBarIndex + 1, new LegacyGameInterfaceLayer(Prefix + "Health Bars", DrawHealthBars, InterfaceScaleType.Game));
|
||||||
|
|
||||||
|
int builderIndex = layers.FindIndex(l => l.Name == "Vanilla: Builder Accessories Bar");
|
||||||
|
if (builderIndex >= 0)
|
||||||
|
layers.Insert(builderIndex + 1, new LegacyGameInterfaceLayer(Prefix + "Bank Buttons", DrawBankButtons, InterfaceScaleType.UI));
|
||||||
|
|
||||||
int mouseTextIndex = layers.FindIndex(l => l.Name == "Vanilla: Mouse Text");
|
int mouseTextIndex = layers.FindIndex(l => l.Name == "Vanilla: Mouse Text");
|
||||||
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()
|
||||||
{
|
{
|
||||||
@@ -81,6 +90,22 @@ public static class InterfaceLayers
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool DrawBankButtons()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Plugin.Active && !Main.gameMenu && Settings.BankButtons)
|
||||||
|
BankButtons.Draw(Main.spriteBatch);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error("Bank buttons layer", e);
|
||||||
|
Widgets.Recover(Main.spriteBatch, Main.UIScaleMatrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static bool DrawWindows()
|
static bool DrawWindows()
|
||||||
{
|
{
|
||||||
Unblock();
|
Unblock();
|
||||||
|
|||||||
@@ -1,89 +1,14 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Terraria;
|
using Terraria;
|
||||||
using Terraria.GameContent.Creative;
|
|
||||||
using Terraria.ID;
|
|
||||||
using Terraria.Localization;
|
|
||||||
using Terraria.UI;
|
|
||||||
#if TMODLOADER
|
|
||||||
using Terraria.ModLoader;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace TerraSharp.UI;
|
namespace TerraSharp.UI;
|
||||||
|
|
||||||
public sealed class ItemBrowser() : GridBrowser("items", "Item Browser", new Vector2(660, 120), 532, 520, "Search items by name or ID", "items", new IdIndex(() => ItemCount, Lang.GetItemNameValue), Categories())
|
public sealed class ItemBrowser() : ItemGrid("items", "Item Browser", new Vector2(660, 120), 534, 554, [])
|
||||||
{
|
{
|
||||||
const int InventorySlots = 50;
|
const int InventorySlots = 50;
|
||||||
|
|
||||||
#if TMODLOADER
|
|
||||||
static int ItemCount => ItemLoader.ItemCount;
|
|
||||||
#else
|
|
||||||
static int ItemCount => ItemID.Count;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
enum WeaponClass { Melee, Ranged, Magic, Summon, Other }
|
|
||||||
|
|
||||||
static readonly IItemEntryFilter Weapon = new ItemFilters.Weapon();
|
|
||||||
|
|
||||||
// Journey duplication filters paired with Infinite_Icons frame indices.
|
|
||||||
static readonly (IItemEntryFilter Filter, int Icon)[] Filters = [(new ItemFilters.Tools(), 6), (new ItemFilters.Armor(), 2), (new ItemFilters.Vanity(), 8), (new ItemFilters.Accessories(), 1), (new ItemFilters.MiscAccessories(), 9), (new ItemFilters.Consumables(), 3), (new ItemFilters.BuildingBlock(), 4), (new ItemFilters.Furniture(), 7), (new ItemFilters.Materials(), 10)];
|
|
||||||
|
|
||||||
static BrowserCategory[] Categories() =>
|
|
||||||
[
|
|
||||||
new("All", _ => true),
|
|
||||||
new(Language.GetTextValue(Weapon.GetDisplayNameKey()), type => Fits(Weapon, type), 0, [.. Enum.GetValues(typeof(WeaponClass)).Cast<WeaponClass>().Select(weaponClass => new BrowserCategory(weaponClass.ToString(), type => ContentSample(type) is { } item && ClassOf(item) == weaponClass)), new("Ammo", type => ContentSample(type) is { ammo: > 0 })]),
|
|
||||||
.. Filters.Select(entry => new BrowserCategory(Language.GetTextValue(entry.Filter.GetDisplayNameKey()), type => Fits(entry.Filter, type), entry.Icon)),
|
|
||||||
new(Language.GetTextValue("CreativePowers.TabMisc"), type => !Fits(Weapon, type) && !Filters.Any(entry => Fits(entry.Filter, type)), 5),
|
|
||||||
];
|
|
||||||
|
|
||||||
static Item? ContentSample(int type) => ContentSamples.ItemsByType.TryGetValue(type, out Item? item) ? item : null;
|
|
||||||
|
|
||||||
// Whips must be classified as summon weapons before checking melee.
|
|
||||||
static WeaponClass? ClassOf(Item item)
|
|
||||||
{
|
|
||||||
if (!Weapon.FitsFilter(item) || item.pick > 0 || item.axe > 0 || item.hammer > 0 || item.ammo > 0)
|
|
||||||
return null;
|
|
||||||
#if TMODLOADER
|
|
||||||
if (item.CountsAsClass(DamageClass.Summon))
|
|
||||||
return WeaponClass.Summon;
|
|
||||||
if (item.CountsAsClass(DamageClass.Magic))
|
|
||||||
return WeaponClass.Magic;
|
|
||||||
if (item.CountsAsClass(DamageClass.Ranged))
|
|
||||||
return WeaponClass.Ranged;
|
|
||||||
if (item.CountsAsClass(DamageClass.Melee))
|
|
||||||
return WeaponClass.Melee;
|
|
||||||
#else
|
|
||||||
if (item.summon)
|
|
||||||
return WeaponClass.Summon;
|
|
||||||
if (item.magic)
|
|
||||||
return WeaponClass.Magic;
|
|
||||||
if (item.ranged)
|
|
||||||
return WeaponClass.Ranged;
|
|
||||||
if (item.melee)
|
|
||||||
return WeaponClass.Melee;
|
|
||||||
#endif
|
|
||||||
return WeaponClass.Other;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool Fits(IItemEntryFilter filter, int type) => ContentSample(type) is { } item && filter.FitsFilter(item);
|
|
||||||
|
|
||||||
readonly Dictionary<int, Item> _samples = [];
|
|
||||||
|
|
||||||
public override void OnOpen() => Main.playerInventory = true;
|
public override void OnOpen() => Main.playerInventory = true;
|
||||||
|
|
||||||
Item Sample(int type)
|
|
||||||
{
|
|
||||||
if (!_samples.TryGetValue(type, out Item? item))
|
|
||||||
{
|
|
||||||
item = new Item();
|
|
||||||
item.SetDefaults(type);
|
|
||||||
_samples[type] = item;
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void DrawContent(Rectangle body)
|
protected override void DrawContent(Rectangle body)
|
||||||
{
|
{
|
||||||
DrawSearchGrid(body, body.Y, body.Bottom - 30);
|
DrawSearchGrid(body, body.Y, body.Bottom - 30);
|
||||||
@@ -92,18 +17,10 @@ public sealed class ItemBrowser() : GridBrowser("items", "Item Browser", new Vec
|
|||||||
|
|
||||||
protected override void DrawSlot(int type, Rectangle bounds, bool hot)
|
protected override void DrawSlot(int type, Rectangle bounds, bool hot)
|
||||||
{
|
{
|
||||||
Widgets.Slot(bounds, hot);
|
DrawItem(type, bounds, hot);
|
||||||
Render.Item(Widgets.SpriteBatch, type, new Rectangle(bounds.X + 6, bounds.Y + 6, bounds.Width - 12, bounds.Height - 12));
|
|
||||||
if (!hot)
|
if (!hot)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
#if TMODLOADER
|
|
||||||
Item sample = Sample(type);
|
|
||||||
ItemSlot.MouseHover(ref sample, ItemSlot.Context.CreativeInfinite);
|
|
||||||
#else
|
|
||||||
ItemSlot.MouseHover(Sample(type), ItemSlot.Context.CreativeInfinite);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (Widgets.Click)
|
if (Widgets.Click)
|
||||||
Give(type, Widgets.Shift);
|
Give(type, Widgets.Shift);
|
||||||
else if (Widgets.RightClick)
|
else if (Widgets.RightClick)
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
using System;
|
||||||
|
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;
|
||||||
|
#endif
|
||||||
|
using TerraSharp.Features;
|
||||||
|
|
||||||
|
namespace TerraSharp.UI;
|
||||||
|
|
||||||
|
// Edits the cursor item, or the held item when the cursor is empty. Only type, stack and prefix survive a save.
|
||||||
|
public sealed class ItemEditor() : Window("itemEditor", "Item Editor", new Vector2(700, 100), 480, 600)
|
||||||
|
{
|
||||||
|
const int RowHeight = 30;
|
||||||
|
|
||||||
|
readonly Scroll _scroll = new();
|
||||||
|
readonly Item _reference = new();
|
||||||
|
(int Type, int Prefix) _referenceKey = (-1, -1);
|
||||||
|
(int Prefix, int Value, int Rarity)[] _prefixOptions = [];
|
||||||
|
int _prefixType = -1;
|
||||||
|
int _x, _y, _width;
|
||||||
|
bool _firstGroup;
|
||||||
|
|
||||||
|
public override void OnOpen() => Main.playerInventory = true;
|
||||||
|
|
||||||
|
public override void DrawBody(Rectangle body)
|
||||||
|
{
|
||||||
|
if (Main.gameMenu)
|
||||||
|
{
|
||||||
|
Render.TextCentered(Widgets.SpriteBatch, "Enter a world to use this.", body, Widgets.Dim, 0.9f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shows and edits base stats instead of the Damage Multiplier, Rapid Attack and Instant Break overrides.
|
||||||
|
HeldItemOverrides.Restore();
|
||||||
|
Item item = ItemFeatures.EditTarget;
|
||||||
|
DrawHeader(body, item);
|
||||||
|
|
||||||
|
int top = body.Y + 60;
|
||||||
|
var inner = new Rectangle(body.X, top, body.Width, body.Bottom - top);
|
||||||
|
Widgets.Panel(inner, Widgets.InnerBg);
|
||||||
|
if (item.type == 0)
|
||||||
|
{
|
||||||
|
Render.TextCentered(Widgets.SpriteBatch, "Hold an item or pick one up with the cursor.", inner, Widgets.Dim, 0.8f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var area = new Rectangle(inner.X + 12, inner.Y + 8, inner.Width - 38, inner.Height - 16);
|
||||||
|
_y = _scroll.Begin(area);
|
||||||
|
int start = _y;
|
||||||
|
(_x, _width, _firstGroup) = (area.X, area.Width, true);
|
||||||
|
Reference(item);
|
||||||
|
DrawActions(item);
|
||||||
|
DrawPrefixes(item);
|
||||||
|
DrawStats(item);
|
||||||
|
_scroll.End(area, _y - start + 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawHeader(Rectangle body, Item item)
|
||||||
|
{
|
||||||
|
var slot = new Rectangle(body.X, body.Y, 52, 52);
|
||||||
|
Widgets.Slot(slot, false);
|
||||||
|
Render.Item(Widgets.SpriteBatch, item.type, new Rectangle(slot.X + 8, slot.Y + 8, slot.Width - 16, slot.Height - 16));
|
||||||
|
if (item.stack > 1)
|
||||||
|
Render.Text(Widgets.SpriteBatch, item.stack.ToString(), new Vector2(slot.X + 8, slot.Bottom - 20), Color.White, 0.7f);
|
||||||
|
|
||||||
|
string source = Main.mouseItem.type != 0 ? "Cursor item" : "Held item";
|
||||||
|
Render.Text(Widgets.SpriteBatch, item.type == 0 ? "No item" : item.AffixName(), new Vector2(slot.Right + 12, body.Y + 4), item.type == 0 ? Widgets.Dim : Color.White, 0.95f);
|
||||||
|
Render.Text(Widgets.SpriteBatch, item.type == 0 ? source : $"{source} #{item.type}", new Vector2(slot.Right + 12, body.Y + 30), Widgets.Dim, 0.72f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults for the current type and prefix; right-click on a stat resets to these.
|
||||||
|
void Reference(Item item)
|
||||||
|
{
|
||||||
|
if (_referenceKey == (item.type, item.prefix))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_reference.SetDefaults(item.type);
|
||||||
|
if (item.prefix > 0)
|
||||||
|
_reference.Prefix(item.prefix);
|
||||||
|
_referenceKey = (item.type, item.prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Group(string title)
|
||||||
|
{
|
||||||
|
if (!_firstGroup)
|
||||||
|
_y += 8;
|
||||||
|
_firstGroup = false;
|
||||||
|
Widgets.Text(title, new Vector2(_x, _y + 2), Widgets.Header, 0.88f);
|
||||||
|
float width = Render.Measure(title, 0.88f).X;
|
||||||
|
Widgets.Separator(_x + (int)width + 12, _y + 12, _width - (int)width - 12);
|
||||||
|
_y += 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawActions(Item item)
|
||||||
|
{
|
||||||
|
Group("Actions");
|
||||||
|
string[] labels = ["Max Stack", "Duplicate", "Reset Stats"];
|
||||||
|
int width = (_width - 12) / labels.Length;
|
||||||
|
for (int i = 0; i < labels.Length; i++)
|
||||||
|
{
|
||||||
|
if (!Widgets.Button(new Rectangle(_x + i * (width + 6), _y + 2, width, 30), labels[i], false, 0.8f))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
switch (i)
|
||||||
|
{
|
||||||
|
case 0: ItemFeatures.MaxStack(); break;
|
||||||
|
case 1: ItemFeatures.Duplicate(); break;
|
||||||
|
case 2: ResetStats(item); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_y += 38;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ResetStats(Item item)
|
||||||
|
{
|
||||||
|
int stack = item.stack, prefix = item.prefix;
|
||||||
|
bool favorited = item.favorited;
|
||||||
|
item.SetDefaults(item.type);
|
||||||
|
if (prefix > 0)
|
||||||
|
item.Prefix(prefix);
|
||||||
|
(item.stack, item.favorited) = (stack, favorited);
|
||||||
|
}
|
||||||
|
|
||||||
|
static IEnumerable<int> Prefixes(Item item)
|
||||||
|
{
|
||||||
|
#if TMODLOADER
|
||||||
|
return Enumerable.Range(1, PrefixLoader.PrefixCount - 1).Where(item.CanApplyPrefix);
|
||||||
|
#else
|
||||||
|
return item.GetRollablePrefixes() ?? [];
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static string PrefixName(int prefix)
|
||||||
|
{
|
||||||
|
#if TMODLOADER
|
||||||
|
if (prefix >= PrefixID.Count)
|
||||||
|
return PrefixLoader.GetPrefix(prefix)?.DisplayName.Value ?? $"Prefix {prefix}";
|
||||||
|
#endif
|
||||||
|
return Lang.prefix[prefix].Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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;
|
||||||
|
for (int i = 0; i < options.Length; i++)
|
||||||
|
{
|
||||||
|
var bounds = new Rectangle(_x + i % Columns * (width + Gap), _y + i / Columns * 34, width, 30);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ApplyPrefix(Item item, int prefix)
|
||||||
|
{
|
||||||
|
int stack = item.stack;
|
||||||
|
bool favorited = item.favorited;
|
||||||
|
item.SetDefaults(item.type);
|
||||||
|
item.Prefix(prefix);
|
||||||
|
(item.stack, item.favorited) = (stack, favorited);
|
||||||
|
Widgets.Sound(2, -1, -1, 37);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawStats(Item item)
|
||||||
|
{
|
||||||
|
Group("Stats");
|
||||||
|
bool weapon = _reference.damage > 0;
|
||||||
|
if (weapon)
|
||||||
|
{
|
||||||
|
item.damage = IntRow("Damage", item.damage, 0, Math.Max(200, _reference.damage * 10), _reference.damage);
|
||||||
|
item.crit = IntRow("Critical chance", item.crit, 0, 100, _reference.crit, "{0}%");
|
||||||
|
item.knockBack = FloatRow("Knockback", item.knockBack, 0f, 20f, _reference.knockBack, "{0:0.0}");
|
||||||
|
item.scale = FloatRow("Size", item.scale, 0.25f, 5f, _reference.scale, "{0:0.00}x");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_reference.useStyle != 0)
|
||||||
|
{
|
||||||
|
item.useTime = IntRow("Use time", item.useTime, 1, Math.Max(60, _reference.useTime), _reference.useTime, "{0} ticks");
|
||||||
|
item.useAnimation = IntRow("Use animation", item.useAnimation, 1, Math.Max(60, _reference.useAnimation), _reference.useAnimation, "{0} ticks");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_reference.shoot > 0)
|
||||||
|
item.shootSpeed = FloatRow("Shot speed", item.shootSpeed, 0f, 40f, _reference.shootSpeed, "{0:0.0}");
|
||||||
|
if (_reference.defense > 0 || _reference.accessory || _reference.headSlot >= 0 || _reference.bodySlot >= 0 || _reference.legSlot >= 0)
|
||||||
|
item.defense = IntRow("Defense", item.defense, 0, Math.Max(100, _reference.defense * 10), _reference.defense);
|
||||||
|
if (item.maxStack > 1)
|
||||||
|
item.stack = IntRow("Stack", item.stack, 1, item.maxStack, item.maxStack);
|
||||||
|
if (_reference.useStyle != 0)
|
||||||
|
item.autoReuse = BoolRow("Auto reuse", item.autoReuse);
|
||||||
|
}
|
||||||
|
|
||||||
|
float FloatRow(string label, float value, float minimum, float maximum, float fallback, string format)
|
||||||
|
{
|
||||||
|
var row = new Rectangle(_x, _y, _width, RowHeight - 2);
|
||||||
|
string id = "itemEditor|" + label;
|
||||||
|
bool hot = Widgets.Hot(row) || Widgets.ActiveSlider == id;
|
||||||
|
if (hot)
|
||||||
|
Widgets.Tooltip = "Drag to change. Right-click to reset.";
|
||||||
|
Render.TextMid(Widgets.SpriteBatch, $"{label}: {string.Format(format, value)}", row, hot ? Widgets.Yellow : Widgets.Dim, 0.85f);
|
||||||
|
float result = Widgets.ValueBar(id, row, MathHelper.Clamp(value, minimum, maximum), minimum, maximum, fallback);
|
||||||
|
_y += RowHeight;
|
||||||
|
return Widgets.ActiveSlider == id || result != MathHelper.Clamp(value, minimum, maximum) ? result : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int IntRow(string label, int value, int minimum, int maximum, int fallback, string format = "{0}") => (int)Math.Round(FloatRow(label, value, minimum, maximum, fallback, format.Replace("{0}", "{0:0}")));
|
||||||
|
|
||||||
|
bool BoolRow(string label, bool value)
|
||||||
|
{
|
||||||
|
value = Widgets.ToggleRow(new Rectangle(_x, _y, _width, 28), label, value);
|
||||||
|
_y += RowHeight;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Terraria;
|
||||||
|
using Terraria.GameContent.Creative;
|
||||||
|
using Terraria.ID;
|
||||||
|
using Terraria.Localization;
|
||||||
|
using Terraria.UI;
|
||||||
|
#if TMODLOADER
|
||||||
|
using Terraria.ModLoader;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace TerraSharp.UI;
|
||||||
|
|
||||||
|
public abstract class ItemGrid(string id, string title, Vector2 position, int width, int height, BrowserCategory[] extra) : GridBrowser(id, title, position, width, height, "Search items by name or ID", "items", new IdIndex(() => ItemCount, Lang.GetItemNameValue), [.. Categories(), .. extra], Sorts)
|
||||||
|
{
|
||||||
|
#if TMODLOADER
|
||||||
|
static int ItemCount => ItemLoader.ItemCount;
|
||||||
|
#else
|
||||||
|
static int ItemCount => ItemID.Count;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum WeaponClass { Melee, Ranged, Magic, Summon, Other }
|
||||||
|
|
||||||
|
static readonly IItemEntryFilter Weapon = new ItemFilters.Weapon();
|
||||||
|
|
||||||
|
// Journey duplication filters paired with Infinite_Icons frame indices; 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, 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.
|
||||||
|
static WeaponClass? ClassOf(Item item)
|
||||||
|
{
|
||||||
|
if (!Weapon.FitsFilter(item) || item.pick > 0 || item.axe > 0 || item.hammer > 0 || item.ammo > 0)
|
||||||
|
return null;
|
||||||
|
#if TMODLOADER
|
||||||
|
if (item.CountsAsClass(DamageClass.Summon))
|
||||||
|
return WeaponClass.Summon;
|
||||||
|
if (item.CountsAsClass(DamageClass.Magic))
|
||||||
|
return WeaponClass.Magic;
|
||||||
|
if (item.CountsAsClass(DamageClass.Ranged))
|
||||||
|
return WeaponClass.Ranged;
|
||||||
|
if (item.CountsAsClass(DamageClass.Melee))
|
||||||
|
return WeaponClass.Melee;
|
||||||
|
#else
|
||||||
|
if (item.summon)
|
||||||
|
return WeaponClass.Summon;
|
||||||
|
if (item.magic)
|
||||||
|
return WeaponClass.Magic;
|
||||||
|
if (item.ranged)
|
||||||
|
return WeaponClass.Ranged;
|
||||||
|
if (item.melee)
|
||||||
|
return WeaponClass.Melee;
|
||||||
|
#endif
|
||||||
|
return WeaponClass.Other;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Fits(IItemEntryFilter filter, int type) => ContentSample(type) is { } item && filter.FitsFilter(item);
|
||||||
|
|
||||||
|
readonly Dictionary<int, Item> _samples = [];
|
||||||
|
|
||||||
|
Item Sample(int type)
|
||||||
|
{
|
||||||
|
if (!_samples.TryGetValue(type, out Item? item))
|
||||||
|
{
|
||||||
|
item = new Item();
|
||||||
|
item.SetDefaults(type);
|
||||||
|
_samples[type] = item;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void DrawItem(int type, Rectangle bounds, bool hot, bool selected = false)
|
||||||
|
{
|
||||||
|
Widgets.Slot(bounds, hot, selected);
|
||||||
|
Render.Item(Widgets.SpriteBatch, type, new Rectangle(bounds.X + 6, bounds.Y + 6, bounds.Width - 12, bounds.Height - 12));
|
||||||
|
if (!hot)
|
||||||
|
return;
|
||||||
|
|
||||||
|
#if TMODLOADER
|
||||||
|
Item sample = Sample(type);
|
||||||
|
ItemSlot.MouseHover(ref sample, ItemSlot.Context.CreativeInfinite);
|
||||||
|
#else
|
||||||
|
ItemSlot.MouseHover(Sample(type), ItemSlot.Context.CreativeInfinite);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
|
||||||
|
namespace TerraSharp.UI;
|
||||||
|
|
||||||
|
// Edits a persisted set of item types, such as the auto sell list.
|
||||||
|
public sealed class ItemPicker(string id, string title, Vector2 position, HashSet<int> list) : ItemGrid(id, title, position, 534, 554, [new("Selected", list.Contains)])
|
||||||
|
{
|
||||||
|
protected override void DrawContent(Rectangle body)
|
||||||
|
{
|
||||||
|
DrawSearchGrid(body, body.Y, body.Bottom - 40);
|
||||||
|
var footer = new Rectangle(body.X + 4, body.Bottom - 34, body.Width - 4, 32);
|
||||||
|
Render.TextMid(Widgets.SpriteBatch, $"{list.Count} selected Click: add or remove", footer, Widgets.Dim, 0.72f);
|
||||||
|
if (!Widgets.Button(new Rectangle(footer.Right - 100, footer.Y + 1, 100, 30), "Clear", false, 0.8f))
|
||||||
|
return;
|
||||||
|
|
||||||
|
list.Clear();
|
||||||
|
Index.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DrawSlot(int type, Rectangle bounds, bool hot)
|
||||||
|
{
|
||||||
|
DrawItem(type, bounds, hot, list.Contains(type));
|
||||||
|
if (!hot || !Widgets.Click)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!list.Remove(type))
|
||||||
|
list.Add(type);
|
||||||
|
Index.Refresh();
|
||||||
|
Widgets.Sound(12);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ using Terraria.GameContent.Events;
|
|||||||
using TerraSharp.Core;
|
using TerraSharp.Core;
|
||||||
using TerraSharp.Features;
|
using TerraSharp.Features;
|
||||||
using TerraSharp.Features.Combat;
|
using TerraSharp.Features.Combat;
|
||||||
|
using TerraSharp.Patches;
|
||||||
using TerraSharp.Simulation;
|
using TerraSharp.Simulation;
|
||||||
|
|
||||||
namespace TerraSharp.UI;
|
namespace TerraSharp.UI;
|
||||||
@@ -19,6 +20,9 @@ public sealed partial class MainWindow
|
|||||||
{
|
{
|
||||||
Group("Survival");
|
Group("Survival");
|
||||||
Toggle("God Mode", ref Settings.GodMode);
|
Toggle("God Mode", ref Settings.GodMode);
|
||||||
|
Toggle("Ghost Mode", ref Settings.GhostMode);
|
||||||
|
if (Toggle("Defense Multiplier", ref Settings.DefenseBoost))
|
||||||
|
Sub(() => Slider("Multiplier", ref Settings.DefenseMult, 1f, 20f, 3f, "x{0:0.0}"));
|
||||||
Toggle("Infinite Breath", ref Settings.InfBreath);
|
Toggle("Infinite Breath", ref Settings.InfBreath);
|
||||||
Toggle("Infinite Flight", ref Settings.InfFlight);
|
Toggle("Infinite Flight", ref Settings.InfFlight);
|
||||||
Toggle("Infinite Health", ref Settings.InfHealth);
|
Toggle("Infinite Health", ref Settings.InfHealth);
|
||||||
@@ -44,6 +48,16 @@ public sealed partial class MainWindow
|
|||||||
case 0: PlayerFeatures.FullHeal(); break;
|
case 0: PlayerFeatures.FullHeal(); break;
|
||||||
case 1: PlayerFeatures.TeleportSpawn(); break;
|
case 1: PlayerFeatures.TeleportSpawn(); break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
void MovementPage()
|
||||||
@@ -97,6 +111,8 @@ public sealed partial class MainWindow
|
|||||||
Slider("Max range", ref Settings.AimRange, 5f, 150f, 60f, "{0:0} tiles");
|
Slider("Max range", ref Settings.AimRange, 5f, 150f, 60f, "{0:0} tiles");
|
||||||
Toggle("Show target", ref Settings.AimDrawTarget);
|
Toggle("Show target", ref Settings.AimDrawTarget);
|
||||||
});
|
});
|
||||||
|
Toggle("Auto Dodge", ref Settings.AutoDodge);
|
||||||
|
Toggle("Magic Bullets", ref Settings.MagicBullets);
|
||||||
Toggle("Shot Preview", ref Settings.ShotPreview);
|
Toggle("Shot Preview", ref Settings.ShotPreview);
|
||||||
|
|
||||||
Group("Enemies");
|
Group("Enemies");
|
||||||
@@ -121,6 +137,15 @@ public sealed partial class MainWindow
|
|||||||
void ItemsPage()
|
void ItemsPage()
|
||||||
{
|
{
|
||||||
Group("Items");
|
Group("Items");
|
||||||
|
if (Toggle("Auto Sell", ref Settings.AutoSell))
|
||||||
|
Sub(() =>
|
||||||
|
{
|
||||||
|
Note($"{Settings.SellList.Count} items listed. Favorited items are never sold.");
|
||||||
|
if (Buttons(WindowManager.SellList.Open ? "Close Sell List" : "Edit Sell List") == 0)
|
||||||
|
WindowManager.Toggle(WindowManager.SellList);
|
||||||
|
});
|
||||||
|
Toggle("Auto Deposit", ref Settings.AutoDeposit);
|
||||||
|
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);
|
||||||
Toggle("Item Vacuum", ref Settings.ItemVacuum);
|
Toggle("Item Vacuum", ref Settings.ItemVacuum);
|
||||||
@@ -135,8 +160,11 @@ public sealed partial class MainWindow
|
|||||||
Toggle("Sonar", ref Settings.Sonar);
|
Toggle("Sonar", ref Settings.Sonar);
|
||||||
|
|
||||||
Group("Actions");
|
Group("Actions");
|
||||||
if (Buttons(WindowManager.Items.Open ? "Close Item Browser" : "Item Browser") == 0)
|
switch (Buttons(WindowManager.Items.Open ? "Close Item Browser" : "Item Browser", WindowManager.Editor.Open ? "Close Item Editor" : "Item Editor"))
|
||||||
WindowManager.Toggle(WindowManager.Items);
|
{
|
||||||
|
case 0: WindowManager.Toggle(WindowManager.Items); break;
|
||||||
|
case 1: WindowManager.Toggle(WindowManager.Editor); break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VisualsPage()
|
void VisualsPage()
|
||||||
@@ -154,7 +182,17 @@ public sealed partial class MainWindow
|
|||||||
Toggle("Tracers", ref Settings.EspLines);
|
Toggle("Tracers", ref Settings.EspLines);
|
||||||
|
|
||||||
Group("Targets");
|
Group("Targets");
|
||||||
|
Toggle("Boss cursor", ref Settings.EspBossCursor);
|
||||||
ToggleColor("Bosses (Rainbow)", ref Settings.EspBossRainbow, ref Settings.ColBoss, nameof(Settings.ColBoss));
|
ToggleColor("Bosses (Rainbow)", ref Settings.EspBossRainbow, ref Settings.ColBoss, nameof(Settings.ColBoss));
|
||||||
|
ToggleColor("Chests", ref Settings.EspChests, ref Settings.ColChests, nameof(Settings.ColChests));
|
||||||
|
if (Settings.EspChests)
|
||||||
|
Sub(() =>
|
||||||
|
{
|
||||||
|
if (Toggle("Only with listed items", ref Settings.EspChestsListed))
|
||||||
|
Note($"{Settings.ChestList.Count} items listed. Multiplayer: only opened chests.");
|
||||||
|
if (Buttons(WindowManager.ChestList.Open ? "Close Item List" : "Edit Item List") == 0)
|
||||||
|
WindowManager.Toggle(WindowManager.ChestList);
|
||||||
|
});
|
||||||
ToggleColor("Critters", ref Settings.EspCritters, ref Settings.ColCritters, nameof(Settings.ColCritters));
|
ToggleColor("Critters", ref Settings.EspCritters, ref Settings.ColCritters, nameof(Settings.ColCritters));
|
||||||
ToggleColor("Friendly projectiles", ref Settings.EspProjFriendly, ref Settings.ColProjFriendly, nameof(Settings.ColProjFriendly));
|
ToggleColor("Friendly projectiles", ref Settings.EspProjFriendly, ref Settings.ColProjFriendly, nameof(Settings.ColProjFriendly));
|
||||||
ToggleColor("Ground items", ref Settings.EspItems, ref Settings.ColItems, nameof(Settings.ColItems));
|
ToggleColor("Ground items", ref Settings.EspItems, ref Settings.ColItems, nameof(Settings.ColItems));
|
||||||
@@ -173,6 +211,23 @@ public sealed partial class MainWindow
|
|||||||
Toggle("Night Owl", ref Settings.NightOwl);
|
Toggle("Night Owl", ref Settings.NightOwl);
|
||||||
Toggle("Shine", ref Settings.Shine);
|
Toggle("Shine", ref Settings.Shine);
|
||||||
Toggle("Spelunker", ref Settings.Spelunker);
|
Toggle("Spelunker", ref Settings.Spelunker);
|
||||||
|
|
||||||
|
Group("Damage Numbers");
|
||||||
|
Note("Colors your hits by damage class. Crits are lighter.");
|
||||||
|
ToggleColor("Magic", ref Settings.DamageMagic, ref Settings.ColDamageMagic, nameof(Settings.ColDamageMagic));
|
||||||
|
ToggleColor("Melee", ref Settings.DamageMelee, ref Settings.ColDamageMelee, nameof(Settings.ColDamageMelee));
|
||||||
|
ToggleColor("Ranged", ref Settings.DamageRanged, ref Settings.ColDamageRanged, nameof(Settings.ColDamageRanged));
|
||||||
|
ToggleColor("Summon", ref Settings.DamageSummon, ref Settings.ColDamageSummon, nameof(Settings.ColDamageSummon));
|
||||||
|
|
||||||
|
Group("View");
|
||||||
|
Toggle("Full Bright", ref Settings.FullBright);
|
||||||
|
if (Toggle("Extended Zoom", ref Settings.ExtendedZoom))
|
||||||
|
Sub(() =>
|
||||||
|
{
|
||||||
|
float zoom = Main.GameZoomTarget;
|
||||||
|
Slider("Zoom", ref zoom, ZoomPatch.MinZoom, ZoomPatch.MaxZoom, 1f, "{0:0.00}x");
|
||||||
|
Main.GameZoomTarget = zoom;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void WorldPage()
|
void WorldPage()
|
||||||
@@ -228,6 +283,18 @@ public sealed partial class MainWindow
|
|||||||
if (WorldFeatures.InvasionName is { } active && Buttons($"Stop {active}") == 0)
|
if (WorldFeatures.InvasionName is { } active && Buttons($"Stop {active}") == 0)
|
||||||
WorldFeatures.StopInvasion();
|
WorldFeatures.StopInvasion();
|
||||||
|
|
||||||
|
Group("Map");
|
||||||
|
Toggle("Map Teleport", ref Settings.MapTeleport);
|
||||||
|
if (Buttons(MapFeatures.Revealing ? $"Revealing... {MapFeatures.RevealProgress:0%}" : "Reveal Map") == 0 && !MapFeatures.Revealing)
|
||||||
|
MapFeatures.StartReveal();
|
||||||
|
|
||||||
|
Group("Spawning");
|
||||||
|
Note(NpcSpawner.Multiplayer ? "Multiplayer: boss summons only, spawn rate is ignored." : "Spawn rate only affects single player.");
|
||||||
|
if (Toggle("Spawn Rate", ref Settings.SpawnRateBoost))
|
||||||
|
Sub(() => Slider("Multiplier", ref Settings.SpawnRateMult, 0f, 20f, 5f, "x{0:0.0}", v => v < 0.05f ? "No spawns" : null));
|
||||||
|
if (Buttons(WindowManager.Npcs.Open ? "Close NPC Spawner" : "NPC Spawner") == 0)
|
||||||
|
WindowManager.Toggle(WindowManager.Npcs);
|
||||||
|
|
||||||
Group("Waypoints");
|
Group("Waypoints");
|
||||||
Note("Saved per world.");
|
Note("Saved per world.");
|
||||||
if (Buttons("Save Current Position") == 0)
|
if (Buttons("Save Current Position") == 0)
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ public sealed partial class MainWindow
|
|||||||
["Infinite Flight"] = "Unlimited wing and rocket boot flight time.",
|
["Infinite Flight"] = "Unlimited wing and rocket boot flight time.",
|
||||||
["Auto Heal"] = "Uses Quick Heal below the set health level. Waits for potion sickness to end.",
|
["Auto Heal"] = "Uses Quick Heal below the set health level. Waits for potion sickness to end.",
|
||||||
["Auto Mana"] = "Uses Quick Mana below the set mana level.",
|
["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.",
|
["Fly"] = "Move through blocks with your movement keys. Up or Jump rises; Down descends.",
|
||||||
["Gravity Flip"] = "Walk on the ceiling.",
|
["Gravity Flip"] = "Walk on the ceiling.",
|
||||||
["Lava Immune"] = "Also protects against hot blocks.",
|
["Lava Immune"] = "Also protects against hot blocks.",
|
||||||
@@ -26,6 +31,8 @@ public sealed partial class MainWindow
|
|||||||
["Prediction"] = "Leads moving enemies and accounts for projectile drop.",
|
["Prediction"] = "Leads moving enemies and accounts for projectile drop.",
|
||||||
["Exact check"] = "Checks and corrects aim before firing. Auto Attack skips known misses. Orange brackets mean an unconfirmed shot.",
|
["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.",
|
||||||
|
["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.",
|
||||||
@@ -34,6 +41,10 @@ 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.",
|
||||||
|
["Auto Sell"] = "Turns listed items in your inventory into coins. Items worth nothing are deleted.",
|
||||||
|
["Item Editor"] = "Change the prefix, stats and stack of the held or cursor item.",
|
||||||
["Auto Fishing"] = "Reels in on a bite while you hold a fishing pole.",
|
["Auto Fishing"] = "Reels in on a bite while you hold a fishing pole.",
|
||||||
["Recast"] = "Casts again where you last cast by hand.",
|
["Recast"] = "Casts again where you last cast by hand.",
|
||||||
["Catch enemies"] = "Reels in enemy catches too.",
|
["Catch enemies"] = "Reels in enemy catches too.",
|
||||||
@@ -44,6 +55,7 @@ public sealed partial class MainWindow
|
|||||||
["Distance"] = "Shown in feet. 1 tile = 2 ft.",
|
["Distance"] = "Shown in feet. 1 tile = 2 ft.",
|
||||||
["Tracers"] = "Draws lines from you to each target.",
|
["Tracers"] = "Draws lines from you to each target.",
|
||||||
["Projectile paths"] = "Shows projectile paths until they hit a block or expire.",
|
["Projectile paths"] = "Shows projectile paths until they hit a block or expire.",
|
||||||
|
["Boss cursor"] = "Shows off-screen bosses at the screen edge with their map head.",
|
||||||
["Bosses (Rainbow)"] = "Cycles boss colors. Turn off to use the chosen color.",
|
["Bosses (Rainbow)"] = "Cycles boss colors. Turn off to use the chosen color.",
|
||||||
["Spelunker"] = "Makes ores, gems and treasure glow.",
|
["Spelunker"] = "Makes ores, gems and treasure glow.",
|
||||||
["Hunter"] = "Makes enemies glow.",
|
["Hunter"] = "Makes enemies glow.",
|
||||||
@@ -51,6 +63,12 @@ public sealed partial class MainWindow
|
|||||||
["Night Owl"] = "Brightens dark areas.",
|
["Night Owl"] = "Brightens dark areas.",
|
||||||
["Danger Sense"] = "Makes traps and hazards glow.",
|
["Danger Sense"] = "Makes traps and hazards glow.",
|
||||||
["Biome Sight"] = "Makes Corruption, Crimson and Hallow blocks glow.",
|
["Biome Sight"] = "Makes Corruption, Crimson and Hallow blocks glow.",
|
||||||
|
["Full Bright"] = "Lights everything at full brightness.",
|
||||||
|
["Extended Zoom"] = "Zoom in up to 8x and out until the edge of what the game draws.",
|
||||||
|
["Chests"] = "Marks chests. Their contents can be filtered with an item list.",
|
||||||
|
["Only with listed items"] = "Only marks chests holding an item from the list.",
|
||||||
|
["Map Teleport"] = "Right-click the fullscreen map to teleport there.",
|
||||||
|
["Reveal Map"] = "Reveals the whole map. Multiplayer only reveals areas the server has sent.",
|
||||||
["Full Heal"] = "Refills health and mana.",
|
["Full Heal"] = "Refills health and mana.",
|
||||||
["TP to Spawn"] = "Recalls like a Magic Mirror: to your bed if set, otherwise the world's spawn point.",
|
["TP to Spawn"] = "Recalls like a Magic Mirror: to your bed if set, otherwise the world's spawn point.",
|
||||||
["Reset Learned Ballistics"] = "Clears learned projectile data. Learns it again as you shoot.",
|
["Reset Learned Ballistics"] = "Clears learned projectile data. Learns it again as you shoot.",
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ 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)),
|
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)),
|
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.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)),
|
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)),
|
new Page("World", WorldPage, () => Count(Settings.FreezeTime, Settings.SpawnRateBoost, Settings.MapTeleport)),
|
||||||
new Page("Keys", KeysPage, () => 0),
|
new Page("Keys", KeysPage, () => 0),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using Terraria;
|
||||||
|
using Terraria.ID;
|
||||||
|
#if TMODLOADER
|
||||||
|
using Terraria.ModLoader;
|
||||||
|
#endif
|
||||||
|
using TerraSharp.Features;
|
||||||
|
|
||||||
|
namespace TerraSharp.UI;
|
||||||
|
|
||||||
|
public sealed class NpcBrowser() : GridBrowser("npcs", "NPC Spawner", new Vector2(660, 140), 532, 554, "Search NPCs by name or ID", "NPCs", new IdIndex(() => NpcCount, Lang.GetNPCNameValue), Categories, Sorts)
|
||||||
|
{
|
||||||
|
const int Batch = 10;
|
||||||
|
|
||||||
|
#if TMODLOADER
|
||||||
|
static int NpcCount => NPCLoader.NPCCount;
|
||||||
|
#else
|
||||||
|
static int NpcCount => NPCID.Count;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static readonly BrowserCategory[] Categories = [new("All", NpcSpawner.Available), new("Bosses", type => NpcSpawner.Available(type) && IsBoss(type)), new("Enemies", type => NpcSpawner.Available(type) && Sample(type) is { friendly: false, townNPC: false } && !IsBoss(type) && !IsCritter(type)), new("Town", type => NpcSpawner.Available(type) && Sample(type) is { townNPC: true }), new("Critters", type => NpcSpawner.Available(type) && IsCritter(type))];
|
||||||
|
|
||||||
|
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 };
|
||||||
|
|
||||||
|
static bool IsCritter(int type) => NPCID.Sets.CountsAsCritter[type] || Main.npcCatchable[type];
|
||||||
|
|
||||||
|
protected override void DrawContent(Rectangle body)
|
||||||
|
{
|
||||||
|
if (_multiplayer != NpcSpawner.Multiplayer)
|
||||||
|
{
|
||||||
|
_multiplayer = NpcSpawner.Multiplayer;
|
||||||
|
Index.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawSearchGrid(body, body.Y, body.Bottom - 30);
|
||||||
|
string hint = _multiplayer ? "Multiplayer: only boss summons the server accepts" : $"Click: spawn in front of you Shift click: spawn {Batch}";
|
||||||
|
Render.TextMid(Widgets.SpriteBatch, hint, new Rectangle(body.X + 4, body.Bottom - 26, body.Width, 26), Widgets.Dim, 0.72f);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DrawSlot(int type, Rectangle bounds, bool hot)
|
||||||
|
{
|
||||||
|
Widgets.Slot(bounds, hot);
|
||||||
|
Main.instance.LoadNPC(type);
|
||||||
|
if (GameTextures.Get("Npc", type) is { } texture)
|
||||||
|
Widgets.Icon(texture, new Rectangle(bounds.X + 4, bounds.Y + 4, bounds.Width - 8, bounds.Height - 8), GameTextures.Frame(texture, 1, Main.npcFrameCount[type], 0, 0));
|
||||||
|
if (!hot)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string action = _multiplayer ? "Click to summon" : "Click to spawn";
|
||||||
|
Widgets.Tooltip = $"{Index.Name(type)}\n{(IsBoss(type) ? "Boss" : "NPC")} #{type} {action}";
|
||||||
|
if (!Widgets.Click)
|
||||||
|
return;
|
||||||
|
|
||||||
|
NpcSpawner.Spawn(type, Widgets.Shift ? Batch : 1);
|
||||||
|
Widgets.Sound(12);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -127,7 +127,7 @@ public static class Render
|
|||||||
return text + "...";
|
return text + "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Item(SpriteBatch spriteBatch, int type, Rectangle bounds)
|
public static void Item(SpriteBatch spriteBatch, int type, Rectangle bounds, Color? color = null)
|
||||||
{
|
{
|
||||||
if (type <= 0)
|
if (type <= 0)
|
||||||
return;
|
return;
|
||||||
@@ -139,8 +139,10 @@ public static class Render
|
|||||||
|
|
||||||
float scale = Math.Min(1f, Math.Min(bounds.Width / (float)frame.Width, bounds.Height / (float)frame.Height));
|
float scale = Math.Min(1f, Math.Min(bounds.Width / (float)frame.Width, bounds.Height / (float)frame.Height));
|
||||||
Vector2 size = new Vector2(frame.Width, frame.Height) * scale;
|
Vector2 size = new Vector2(frame.Width, frame.Height) * scale;
|
||||||
spriteBatch.Draw(texture, new Vector2(bounds.X + (bounds.Width - size.X) / 2f, bounds.Y + (bounds.Height - size.Y) / 2f), frame, Color.White, 0f, Vector2.Zero, scale, SpriteEffects.None, 0f);
|
spriteBatch.Draw(texture, new Vector2(bounds.X + (bounds.Width - size.X) / 2f, bounds.Y + (bounds.Height - size.Y) / 2f), frame, color ?? Color.White, 0f, Vector2.Zero, scale, SpriteEffects.None, 0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static 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);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Terraria;
|
||||||
|
using TerraSharp.Core;
|
||||||
|
using TerraSharp.Features;
|
||||||
|
|
||||||
|
namespace TerraSharp.UI;
|
||||||
|
|
||||||
|
public sealed class StatsConfig() : Window("statsConfig", "Stats Overlay", Vector2.Zero, 300, 640)
|
||||||
|
{
|
||||||
|
const int RowHeight = 30;
|
||||||
|
|
||||||
|
public override bool Popup => true;
|
||||||
|
|
||||||
|
public void ShowBeside(Rectangle anchor)
|
||||||
|
{
|
||||||
|
Pos = new Vector2(anchor.Right + 8, anchor.Y);
|
||||||
|
WindowManager.Show(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void DrawBody(Rectangle body)
|
||||||
|
{
|
||||||
|
int y = body.Y;
|
||||||
|
Render.TextMid(Widgets.SpriteBatch, "Drag the overlay while the menu is open.", new Rectangle(body.X + 2, y, body.Width, 22), Widgets.Dim * 0.8f, 0.7f);
|
||||||
|
y += 26;
|
||||||
|
|
||||||
|
Settings.StatIcons = Widgets.ToggleRow(new Rectangle(body.X, y, body.Width, 28), "Icons", Settings.StatIcons);
|
||||||
|
y += RowHeight;
|
||||||
|
Widgets.Separator(body.X, y + 2, body.Width);
|
||||||
|
y += 10;
|
||||||
|
|
||||||
|
foreach ((StatLine line, string label, _) in StatsWindow.Lines)
|
||||||
|
{
|
||||||
|
bool shown = Widgets.ToggleRow(new Rectangle(body.X, y, body.Width, 28), label, (Settings.StatLines & line) != 0);
|
||||||
|
Settings.StatLines = shown ? Settings.StatLines | line : Settings.StatLines & ~line;
|
||||||
|
y += RowHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Widgets.Button(new Rectangle(body.X, body.Bottom - 32, body.Width, 32), "Reset Stats", false, 0.82f))
|
||||||
|
Stats.Reset();
|
||||||
|
if (Widgets.Hot(new Rectangle(body.X, body.Bottom - 32, body.Width, 32)))
|
||||||
|
Widgets.Tooltip = "Clears damage, kills, deaths and session time.";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Terraria;
|
||||||
|
using Terraria.ID;
|
||||||
|
using TerraSharp.Core;
|
||||||
|
using TerraSharp.Features;
|
||||||
|
|
||||||
|
namespace TerraSharp.UI;
|
||||||
|
|
||||||
|
// Overlay without a frame; it only takes the mouse, to be dragged, while the menu is open.
|
||||||
|
public sealed class StatsWindow() : Window("stats", "", new Vector2(20, 280), PanelWidth, RowHeight)
|
||||||
|
{
|
||||||
|
const int PanelWidth = 240, RowHeight = 24, IconSize = 20;
|
||||||
|
const float TextScale = 0.8f;
|
||||||
|
|
||||||
|
// Main.DrawInfoAccs converts velocity to the stopwatch's miles per hour this way.
|
||||||
|
const float MilesPerHour = 216000f / 42240f;
|
||||||
|
|
||||||
|
// Icons are the accessories and items that show or affect each stat in vanilla.
|
||||||
|
public static readonly (StatLine Line, string Label, int Icon)[] Lines =
|
||||||
|
[
|
||||||
|
(StatLine.Dps, "DPS", ItemID.DPSMeter),
|
||||||
|
(StatLine.PeakDps, "Peak DPS", ItemID.DestroyerEmblem),
|
||||||
|
(StatLine.Damage, "Damage dealt", ItemID.WarriorEmblem),
|
||||||
|
(StatLine.BestHit, "Best hit", ItemID.AvengerEmblem),
|
||||||
|
(StatLine.Kills, "Kills", ItemID.TallyCounter),
|
||||||
|
(StatLine.Time, "Session time", ItemID.GoldWatch),
|
||||||
|
(StatLine.Deaths, "Deaths", ItemID.Tombstone),
|
||||||
|
(StatLine.Minions, "Minions", ItemID.SlimeStaff),
|
||||||
|
(StatLine.Sentries, "Sentries", ItemID.QueenSpiderStaff),
|
||||||
|
(StatLine.Defense, "Defense", ItemID.CobaltShield),
|
||||||
|
(StatLine.DamageReduction, "Damage reduction", ItemID.WormScarf),
|
||||||
|
(StatLine.Crit, "Critical chance", ItemID.EyeoftheGolem),
|
||||||
|
(StatLine.LifeRegen, "Life regen", ItemID.BandofRegeneration),
|
||||||
|
(StatLine.Speed, "Speed", ItemID.Stopwatch),
|
||||||
|
(StatLine.Luck, "Luck", ItemID.LuckPotion),
|
||||||
|
];
|
||||||
|
|
||||||
|
public override bool Open { get => Settings.ShowStats; set => Settings.ShowStats = value; }
|
||||||
|
|
||||||
|
public override bool Frameless => true;
|
||||||
|
|
||||||
|
public override bool Interactive => WindowManager.Menu.Open && !Main.gameMenu;
|
||||||
|
|
||||||
|
public override void DrawBody(Rectangle body)
|
||||||
|
{
|
||||||
|
if (Main.gameMenu)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Interactive)
|
||||||
|
Render.Outline(Widgets.SpriteBatch, body, Widgets.Hot(body) ? Widgets.Yellow * 0.6f : Color.White * 0.2f);
|
||||||
|
|
||||||
|
Player player = Main.LocalPlayer;
|
||||||
|
int y = body.Y, shown = 0;
|
||||||
|
foreach ((StatLine line, string label, int icon) in Lines)
|
||||||
|
{
|
||||||
|
if ((Settings.StatLines & line) == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Row(new Rectangle(body.X, y, body.Width, RowHeight), label, Value(line, player), Settings.StatIcons ? icon : null);
|
||||||
|
y += RowHeight;
|
||||||
|
shown++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Height = Math.Max(1, shown) * RowHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Value(StatLine line, Player player) => line switch
|
||||||
|
{
|
||||||
|
StatLine.Dps => $"{Stats.Dps:0}",
|
||||||
|
StatLine.PeakDps => $"{Stats.PeakDps:0}",
|
||||||
|
StatLine.Damage => $"{Stats.Damage:N0}",
|
||||||
|
StatLine.BestHit => $"{Stats.BestHit:N0}",
|
||||||
|
StatLine.Kills => $"{Stats.Kills:N0}",
|
||||||
|
StatLine.Time => SessionTime(),
|
||||||
|
StatLine.Deaths => $"{Stats.Deaths}",
|
||||||
|
StatLine.Minions => $"{player.slotsMinions:0.#} / {player.maxMinions}",
|
||||||
|
StatLine.Sentries => $"{Sentries(player)} / {player.maxTurrets}",
|
||||||
|
StatLine.Defense => $"{(int)player.statDefense}",
|
||||||
|
StatLine.DamageReduction => $"{player.endurance:0%}",
|
||||||
|
StatLine.Crit => player.HeldItem.damage > 0 ? $"{player.GetWeaponCrit(player.HeldItem)}%" : "-",
|
||||||
|
StatLine.LifeRegen => $"{player.lifeRegen / 2f:0.#}/s",
|
||||||
|
StatLine.Speed => $"{player.velocity.Length() * MilesPerHour:0} mph",
|
||||||
|
StatLine.Luck => $"{player.luck:0.00}",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
|
||||||
|
static string SessionTime()
|
||||||
|
{
|
||||||
|
TimeSpan time = TimeSpan.FromSeconds(Stats.Ticks / 60);
|
||||||
|
return $"{(int)time.TotalHours}:{time.Minutes:00}:{time.Seconds:00}";
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Sentries(Player player)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < Main.maxProjectiles; i++)
|
||||||
|
if (Main.projectile[i] is { active: true, sentry: true } projectile && projectile.owner == player.whoAmI)
|
||||||
|
count++;
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Row(Rectangle row, string label, string value, int? icon)
|
||||||
|
{
|
||||||
|
int textX = row.X;
|
||||||
|
if (icon is { } type)
|
||||||
|
{
|
||||||
|
Render.Item(Widgets.SpriteBatch, type, new Rectangle(row.X, row.Y + (row.Height - IconSize) / 2, IconSize, IconSize));
|
||||||
|
textX += IconSize + 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
Render.TextMid(Widgets.SpriteBatch, label, new Rectangle(textX, row.Y, row.Right - textX, row.Height), Widgets.Dim, TextScale);
|
||||||
|
Render.TextRight(Widgets.SpriteBatch, value, row, Color.White, TextScale);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -227,6 +227,20 @@ public static class Widgets
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool ToggleRow(Rectangle row, string label, bool value)
|
||||||
|
{
|
||||||
|
bool hot = Hot(row);
|
||||||
|
if (hot && Click)
|
||||||
|
{
|
||||||
|
value = !value;
|
||||||
|
Sound(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
Render.TextMid(SpriteBatch, label, row, hot ? Yellow : value ? On : Color.White, 0.85f);
|
||||||
|
ToggleIcon(new Rectangle(row.Right - 28, row.Y + 2, 24, 24), value, hot);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
public static void ToggleIcon(Rectangle bounds, bool on, bool hot)
|
public static void ToggleIcon(Rectangle bounds, bool on, bool hot)
|
||||||
{
|
{
|
||||||
Render.TextRight(SpriteBatch, on ? "On" : "Off", new Rectangle(bounds.X - 44, bounds.Y, 38, bounds.Height), on ? On : Dim * 0.75f, 0.78f);
|
Render.TextRight(SpriteBatch, on ? "On" : "Off", new Rectangle(bounds.X - 44, bounds.Y, 38, bounds.Height), on ? On : Dim * 0.75f, 0.78f);
|
||||||
@@ -247,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);
|
||||||
|
|||||||
@@ -6,15 +6,27 @@ 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;
|
||||||
|
|
||||||
public int Width { get; } = width;
|
public int Width { get; } = width;
|
||||||
|
|
||||||
public int Height { get; } = height;
|
public int Height { get; protected set; } = height;
|
||||||
|
|
||||||
public bool Open { get; set; }
|
public virtual bool Open { get; set; }
|
||||||
|
|
||||||
|
// Draws only the body: no panel, title or close button.
|
||||||
|
public virtual bool Frameless => false;
|
||||||
|
|
||||||
|
// Closes on a click outside it or when the menu closes.
|
||||||
|
public virtual bool Popup => false;
|
||||||
|
|
||||||
|
// A window that is not interactive lets the mouse through to the game.
|
||||||
|
public virtual bool Interactive => true;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using Terraria;
|
using Terraria;
|
||||||
@@ -13,11 +14,20 @@ public static class WindowManager
|
|||||||
public static readonly MainWindow Menu = new();
|
public static readonly MainWindow Menu = new();
|
||||||
public static readonly ItemBrowser Items = new();
|
public static readonly ItemBrowser Items = new();
|
||||||
public static readonly BuffBrowser Buffs = new();
|
public static readonly BuffBrowser Buffs = new();
|
||||||
|
public static readonly NpcBrowser Npcs = new();
|
||||||
|
public static readonly ItemEditor Editor = new();
|
||||||
|
public static readonly ItemPicker SellList = new("sellList", "Auto Sell List", new Vector2(680, 130), Settings.SellList);
|
||||||
|
public static readonly ItemPicker ChestList = new("chestList", "Chest Finder Items", new Vector2(700, 140), Settings.ChestList);
|
||||||
|
public static readonly StatsWindow Stats = new();
|
||||||
|
public static readonly StatsConfig StatsSetup = new();
|
||||||
|
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, Menu];
|
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;
|
||||||
|
|
||||||
@@ -25,11 +35,11 @@ public static class WindowManager
|
|||||||
|
|
||||||
public static bool MouseOver { get; private set; }
|
public static bool MouseOver { get; private set; }
|
||||||
|
|
||||||
public static bool AnyOpen => Stack.Exists(w => w.Open);
|
public static bool AnyOpen => Stack.Exists(w => w.Open && w.Interactive);
|
||||||
|
|
||||||
public static IEnumerable<Window> All => Stack;
|
public static IEnumerable<Window> All => Stack;
|
||||||
|
|
||||||
public static bool Contains(Point point) => Stack.Exists(w => w.Open && w.Bounds.Contains(point));
|
public static bool Contains(Point point) => Stack.Exists(w => w.Open && w.Interactive && w.Bounds.Contains(point));
|
||||||
|
|
||||||
public static void Show(Window window)
|
public static void Show(Window window)
|
||||||
{
|
{
|
||||||
@@ -51,6 +61,10 @@ public static class WindowManager
|
|||||||
window.Open = false;
|
window.Open = false;
|
||||||
Widgets.Sound(11);
|
Widgets.Sound(11);
|
||||||
Unfocus();
|
Unfocus();
|
||||||
|
if (window == Menu)
|
||||||
|
foreach (Window popup in Stack)
|
||||||
|
if (popup.Popup)
|
||||||
|
popup.Open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Toggle(Window window)
|
public static void Toggle(Window window)
|
||||||
@@ -107,7 +121,8 @@ public static class WindowManager
|
|||||||
Unfocus();
|
Unfocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Vector2 ClampPosition(Window window, Vector2 position) => new(MathHelper.Clamp(position.X, 0f, Math.Max(0f, Main.screenWidth / Main.UIScale - window.Width)), MathHelper.Clamp(position.Y, 0f, Math.Max(0f, Main.screenHeight / Main.UIScale - window.Height)));
|
// PlayerInput.SetZoom_UI already divides Main.screenWidth/Height by the UI scale inside UI layers.
|
||||||
|
static Vector2 ClampPosition(Window window, Vector2 position) => new(MathHelper.Clamp(position.X, 0f, Math.Max(0f, PlayerInput.RealScreenWidth / Main.UIScale - window.Width)), MathHelper.Clamp(position.Y, 0f, Math.Max(0f, PlayerInput.RealScreenHeight / Main.UIScale - window.Height)));
|
||||||
|
|
||||||
public static void Draw(SpriteBatch spriteBatch)
|
public static void Draw(SpriteBatch spriteBatch)
|
||||||
{
|
{
|
||||||
@@ -117,8 +132,10 @@ public static class WindowManager
|
|||||||
foreach (Window window in Stack)
|
foreach (Window window in Stack)
|
||||||
window.Pos = ClampPosition(window, window.Pos);
|
window.Pos = ClampPosition(window, window.Pos);
|
||||||
|
|
||||||
Window? top = Stack.FindLast(w => w.Open && w.Bounds.Contains(Widgets.Mouse));
|
Window? top = Stack.FindLast(w => w.Open && w.Interactive && w.Bounds.Contains(Widgets.Mouse));
|
||||||
MouseOver = Widgets.InputOk && top is not null;
|
MouseOver = Widgets.InputOk && top is not null;
|
||||||
|
// Popups the click landed outside of; a button that toggles one this frame still works.
|
||||||
|
Window[] dismissed = Widgets.Click ? [.. Stack.Where(w => w.Open && w.Popup && w != top)] : [];
|
||||||
if (Widgets.Click)
|
if (Widgets.Click)
|
||||||
if (top is null)
|
if (top is null)
|
||||||
Unfocus();
|
Unfocus();
|
||||||
@@ -143,6 +160,8 @@ public static class WindowManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widgets.Blocked = false;
|
Widgets.Blocked = false;
|
||||||
|
foreach (Window popup in dismissed)
|
||||||
|
popup.Open = false;
|
||||||
|
|
||||||
if (Widgets.Tooltip is not null && MouseOver)
|
if (Widgets.Tooltip is not null && MouseOver)
|
||||||
Main.instance.MouseText(Widgets.Tooltip, 0, 0, -1, -1, -1, -1, 0);
|
Main.instance.MouseText(Widgets.Tooltip, 0, 0, -1, -1, -1, -1, 0);
|
||||||
@@ -151,6 +170,21 @@ public static class WindowManager
|
|||||||
static void DrawFrame(Window window)
|
static void DrawFrame(Window window)
|
||||||
{
|
{
|
||||||
Rectangle bounds = window.Bounds;
|
Rectangle bounds = window.Bounds;
|
||||||
|
if (window.Frameless)
|
||||||
|
{
|
||||||
|
bool hot = Widgets.Hot(bounds) && window.Draggable;
|
||||||
|
if (hot)
|
||||||
|
Widgets.Tooltip = "Drag to move";
|
||||||
|
if (hot && Widgets.Click)
|
||||||
|
{
|
||||||
|
_dragging = window;
|
||||||
|
_dragOffset = window.Pos - new Vector2(Widgets.Mouse.X, Widgets.Mouse.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.DrawBody(bounds);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Render.Fill(Widgets.SpriteBatch, new Rectangle(bounds.X + 4, bounds.Y + 4, bounds.Width - 8, bounds.Height - 8), new Color(16, 20, 40) * 0.92f);
|
Render.Fill(Widgets.SpriteBatch, new Rectangle(bounds.X + 4, bounds.Y + 4, bounds.Width - 8, bounds.Height - 8), new Color(16, 20, 40) * 0.92f);
|
||||||
Widgets.Panel(bounds, Widgets.WindowBg);
|
Widgets.Panel(bounds, Widgets.WindowBg);
|
||||||
|
|
||||||
@@ -160,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,51 @@ public static class Esp
|
|||||||
DrawNpcs(spriteBatch);
|
DrawNpcs(spriteBatch);
|
||||||
if (Settings.EspItems)
|
if (Settings.EspItems)
|
||||||
DrawItems(spriteBatch);
|
DrawItems(spriteBatch);
|
||||||
|
if (Settings.EspChests)
|
||||||
|
DrawChests(spriteBatch);
|
||||||
if (Settings.EspWaypoints)
|
if (Settings.EspWaypoints)
|
||||||
foreach (Waypoint waypoint in Waypoints.Current)
|
foreach (Waypoint waypoint in Waypoints.Current)
|
||||||
Box(spriteBatch, waypoint.Position, _player.width, _player.height, Premultiplied(Settings.ColWaypoint), waypoint.Name, -1, -1, true, true);
|
Box(spriteBatch, waypoint.Position, _player.width, _player.height, Premultiplied(Settings.ColWaypoint), waypoint.Name, -1, -1, true, true);
|
||||||
|
if (Settings.EspBossCursor)
|
||||||
|
DrawBossCursors(spriteBatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Off-screen bosses get their map head and an arrow at the screen edge.
|
||||||
|
static void DrawBossCursors(SpriteBatch spriteBatch)
|
||||||
|
{
|
||||||
|
const float Margin = 40f;
|
||||||
|
var center = new Vector2(Main.screenWidth / 2f, Main.screenHeight / 2f);
|
||||||
|
Color color = Premultiplied(Settings.EspBossRainbow ? Rainbow() : Settings.ColBoss);
|
||||||
|
for (int i = 0; i < Main.maxNPCs; i++)
|
||||||
|
{
|
||||||
|
NPC npc = Main.npc[i];
|
||||||
|
if (!npc.active)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int head = npc.GetBossHeadTextureIndex();
|
||||||
|
if (head < 0 && !npc.boss)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Vector2 topLeft = Render.ToScreen(npc.position), bottomRight = Render.ToScreen(npc.position + new Vector2(npc.width, npc.height));
|
||||||
|
if (bottomRight.X >= 0 && bottomRight.Y >= 0 && topLeft.X <= Main.screenWidth && topLeft.Y <= Main.screenHeight)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Vector2 offset = (topLeft + bottomRight) / 2f - center;
|
||||||
|
float scale = Math.Min((center.X - Margin) / Math.Abs(offset.X), (center.Y - Margin) / Math.Abs(offset.Y));
|
||||||
|
Vector2 position = center + offset * scale, direction = Vector2.Normalize(offset), side = new(-direction.Y, direction.X);
|
||||||
|
|
||||||
|
Vector2 tip = position + direction * 28f, back = tip - direction * 10f;
|
||||||
|
Render.Line(spriteBatch, back + side * 8f, tip, color, 2f);
|
||||||
|
Render.Line(spriteBatch, back - side * 8f, tip, color, 2f);
|
||||||
|
|
||||||
|
if (GameTextures.Get("NpcHeadBoss", head) is { } texture)
|
||||||
|
spriteBatch.Draw(texture, position, null, Color.White, npc.GetBossHeadRotation(), new Vector2(texture.Width, texture.Height) / 2f, 1f, npc.GetBossHeadSpriteEffects(), 0f);
|
||||||
|
else
|
||||||
|
Render.Fill(spriteBatch, new Rectangle((int)position.X - 6, (int)position.Y - 6, 12, 12), color);
|
||||||
|
|
||||||
|
if (Settings.EspDistance)
|
||||||
|
Render.TextCenteredAt(spriteBatch, $"{(int)(Vector2.Distance(npc.Center, _player.Center) / 8f)} ft", position.X, position.Y + 18f, color, 0.7f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void DrawProjectiles(SpriteBatch spriteBatch)
|
static void DrawProjectiles(SpriteBatch spriteBatch)
|
||||||
@@ -116,6 +158,44 @@ public static class Esp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multiplayer clients only know the contents of chests they have opened.
|
||||||
|
static void DrawChests(SpriteBatch spriteBatch)
|
||||||
|
{
|
||||||
|
Color color = Premultiplied(Settings.ColChests);
|
||||||
|
for (int i = 0; i < Main.maxChests; i++)
|
||||||
|
{
|
||||||
|
Chest? chest = Main.chest[i];
|
||||||
|
if (chest is not null && ChestLabel(chest) is { } label)
|
||||||
|
Box(spriteBatch, new Vector2(chest.x * 16, chest.y * 16), 32, 32, color, label, -1, -1, true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static string? ChestLabel(Chest chest)
|
||||||
|
{
|
||||||
|
string name = string.IsNullOrEmpty(chest.name) ? "Chest" : chest.name;
|
||||||
|
if (!Settings.EspChestsListed)
|
||||||
|
return name;
|
||||||
|
if (chest.item is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
Item? first = null;
|
||||||
|
int matches = 0;
|
||||||
|
foreach (Item? item in chest.item)
|
||||||
|
{
|
||||||
|
if (item is null || item.type == 0 || !Settings.ChestList.Contains(item.type))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
first ??= item;
|
||||||
|
matches++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
string found = first.stack > 1 ? $"{first.Name} x{first.stack}" : first.Name;
|
||||||
|
return matches > 1 ? $"{name}: {found} +{matches - 1}" : $"{name}: {found}";
|
||||||
|
}
|
||||||
|
|
||||||
// Anchors and scale match Main.DrawInterface_14_EntityHealthBars.
|
// Anchors and scale match Main.DrawInterface_14_EntityHealthBars.
|
||||||
static (Vector2 Position, float Scale) NpcBar(NPC npc)
|
static (Vector2 Position, float Scale) NpcBar(NPC npc)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user