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
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user