feat: Auto dodge uses dashes and teleport rods
This commit is contained in:
1 parent
4e95719c2f
commit
9de8112ce9
2 files changed
+147
-21
No files matched your search
@@ -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,14 +269,25 @@ 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)))
|
||||
return t;
|
||||
if (!bounds.Intersects(new Rectangle((int)(center.X - threat.Width / 2f), (int)(center.Y - threat.Height / 2f), threat.Width, threat.Height)))
|
||||
continue;
|
||||
|
||||
_hitDamage = threat.Damage;
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
return Horizon + 1;
|
||||
}
|
||||
|
||||
// Main.CalculateDamagePlayersTake from 1.4.5 (missing in tModLoader), then damage reduction.
|
||||
static int Taken(Player player, int damage)
|
||||
{
|
||||
int defense = player.statDefense;
|
||||
float armor = Main.masterMode ? 1f : Main.expertMode ? 0.75f : 0.5f;
|
||||
return (int)(Math.Max(1f, damage - defense * armor) * (1f - player.endurance));
|
||||
}
|
||||
|
||||
// Enemies the current swing will knock back, or that a hit is already pushing away, are not contact threats.
|
||||
static bool KnockedAway(Player player, NPC npc)
|
||||
{
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in new issue
Block a user