From 9de8112ce9b6a6bcf566f0e9b0e25ca85394eaca Mon Sep 17 00:00:00 2001 From: LeonardoVAC Date: Fri, 16 Oct 2026 01:58:27 -0300 Subject: [PATCH] feat: Auto dodge uses dashes and teleport rods --- src/TerraSharp/Features/Combat/AutoDodge.cs | 166 +++++++++++++++++--- src/TerraSharp/UI/MainWindow.Tooltips.cs | 2 +- 2 files changed, 147 insertions(+), 21 deletions(-) diff --git a/src/TerraSharp/Features/Combat/AutoDodge.cs b/src/TerraSharp/Features/Combat/AutoDodge.cs index 2f61f94..c4dc60c 100644 --- a/src/TerraSharp/Features/Combat/AutoDodge.cs +++ b/src/TerraSharp/Features/Combat/AutoDodge.cs @@ -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 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); } } diff --git a/src/TerraSharp/UI/MainWindow.Tooltips.cs b/src/TerraSharp/UI/MainWindow.Tooltips.cs index 9c6d498..8d9255e 100644 --- a/src/TerraSharp/UI/MainWindow.Tooltips.cs +++ b/src/TerraSharp/UI/MainWindow.Tooltips.cs @@ -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.",