feat: Auto dodge uses dashes and teleport rods

This commit is contained in:
VAC committed 2026-10-16 01:58:27 -03:00
1 parent 4e95719c2f
commit 9de8112ce9
2 files changed
+146 -20

No files matched your search

+145 -19
View File
@@ -2,12 +2,13 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Terraria; using Terraria;
using Terraria.ID;
using TerraSharp.Core; using TerraSharp.Core;
using TerraSharp.Simulation; using TerraSharp.Simulation;
namespace TerraSharp.Features.Combat; namespace TerraSharp.Features.Combat;
// Steers the local player out of predicted projectile and contact hits. // Steers the local player out of predicted projectile and contact hits, using dashes and teleport rods when running and jumping are not enough.
public static class AutoDodge public static class AutoDodge
{ {
const int Horizon = 24; const int Horizon = 24;
@@ -19,23 +20,35 @@ public static class AutoDodge
const int JumpCooldown = 12; const int JumpCooldown = 12;
const float Gravity = 0.4f, MaxFall = 10f, FastFall = 8f; 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. // Horizontal speed away from the player that counts as being knocked back, not drifting.
const float KnockbackSpeed = 1f; const float KnockbackSpeed = 1f;
// Teleport only for hits this close; a later run or jump may still avoid farther ones.
const int TeleportWithin = 8;
const int TeleportCooldown = 30;
static readonly float[] TeleportRadii = [6f, 10f, 14f, 18f];
const int TeleportDirections = 16;
const int TempleWall = 87;
sealed class Threat sealed class Threat
{ {
public readonly Vector2[] Path = new Vector2[Horizon + 1]; public readonly Vector2[] Path = new Vector2[Horizon + 1];
public int Count, Width, Height; public int Count, Width, Height, Damage;
} }
readonly record struct Move(int Horizontal, int Vertical); readonly record struct Move(int Horizontal, int Vertical, bool Dash = false);
static readonly Move Stay = new(0, 0); 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[] Moves = [new(-1, 0), new(1, 0), new(0, -1), new(-1, -1), new(1, -1), new(0, 1), new(-1, 1), new(1, 1)];
static readonly Move[] Dashes = [new(-1, 0, true), new(1, 0, true)];
static readonly Threat[] Pool = new Threat[MaxThreats]; static readonly Threat[] Pool = new Threat[MaxThreats];
static readonly List<Threat> Threats = []; static readonly List<Threat> Threats = [];
static readonly Vector2[] Trail = new Vector2[Horizon + 1]; static readonly Vector2[] Trail = new Vector2[Horizon + 1];
static int _lastJump = -JumpCooldown; static int _lastJump = -JumpCooldown, _lastTeleport = -TeleportCooldown;
static int _hitDamage;
static AutoDodge() static AutoDodge()
{ {
@@ -50,17 +63,24 @@ public static class AutoDodge
// Mirrors the jump conditions in Player.JumpMovement; the game itself performs the jump. // 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))); 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 #if TMODLOADER
static bool HasExtraJump(Player player) => player.AnyExtraJumpUsable(); static bool HasExtraJump(Player player) => player.AnyExtraJumpUsable();
static bool MountFlies(Player player) => player.mount.Active && player.mount.CanFly(); static bool MountFlies(Player player) => player.mount.Active && player.mount.CanFly();
static int Wall(int x, int y) => Main.tile[x, y].WallType;
#else #else
static bool MountFlies(Player player) => player.mount.Active && player.mount.CanFly(player); 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 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 #endif
// Player.HorizontalMovement prefix: JumpMovement and WingMovement run later this tick and read the controls set here. // Player.HorizontalMovement prefix: DashMovement, JumpMovement and WingMovement run later this tick and read the controls set here.
public static void Steer(Player player) public static void Steer(Player player)
{ {
if (!Settings.AutoDodge || Settings.GodMode || player.dead) if (!Settings.AutoDodge || Settings.GodMode || player.dead)
@@ -71,10 +91,11 @@ public static class AutoDodge
return; return;
float speed = Settings.Fly ? Settings.FlySpeed : Math.Max(Math.Max(player.maxRunSpeed, player.accRunSpeed), 3f); float speed = Settings.Fly ? Settings.FlySpeed : Math.Max(Math.Max(player.maxRunSpeed, player.accRunSpeed), 3f);
int bestHit = FirstHit(player, Stay, speed, false); int bestHit = FirstHit(player, null, Stay, speed, false);
if (bestHit > Horizon) if (bestHit > Horizon)
return; return;
int incoming = _hitDamage;
Move best = Stay; Move best = Stay;
bool canRise = CanRise(player), canFall = !Grounded(player); bool canRise = CanRise(player), canFall = !Grounded(player);
foreach (Move move in Moves) foreach (Move move in Moves)
@@ -82,11 +103,22 @@ public static class AutoDodge
if ((move.Vertical < 0 && !canRise) || (move.Vertical > 0 && !canFall)) if ((move.Vertical < 0 && !canRise) || (move.Vertical > 0 && !canFall))
continue; continue;
int hit = FirstHit(player, move, speed, true); int hit = FirstHit(player, null, move, speed, true);
if (hit > bestHit) if (hit > bestHit)
(best, bestHit) = (move, hit); (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) if (best != Stay)
Apply(player, best, speed); Apply(player, best, speed);
} }
@@ -95,6 +127,14 @@ public static class AutoDodge
{ {
player.controlLeft = move.Horizontal < 0; player.controlLeft = move.Horizontal < 0;
player.controlRight = move.Horizontal > 0; player.controlRight = move.Horizontal > 0;
if (move.Dash)
{
// DoCommonDashHandle reads a fresh press with dashTime already set as the second tap of a double tap.
player.dashTime = move.Horizontal * 15;
(player.releaseLeft, player.releaseRight) = (move.Horizontal < 0, move.Horizontal > 0);
return;
}
if (move.Horizontal != 0) if (move.Horizontal != 0)
player.velocity.X = move.Horizontal * speed; player.velocity.X = move.Horizontal * speed;
@@ -113,17 +153,88 @@ public static class AutoDodge
} }
} }
// Player positions for the move: jumps rise at Player.jumpSpeed for Player.jumpHeight ticks, everything else falls. // 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 void Predict(Player player, Move move, float speed) static bool TryTeleport(Player player, float speed, int incoming)
{ {
bool grounded = Grounded(player); 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); bool fly = Settings.Fly || (move.Vertical < 0 && Flying(player) && !grounded);
float horizontal = move.Horizontal != 0 ? move.Horizontal * speed : player.velocity.X; float horizontal = move.Dash ? move.Horizontal * DashSpeed : move.Horizontal != 0 ? move.Horizontal * speed : moving ? player.velocity.X : 0f;
float vertical = player.velocity.Y * player.gravDir; float vertical = moving ? player.velocity.Y * player.gravDir : 0f;
if (move.Vertical > 0) if (move.Vertical > 0)
vertical = Math.Max(vertical, FastFall); vertical = Math.Max(vertical, FastFall);
Vector2 position = player.position; Vector2 position = spot ?? player.position;
Trail[0] = position; Trail[0] = position;
for (int t = 1; t <= Horizon; t++) for (int t = 1; t <= Horizon; t++)
{ {
@@ -131,16 +242,19 @@ public static class AutoDodge
vertical = fly || t <= Player.jumpHeight ? -Player.jumpSpeed : Math.Min(vertical + Gravity, MaxFall); vertical = fly || t <= Player.jumpHeight ? -Player.jumpSpeed : Math.Min(vertical + Gravity, MaxFall);
else if (!grounded || move.Vertical > 0) else if (!grounded || move.Vertical > 0)
vertical = Math.Min(vertical + Gravity, MaxFall); 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); position += new Vector2(horizontal, vertical * player.gravDir);
Trail[t] = position; Trail[t] = position;
} }
} }
// First tick a threat overlaps the player, or Horizon + 1 when none do; immunity frames count as safe. // 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, Move move, float speed, bool checkWalls) static int FirstHit(Player player, Vector2? spot, Move move, float speed, bool checkWalls)
{ {
Predict(player, move, speed); _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)) if (checkWalls && TileCollision.Sweep(player.Center, Trail[WallCheckTicks] + new Vector2(player.width / 2f, player.height / 2f), player.width, player.height))
return -1; return -1;
@@ -155,7 +269,10 @@ public static class AutoDodge
continue; continue;
Vector2 center = threat.Path[t]; Vector2 center = threat.Path[t];
if (bounds.Intersects(new Rectangle((int)(center.X - threat.Width / 2f), (int)(center.Y - threat.Height / 2f), threat.Width, threat.Height))) if (!bounds.Intersects(new Rectangle((int)(center.X - threat.Width / 2f), (int)(center.Y - threat.Height / 2f), threat.Width, threat.Height)))
continue;
_hitDamage = threat.Damage;
return t; return t;
} }
} }
@@ -163,6 +280,14 @@ public static class AutoDodge
return Horizon + 1; return Horizon + 1;
} }
// Main.CalculateDamagePlayersTake from 1.4.5 (missing in tModLoader), then damage reduction.
static int Taken(Player player, int damage)
{
int defense = player.statDefense;
float armor = Main.masterMode ? 1f : Main.expertMode ? 0.75f : 0.5f;
return (int)(Math.Max(1f, damage - defense * armor) * (1f - player.endurance));
}
// Enemies the current swing will knock back, or that a hit is already pushing away, are not contact threats. // 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) static bool KnockedAway(Player player, NPC npc)
{ {
@@ -197,7 +322,8 @@ public static class AutoDodge
Threat threat = Pool[Threats.Count]; Threat threat = Pool[Threats.Count];
threat.Count = Ballistics.Path(Ballistics.Get(projectile.type), projectile.Center, velocity, age, Horizon, threat.Path); threat.Count = Ballistics.Path(Ballistics.Get(projectile.type), projectile.Center, velocity, age, Horizon, threat.Path);
(threat.Width, threat.Height) = (Math.Max(4, projectile.width), Math.Max(4, projectile.height)); // Projectile.Damage doubles hostile projectile damage against players.
(threat.Width, threat.Height, threat.Damage) = (Math.Max(4, projectile.width), Math.Max(4, projectile.height), Taken(player, projectile.damage * 2));
Threats.Add(threat); Threats.Add(threat);
} }
@@ -210,7 +336,7 @@ public static class AutoDodge
Threat threat = Pool[Threats.Count]; Threat threat = Pool[Threats.Count];
for (int t = 0; t <= ContactHorizon; t++) for (int t = 0; t <= ContactHorizon; t++)
threat.Path[t] = npc.Center + NpcMotion.Offset(npc, t); threat.Path[t] = npc.Center + NpcMotion.Offset(npc, t);
(threat.Count, threat.Width, threat.Height) = (ContactHorizon + 1, npc.width, npc.height); (threat.Count, threat.Width, threat.Height, threat.Damage) = (ContactHorizon + 1, npc.width, npc.height, Taken(player, npc.damage));
Threats.Add(threat); Threats.Add(threat);
} }
} }
+1 -1
View File
@@ -32,7 +32,7 @@ public sealed partial class MainWindow
["Exact check"] = "Checks and corrects aim before firing. Auto Attack skips known misses. Orange brackets mean an unconfirmed shot.", ["Exact check"] = "Checks and corrects aim before firing. Auto Attack skips known misses. Orange brackets mean an unconfirmed shot.",
["Shot Preview"] = "Shows your next shot's path, including spread and homing.", ["Shot Preview"] = "Shows your next shot's path, including spread and homing.",
["Magic Bullets"] = "Your projectiles home in on the aimbot target or the nearest enemy, through blocks.", ["Magic Bullets"] = "Your projectiles home in on the aimbot target or the nearest enemy, through blocks.",
["Auto Dodge"] = "Moves you out of the way of incoming projectiles and enemies.", ["Auto Dodge"] = "Moves you out of the way of incoming projectiles and enemies. Uses jumps, wings, dashes and Rod of Harmony or Discord when needed.",
["Auto Attack"] = "Swings at enemies in reach and shoots at enemies your weapon can hit.", ["Auto Attack"] = "Swings at enemies in reach and shoots at enemies your weapon can hit.",
["Show target"] = "Shows the cursor radius, target brackets and aim point.", ["Show target"] = "Shows the cursor radius, target brackets and aim point.",
["Kill Aura"] = "Kills enemies in range and drops their loot.", ["Kill Aura"] = "Kills enemies in range and drops their loot.",