fix: Auto dodge ignores knockback and lifts without a way to fly
This commit is contained in:
1 parent
9d748d679b
commit
4e95719c2f
1 file changed
+104
-29
@@ -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<Threat> 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user