From 4e95719c2fd24ba952d3c18a27db4c6724b4fb4f Mon Sep 17 00:00:00 2001 From: LeonardoVAC Date: Fri, 16 Oct 2026 01:58:26 -0300 Subject: [PATCH] fix: Auto dodge ignores knockback and lifts without a way to fly --- src/TerraSharp/Features/Combat/AutoDodge.cs | 133 +++++++++++++++----- 1 file changed, 104 insertions(+), 29 deletions(-) diff --git a/src/TerraSharp/Features/Combat/AutoDodge.cs b/src/TerraSharp/Features/Combat/AutoDodge.cs index 163dcb7..2f61f94 100644 --- a/src/TerraSharp/Features/Combat/AutoDodge.cs +++ b/src/TerraSharp/Features/Combat/AutoDodge.cs @@ -11,10 +11,16 @@ namespace TerraSharp.Features.Combat; 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; + + // Horizontal speed away from the player that counts as being knocked back, not drifting. + const float KnockbackSpeed = 1f; sealed class Threat { @@ -24,9 +30,12 @@ public static class AutoDodge 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 List Threats = []; + static readonly Vector2[] Trail = new Vector2[Horizon + 1]; + static int _lastJump = -JumpCooldown; static AutoDodge() { @@ -34,10 +43,27 @@ public static class AutoDodge 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) { - if (!Settings.AutoDodge || player.dead) + if (!Settings.AutoDodge || Settings.GodMode || player.dead) return; CollectThreats(player); @@ -45,54 +71,84 @@ public static class AutoDodge return; float speed = Settings.Fly ? Settings.FlySpeed : Math.Max(Math.Max(player.maxRunSpeed, player.accRunSpeed), 3f); - float jump = Player.jumpSpeed * 1.5f; - int baseline = FirstHit(player, player.velocity, false); - if (baseline > Horizon) + int bestHit = FirstHit(player, Stay, speed, false); + if (bestHit > Horizon) return; - Move best = Moves[0]; - int bestHit = baseline; + Move best = Stay; + bool canRise = CanRise(player), canFall = !Grounded(player); foreach (Move move in Moves) { - if (move == Moves[0]) + if ((move.Vertical < 0 && !canRise) || (move.Vertical > 0 && !canFall)) continue; - var velocity = new Vector2(move.Horizontal * speed, move.Vertical == 0 ? player.velocity.Y : move.Vertical * jump * player.gravDir); - int hit = FirstHit(player, velocity, true); + int hit = FirstHit(player, move, speed, true); if (hit > bestHit) (best, bestHit) = (move, hit); } - if (best == Moves[0]) - return; - - Apply(player, best, speed, jump); + if (best != Stay) + Apply(player, best, speed); } - 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.controlRight = move.Horizontal > 0; if (move.Horizontal != 0) player.velocity.X = move.Horizontal * speed; - player.controlUp = move.Vertical < 0; + player.controlUp = move.Vertical < 0 && Settings.Fly; player.controlDown = move.Vertical > 0; - if (move.Vertical != 0) - player.velocity.Y = move.Vertical * jump * player.gravDir; + 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; + } } - // First tick a threat overlaps the player moving at this velocity, or Horizon + 1 when none do. - static int FirstHit(Player player, Vector2 velocity, bool checkWalls) + // 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) { - Vector2 size = new(player.width, player.height); - if (checkWalls && TileCollision.Sweep(player.Center, player.Center + velocity * WallCheckTicks, player.width, player.height)) - return -1; + bool grounded = 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; + if (move.Vertical > 0) + vertical = Math.Max(vertical, FastFall); + Vector2 position = player.position; + Trail[0] = position; for (int t = 1; t <= Horizon; t++) { - Vector2 position = player.position + velocity * t; - var bounds = new Rectangle((int)(position.X - Margin), (int)(position.Y - Margin), (int)(size.X + Margin * 2), (int)(size.Y + Margin * 2)); + 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); + + 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) { if (t >= threat.Count) @@ -107,6 +163,25 @@ public static class AutoDodge 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) { Threats.Clear(); @@ -129,13 +204,13 @@ public static class AutoDodge 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) + 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 <= Horizon; t++) + for (int t = 0; t <= ContactHorizon; 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); } }