From e58655d305dda78ac1f18e753b01fa37aa60056b Mon Sep 17 00:00:00 2001
From: Thomas Brooks <34015422+NostraThomas99@users.noreply.github.com>
Date: Sun, 15 Dec 2024 05:37:50 -0600
Subject: [PATCH] Basic BLU Rotation
Added "Basic BLU" rotation, a blue mage rotation that's actually sort of useful. Swap to BLU and see the rotation configuration menu for more information.
Misc improvements to core functionality to support Basic BLU
---
BasicRotations/Limited Jobs/BLU_Basic.cs | 174 ++++++++++++++++++
.../Actions/ActionBasicInfo.cs | 22 +--
.../Rotations/Basic/BlueMageRotation.cs | 92 ++++++++-
RotationSolver/Helpers/DownloadHelper.cs | 3 +-
RotationSolver/Helpers/RotationLoadContext.cs | 1 +
RotationSolver/UI/RotationConfigWindow.cs | 1 +
RotationSolver/Updaters/PreviewUpdater.cs | 15 ++
7 files changed, 286 insertions(+), 22 deletions(-)
create mode 100644 BasicRotations/Limited Jobs/BLU_Basic.cs
diff --git a/BasicRotations/Limited Jobs/BLU_Basic.cs b/BasicRotations/Limited Jobs/BLU_Basic.cs
new file mode 100644
index 000000000..07b62767e
--- /dev/null
+++ b/BasicRotations/Limited Jobs/BLU_Basic.cs
@@ -0,0 +1,174 @@
+namespace DefaultRotations.Magical;
+
+[Rotation("Basic BLU", CombatType.PvE, GameVersion = "7.11")]
+[SourceCode(Path = "main/BasicRotations/Limited Jobs/BLU_Basic.cs")]
+[Api(4)]
+public sealed class Blue_Basic : BlueMageRotation
+{
+
+ [RotationConfig(CombatType.PvE, Name = "Single Target Spell")]
+ public BluDPSSpell SingleTargetDPSSpell { get; set; } = BluDPSSpell.SonicBoom;
+
+ [RotationConfig(CombatType.PvE, Name = "AoE Spell")]
+ public BluAOESpell AoeSpell { get; set; } = BluAOESpell.MindBlast;
+
+ [RotationConfig(CombatType.PvE, Name = "Healing Spell")]
+ public BluHealSpell HealSpell { get; set; } = BluHealSpell.WhiteWind;
+
+ [RotationConfig(CombatType.PvE, Name = "Use Basic Instinct")]
+ public bool UseBasicInstinct { get; set; } = true;
+
+ [RotationConfig(CombatType.PvE, Name = "Use Mighty Guard")]
+ public bool UseMightyGuard { get; set; } = true;
+
+ #region Countdown logic
+ // Defines logic for actions to take during the countdown before combat starts.
+ protected override IAction? CountDownAction(float remainTime)
+ {
+
+ return base.CountDownAction(remainTime);
+ }
+ #endregion
+
+ #region Emergency Logic
+ // Determines emergency actions to take based on the next planned GCD action.
+ protected override bool EmergencyAbility(IAction nextGCD, out IAction? act)
+ {
+ act = null;
+
+ return base.EmergencyAbility(nextGCD, out act);
+ }
+ #endregion
+
+ #region Move oGCD Logic
+ protected override bool MoveForwardAbility(IAction nextGCD, out IAction? act)
+ {
+ act = null;
+
+
+ return base.MoveForwardAbility(nextGCD, out act);
+ }
+
+ protected override bool MoveBackAbility(IAction nextGCD, out IAction? act)
+ {
+ act = null;
+
+
+ return base.MoveBackAbility(nextGCD, out act);
+ }
+
+ protected override bool SpeedAbility(IAction nextGCD, out IAction? act)
+ {
+ act = null;
+
+
+ return base.SpeedAbility(nextGCD, out act);
+ }
+ #endregion
+
+ #region Heal/Defense oGCD Logic
+ protected override bool HealSingleAbility(IAction nextGCD, out IAction? act)
+ {
+ act = null;
+
+
+ return base.HealSingleAbility(nextGCD, out act);
+ }
+
+ protected override bool DefenseAreaAbility(IAction nextGCD, out IAction? act)
+ {
+ act = null;
+
+
+ return base.DefenseAreaAbility(nextGCD, out act);
+ }
+
+ protected override bool DefenseSingleAbility(IAction nextGCD, out IAction? act)
+ {
+ act = null;
+
+
+ return base.DefenseSingleAbility(nextGCD, out act);
+ }
+ #endregion
+
+ #region oGCD Logic
+ protected override bool AttackAbility(IAction nextGCD, out IAction? act)
+ {
+ act = null;
+
+ return base.AttackAbility(nextGCD, out act);
+ }
+
+ protected override bool GeneralAbility(IAction nextGCD, out IAction? act)
+ {
+ act = null;
+ if (Player.CurrentMp < 6000 && InCombat && LucidDreamingPvE.CanUse(out act)) return true;
+ //if (AethericMimicryPvE_19239.CanUse(out act)) return true;
+ return base.GeneralAbility(nextGCD, out act);
+ }
+ #endregion
+
+ #region GCD Logic
+
+ protected override bool EmergencyGCD(out IAction? act)
+ {
+ return base.EmergencyGCD(out act);
+ }
+
+ protected override bool MyInterruptGCD(out IAction? act)
+ {
+ if (FlyingSardinePvE.CanUse(out act)) return true;
+ return base.MyInterruptGCD(out act);
+ }
+
+ protected override bool DefenseAreaGCD(out IAction? act)
+ {
+ //if (ColdFogPvE.CanUse(out act)) return true;
+ return base.DefenseAreaGCD(out act);
+ }
+
+ protected override bool DefenseSingleGCD(out IAction? act)
+ {
+ return base.DefenseSingleGCD(out act);
+ }
+
+ protected override bool HealAreaGCD(out IAction? act)
+ {
+ if (BluHealSpellActions[HealSpell].CanUse(out act)) return true;
+ return base.HealAreaGCD(out act);
+ }
+
+ protected override bool HealSingleGCD(out IAction? act)
+ {
+ if (BluHealSpellActions[HealSpell].CanUse(out act)) return true;
+ return base.HealSingleGCD(out act);
+ }
+
+ protected override bool MoveForwardGCD(out IAction? act)
+ {
+ return base.MoveForwardGCD(out act);
+ }
+
+ protected override bool DispelGCD(out IAction? act)
+ {
+ return base.DispelGCD(out act);
+ }
+
+ protected override bool RaiseGCD(out IAction? act)
+ {
+ //if (AngelWhisperPvE.CanUse(out act)) return true;
+ return base.RaiseGCD(out act);
+ }
+
+ protected override bool GeneralGCD(out IAction? act)
+ {
+ if (UseMightyGuard && MightyGuardPvE.CanUse(out act)) return true;
+ if (UseBasicInstinct && BasicInstinctPvE.CanUse(out act)) return true;
+ if (BluAOESpellActions[AoeSpell].CanUse(out act)) return true;
+ if (BluDPSSpellActions[SingleTargetDPSSpell].CanUse(out act)) return true;
+ if (FlyingSardinePvE.CanUse(out act)) return true;
+ return base.GeneralGCD(out act);
+ }
+ #endregion
+}
\ No newline at end of file
diff --git a/RotationSolver.Basic/Actions/ActionBasicInfo.cs b/RotationSolver.Basic/Actions/ActionBasicInfo.cs
index a8ad3da6c..15445a3a9 100644
--- a/RotationSolver.Basic/Actions/ActionBasicInfo.cs
+++ b/RotationSolver.Basic/Actions/ActionBasicInfo.cs
@@ -1,6 +1,7 @@
using ECommons.ExcelServices;
using ECommons.GameHelpers;
using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
namespace RotationSolver.Basic.Actions;
@@ -150,9 +151,7 @@ internal readonly bool BasicCheck(bool skipStatusProvideCheck, bool skipComboChe
{
if (!IsActionEnabled() || !IsOnSlot) return false;
if (IsLimitBreak) return true;
- if (IsActionDisabled() || !EnoughLevel || !HasEnoughMP() || !IsQuestUnlocked()) return false;
-
- var player = Player.Object;
+ if (IsActionDisabled() || !EnoughLevel || !HasEnoughMP() || !SpellUnlocked) return false;
if (IsStatusNeeded() || IsStatusProvided(skipStatusProvideCheck)) return false;
if (IsLimitBreakLevelLow() || !IsComboValid(skipComboCheck) || !IsRoleActionValid()) return false;
@@ -163,24 +162,17 @@ internal readonly bool BasicCheck(bool skipStatusProvideCheck, bool skipComboChe
return true;
}
+ ///
+ /// Whether the spell is unlocked by the player
+ ///
+ public unsafe bool SpellUnlocked => _action.Action.UnlockLink.RowId <= 0 || UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(_action.Action.UnlockLink.RowId);
+
private bool IsActionEnabled() => _action.Config?.IsEnabled ?? false;
private bool IsActionDisabled() => !IBaseAction.ForceEnable && (DataCenter.DisabledActionSequencer?.Contains(ID) ?? false);
private bool HasEnoughMP() => DataCenter.CurrentMp >= MPNeed;
- private bool IsQuestUnlocked()
- {
- if (_action.Setting.UnlockedByQuestID == 0) return true;
- var isUnlockQuestComplete = QuestManager.IsQuestComplete(_action.Setting.UnlockedByQuestID);
- if (!isUnlockQuestComplete)
- {
- var warning = $"The action {Name} is locked by the quest {_action.Setting.UnlockedByQuestID}. Please complete this quest to learn this action.";
- WarningHelper.AddSystemWarning(warning);
- }
- return isUnlockQuestComplete;
- }
-
private bool IsStatusNeeded()
{
var player = Player.Object;
diff --git a/RotationSolver.Basic/Rotations/Basic/BlueMageRotation.cs b/RotationSolver.Basic/Rotations/Basic/BlueMageRotation.cs
index ce3d91aab..0035fd67a 100644
--- a/RotationSolver.Basic/Rotations/Basic/BlueMageRotation.cs
+++ b/RotationSolver.Basic/Rotations/Basic/BlueMageRotation.cs
@@ -1,7 +1,58 @@
-namespace RotationSolver.Basic.Rotations.Basic;
+using ECommons;
+
+namespace RotationSolver.Basic.Rotations.Basic;
partial class BlueMageRotation
{
+ public enum BluDPSSpell : byte
+ {
+ WaterCannon,
+ SonicBoom,
+ GoblinPunch,
+
+ }
+
+ public enum BluAOESpell : byte
+ {
+ Glower,
+ FlyingFrenzy,
+ FlameThrower,
+ DrillCannons,
+ Plaincracker,
+ HighVoltage,
+ MindBlast,
+ ThousandNeedles,
+ }
+
+ public enum BluHealSpell : byte
+ {
+ WhiteWind,
+ AngelsSnack,
+ }
+
+ public BlueMageRotation()
+ {
+ BluDPSSpellActions.Add(BluDPSSpell.WaterCannon, WaterCannonPvE);
+ BluDPSSpellActions.Add(BluDPSSpell.SonicBoom, SonicBoomPvE);
+ BluDPSSpellActions.Add(BluDPSSpell.GoblinPunch, GoblinPunchPvE);
+
+ BluHealSpellActions.Add(BluHealSpell.WhiteWind, WhiteWindPvE);
+ BluHealSpellActions.Add(BluHealSpell.AngelsSnack, AngelsSnackPvE);
+
+ BluAOESpellActions.Add(BluAOESpell.Glower, GlowerPvE);
+ BluAOESpellActions.Add(BluAOESpell.FlyingFrenzy, FlyingFrenzyPvE);
+ BluAOESpellActions.Add(BluAOESpell.FlameThrower, FlameThrowerPvE);
+ BluAOESpellActions.Add(BluAOESpell.DrillCannons, DrillCannonsPvE);
+ BluAOESpellActions.Add(BluAOESpell.Plaincracker, PlaincrackerPvE);
+ BluAOESpellActions.Add(BluAOESpell.HighVoltage, HighVoltagePvE);
+ BluAOESpellActions.Add(BluAOESpell.MindBlast, MindBlastPvE);
+ BluAOESpellActions.Add(BluAOESpell.ThousandNeedles, _1000NeedlesPvE);
+ }
+
+ public Dictionary BluDPSSpellActions = [];
+ public Dictionary BluAOESpellActions = [];
+ public Dictionary BluHealSpellActions = [];
+
///
///
///
@@ -30,10 +81,8 @@ public enum BLUID : byte
DPS,
}
- ///
- ///
- ///
- protected static BLUID BlueId { get; set; } = BLUID.DPS;
+ [RotationConfig(CombatType.PvE, Name = "Aetheric Mimicry Role")]
+ public static BLUID BlueId { get; set; } = BLUID.DPS;
static partial void ModifySongOfTormentPvE(ref ActionSetting setting)
{
@@ -214,6 +263,12 @@ static partial void ModifySeaShantyPvE(ref ActionSetting setting)
}
+ static partial void ModifyMightyGuardPvE(ref ActionSetting setting)
+ {
+ setting.IsFriendly = true;
+ setting.StatusProvide = [StatusID.MightyGuard];
+ }
+
static partial void ModifyBeingMortalPvE(ref ActionSetting setting)
{
setting.IsFriendly = false;
@@ -223,6 +278,15 @@ static partial void ModifyBeingMortalPvE(ref ActionSetting setting)
};
}
+ static partial void ModifyGlowerPvE(ref ActionSetting setting)
+ {
+ setting.IsFriendly = false;
+ setting.CreateConfig = () => new ActionConfig()
+ {
+ AoeCount = 1,
+ };
+ }
+
//Optional
static partial void ModifyFlyingSardinePvE(ref ActionSetting setting)
@@ -230,11 +294,27 @@ static partial void ModifyFlyingSardinePvE(ref ActionSetting setting)
}
+ static partial void ModifyMindBlastPvE(ref ActionSetting setting)
+ {
+ setting.CreateConfig = () => new ActionConfig()
+ {
+ AoeCount = 3,
+ };
+ setting.IsFriendly = false;
+ }
+
static partial void ModifyWhiteWindPvE(ref ActionSetting setting)
{
setting.IsFriendly = true;
}
+ static partial void ModifyBasicInstinctPvE(ref ActionSetting setting)
+ {
+ setting.IsFriendly = true;
+ setting.StatusProvide = [StatusID.BasicInstinct];
+ setting.ActionCheck = () => IsInDuty && PartyMembers.Count() <= 1 && DataCenter.TerritoryContentType != TerritoryContentType.TheMaskedCarnivale;
+ }
+
///
///
///
@@ -353,7 +433,7 @@ public override void DisplayStatus()
// public static IBLUAction FireAngon { get; } = new BLUAction(ActionID.FireAngon);
// ///
-// ///
+// ///
// ///
// public static IBLUAction MindBlast { get; } = new BLUAction(ActionID.MindBlast);
diff --git a/RotationSolver/Helpers/DownloadHelper.cs b/RotationSolver/Helpers/DownloadHelper.cs
index 33062e166..d4f6ddfae 100644
--- a/RotationSolver/Helpers/DownloadHelper.cs
+++ b/RotationSolver/Helpers/DownloadHelper.cs
@@ -1,4 +1,5 @@
-using RotationSolver.UI;
+using ECommons.DalamudServices;
+using RotationSolver.UI;
namespace RotationSolver.Helpers;
diff --git a/RotationSolver/Helpers/RotationLoadContext.cs b/RotationSolver/Helpers/RotationLoadContext.cs
index 2cb83af9f..5f6f983e9 100644
--- a/RotationSolver/Helpers/RotationLoadContext.cs
+++ b/RotationSolver/Helpers/RotationLoadContext.cs
@@ -2,6 +2,7 @@
using Lumina.Excel;
//using Lumina.Excel.CustomSheets;
using System.Runtime.Loader;
+using ECommons.DalamudServices;
namespace RotationSolver.Helpers;
diff --git a/RotationSolver/UI/RotationConfigWindow.cs b/RotationSolver/UI/RotationConfigWindow.cs
index 0835a6ef0..a9ee8b160 100644
--- a/RotationSolver/UI/RotationConfigWindow.cs
+++ b/RotationSolver/UI/RotationConfigWindow.cs
@@ -1634,6 +1634,7 @@ static void DrawActionDebug()
ImGui.Text($"Can Use: {action.CanUse(out _)} ");
ImGui.Text("IgnoreCastCheck:" + action.CanUse(out _, skipCastingCheck: true).ToString());
ImGui.Text("Target Name: " + action.Target.Target?.Name ?? string.Empty);
+ ImGui.Text($"SpellUnlocked: {action.Info.SpellUnlocked} ({action.Action.UnlockLink.RowId})");
}
catch
{
diff --git a/RotationSolver/Updaters/PreviewUpdater.cs b/RotationSolver/Updaters/PreviewUpdater.cs
index e61424571..2fc415ca4 100644
--- a/RotationSolver/Updaters/PreviewUpdater.cs
+++ b/RotationSolver/Updaters/PreviewUpdater.cs
@@ -43,6 +43,7 @@ private static void UpdateEntry()
Job.SMN => BitmapFontIcon.Summoner,
Job.RDM => BitmapFontIcon.RedMage,
Job.PCT => BitmapFontIcon.Pictomancer,
+ Job.BLU => BitmapFontIcon.BlueMage,
Job.MNK => BitmapFontIcon.Monk,
Job.SAM => BitmapFontIcon.Samurai,
@@ -54,6 +55,20 @@ private static void UpdateEntry()
Job.BRD => BitmapFontIcon.Bard,
Job.MCH => BitmapFontIcon.Machinist,
Job.DNC => BitmapFontIcon.Dancer,
+
+ Job.BSM => BitmapFontIcon.Blacksmith,
+ Job.ARM => BitmapFontIcon.Armorer,
+ Job.WVR => BitmapFontIcon.Weaver,
+ Job.ALC => BitmapFontIcon.Alchemist,
+ Job.CRP => BitmapFontIcon.Carpenter,
+ Job.LTW => BitmapFontIcon.Leatherworker,
+ Job.CUL => BitmapFontIcon.Culinarian,
+ Job.GSM => BitmapFontIcon.Goldsmith,
+
+ Job.FSH => BitmapFontIcon.Fisher,
+ Job.MIN => BitmapFontIcon.Miner,
+ Job.BTN => BitmapFontIcon.Botanist,
+
_ => BitmapFontIcon.ExclamationRectangle,
};