fix: Auto dodge ignores knockback and lifts without a way to fly

This commit is contained in:
VAC committed 2026-10-16 01:58:26 -03:00
1 parent 9d748d679b
commit 4e95719c2f
1 file changed
+104 -29
+104 -29
View File
@@ -11,10 +11,16 @@ namespace TerraSharp.Features.Combat;
public static class AutoDodge public static class AutoDodge
{ {
const int Horizon = 24; const int Horizon = 24;
const int ContactHorizon = 12;
const int MaxThreats = 48; const int MaxThreats = 48;
const float ScanRange = 60f * 16f; const float ScanRange = 60f * 16f;
const float Margin = 6f; const float Margin = 6f;
const int WallCheckTicks = 6; const int WallCheckTicks = 6;
const int JumpCooldown = 12;
const float Gravity = 0.4f, MaxFall = 10f, FastFall = 8f;
// Horizontal speed away from the player that counts as being knocked back, not drifting.
const float KnockbackSpeed = 1f;
sealed class Threat sealed class Threat
{ {
@@ -24,9 +30,12 @@ public static class AutoDodge
readonly record struct Move(int Horizontal, int Vertical); readonly record struct Move(int Horizontal, int Vertical);
static readonly Move[] Moves = [new(0, 0), new(-1, 0), new(1, 0), new(0, -1), new(-1, -1), new(1, -1), new(0, 1), new(-1, 1), new(1, 1)]; static readonly Move Stay = new(0, 0);
static readonly Move[] Moves = [new(-1, 0), new(1, 0), new(0, -1), new(-1, -1), new(1, -1), new(0, 1), new(-1, 1), new(1, 1)];
static readonly 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 int _lastJump = -JumpCooldown;
static AutoDodge() static AutoDodge()
{ {
@@ -34,10 +43,27 @@ public static class AutoDodge
Pool[i] = new Threat(); Pool[i] = new Threat();
} }
// Player.HorizontalMovement prefix: controls and velocity set here drive this tick's movement. static bool Grounded(Player player) => player.velocity.Y == 0f || player.sliding;
static bool Flying(Player player) => Settings.Fly || player.wingTime > 0f || player.rocketTime > 0 || MountFlies(player);
// Mirrors the jump conditions in Player.JumpMovement; the game itself performs the jump.
static bool CanRise(Player player) => Settings.Fly || (Clock.Tick - _lastJump >= JumpCooldown && (Grounded(player) || Flying(player) || HasExtraJump(player)));
#if TMODLOADER
static bool HasExtraJump(Player player) => player.AnyExtraJumpUsable();
static bool MountFlies(Player player) => player.mount.Active && player.mount.CanFly();
#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;
#endif
// Player.HorizontalMovement prefix: 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 || player.dead) if (!Settings.AutoDodge || Settings.GodMode || player.dead)
return; return;
CollectThreats(player); CollectThreats(player);
@@ -45,54 +71,84 @@ 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);
float jump = Player.jumpSpeed * 1.5f; int bestHit = FirstHit(player, Stay, speed, false);
int baseline = FirstHit(player, player.velocity, false); if (bestHit > Horizon)
if (baseline > Horizon)
return; return;
Move best = Moves[0]; Move best = Stay;
int bestHit = baseline; bool canRise = CanRise(player), canFall = !Grounded(player);
foreach (Move move in Moves) foreach (Move move in Moves)
{ {
if (move == Moves[0]) if ((move.Vertical < 0 && !canRise) || (move.Vertical > 0 && !canFall))
continue; continue;
var velocity = new Vector2(move.Horizontal * speed, move.Vertical == 0 ? player.velocity.Y : move.Vertical * jump * player.gravDir); int hit = FirstHit(player, move, speed, true);
int hit = FirstHit(player, velocity, true);
if (hit > bestHit) if (hit > bestHit)
(best, bestHit) = (move, hit); (best, bestHit) = (move, hit);
} }
if (best == Moves[0]) if (best != Stay)
return; Apply(player, best, speed);
Apply(player, best, speed, jump);
} }
static void Apply(Player player, Move move, float speed, float jump) static void Apply(Player player, Move move, float speed)
{ {
player.controlLeft = move.Horizontal < 0; player.controlLeft = move.Horizontal < 0;
player.controlRight = move.Horizontal > 0; player.controlRight = move.Horizontal > 0;
if (move.Horizontal != 0) if (move.Horizontal != 0)
player.velocity.X = move.Horizontal * speed; player.velocity.X = move.Horizontal * speed;
player.controlUp = move.Vertical < 0; player.controlUp = move.Vertical < 0 && Settings.Fly;
player.controlDown = move.Vertical > 0; player.controlDown = move.Vertical > 0;
if (move.Vertical != 0) if (move.Vertical > 0)
player.velocity.Y = move.Vertical * jump * player.gravDir; player.velocity.Y = Math.Max(player.velocity.Y * player.gravDir, FastFall) * player.gravDir;
if (move.Vertical >= 0)
return;
player.controlJump = true;
if (Grounded(player) || !Flying(player))
{
player.releaseJump = true;
_lastJump = Clock.Tick;
}
} }
// First tick a threat overlaps the player moving at this velocity, or Horizon + 1 when none do. // Player positions for the move: jumps rise at Player.jumpSpeed for Player.jumpHeight ticks, everything else falls.
static int FirstHit(Player player, Vector2 velocity, bool checkWalls) static void Predict(Player player, Move move, float speed)
{ {
Vector2 size = new(player.width, player.height); bool grounded = Grounded(player);
if (checkWalls && TileCollision.Sweep(player.Center, player.Center + velocity * WallCheckTicks, player.width, player.height)) bool fly = Settings.Fly || (move.Vertical < 0 && Flying(player) && !grounded);
return -1; float horizontal = move.Horizontal != 0 ? move.Horizontal * speed : player.velocity.X;
float vertical = player.velocity.Y * player.gravDir;
if (move.Vertical > 0)
vertical = Math.Max(vertical, FastFall);
Vector2 position = player.position;
Trail[0] = position;
for (int t = 1; t <= Horizon; t++) for (int t = 1; t <= Horizon; t++)
{ {
Vector2 position = player.position + velocity * t; if (move.Vertical < 0)
var bounds = new Rectangle((int)(position.X - Margin), (int)(position.Y - Margin), (int)(size.X + Margin * 2), (int)(size.Y + Margin * 2)); vertical = fly || t <= Player.jumpHeight ? -Player.jumpSpeed : Math.Min(vertical + Gravity, MaxFall);
else if (!grounded || move.Vertical > 0)
vertical = Math.Min(vertical + Gravity, MaxFall);
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)
{
Predict(player, 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) foreach (Threat threat in Threats)
{ {
if (t >= threat.Count) if (t >= threat.Count)
@@ -107,6 +163,25 @@ public static class AutoDodge
return Horizon + 1; return Horizon + 1;
} }
// 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) static void CollectThreats(Player player)
{ {
Threats.Clear(); Threats.Clear();
@@ -129,13 +204,13 @@ public static class AutoDodge
for (int i = 0; i < Main.maxNPCs && Threats.Count < MaxThreats; i++) for (int i = 0; i < Main.maxNPCs && Threats.Count < MaxThreats; i++)
{ {
NPC npc = Main.npc[i]; NPC npc = Main.npc[i];
if (!Hostility.IsHostile(npc) || npc.damage <= 0 || Vector2.DistanceSquared(npc.Center, player.Center) > rangeSq) if (!Hostility.IsHostile(npc) || npc.damage <= 0 || Vector2.DistanceSquared(npc.Center, player.Center) > rangeSq || KnockedAway(player, npc))
continue; continue;
Threat threat = Pool[Threats.Count]; Threat threat = Pool[Threats.Count];
for (int t = 0; t <= Horizon; t++) for (int t = 0; t <= ContactHorizon; t++)
threat.Path[t] = npc.Center + NpcMotion.Offset(npc, t); threat.Path[t] = npc.Center + NpcMotion.Offset(npc, t);
(threat.Count, threat.Width, threat.Height) = (Horizon + 1, npc.width, npc.height); (threat.Count, threat.Width, threat.Height) = (ContactHorizon + 1, npc.width, npc.height);
Threats.Add(threat); Threats.Add(threat);
} }
} }