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 Microsoft.Xna.Framework;
using Terraria;
using Terraria.ID;
using TerraSharp.Core;
using TerraSharp.Simulation;
namespace TerraSharp.Features.Combat;
// Steers the local player out of predicted projectile and contact hits.
// Steers the local player out of predicted projectile and contact hits, using dashes and teleport rods when running and jumping are not enough.
public static class AutoDodge
{
const int Horizon = 24;
@@ -19,23 +20,35 @@ public static class AutoDodge
const int JumpCooldown = 12;
const float Gravity = 0.4f, MaxFall = 10f, FastFall = 8f;
// Slowest dash in Player.DashMovement (Tabi, Master Ninja Gear).
const float DashSpeed = 14.5f;
// Horizontal speed away from the player that counts as being knocked back, not drifting.
const float KnockbackSpeed = 1f;
// Teleport only for hits this close; a later run or jump may still avoid farther ones.
const int TeleportWithin = 8;
const int TeleportCooldown = 30;
static readonly float[] TeleportRadii = [6f, 10f, 14f, 18f];
const int TeleportDirections = 16;
const int TempleWall = 87;
sealed class Threat
{
public readonly Vector2[] Path = new Vector2[Horizon + 1];
public int Count, Width, Height;
public int Count, Width, Height, Damage;
}
readonly record struct Move(int Horizontal, int Vertical);
readonly record struct Move(int Horizontal, int Vertical, bool Dash = false);
static readonly Move 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;
static int _lastJump = -JumpCooldown, _lastTeleport = -TeleportCooldown;
static int _hitDamage;
static AutoDodge()
{
@@ -50,17 +63,24 @@ public static class AutoDodge
// 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: 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)
{
if (!Settings.AutoDodge || Settings.GodMode || player.dead)
@@ -71,10 +91,11 @@ public static class AutoDodge
return;
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)
return;
int incoming = _hitDamage;
Move best = Stay;
bool canRise = CanRise(player), canFall = !Grounded(player);
foreach (Move move in Moves)
@@ -82,11 +103,22 @@ public static class AutoDodge
if ((move.Vertical < 0 && !canRise) || (move.Vertical > 0 && !canFall))
continue;
int hit = FirstHit(player, move, speed, true);
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);
}
@@ -95,6 +127,14 @@ public static class AutoDodge
{
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;
@@ -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.
static void Predict(Player player, Move move, float speed)
// 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)
{
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);
float horizontal = move.Horizontal != 0 ? move.Horizontal * speed : player.velocity.X;
float vertical = player.velocity.Y * player.gravDir;
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 = player.position;
Vector2 position = spot ?? player.position;
Trail[0] = position;
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);
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.
static int FirstHit(Player player, Move move, float speed, bool checkWalls)
// 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)
{
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))
return -1;
@@ -155,7 +269,10 @@ public static class AutoDodge
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)))
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;
}
}
@@ -163,6 +280,14 @@ public static class AutoDodge
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)
{
@@ -197,7 +322,8 @@ public static class AutoDodge
Threat threat = Pool[Threats.Count];
threat.Count = Ballistics.Path(Ballistics.Get(projectile.type), projectile.Center, velocity, age, Horizon, threat.Path);
(threat.Width, threat.Height) = (Math.Max(4, projectile.width), Math.Max(4, projectile.height));
// Projectile.Damage doubles hostile projectile damage against players.
(threat.Width, threat.Height, threat.Damage) = (Math.Max(4, projectile.width), Math.Max(4, projectile.height), Taken(player, projectile.damage * 2));
Threats.Add(threat);
}
@@ -210,7 +336,7 @@ public static class AutoDodge
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) = (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);
}
}
+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.",
["Shot Preview"] = "Shows your next shot's path, including spread and homing.",
["Magic Bullets"] = "Your projectiles home in on the aimbot target or the nearest enemy, through blocks.",
["Auto Dodge"] = "Moves you out of the way of incoming projectiles and enemies.",
["Auto Dodge"] = "Moves you out of the way of incoming projectiles and enemies. Uses jumps, wings, dashes and Rod of Harmony or Discord when needed.",
["Auto Attack"] = "Swings at enemies in reach and shoots at enemies your weapon can hit.",
["Show target"] = "Shows the cursor radius, target brackets and aim point.",
["Kill Aura"] = "Kills enemies in range and drops their loot.",