Skip to content

Commit

Permalink
Merge pull request #580 from xanunderscore/wip
Browse files Browse the repository at this point in the history
WIP deep dungeon auto clear modules
  • Loading branch information
awgil authored Feb 2, 2025
2 parents beb0803 + ed99643 commit 9f8048f
Show file tree
Hide file tree
Showing 326 changed files with 7,655 additions and 1,362 deletions.
17 changes: 17 additions & 0 deletions .github/ISSUE_TEMPLATE/rotation-bug-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
name: Rotation bug report
about: Unexpected rotation behavior, crash, error in logs, etc.
title: "[BUG] L100 JobNameHere: Issue Summary Here"
labels: ''
assignees: ''

---

**Bug description**
e.g. "Reaper using Enshroud too early before raid buffs"

**Replay info** - only one choice is needed
- [ ] I have a(n anonymized) video recording of this interaction
- [ ] I have a replay file and I will DM it to you upon request (or just post it here if I don't care about privacy)
- [ ] I can attach my `/xllog` from when the issue occurred
- [ ] I have a screenshot of or link to xivanalysis
10 changes: 10 additions & 0 deletions .github/ISSUE_TEMPLATE/something-that-isn-t-a-rotation-bug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
name: Something that isn't a rotation bug
about: Not a rotation bug
title: ''
labels: ''
assignees: ''

---


27 changes: 25 additions & 2 deletions BossMod/ActionQueue/ActionDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,13 @@ public float MainReadyIn(ReadOnlySpan<Cooldown> cooldowns, ReadOnlySpan<ClientSt
if (MainCooldownGroup < 0)
return 0;
var cdg = cooldowns[ActualMainCooldownGroup(dutyActions)];
return !IsMultiCharge || cdg.Total < Cooldown ? cdg.Remaining : Cooldown - cdg.Elapsed;
var max = MaxChargesAtCap();

// GCDs with multiple charges can be affected OR unaffected by haste depending on how many charges the user currently has access to
// the only separate-cooldown GCD that increases to >1 charge via trait is currently MCH Drill; others have multiple charges at unlock - SGE Phlegma, RPR Soul Slice, BLU Surpanakha
var cooldownSingleCharge = IsGCD && max > 1 && cdg.Total > 0 ? cdg.Total / max : Cooldown;

return !IsMultiCharge || cdg.Total < cooldownSingleCharge ? cdg.Remaining : cooldownSingleCharge - cdg.Elapsed;
}

public float ExtraReadyIn(ReadOnlySpan<Cooldown> cooldowns) => ExtraCooldownGroup >= 0 ? cooldowns[ExtraCooldownGroup].Remaining : 0;
Expand Down Expand Up @@ -173,6 +179,14 @@ public sealed class ActionDefinitions : IDisposable
public static readonly ActionID IDPotionInt = new(ActionType.Item, 1044165); // hq grade 2 gemdraught of intelligence
public static readonly ActionID IDPotionMnd = new(ActionType.Item, 1044166); // hq grade 2 gemdraught of mind

// deep dungeon consumables
public static readonly ActionID IDSustainingPotion = new(ActionType.Item, 20309);
public static readonly ActionID IDMaxPotion = new(ActionType.Item, 1013637);
public static readonly ActionID IDEmpyreanPotion = new(ActionType.Item, 23163);
public static readonly ActionID IDSuperPotion = new(ActionType.Item, 1023167);
public static readonly ActionID IDOrthosPotion = new(ActionType.Item, 38944);
public static readonly ActionID IDHyperPotion = new(ActionType.Item, 1038956);

// special general actions that we support
public static readonly ActionID IDGeneralLimitBreak = new(ActionType.General, 3);
public static readonly ActionID IDGeneralSprint = new(ActionType.General, 4);
Expand Down Expand Up @@ -219,6 +233,13 @@ private ActionDefinitions()
RegisterPotion(IDPotionInt);
RegisterPotion(IDPotionMnd);

RegisterPotion(IDSustainingPotion, 1.1f);
RegisterPotion(IDMaxPotion, 1.1f);
RegisterPotion(IDEmpyreanPotion, 1.1f);
RegisterPotion(IDSuperPotion, 1.1f);
RegisterPotion(IDOrthosPotion, 1.1f);
RegisterPotion(IDHyperPotion, 1.1f);

// special content actions - bozja, deep dungeons, etc
for (var i = BozjaHolsterID.None + 1; i < BozjaHolsterID.Count; ++i)
RegisterBozja(i);
Expand Down Expand Up @@ -380,7 +401,7 @@ public void RegisterSpell<AID>(AID aid, bool isPhysRanged = false, float instant

private void Register(ActionID aid, ActionDefinition definition) => _definitions.Add(aid, definition);

private void RegisterPotion(ActionID aid)
private void RegisterPotion(ActionID aid, float animLock = 0.6f)
{
var baseId = aid.ID % 500000;
var item = ItemData(baseId);
Expand All @@ -399,6 +420,7 @@ private void RegisterPotion(ActionID aid)
CastTime = castTime,
MainCooldownGroup = cdgroup,
Cooldown = cooldown,
InstantAnimLock = animLock,
};
var aidHQ = new ActionID(ActionType.Item, baseId + 1000000);
_definitions[aidHQ] = new(aidHQ)
Expand All @@ -408,6 +430,7 @@ private void RegisterPotion(ActionID aid)
CastTime = castTime,
MainCooldownGroup = cdgroup,
Cooldown = cooldown * 0.9f,
InstantAnimLock = animLock
};
}

Expand Down
1 change: 1 addition & 0 deletions BossMod/ActionQueue/Melee/MNK.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ public Definitions(ActionDefinitions d)
d.RegisterSpell(AID.ElixirBurst);
d.RegisterSpell(AID.WindsReply);
d.RegisterSpell(AID.FiresReply);
d.RegisterSpell(AID.EarthsReply);
d.RegisterSpell(AID.EnlightenedMeditation);
d.RegisterSpell(AID.ForbiddenMeditation);
d.RegisterSpell(AID.InspiritedMeditation);
Expand Down
13 changes: 13 additions & 0 deletions BossMod/ActionQueue/Roleplay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

public enum AID : uint
{
// palace of the dead/EO transformations
Pummel = 6273,
VoidFireII = 6274,
HeavenlyJudge = 6871,
Rotosmash = 32781,
WreckingBall = 32782,

// magitek reaper in Fly Free, My Pretty
MagitekCannon = 7619, // range 30 radius 6 ground targeted aoe
PhotonStream = 7620, // range 10 width 4 rect aoe
Expand Down Expand Up @@ -262,6 +269,12 @@ public sealed class Definitions : IDisposable
{
public Definitions(ActionDefinitions d)
{
d.RegisterSpell(AID.Pummel);
d.RegisterSpell(AID.VoidFireII);
d.RegisterSpell(AID.HeavenlyJudge);
d.RegisterSpell(AID.Rotosmash);
d.RegisterSpell(AID.WreckingBall);

d.RegisterSpell(AID.MagitekCannon);
d.RegisterSpell(AID.PhotonStream);
d.RegisterSpell(AID.DiffractiveMagitekCannon);
Expand Down
2 changes: 1 addition & 1 deletion BossMod/Autorotation/MiscAI/AutoFarm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public enum PriorityStrategy { None, Prioritize }

public static RotationModuleDefinition Definition()
{
RotationModuleDefinition res = new("Automatic targeting", "Collection of utilities to automatically target and pull mobs based on different criteria.", "AI", "veyn", RotationModuleQuality.Basic, new(~0ul), 1000, 1, RotationModuleOrder.HighLevel);
RotationModuleDefinition res = new("Automatic targeting", "Collection of utilities to automatically target and pull mobs based on different criteria.", "AI", "veyn", RotationModuleQuality.Basic, new(~0ul), 1000, 1, RotationModuleOrder.HighLevel, CanUseWhileRoleplaying: true);

res.Define(Track.General).As<GeneralStrategy>("General")
.AddOption(GeneralStrategy.FightBack, "FightBack", "Automatically engage any mobs that are in combat with player, but don't pull new mobs", supportedTargets: ActionTargets.Hostile)
Expand Down
15 changes: 13 additions & 2 deletions BossMod/Autorotation/MiscAI/NormalMovement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public enum SpecialModesStrategy { Automatic, Ignore }

public static RotationModuleDefinition Definition()
{
var res = new RotationModuleDefinition("Automatic movement", "Automatically move character based on pathfinding or explicit coordinates.", "AI", "veyn", RotationModuleQuality.WIP, new(~0ul), 1000, 1, RotationModuleOrder.Movement);
var res = new RotationModuleDefinition("Automatic movement", "Automatically move character based on pathfinding or explicit coordinates.", "AI", "veyn", RotationModuleQuality.WIP, new(~0ul), 1000, 1, RotationModuleOrder.Movement, CanUseWhileRoleplaying: true);
res.Define(Track.Destination).As<DestinationStrategy>("Destination", "Destination", 30)
.AddOption(DestinationStrategy.None, "None", "No automatic movement")
.AddOption(DestinationStrategy.Pathfind, "Pathfind", "Use standard pathfinding to find best position")
Expand Down Expand Up @@ -64,7 +64,18 @@ public override void Execute(StrategyValues strategy, ref Actor? primaryTarget,
}

if (Hints.InteractWithTarget != null)
Hints.GoalZones.Add(Hints.GoalSingleTarget(Hints.InteractWithTarget.Position, 2, 100)); // strongly prefer moving towards interact target
{
// strongly prefer moving towards interact target
Hints.GoalZones.Add(p =>
{
var length = (p - Hints.InteractWithTarget.Position).Length();

// 99% of eventobjects have an interact range of 3.5y, while the rest have a range of 2.09y
// checking only for the shorter range here would be fine in the vast majority of cases, but it can break interact pathfinding in the case that the target object is partially covered by a forbidden zone with a radius between 2.1 and 3.5
// this is specifically an issue in the metal gear thancred solo duty in endwalker
return length <= 2.09f ? 101 : length <= 3.5f ? 100 : 0;
});
}
}

var speed = Player.FindStatus(ClassShared.SID.Sprint) != null ? 7.8f : 6;
Expand Down
8 changes: 6 additions & 2 deletions BossMod/Autorotation/RotationModuleManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ public Preset? Preset
(uint)Roleplay.SID.BorrowedFlesh, // used specifically for In from the Cold (Endwalker)
(uint)Roleplay.SID.FreshPerspective, // sapphire weapon quest
565, // "Transfiguration" from certain pomanders in Palace of the Dead
439, // "Toad", palace of the dead
1546, // "Odder", heaven-on-high
3502, // "Owlet", EO
404, // "Transporting", not a transformation but prevents actions
];

public static bool IsTransformStatus(ActorStatus st) => TransformationStatuses.Contains(st.ID);
Expand All @@ -69,8 +73,8 @@ public RotationModuleManager(RotationDatabase db, BossModuleManager bmm, AIHints
WorldState.Actors.InCombatChanged.Subscribe(OnCombatChanged),
WorldState.Actors.IsDeadChanged.Subscribe(OnDeadChanged),
WorldState.Actors.CastEvent.Subscribe(OnCastEvent),
WorldState.Actors.StatusGain.Subscribe((a, idx) => DirtyActiveModules(PlayerInstanceId == a.InstanceID && a.Statuses[idx].ID == (uint)Roleplay.SID.RolePlaying)),
WorldState.Actors.StatusLose.Subscribe((a, idx) => DirtyActiveModules(PlayerInstanceId == a.InstanceID && a.Statuses[idx].ID == (uint)Roleplay.SID.RolePlaying)),
WorldState.Actors.StatusGain.Subscribe((a, idx) => DirtyActiveModules(PlayerInstanceId == a.InstanceID && IsTransformStatus(a.Statuses[idx]))),
WorldState.Actors.StatusLose.Subscribe((a, idx) => DirtyActiveModules(PlayerInstanceId == a.InstanceID && IsTransformStatus(a.Statuses[idx]))),
WorldState.Party.Modified.Subscribe(op => DirtyActiveModules(op.Slot == PlayerSlot)),
WorldState.Client.ActionRequested.Subscribe(OnActionRequested),
WorldState.Client.CountdownChanged.Subscribe(OnCountdownChanged),
Expand Down
198 changes: 198 additions & 0 deletions BossMod/Autorotation/Standard/xan/AI/DeepDungeon.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
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.HPRatio <= 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);
Hints.ActionsToExecute.Push(attack, primaryTarget, ActionQueue.Priority.High, targetPos: primaryTarget.PosRot.XYZ(), castTime: castTime - 0.5f);
}

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;
}
}
Loading

0 comments on commit 9f8048f

Please sign in to comment.