Skip to content

Commit

Permalink
update rotations
Browse files Browse the repository at this point in the history
  • Loading branch information
xanunderscore committed Jan 29, 2025
1 parent 5f95e89 commit 7998936
Show file tree
Hide file tree
Showing 9 changed files with 368 additions and 127 deletions.
199 changes: 199 additions & 0 deletions BossMod/Autorotation/Standard/xan/AI/DeepDungeon.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
namespace BossMod.Autorotation.xan;

public class DeepDungeonAI(RotationModuleManager manager, Actor player) : AIBase(manager, player)
{
public enum Track { Potion, Kite }

public static RotationModuleDefinition Definition()
{
var def = new RotationModuleDefinition("Deep Dungeon AI", "Utilities for deep dungeon - potion/pomander user", "AI (xan)", "xan", RotationModuleQuality.Basic, new BitMask(~0ul), 100, CanUseWhileRoleplaying: true);

def.AbilityTrack(Track.Potion, "Potion");
def.AbilityTrack(Track.Kite, "Kite enemies");

return def;
}

enum OID : uint
{
Unei = 0x3E1A,
}

enum Transformation : uint
{
None,
Manticore,
Succubus,
Kuribu,
Dreadnaught
}

enum SID : uint
{
Transfiguration = 565,
ItemPenalty = 1094,
}

public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving)
{
if (World.DeepDungeon.DungeonId == 0)
return;

var transformation = Transformation.None;
if (Player.FindStatus(SID.Transfiguration) is { } status)
{
transformation = (status.Extra & 0xFF) switch
{
42 => Transformation.Manticore,
43 => Transformation.Succubus,
49 => Transformation.Kuribu,
244 => Transformation.Dreadnaught,
_ => Transformation.None
};
}

if (transformation != Transformation.None)
{
DoTransformActions(strategy, primaryTarget, transformation);
return;
}

if (IsRanged && !Player.InCombat && primaryTarget is Actor target && !target.InCombat && !target.IsAlly)
// bandaid fix to help deal with constant LOS issues
Hints.GoalZones.Add(Hints.GoalSingleTarget(target, 3, 0.1f));

SetupKiteZone(strategy, primaryTarget);

if (Player.FindStatus(SID.ItemPenalty) != null)
return;

var (regenAction, potAction) = World.DeepDungeon.DungeonId switch
{
DeepDungeonState.DungeonType.POTD => (ActionDefinitions.IDSustainingPotion, ActionDefinitions.IDMaxPotion),
DeepDungeonState.DungeonType.HOH => (ActionDefinitions.IDEmpyreanPotion, ActionDefinitions.IDSuperPotion),
DeepDungeonState.DungeonType.EO => (ActionDefinitions.IDOrthosPotion, ActionDefinitions.IDHyperPotion),
_ => (default, default)
};

if (regenAction != default && ShouldPotion(strategy))
Hints.ActionsToExecute.Push(regenAction, Player, ActionQueue.Priority.Medium);

if (potAction != default && Player.PredictedHPRatio <= 0.3f)
Hints.ActionsToExecute.Push(potAction, Player, ActionQueue.Priority.VeryHigh);
}

private bool IsRanged => Player.Class.GetRole() is Role.Ranged or Role.Healer;

private static readonly HashSet<uint> NoMeleeAutos = [
// hoh
0x22C3, // heavenly onibi
0x22C5, // heavenly dhruva
0x22C6, // heavenly sai taisui
0x22DC, // heavenly dogu
0x22DE, // heavenly ganseki
0x22ED, // heavenly kongorei
0x22EF, // heavenly maruishi
0x22F3, // heavenly rachimonai
0x22FC, // heavenly doguzeri
0x2320, // heavenly nuppeppo (WHM) (uses stone)

// orthos
0x3DCC, // orthos imp
0x3DCE, // orthos fachan
0x3DD2, // orthos water sprite
0x3DD4, // orthos microsystem
0x3DD5, // orthosystem β
0x3DE0, // orthodemolisher
0x3DE2, // orthodroid
0x3DFD, // orthos apa
0x3E10, // orthos ice sprite
0x3E5C, // orthos ahriman
0x3E62, // orthos abyss
0x3E63, // orthodrone
0x3E64, // orthosystem γ
0x3E66, // orthosystem α
];

private void SetupKiteZone(StrategyValues strategy, Actor? primaryTarget)
{
if (!IsRanged || primaryTarget == null || !Player.InCombat || !strategy.Enabled(Track.Kite))
return;

// wew
if (NoMeleeAutos.Contains(primaryTarget.OID))
return;

// assume we don't need to kite if mob is busy casting (TODO: some mob spells can be cast while moving, maybe there's a column in sheets for it)
if (primaryTarget.CastInfo != null)
return;

float maxRange = 25;
float maxKite = 9;

var primaryPos = primaryTarget.Position;
var total = maxRange + Player.HitboxRadius + primaryTarget.HitboxRadius;
var totalKite = maxKite + Player.HitboxRadius + primaryTarget.HitboxRadius;
float goalFactor = 0.05f;
Hints.GoalZones.Add(pos =>
{
var dist = (pos - primaryPos).Length();
return dist <= total && dist >= totalKite ? goalFactor : 0;
});
}

private void DoTransformActions(StrategyValues strategy, Actor? primaryTarget, Transformation t)
{
if (primaryTarget == null)
return;

Func<WPos, float> goal;
ActionID attack;
int numTargets;
var castTime = 0f;

switch (t)
{
case Transformation.Manticore:
goal = Hints.GoalSingleTarget(primaryTarget, 3);
numTargets = 1;
attack = ActionID.MakeSpell(Roleplay.AID.Pummel);
break;
case Transformation.Succubus:
goal = Hints.GoalSingleTarget(primaryTarget, 25);
numTargets = Hints.NumPriorityTargetsInAOECircle(primaryTarget.Position, 5);
attack = ActionID.MakeSpell(Roleplay.AID.VoidFireII);
castTime = 2.5f;
break;
case Transformation.Kuribu:
// heavenly judge is ground targeted
goal = Hints.GoalSingleTarget(primaryTarget.Position, 25);
numTargets = Hints.NumPriorityTargetsInAOECircle(primaryTarget.Position, 6);
attack = ActionID.MakeSpell(Roleplay.AID.HeavenlyJudge);
castTime = 2.5f;
break;
case Transformation.Dreadnaught:
goal = Hints.GoalSingleTarget(primaryTarget, 3);
numTargets = 1;
attack = ActionID.MakeSpell(Roleplay.AID.Rotosmash);
break;
default:
return;
}

if (numTargets == 0)
return;

Hints.GoalZones.Add(goal);
if (castTime == 0 || Hints.MaxCastTimeEstimate >= (castTime - 0.5f))
Hints.ActionsToExecute.Push(attack, primaryTarget, ActionQueue.Priority.High, targetPos: primaryTarget.PosRot.XYZ());
}

private bool ShouldPotion(StrategyValues strategy)
{
if (World.Actors.Any(w => w.OID == (uint)OID.Unei) || !strategy.Enabled(Track.Potion))
return false;

var ratio = Player.ClassCategory is ClassCategory.Tank ? 0.4f : 0.6f;
return Player.PredictedHPRatio < ratio && Player.FindStatus(648) == null && Player.InCombat;
}
}
2 changes: 1 addition & 1 deletion BossMod/Autorotation/Standard/xan/AI/Healer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public override void Execute(StrategyValues strategy, ref Actor? primaryTarget,
private void UseGCD<AID>(AID action, Actor? target, int extraPriority = 0) where AID : Enum
=> UseGCD(ActionID.MakeSpell(action), target, extraPriority);
private void UseGCD(ActionID action, Actor? target, int extraPriority = 0)
=> Hints.ActionsToExecute.Push(action, target, ActionQueue.Priority.High + 500 + extraPriority); // TODO[cast-time]-xan: verify all callers
=> Hints.ActionsToExecute.Push(action, target, ActionQueue.Priority.High + 500 + extraPriority);

private void UseOGCD<AID>(AID action, Actor? target, int extraPriority = 0) where AID : Enum
=> UseOGCD(ActionID.MakeSpell(action), target, extraPriority);
Expand Down
6 changes: 3 additions & 3 deletions BossMod/Autorotation/Standard/xan/AI/Melee.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public override void Execute(StrategyValues strategy, ref Actor? primaryTarget,
Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.SecondWind), Player, ActionQueue.Priority.Medium);

// bloodbath
if (strategy.Enabled(Track.Bloodbath) && Player.InCombat && Player.PredictedHPRatio <= 0.75)
if (strategy.Enabled(Track.Bloodbath) && Player.InCombat && Player.PredictedHPRatio <= 0.3)
Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.Bloodbath), Player, ActionQueue.Priority.Medium);

// low blow
Expand Down Expand Up @@ -66,7 +66,7 @@ private void ExecLB(StrategyValues strategy, Actor? primaryTarget)
case 1:
break;
case 2:
Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.Bladedance), primaryTarget, ActionQueue.Priority.VeryHigh, castTime: 3);
Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.Bladedance), primaryTarget, ActionQueue.Priority.VeryHigh);
break;
case 3:
var lb3 = Player.Class switch
Expand All @@ -80,7 +80,7 @@ private void ExecLB(StrategyValues strategy, Actor? primaryTarget)
_ => default
};
if (lb3 != default)
Hints.ActionsToExecute.Push(lb3, primaryTarget, ActionQueue.Priority.VeryHigh, castTime: 4.5f);
Hints.ActionsToExecute.Push(lb3, primaryTarget, ActionQueue.Priority.VeryHigh);
break;
}
}
Expand Down
6 changes: 3 additions & 3 deletions BossMod/Autorotation/Standard/xan/AI/Ranged.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ private void ExecLB(StrategyValues strategy, Actor? primaryTarget)
{
case 1:
if (lbTarget(2) is Actor a)
Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.BigShot), a, ActionQueue.Priority.VeryHigh, castTime: 2);
Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.BigShot), a, ActionQueue.Priority.VeryHigh);
break;
case 2:
if (lbTarget(2.5f) is Actor b)
Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.Desperado), b, ActionQueue.Priority.VeryHigh, castTime: 3);
Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.Desperado), b, ActionQueue.Priority.VeryHigh);
break;
case 3:
var lb3 = Player.Class switch
Expand All @@ -61,7 +61,7 @@ private void ExecLB(StrategyValues strategy, Actor? primaryTarget)
_ => default
};
if (lbTarget(4) is Actor c && lb3 != default)
Hints.ActionsToExecute.Push(lb3, c, ActionQueue.Priority.VeryHigh, castTime: 4.5f);
Hints.ActionsToExecute.Push(lb3, c, ActionQueue.Priority.VeryHigh);
break;
}
}
Expand Down
51 changes: 45 additions & 6 deletions BossMod/Autorotation/Standard/xan/Basexan.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using static BossMod.AIHints;
using System.Diagnostics.CodeAnalysis;
using static BossMod.AIHints;

namespace BossMod.Autorotation.xan;

Expand Down Expand Up @@ -71,6 +72,15 @@ public bool CanWeave(AID aid, int extraGCDs = 0, float extraFixedDelay = 0)
protected int NextGCDPrio;
protected uint MP;

protected AID HighestUnlocked(params AID[] actions)
{
foreach (var act in actions)
if (Unlocked(act))
return act;

return default;
}

protected AID ComboLastMove => (AID)(object)World.Client.ComboState.Action;

protected void PushGCD<P>(AID aid, Actor? target, P priority, float delay = 0) where P : Enum
Expand Down Expand Up @@ -114,11 +124,11 @@ protected bool PushAction(AID aid, Actor? target, float priority, float delay)
if ((uint)(object)aid == 0)
return false;

if (!CanCast(aid)) // TODO[cast-time]-xan: don't do this, it's now not reliable, instead queue all cast options at different prios
if (!CanCast(aid))
return false;

var def = ActionDefinitions.Instance.Spell(aid);
if (def == null)
if (def == null || !def.IsUnlocked(World, Player))
return false;

if (def.Range != 0 && target == null)
Expand All @@ -137,7 +147,7 @@ protected bool PushAction(AID aid, Actor? target, float priority, float delay)
targetPos = target.PosRot.XYZ();
}

Hints.ActionsToExecute.Push(ActionID.MakeSpell(aid), target, priority, delay: delay, targetPos: targetPos); // TODO[cast-time]-xan: verify all callers
Hints.ActionsToExecute.Push(ActionID.MakeSpell(aid), target, priority, delay: delay, targetPos: targetPos);
return true;
}

Expand Down Expand Up @@ -367,9 +377,14 @@ protected void UpdatePositionals(Enemy? enemy, ref (Positional pos, bool imm) po

private void EstimateCastTime()
{
MaxCastTime = Hints.MaxCastTime; // TODO[cast-time]-xan: this is now wrong, needs to be reviewed and fixed (queue all actions with casttime=time they need)
MaxCastTime = Hints.MaxCastTimeEstimate;

if (Player.PendingKnockbacks.Count > 0)
{
MaxCastTime = 0f;
return;
}

// TODO[cast-time]-xan: this is not really correct in most cases: even if target is a gaze source, it's possible to start casting then rotate to be >45 && <75 degrees and finish cast successfully; the gaze avoidance tweak handles that
var forbiddenDir = Hints.ForbiddenDirections.Where(d => Player.Rotation.AlmostEqual(d.center, d.halfWidth.Rad)).Select(d => d.activation).DefaultIfEmpty(DateTime.MinValue).Min();
if (forbiddenDir > World.CurrentTime)
{
Expand All @@ -379,12 +394,33 @@ private void EstimateCastTime()
}
}

private float? _prevCountdown;
private DateTime _cdLockout;

[SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "determinism is intentional here")]
private void PretendCountdown()
{
if (CountdownRemaining == null)
{
_cdLockout = DateTime.MinValue;
_prevCountdown = null;
}
else if (_prevCountdown == null)
{
var wait = (float)new Random((int)World.Frame.Index).NextDouble() + 0.5f;
_cdLockout = World.FutureTime(wait);
_prevCountdown = CountdownRemaining;
}
}

public sealed override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving)
{
NextGCD = default;
NextGCDPrio = 0;
PlayerTarget = Hints.FindEnemy(primaryTarget);

PretendCountdown();

var pelo = Player.FindStatus(ClassShared.SID.Peloton);
PelotonLeft = pelo != null ? StatusDuration(pelo.Value.ExpireAt) : 0;
SwiftcastLeft = MathF.Max(StatusLeft(ClassShared.SID.Swiftcast), StatusLeft(ClassShared.SID.LostChainspell));
Expand All @@ -410,6 +446,9 @@ public sealed override void Execute(StrategyValues strategy, ref Actor? primaryT
// TODO max MP can be higher in eureka/bozja
MP = (uint)Math.Clamp(Player.PredictedMPRaw, 0, 10000);

if (_cdLockout > World.CurrentTime)
return;

if (Player.MountId is not (103 or 117 or 128))
Exec(strategy, PlayerTarget);
}
Expand Down
Loading

0 comments on commit 7998936

Please sign in to comment.