├── .gitignore ├── .mailmap ├── res ├── eno_swap.gif ├── souleater_combo.gif └── hypercharge_heat_blast.gif ├── dalamud-plugin-ui-colours.png ├── XIVComboVX ├── Attributes │ ├── DangerousAttribute.cs │ ├── ExperimentalAttribute.cs │ ├── ConflictsAttribute.cs │ ├── ParentPresetAttribute.cs │ ├── LucidWeavingSettingAttribute.cs │ ├── DeprecatedAttribute.cs │ ├── ComboDetailSettingAttribute.cs │ └── CustomComboInfoAttribute.cs ├── DalamudPackager.targets ├── XIVComboVX.json ├── LogTag.cs ├── Combos │ ├── DOH.cs │ ├── AST.cs │ ├── Common.cs │ ├── MNK.cs │ ├── VPR.cs │ ├── DRK.cs │ ├── PCT.cs │ ├── SCH.cs │ ├── WHM.cs │ ├── PLD.cs │ ├── SMN.cs │ ├── WAR.cs │ ├── SAM.cs │ ├── DRG.cs │ ├── BLM.cs │ ├── BRD.cs │ ├── NIN.cs │ ├── MCH.cs │ ├── DOL.cs │ └── DNC.cs ├── GlobalSuppressions.cs ├── XIVComboVX.csproj ├── packages.lock.json ├── dalamud.props ├── GameData │ ├── GameState.cs │ ├── Labels.cs │ ├── PluginAddressResolver.cs │ ├── IconReplacer.cs │ └── CooldownData.cs ├── SingleTickLogger.cs ├── Service.cs ├── ChatUtil.cs ├── Config │ ├── ComboDetailSetting.cs │ └── UpdateAlerter.cs ├── Ipc.cs └── framework.props ├── .github └── workflows │ ├── stale.yml │ ├── pr-test.yml │ └── deploy.yml ├── XIVComboVX.sln └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | bin/ 3 | obj/ 4 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Aryanna Morgan 2 | 3 | -------------------------------------------------------------------------------- /res/eno_swap.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VariableVixen/XIVComboPlugin/HEAD/res/eno_swap.gif -------------------------------------------------------------------------------- /res/souleater_combo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VariableVixen/XIVComboPlugin/HEAD/res/souleater_combo.gif -------------------------------------------------------------------------------- /dalamud-plugin-ui-colours.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VariableVixen/XIVComboPlugin/HEAD/dalamud-plugin-ui-colours.png -------------------------------------------------------------------------------- /res/hypercharge_heat_blast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VariableVixen/XIVComboPlugin/HEAD/res/hypercharge_heat_blast.gif -------------------------------------------------------------------------------- /XIVComboVX/Attributes/DangerousAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VariableVixen.XIVComboVX.Attributes; 4 | 5 | [AttributeUsage(AttributeTargets.Field)] 6 | internal class DangerousAttribute: Attribute { } 7 | -------------------------------------------------------------------------------- /XIVComboVX/Attributes/ExperimentalAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VariableVixen.XIVComboVX.Attributes; 4 | 5 | [AttributeUsage(AttributeTargets.Field)] 6 | internal class ExperimentalAttribute: Attribute { } 7 | -------------------------------------------------------------------------------- /XIVComboVX/DalamudPackager.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /XIVComboVX/Attributes/ConflictsAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VariableVixen.XIVComboVX.Attributes; 4 | 5 | [AttributeUsage(AttributeTargets.Field)] 6 | internal class ConflictsAttribute: Attribute { 7 | public CustomComboPreset[] Conflicts { get; } 8 | internal ConflictsAttribute(params CustomComboPreset[] conflictingPresets) => this.Conflicts = conflictingPresets; 9 | } 10 | -------------------------------------------------------------------------------- /XIVComboVX/Attributes/ParentPresetAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using VariableVixen.XIVComboVX; 4 | 5 | namespace VariableVixen.XIVComboVX.Attributes; 6 | 7 | [AttributeUsage(AttributeTargets.Field)] 8 | internal class ParentPresetAttribute: Attribute { 9 | public CustomComboPreset Parent { get; } 10 | internal ParentPresetAttribute(CustomComboPreset required) => this.Parent = required; 11 | } 12 | -------------------------------------------------------------------------------- /XIVComboVX/XIVComboVX.json: -------------------------------------------------------------------------------- 1 | { 2 | "Author": "attick, daemitus, VariableVixen", 3 | "Name": "XIV Combo Very Expanded", 4 | "Description": "This plugin condenses combos and mutually exclusive abilities onto a single button. To a degree that some would call absurd, but that's why it's all configurable. Let the players decide.", 5 | "RepoUrl": "https://github.com/VariableVixen/XIVComboPlugin", 6 | "Punchline": "Why can't I hold all these buttons?" 7 | } 8 | -------------------------------------------------------------------------------- /XIVComboVX/LogTag.cs: -------------------------------------------------------------------------------- 1 | namespace VariableVixen.XIVComboVX; 2 | 3 | public static class LogTag { 4 | public const string 5 | Ipc = "[IPCMESG]", 6 | Combo = "[REPLACE]", 7 | StatusEffect = "[EFFECTS]", 8 | DataCache = "[CACHING]", 9 | ConfigWindow = "[CONFWIN]", 10 | ConfigCleanup = "[CLEANUP]", 11 | ConfigUpgrade = "[UPGRADE]", 12 | UpdateAlert = "[UPDATER]", 13 | CoreSetup = "[CORINIT]", 14 | SignatureScan = "[ADDRESS]", 15 | SingleTick = "[TICKLOG]"; 16 | } 17 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/DOH.cs: -------------------------------------------------------------------------------- 1 | namespace VariableVixen.XIVComboVX.Combos; 2 | 3 | internal static class DOH { 4 | public const byte JobID = 98; 5 | 6 | public const uint 7 | Placeholder = 0; 8 | 9 | public static class Buffs { 10 | public const ushort 11 | Placeholder = 0; 12 | } 13 | 14 | public static class Debuffs { 15 | public const ushort 16 | Placeholder = 0; 17 | } 18 | 19 | public static class Levels { 20 | public const byte 21 | Placeholder = 0; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /XIVComboVX/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "Standard formatting layout for combo classes")] 9 | [assembly: SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "it's a stupid rule")] 10 | -------------------------------------------------------------------------------- /XIVComboVX/XIVComboVX.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | XIVComboVX 5 | VariableVixen, FrigidWalrus, attick, daemitus 6 | 9.33.9 7 | This plugin condenses various abilities onto single buttons. 8 | https://github.com/VariableVixen/XIVComboPlugin 9 | Copyleft attick 2020 baybeeee 10 | net10.0-windows7.0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /XIVComboVX/Attributes/LucidWeavingSettingAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VariableVixen.XIVComboVX.Attributes; 4 | 5 | [AttributeUsage(AttributeTargets.Property)] 6 | internal class LucidWeavingSettingAttribute(CustomComboPreset combo): ComboDetailSettingAttribute(combo, LucidWeaveName, LucidWeaveDesc, LucidWeaveManaMin, LucidWeaveManaMax) { 7 | internal const int 8 | LucidWeaveManaMin = 1000, 9 | LucidWeaveManaMax = 8000; 10 | internal const string 11 | LucidWeaveName = "MP threshold", 12 | LucidWeaveDesc = "When your MP is below this limit, change this button into Lucid Dreaming while weaving\n(LD restores about one third of maximum MP, for reference)"; 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '23 */12 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v8 11 | with: 12 | days-before-issue-stale: 30 13 | days-before-pr-stale: 60 14 | stale-issue-message: "There has been no activity on this issue for 30 days. If there is no change in the next week, it will be closed automatically." 15 | close-issue-message: "" 16 | stale-pr-message: "There has been no activity on this PR for 60 days. If there is no change in the next week, it will be closed automatically." 17 | close-pr-message: "" 18 | exempt-all-milestones: true 19 | exempt-draft-pr: true -------------------------------------------------------------------------------- /XIVComboVX/Attributes/DeprecatedAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | using VariableVixen.XIVComboVX; 5 | 6 | namespace VariableVixen.XIVComboVX.Attributes; 7 | 8 | [AttributeUsage(AttributeTargets.Field)] 9 | internal class DeprecatedAttribute: Attribute { 10 | public const string Explanation = "Deprecated replacers are no longer being actively updated, and should be considered outdated. They may be removed in future versions."; 11 | 12 | public CustomComboPreset[] Recommended { get; } 13 | public string Information { get; } 14 | internal DeprecatedAttribute(string label, params CustomComboPreset[] suggestions) { 15 | this.Information = label; 16 | this.Recommended = suggestions.OrderBy(p => (uint)p).ToArray(); 17 | } 18 | internal DeprecatedAttribute(params CustomComboPreset[] suggestions) : this(string.Empty, suggestions) { } 19 | internal DeprecatedAttribute(string label) : this(label, []) { } 20 | } 21 | -------------------------------------------------------------------------------- /XIVComboVX/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": { 4 | "net10.0-windows7.0": { 5 | "DalamudPackager": { 6 | "type": "Direct", 7 | "requested": "[14.0.0, )", 8 | "resolved": "14.0.0", 9 | "contentHash": "9c1q/eAeAs82mkQWBOaCvbt3GIQxAIadz5b/7pCXDIy9nHPtnRc+tDXEvKR+M36Wvi7n+qBTevRupkLUQp6DFA==" 10 | }, 11 | "DotNet.ReproducibleBuilds": { 12 | "type": "Direct", 13 | "requested": "[1.2.39, )", 14 | "resolved": "1.2.39", 15 | "contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg==" 16 | }, 17 | "Microsoft.NET.ILLink.Tasks": { 18 | "type": "Direct", 19 | "requested": "[10.0.1, )", 20 | "resolved": "10.0.1", 21 | "contentHash": "ISahzLHsHY7vrwqr2p1YWZ+gsxoBRtH7gWRDK8fDUst9pp2He0GiesaqEfeX0V8QMCJM3eNEHGGpnIcPjFo2NQ==" 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /XIVComboVX/dalamud.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | x64 6 | x64 7 | net10.0-windows7.0 8 | Library 9 | false 10 | true 11 | false 12 | 13 | 14 | 15 | 16 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /XIVComboVX/GameData/GameState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Dalamud.Game.NativeWrapper; 4 | 5 | using FFXIVClientStructs.FFXIV.Component.GUI; 6 | 7 | namespace VariableVixen.XIVComboVX.GameData; 8 | 9 | internal unsafe class GameState: IDisposable { 10 | private bool disposed; 11 | 12 | private AtkUnitBasePtr chatLogPointer; 13 | 14 | internal AtkUnitBasePtr ChatLog { 15 | get { 16 | if (Service.ObjectTable.LocalPlayer is null) 17 | return null; 18 | if (this.chatLogPointer.IsNull) 19 | this.chatLogPointer = Service.GameGui.GetAddonByName("ChatLog", 1); 20 | return this.chatLogPointer; 21 | } 22 | } 23 | 24 | internal bool IsChatVisible => this.ChatLog.IsVisible; 25 | 26 | #region Registration and cleanup 27 | 28 | internal GameState() => Service.Client.Logout += this.clearCacheOnLogout; 29 | 30 | private void clearCacheOnLogout(int type, int code) => this.chatLogPointer = null; 31 | 32 | public void Dispose() { 33 | if (this.disposed) 34 | return; 35 | this.disposed = true; 36 | 37 | Service.Client.Logout -= this.clearCacheOnLogout; 38 | this.chatLogPointer = null; 39 | } 40 | 41 | #endregion 42 | 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/pr-test.yml: -------------------------------------------------------------------------------- 1 | name: Compile 2 | 3 | # Put your personal access token in a repository secret named PAT for cross-repository access 4 | permissions: 5 | contents: write 6 | 7 | on: 8 | workflow_dispatch: 9 | pull_request: 10 | paths-ignore: 11 | - '*.md' 12 | branches: 13 | - master 14 | - main 15 | - release 16 | 17 | env: 18 | INTERNAL_NAME: XIVComboVX 19 | CONFIGURATION: release 20 | DOTNET_CLI_TELEMETRY_OPTOUT: true 21 | DOTNET_NOLOGO: true 22 | 23 | jobs: 24 | build: 25 | runs-on: windows-2022 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | with: 30 | submodules: recursive 31 | - name: Setup MSBuild 32 | uses: microsoft/setup-msbuild@v1.1.3 33 | - name: Download Dalamud 34 | run: | 35 | Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip 36 | Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev\" 37 | - name: Restore 38 | run: dotnet restore -r win 39 | - name: Build 40 | run: dotnet build -c ${{ env.CONFIGURATION }} --no-restore 41 | - name: Upload build 42 | uses: actions/upload-artifact@v4 43 | with: 44 | name: PluginRepoZip 45 | path: ${{ env.INTERNAL_NAME }}/bin/${{ env.CONFIGURATION }}/portable/${{ env.INTERNAL_NAME }} 46 | if-no-files-found: error 47 | retention-days: 14 48 | -------------------------------------------------------------------------------- /XIVComboVX/Attributes/ComboDetailSettingAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using VariableVixen.XIVComboVX; 4 | 5 | namespace VariableVixen.XIVComboVX.Attributes; 6 | 7 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] 8 | internal class ComboDetailSettingAttribute: Attribute { 9 | public CustomComboPreset Combo { get; } 10 | public double Min { get; } = float.MinValue; 11 | public double Max { get; } = float.MaxValue; 12 | public int Precision { get; } = 0; 13 | 14 | public string Label { get; } 15 | public string? Description { get; } 16 | 17 | public ComboDetailSettingAttribute(CustomComboPreset combo, string lbl, string? tooltip, double minimum, double maximum, int decimals) { 18 | this.Combo = combo; 19 | this.Label = lbl; 20 | this.Description = tooltip; 21 | if (decimals >= 0) 22 | this.Precision = decimals; 23 | if (!double.IsNaN(minimum) && double.IsFinite(minimum)) 24 | this.Min = minimum; 25 | if (!double.IsNaN(maximum) && double.IsFinite(maximum)) 26 | this.Max = maximum; 27 | } 28 | public ComboDetailSettingAttribute(CustomComboPreset combo, string lbl, string? tooltip, double minimum, double maximum) : this(combo, lbl, tooltip, minimum, maximum, 2) { } 29 | public ComboDetailSettingAttribute(CustomComboPreset combo, string lbl, string? tooltip) : this(combo, lbl, tooltip, double.NaN, double.NaN) { } 30 | public ComboDetailSettingAttribute(CustomComboPreset combo, string lbl) : this(combo, lbl, null) { } 31 | } 32 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/AST.cs: -------------------------------------------------------------------------------- 1 | namespace VariableVixen.XIVComboVX.Combos; 2 | 3 | internal static class AST { 4 | public const byte JobID = 33; 5 | 6 | public const uint 7 | Ascend = 3603, 8 | Benefic = 3594, 9 | Malefic = 3596, 10 | Malefic2 = 3598, 11 | Lightspeed = 3606, 12 | Benefic2 = 3610, 13 | Synastry = 3612, 14 | CollectiveUnconscious = 3613, 15 | Gravity = 3615, 16 | Balance = 4401, 17 | Bole = 4404, 18 | Arrow = 4402, 19 | Spear = 4403, 20 | Ewer = 4405, 21 | Spire = 4406, 22 | EarthlyStar = 7439, 23 | Malefic3 = 7442, 24 | MinorArcana = 7443, 25 | SleeveDraw = 7448, 26 | Divination = 16552, 27 | CelestialOpposition = 16553, 28 | Malefic4 = 16555, 29 | Horoscope = 16557, 30 | NeutralSect = 16559, 31 | FallMalefic = 25871, 32 | Gravity2 = 25872, 33 | Exaltation = 25873, 34 | Macrocosmos = 25874; 35 | 36 | public static class Buffs { 37 | public const ushort 38 | LordOfCrownsDrawn = 2054, 39 | LadyOfCrownsDrawn = 2055, 40 | ClarifyingDraw = 2713; 41 | } 42 | 43 | public static class Debuffs { 44 | // public const ushort placeholder = 0; 45 | } 46 | 47 | public static class Levels { 48 | public const byte 49 | Benefic2 = 26, 50 | Draw = 30, 51 | Astrodyne = 50, 52 | MinorArcana = 70, 53 | CrownPlay = 70; 54 | } 55 | } 56 | 57 | internal class AstrologianSwiftcastRaiserFeature: SwiftRaiseCombo { 58 | public override CustomComboPreset Preset => CustomComboPreset.AstrologianSwiftcastRaiserFeature; 59 | } 60 | 61 | 62 | internal class AstrologianBeneficFeature: CustomCombo { 63 | public override CustomComboPreset Preset => CustomComboPreset.AstrologianBeneficFeature; 64 | public override uint[] ActionIDs { get; } = [AST.Benefic2]; 65 | 66 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 67 | 68 | if (level < AST.Levels.Benefic2) 69 | return AST.Benefic; 70 | 71 | return actionID; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /XIVComboVX/GameData/Labels.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | 4 | using Lumina.Excel; 5 | 6 | using GameAction = Lumina.Excel.Sheets.Action; 7 | using GameStatus = Lumina.Excel.Sheets.Status; 8 | 9 | namespace VariableVixen.XIVComboVX.GameData; 10 | 11 | internal static class Labels { 12 | private static readonly Dictionary actions = []; 13 | private static readonly Dictionary effects = []; 14 | 15 | internal static string Action(uint actionID) { 16 | if (actionID is 0) 17 | return "(no action)#0"; 18 | if (!actions.TryGetValue(actionID, out string? label) || string.IsNullOrWhiteSpace(label)) 19 | return $"(unknown action)#{actionID}"; 20 | return $"(action {label})#{actionID}"; 21 | } 22 | internal static string Status(uint statusID) { 23 | if (statusID is 0) 24 | return "(no status)#0"; 25 | if (!effects.TryGetValue(statusID, out string? label) || string.IsNullOrWhiteSpace(label)) 26 | return $"(unknown status)#{statusID}"; 27 | return $"(status {label})#{statusID}"; 28 | } 29 | 30 | internal static void Load() { 31 | Stopwatch timer = new(); 32 | 33 | Service.Log.Info($"{LogTag.CoreSetup} Indexing player action names"); 34 | timer.Restart(); 35 | ExcelSheet actionSheet = Service.DataManager.GetExcelSheet(); 36 | foreach (GameAction row in actionSheet) { 37 | actions[row.RowId] = row.Name.ExtractText(); 38 | } 39 | timer.Stop(); 40 | Service.Log.Info($"{LogTag.CoreSetup} Indexed {actions.Count} player actions in {timer.ElapsedMilliseconds}ms"); 41 | 42 | Service.Log.Info($"{LogTag.CoreSetup} Indexing status effect names"); 43 | timer.Restart(); 44 | ExcelSheet effectSheet = Service.DataManager.GetExcelSheet(); 45 | foreach (GameStatus row in effectSheet) { 46 | effects[row.RowId] = row.Name.ExtractText(); 47 | } 48 | timer.Stop(); 49 | Service.Log.Info($"{LogTag.CoreSetup} Indexed {effects.Count} status effects in {timer.ElapsedMilliseconds}ms"); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/Common.cs: -------------------------------------------------------------------------------- 1 | namespace VariableVixen.XIVComboVX.Combos; 2 | 3 | internal static class Common { 4 | public const uint 5 | // everyone 6 | Sprint = 4, 7 | // melee DPS 8 | Bloodbath = 7542, 9 | SecondWind = 7541, 10 | // ranged DPS 11 | Peloton = 7557, 12 | // tanks 13 | LowBlow = 7540, 14 | Interject = 7538, 15 | // mages 16 | Swiftcast = 7561, 17 | // healers 18 | LucidDreaming = 7562; 19 | 20 | internal static class Buffs { 21 | public const ushort 22 | Swiftcast1 = 167, 23 | Swiftcast2 = 1325, 24 | Swiftcast3 = 1987, 25 | LostChainspell = 2560; 26 | } 27 | 28 | internal static class Levels { 29 | public const uint 30 | Bloodbath = 12, 31 | SecondWind = 8, 32 | Peloton = 20, 33 | LucidDreaming = 14, 34 | Swiftcast = 18; 35 | } 36 | } 37 | 38 | internal abstract class SwiftRaiseCombo: CustomCombo { 39 | public override uint[] ActionIDs { get; } = [AST.Ascend, RDM.Verraise, SCH.Resurrection, SGE.Egeiro, SMN.Resurrection, WHM.Raise]; 40 | 41 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 42 | return level >= Common.Levels.Swiftcast && ShouldSwiftcast 43 | ? Common.Swiftcast 44 | : actionID; 45 | } 46 | } 47 | 48 | internal abstract class StunInterruptCombo: CustomCombo { 49 | public override uint[] ActionIDs { get; } = [Common.LowBlow, Common.Interject]; 50 | 51 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 52 | return CanInterrupt && IsOffCooldown(Common.Interject) 53 | ? Common.Interject 54 | : Common.LowBlow; 55 | } 56 | } 57 | 58 | internal abstract class SecondBloodbathCombo: CustomCombo { 59 | public override uint[] ActionIDs { get; } = [Common.Bloodbath, Common.SecondWind]; 60 | 61 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 62 | if (level < Common.Levels.Bloodbath) 63 | return Common.SecondWind; 64 | 65 | return PickByCooldown(actionID, Common.Bloodbath, Common.SecondWind); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /XIVComboVX/Attributes/CustomComboInfoAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | using VariableVixen.XIVComboVX.Combos; 5 | 6 | namespace VariableVixen.XIVComboVX.Attributes; 7 | 8 | [AttributeUsage(AttributeTargets.Field)] 9 | internal class CustomComboInfoAttribute: Attribute { 10 | internal CustomComboInfoAttribute(string fancyName, string description, byte jobID, [CallerLineNumber] int order = 0) { 11 | this.FancyName = fancyName; 12 | this.Description = description; 13 | this.JobID = jobID; 14 | this.Order = order; 15 | } 16 | 17 | public string FancyName { get; } 18 | public string Description { get; } 19 | public byte JobID { get; } 20 | public string JobName => jobIdToName(this.JobID); 21 | public int Order { get; } 22 | 23 | private static string jobIdToName(byte key) { // TODO replace this with a lumina lookup to the ClassJob sheet 24 | return key switch { 25 | 0 => "Universal", 26 | 1 => "Gladiator", 27 | 2 => "Pugilist", 28 | 3 => "Marauder", 29 | 4 => "Lancer", 30 | 5 => "Archer", 31 | 6 => "Conjurer", 32 | 7 => "Thaumaturge", 33 | 8 => "Carpenter", 34 | 9 => "Blacksmith", 35 | 10 => "Armorer", 36 | 11 => "Goldsmith", 37 | 12 => "Leatherworker", 38 | 13 => "Weaver", 39 | 14 => "Alchemist", 40 | 15 => "Culinarian", 41 | 16 => "Miner", 42 | 17 => "Botanist", 43 | 18 => "Fisher", 44 | 19 => "Paladin", 45 | 20 => "Monk", 46 | 21 => "Warrior", 47 | 22 => "Dragoon", 48 | 23 => "Bard", 49 | 24 => "White Mage", 50 | 25 => "Black Mage", 51 | 26 => "Arcanist", 52 | 27 => "Summoner", 53 | 28 => "Scholar", 54 | 29 => "Rogue", 55 | 30 => "Ninja", 56 | 31 => "Machinist", 57 | 32 => "Dark Knight", 58 | 33 => "Astrologian", 59 | 34 => "Samurai", 60 | 35 => "Red Mage", 61 | 36 => "Blue Mage", 62 | 37 => "Gunbreaker", 63 | 38 => "Dancer", 64 | 39 => "Reaper", 65 | 40 => "Sage", 66 | 41 => "Viper", 67 | 42 => "Pictomancer", 68 | DOL.JobID => "Disciple of the Land", 69 | DOH.JobID => "Disciple of the Hand", 70 | _ => "Unknown", 71 | }; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /XIVComboVX.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.2.32616.157 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XIVComboVX", "XIVComboVX/XIVComboVX.csproj", "{619DF476-7225-4783-95A5-D29A8816EE66}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8948C02A-914E-4A51-B1FE-2C608492EBF5}" 8 | ProjectSection(SolutionItems) = preProject 9 | .gitignore = .gitignore 10 | EndProjectSection 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Debug|x64 = Debug|x64 16 | Debug|x86 = Debug|x86 17 | Release|Any CPU = Release|Any CPU 18 | Release|x64 = Release|x64 19 | Release|x86 = Release|x86 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {619DF476-7225-4783-95A5-D29A8816EE66}.Debug|Any CPU.ActiveCfg = Debug|x64 23 | {619DF476-7225-4783-95A5-D29A8816EE66}.Debug|Any CPU.Build.0 = Debug|x64 24 | {619DF476-7225-4783-95A5-D29A8816EE66}.Debug|x64.ActiveCfg = Debug|Any CPU 25 | {619DF476-7225-4783-95A5-D29A8816EE66}.Debug|x64.Build.0 = Debug|Any CPU 26 | {619DF476-7225-4783-95A5-D29A8816EE66}.Debug|x86.ActiveCfg = Debug|Any CPU 27 | {619DF476-7225-4783-95A5-D29A8816EE66}.Debug|x86.Build.0 = Debug|Any CPU 28 | {619DF476-7225-4783-95A5-D29A8816EE66}.Release|Any CPU.ActiveCfg = Release|x64 29 | {619DF476-7225-4783-95A5-D29A8816EE66}.Release|Any CPU.Build.0 = Release|x64 30 | {619DF476-7225-4783-95A5-D29A8816EE66}.Release|x64.ActiveCfg = Release|Any CPU 31 | {619DF476-7225-4783-95A5-D29A8816EE66}.Release|x64.Build.0 = Release|Any CPU 32 | {619DF476-7225-4783-95A5-D29A8816EE66}.Release|x86.ActiveCfg = Release|Any CPU 33 | {619DF476-7225-4783-95A5-D29A8816EE66}.Release|x86.Build.0 = Release|Any CPU 34 | EndGlobalSection 35 | GlobalSection(SolutionProperties) = preSolution 36 | HideSolutionNode = FALSE 37 | EndGlobalSection 38 | GlobalSection(ExtensibilityGlobals) = postSolution 39 | SolutionGuid = {37674B88-2038-4C63-A979-84404391773A} 40 | EndGlobalSection 41 | EndGlobal 42 | -------------------------------------------------------------------------------- /XIVComboVX/SingleTickLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | using Dalamud.Plugin.Services; 5 | 6 | namespace VariableVixen.XIVComboVX; 7 | 8 | internal class SingleTickLogger: IDisposable { 9 | public const string STACK_TRACE_MSG = "autogenerated stack trace exception"; 10 | private bool disposed; 11 | 12 | public bool Enabled { get; protected set; } = false; 13 | public bool EnabledNextTick { get; protected set; } = false; 14 | 15 | internal SingleTickLogger() => Service.Framework.Update += this.onTick; 16 | 17 | public void EnableNextTick() { 18 | this.EnabledNextTick = true; 19 | Service.Log.Information($"{LogTag.SingleTick} Enabled logging snapshot for next tick"); 20 | } 21 | 22 | private void onTick(IFramework framework) { 23 | if (this.Enabled) 24 | Service.Log.Information($"{LogTag.SingleTick} Logging snapshot complete"); 25 | if (this.EnabledNextTick) 26 | Service.Log.Information($"{LogTag.SingleTick} Beginning logging snapshot"); 27 | this.Enabled = this.EnabledNextTick; 28 | this.EnabledNextTick = false; 29 | } 30 | 31 | internal void Fatal(string msg, Exception? cause = null) { 32 | if (this.Enabled) 33 | Service.Log.Fatal($"{msg}\n{cause ?? new Exception(STACK_TRACE_MSG)}"); 34 | } 35 | internal void Error(string msg, Exception? cause = null) { 36 | if (this.Enabled) 37 | Service.Log.Error($"{msg}\n{cause ?? new Exception(STACK_TRACE_MSG)}"); 38 | } 39 | internal void Warning(string msg, Exception? cause = null) { 40 | if (this.Enabled) 41 | Service.Log.Warning($"{msg}\n{cause ?? new Exception(STACK_TRACE_MSG)}"); 42 | } 43 | internal void Info(string msg) { 44 | if (this.Enabled) 45 | Service.Log.Info(msg); 46 | } 47 | [Conditional("DEBUG")] 48 | internal void Debug(string msg) { 49 | if (this.Enabled) 50 | Service.Log.Information(msg); 51 | } 52 | [Conditional("DEBUG")] 53 | internal void Trace(string msg) { 54 | if (this.Enabled) { 55 | Service.Log.Information( 56 | #if TRACE 57 | $"{msg}\n{new StackTrace(true)}" 58 | #else 59 | msg 60 | #endif 61 | ); 62 | } 63 | } 64 | 65 | #region IDisposable 66 | 67 | public void Dispose() { 68 | if (this.disposed) 69 | return; 70 | this.disposed = true; 71 | 72 | Service.Framework.Update -= this.onTick; 73 | } 74 | 75 | #endregion 76 | 77 | } 78 | -------------------------------------------------------------------------------- /XIVComboVX/GameData/PluginAddressResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | using FFXIVClientStructs.FFXIV.Client.Game; 5 | 6 | using VariableVixen.XIVComboVX; 7 | 8 | namespace VariableVixen.XIVComboVX.GameData; 9 | 10 | internal class PluginAddressResolver { 11 | private const string AddrFmtSpec = "X16"; 12 | 13 | public Exception? LoadFailReason { get; private set; } 14 | public bool LoadSuccessful => this.LoadFailReason is null; 15 | 16 | public nint ComboTimer { get; private set; } = nint.Zero; 17 | public string ComboTimerAddr => this.ComboTimer.ToInt64().ToString(AddrFmtSpec); 18 | 19 | public nint LastComboMove => this.ComboTimer + 0x4; 20 | public string LastComboMoveAddr => this.LastComboMove.ToInt64().ToString(AddrFmtSpec); 21 | 22 | public nint IsActionIdReplaceable { get; private set; } = nint.Zero; 23 | public string IsActionIdReplaceableAddr => this.IsActionIdReplaceable.ToInt64().ToString(AddrFmtSpec); 24 | 25 | 26 | internal unsafe void Setup() { 27 | try { 28 | Service.Log.Information($"{LogTag.SignatureScan} Scanning for ComboTimer signature"); 29 | this.ComboTimer = new nint(&ActionManager.Instance()->Combo.Timer); 30 | 31 | Service.Log.Information($"{LogTag.SignatureScan} Scanning for IsActionIdReplaceable signature"); 32 | this.IsActionIdReplaceable = Service.SigScanner.ScanText("40 53 48 83 EC 20 8B D9 48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 85 C0 74 1F"); 33 | } 34 | catch (Exception ex) { 35 | this.LoadFailReason = ex; 36 | StringBuilder msg = new($"{LogTag.SignatureScan} "); 37 | msg.AppendLine("Address scanning failed, plugin cannot load."); 38 | msg.AppendLine("Please present this error message to the developer."); 39 | msg.AppendLine(); 40 | msg.Append("Signature scan failed for "); 41 | if (this.ComboTimer == nint.Zero) 42 | msg.Append("ComboTimer"); 43 | else if (this.IsActionIdReplaceable == nint.Zero) 44 | msg.Append("IsActionIdReplaceable"); 45 | msg.AppendLine(":"); 46 | msg.Append(ex.ToString()); 47 | Service.Log.Fatal(msg.ToString()); 48 | return; 49 | } 50 | 51 | Service.Log.Information($"{LogTag.SignatureScan} Address resolution successful"); 52 | 53 | Service.Log.Information($"{LogTag.SignatureScan} IsIconReplaceable 0x{this.IsActionIdReplaceableAddr}"); 54 | Service.Log.Information($"{LogTag.SignatureScan} ComboTimer 0x{this.ComboTimerAddr}"); 55 | Service.Log.Information($"{LogTag.SignatureScan} LastComboMove 0x{this.LastComboMoveAddr}"); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/MNK.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | using Dalamud.Game.ClientState.JobGauge.Enums; 5 | using Dalamud.Game.ClientState.JobGauge.Types; 6 | 7 | using VariableVixen.XIVComboVX; 8 | 9 | namespace VariableVixen.XIVComboVX.Combos; 10 | 11 | internal static class MNK { 12 | public const byte ClassID = 2; 13 | public const byte JobID = 20; 14 | 15 | public const uint 16 | Bootshine = 53, 17 | TrueStrike = 54, 18 | SnapPunch = 56, 19 | TwinSnakes = 61, 20 | ArmOfTheDestroyer = 62, 21 | Demolish = 66, 22 | PerfectBalance = 69, 23 | Rockbreaker = 70, 24 | DragonKick = 74, 25 | Meditation = 3546, 26 | RiddleOfEarth = 7394, 27 | RiddleOfFire = 7395, 28 | Brotherhood = 7396, 29 | FourPointFury = 16473, 30 | Enlightenment = 16474, 31 | HowlingFist = 25763, 32 | MasterfulBlitz = 25764, 33 | RiddleOfWind = 25766, 34 | ShadowOfTheDestroyer = 25767; 35 | 36 | public static class Buffs { 37 | public const ushort 38 | OpoOpoForm = 107, 39 | RaptorForm = 108, 40 | CoerlForm = 109, 41 | PerfectBalance = 110, 42 | FifthChakra = 797, 43 | LeadenFist = 1861, 44 | Brotherhood = 1185, 45 | RiddleOfFire = 1181, 46 | FormlessFist = 2513, 47 | DisciplinedFist = 3001; 48 | } 49 | 50 | public static class Debuffs { 51 | public const ushort 52 | Demolish = 246; 53 | } 54 | 55 | public static class Levels { 56 | public const byte 57 | TrueStrike = 4, 58 | SnapPunch = 6, 59 | Bloodbath = 12, 60 | Meditation = 15, 61 | TwinSnakes = 18, 62 | ArmOfTheDestroyer = 26, 63 | Rockbreaker = 30, 64 | Demolish = 30, 65 | FourPointFury = 45, 66 | HowlingFist = 40, 67 | DragonKick = 50, 68 | PerfectBalance = 50, 69 | FormShift = 52, 70 | EnhancedPerfectBalance = 60, 71 | MasterfulBlitz = 60, 72 | RiddleOfEarth = 64, 73 | RiddleOfFire = 68, 74 | Brotherhood = 70, 75 | Enlightenment = 70, 76 | RiddleOfWind = 72, 77 | ShadowOfTheDestroyer = 82; 78 | } 79 | } 80 | 81 | // apparently MNK had some big changes, and neither of the devs plays, cares for, or even understands MNK, so guess what doesn't have combos until someone else writes them? 82 | // Aside from one replacer, which is shared by several classes and is, in fact, a great idea. 83 | // All credit to Frigid for coming up with AND the initial implementation, which Vixen just stuck into a common class for convenience. 84 | 85 | internal class MonkBloodbathReplacer: SecondBloodbathCombo { 86 | public override CustomComboPreset Preset => CustomComboPreset.MonkBloodbathReplacer; 87 | } 88 | -------------------------------------------------------------------------------- /XIVComboVX/Service.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game; 2 | using Dalamud.Game.ClientState.Objects; 3 | using Dalamud.IoC; 4 | using Dalamud.Plugin; 5 | using Dalamud.Plugin.Services; 6 | 7 | using VariableVixen.XIVComboVX.Config; 8 | using VariableVixen.XIVComboVX.GameData; 9 | 10 | namespace VariableVixen.XIVComboVX; 11 | 12 | internal class Service { 13 | public static Plugin Plugin { get; set; } = null!; 14 | 15 | public static PluginConfiguration Configuration { get; set; } = null!; 16 | 17 | public static IconReplacer IconReplacer { get; set; } = null!; 18 | 19 | public static PluginAddressResolver Address { get; set; } = null!; 20 | 21 | public static SingleTickLogger TickLogger { get; set; } = null!; 22 | 23 | public static GameState GameState { get; set; } = null!; 24 | 25 | public static UpdateAlerter? UpdateAlert { get; set; } = null; 26 | 27 | public static ChatUtil ChatUtils { get; set; } = null!; 28 | 29 | public static Ipc Ipc { get; set; } = null!; 30 | 31 | [PluginService] public static IDataManager DataManager { get; set; } = null!; 32 | [PluginService] public static IPluginLog Log { get; private set; } = null!; 33 | [PluginService] public static IDalamudPluginInterface Interface { get; private set; } = null!; 34 | [PluginService] public static ISigScanner SigScanner { get; private set; } = null!; 35 | [PluginService] public static IBuddyList BuddyList { get; private set; } = null!; 36 | [PluginService] public static IChatGui ChatGui { get; private set; } = null!; 37 | [PluginService] public static IClientState Client { get; private set; } = null!; 38 | [PluginService] public static ICommandManager Commands { get; private set; } = null!; 39 | [PluginService] public static ICondition Conditions { get; private set; } = null!; 40 | [PluginService] public static IDataManager GameData { get; private set; } = null!; 41 | [PluginService] public static IFramework Framework { get; private set; } = null!; 42 | [PluginService] public static IJobGauges JobGauge { get; private set; } = null!; 43 | [PluginService] public static ITargetManager Targets { get; private set; } = null!; 44 | [PluginService] public static IGameGui GameGui { get; private set; } = null!; 45 | [PluginService] public static IGameInteropProvider Interop { get; private set; } = null!; 46 | [PluginService] public static INotificationManager Notifications { get; private set; } = null!; 47 | [PluginService] public static IObjectTable ObjectTable { get; private set; } = null!; 48 | [PluginService] public static IPlayerState PlayerState { get; private set; } = null!; 49 | 50 | } 51 | -------------------------------------------------------------------------------- /XIVComboVX/ChatUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | using Dalamud.Game.Text; 6 | using Dalamud.Game.Text.SeStringHandling; 7 | using Dalamud.Game.Text.SeStringHandling.Payloads; 8 | 9 | namespace VariableVixen.XIVComboVX; 10 | 11 | internal class ChatUtil: IDisposable { 12 | private bool disposed; 13 | 14 | private const uint 15 | OpenConfigId = 0, 16 | OpenIssueTrackerId = 1; 17 | 18 | internal readonly DalamudLinkPayload 19 | openConfig, 20 | openIssueTracker; 21 | 22 | internal const ushort 23 | ColourForeWarning = 32, 24 | ColourForeError = 17, 25 | ColourForeOpenConfig = 34, 26 | ColourGlowOpenConfig = 37; 27 | 28 | internal ChatUtil() { 29 | this.openConfig = Service.ChatGui.AddChatLinkHandler(OpenConfigId, this.onClickChatLink); 30 | this.openIssueTracker = Service.ChatGui.AddChatLinkHandler(OpenIssueTrackerId, this.onClickChatLink); 31 | } 32 | 33 | internal void AddOpenConfigLink(SeStringBuilder sb, string label) { 34 | sb.AddUiForeground(ColourForeOpenConfig); 35 | sb.AddUiGlow(ColourGlowOpenConfig); 36 | sb.Add(this.openConfig); 37 | sb.AddText(label); 38 | sb.Add(RawPayload.LinkTerminator); 39 | sb.AddUiGlowOff(); 40 | sb.AddUiForegroundOff(); 41 | } 42 | internal void AddOpenIssueTrackerLink(SeStringBuilder sb, string label) { 43 | sb.AddUiForeground(ColourForeOpenConfig); 44 | sb.AddUiGlow(ColourGlowOpenConfig); 45 | sb.Add(this.openIssueTracker); 46 | sb.AddText(label); 47 | sb.Add(RawPayload.LinkTerminator); 48 | sb.AddUiGlowOff(); 49 | sb.AddUiForegroundOff(); 50 | } 51 | 52 | [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Convention")] 53 | internal void Print(XivChatType type, params Payload[] payloads) { 54 | if (payloads.Length > 0) { 55 | Service.ChatGui.Print(new XivChatEntry() { 56 | Type = type, 57 | Message = new SeString(payloads), 58 | }); 59 | } 60 | } 61 | 62 | private void onClickChatLink(uint id, SeString source) { 63 | switch (id) { 64 | case OpenConfigId: 65 | Service.Plugin.OnPluginCommand("", ""); 66 | break; 67 | case OpenIssueTrackerId: 68 | Process.Start(new ProcessStartInfo("https://github.com/VariableVixen/XIVComboPlugin/issues") { UseShellExecute = true }); 69 | break; 70 | default: 71 | Service.ChatGui.Print(new XivChatEntry() { 72 | Type = XivChatType.SystemError, 73 | Message = new SeString( 74 | new TextPayload($"An internal error has occured: no handler is registered for id {id}.") 75 | ), 76 | }); 77 | break; 78 | } 79 | } 80 | 81 | #region Disposable 82 | 83 | public void Dispose() { 84 | if (this.disposed) 85 | return; 86 | this.disposed = true; 87 | 88 | Service.ChatGui.RemoveChatLinkHandler(); 89 | } 90 | 91 | #endregion 92 | 93 | } 94 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/VPR.cs: -------------------------------------------------------------------------------- 1 | using VariableVixen.XIVComboVX; 2 | 3 | namespace VariableVixen.XIVComboVX.Combos; 4 | 5 | internal static class VPR { 6 | public const byte JobID = 41; 7 | // Alright, there's some kind of skullduggery going on here. some skills have duplicates with different IDs, some aren't even listed under VPR. Doing my best. 8 | public const uint 9 | SteelFangs = 34606, // Might be 39157 10 | HuntersSting = 39159, 11 | DreadFangs = 34607, 12 | WrithingSnap = 34632, 13 | SwiftskinsSting = 39160, 14 | SteelMaw = 34614, 15 | FlankstingStrike = 34610, // Might be 38118 OR 38798 16 | FlanksbaneFang = 34610, 17 | HindstingStrike = 34612, 18 | HindsbaneFang = 34613, 19 | DreadMaw = 34615, 20 | Slither = 34646, // Might be 39184 21 | HuntersBite = 34616, 22 | SwiftskinsBite = 34617, 23 | JaggedMaw = 34618, 24 | BloodiedMaw = 34619, 25 | SerpentsTail = 35920, // Might be 39183 26 | DeathRattle = 39174, 27 | LastLash = 34635, 28 | Dreadwinder = 34620, 29 | HuntersCoil = 34621, 30 | SwiftskinsCoil = 34622, // Might be 39167 31 | PitOfDread = 34623, 32 | HuntersDen = 34624, 33 | SwiftskinsDen = 34625, 34 | Twinfang = 35921, 35 | Twinblood = 35922, 36 | TwinfangBite = 39175, 37 | TwinbloodBite = 39176, 38 | TwinfangThresh = 34638, 39 | TwinbloodThresh = 34639, 40 | UncoiledFury = 34633, // Might be 39168 41 | SerpentsIre = 34647, 42 | Reawaken = 34626, 43 | FirstGeneration = 39169, 44 | SecondGeneration = 39170, 45 | ThirdGeneration = 39171, 46 | FourthGeneration = 39172, 47 | UncoiledTwinfang = 39177, 48 | UncoiledTwinblood = 34645, 49 | Ouroboros = 39173, 50 | FirstLegacy = 39179, 51 | SecondLegacy = 39180, 52 | ThirdLegacy = 39181, 53 | FourthLegacy = 39182, 54 | PiercingFangs = 39158, 55 | BarbarousBite = 39161, 56 | RavenousBite = 39163, 57 | HuntersSnap = 39166, 58 | SnakeScales = 39185, 59 | Backlash = 39186, // Might be 39187 60 | FuriousBacklash = 39188, 61 | RattlingCoil = 39189, 62 | WorldSwallower = 39190; 63 | 64 | public static class Buffs { 65 | public const ushort 66 | HuntersInstinct = ushort.MaxValue, 67 | Swiftscaled = ushort.MaxValue, 68 | FlankstungVenom = 116, 69 | HindstungVenom = 118, 70 | FlanksbaneVenom = 117, 71 | HindsbaneVenom = 119; 72 | } 73 | 74 | public static class Debuffs { 75 | public const ushort 76 | NoxiousGnash = ushort.MaxValue; 77 | } 78 | 79 | public static class Levels { 80 | public const byte 81 | SteelFangs = 1, 82 | HuntersSting = 5, 83 | DreadFangs = 10, 84 | WrithingSnap = 15, 85 | SwiftskinsSting = 20, 86 | SteelMaw = 25, 87 | FlankstingStrike = 30, 88 | DreadMaw = 35, 89 | Slither = 40, 90 | HuntersBite = 40, 91 | SwiftskinsBite = 45, 92 | JaggedMaw = 50, 93 | DeathRattle = 55, 94 | LastLash = 60, 95 | Dreadwinder = 65, 96 | HuntersCoil = 65, 97 | SwiftskinsCoil = 65, 98 | PitOfDread = 70, 99 | HuntersDen = 70, 100 | SwiftskinsDen = 70, 101 | TwinfangBite = 75, 102 | TwinfangThresh = 80, 103 | UncoiledFury = 82, 104 | SerpentsIre = 86, 105 | Reawaken = 90, 106 | Generations = 90, 107 | UncoiledTwinfang = 92, 108 | Ouroboros = 96, 109 | Legacies = 100; 110 | } 111 | } 112 | 113 | internal class ViperBloodbathReplacer: SecondBloodbathCombo { 114 | public override CustomComboPreset Preset => CustomComboPreset.ViperBloodbathReplacer; 115 | } 116 | -------------------------------------------------------------------------------- /XIVComboVX/Config/ComboDetailSetting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | using Dalamud.Bindings.ImGui; 5 | 6 | using VariableVixen.XIVComboVX.Attributes; 7 | 8 | namespace VariableVixen.XIVComboVX.Config; 9 | 10 | internal class ComboDetailSetting { 11 | public PropertyInfo Property { get; } 12 | public CustomComboPreset Combo { get; } 13 | public Type Type { get; } 14 | public ImGuiDataType ImGuiType { get; } 15 | 16 | public int Precision { get; } 17 | public double Max { get; } 18 | public double Min { get; } 19 | public double Val { 20 | get => Convert.ToDouble(this.Property.GetValue(Service.Configuration) ?? default(double)); 21 | set { 22 | if (double.IsNaN(value) || double.IsInfinity(value)) 23 | return; 24 | if (value > this.Max) 25 | value = this.Max; 26 | if (value < this.Min) 27 | value = this.Min; 28 | // Apparently these can't be switch/case'd, because the `typeof(type)` expression isn't a constant 29 | if (this.Type == typeof(int)) 30 | this.Property.SetValue(Service.Configuration, (int)value); 31 | else if (this.Type == typeof(uint)) 32 | this.Property.SetValue(Service.Configuration, (uint)value); 33 | else if (this.Type == typeof(float)) 34 | this.Property.SetValue(Service.Configuration, (float)value); 35 | else 36 | throw new ArgumentException($"Cannot assign value ({value.GetType().Name} {value}) to property ({this.Type.Name} {this.Property.Name})"); 37 | } 38 | } 39 | 40 | public string Label { get; } 41 | public string? Description { get; } 42 | 43 | internal ComboDetailSetting(PropertyInfo prop, ComboDetailSettingAttribute attr) { 44 | this.Combo = attr.Combo; 45 | this.Label = attr.Label; 46 | this.Description = attr.Description; 47 | this.Property = prop; 48 | this.Type = prop.PropertyType; 49 | this.ImGuiType = this.Type == typeof(int) 50 | ? ImGuiDataType.S64 51 | : this.Type == typeof(uint) 52 | ? ImGuiDataType.U64 53 | : ImGuiDataType.Double; 54 | this.Precision = this.ImGuiType == ImGuiDataType.Float ? attr.Precision : 0; 55 | 56 | double typeMin = this.ImGuiType switch { 57 | ImGuiDataType.S64 => int.MinValue, 58 | ImGuiDataType.U64 => uint.MinValue, 59 | ImGuiDataType.Double => (double)float.MinValue, 60 | _ => 0, 61 | }; 62 | double typeMax = this.ImGuiType switch { 63 | ImGuiDataType.S64 => int.MaxValue, 64 | ImGuiDataType.U64 => uint.MaxValue, 65 | ImGuiDataType.Double => (double)float.MaxValue, 66 | _ => 0, 67 | }; 68 | if (attr.Min < typeMin) { 69 | Service.Log.Warning($"{LogTag.ConfigWindow} {this.Combo}:{this.Property.Name} has minimum value {attr.Min} below {this.Type.Name}.MinValue, bounding to {typeMin}"); 70 | this.Min = typeMin; 71 | } 72 | else if (attr.Min > typeMax) { 73 | Service.Log.Warning($"{LogTag.ConfigWindow} {this.Combo}:{this.Property.Name} has minimum value {attr.Min} above {this.Type.Name}.MaxValue, bounding to {typeMax}"); 74 | this.Min = typeMax; 75 | } 76 | else { 77 | this.Min = attr.Min; 78 | } 79 | if (attr.Max > typeMax) { 80 | Service.Log.Warning($"{LogTag.ConfigWindow} {this.Combo}:{this.Property.Name} has maximum value {attr.Max} above {this.Type.Name}.MaxValue, bounding to {typeMax}"); 81 | this.Max = typeMax; 82 | } 83 | else if (attr.Max < typeMin) { 84 | Service.Log.Warning($"{LogTag.ConfigWindow} {this.Combo}:{this.Property.Name} has maximum value {attr.Max} below {this.Type.Name}.MinValue, bounding to {typeMin}"); 85 | this.Max = typeMin; 86 | } 87 | else { 88 | this.Max = attr.Max; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XIVComboVX 2 | _Finally, hotbar space..._ 3 | 4 | ![GitHub build status](https://img.shields.io/github/actions/workflow/status/VariableVixen/XIVComboPlugin/build.yml?logo=github) 5 | ![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/VariableVixen/XIVComboPlugin?label=version&color=informational) 6 | ![GitHub last commit (branch)](https://img.shields.io/github/last-commit/VariableVixen/XIVComboPlugin/master?label=updated) 7 | [![GitHub issues](https://img.shields.io/github/issues-raw/VariableVixen/XIVComboPlugin?label=known%20issues)](https://github.com/VariableVixen/XIVComboPlugin/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) 8 | 9 | [![Support me!](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/V7V7IK9UU) 10 | 11 | ## About 12 | [![License](https://img.shields.io/github/license/VariableVixen/XIVComboPlugin?logo=github&color=informational&cacheSeconds=86400)](https://github.com/VariableVixen/XIVComboPlugin/blob/master/LICENSE) 13 | 14 | XIVCombo is a plugin to allow for "one-button" combo chains, as well as implementing various other mutually-exclusive button consolidation and quality of life replacements. Thanks to Meli for the initial start, attick and daemitus for continuing, and obviously goat for making any of this possible. 15 | 16 | For some jobs, this frees a massive amount of hotbar space (looking at you, DRG). For most, it removes a lot of mindless tedium associated with having to press various buttons that have little logical reason to be separate. 17 | 18 | ## Installation 19 | Type `/xlplugins` in-game to access the plugin installer and updater. Note that you will need to add [my custom plugin repository](https://github.com/VariableVixen/MyDalamudPlugins) (full instructions included at that link) in order to find this plugin. 20 | 21 | ## In-game usage 22 | * Type `/pcombo` to pull up a GUI for editing active combo replacements. 23 | * Drag the named ability from your ability list onto your hotbar to use. 24 | * For example, to use DRK's Souleater combo, first check the box, then place Souleater on your bar. It should automatically turn into Hard Slash. 25 | * The description associated with each combo should be enough to tell you which ability needs to be placed on the hotbar. 26 | 27 | ### Examples 28 | ![DRK Souleater combo](https://github.com/VariableVixen/XIVComboPlugin/raw/master/res/souleater_combo.gif) 29 | 30 | ![MCH Hypercharge/Heat Blast combo](https://github.com/VariableVixen/XIVComboPlugin/raw/master/res/hypercharge_heat_blast.gif) 31 | 32 | ![BLM Enochian switcher (outdated)](https://github.com/VariableVixen/XIVComboPlugin/raw/master/res/eno_swap.gif) 33 | 34 | ## Why Another Fork? 35 | Because the original fork developer (daemitus) has a different philosophy regarding how much the plugin should be allowed to do. They want to avoid "intelligent" decisions in the plugin, because they feel it's too close to botting. While I respect their decision and their reasoning, I also personally disagree with it, and additionally believe that since this plugin _only_ operates in PvE, there's no real harm or reason to restrict it like that. You aren't gaining an advantage over another player unless you're comparing parses, and even then nobody "wins" or "loses" anything. 36 | 37 | Furthermore, I've seen _so_ many people who want to play this game, but have some disability or another - carpal tunnel, for instance - that makes it hard for them to do so. It is my hope that my fork will make the game more accessible to people like that, thereby bringing in more players to enjoy it. 38 | 39 | ## Full list of supported combos 40 | This has been removed (it was a long time coming) because the list is both _so_ extensive and also tree-shaped with parent/child combos that it really can't be well represented in a flat list. 41 | 42 | If you have any requests, including combos existing in other forks but not in this one, please [open a feature request](https://github.com/VariableVixen/XIVComboPlugin/issues/new/choose) here on the repo's issue tracker. 43 | 44 | -------------------------------------------------------------------------------- /XIVComboVX/Ipc.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | using Dalamud.Plugin.Ipc; 8 | using Dalamud.Plugin.Ipc.Exceptions; 9 | 10 | namespace VariableVixen.XIVComboVX; 11 | 12 | internal class Ipc: IDisposable { 13 | private const int LoopDelaySec = 300; 14 | private const int InitialDelaySec = 5; 15 | 16 | private const string TippyPluginId = "Tippy"; 17 | private const string TippyRegisterTipId = "Tippy.RegisterTip"; 18 | private const string TippyRegisterMessageId = "Tippy.RegisterMessage"; 19 | 20 | private readonly CancellationTokenSource stop = new(); 21 | private readonly Task registrationWorker; 22 | private bool disposed; 23 | 24 | internal static bool TippyLoaded => Service.Interface.InstalledPlugins.Where(state => state.InternalName == TippyPluginId).Any(); 25 | 26 | private readonly ICallGateSubscriber tippyRegisterTip; 27 | private readonly ICallGateSubscriber tippyRegisterMessage; 28 | 29 | private readonly ConcurrentQueue tippyRegistrationQueue = new(); 30 | 31 | internal Ipc() { 32 | this.tippyRegisterTip = Service.Interface.GetIpcSubscriber(TippyRegisterTipId); 33 | this.tippyRegisterMessage = Service.Interface.GetIpcSubscriber(TippyRegisterMessageId); 34 | this.registrationWorker = Task.Run(this.registrationLoop, this.stop.Token); 35 | } 36 | 37 | private async void registrationLoop() { 38 | await Task.Delay(InitialDelaySec * 1000, this.stop.Token); 39 | while (!this.stop.IsCancellationRequested) { 40 | try { // catches cancellation so the task shows as completed, once it's actually gotten started 41 | 42 | if (TippyLoaded) { 43 | if (!this.tippyRegistrationQueue.IsEmpty) { 44 | string tippyTip = null!; 45 | try { 46 | while (this.tippyRegistrationQueue.TryDequeue(out tippyTip!)) { 47 | if (!string.IsNullOrWhiteSpace(tippyTip)) 48 | this.tippyRegisterTip.InvokeFunc(tippyTip); // ignore the return value cause if Tippy just says "no" then what are WE gonna do about it? 49 | } 50 | } 51 | catch (IpcNotReadyError) { 52 | // We already got the value out of the queue, but registering it failed, so put it back in 53 | this.AddTips(tippyTip); 54 | } 55 | catch (IpcError ex) { 56 | Service.Log.Error($"{LogTag.Ipc} Failed to register tip for Tippy's pool", ex); 57 | this.tippyRegistrationQueue.Clear(); 58 | } 59 | } 60 | } 61 | 62 | // Only try to do things every so often 63 | await Task.Delay(LoopDelaySec * 1000, this.stop.Token); 64 | } 65 | catch (TaskCanceledException) { // if cancellation is requested once we get started, it's considered completion cause this is infinite 66 | return; 67 | } 68 | } 69 | } 70 | 71 | #region Tippy API 72 | internal void AddTips(params string[] tips) { 73 | if (this.disposed) 74 | return; 75 | 76 | foreach (string tip in tips) { 77 | if (!string.IsNullOrWhiteSpace(tip)) 78 | this.tippyRegistrationQueue.Enqueue(tip); 79 | } 80 | } 81 | 82 | internal bool ShowTippyMessage(string message) { 83 | if (this.disposed) 84 | return false; 85 | 86 | try { 87 | return this.tippyRegisterMessage.InvokeFunc(message); 88 | } 89 | catch (IpcNotReadyError) { 90 | return false; 91 | } 92 | catch (IpcError ex) { 93 | Service.Log.Error($"{LogTag.Ipc} Failed to register priority message for Tippy", ex); 94 | return false; 95 | } 96 | } 97 | #endregion 98 | 99 | #region Disposable 100 | protected virtual void Dispose(bool disposing) { 101 | if (this.disposed) 102 | return; 103 | this.disposed = true; 104 | 105 | if (disposing) { 106 | this.stop.Cancel(); 107 | this.registrationWorker.Wait(); 108 | this.registrationWorker.Dispose(); 109 | this.stop.Dispose(); 110 | this.tippyRegistrationQueue.Clear(); 111 | } 112 | 113 | } 114 | 115 | public void Dispose() { 116 | this.Dispose(true); 117 | GC.SuppressFinalize(this); 118 | } 119 | #endregion 120 | 121 | } 122 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | # Put your personal access token in a repository secret named PAT for cross-repository access 4 | permissions: 5 | contents: write 6 | 7 | on: 8 | workflow_dispatch: 9 | push: 10 | paths-ignore: 11 | - '*.md' 12 | branches: 13 | - master 14 | - main 15 | - release 16 | 17 | env: 18 | INTERNAL_NAME: XIVComboVX 19 | CONFIGURATION: release 20 | PERSONAL_PLUGIN_REPO: VariableVixen/MyDalamudPlugins 21 | DOTNET_CLI_TELEMETRY_OPTOUT: true 22 | DOTNET_NOLOGO: true 23 | 24 | jobs: 25 | precheck: 26 | runs-on: ubuntu-latest 27 | outputs: 28 | buildVersion: ${{ steps.data.outputs.buildVersion }} 29 | tagName: ${{ steps.data.outputs.tagName }} 30 | tagExists: ${{ steps.check-version-tag.outputs.tagExists }} 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | with: 35 | fetch-depth: 0 36 | - name: Load values 37 | id: data 38 | run: | 39 | buildVersion=$(grep -Ei '[^<]+' "${{ env.INTERNAL_NAME }}/${{ env.INTERNAL_NAME }}.csproj" | sed -e 's/.\+\([^<]\+\)<.\+/\1/i' -) 40 | echo "buildVersion=$buildVersion" >> "$GITHUB_OUTPUT" 41 | echo "tagName=v$buildVersion" >> "$GITHUB_OUTPUT" 42 | shell: bash 43 | - name: Check if version exists 44 | id: check-version-tag 45 | run: | 46 | if git show-ref --quiet --tags "${{ steps.data.outputs.tagName }}"; then echo "tagExists=true" >> "$GITHUB_OUTPUT"; else echo "tagExists=false" >> "$GITHUB_OUTPUT"; fi 47 | shell: bash 48 | - name: Debug 49 | run: | 50 | echo "Current version is ${{ steps.data.outputs.buildVersion }} for tag ${{ steps.data.outputs.tagName }} (exists = ${{ steps.check-version-tag.outputs.tagExists }})" 51 | shell: bash 52 | 53 | build: 54 | needs: precheck 55 | if: needs.precheck.outputs.tagExists == 'false' 56 | runs-on: windows-2022 57 | steps: 58 | - name: Checkout 59 | uses: actions/checkout@v4 60 | with: 61 | submodules: recursive 62 | - name: Setup MSBuild 63 | uses: microsoft/setup-msbuild@v1.1.3 64 | - name: Download Dalamud 65 | run: | 66 | Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip 67 | Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev\" 68 | - name: Restore 69 | run: dotnet restore -r win 70 | - name: Build 71 | run: dotnet build -c ${{ env.CONFIGURATION }} --no-restore 72 | - name: Upload build 73 | uses: actions/upload-artifact@v4 74 | with: 75 | name: PluginRepoZip 76 | path: ${{ env.INTERNAL_NAME }}/bin/${{ env.CONFIGURATION }}/${{ env.INTERNAL_NAME }} 77 | if-no-files-found: error 78 | retention-days: 14 79 | - name: Tag 80 | run: | 81 | git tag -am "[Automated build $BUILD_VERSION]" "$TAG_NAME" 82 | git push origin "$TAG_NAME" 83 | shell: bash 84 | env: 85 | BUILD_VERSION: ${{ needs.precheck.outputs.buildVersion }} 86 | TAG_NAME: ${{ needs.precheck.outputs.tagName }} 87 | GIT_AUTHOR_NAME: GitHub Action 88 | GIT_COMMITTER_NAME: GitHub Action 89 | GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com 90 | GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com 91 | 92 | deploy: 93 | needs: build 94 | runs-on: ubuntu-latest 95 | steps: 96 | - name: Checkout 97 | uses: actions/checkout@v4 98 | with: 99 | ref: master 100 | repository: ${{ env.PERSONAL_PLUGIN_REPO }} 101 | token: ${{ secrets.PAT }} 102 | - name: Download build 103 | uses: actions/download-artifact@v4 104 | with: 105 | name: PluginRepoZip 106 | path: plugins/${{ env.INTERNAL_NAME }} 107 | - name: Commit 108 | uses: EndBug/add-and-commit@v9 109 | with: 110 | add: plugins/ 111 | pathspec_error_handling: exitImmediately 112 | author_name: GitHub Action 113 | author_email: github-actions[bot]@users.noreply.github.com 114 | message: Update ${{ env.INTERNAL_NAME }} 115 | -------------------------------------------------------------------------------- /XIVComboVX/GameData/IconReplacer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | using Dalamud.Game.ClientState.Objects.SubKinds; 7 | using Dalamud.Hooking; 8 | 9 | using FFXIVClientStructs.FFXIV.Client.Game; 10 | 11 | namespace VariableVixen.XIVComboVX.GameData; 12 | 13 | internal class IconReplacer: IDisposable { 14 | 15 | private delegate ulong IsIconReplaceableDelegate(uint actionID); 16 | private delegate uint GetIconDelegate(nint actionManager, uint actionID); 17 | private delegate nint GetActionCooldownSlotDelegate(nint actionManager, int cooldownGroup); 18 | 19 | private readonly Hook isIconReplaceableHook; 20 | private readonly Hook getIconHook; 21 | 22 | private nint actionManager = nint.Zero; 23 | 24 | private readonly Dictionary> customCombos = []; 25 | 26 | public IconReplacer() { 27 | Service.Log.Information($"{LogTag.CoreSetup} Loading registered combos"); 28 | int total = 0; 29 | IEnumerable combos = Assembly.GetAssembly(this.GetType())!.GetTypes() 30 | .Where(t => !t.IsAbstract && (t.BaseType == typeof(CustomCombo) || t.BaseType?.BaseType == typeof(CustomCombo))) 31 | .Select(t => { 32 | ++total; 33 | return Activator.CreateInstance(t); 34 | }) 35 | .Cast(); 36 | foreach (CustomCombo combo in combos) { 37 | uint[] actions = combo.ActionIDs; 38 | if (actions.Length == 0) 39 | actions = [0]; 40 | foreach (uint id in actions) { 41 | if (!this.customCombos.TryGetValue(id, out List? all)) { 42 | all = []; 43 | this.customCombos[id] = all; 44 | } 45 | all.Add(combo); 46 | } 47 | } 48 | Service.Log.Information($"{LogTag.CoreSetup} Loaded {total} replacers for {this.customCombos.Count} actions"); 49 | 50 | this.getIconHook = Service.Interop.HookFromAddress(ActionManager.Addresses.GetAdjustedActionId.Value, this.getIconDetour); 51 | this.isIconReplaceableHook = Service.Interop.HookFromAddress(Service.Address.IsActionIdReplaceable, this.isIconReplaceableDetour); 52 | 53 | this.getIconHook.Enable(); 54 | this.isIconReplaceableHook.Enable(); 55 | 56 | } 57 | 58 | public void Dispose() { 59 | this.getIconHook?.Disable(); 60 | this.isIconReplaceableHook?.Disable(); 61 | 62 | this.getIconHook?.Dispose(); 63 | this.isIconReplaceableHook?.Dispose(); 64 | } 65 | 66 | private ulong isIconReplaceableDetour(uint actionID) => 1; 67 | 68 | private unsafe uint getIconDetour(nint actionManager, uint actionID) { 69 | try { 70 | this.actionManager = actionManager; 71 | 72 | if (!Service.Configuration.Active) 73 | return this.OriginalHook(actionID); 74 | 75 | if (!this.customCombos.TryGetValue(actionID, out List? combos) && !this.customCombos.TryGetValue(0, out combos)) { 76 | Service.TickLogger.Info($"{LogTag.Combo} No replacers found for action {Labels.Action(actionID)}"); 77 | return this.OriginalHook(actionID); 78 | } 79 | 80 | IPlayerCharacter? player = Service.ObjectTable.LocalPlayer; 81 | if (player?.IsValid() is not true) { 82 | CustomCombo.CachedLocalPlayer = null; 83 | Service.TickLogger.Warning($"{LogTag.Combo} Cannot replace action {Labels.Action(actionID)} without a player"); 84 | return this.OriginalHook(actionID); 85 | } 86 | CustomCombo.CachedLocalPlayer = player; 87 | 88 | uint lastComboActionId = *(uint*)Service.Address.LastComboMove; 89 | float comboTime = *(float*)Service.Address.ComboTimer; 90 | byte level = player.Level; 91 | uint classJobID = player.ClassJob.RowId; 92 | 93 | Service.TickLogger.Info($"{LogTag.Combo} Checking {combos.Count} replacer{(combos.Count == 1 ? "" : "s")} for action {Labels.Action(actionID)}"); 94 | foreach (CustomCombo combo in combos) { 95 | if (combo.TryInvoke(actionID, lastComboActionId, comboTime, level, classJobID, out uint newActionID)) 96 | return this.OriginalHook(newActionID); 97 | } 98 | 99 | Service.TickLogger.Info($"{LogTag.Combo} No replacement for {Labels.Action(actionID)}"); 100 | return this.OriginalHook(actionID); 101 | } 102 | catch (Exception ex) { 103 | Service.TickLogger.Error($"{LogTag.Combo} Don't crash the game", ex); 104 | return this.getIconHook.Original(actionManager, actionID); 105 | } 106 | } 107 | 108 | 109 | public uint OriginalHook(uint actionID) => this.getIconHook.Original(this.actionManager, actionID); 110 | 111 | } 112 | -------------------------------------------------------------------------------- /XIVComboVX/GameData/CooldownData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Runtime.InteropServices; 3 | 4 | using Dalamud.Game.ClientState.Objects.SubKinds; 5 | 6 | using ActionManager = FFXIVClientStructs.FFXIV.Client.Game.ActionManager; 7 | 8 | namespace VariableVixen.XIVComboVX.GameData; 9 | 10 | [StructLayout(LayoutKind.Explicit)] 11 | internal readonly struct CooldownData { 12 | [FieldOffset(0x0)] private readonly bool isCooldown; 13 | [FieldOffset(0x4)] private readonly uint actionID; 14 | [FieldOffset(0x8)] private readonly float cooldownElapsed; 15 | [FieldOffset(0xC)] private readonly float cooldownTotal; 16 | 17 | private static readonly Dictionary<(uint ActionID, uint ClassJobID, byte Level), (ushort CurrentMax, ushort Max)> chargesCache = []; 18 | private static unsafe (ushort Current, ushort Max) getMaxCharges(uint actionID) { 19 | IPlayerCharacter? player = CustomCombo.CachedLocalPlayer; 20 | if (player is null) 21 | return (0, 0); 22 | 23 | uint job = player.ClassJob.RowId; 24 | byte level = player.Level; 25 | if (job == 0 || level == 0) 26 | return (0, 0); 27 | 28 | (uint actionID, uint job, byte level) key = (actionID, job, level); 29 | if (chargesCache.TryGetValue(key, out (ushort CurrentMax, ushort Max) found)) 30 | return found; 31 | 32 | ushort cur = ActionManager.GetMaxCharges(actionID, 0); 33 | ushort max = ActionManager.GetMaxCharges(actionID, 100); 34 | return chargesCache[key] = (cur, max); 35 | } 36 | 37 | public uint ActionID => this.actionID; 38 | 39 | /// 40 | /// Whether this action is currently on cooldown, even if charges may be available 41 | /// 42 | public bool IsCooldown { 43 | get { 44 | (ushort cur, ushort max) = getMaxCharges(this.ActionID); 45 | return cur == max 46 | ? this.isCooldown 47 | : this.cooldownElapsed < this.CooldownTotal; 48 | } 49 | } 50 | 51 | /// 52 | /// Elapsed time on the cooldown, covering only the number of max charges available at current level (if applicable) 53 | /// 54 | public float CooldownElapsed => this.cooldownElapsed <= 0 || this.cooldownElapsed >= this.CooldownTotal 55 | ? 0 56 | : this.cooldownElapsed; 57 | 58 | /// 59 | /// Total cooldown time, covering only the number of max charges available at current level (if applicable) 60 | /// 61 | public float CooldownTotal { 62 | get { 63 | if (this.cooldownTotal <= 0) 64 | return 0; 65 | 66 | (ushort cur, ushort max) = getMaxCharges(this.ActionID); 67 | return cur == max 68 | ? this.cooldownTotal 69 | : this.cooldownTotal / max * cur; 70 | } 71 | } 72 | 73 | /// 74 | /// Remaining cooldown time, covering only the number of max charges available at current level (if applicable) 75 | /// 76 | public float CooldownRemaining => this.IsCooldown ? this.CooldownTotal - this.CooldownElapsed : 0; 77 | 78 | /// 79 | /// The maximum number of charges available at the current level 80 | /// 81 | public ushort MaxCharges => getMaxCharges(this.ActionID).Current; 82 | 83 | /// 84 | /// Whether the action has any charges available at the current level 85 | /// 86 | public bool HasCharges => this.MaxCharges > 1; 87 | 88 | /// 89 | /// The number of charges left at the current level 90 | /// 91 | public ushort RemainingCharges { 92 | get { 93 | ushort curMax = this.MaxCharges; 94 | 95 | return !this.IsCooldown 96 | ? curMax 97 | : (ushort)(this.CooldownElapsed / (this.CooldownTotal / curMax)); 98 | } 99 | } 100 | 101 | /// 102 | /// The cooldown time remaining for the currently-recovering charge. 103 | /// 104 | public float ChargeCooldownRemaining { 105 | get { 106 | if (!this.IsCooldown) 107 | return 0; 108 | 109 | (ushort cur, ushort _) = getMaxCharges(this.ActionID); 110 | 111 | return this.CooldownRemaining % (this.CooldownTotal / cur); 112 | } 113 | } 114 | 115 | /// 116 | /// Whether this action has at least one charge out of however many it has total, even if it can only have one "charge" 117 | /// 118 | public bool Available => !this.IsCooldown || this.RemainingCharges > 0; 119 | 120 | public string DebugLabel 121 | => $"{(this.IsCooldown ? "on" : "off")} cd," 122 | + $" {(this.HasCharges ? this.RemainingCharges + "/" + this.MaxCharges : "no")} charges," 123 | + $" {this.CooldownElapsed}/{this.CooldownTotal}[{this.cooldownTotal}] ({this.CooldownRemaining})"; 124 | } 125 | -------------------------------------------------------------------------------- /XIVComboVX/Config/UpdateAlerter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | using Dalamud.Game.Text; 7 | using Dalamud.Game.Text.SeStringHandling; 8 | using Dalamud.Game.Text.SeStringHandling.Payloads; 9 | 10 | namespace VariableVixen.XIVComboVX.Config; 11 | 12 | internal class UpdateAlerter: IDisposable { 13 | private const int 14 | MessageDelayMs = 500, 15 | LoginDelayMs = 500; 16 | 17 | private bool disposed; 18 | 19 | private bool seenUpdateMessage = false; 20 | #pragma warning disable IDE0052 // Remove unread private members - used in release configuration but not debug 21 | private readonly bool isFreshInstall = false; 22 | private readonly Version current; 23 | #pragma warning restore IDE0052 // Remove unread private members 24 | 25 | private CancellationTokenSource? realAborter; 26 | private CancellationTokenSource? aborter { 27 | get { 28 | lock (this) { 29 | return this.realAborter; 30 | } 31 | } 32 | set { 33 | lock (this) { 34 | this.realAborter?.Cancel(); 35 | this.realAborter = value; 36 | } 37 | } 38 | } 39 | 40 | internal UpdateAlerter(Version to, bool isFresh) { 41 | this.current = to; 42 | this.isFreshInstall = isFresh; 43 | if (Service.Configuration.ShowUpdateMessage) { 44 | this.Register(); 45 | this.CheckMessage(); 46 | } 47 | } 48 | 49 | internal void CheckMessage() { 50 | Service.Log.Information($"{LogTag.UpdateAlert} Checking whether to display update message"); 51 | if (this.disposed) { 52 | this.Unregister(); 53 | Service.Log.Information($"{LogTag.UpdateAlert} Update alerter already disposed"); 54 | return; 55 | } 56 | if (this.seenUpdateMessage) { 57 | this.Unregister(); 58 | Service.Log.Information($"{LogTag.UpdateAlert} Message already displayed, unregistering"); 59 | return; 60 | } 61 | 62 | Service.Log.Information($"{LogTag.UpdateAlert} Checks passed, delaying message by {MessageDelayMs}ms - may be reset if message is triggered again within that time"); 63 | 64 | this.aborter = new(); 65 | 66 | Task.Delay(MessageDelayMs, this.aborter.Token).ContinueWith(waiter => { 67 | if (waiter.Status is TaskStatus.RanToCompletion) 68 | this.DisplayMessage(); 69 | }); 70 | } 71 | 72 | internal void DisplayMessage() { 73 | 74 | this.aborter?.Cancel(); 75 | this.seenUpdateMessage = true; 76 | this.Unregister(); 77 | 78 | Service.Log.Information($"{LogTag.UpdateAlert} Displaying update alert in game chat"); 79 | 80 | List parts = []; 81 | #if !DEBUG 82 | if (!Service.Interface.IsDev) { 83 | parts.Add(new TextPayload( 84 | this.isFreshInstall 85 | ? $"{Service.Plugin.ShortPluginSignature} has been installed. By default, all features are disabled.\n" 86 | : $"{Plugin.Name} has been updated to {this.current}. Features may have been added or changed.\n" 87 | )); 88 | } 89 | #endif 90 | parts.AddRange([ 91 | new UIForegroundPayload(ChatUtil.ColourForeOpenConfig), 92 | new UIGlowPayload(ChatUtil.ColourGlowOpenConfig), 93 | Service.ChatUtils.openConfig, 94 | new TextPayload($"[Open {Plugin.Name} Settings]"), 95 | RawPayload.LinkTerminator, 96 | new UIGlowPayload(0), 97 | new UIForegroundPayload(0), 98 | ]); 99 | 100 | Service.ChatUtils.Print(XivChatType.Notice, parts.ToArray()); 101 | 102 | } 103 | 104 | internal void Register() { 105 | Service.Log.Information($"{LogTag.UpdateAlert} Registering update alerter"); 106 | Service.ChatGui.ChatMessage += this.onChatMessage; 107 | Service.Client.Login += this.onLogin; 108 | } 109 | 110 | internal void Unregister() { 111 | Service.Log.Information($"{LogTag.UpdateAlert} Unregistering update alerter"); 112 | Service.ChatGui.ChatMessage -= this.onChatMessage; 113 | Service.Client.Login -= this.onLogin; 114 | } 115 | 116 | private void onChatMessage(XivChatType type, int timestamp, ref SeString sender, ref SeString message, ref bool isHandled) { 117 | if (type is XivChatType.Urgent or XivChatType.Notice or XivChatType.SystemMessage) 118 | this.CheckMessage(); 119 | } 120 | private async void onLogin() { 121 | do { 122 | await Task.Delay(LoginDelayMs); 123 | } while (!Service.Client.IsLoggedIn || Service.PlayerState.ContentId == 0 || Service.ObjectTable.LocalPlayer is null); 124 | this.CheckMessage(); 125 | } 126 | 127 | 128 | #region Disposing 129 | 130 | public void Dispose() { 131 | if (this.disposed) 132 | return; 133 | this.disposed = true; 134 | 135 | this.Unregister(); 136 | } 137 | 138 | #endregion 139 | 140 | } 141 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/DRK.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | 3 | using VariableVixen.XIVComboVX; 4 | 5 | namespace VariableVixen.XIVComboVX.Combos; 6 | 7 | internal static class DRK { 8 | public const byte JobID = 32; 9 | 10 | public const uint 11 | HardSlash = 3617, 12 | Unleash = 3621, 13 | SyphonStrike = 3623, 14 | Souleater = 3632, 15 | BloodWeapon = 3625, 16 | SaltedEarth = 3639, 17 | AbyssalDrain = 3641, 18 | CarveAndSpit = 3643, 19 | Quietus = 7391, 20 | Bloodspiller = 7392, 21 | FloodOfDarkness = 16466, 22 | EdgeOfDarkness = 16467, 23 | StalwartSoul = 16468, 24 | FloodOfShadow = 16469, 25 | EdgeOfShadow = 16470, 26 | LivingShadow = 16472, 27 | SaltAndDarkness = 25755, 28 | Shadowbringer = 25757; 29 | 30 | public static class Buffs { 31 | public const ushort 32 | BloodWeapon = 742, 33 | Darkside = 751, 34 | Delirium = 1972; 35 | } 36 | public static class Debuffs { 37 | // public const ushort placeholder = 0; 38 | } 39 | 40 | public static class Levels { 41 | public const byte 42 | SyphonStrike = 2, 43 | Souleater = 26, 44 | FloodOfDarkness = 30, 45 | BloodWeapon = 35, 46 | EdgeOfDarkness = 40, 47 | StalwartSoul = 40, 48 | SaltedEarth = 52, 49 | AbyssalDrain = 56, 50 | CarveAndSpit = 60, 51 | Bloodspiller = 62, 52 | Quietus = 64, 53 | Delirium = 68, 54 | Shadow = 74, 55 | LivingShadow = 80, 56 | SaltAndDarkness = 86, 57 | Shadowbringer = 90; 58 | } 59 | } 60 | 61 | internal class DarkStunInterruptFeature: StunInterruptCombo { 62 | public override CustomComboPreset Preset { get; } = CustomComboPreset.DarkStunInterruptFeature; 63 | } 64 | 65 | internal class DarkSouleater: CustomCombo { 66 | public override CustomComboPreset Preset => CustomComboPreset.DrkAny; 67 | public override uint[] ActionIDs { get; } = [DRK.Souleater]; 68 | 69 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 70 | 71 | if (IsEnabled(CustomComboPreset.DarkSouleaterOvercapFeature)) { 72 | DRKGauge gauge = GetJobGauge(); 73 | 74 | if (level >= DRK.Levels.Bloodspiller && (gauge.Blood > 80 || gauge.Blood > 70 && SelfHasEffect(DRK.Buffs.BloodWeapon))) 75 | return OriginalHook(DRK.Bloodspiller); 76 | } 77 | 78 | if (level >= DRK.Levels.Delirium && IsEnabled(CustomComboPreset.DarkDeliriumFeature) && SelfHasEffect(DRK.Buffs.Delirium)) 79 | return OriginalHook(DRK.Bloodspiller); 80 | 81 | if (IsEnabled(CustomComboPreset.DarkSouleaterCombo)) { 82 | 83 | if (lastComboMove is DRK.SyphonStrike && level >= DRK.Levels.Souleater) 84 | return DRK.Souleater; 85 | if (lastComboMove is DRK.HardSlash && level >= DRK.Levels.SyphonStrike) 86 | return DRK.SyphonStrike; 87 | 88 | return DRK.HardSlash; 89 | } 90 | 91 | return actionID; 92 | } 93 | } 94 | 95 | internal class DarkStalwartSoul: CustomCombo { 96 | public override CustomComboPreset Preset => CustomComboPreset.DrkAny; 97 | public override uint[] ActionIDs { get; } = [DRK.StalwartSoul]; 98 | 99 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 100 | 101 | if (IsEnabled(CustomComboPreset.DarkStalwartSoulOvercapFeature)) { 102 | DRKGauge gauge = GetJobGauge(); 103 | 104 | if (level >= DRK.Levels.Quietus && (gauge.Blood > 80 || gauge.Blood > 70 && SelfHasEffect(DRK.Buffs.BloodWeapon))) 105 | return OriginalHook(DRK.Quietus); 106 | } 107 | 108 | if (level >= DRK.Levels.Delirium && IsEnabled(CustomComboPreset.DarkDeliriumFeature) && SelfHasEffect(DRK.Buffs.Delirium)) 109 | return OriginalHook(DRK.Quietus); 110 | 111 | if (IsEnabled(CustomComboPreset.DarkStalwartSoulCombo)) { 112 | 113 | if (lastComboMove is DRK.Unleash && level >= DRK.Levels.StalwartSoul) 114 | return DRK.StalwartSoul; 115 | 116 | return DRK.Unleash; 117 | } 118 | 119 | return actionID; 120 | } 121 | } 122 | 123 | internal class DarkShadowbringerFeature: CustomCombo { 124 | public override CustomComboPreset Preset { get; } = CustomComboPreset.DarkShadowbringerFeature; 125 | public override uint[] ActionIDs { get; } = [DRK.EdgeOfDarkness, DRK.EdgeOfShadow, DRK.FloodOfDarkness, DRK.FloodOfShadow]; 126 | 127 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 128 | 129 | if (level >= DRK.Levels.Shadowbringer && SelfHasEffect(DRK.Buffs.Darkside) && LocalPlayer.CurrentMp < 6000) 130 | return DRK.Shadowbringer; 131 | 132 | return actionID; 133 | } 134 | } 135 | 136 | internal class DarkCarveAndSpitAbyssalDrain: CustomCombo { 137 | public override CustomComboPreset Preset { get; } = CustomComboPreset.DrkAny; 138 | public override uint[] ActionIDs { get; } = [DRK.CarveAndSpit, DRK.AbyssalDrain]; 139 | 140 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 141 | 142 | if (level >= DRK.Levels.BloodWeapon && IsEnabled(CustomComboPreset.DarkBloodWeaponFeature) && IsOffCooldown(DRK.BloodWeapon)) 143 | return DRK.BloodWeapon; 144 | 145 | return actionID; 146 | } 147 | } 148 | 149 | internal class DarkQuietusBloodspiller: CustomCombo { 150 | public override CustomComboPreset Preset { get; } = CustomComboPreset.DrkAny; 151 | public override uint[] ActionIDs { get; } = [DRK.Quietus, DRK.Bloodspiller]; 152 | 153 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 154 | 155 | // TODO: integrate this functionality into the Stalwart / Souleater features 156 | if (level >= DRK.Levels.LivingShadow && IsEnabled(CustomComboPreset.DarkLivingShadowFeature) && IsOffCooldown(DRK.LivingShadow)) 157 | return DRK.LivingShadow; 158 | 159 | return actionID; 160 | } 161 | } 162 | 163 | internal class DarkLivingShadow: CustomCombo { 164 | public override CustomComboPreset Preset { get; } = CustomComboPreset.DrkAny; 165 | public override uint[] ActionIDs { get; } = [DRK.LivingShadow]; 166 | 167 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 168 | DRKGauge gauge = GetJobGauge(); 169 | 170 | // TODO: integrate this functionality into the Quietus / Bloodspiller features 171 | 172 | if (IsEnabled(CustomComboPreset.DarkLivingShadowbringerFeature)) { 173 | if (level >= DRK.Levels.Shadowbringer && gauge.ShadowTimeRemaining > 0 && HasCharges(DRK.Shadowbringer)) 174 | return DRK.Shadowbringer; 175 | } 176 | 177 | if (IsEnabled(CustomComboPreset.DarkLivingShadowbringerHpFeature)) { 178 | if (level >= DRK.Levels.Shadowbringer && HasCharges(DRK.Shadowbringer) && IsOnCooldown(DRK.LivingShadow)) 179 | return DRK.Shadowbringer; 180 | } 181 | 182 | return actionID; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/PCT.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | 3 | namespace VariableVixen.XIVComboVX.Combos; 4 | 5 | internal class PCT { 6 | public const byte JobID = 42; 7 | 8 | public const uint 9 | FireRed = 34650, 10 | AeroGreen = 34651, 11 | WaterBlue = 34652, 12 | BlizzardCyan = 34653, 13 | EarthYellow = 34654, 14 | ThunderMagenta = 34655, 15 | ExtraFireRed = 34656, 16 | ExtraAeroGreen = 34657, 17 | ExtraWaterBlue = 34658, 18 | ExtraBlizzardCyan = 34659, 19 | ExtraEarthYellow = 34660, 20 | ExtraThunderMagenta = 34661, 21 | HolyWhite = 34662, 22 | CometBlack = 34663, 23 | PomMotif = 34664, 24 | WingMotif = 34665, 25 | ClawMotif = 34666, 26 | MawMotif = 34667, 27 | HammerMotif = 34668, 28 | StarrySkyMotif = 34669, 29 | PomMuse = 34670, 30 | WingedMuse = 34671, 31 | ClawedMuse = 34672, 32 | FangedMuse = 34673, 33 | StrikingMuse = 34674, 34 | StarryMuse = 34675, 35 | MogOftheAges = 34676, 36 | Retribution = 34677, 37 | HammerStamp = 34678, 38 | HammerBrush = 34679, 39 | PolishingHammer = 34680, 40 | StarPrism = 34681, 41 | SubtractivePalette = 34683, 42 | Smudge = 34684, 43 | TemperaCoat = 34685, 44 | TemperaGrassa = 34686, 45 | RainbowDrip = 34688, 46 | CreatureMotif = 34689, 47 | WeaponMotif = 34690, 48 | LandscapeMotif = 34691, 49 | LivingMuse = 35347, 50 | SteelMuse = 35348, 51 | ScenicMuse = 35349; 52 | 53 | public static class Buffs { 54 | public const ushort 55 | SubtractivePalette = 4102, 56 | SubtractivePaletteStack = 3674, 57 | Chroma2Ready = 3675, 58 | Chroma3Ready = 3676, 59 | RainbowReady = 3679, 60 | HammerReady = 3680, 61 | StarPrismReady = 3681, 62 | Installation = 3688, 63 | ArtisticInstallation = 3689, 64 | SubtractivePaletteReady = 3690, 65 | InvertedColors = 3691; 66 | } 67 | 68 | public static class Debuffs { 69 | public const ushort 70 | DebuffPH = ushort.MaxValue; 71 | } 72 | 73 | public static class Levels { 74 | public const byte 75 | FireRed = 1, 76 | AeroGreen = 5, 77 | TemperaCoat = 10, 78 | WaterBlue = 15, 79 | Smudge = 20, 80 | ExtraFireRed = 25, 81 | CreatureMotif = 30, 82 | PomMotif = 30, 83 | WingMotif = 30, 84 | PomMuse = 30, 85 | WingedMuse = 30, 86 | MogOftheAges = 30, 87 | ExtraAeroGreen = 35, 88 | ExtraWaterBlue = 45, 89 | HammerMotif = 50, 90 | HammerStamp = 50, 91 | WeaponMotif = 50, 92 | StrikingMuse = 50, 93 | SubtractivePalette = 60, 94 | BlizzardCyan = 60, 95 | EarthYellow = 60, 96 | ThunderMagenta = 60, 97 | ExtraBlizzardCyan = 60, 98 | ExtraEarthYellow = 60, 99 | ExtraThunderMagenta = 60, 100 | StarrySkyMotif = 70, 101 | LandscapeMotif = 70, 102 | HolyWhite = 80, 103 | HammerBrush = 86, 104 | PolishingHammer = 86, 105 | TemperaGrassa = 88, 106 | CometBlack = 90, 107 | RainbowDrip = 92, 108 | ClawMotif = 96, 109 | MawMotif = 96, 110 | ClawedMuse = 96, 111 | FangedMuse = 96, 112 | StarryMuse = 70, 113 | Retribution = 96, 114 | StarPrism = 100; 115 | } 116 | } 117 | 118 | internal class PictomancerSTCombo: CustomCombo { 119 | public override CustomComboPreset Preset { get; } = CustomComboPreset.PictomancerSTComboFeature; 120 | public override uint[] ActionIDs { get; } = [PCT.BlizzardCyan]; 121 | 122 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte levels) { 123 | 124 | if (!SelfHasEffect(PCT.Buffs.SubtractivePaletteStack)) { 125 | 126 | if (SelfHasEffect(PCT.Buffs.Chroma2Ready)) 127 | return PCT.AeroGreen; 128 | 129 | if (SelfHasEffect(PCT.Buffs.Chroma3Ready)) 130 | return PCT.WaterBlue; 131 | 132 | return PCT.FireRed; 133 | } 134 | 135 | return actionID; 136 | } 137 | } 138 | 139 | internal class PictomancerAoECombo: CustomCombo { 140 | public override CustomComboPreset Preset { get; } = CustomComboPreset.PictomancerAOEComboFeature; 141 | public override uint[] ActionIDs { get; } = [PCT.ExtraBlizzardCyan]; 142 | 143 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte levels) { 144 | 145 | if (!SelfHasEffect(PCT.Buffs.SubtractivePaletteStack)) { 146 | 147 | if (SelfHasEffect(PCT.Buffs.Chroma2Ready)) 148 | return PCT.ExtraAeroGreen; 149 | 150 | if (SelfHasEffect(PCT.Buffs.Chroma3Ready)) 151 | return PCT.ExtraWaterBlue; 152 | 153 | return PCT.ExtraFireRed; 154 | } 155 | 156 | return actionID; 157 | 158 | } 159 | } 160 | 161 | internal class PictomancerHammerCombo: CustomCombo { 162 | public override CustomComboPreset Preset { get; } = CustomComboPreset.PictomancerWeaponMotifCombo; 163 | public override uint[] ActionIDs { get; } = [PCT.SteelMuse]; 164 | 165 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 166 | PCTGauge gauge = GetJobGauge(); 167 | 168 | if (IsEnabled(CustomComboPreset.PictomancerWeaponMotifCombo)) { 169 | 170 | if (IsEnabled(CustomComboPreset.PictomancerHammerCombo) && SelfHasEffect(PCT.Buffs.HammerReady)) 171 | return OriginalHook(PCT.HammerStamp); 172 | 173 | if (gauge.WeaponMotifDrawn) 174 | return OriginalHook(PCT.SteelMuse); 175 | 176 | } 177 | 178 | return OriginalHook(PCT.WeaponMotif); 179 | } 180 | 181 | 182 | internal class PictomancerScenicCombo: CustomCombo { 183 | public override CustomComboPreset Preset { get; } = CustomComboPreset.PictomancerScenicCombo; 184 | public override uint[] ActionIDs { get; } = [PCT.ScenicMuse]; 185 | 186 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 187 | PCTGauge gauge = GetJobGauge(); 188 | 189 | if (IsEnabled(CustomComboPreset.PictomancerScenicCombo)) { 190 | 191 | if (IsEnabled(CustomComboPreset.PictomancerStarPrismCombo) && level >= 100 && SelfHasEffect(PCT.Buffs.StarPrismReady)) 192 | return PCT.StarPrism; 193 | 194 | if (gauge.LandscapeMotifDrawn) 195 | return PCT.ScenicMuse; 196 | 197 | } 198 | 199 | return PCT.StarrySkyMotif; 200 | } 201 | } 202 | } 203 | 204 | 205 | internal class PictoimancerHolyCometCombo: CustomCombo { 206 | public override CustomComboPreset Preset { get; } = CustomComboPreset.PictomancerHolyCometCombo; 207 | public override uint[] ActionIDs { get; } = [PCT.HolyWhite]; 208 | 209 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 210 | PCTGauge gauge = GetJobGauge(); 211 | 212 | if (level >= PCT.Levels.CometBlack && SelfHasEffect(PCT.Buffs.InvertedColors)) 213 | return PCT.CometBlack; 214 | 215 | return actionID; 216 | } 217 | } 218 | 219 | 220 | 221 | internal class PictomancerCreatureCombo: CustomCombo { 222 | public override CustomComboPreset Preset { get; } = CustomComboPreset.PictomancerLivingMuseCombo; 223 | public override uint[] ActionIDs { get; } = [PCT.LivingMuse]; 224 | 225 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 226 | PCTGauge gauge = GetJobGauge(); 227 | 228 | if (gauge.CreatureMotifDrawn) 229 | return OriginalHook(PCT.LivingMuse); 230 | 231 | return OriginalHook(PCT.CreatureMotif); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/SCH.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | 3 | namespace VariableVixen.XIVComboVX.Combos; 4 | 5 | internal static class SCH { 6 | public const byte JobID = 28; 7 | 8 | public const uint 9 | Resurrection = 173, 10 | Aetherflow = 166, 11 | EnergyDrain = 167, 12 | SacredSoil = 188, 13 | Lustrate = 189, 14 | Indomitability = 3583, 15 | Broil = 3584, 16 | DeploymentTactics = 3585, 17 | EmergencyTactics = 3586, 18 | Dissipation = 3587, 19 | Excogitation = 7434, 20 | Broil2 = 7435, 21 | ChainStratagem = 7436, 22 | Aetherpact = 7437, 23 | WhisperingDawn = 16537, 24 | FeyIllumination = 16538, 25 | ArtOfWar = 16539, 26 | Broil3 = 16541, 27 | Recitation = 16542, 28 | FeyBless = 16543, 29 | SummonSeraph = 16545, 30 | Consolation = 16546, 31 | SummonEos = 17215, 32 | SummonSelene = 17216, 33 | Ruin = 17869, 34 | Ruin2 = 17870, 35 | Broil4 = 25865, 36 | ArtOfWar2 = 25866, 37 | BanefulImpaction = 37012; 38 | 39 | public static class Buffs { 40 | public const ushort 41 | Dissipation = 791, 42 | Recitation = 1896, 43 | ImpactImminent = 3882; 44 | } 45 | 46 | public static class Debuffs { 47 | // public const ushort placeholder = 0; 48 | } 49 | 50 | public static class Levels { 51 | public const byte 52 | Aetherflow = 45, 53 | Lustrate = 45, 54 | Excogitation = 62, 55 | ChainStratagem = 66, 56 | Recitation = 74, 57 | Consolation = 80, 58 | SummonSeraph = 80, 59 | BanefulImpaction = 92; 60 | } 61 | } 62 | 63 | internal class ScholarSwiftcastRaiserFeature: SwiftRaiseCombo { 64 | public override CustomComboPreset Preset => CustomComboPreset.ScholarSwiftcastRaiserFeature; 65 | } 66 | 67 | internal class ScholarArtOfWar: CustomCombo { 68 | public override CustomComboPreset Preset => CustomComboPreset.ScholarLucidArtOfWar; 69 | public override uint[] ActionIDs { get; } = [SCH.ArtOfWar, SCH.ArtOfWar2]; 70 | 71 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 72 | 73 | if (CheckLucidWeave(this.Preset, level, Service.Configuration.ScholarLucidArtOfWarManaThreshold, actionID)) 74 | return Common.LucidDreaming; 75 | 76 | return actionID; 77 | } 78 | } 79 | 80 | internal class ScholarRuinBoil: CustomCombo { 81 | public override CustomComboPreset Preset => CustomComboPreset.SchAny; 82 | public override uint[] ActionIDs { get; } = [SCH.Ruin, SCH.Broil, SCH.Broil2, SCH.Broil3, SCH.Broil4]; 83 | 84 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 85 | 86 | if (CheckLucidWeave(CustomComboPreset.ScholarLucidRuinBroil, level, Service.Configuration.ScholarLucidRuinBroilManaThreshold, actionID)) 87 | return Common.LucidDreaming; 88 | 89 | if (IsEnabled(CustomComboPreset.ScholarMobileRuinBroil)) { 90 | if (IsMoving) 91 | return SCH.Ruin2; 92 | } 93 | 94 | return actionID; 95 | } 96 | } 97 | 98 | internal class ScholarFeyBless: CustomCombo { 99 | public override CustomComboPreset Preset => CustomComboPreset.ScholarSeraphConsolationFeature; 100 | public override uint[] ActionIDs { get; } = [SCH.FeyBless]; 101 | 102 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 103 | 104 | if (level >= SCH.Levels.Consolation && GetJobGauge().SeraphTimer > 0) 105 | return SCH.Consolation; 106 | 107 | return actionID; 108 | } 109 | } 110 | 111 | internal class ScholarExcogitation: CustomCombo { 112 | public override CustomComboPreset Preset => CustomComboPreset.ScholarExcogFallbackFeature; 113 | public override uint[] ActionIDs { get; } = [SCH.Excogitation]; 114 | 115 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 116 | 117 | if (IsEnabled(CustomComboPreset.ScholarExcogitationRecitationFeature)) { 118 | if (level >= SCH.Levels.Recitation && IsOffCooldown(SCH.Recitation)) 119 | return SCH.Recitation; 120 | } 121 | 122 | if (IsEnabled(CustomComboPreset.ScholarExcogFallbackFeature)) { 123 | if (level < SCH.Levels.Excogitation || IsOnCooldown(SCH.Excogitation)) 124 | return SCH.Lustrate; 125 | } 126 | 127 | return actionID; 128 | } 129 | } 130 | 131 | internal class ScholarEnergyDrain: CustomCombo { 132 | public override CustomComboPreset Preset => CustomComboPreset.ScholarEnergyDrainAetherflowFeature; 133 | public override uint[] ActionIDs { get; } = [SCH.EnergyDrain]; 134 | 135 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 136 | 137 | if (level >= SCH.Levels.Aetherflow && GetJobGauge().Aetherflow == 0) 138 | return SCH.Aetherflow; 139 | 140 | return actionID; 141 | } 142 | } 143 | 144 | internal class ScholarLustrate: CustomCombo { 145 | public override CustomComboPreset Preset { get; } = CustomComboPreset.SmnAny; 146 | public override uint[] ActionIDs { get; } = [SCH.Lustrate]; 147 | 148 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 149 | 150 | if (IsEnabled(CustomComboPreset.ScholarLustrateRecitationFeature)) { 151 | if (level >= SCH.Levels.Recitation && IsOffCooldown(SCH.Recitation)) 152 | return SCH.Recitation; 153 | } 154 | 155 | if (IsEnabled(CustomComboPreset.ScholarLustrateExcogitationFeature)) { 156 | if (level >= SCH.Levels.Excogitation && IsOffCooldown(SCH.Excogitation)) 157 | return SCH.Excogitation; 158 | } 159 | 160 | if (IsEnabled(CustomComboPreset.ScholarLustrateAetherflowFeature)) { 161 | if (level >= SCH.Levels.Aetherflow && GetJobGauge().Aetherflow == 0) 162 | return SCH.Aetherflow; 163 | } 164 | 165 | return actionID; 166 | } 167 | } 168 | 169 | internal class ScholarIndomitability: CustomCombo { 170 | public override CustomComboPreset Preset { get; } = CustomComboPreset.ScholarIndomAetherflowFeature; 171 | public override uint[] ActionIDs { get; } = [SCH.Indomitability]; 172 | 173 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 174 | 175 | if (level >= SCH.Levels.Aetherflow && GetJobGauge().Aetherflow == 0 && !SelfHasEffect(SCH.Buffs.Recitation)) 176 | return SCH.Aetherflow; 177 | 178 | return actionID; 179 | } 180 | } 181 | 182 | internal class ScholarSummon: CustomCombo { 183 | public override CustomComboPreset Preset { get; } = CustomComboPreset.ScholarSeraphFeature; 184 | public override uint[] ActionIDs { get; } = [SCH.SummonEos, SCH.SummonSelene]; 185 | 186 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 187 | SCHGauge gauge = GetJobGauge(); 188 | 189 | if (gauge.SeraphTimer > 0 || HasPetPresent) 190 | return OriginalHook(SCH.SummonSeraph); 191 | 192 | return actionID; 193 | } 194 | } 195 | 196 | internal class ScholarChainStratagem: CustomCombo { 197 | public override CustomComboPreset Preset => CustomComboPreset.ScholarChainStratagemBanefulImpaction; 198 | public override uint[] ActionIDs { get; } = [SCH.ChainStratagem]; 199 | 200 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 201 | 202 | if (level >= SCH.Levels.BanefulImpaction && SelfHasEffect(SCH.Buffs.ImpactImminent)) 203 | return SCH.BanefulImpaction; 204 | 205 | return actionID; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /XIVComboVX/framework.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 44 | 45 | 46 | $(Product.Replace("-", ".")) 47 | $(Product.Replace("-", ".").Replace(" ", "")) 48 | $(Product) 49 | https://github.com/VariableVixen/$(Product) 50 | 51 | 52 | 53 | VariableVixen.$(PackageId) 54 | enable 55 | enable 56 | true 57 | true 58 | 12 59 | false 60 | false 61 | true 62 | false 63 | true 64 | false 65 | Recommended 66 | All 67 | None 68 | true 69 | false 70 | CA1805,CA1852,CA1707,$(NoWarn) 71 | $(SolutionDir)=./ 72 | bin\$(Configuration.ToLower()) 73 | 74 | 75 | 76 | true 77 | Copyleft $(Authors) 78 | $(PackageProjectUrl).git 79 | x64 80 | x64 81 | net9 82 | $(Version) 83 | $(AssemblyVersion) 84 | bin\publish\$(RuntimeIdentifier.ToLower())-$(Configuration.ToLower())\ 85 | 86 | 87 | 88 | 89 | 90 | VariableVixen 91 | 92 | 93 | 94 | 95 | VariableVixen, $(Authors) 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | true 104 | false 105 | true 106 | 107 | 108 | 109 | 110 | false 111 | true 112 | false 113 | 114 | 115 | 116 | 117 | 118 | true 119 | true 120 | 121 | 122 | 123 | 124 | 125 | true 126 | embedded 127 | false 128 | true 129 | DEBUG;TRACE 130 | $(SourceRevisionId).debug 131 | debug 132 | 133 | 134 | 135 | 136 | false 137 | none 138 | true 139 | false 140 | RELEASE;STRIPPED 141 | 142 | 143 | 144 | 145 | true 146 | portable 147 | true 148 | false 149 | RELEASE;TRACE 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/WHM.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | 3 | namespace VariableVixen.XIVComboVX.Combos; 4 | 5 | internal static class WHM { 6 | public const byte JobID = 24; 7 | 8 | public const uint 9 | Stone = 119, 10 | Cure = 120, 11 | Aero = 121, 12 | Medica = 124, 13 | Raise = 125, 14 | Stone2 = 127, 15 | Aero2 = 132, 16 | Cure2 = 135, 17 | PresenceOfMind = 136, 18 | Holy = 139, 19 | Benediction = 140, 20 | Stone3 = 3568, 21 | Asylum = 3569, 22 | Tetragrammaton = 3570, 23 | Assize = 3571, 24 | Stone4 = 7431, 25 | PlenaryIndulgence = 7433, 26 | AfflatusSolace = 16531, 27 | Dia = 16532, 28 | Glare = 16533, 29 | AfflatusRapture = 16534, 30 | AfflatusMisery = 16535, 31 | Temperance = 16536, 32 | Glare3 = 25859, 33 | Holy3 = 25860, 34 | Aquaveil = 25861, 35 | LiturgyOfTheBell = 25862, 36 | Glare4 = 37009, 37 | DivineCaress = 37011; 38 | 39 | public static class Buffs { 40 | public const ushort 41 | SacredSight = 3879, 42 | DivineGrace = 3881; 43 | } 44 | 45 | public static class Debuffs { 46 | public const ushort 47 | Aero = 143, 48 | Aero2 = 144, 49 | Dia = 1871; 50 | } 51 | 52 | public static class Levels { 53 | public const byte 54 | PresenceOfMind = 30, 55 | Cure2 = 30, 56 | AfflatusSolace = 52, 57 | AfflatusMisery = 74, 58 | AfflatusRapture = 76, 59 | Glare4 = 92, 60 | DivineCaress = 100; 61 | } 62 | } 63 | 64 | internal class WhiteMageSwiftcastRaiserFeature: SwiftRaiseCombo { 65 | public override CustomComboPreset Preset => CustomComboPreset.WhiteMageSwiftcastRaiserFeature; 66 | } 67 | 68 | internal class WhiteMageAero: CustomCombo { 69 | public override CustomComboPreset Preset => CustomComboPreset.WhmAny; 70 | public override uint[] ActionIDs { get; } = [WHM.Aero, WHM.Aero2, WHM.Dia]; 71 | 72 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 73 | 74 | if (CheckLucidWeave(CustomComboPreset.WhiteMageLucidWeave, level, Service.Configuration.WhiteMageLucidWeaveManaThreshold, actionID)) 75 | return Common.LucidDreaming; 76 | 77 | return actionID; 78 | } 79 | } 80 | 81 | internal class WhiteMageStone: CustomCombo { 82 | public override CustomComboPreset Preset => CustomComboPreset.WhmAny; 83 | public override uint[] ActionIDs { get; } = [WHM.Stone, WHM.Stone2, WHM.Stone3, WHM.Stone4, WHM.Glare, WHM.Glare3, WHM.Glare4]; 84 | 85 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 86 | 87 | if (CheckLucidWeave(CustomComboPreset.WhiteMageLucidWeave, level, Service.Configuration.WhiteMageLucidWeaveManaThreshold, actionID)) 88 | return Common.LucidDreaming; 89 | 90 | if (IsEnabled(CustomComboPreset.WhiteMageDotRefresh)) { 91 | ushort effectIdToTrack = OriginalHook(WHM.Aero) switch { 92 | WHM.Aero => WHM.Debuffs.Aero, 93 | WHM.Aero2 => WHM.Debuffs.Aero2, 94 | WHM.Dia => WHM.Debuffs.Dia, 95 | _ => 0, 96 | }; 97 | 98 | if (effectIdToTrack > 0 && HasTarget && TargetOwnEffectDuration(effectIdToTrack) < Service.Configuration.WhiteMageDotRefreshDuration) 99 | return OriginalHook(WHM.Aero); 100 | } 101 | 102 | if (IsEnabled(CustomComboPreset.WhiteMageGlareIVCombo)) { 103 | if (level >= WHM.Levels.Glare4 && SelfHasEffect(WHM.Buffs.SacredSight)) 104 | return WHM.Glare4; 105 | } 106 | 107 | if (IsEnabled(CustomComboPreset.WhiteMageGlareOfMind)) { 108 | if (level >= WHM.Levels.PresenceOfMind && IsOffCooldown(WHM.PresenceOfMind)) 109 | return WHM.PresenceOfMind; 110 | } 111 | 112 | 113 | return actionID; 114 | } 115 | } 116 | 117 | internal class WhiteMageSolaceMiseryFeature: CustomCombo { 118 | public override CustomComboPreset Preset => CustomComboPreset.WhiteMageSolaceMiseryFeature; 119 | public override uint[] ActionIDs { get; } = [WHM.AfflatusSolace]; 120 | 121 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 122 | 123 | if (level >= WHM.Levels.AfflatusMisery && GetJobGauge().BloodLily == 3) 124 | return WHM.AfflatusMisery; 125 | 126 | return actionID; 127 | } 128 | } 129 | 130 | internal class WhiteMageRaptureMiseryFeature: CustomCombo { 131 | public override CustomComboPreset Preset => CustomComboPreset.WhiteMageRaptureMiseryFeature; 132 | public override uint[] ActionIDs { get; } = [WHM.AfflatusRapture]; 133 | 134 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 135 | 136 | if (level >= WHM.Levels.AfflatusMisery && GetJobGauge().BloodLily == 3 && HasTarget) 137 | return WHM.AfflatusMisery; 138 | 139 | return actionID; 140 | } 141 | } 142 | 143 | internal class WhiteMageHoly: CustomCombo { 144 | public override CustomComboPreset Preset { get; } = CustomComboPreset.WhmAny; 145 | public override uint[] ActionIDs { get; } = [WHM.Holy, WHM.Holy3]; 146 | 147 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 148 | 149 | if (CheckLucidWeave(CustomComboPreset.WhiteMageLucidWeave, level, Service.Configuration.WhiteMageLucidWeaveManaThreshold, actionID)) 150 | return Common.LucidDreaming; 151 | 152 | if (IsEnabled(CustomComboPreset.WhiteMageHolyMiseryFeature)) { 153 | if (level >= WHM.Levels.AfflatusMisery && GetJobGauge().BloodLily == 3 && HasTarget) 154 | return WHM.AfflatusMisery; 155 | } 156 | 157 | return actionID; 158 | } 159 | } 160 | 161 | internal class WhiteMageCure2: CustomCombo { 162 | public override CustomComboPreset Preset => CustomComboPreset.WhiteMageCureFeature; 163 | public override uint[] ActionIDs { get; } = [WHM.Cure2]; 164 | 165 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 166 | 167 | if (IsEnabled(CustomComboPreset.WhiteMageCureFeature)) { 168 | if (level < WHM.Levels.Cure2) 169 | return WHM.Cure; 170 | } 171 | 172 | if (IsEnabled(CustomComboPreset.WhiteMageAfflatusFeature)) { 173 | WHMGauge gauge = GetJobGauge(); 174 | 175 | if (IsEnabled(CustomComboPreset.WhiteMageSolaceMiseryFeature)) { 176 | if (level >= WHM.Levels.AfflatusMisery && gauge.BloodLily == 3) 177 | return WHM.AfflatusMisery; 178 | } 179 | 180 | if (level >= WHM.Levels.AfflatusSolace && gauge.Lily > 0) 181 | return WHM.AfflatusSolace; 182 | } 183 | 184 | return actionID; 185 | } 186 | } 187 | 188 | internal class WhiteMageMedica: CustomCombo { 189 | public override CustomComboPreset Preset => CustomComboPreset.WhiteMageAfflatusFeature; 190 | public override uint[] ActionIDs { get; } = [WHM.Medica]; 191 | 192 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 193 | WHMGauge gauge = GetJobGauge(); 194 | 195 | if (IsEnabled(CustomComboPreset.WhiteMageRaptureMiseryFeature)) { 196 | if (level >= WHM.Levels.AfflatusMisery && gauge.BloodLily == 3 && HasTarget) 197 | return WHM.AfflatusMisery; 198 | } 199 | 200 | if (level >= WHM.Levels.AfflatusRapture && gauge.Lily > 0) 201 | return WHM.AfflatusRapture; 202 | 203 | return actionID; 204 | } 205 | } 206 | 207 | internal class WhiteMagePresenceOfMind: CustomCombo { 208 | public override CustomComboPreset Preset => CustomComboPreset.WhiteMagePresenceOfMindGlare4; 209 | public override uint[] ActionIDs { get; } = [WHM.PresenceOfMind]; 210 | 211 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 212 | 213 | if (level >= WHM.Levels.Glare4 && SelfHasEffect(WHM.Buffs.SacredSight)) 214 | return WHM.Glare4; 215 | 216 | return actionID; 217 | } 218 | } 219 | 220 | internal class WhiteMageTemperance: CustomCombo { 221 | public override CustomComboPreset Preset => CustomComboPreset.WhiteMageTemperanceDivineCaress; 222 | public override uint[] ActionIDs { get; } = [WHM.Temperance]; 223 | 224 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 225 | 226 | if (level >= WHM.Levels.DivineCaress && SelfHasEffect(WHM.Buffs.DivineGrace)) 227 | return WHM.DivineCaress; 228 | 229 | return actionID; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/PLD.cs: -------------------------------------------------------------------------------- 1 | using VariableVixen.XIVComboVX; 2 | 3 | namespace VariableVixen.XIVComboVX.Combos; 4 | 5 | internal static class PLD { 6 | public const byte JobID = 19; 7 | 8 | public const uint 9 | FastBlade = 9, 10 | RiotBlade = 15, 11 | Sentinel = 17, 12 | Guardian = ushort.MaxValue, 13 | FightOrFlight = 20, 14 | RageOfHalone = 21, 15 | CircleOfScorn = 23, 16 | ShieldLob = 24, 17 | SpiritsWithin = 29, 18 | GoringBlade = 3538, 19 | RoyalAuthority = 3539, 20 | Sheltron = 3542, 21 | TotalEclipse = 7381, 22 | Requiescat = 7383, 23 | Imperator = ushort.MaxValue, 24 | HolySpirit = 7384, 25 | Prominence = 16457, 26 | HolyCircle = 16458, 27 | Confiteor = 16459, 28 | Atonement = 16460, 29 | Supplication = ushort.MaxValue, 30 | Sepulchre = ushort.MaxValue, 31 | Intervene = 16461, 32 | Expiacion = 25747, 33 | BladeOfFaith = 25748, 34 | BladeOfTruth = 25749, 35 | BladeOfValor = 25750, 36 | BladeOfHonor = ushort.MaxValue; 37 | 38 | public static class Buffs { 39 | public const ushort 40 | GoringBladeReady = ushort.MaxValue, 41 | AttonementReady = ushort.MaxValue, 42 | SupplicationReady = ushort.MaxValue, 43 | SepulchreReady = ushort.MaxValue, 44 | BladeOfHonorReady = ushort.MaxValue, 45 | FightOrFlight = 76, 46 | Requiescat = 1368, 47 | //SwordOath = 1902, 48 | DivineMight = 2673, 49 | ConfiteorReady = 3019; 50 | } 51 | 52 | public static class Debuffs { 53 | public const ushort 54 | GoringBlade = 725, 55 | BladeOfValor = 2721; 56 | } 57 | 58 | public static class Levels { 59 | public const byte 60 | FightOrFlight = 2, 61 | RiotBlade = 4, 62 | TotalEclipse = 6, 63 | ShieldLob = 15, 64 | SpiritsWithin = 30, 65 | Sheltron = 35, 66 | Sentinel = 38, 67 | CircleOfScorn = 50, 68 | RageOfHalone = 26, 69 | Prominence = 40, 70 | GoringBlade = 54, 71 | RoyalAuthority = 60, 72 | HolySpirit = 64, 73 | Requiescat = 68, 74 | HolyCircle = 72, 75 | Intervene = 74, 76 | Atonement = 76, 77 | Confiteor = 80, 78 | Expiacion = 86, 79 | BladeOfFaith = 90, 80 | BladeOfTruth = 90, 81 | BladeOfValor = 90; 82 | } 83 | } 84 | 85 | internal class PaladinStunInterruptFeature: StunInterruptCombo { 86 | public override CustomComboPreset Preset { get; } = CustomComboPreset.PaladinStunInterruptFeature; 87 | } 88 | 89 | internal class PaladinRoyalAuthorityCombo: CustomCombo { 90 | public override CustomComboPreset Preset => CustomComboPreset.PaladinRoyalAuthorityCombo; 91 | public override uint[] ActionIDs { get; } = [PLD.RageOfHalone, PLD.RoyalAuthority]; 92 | 93 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 94 | 95 | if (CanWeave(actionID)) { 96 | 97 | if (IsEnabled(CustomComboPreset.PaladinRoyalWeaveFightOrFlight) && level >= PLD.Levels.FightOrFlight) { 98 | if (CanUse(PLD.FightOrFlight)) 99 | return PLD.FightOrFlight; 100 | } 101 | 102 | if (IsEnabled(CustomComboPreset.PaladinRoyalWeaveSpiritsWithin) && level >= PLD.Levels.SpiritsWithin) { 103 | uint actual = OriginalHook(PLD.SpiritsWithin); 104 | if (CanUse(actual)) 105 | return actual; 106 | } 107 | 108 | } 109 | 110 | if (IsEnabled(CustomComboPreset.PaladinRoyalConfiteor) && level >= PLD.Levels.Confiteor) { 111 | if (SelfHasEffect(PLD.Buffs.Requiescat)) 112 | return OriginalHook(PLD.Confiteor); 113 | } 114 | 115 | if (IsEnabled(CustomComboPreset.PaladinRoyalAuthorityGoringBlade) && level >= PLD.Levels.GoringBlade) { 116 | if (SelfHasEffect(PLD.Buffs.GoringBladeReady)) 117 | return PLD.GoringBlade; 118 | } 119 | 120 | if (IsEnabled(CustomComboPreset.PaladinRoyalAuthorityHolySpirit)) { 121 | if (level >= PLD.Levels.HolySpirit) { 122 | if (SelfHasEffect(PLD.Buffs.DivineMight)) 123 | return PLD.HolySpirit; 124 | } 125 | } 126 | 127 | if (lastComboMove is PLD.FastBlade) { 128 | if (level >= PLD.Levels.RiotBlade) 129 | return PLD.RiotBlade; 130 | } 131 | else if (lastComboMove is PLD.RiotBlade) { 132 | if (level >= PLD.Levels.RageOfHalone) 133 | return OriginalHook(PLD.RageOfHalone); 134 | } 135 | 136 | if (IsEnabled(CustomComboPreset.PaladinRoyalAuthorityRangeSwapFeature)) { 137 | if (level >= PLD.Levels.Intervene) { 138 | if (TargetDistance is > 3 and <= 20) 139 | return PLD.Intervene; 140 | } 141 | else if (IsEnabled(CustomComboPreset.PaladinInterveneSyncFeature)) { 142 | if (level >= PLD.Levels.ShieldLob) { 143 | if (TargetDistance is > 3 and <= 20) 144 | return PLD.ShieldLob; 145 | } 146 | } 147 | } 148 | 149 | if (IsEnabled(CustomComboPreset.PaladinAtonementFeature)) { 150 | if (level >= PLD.Levels.Atonement) { 151 | if (SelfHasEffect(PLD.Buffs.AttonementReady)) 152 | return PLD.Atonement; 153 | if (SelfHasEffect(PLD.Buffs.SupplicationReady)) 154 | return PLD.Supplication; 155 | if (SelfHasEffect(PLD.Buffs.SepulchreReady)) 156 | return PLD.Sepulchre; 157 | } 158 | } 159 | 160 | return PLD.FastBlade; 161 | } 162 | } 163 | 164 | internal class PaladinProminenceCombo: CustomCombo { 165 | public override CustomComboPreset Preset => CustomComboPreset.PaladinProminenceCombo; 166 | public override uint[] ActionIDs { get; } = [PLD.Prominence]; 167 | 168 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 169 | 170 | if (CanWeave(actionID)) { 171 | 172 | if (IsEnabled(CustomComboPreset.PaladinProminenceWeaveFightOrFlight) && level >= PLD.Levels.FightOrFlight) { 173 | if (CanUse(PLD.FightOrFlight)) 174 | return PLD.FightOrFlight; 175 | } 176 | 177 | if (IsEnabled(CustomComboPreset.PaladinProminenceWeaveCircleOfScorn) && level >= PLD.Levels.CircleOfScorn) { 178 | if (CanUse(PLD.CircleOfScorn)) 179 | return PLD.CircleOfScorn; 180 | } 181 | 182 | } 183 | 184 | if (IsEnabled(CustomComboPreset.PaladinProminenceConfiteor) && level >= PLD.Levels.Confiteor) { 185 | if (SelfHasEffect(PLD.Buffs.Requiescat)) 186 | return OriginalHook(PLD.Confiteor); 187 | } 188 | 189 | if (IsEnabled(CustomComboPreset.PaladinProminenceHolyCircle)) { 190 | if (level >= PLD.Levels.HolyCircle) { 191 | if (SelfHasEffect(PLD.Buffs.DivineMight)) 192 | return PLD.HolyCircle; 193 | } 194 | } 195 | 196 | if (lastComboMove is PLD.TotalEclipse && level >= PLD.Levels.Prominence) 197 | return PLD.Prominence; 198 | 199 | return PLD.TotalEclipse; 200 | } 201 | } 202 | 203 | internal class PaladinHolySpiritHolyCircle: CustomCombo { 204 | public override CustomComboPreset Preset => CustomComboPreset.PaladinHolyConfiteor; 205 | public override uint[] ActionIDs { get; } = [PLD.HolySpirit, PLD.HolyCircle]; 206 | 207 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 208 | 209 | if (level >= PLD.Levels.Confiteor) { 210 | if (SelfHasEffect(PLD.Buffs.Requiescat)) 211 | return OriginalHook(PLD.Confiteor); 212 | } 213 | 214 | return actionID; 215 | } 216 | } 217 | 218 | internal class PaladinRequiescat: CustomCombo { 219 | public override CustomComboPreset Preset => CustomComboPreset.PaladinRequiescatConfiteor; 220 | public override uint[] ActionIDs { get; } = [PLD.Requiescat, PLD.Imperator]; 221 | 222 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 223 | 224 | if (level >= PLD.Levels.Confiteor) { 225 | if (SelfHasEffect(PLD.Buffs.ConfiteorReady)) 226 | return OriginalHook(PLD.Confiteor); 227 | } 228 | 229 | return actionID; 230 | } 231 | } 232 | 233 | internal class PaladinInterveneSyncFeature: CustomCombo { 234 | public override CustomComboPreset Preset => CustomComboPreset.PaladinInterveneSyncFeature; 235 | public override uint[] ActionIDs { get; } = [PLD.Intervene]; 236 | 237 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) => level < PLD.Levels.Intervene ? PLD.ShieldLob : actionID; 238 | } 239 | 240 | internal class PaladinSheltron: CustomCombo { 241 | public override CustomComboPreset Preset => CustomComboPreset.PaladinSheltronSentinel; 242 | public override uint[] ActionIDs { get; } = [PLD.Sheltron]; 243 | 244 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 245 | uint sentinel = OriginalHook(PLD.Sentinel); 246 | return level > PLD.Levels.Sentinel && CanUse(sentinel) ? sentinel : actionID; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/SMN.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | 3 | namespace VariableVixen.XIVComboVX.Combos; 4 | 5 | internal static class SMN 6 | { 7 | public const byte ClassID = 26; 8 | public const byte JobID = 27; 9 | 10 | public const uint 11 | 12 | Ruin = 163, 13 | Ruin2 = 172, 14 | Ruin3 = 3579, 15 | Ruin4 = 7426, 16 | Fester = 181, 17 | Painflare = 3578, 18 | DreadwyrmTrance = 3581, 19 | Deathflare = 3582, 20 | SummonBahamut = 7427, 21 | EnkindleBahamut = 7429, 22 | Physick = 16230, 23 | EnergySyphon = 16510, 24 | Outburst = 16511, 25 | EnkindlePhoenix = 16516, 26 | EnergyDrain = 16508, 27 | SummonCarbuncle = 25798, 28 | RadiantAegis = 25799, 29 | Aethercharge = 25800, 30 | SearingLight = 25801, 31 | SummonRuby = 25802, 32 | SummonTopaz = 25803, 33 | SummonEmerald = 25804, 34 | SummonIfrit = 25805, 35 | SummonTitan = 25806, 36 | SummonGaruda = 25807, 37 | AstralFlow = 25822, 38 | TriDisaster = 25826, 39 | Rekindle = 25830, 40 | SummonPhoenix = 25831, 41 | CrimsonCyclone = 25835, 42 | MountainBuster = 25836, 43 | Slipstream = 25837, 44 | SummonIfrit2 = 25838, 45 | SummonTitan2 = 25839, 46 | SummonGaruda2 = 25840, 47 | CrimsonStrike = 25885, 48 | Gemshine = 25883, 49 | PreciousBrilliance = 25884, 50 | Necrosis = 36990, 51 | SearingFlash = 36991, 52 | SummonSolarBahamut = 36992, 53 | Sunflare = 36996, 54 | LuxSolaris = 36997, 55 | EnkindleSolarBahamut = 36998, 56 | Resurrection = 173; 57 | 58 | public static class Buffs 59 | { 60 | public const ushort 61 | Aetherflow = 304, 62 | FurtherRuin = 2701, 63 | SearingLight = 2703, 64 | IfritsFavor = 2724, 65 | GarudasFavor = 2725, 66 | TitansFavor = 2853, 67 | RubysGlimmer = 3873, 68 | LuxSolarisReady = 3874, 69 | CrimsonStrikeReady = 4403; 70 | } 71 | 72 | public static class Debuffs 73 | { 74 | public const ushort 75 | Placeholder = 0; 76 | } 77 | 78 | public static class Levels 79 | { 80 | public const byte 81 | SummonCarbuncle = 2, 82 | RadiantAegis = 2, 83 | Gemshine = 6, 84 | EnergyDrain = 10, 85 | Fester = 10, 86 | PreciousBrilliance = 26, 87 | Painflare = 40, 88 | EnergySyphon = 52, 89 | Ruin3 = 54, 90 | Ruin4 = 62, 91 | SearingLight = 66, 92 | EnkindleBahamut = 70, 93 | Rekindle = 80, 94 | ElementalMastery = 86, 95 | SummonPhoenix = 80, 96 | Necrosis = 92, 97 | SummonSolarBahamut = 100, 98 | LuxSolaris = 100, 99 | Resurrection = 12; 100 | } 101 | } 102 | 103 | internal class SummonerSwiftcastRaiserFeature: SwiftRaiseCombo { 104 | public override CustomComboPreset Preset => CustomComboPreset.SummonerSwiftcastRaiserFeature; 105 | } 106 | 107 | // returning Soon™ (when we have the time to go over everything) 108 | 109 | internal class SummonerEDFesterCombo: CustomCombo { 110 | public override CustomComboPreset Preset => CustomComboPreset.SummonerEDFesterCombo; 111 | public override uint[] ActionIDs { get; } = [SMN.Fester]; 112 | 113 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 114 | 115 | if (level >= SMN.Levels.EnergyDrain && !GetJobGauge().HasAetherflowStacks) 116 | return SMN.EnergyDrain; 117 | 118 | return actionID; 119 | } 120 | } 121 | 122 | internal class SummonerESPainflareCombo: CustomCombo { 123 | public override CustomComboPreset Preset => CustomComboPreset.SummonerESPainflareCombo; 124 | public override uint[] ActionIDs { get; } = [SMN.Painflare]; 125 | 126 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 127 | 128 | if (level >= SMN.Levels.EnergySyphon && !GetJobGauge().HasAetherflowStacks) 129 | return SMN.EnergySyphon; 130 | 131 | if (level < SMN.Levels.EnergySyphon && !GetJobGauge().HasAetherflowStacks) 132 | return SMN.EnergyDrain; 133 | 134 | return actionID; 135 | } 136 | } 137 | 138 | internal class SummonerRuinFeature: CustomCombo { 139 | public override CustomComboPreset Preset { get; } = CustomComboPreset.SmnAny; 140 | public override uint[] ActionIDs { get; } = [SMN.Ruin, SMN.Ruin2, SMN.Ruin3]; 141 | 142 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 143 | SMNGauge gauge = GetJobGauge(); 144 | 145 | if (IsEnabled(CustomComboPreset.SummonerRuinTitansFavorFeature) && level >= SMN.Levels.ElementalMastery && SelfHasEffect(SMN.Buffs.TitansFavor)) 146 | return SMN.MountainBuster; 147 | 148 | if (IsEnabled(CustomComboPreset.SummonerRuinFeature) && level >= SMN.Levels.Gemshine && (gauge.IsIfritAttuned || gauge.IsTitanAttuned || gauge.IsGarudaAttuned)) 149 | return OriginalHook(SMN.Gemshine); 150 | 151 | if (IsEnabled(CustomComboPreset.SummonerFurtherRuinFeature) && level >= SMN.Levels.Ruin4 && gauge.SummonTimerRemaining == 0 && gauge.AttunementTimerRemaining == 0 && SelfHasEffect(SMN.Buffs.FurtherRuin)) 152 | return SMN.Ruin4; 153 | 154 | return actionID; 155 | } 156 | } 157 | 158 | internal class SummonerOutburstFeature: CustomCombo { 159 | public override CustomComboPreset Preset { get; } = CustomComboPreset.SmnAny; 160 | public override uint[] ActionIDs { get; } = [SMN.Outburst, SMN.TriDisaster]; 161 | 162 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 163 | SMNGauge gauge = GetJobGauge(); 164 | 165 | if (IsEnabled(CustomComboPreset.SummonerOutburstTitansFavorFeature) && level >= SMN.Levels.ElementalMastery && SelfHasEffect(SMN.Buffs.TitansFavor)) 166 | return SMN.MountainBuster; 167 | 168 | if (IsEnabled(CustomComboPreset.SummonerOutburstFeature) && level >= SMN.Levels.PreciousBrilliance && (gauge.IsIfritAttuned || gauge.IsTitanAttuned || gauge.IsGarudaAttuned)) 169 | return OriginalHook(SMN.PreciousBrilliance); 170 | 171 | if (IsEnabled(CustomComboPreset.SummonerFurtherOutburstFeature) && level >= SMN.Levels.Ruin4 && gauge.SummonTimerRemaining == 0 && gauge.AttunementTimerRemaining == 0 && SelfHasEffect(SMN.Buffs.FurtherRuin)) 172 | return SMN.Ruin4; 173 | 174 | return actionID; 175 | } 176 | } 177 | /* 178 | internal class SummonerShinyFeature: CustomCombo { 179 | public override CustomComboPreset Preset { get; } = CustomComboPreset.SmnAny; 180 | public override uint[] ActionIDs { get; } = [SMN.Gemshine, SMN.PreciousBrilliance]; 181 | 182 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 183 | SMNGauge gauge = GetJobGauge(); 184 | 185 | if (IsEnabled(CustomComboPreset.SummonerShinyTitansFavorFeature) && level >= SMN.Levels.ElementalMastery && SelfHasEffect(SMN.Buffs.TitansFavor)) 186 | return SMN.MountainBuster; 187 | 188 | if (IsEnabled(CustomComboPreset.SummonerShinyEnkindleFeature) && level >= SMN.Levels.EnkindleBahamut && !gauge.IsIfritAttuned && !gauge.IsTitanAttuned && !gauge.IsGarudaAttuned && gauge.SummonTimerRemaining > 0) 189 | return OriginalHook(SMN.EnkindleBahamut); 190 | 191 | if (IsEnabled(CustomComboPreset.SummonerFurtherShinyFeature) && level >= SMN.Levels.Ruin4 && gauge.SummonTimerRemaining == 0 && gauge.AttunmentTimerRemaining == 0 && SelfHasEffect(SMN.Buffs.FurtherRuin)) 192 | return SMN.Ruin4; 193 | 194 | return actionID; 195 | } 196 | } 197 | 198 | internal class SummonerDemiFeature: CustomCombo { 199 | public override CustomComboPreset Preset { get; } = CustomComboPreset.SummonerDemiEnkindleFeature; 200 | public override uint[] ActionIDs { get; } = [SMN.Aethercharge, SMN.DreadwyrmTrance, SMN.SummonBahamut]; 201 | 202 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 203 | SMNGauge gauge = GetJobGauge(); 204 | 205 | if (level >= SMN.Levels.EnkindleBahamut && !gauge.IsIfritAttuned && !gauge.IsTitanAttuned && !gauge.IsGarudaAttuned && gauge.SummonTimerRemaining > 0) 206 | return OriginalHook(SMN.EnkindleBahamut); 207 | 208 | return actionID; 209 | } 210 | } 211 | 212 | internal class SummonerRadiantCarbundleFeature: CustomCombo { 213 | public override CustomComboPreset Preset { get; } = CustomComboPreset.SummonerRadiantCarbuncleFeature; 214 | public override uint[] ActionIDs { get; } = [SMN.RadiantAegis]; 215 | 216 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 217 | 218 | if (level >= SMN.Levels.SummonCarbuncle && !HasPetPresent && GetJobGauge().Attunement == 0) 219 | return SMN.SummonCarbuncle; 220 | 221 | return actionID; 222 | } 223 | } 224 | 225 | internal class SummonerSlipcastFeature: CustomCombo { 226 | public override CustomComboPreset Preset { get; } = CustomComboPreset.SummonerSlipcastFeature; 227 | public override uint[] ActionIDs { get; } = [SMN.AstralFlow, SMN.Slipstream]; 228 | 229 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 230 | 231 | if (SelfHasEffect(SMN.Buffs.GarudasFavor) && IsOffCooldown(Common.Swiftcast)) 232 | return Common.Swiftcast; 233 | 234 | return OriginalHook(actionID); 235 | } 236 | } 237 | */ 238 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/WAR.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | 3 | namespace VariableVixen.XIVComboVX.Combos; 4 | 5 | internal static class WAR { 6 | public const byte JobID = 21; 7 | 8 | public const uint 9 | HeavySwing = 31, 10 | Maim = 37, 11 | Berserk = 38, 12 | ThrillOfBattle = 40, 13 | Overpower = 41, 14 | StormsPath = 42, 15 | StormsEye = 45, 16 | Tomahawk = 46, 17 | InnerBeast = 49, 18 | SteelCyclone = 51, 19 | Infuriate = 52, 20 | FellCleave = 3549, 21 | Decimate = 3550, 22 | RawIntuition = 3551, 23 | Equilibrium = 3552, 24 | Upheaval = 7387, 25 | InnerRelease = 7389, 26 | MythrilTempest = 16462, 27 | ChaoticCyclone = 16463, 28 | NascentFlash = 16464, 29 | InnerChaos = 16465, 30 | Bloodwhetting = 25751, 31 | Orogeny = 25752, 32 | PrimalRend = 25753; 33 | 34 | public static class Buffs { 35 | public const ushort 36 | Berserk = 86, 37 | InnerRelease = 1177, 38 | NascentChaos = 1897, 39 | PrimalRendReady = 2624, 40 | PrimalRuinationReady = ushort.MaxValue, 41 | SurgingTempest = 2677; 42 | } 43 | 44 | public static class Debuffs { 45 | // public const ushort placeholder = 0; 46 | } 47 | 48 | public static class Levels { 49 | public const byte 50 | Maim = 4, 51 | Berserk = 6, 52 | Overpower = 10, 53 | Tomahawk = 15, 54 | StormsPath = 26, 55 | ThrillOfBattle = 30, 56 | InnerBeast = 35, 57 | MythrilTempest = 40, 58 | StormsEye = 50, 59 | Infuriate = 50, 60 | FellCleave = 54, 61 | RawIntuition = 56, 62 | Equilibrium = 58, 63 | Decimate = 60, 64 | Upheaval = 64, 65 | InnerRelease = 70, 66 | ChaoticCyclone = 72, 67 | MythrilTempestTrait = 74, 68 | NascentFlash = 76, 69 | InnerChaos = 80, 70 | Bloodwhetting = 82, 71 | Orogeny = 86, 72 | PrimalRend = 90; 73 | } 74 | } 75 | 76 | internal class WarriorStunInterruptFeature: StunInterruptCombo { 77 | public override CustomComboPreset Preset { get; } = CustomComboPreset.WarriorStunInterruptFeature; 78 | } 79 | 80 | internal class WarriorStormsPathCombo: CustomCombo { 81 | public override CustomComboPreset Preset => CustomComboPreset.WarriorStormsPathCombo; 82 | public override uint[] ActionIDs { get; } = [WAR.StormsPath]; 83 | 84 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 85 | 86 | if (IsEnabled(CustomComboPreset.WarriorSmartWeaveSingleTargetPath)) { 87 | if (level >= WAR.Levels.Upheaval) { 88 | if (SelfHasEffect(WAR.Buffs.SurgingTempest) || !IsEnabled(CustomComboPreset.WarriorSmartWeaveSingleTargetPathOnlyBuffed)) { 89 | if (CanWeave(actionID) && CanUse(WAR.Upheaval)) 90 | return WAR.Upheaval; 91 | } 92 | } 93 | } 94 | 95 | if (IsEnabled(CustomComboPreset.WarriorInnerReleaseFeature) && level >= WAR.Levels.InnerRelease && SelfHasEffect(WAR.Buffs.InnerRelease)) 96 | return OriginalHook(WAR.FellCleave); 97 | 98 | uint finalAction = WAR.StormsPath; 99 | byte finalLevel = WAR.Levels.StormsPath; 100 | if (IsEnabled(CustomComboPreset.WarriorSmartStormCombo)) { 101 | if (level >= WAR.Levels.StormsEye) { 102 | if (SelfEffectDuration(WAR.Buffs.SurgingTempest) <= Service.Configuration.WarriorStormBuffSaverBuffTime) { 103 | finalAction = WAR.StormsEye; 104 | finalLevel = WAR.Levels.StormsEye; 105 | } 106 | } 107 | } 108 | 109 | bool inCombo = false; 110 | uint nextCombo = 0; 111 | if (lastComboMove is WAR.Maim && level >= finalLevel) { 112 | inCombo = true; 113 | nextCombo = finalAction; 114 | } 115 | if (lastComboMove is WAR.HeavySwing && level >= WAR.Levels.Maim) { 116 | inCombo = true; 117 | nextCombo = WAR.Maim; 118 | } 119 | 120 | if (IsEnabled(CustomComboPreset.WarriorGaugeOvercapPathFeature)) { 121 | if (level >= WAR.Levels.InnerBeast) { 122 | if (GetJobGauge().BeastGauge > (inCombo ? 90 : 70)) 123 | return OriginalHook(WAR.FellCleave); 124 | } 125 | } 126 | 127 | return inCombo ? nextCombo : WAR.HeavySwing; 128 | } 129 | } 130 | 131 | internal class WarriorStormsEyeCombo: CustomCombo { 132 | public override CustomComboPreset Preset => CustomComboPreset.WarriorStormsEyeCombo; 133 | public override uint[] ActionIDs { get; } = [WAR.StormsEye]; 134 | 135 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 136 | 137 | if (IsEnabled(CustomComboPreset.WarriorSmartWeaveSingleTargetEye)) { 138 | if (level >= WAR.Levels.Upheaval) { 139 | if (SelfHasEffect(WAR.Buffs.SurgingTempest) || !IsEnabled(CustomComboPreset.WarriorSmartWeaveSingleTargetEyeOnlyBuffed)) { 140 | if (CanWeave(actionID) && CanUse(WAR.Upheaval)) 141 | return WAR.Upheaval; 142 | } 143 | } 144 | } 145 | 146 | if (IsEnabled(CustomComboPreset.WarriorInnerReleaseFeature)) { 147 | if (level >= WAR.Levels.InnerRelease) { 148 | if (SelfHasEffect(WAR.Buffs.InnerRelease)) 149 | return OriginalHook(WAR.FellCleave); 150 | } 151 | } 152 | 153 | bool inCombo = false; 154 | uint nextCombo = 0; 155 | if (lastComboMove is WAR.Maim && level >= WAR.Levels.StormsEye) { 156 | inCombo = true; 157 | nextCombo = IsEnabled(CustomComboPreset.WarriorStormsEyeBuffOvercapSaver) && SelfEffectDuration(WAR.Buffs.SurgingTempest) > 30 158 | ? WAR.StormsPath 159 | : WAR.StormsEye; 160 | } 161 | if (lastComboMove is WAR.HeavySwing && level >= WAR.Levels.Maim) { 162 | inCombo = true; 163 | nextCombo = WAR.Maim; 164 | } 165 | 166 | if (IsEnabled(CustomComboPreset.WarriorGaugeOvercapEyeFeature)) { 167 | if (level >= WAR.Levels.InnerBeast) { 168 | if (GetJobGauge().BeastGauge > (inCombo ? 90 : 80)) 169 | return OriginalHook(WAR.FellCleave); 170 | } 171 | } 172 | 173 | return inCombo ? nextCombo : WAR.HeavySwing; 174 | } 175 | } 176 | 177 | internal class WarriorMythrilTempestCombo: CustomCombo { 178 | public override CustomComboPreset Preset => CustomComboPreset.WarriorMythrilTempestCombo; 179 | 180 | public override uint[] ActionIDs { get; } = [WAR.MythrilTempest]; 181 | 182 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 183 | 184 | if (IsEnabled(CustomComboPreset.WarriorSmartWeaveAOE)) { 185 | if (level >= WAR.Levels.Orogeny) { 186 | if (SelfHasEffect(WAR.Buffs.SurgingTempest) || !IsEnabled(CustomComboPreset.WarriorSmartWeaveAOEOnlyBuffed)) { 187 | if (CanWeave(actionID) && CanUse(WAR.Orogeny)) 188 | return WAR.Orogeny; 189 | } 190 | } 191 | } 192 | 193 | if (IsEnabled(CustomComboPreset.WarriorInnerReleaseFeature)) { 194 | if (SelfHasEffect(WAR.Buffs.InnerRelease)) 195 | return OriginalHook(WAR.Decimate); 196 | } 197 | 198 | bool inCombo = false; 199 | uint nextCombo = 0; 200 | if (lastComboMove is WAR.Overpower && level >= WAR.Levels.MythrilTempest) { 201 | inCombo = true; 202 | nextCombo = WAR.MythrilTempest; 203 | } 204 | 205 | if (IsEnabled(CustomComboPreset.WarriorGaugeOvercapTempestFeature)) { 206 | if (level >= WAR.Levels.MythrilTempestTrait) { 207 | if (GetJobGauge().BeastGauge > (inCombo ? 90 : 80)) 208 | return OriginalHook(WAR.Decimate); 209 | } 210 | } 211 | 212 | return inCombo ? nextCombo : WAR.Overpower; 213 | } 214 | } 215 | 216 | internal class WarriorNascentFlashFeature: CustomCombo { 217 | public override CustomComboPreset Preset => CustomComboPreset.WarriorNascentFlashFeature; 218 | public override uint[] ActionIDs { get; } = [WAR.NascentFlash]; 219 | 220 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 221 | 222 | if (level < WAR.Levels.NascentFlash) 223 | return WAR.RawIntuition; 224 | 225 | return actionID; 226 | } 227 | } 228 | 229 | internal class WarriorFellCleaveDecimate: CustomCombo { 230 | public override CustomComboPreset Preset { get; } = CustomComboPreset.WarAny; 231 | public override uint[] ActionIDs { get; } = [WAR.InnerBeast, WAR.FellCleave, WAR.SteelCyclone, WAR.Decimate]; 232 | 233 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 234 | 235 | if (IsEnabled(CustomComboPreset.WarriorPrimalBeastFeature)) { 236 | if (level >= WAR.Levels.PrimalRend && SelfHasEffect(WAR.Buffs.PrimalRendReady)) 237 | return WAR.PrimalRend; 238 | } 239 | 240 | uint normal = OriginalHook(actionID); 241 | 242 | if (IsEnabled(CustomComboPreset.WarriorInfuriateBeastFeature) && level >= WAR.Levels.Infuriate) { 243 | WARGauge gauge = GetJobGauge(); 244 | int threshold = IsEnabled(CustomComboPreset.WarriorInfuriateBeastRaidModeFeature) 245 | ? 60 246 | : 50; 247 | 248 | if (gauge.BeastGauge >= 50 && normal is WAR.InnerChaos or WAR.ChaoticCyclone) 249 | return normal; 250 | 251 | if (gauge.BeastGauge < threshold && !SelfHasEffect(WAR.Buffs.InnerRelease)) 252 | return WAR.Infuriate; 253 | } 254 | 255 | return normal; 256 | } 257 | } 258 | 259 | internal class WarriorBerserkInnerRelease: CustomCombo { 260 | public override CustomComboPreset Preset { get; } = CustomComboPreset.WarriorPrimalReleaseFeature; 261 | public override uint[] ActionIDs { get; } = [WAR.Berserk, WAR.InnerRelease]; 262 | 263 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 264 | 265 | if (level >= WAR.Levels.PrimalRend && (SelfHasEffect(WAR.Buffs.PrimalRendReady) || SelfHasEffect(WAR.Buffs.PrimalRuinationReady))) 266 | return OriginalHook(WAR.PrimalRend); 267 | 268 | return actionID; 269 | } 270 | } 271 | 272 | internal class WarriorBloodwhetting: CustomCombo { 273 | public override CustomComboPreset Preset { get; } = CustomComboPreset.WarriorHealthyBalancedDietFeature; 274 | public override uint[] ActionIDs { get; } = [WAR.Bloodwhetting, WAR.RawIntuition]; 275 | 276 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 277 | uint rawBlood = OriginalHook(WAR.RawIntuition); 278 | 279 | if (level >= WAR.Levels.RawIntuition && IsOffCooldown(rawBlood)) 280 | return rawBlood; 281 | 282 | if (level >= WAR.Levels.ThrillOfBattle && IsOffCooldown(WAR.ThrillOfBattle)) 283 | return WAR.ThrillOfBattle; 284 | 285 | if (level >= WAR.Levels.Equilibrium && IsOffCooldown(WAR.Equilibrium)) 286 | return WAR.Equilibrium; 287 | 288 | return actionID; 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/SAM.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Enums; 2 | using Dalamud.Game.ClientState.JobGauge.Types; 3 | 4 | using VariableVixen.XIVComboVX; 5 | 6 | namespace VariableVixen.XIVComboVX.Combos; 7 | 8 | internal static class SAM { 9 | public const byte JobID = 34; 10 | 11 | public const uint 12 | // Single target 13 | Hakaze = 7477, 14 | Jinpu = 7478, 15 | Shifu = 7479, 16 | Yukikaze = 7480, 17 | Gekko = 7481, 18 | Kasha = 7482, 19 | // AoE 20 | Fuga = 7483, 21 | Mangetsu = 7484, 22 | Oka = 7485, 23 | Fuko = 25780, 24 | // Iaijutsu and Tsubame 25 | Iaijutsu = 7867, 26 | TsubameGaeshi = 16483, 27 | KaeshiHiganbana = 16484, 28 | Shoha = 16487, 29 | // Misc 30 | HissatsuShinten = 7490, 31 | HissatsuKyuten = 7491, 32 | HissatsuSenei = 16481, 33 | HissatsuGuren = 7496, 34 | Ikishoten = 16482, 35 | OgiNamikiri = 25781, 36 | KaeshiNamikiri = 25782, 37 | Gyofu = 36963, 38 | Zanshin = 36964; 39 | 40 | public static class Buffs { 41 | public const ushort 42 | MeikyoShisui = 1233, 43 | EyesOpen = 1252, 44 | Jinpu = 1298, 45 | Shifu = 1299, 46 | OgiNamikiriReady = 2959, 47 | ZanshinReady = 3855; 48 | } 49 | 50 | public static class Debuffs { 51 | // public const ushort placeholder = 0; 52 | } 53 | 54 | public static class Levels { 55 | public const byte 56 | Jinpu = 4, 57 | Shifu = 18, 58 | Gekko = 30, 59 | Mangetsu = 35, 60 | Kasha = 40, 61 | Oka = 45, 62 | Yukikaze = 50, 63 | MeikyoShisui = 50, 64 | HissatsuKyuten = 62, 65 | HissatsuGuren = 70, 66 | HissatsuSenei = 72, 67 | TsubameGaeshi = 76, 68 | Shoha = 80, 69 | Hyosetsu = 86, 70 | Fuko = 86, 71 | KaeshiNamikiri = 90, 72 | OgiNamikiri = 90, 73 | Bloodbath = 12, 74 | Gyofu = 92, 75 | Zanshin = 96; 76 | } 77 | } 78 | 79 | // returning Soon™ (when we have the time to go over everything) 80 | 81 | internal class SamuraiBloodbathReplacer: SecondBloodbathCombo { 82 | public override CustomComboPreset Preset => CustomComboPreset.SamuraiBloodbathReplacer; 83 | } 84 | 85 | internal class SamuraiYukikazeCombo: CustomCombo { 86 | public override CustomComboPreset Preset => CustomComboPreset.SamuraiYukikazeCombo; 87 | public override uint[] ActionIDs { get; } = [SAM.Yukikaze]; 88 | 89 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 90 | bool meikyo = SelfHasEffect(SAM.Buffs.MeikyoShisui); 91 | 92 | if (level >= SAM.Levels.MeikyoShisui && meikyo || lastComboMove is SAM.Hakaze or SAM.Gyofu && level >= SAM.Levels.Yukikaze) 93 | return SAM.Yukikaze; 94 | 95 | return OriginalHook(SAM.Hakaze); 96 | } 97 | } 98 | 99 | internal class SamuraiGekkoCombo: CustomCombo { 100 | public override CustomComboPreset Preset => CustomComboPreset.SamuraiGekkoCombo; 101 | public override uint[] ActionIDs { get; } = [SAM.Gekko]; 102 | 103 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 104 | bool meikyo = SelfHasEffect(SAM.Buffs.MeikyoShisui); 105 | 106 | if (level >= SAM.Levels.MeikyoShisui && meikyo) 107 | return SAM.Gekko; 108 | 109 | if (!meikyo) { 110 | 111 | if (lastComboMove is SAM.Hakaze or SAM.Gyofu && level >= SAM.Levels.Jinpu) 112 | return SAM.Jinpu; 113 | 114 | if (lastComboMove is SAM.Jinpu && level >= SAM.Levels.Gekko) 115 | return SAM.Gekko; 116 | 117 | } 118 | 119 | return IsEnabled(CustomComboPreset.SamuraiGekkoOption) 120 | ? SAM.Jinpu 121 | : OriginalHook(SAM.Hakaze); 122 | } 123 | } 124 | 125 | internal class SamuraiKashaCombo: CustomCombo { 126 | public override CustomComboPreset Preset => CustomComboPreset.SamuraiKashaCombo; 127 | public override uint[] ActionIDs { get; } = [SAM.Kasha]; 128 | 129 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 130 | bool meikyo = SelfHasEffect(SAM.Buffs.MeikyoShisui); 131 | 132 | if (meikyo && SelfHasEffect(SAM.Buffs.MeikyoShisui)) 133 | return SAM.Kasha; 134 | 135 | if (!meikyo) { 136 | if (lastComboMove is SAM.Hakaze or SAM.Gyofu && level >= SAM.Levels.Shifu) 137 | return SAM.Shifu; 138 | 139 | if (lastComboMove == SAM.Shifu && level >= SAM.Levels.Kasha) 140 | return SAM.Kasha; 141 | } 142 | 143 | return IsEnabled(CustomComboPreset.SamuraiKashaOption) 144 | ? SAM.Shifu 145 | : OriginalHook(SAM.Hakaze); 146 | } 147 | } 148 | 149 | internal class SamuraiMangetsuCombo: CustomCombo { 150 | public override CustomComboPreset Preset => CustomComboPreset.SamuraiMangetsuCombo; 151 | public override uint[] ActionIDs { get; } = [SAM.Mangetsu]; 152 | 153 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 154 | bool meikyo = SelfHasEffect(SAM.Buffs.MeikyoShisui); 155 | 156 | if (level >= SAM.Levels.MeikyoShisui && meikyo || lastComboMove is SAM.Fuga or SAM.Fuko && level >= SAM.Levels.Mangetsu) 157 | return SAM.Mangetsu; 158 | 159 | return OriginalHook(SAM.Fuga); 160 | } 161 | } 162 | 163 | internal class SamuraiOkaCombo: CustomCombo { 164 | public override CustomComboPreset Preset => CustomComboPreset.SamuraiOkaCombo; 165 | public override uint[] ActionIDs { get; } = [SAM.Oka]; 166 | 167 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 168 | bool meikyo = SelfHasEffect(SAM.Buffs.MeikyoShisui); 169 | 170 | if (level >= SAM.Levels.MeikyoShisui && meikyo || lastComboMove is SAM.Fuga or SAM.Fuko && level >= SAM.Levels.Oka) 171 | return SAM.Oka; 172 | 173 | return OriginalHook(SAM.Fuga); 174 | } 175 | } 176 | 177 | internal class SamuraiTsubameGaeshiFeature: CustomCombo { 178 | public override CustomComboPreset Preset => CustomComboPreset.SamAny; 179 | public override uint[] ActionIDs { get; } = [SAM.TsubameGaeshi]; 180 | 181 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 182 | SAMGauge gauge = GetJobGauge(); 183 | 184 | if (level >= SAM.Levels.Shoha && IsEnabled(CustomComboPreset.SamuraiTsubameGaeshiShohaFeature) && gauge.MeditationStacks >= 3) 185 | return SAM.Shoha; 186 | 187 | if (IsEnabled(CustomComboPreset.SamuraiTsubameGaeshiIaijutsuFeature)) { 188 | if (level >= SAM.Levels.TsubameGaeshi && gauge.Sen == Sen.None) 189 | return OriginalHook(SAM.TsubameGaeshi); 190 | 191 | return OriginalHook(SAM.Iaijutsu); 192 | } 193 | 194 | return actionID; 195 | } 196 | } 197 | 198 | internal class SamuraiIaijutsuFeature: CustomCombo { 199 | public override CustomComboPreset Preset => CustomComboPreset.SamAny; 200 | public override uint[] ActionIDs { get; } = [SAM.Iaijutsu]; 201 | 202 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 203 | SAMGauge gauge = GetJobGauge(); 204 | 205 | if (level >= SAM.Levels.Shoha && IsEnabled(CustomComboPreset.SamuraiIaijutsuShohaFeature) && gauge.MeditationStacks >= 3) 206 | return SAM.Shoha; 207 | 208 | if (IsEnabled(CustomComboPreset.SamuraiIaijutsuTsubameGaeshiFeature)) { 209 | if (level >= SAM.Levels.TsubameGaeshi && gauge.Sen == Sen.None) 210 | return OriginalHook(SAM.TsubameGaeshi); 211 | 212 | return OriginalHook(SAM.Iaijutsu); 213 | } 214 | 215 | return actionID; 216 | } 217 | } 218 | 219 | internal class SamuraiShinten: CustomCombo { 220 | public override CustomComboPreset Preset { get; } = CustomComboPreset.SamAny; 221 | public override uint[] ActionIDs { get; } = [SAM.HissatsuShinten]; 222 | 223 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 224 | 225 | if (IsEnabled(CustomComboPreset.SamuraiShintenShohaFeature) && level >= SAM.Levels.Shoha && GetJobGauge().MeditationStacks >= 3) 226 | return SAM.Shoha; 227 | 228 | if (IsEnabled(CustomComboPreset.SamuraiShintenSeneiFeature)) { 229 | 230 | if (level >= SAM.Levels.HissatsuSenei && IsOffCooldown(SAM.HissatsuSenei)) 231 | return SAM.HissatsuSenei; 232 | 233 | if (IsEnabled(CustomComboPreset.SamuraiGurenSeneiLevelSyncFeature)) { 234 | if (level is >= SAM.Levels.HissatsuGuren and < SAM.Levels.HissatsuSenei && IsOffCooldown(SAM.HissatsuGuren)) 235 | return SAM.HissatsuGuren; 236 | } 237 | 238 | } 239 | 240 | return actionID; 241 | } 242 | } 243 | 244 | internal class SamuraiSenei: CustomCombo { 245 | public override CustomComboPreset Preset { get; } = CustomComboPreset.SamAny; 246 | public override uint[] ActionIDs { get; } = [SAM.HissatsuSenei]; 247 | 248 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 249 | 250 | if (IsEnabled(CustomComboPreset.SamuraiGurenSeneiLevelSyncFeature)) { 251 | if (level is >= SAM.Levels.HissatsuGuren and < SAM.Levels.HissatsuSenei) 252 | return SAM.HissatsuGuren; 253 | } 254 | 255 | return actionID; 256 | } 257 | } 258 | 259 | internal class SamuraiKyuten: CustomCombo { 260 | public override CustomComboPreset Preset { get; } = CustomComboPreset.SamAny; 261 | public override uint[] ActionIDs { get; } = [SAM.HissatsuKyuten]; 262 | 263 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 264 | 265 | if (IsEnabled(CustomComboPreset.SamuraiKyutenShohaFeature) && level >= SAM.Levels.Shoha && GetJobGauge().MeditationStacks >= 3) 266 | return SAM.Shoha; 267 | 268 | if (IsEnabled(CustomComboPreset.SamuraiKyutenGurenFeature) 269 | && level >= SAM.Levels.HissatsuGuren 270 | && IsOffCooldown(SAM.HissatsuGuren) 271 | ) { 272 | return SAM.HissatsuGuren; 273 | } 274 | 275 | return actionID; 276 | } 277 | } 278 | 279 | internal class SamuraiIkishotenNamikiriFeature: CustomCombo { 280 | public override CustomComboPreset Preset { get; } = CustomComboPreset.SamuraiIkishotenNamikiriFeature; 281 | 282 | public override uint[] ActionIDs { get; } = [SAM.Ikishoten]; 283 | 284 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 285 | 286 | if (level >= SAM.Levels.OgiNamikiri) { 287 | SAMGauge gauge = GetJobGauge(); 288 | 289 | if (level >= SAM.Levels.Shoha && gauge.MeditationStacks >= 3) 290 | return SAM.Shoha; 291 | 292 | if (gauge.Kaeshi == Kaeshi.Namikiri) 293 | return SAM.KaeshiNamikiri; 294 | 295 | if (SelfHasEffect(SAM.Buffs.OgiNamikiriReady)) 296 | return SAM.OgiNamikiri; 297 | } 298 | 299 | return actionID; 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/DRG.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | 3 | using VariableVixen.XIVComboVX; 4 | 5 | namespace VariableVixen.XIVComboVX.Combos; 6 | 7 | internal static class DRG { 8 | public const byte JobID = 22; 9 | 10 | public const uint 11 | // Single Target 12 | TrueThrust = 75, 13 | VorpalThrust = 78, 14 | Disembowel = 87, 15 | FullThrust = 84, 16 | ChaosThrust = 88, 17 | HeavensThrust = 25771, 18 | ChaoticSpring = 25772, 19 | WheelingThrust = 3556, 20 | FangAndClaw = 3554, 21 | RaidenThrust = 16479, 22 | // AoE 23 | DoomSpike = 86, 24 | SonicThrust = 7397, 25 | CoerthanTorment = 16477, 26 | DraconianFury = 25770, 27 | // Combined 28 | Geirskogul = 3555, 29 | Nastrond = 7400, 30 | // Jumps 31 | Jump = 92, 32 | SpineshatterDive = 95, 33 | DragonfireDive = 96, 34 | HighJump = 16478, 35 | MirageDive = 7399, 36 | // Dragon 37 | Stardiver = 16480, 38 | WyrmwindThrust = 25773; 39 | 40 | 41 | public static class Buffs { 42 | public const ushort 43 | FangAndClawBared = 802, 44 | WheelInMotion = 803, 45 | DiveReady = 1243, 46 | DraconianFire = 1863, 47 | PowerSurge = 2720; 48 | } 49 | 50 | public static class Debuffs { 51 | public const ushort 52 | ChaosThrust = 1312; // either 1312 or 118, but I don't know which yet 53 | } 54 | 55 | public static class Levels { 56 | public const byte 57 | VorpalThrust = 4, 58 | Disembowel = 18, 59 | FullThrust = 26, 60 | SpineshatterDive = 45, 61 | DragonfireDive = 50, 62 | ChaosThrust = 50, 63 | FangAndClaw = 56, 64 | WheelingThrust = 58, 65 | Geirskogul = 60, 66 | SonicThrust = 62, 67 | MirageDive = 68, 68 | LifeOfTheDragon = 70, 69 | CoerthanTorment = 72, 70 | HighJump = 74, 71 | RaidenThrust = 76, 72 | Stardiver = 80, 73 | HeavensThrust = 86, 74 | ChaoticSpring = 86; 75 | } 76 | } 77 | 78 | internal class DragoonBloodbathReplacer: SecondBloodbathCombo { 79 | public override CustomComboPreset Preset => CustomComboPreset.DragoonBloodbathReplacer; 80 | } 81 | 82 | /* returning Soon™ (when we have the time to go over everything) 83 | 84 | internal class DragoonCoerthanTorment: CustomCombo { 85 | public override CustomComboPreset Preset => CustomComboPreset.DrgAny; 86 | public override uint[] ActionIDs { get; } = [DRG.CoerthanTorment]; 87 | 88 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 89 | 90 | if (IsEnabled(CustomComboPreset.DragoonCoerthanWyrmwindFeature)) { 91 | if (GetJobGauge().FirstmindsFocusCount == 2) 92 | return DRG.WyrmwindThrust; 93 | } 94 | 95 | if (IsEnabled(CustomComboPreset.DragoonCoerthanTormentCombo)) { 96 | 97 | if (comboTime > 0) { 98 | 99 | if (level >= DRG.Levels.SonicThrust && lastComboMove is DRG.DoomSpike or DRG.DraconianFury) 100 | return DRG.SonicThrust; 101 | 102 | if (level >= DRG.Levels.CoerthanTorment && lastComboMove is DRG.SonicThrust) 103 | return DRG.CoerthanTorment; 104 | } 105 | 106 | return OriginalHook(DRG.DoomSpike); 107 | } 108 | 109 | return actionID; 110 | } 111 | } 112 | 113 | internal class DragoonTotalThrust: CustomCombo { 114 | public override CustomComboPreset Preset => CustomComboPreset.DragoonTotalThrustCombo; 115 | public override uint[] ActionIDs => [DRG.TrueThrust]; 116 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 117 | 118 | // if currently in skill chain, complete that first 119 | if (comboTime > 0) { 120 | 121 | // True/Raiden Thrust -> [Vorpal Thrust -> Full/Heavens' Thrust] 122 | if (lastComboActionId is DRG.VorpalThrust && level >= DRG.Levels.FullThrust) 123 | return OriginalHook(DRG.FullThrust); 124 | 125 | // True/Raiden Thrust -> [Disembowel -> Chaos Thrust/Spring] 126 | if (lastComboActionId is DRG.Disembowel && level >= DRG.Levels.ChaosThrust) 127 | return OriginalHook(DRG.ChaosThrust); 128 | 129 | if (lastComboActionId is DRG.TrueThrust or DRG.RaidenThrust) { 130 | 131 | // if Chaos Thrust chain's self-buff OR target-dot is not up-with-minimum-time-left, start Chaos Thrust chain 132 | if (IsEnabled(CustomComboPreset.DragoonTotalThrustBuffSaver) && level >= DRG.Levels.Disembowel) { 133 | if (SelfEffectDuration(DRG.Buffs.PowerSurge) <= Service.Configuration.DragoonPowerSurgeBuffSaverBuffTime) 134 | return DRG.Disembowel; 135 | } 136 | if (IsEnabled(CustomComboPreset.DragoonTotalThrustDotSaver) && level >= DRG.Levels.ChaosThrust) { 137 | if (TargetOwnEffectDuration(DRG.Debuffs.ChaosThrust) <= Service.Configuration.DragoonChaosDotSaverDebuffTime) 138 | return DRG.Disembowel; 139 | } 140 | 141 | // start Full Thrust chain 142 | if (level >= DRG.Levels.VorpalThrust) 143 | return DRG.VorpalThrust; 144 | } 145 | } 146 | 147 | // if able to use Fang and Claw, Wheeling Thrust, or Raiden Thrust, do that 148 | if (level >= DRG.Levels.FangAndClaw && SelfHasEffect(DRG.Buffs.FangAndClawBared)) 149 | return DRG.FangAndClaw; 150 | if (level >= DRG.Levels.WheelingThrust && SelfHasEffect(DRG.Buffs.WheelInMotion)) 151 | return DRG.WheelingThrust; 152 | if (level >= DRG.Levels.RaidenThrust && SelfHasEffect(DRG.Buffs.DraconianFire)) 153 | return DRG.RaidenThrust; 154 | 155 | // fall back to True Thrust, or Vorpal if enabled 156 | return IsEnabled(CustomComboPreset.DragoonTotalThrustVorpalSkipFirst) && level >= DRG.Levels.VorpalThrust 157 | ? DRG.VorpalThrust 158 | : DRG.TrueThrust; 159 | } 160 | } 161 | 162 | internal class DragoonChaosThrust: CustomCombo { 163 | public override CustomComboPreset Preset => CustomComboPreset.DragoonChaosThrustCombo; 164 | public override uint[] ActionIDs { get; } = [DRG.ChaosThrust, DRG.ChaoticSpring]; 165 | 166 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 167 | 168 | if (level >= DRG.Levels.FangAndClaw && SelfHasEffect(DRG.Buffs.FangAndClawBared)) 169 | return DRG.FangAndClaw; 170 | 171 | if (level >= DRG.Levels.WheelingThrust && SelfHasEffect(DRG.Buffs.WheelInMotion)) 172 | return DRG.WheelingThrust; 173 | 174 | if (comboTime > 0) { 175 | 176 | if (lastComboMove is DRG.Disembowel && level >= DRG.Levels.ChaosThrust) 177 | return OriginalHook(DRG.ChaosThrust); 178 | 179 | if (lastComboMove is DRG.TrueThrust or DRG.RaidenThrust && level >= DRG.Levels.Disembowel) 180 | return DRG.Disembowel; 181 | 182 | } 183 | 184 | return IsEnabled(CustomComboPreset.DragoonChaosThrustLateOption) 185 | ? DRG.Disembowel 186 | : OriginalHook(DRG.TrueThrust); 187 | } 188 | } 189 | 190 | internal class DragoonFullThrustCombo: CustomCombo { 191 | public override CustomComboPreset Preset => CustomComboPreset.DragoonFullThrustCombo; 192 | public override uint[] ActionIDs { get; } = [DRG.FullThrust, DRG.HeavensThrust]; 193 | 194 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 195 | 196 | if (level >= DRG.Levels.WheelingThrust && SelfHasEffect(DRG.Buffs.WheelInMotion)) 197 | return DRG.WheelingThrust; 198 | 199 | if (level >= DRG.Levels.FangAndClaw && SelfHasEffect(DRG.Buffs.FangAndClawBared)) 200 | return DRG.FangAndClaw; 201 | 202 | if (comboTime > 0) { 203 | 204 | if (lastComboMove is DRG.TrueThrust or DRG.RaidenThrust) { 205 | if (IsEnabled(CustomComboPreset.DragoonFullThrustBuffSaver) && level >= DRG.Levels.Disembowel) { 206 | if (SelfEffectDuration(DRG.Buffs.PowerSurge) < Service.Configuration.DragoonPowerSurgeBuffSaverBuffTime) 207 | return DRG.Disembowel; 208 | } 209 | if (IsEnabled(CustomComboPreset.DragoonFullThrustDotSaver) && level >= DRG.Levels.ChaosThrust) { 210 | if (TargetOwnEffectDuration(DRG.Debuffs.ChaosThrust) < Service.Configuration.DragoonChaosDotSaverDebuffTime) 211 | return DRG.Disembowel; 212 | } 213 | if (level >= DRG.Levels.VorpalThrust) 214 | return DRG.VorpalThrust; 215 | } 216 | 217 | if (lastComboMove is DRG.VorpalThrust) { 218 | if (level >= DRG.Levels.FullThrust) 219 | return OriginalHook(DRG.FullThrust); 220 | } 221 | } 222 | 223 | return IsEnabled(CustomComboPreset.DragoonFullThrustLateOption) 224 | ? DRG.VorpalThrust 225 | : OriginalHook(DRG.TrueThrust); 226 | } 227 | } 228 | 229 | internal class DragoonStardiver: CustomCombo { 230 | public override CustomComboPreset Preset { get; } = CustomComboPreset.DrgAny; 231 | public override uint[] ActionIDs { get; } = [DRG.Stardiver]; 232 | 233 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 234 | DRGGauge gauge = GetJobGauge(); 235 | 236 | if (IsEnabled(CustomComboPreset.DragoonStardiverNastrondFeature)) { 237 | if (level >= DRG.Levels.Geirskogul && (!gauge.IsLOTDActive || IsOffCooldown(DRG.Nastrond) || IsOnCooldown(DRG.Stardiver))) 238 | return OriginalHook(DRG.Geirskogul); 239 | } 240 | 241 | if (IsEnabled(CustomComboPreset.DragoonStardiverDragonfireDiveFeature)) { 242 | if (level < DRG.Levels.Stardiver || !gauge.IsLOTDActive || IsOnCooldown(DRG.Stardiver) || (IsOffCooldown(DRG.DragonfireDive) && gauge.LOTDTimer > 7.5)) 243 | return DRG.DragonfireDive; 244 | } 245 | 246 | return actionID; 247 | } 248 | } 249 | 250 | internal class DragoonDiveFeature: CustomCombo { 251 | public override CustomComboPreset Preset { get; } = CustomComboPreset.DragoonDiveFeature; 252 | public override uint[] ActionIDs { get; } = [DRG.SpineshatterDive, DRG.DragonfireDive, DRG.Stardiver]; 253 | 254 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 255 | 256 | if (level >= DRG.Levels.Stardiver && GetJobGauge().IsLOTDActive) 257 | return PickByCooldown(actionID, DRG.SpineshatterDive, DRG.DragonfireDive, DRG.Stardiver); 258 | 259 | if (level >= DRG.Levels.DragonfireDive) 260 | return PickByCooldown(actionID, DRG.SpineshatterDive, DRG.DragonfireDive); 261 | 262 | return DRG.SpineshatterDive; 263 | } 264 | } 265 | 266 | internal class DragoonMirageJumpFeature: CustomCombo { 267 | public override CustomComboPreset Preset { get; } = CustomComboPreset.DragoonMirageJumpFeature; 268 | public override uint[] ActionIDs { get; } = [DRG.Jump, DRG.HighJump]; 269 | 270 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 271 | 272 | if (level >= DRG.Levels.MirageDive && SelfHasEffect(DRG.Buffs.DiveReady)) 273 | return DRG.MirageDive; 274 | 275 | return actionID; 276 | } 277 | } 278 | */ 279 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/BLM.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | 3 | namespace VariableVixen.XIVComboVX.Combos; 4 | 5 | internal static class BLM { 6 | public const byte JobID = 25; 7 | 8 | public const uint 9 | Fire = 141, 10 | Blizzard = 142, 11 | Thunder = 144, 12 | Fire2 = 147, 13 | Transpose = 149, 14 | Fire3 = 152, 15 | Thunder3 = 153, 16 | Blizzard3 = 154, 17 | Scathe = 156, 18 | Freeze = 159, 19 | Flare = 162, 20 | LeyLines = 3573, 21 | Blizzard4 = 3576, 22 | Fire4 = 3577, 23 | BetweenTheLines = 7419, 24 | Despair = 16505, 25 | UmbralSoul = 16506, 26 | Xenoglossy = 16507, 27 | Blizzard2 = 25793, 28 | HighFire2 = 25794, 29 | HighBlizzard2 = 25795, 30 | Paradox = 25797, 31 | HighThunder = 36986, 32 | HighThunder2 = 36987, 33 | Retrace = 36988, 34 | FlareStar = 36989; 35 | 36 | public static class Buffs { 37 | public const ushort 38 | Thundercloud = 164, 39 | Firestarter = 165, 40 | LeyLines = 737, 41 | EnhancedFlare = 2960; 42 | } 43 | 44 | public static class Debuffs { 45 | public const ushort 46 | Thunder = 161, 47 | Thunder3 = 163, 48 | HighThunder = 3871, 49 | Highthunder2 = 3872; 50 | } 51 | 52 | public static class Levels { 53 | public const byte 54 | Blizzard = 1, 55 | Fire = 2, 56 | Transpose = 4, 57 | Thunder = 6, 58 | Blizzard2 = 12, 59 | Scathe = 15, 60 | Fire2 = 18, 61 | Thunder2 = 26, 62 | Manaward = 30, 63 | Manafont = 30, 64 | Fire3 = 35, 65 | Blizzard3 = 35, 66 | UmbralSoul = 35, 67 | Freeze = 40, 68 | Thunder3 = 45, 69 | AetherialManipulation = 50, 70 | Flare = 50, 71 | LeyLines = 52, 72 | Blizzard4 = 58, 73 | Fire4 = 60, 74 | BetweenTheLines = 62, 75 | Thunder4 = 64, 76 | Triplecast = 66, 77 | Foul = 70, 78 | Despair = 72, 79 | Xenoglossy = 80, 80 | HighFire2 = 82, 81 | HighBlizzard2 = 82, 82 | Amplifier = 86, 83 | Paradox = 90, 84 | HighThunder = 92, 85 | HighTunder2 = 92, 86 | Retrace = 96, 87 | FlareStar = 100; 88 | } 89 | } 90 | 91 | /* returning Soon™ (when we have the time to go over everything) 92 | 93 | internal class BlackFireBlizzard4: CustomCombo { 94 | public override CustomComboPreset Preset { get; } = CustomComboPreset.BlmAny; 95 | public override uint[] ActionIDs => [BLM.Fire4, BLM.Blizzard4]; 96 | 97 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 98 | BLMGauge gauge = GetJobGauge(); 99 | 100 | if (IsEnabled(CustomComboPreset.BlackUmbralSoulFeature)) { 101 | if (actionID is BLM.Blizzard4 && level >= BLM.Levels.UmbralSoul && gauge.InUmbralIce && !HasTarget) 102 | return BLM.UmbralSoul; 103 | } 104 | 105 | if (actionID is BLM.Fire4 or BLM.Blizzard4) { 106 | 107 | if (IsEnabled(CustomComboPreset.BlackEnochianFeature)) { 108 | 109 | if (gauge.InAstralFire) { 110 | 111 | if (IsEnabled(CustomComboPreset.BlackEnochianDespairFeature) && level >= BLM.Levels.Despair && LocalPlayer?.CurrentMp < 2400) 112 | return BLM.Despair; 113 | 114 | if (IsEnabled(CustomComboPreset.BlackEnochianNoSyncFeature) || level >= BLM.Levels.Fire4) 115 | return BLM.Fire4; 116 | 117 | return BLM.Fire; 118 | } 119 | 120 | if (gauge.InUmbralIce) { 121 | 122 | if (IsEnabled(CustomComboPreset.BlackEnochianNoSyncFeature) || level >= BLM.Levels.Blizzard4) 123 | return BLM.Blizzard4; 124 | 125 | return BLM.Blizzard; 126 | } 127 | } 128 | } 129 | 130 | return actionID; 131 | } 132 | } 133 | 134 | internal class BlackTranspose: CustomCombo { 135 | public override CustomComboPreset Preset { get; } = CustomComboPreset.BlackManaFeature; 136 | public override uint[] ActionIDs => [BLM.Transpose]; 137 | 138 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 139 | BLMGauge gauge = GetJobGauge(); 140 | 141 | if (level >= BLM.Levels.UmbralSoul && gauge.IsEnochianActive && gauge.InUmbralIce) 142 | return BLM.UmbralSoul; 143 | 144 | return actionID; 145 | } 146 | } 147 | 148 | internal class BlackLeyLines: CustomCombo { 149 | public override CustomComboPreset Preset { get; } = CustomComboPreset.BlackLeyLinesFeature; 150 | public override uint[] ActionIDs => [BLM.LeyLines]; 151 | 152 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 153 | 154 | if (level >= BLM.Levels.BetweenTheLines && SelfHasEffect(BLM.Buffs.LeyLines)) 155 | return BLM.BetweenTheLines; 156 | 157 | return actionID; 158 | } 159 | } 160 | 161 | internal class BlackFireFeature: CustomCombo { 162 | public override CustomComboPreset Preset { get; } = CustomComboPreset.BlmAny; 163 | public override uint[] ActionIDs { get; } = [BLM.Fire]; 164 | 165 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 166 | BLMGauge gauge = GetJobGauge(); 167 | 168 | if (level >= BLM.Levels.Paradox && gauge.IsParadoxActive && (gauge.InUmbralIce || LocalPlayer?.CurrentMp >= 1600)) 169 | return BLM.Paradox; 170 | 171 | if (level >= BLM.Levels.Fire3 172 | && ( 173 | (IsEnabled(CustomComboPreset.BlackFireAstralFeature) && gauge.AstralFireStacks <= 1) 174 | || (IsEnabled(CustomComboPreset.BlackFireProcFeature) && SelfHasEffect(BLM.Buffs.Firestarter)) 175 | ) 176 | ) { 177 | return BLM.Fire3; 178 | } 179 | 180 | return OriginalHook(BLM.Fire); 181 | } 182 | } 183 | 184 | internal class BlackBlizzard: CustomCombo { 185 | public override CustomComboPreset Preset { get; } = CustomComboPreset.BlmAny; 186 | public override uint[] ActionIDs => [BLM.Blizzard, BLM.Blizzard3]; 187 | 188 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 189 | BLMGauge gauge = GetJobGauge(); 190 | 191 | if (IsEnabled(CustomComboPreset.BlackUmbralSoulFeature)) { 192 | if (level >= BLM.Levels.UmbralSoul && gauge.InUmbralIce && !HasTarget) 193 | return BLM.UmbralSoul; 194 | } 195 | 196 | if (IsEnabled(CustomComboPreset.BlackBlizzardFeature)) { 197 | 198 | if (level >= BLM.Levels.Paradox && gauge.IsParadoxActive && (gauge.InUmbralIce || LocalPlayer?.CurrentMp >= 1600)) 199 | return BLM.Paradox; 200 | 201 | if (level >= BLM.Levels.Blizzard3) 202 | return BLM.Blizzard3; 203 | 204 | return BLM.Blizzard; 205 | } 206 | 207 | return actionID; 208 | } 209 | } 210 | 211 | internal class BlackFreezeFlare: CustomCombo { 212 | public override CustomComboPreset Preset { get; } = CustomComboPreset.BlmAny; 213 | public override uint[] ActionIDs => [BLM.Freeze, BLM.Flare]; 214 | 215 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 216 | BLMGauge gauge = GetJobGauge(); 217 | 218 | if (actionID is BLM.Freeze) { 219 | if (IsEnabled(CustomComboPreset.BlackUmbralSoulFeature)) { 220 | if (level >= BLM.Levels.UmbralSoul && gauge.InUmbralIce && !HasTarget) 221 | return BLM.UmbralSoul; 222 | } 223 | } 224 | 225 | if (IsEnabled(CustomComboPreset.BlackFreezeFlareFeature)) { 226 | if (level >= BLM.Levels.Freeze && gauge.InUmbralIce) 227 | return BLM.Freeze; 228 | 229 | if (level >= BLM.Levels.Flare && gauge.InAstralFire) 230 | return BLM.Flare; 231 | } 232 | 233 | return actionID; 234 | } 235 | } 236 | 237 | internal class BlackFire2: CustomCombo { 238 | public override CustomComboPreset Preset { get; } = CustomComboPreset.BlackFire2Feature; 239 | public override uint[] ActionIDs => [BLM.Fire2, BLM.HighFire2]; 240 | 241 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 242 | BLMGauge gauge = GetJobGauge(); 243 | 244 | if (IsEnabled(CustomComboPreset.BlackFireBlizzard2Option)) { 245 | if (gauge.AstralFireStacks < 3) 246 | return actionID; 247 | } 248 | 249 | if (level >= BLM.Levels.Flare 250 | && gauge.InAstralFire && 251 | (gauge.UmbralHearts == 1 || LocalPlayer?.CurrentMp < 3800 || SelfHasEffect(BLM.Buffs.EnhancedFlare)) 252 | ) { 253 | return BLM.Flare; 254 | } 255 | 256 | return actionID; 257 | } 258 | } 259 | 260 | internal class BlackBlizzard2: CustomCombo { 261 | public override CustomComboPreset Preset { get; } = CustomComboPreset.BlmAny; 262 | public override uint[] ActionIDs => [BLM.Blizzard2, BLM.HighBlizzard2]; 263 | 264 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 265 | BLMGauge gauge = GetJobGauge(); 266 | 267 | if (IsEnabled(CustomComboPreset.BlackUmbralSoulFeature)) { 268 | if (level >= BLM.Levels.UmbralSoul && gauge.InUmbralIce && !HasTarget) 269 | return BLM.UmbralSoul; 270 | } 271 | 272 | if (IsEnabled(CustomComboPreset.BlackBlizzard2Feature)) { 273 | if (IsEnabled(CustomComboPreset.BlackFireBlizzard2Option)) { 274 | if (gauge.UmbralIceStacks < 3) 275 | return actionID; 276 | } 277 | 278 | if (level >= BLM.Levels.Freeze && gauge.InUmbralIce) 279 | return BLM.Freeze; 280 | } 281 | 282 | return actionID; 283 | } 284 | } 285 | 286 | internal class BlackScathe: CustomCombo { 287 | public override CustomComboPreset Preset { get; } = CustomComboPreset.BlackScatheFeature; 288 | public override uint[] ActionIDs => [BLM.Scathe]; 289 | 290 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 291 | 292 | if (level >= BLM.Levels.Xenoglossy && GetJobGauge().PolyglotStacks > 0) 293 | return BLM.Xenoglossy; 294 | 295 | return actionID; 296 | } 297 | } 298 | 299 | 300 | 301 | internal class BlackFireToIce3: CustomCombo { 302 | public override CustomComboPreset Preset { get;} = CustomComboPreset.BlackFireToIce3; 303 | public override uint[] ActionIDs => [BLM.Blizzard3]; 304 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 305 | BLMGauge gauge = GetJobGauge(); 306 | 307 | if (gauge.InAstralFire && LocalPlayer.CurrentMp >= 1600 && level >= BLM.Blizzard3) 308 | return BLM.Blizzard; 309 | 310 | return BLM.Blizzard3; 311 | 312 | } 313 | } 314 | 315 | internal class BlackIceToFire3: CustomCombo { 316 | public override CustomComboPreset Preset { get; } = CustomComboPreset.BlackIceToFire3; 317 | public override uint[] ActionIDs => [BLM.Fire3]; 318 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 319 | BLMGauge gauge = GetJobGauge(); 320 | 321 | if (gauge.InUmbralIce && level >= BLM.Fire3) 322 | return BLM.Fire; 323 | 324 | return BLM.Fire3; 325 | 326 | } 327 | } 328 | 329 | */ 330 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/BRD.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Enums; 2 | using Dalamud.Game.ClientState.JobGauge.Types; 3 | using Dalamud.Game.ClientState.Statuses; 4 | 5 | namespace VariableVixen.XIVComboVX.Combos; 6 | 7 | internal static class BRD { 8 | public const byte JobID = 23; 9 | 10 | public const uint 11 | HeavyShot = 97, 12 | StraightShot = 98, 13 | VenomousBite = 100, 14 | RagingStrikes = 101, 15 | QuickNock = 106, 16 | Barrage = 107, 17 | Bloodletter = 110, 18 | Windbite = 113, 19 | RainOfDeath = 117, 20 | BattleVoice = 118, 21 | EmpyrealArrow = 3558, 22 | WanderersMinuet = 3559, 23 | IronJaws = 3560, 24 | Sidewinder = 3562, 25 | PitchPerfect = 7404, 26 | CausticBite = 7406, 27 | Stormbite = 7407, 28 | RefulgentArrow = 7409, 29 | BurstShot = 16495, 30 | ApexArrow = 16496, 31 | Shadowbite = 16494, 32 | Ladonsbite = 25783, 33 | BlastArrow = 25784, 34 | RadiantFinale = 25785; 35 | 36 | public static class Buffs { 37 | public const ushort 38 | Troubadour = 1934, 39 | Repertoire = 3137, 40 | WanderersMinuet = 2216, 41 | BlastShotReady = 2692, 42 | HawksEye = 3861; 43 | } 44 | 45 | public static class Debuffs { 46 | public const ushort 47 | VenomousBite = 124, 48 | Windbite = 129, 49 | CausticBite = 1200, 50 | Stormbite = 1201; 51 | } 52 | 53 | public static class Levels { 54 | public const byte 55 | StraightShot = 2, 56 | RagingStrikes = 4, 57 | VenomousBite = 6, 58 | Bloodletter = 12, 59 | Windbite = 30, 60 | RainOfDeath = 45, 61 | BattleVoice = 50, 62 | PitchPerfect = 52, 63 | EmpyrealArrow = 54, 64 | IronJaws = 56, 65 | Sidewinder = 60, 66 | BiteUpgrade = 64, 67 | RefulgentArrow = 70, 68 | Shadowbite = 72, 69 | BurstShot = 76, 70 | ApexArrow = 80, 71 | Ladonsbite = 82, 72 | BlastShot = 86, 73 | RadiantFinale = 90; 74 | } 75 | } 76 | 77 | internal class BardHeavyBurstShot: CustomCombo { 78 | public override CustomComboPreset Preset => CustomComboPreset.BrdAny; 79 | public override uint[] ActionIDs { get; } = [BRD.HeavyShot, BRD.BurstShot]; 80 | 81 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 82 | 83 | if (CanWeave(actionID)) { 84 | 85 | if (IsEnabled(CustomComboPreset.BardWeavePitchPerfect) && level >= BRD.Levels.PitchPerfect) { 86 | BRDGauge gauge = GetJobGauge(); 87 | 88 | if (gauge.Song is Song.Wanderer && gauge.Repertoire > 0) { 89 | IStatus? minuet = SelfFindEffect(BRD.Buffs.WanderersMinuet); 90 | 91 | if (gauge.SongTimer / 1000f <= Service.Configuration.BardWanderersMinuetBuffThreshold) 92 | return BRD.PitchPerfect; 93 | 94 | if (gauge.Repertoire == 3) 95 | return BRD.PitchPerfect; 96 | } 97 | } 98 | 99 | if (IsEnabled(CustomComboPreset.BardWeaveBattleVoice) && level >= BRD.Levels.BattleVoice) { 100 | if (CanUse(BRD.BattleVoice)) 101 | return BRD.BattleVoice; 102 | } 103 | 104 | if (IsEnabled(CustomComboPreset.BardWeaveRagingStrikes) && level >= BRD.Levels.RagingStrikes) { 105 | if (CanUse(BRD.RagingStrikes)) 106 | return BRD.RagingStrikes; 107 | } 108 | 109 | if (IsEnabled(CustomComboPreset.BardWeaveSidewinder) && level >= BRD.Levels.Sidewinder) { 110 | if (CanUse(BRD.Sidewinder)) 111 | return BRD.Sidewinder; 112 | } 113 | 114 | if (IsEnabled(CustomComboPreset.BardWeaveEmpyrealArrow) && level >= BRD.Levels.EmpyrealArrow) { 115 | if (CanUse(BRD.EmpyrealArrow)) 116 | return BRD.EmpyrealArrow; 117 | } 118 | 119 | if (IsEnabled(CustomComboPreset.BardWeaveBloodletter) && level >= BRD.Levels.Bloodletter) { 120 | if (CanUse(BRD.Bloodletter)) 121 | return BRD.Bloodletter; 122 | } 123 | 124 | if (IsEnabled(CustomComboPreset.BardWeaveDeathRain) && level >= BRD.Levels.RainOfDeath) { 125 | if (CanUse(BRD.RainOfDeath)) 126 | return BRD.RainOfDeath; 127 | } 128 | 129 | } 130 | 131 | if (IsEnabled(CustomComboPreset.BardStraightShotIronJaws) && level >= BRD.Levels.VenomousBite) { 132 | ushort poisonStatusId = level >= BRD.Levels.BiteUpgrade 133 | ? BRD.Debuffs.CausticBite 134 | : BRD.Debuffs.VenomousBite; 135 | uint poisonActionId = level >= BRD.Levels.BiteUpgrade 136 | ? BRD.CausticBite 137 | : BRD.VenomousBite; 138 | ushort windStatusId = level >= BRD.Levels.BiteUpgrade 139 | ? BRD.Debuffs.Stormbite 140 | : BRD.Debuffs.Windbite; 141 | uint windActionId = level >= BRD.Levels.BiteUpgrade 142 | ? BRD.Stormbite 143 | : BRD.Windbite; 144 | IStatus? poison = TargetFindOwnEffect(poisonStatusId); 145 | IStatus? wind = level >= BRD.Levels.Windbite 146 | ? TargetFindOwnEffect(windStatusId) 147 | : null; 148 | 149 | if (wind is null && level >= BRD.Levels.Windbite) 150 | return windActionId; 151 | 152 | if (poison is null) 153 | return poisonActionId; 154 | 155 | if (wind is not null && wind.RemainingTime < Service.Configuration.BardBiteDebuffThreshold) { 156 | return level >= BRD.Levels.IronJaws 157 | ? BRD.IronJaws 158 | : windActionId; 159 | } 160 | 161 | if (poison.RemainingTime < Service.Configuration.BardBiteDebuffThreshold) { 162 | return level >= BRD.Levels.IronJaws 163 | ? BRD.IronJaws 164 | : poisonActionId; 165 | } 166 | 167 | } 168 | 169 | if (IsEnabled(CustomComboPreset.BardApexFeature)) { 170 | 171 | if (level >= BRD.Levels.BlastShot && SelfHasEffect(BRD.Buffs.BlastShotReady)) 172 | return BRD.BlastArrow; 173 | 174 | if (level >= BRD.Levels.ApexArrow && GetJobGauge().SoulVoice == 100) 175 | return BRD.ApexArrow; 176 | 177 | } 178 | 179 | if (IsEnabled(CustomComboPreset.BardStraightShotUpgradeFeature)) { 180 | if (level >= BRD.Levels.StraightShot && SelfHasEffect(BRD.Buffs.HawksEye)) 181 | return OriginalHook(BRD.StraightShot); 182 | } 183 | 184 | return actionID; 185 | } 186 | } 187 | 188 | internal class BardIronJaws: CustomCombo { 189 | public override CustomComboPreset Preset => CustomComboPreset.BardIronBites; 190 | public override uint[] ActionIDs { get; } = [BRD.IronJaws]; 191 | 192 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 193 | 194 | if (level < BRD.Levels.Windbite) 195 | return BRD.VenomousBite; 196 | 197 | if (level < BRD.Levels.IronJaws) { 198 | 199 | IStatus? venomous = TargetFindOwnEffect(BRD.Debuffs.VenomousBite); 200 | IStatus? windbite = TargetFindOwnEffect(BRD.Debuffs.Windbite); 201 | 202 | return venomous is null 203 | ? BRD.VenomousBite 204 | : windbite is null 205 | ? BRD.Windbite 206 | : venomous.RemainingTime < windbite.RemainingTime 207 | ? BRD.VenomousBite 208 | : BRD.Windbite; 209 | } 210 | 211 | if (level < BRD.Levels.BiteUpgrade) { 212 | 213 | bool venomous = TargetHasOwnEffect(BRD.Debuffs.VenomousBite); 214 | bool windbite = TargetHasOwnEffect(BRD.Debuffs.Windbite); 215 | 216 | return venomous && windbite 217 | ? BRD.IronJaws 218 | : windbite 219 | ? BRD.VenomousBite 220 | : BRD.Windbite; 221 | } 222 | 223 | bool caustic = TargetHasOwnEffect(BRD.Debuffs.CausticBite); 224 | bool stormbite = TargetHasOwnEffect(BRD.Debuffs.Stormbite); 225 | 226 | return caustic && stormbite 227 | ? BRD.IronJaws 228 | : stormbite 229 | ? BRD.CausticBite 230 | : BRD.Stormbite; 231 | } 232 | } 233 | 234 | internal class BardQuickNockLadonsbite: CustomCombo { 235 | public override CustomComboPreset Preset { get; } = CustomComboPreset.BrdAny; 236 | public override uint[] ActionIDs { get; } = [BRD.QuickNock, BRD.Ladonsbite]; 237 | 238 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 239 | 240 | if (IsEnabled(CustomComboPreset.BardApexFeature)) { 241 | 242 | if (level >= BRD.Levels.ApexArrow && GetJobGauge().SoulVoice == 100) 243 | return BRD.ApexArrow; 244 | 245 | if (level >= BRD.Levels.BlastShot && SelfHasEffect(BRD.Buffs.BlastShotReady)) 246 | return BRD.BlastArrow; 247 | 248 | } 249 | 250 | if (IsEnabled(CustomComboPreset.BardQuickNockLadonsbiteShadowbite)) { 251 | if (level >= BRD.Levels.Shadowbite && SelfHasEffect(BRD.Buffs.HawksEye)) 252 | return BRD.Shadowbite; 253 | } 254 | 255 | return actionID; 256 | } 257 | } 258 | 259 | internal class BardShadowbite: CustomCombo { 260 | public override CustomComboPreset Preset { get; } = CustomComboPreset.BrdAny; 261 | public override uint[] ActionIDs { get; } = [BRD.Shadowbite]; 262 | 263 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 264 | 265 | if (IsEnabled(CustomComboPreset.BardApexFeature)) { 266 | 267 | if (level >= BRD.Levels.ApexArrow && GetJobGauge().SoulVoice == 100) 268 | return BRD.ApexArrow; 269 | 270 | if (level >= BRD.Levels.BlastShot && SelfHasEffect(BRD.Buffs.BlastShotReady)) 271 | return BRD.BlastArrow; 272 | 273 | } 274 | 275 | if (IsEnabled(CustomComboPreset.BardShadowbiteDeathRain)) { 276 | 277 | if (level < BRD.Levels.Shadowbite) 278 | return BRD.RainOfDeath; 279 | 280 | if (CanWeave(actionID) && CanUse(BRD.RainOfDeath)) 281 | return BRD.RainOfDeath; 282 | 283 | } 284 | 285 | return actionID; 286 | } 287 | } 288 | 289 | internal class BardEmpyrealSidewinder: CustomCombo { 290 | public override CustomComboPreset Preset { get; } = CustomComboPreset.BardEmpyrealSidewinder; 291 | public override uint[] ActionIDs { get; } = [BRD.Sidewinder, BRD.EmpyrealArrow]; 292 | 293 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 294 | return level >= BRD.Levels.Sidewinder 295 | ? PickByCooldown(actionID, BRD.EmpyrealArrow, BRD.Sidewinder) 296 | : actionID; 297 | } 298 | } 299 | 300 | internal class BardRadiantFinale: CustomCombo { 301 | public override CustomComboPreset Preset { get; } = CustomComboPreset.BrdAny; 302 | public override uint[] ActionIDs { get; } = [BRD.RadiantFinale]; 303 | 304 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 305 | 306 | if (IsEnabled(CustomComboPreset.BardRadiantStrikesFeature)) { 307 | if (level >= BRD.Levels.RagingStrikes && IsOffCooldown(BRD.RagingStrikes)) 308 | return BRD.RagingStrikes; 309 | } 310 | 311 | if (IsEnabled(CustomComboPreset.BardRadiantVoiceFeature)) { 312 | if (level >= BRD.Levels.BattleVoice && IsOffCooldown(BRD.BattleVoice)) 313 | return BRD.BattleVoice; 314 | } 315 | 316 | if (IsEnabled(CustomComboPreset.BardRadiantStrikesFeature)) { 317 | if (level < BRD.Levels.RadiantFinale) 318 | return BRD.RagingStrikes; 319 | } 320 | 321 | if (IsEnabled(CustomComboPreset.BardRadiantVoiceFeature)) { 322 | if (level < BRD.Levels.RadiantFinale) 323 | return BRD.BattleVoice; 324 | } 325 | 326 | return actionID; 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/NIN.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.Conditions; 2 | using Dalamud.Game.ClientState.JobGauge.Types; 3 | 4 | namespace VariableVixen.XIVComboVX.Combos; 5 | 6 | internal static class NIN { 7 | public const byte JobID = 30; 8 | 9 | public const uint 10 | SpinningEdge = 2240, 11 | GustSlash = 2242, 12 | Hide = 2245, 13 | Assassinate = 8814, 14 | ThrowingDagger = 2247, 15 | Mug = 2248, 16 | DeathBlossom = 2254, 17 | AeolianEdge = 2255, 18 | TrickAttack = 2258, 19 | Ninjutsu = 2260, 20 | Chi = 2261, 21 | JinNormal = 2263, 22 | Kassatsu = 2264, 23 | ArmorCrush = 3563, 24 | DreamWithinADream = 3566, 25 | HellfrogMedium = 7401, 26 | Bhavacakra = 7402, 27 | TenChiJin = 7403, 28 | HakkeMujinsatsu = 16488, 29 | Meisui = 16489, 30 | Jin = 18807, 31 | Bunshin = 16493, 32 | PhantomKamaitachi = 25774, 33 | ForkedRaiju = 25777, 34 | FleetingRaiju = 25778, 35 | TenriJindo = 36961; 36 | 37 | public static class Buffs { 38 | public const ushort 39 | Mudra = 496, 40 | Kassatsu = 497, 41 | ShadowWalker = 3848, 42 | Hidden = 614, 43 | Bunshin = 1954, 44 | RaijuReady = 2690, 45 | PhantomKamaitachiReady = 2723, 46 | TenriJindoReady = 3851; 47 | } 48 | 49 | public static class Debuffs { 50 | // public const ushort placeholder = 0; 51 | } 52 | 53 | public static class Levels { 54 | public const byte 55 | GustSlash = 4, 56 | Hide = 10, 57 | ThrowingDagger = 15, 58 | Mug = 15, 59 | AeolianEdge = 26, 60 | Ninjitsu = 30, 61 | Assassinate = 40, 62 | Suiton = 45, 63 | HakkeMujinsatsu = 52, 64 | ArmorCrush = 54, 65 | DreamWithinADream = 56, 66 | Huraijin = 60, 67 | HellfrogMedium = 62, 68 | Bhavacakra = 68, 69 | TenChiJin = 70, 70 | Meisui = 72, 71 | EnhancedKassatsu = 76, 72 | Bunshin = 80, 73 | PhantomKamaitachi = 82, 74 | Raiju = 90, 75 | TenriJindo = 100; 76 | } 77 | } 78 | 79 | internal class NinjaBloodbathReplacer: SecondBloodbathCombo { 80 | public override CustomComboPreset Preset => CustomComboPreset.NinjaBloodbathReplacer; 81 | } 82 | 83 | internal class NinjaArmorCrushCombo: CustomCombo { 84 | public override CustomComboPreset Preset => CustomComboPreset.NinjaArmorCrushCombo; 85 | public override uint[] ActionIDs { get; } = [NIN.ArmorCrush]; 86 | 87 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 88 | NINGauge gauge = GetJobGauge(); 89 | bool canRaiju = level >= NIN.Levels.Raiju && SelfHasEffect(NIN.Buffs.RaijuReady); 90 | bool isDistant = TargetDistance is > 3; 91 | 92 | if (level >= NIN.Levels.Ninjitsu) { 93 | if (IsEnabled(CustomComboPreset.NinjaGCDNinjutsuFeature) && SelfHasEffect(NIN.Buffs.Mudra)) 94 | return OriginalHook(NIN.Ninjutsu); 95 | } 96 | 97 | if (CanWeave(actionID)) { 98 | bool weaveAll = IsEnabled(CustomComboPreset.NinjaSingleTargetSmartWeaveFeature); 99 | bool weaveBunshin = weaveAll || IsEnabled(CustomComboPreset.NinjaArmorCrushBunshinFeature); 100 | bool weaveBhavacakra = weaveAll || IsEnabled(CustomComboPreset.NinjaArmorCrushBhavacakraFeature); 101 | bool weaveAssassinate = weaveAll || IsEnabled(CustomComboPreset.NinjaArmorCrushAssasinateFeature); 102 | 103 | if (weaveBunshin && level >= NIN.Levels.Bunshin && IsOffCooldown(NIN.Bunshin) && GetJobGauge().Ninki >= 50) 104 | return NIN.Bunshin; 105 | if (weaveBhavacakra && level >= NIN.Levels.Bhavacakra && IsOffCooldown(NIN.Bhavacakra) && GetJobGauge().Ninki >= 50 && !isDistant) 106 | return OriginalHook(NIN.Bhavacakra); 107 | if (weaveAssassinate && level >= NIN.Levels.Assassinate && IsOffCooldown(OriginalHook(NIN.DreamWithinADream)) && !isDistant) 108 | return OriginalHook(NIN.DreamWithinADream); 109 | 110 | } 111 | 112 | if (isDistant) { 113 | if (canRaiju) { 114 | if (IsEnabled(CustomComboPreset.NinjaArmorCrushSmartRaijuFeature) || IsEnabled(CustomComboPreset.NinjaArmorCrushForkedRaijuFeature)) 115 | return NIN.ForkedRaiju; 116 | } 117 | 118 | if (IsEnabled(CustomComboPreset.NinjaArmorCrushThrowingDaggerFeature)) 119 | return level >= NIN.Levels.PhantomKamaitachi && SelfHasEffect(NIN.Buffs.PhantomKamaitachiReady) ? NIN.PhantomKamaitachi : NIN.ThrowingDagger; 120 | } 121 | else if (canRaiju) { 122 | if (IsEnabled(CustomComboPreset.NinjaArmorCrushSmartRaijuFeature) || IsEnabled(CustomComboPreset.NinjaArmorCrushFleetingRaijuFeature)) 123 | return NIN.FleetingRaiju; 124 | } 125 | 126 | if (lastComboMove is NIN.SpinningEdge) { 127 | if (level >= NIN.Levels.GustSlash) 128 | return NIN.GustSlash; 129 | } 130 | 131 | if (lastComboMove is NIN.GustSlash) { 132 | 133 | if (IsEnabled(CustomComboPreset.NinjaArmorCrushGaugeSaver) && gauge.Kazematoi >= 4) 134 | return NIN.AeolianEdge; 135 | 136 | if (level >= NIN.Levels.ArmorCrush) 137 | return NIN.ArmorCrush; 138 | 139 | if (IsEnabled(CustomComboPreset.NinjaArmorCrushFallbackFeature) && level >= NIN.Levels.AeolianEdge) 140 | return NIN.AeolianEdge; 141 | 142 | } 143 | 144 | if (IsEnabled(CustomComboPreset.NinjaArmorCrushKamaitachiFeature)) { 145 | if (level >= NIN.Levels.PhantomKamaitachi && SelfHasEffect(NIN.Buffs.PhantomKamaitachiReady) && !SelfHasEffect(NIN.Buffs.Bunshin)) 146 | return NIN.PhantomKamaitachi; 147 | } 148 | 149 | return NIN.SpinningEdge; 150 | } 151 | } 152 | 153 | internal class NinjaAeolianEdgeCombo: CustomCombo { 154 | public override CustomComboPreset Preset => CustomComboPreset.NinjaAeolianEdgeCombo; 155 | public override uint[] ActionIDs { get; } = [NIN.AeolianEdge]; 156 | 157 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 158 | NINGauge gauge = GetJobGauge(); 159 | bool canRaiju = level >= NIN.Levels.Raiju && SelfHasEffect(NIN.Buffs.RaijuReady); 160 | bool isDistant = TargetDistance is > 3; 161 | 162 | if (IsEnabled(CustomComboPreset.NinjaGCDNinjutsuFeature)) { 163 | if (level >= NIN.Levels.Ninjitsu) { 164 | if (SelfHasEffect(NIN.Buffs.Mudra)) 165 | return OriginalHook(NIN.Ninjutsu); 166 | } 167 | } 168 | 169 | if (CanWeave(actionID)) { 170 | bool weaveAll = IsEnabled(CustomComboPreset.NinjaSingleTargetSmartWeaveFeature); 171 | bool weaveBunshin = weaveAll || IsEnabled(CustomComboPreset.NinjaAeolianEdgeBunshinFeature); 172 | bool weaveBhavacakra = weaveAll || IsEnabled(CustomComboPreset.NinjaAeolianEdgeBhavacakraFeature); 173 | bool weaveAssassinate = weaveAll || IsEnabled(CustomComboPreset.NinjaAeolianEdgeAssasinateFeature); 174 | 175 | if (weaveBunshin && level >= NIN.Levels.Bunshin && IsOffCooldown(NIN.Bunshin) && GetJobGauge().Ninki >= 50) 176 | return NIN.Bunshin; 177 | if (weaveBhavacakra && level >= NIN.Levels.Bhavacakra && IsOffCooldown(NIN.Bhavacakra) && GetJobGauge().Ninki >= 50 && !isDistant) 178 | return OriginalHook(NIN.Bhavacakra); 179 | if (weaveAssassinate && level >= NIN.Levels.Assassinate && IsOffCooldown(OriginalHook(NIN.DreamWithinADream)) && !isDistant) 180 | return OriginalHook(NIN.DreamWithinADream); 181 | 182 | } 183 | 184 | if (isDistant) { 185 | if (canRaiju) { 186 | if (IsEnabled(CustomComboPreset.NinjaAeolianEdgeSmartRaijuFeature) || IsEnabled(CustomComboPreset.NinjaAeolianEdgeForkedRaijuFeature)) 187 | return NIN.ForkedRaiju; 188 | } 189 | 190 | if (IsEnabled(CustomComboPreset.NinjaAeolianEdgeThrowingDaggerFeature)) 191 | return level >= NIN.Levels.PhantomKamaitachi && SelfHasEffect(NIN.Buffs.PhantomKamaitachiReady) ? NIN.PhantomKamaitachi : NIN.ThrowingDagger; 192 | } 193 | else if (canRaiju) { 194 | if (IsEnabled(CustomComboPreset.NinjaAeolianEdgeSmartRaijuFeature) || IsEnabled(CustomComboPreset.NinjaAeolianEdgeFleetingRaijuFeature)) 195 | return NIN.FleetingRaiju; 196 | } 197 | 198 | if (lastComboMove is NIN.SpinningEdge) { 199 | if (level >= NIN.Levels.GustSlash) 200 | return NIN.GustSlash; 201 | } 202 | 203 | if (lastComboMove is NIN.GustSlash) { 204 | 205 | if (IsEnabled(CustomComboPreset.NinjaAeolianEdgeGaugeSaver) && gauge.Kazematoi == 0 && level >= NIN.Levels.ArmorCrush) 206 | return NIN.ArmorCrush; 207 | 208 | if (level >= NIN.Levels.AeolianEdge) 209 | return NIN.AeolianEdge; 210 | 211 | } 212 | 213 | if (IsEnabled(CustomComboPreset.NinjaAeolianEdgeKamaitachiFeature)) { 214 | if (level >= NIN.Levels.PhantomKamaitachi && SelfHasEffect(NIN.Buffs.PhantomKamaitachiReady) && !SelfHasEffect(NIN.Buffs.Bunshin)) 215 | return NIN.PhantomKamaitachi; 216 | } 217 | 218 | return NIN.SpinningEdge; 219 | } 220 | } 221 | 222 | internal class NinjaHakkeMujinsatsuCombo: CustomCombo { 223 | public override CustomComboPreset Preset => CustomComboPreset.NinjaHakkeMujinsatsuCombo; 224 | public override uint[] ActionIDs { get; } = [NIN.HakkeMujinsatsu]; 225 | 226 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 227 | 228 | if (IsEnabled(CustomComboPreset.NinjaGCDNinjutsuFeature)) { 229 | if (SelfHasEffect(NIN.Buffs.Mudra)) 230 | return OriginalHook(NIN.Ninjutsu); 231 | } 232 | 233 | if (IsEnabled(CustomComboPreset.NinjaAOESmartWeaveFeature) && CanWeave(actionID)) { 234 | if (level >= NIN.Levels.HellfrogMedium && GetJobGauge().Ninki >= 50) 235 | return OriginalHook(NIN.HellfrogMedium); 236 | } 237 | 238 | if (level >= NIN.Levels.HakkeMujinsatsu) { 239 | if (lastComboMove is NIN.DeathBlossom) 240 | return NIN.HakkeMujinsatsu; 241 | } 242 | 243 | return NIN.DeathBlossom; 244 | } 245 | } 246 | 247 | internal class NinjaKassatsuTrickFeature: CustomCombo { 248 | public override CustomComboPreset Preset => CustomComboPreset.NinjaKassatsuTrickFeature; 249 | public override uint[] ActionIDs { get; } = [NIN.Kassatsu]; 250 | 251 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 252 | 253 | if (level >= NIN.Levels.Hide) { 254 | if (SelfHasEffect(NIN.Buffs.Hidden)) 255 | return OriginalHook(NIN.TrickAttack); 256 | } 257 | 258 | if (level >= NIN.Levels.Suiton) { 259 | if (SelfHasEffect(NIN.Buffs.ShadowWalker)) 260 | return OriginalHook(NIN.TrickAttack); 261 | } 262 | 263 | return actionID; 264 | } 265 | } 266 | 267 | internal class NinjaHideMugFeature: CustomCombo { 268 | public override CustomComboPreset Preset => CustomComboPreset.NinjaSmartHideFeature; 269 | public override uint[] ActionIDs { get; } = [NIN.Hide]; 270 | 271 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 272 | 273 | if (level >= NIN.Levels.Hide) { 274 | if (SelfHasEffect(NIN.Buffs.Hidden) && HasTarget) 275 | return OriginalHook(NIN.TrickAttack); 276 | } 277 | 278 | if (level >= NIN.Levels.Suiton) { 279 | if (SelfHasEffect(NIN.Buffs.ShadowWalker)) 280 | return OriginalHook(NIN.TrickAttack); 281 | } 282 | 283 | if (level >= NIN.Levels.Mug) { 284 | if (HasCondition(ConditionFlag.InCombat)) 285 | return OriginalHook(NIN.Mug); 286 | } 287 | 288 | return actionID; 289 | } 290 | } 291 | 292 | internal class NinjaKassatsuChiJinFeature: CustomCombo { 293 | public override CustomComboPreset Preset => CustomComboPreset.NinjaKassatsuChiJinFeature; 294 | public override uint[] ActionIDs { get; } = [NIN.Chi]; 295 | 296 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 297 | 298 | if (level >= NIN.Levels.EnhancedKassatsu) { 299 | if (SelfHasEffect(NIN.Buffs.Kassatsu)) 300 | return NIN.Jin; 301 | } 302 | 303 | return actionID; 304 | } 305 | } 306 | 307 | internal class NinjaTCJMeisuiFeature: CustomCombo { 308 | public override CustomComboPreset Preset => CustomComboPreset.NinAny; 309 | public override uint[] ActionIDs { get; } = [NIN.TenChiJin]; 310 | 311 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 312 | 313 | if (IsEnabled(CustomComboPreset.NinjaTCJMeisuiFeature)) { 314 | if (level >= NIN.Levels.Meisui) { 315 | if (SelfHasEffect(NIN.Buffs.ShadowWalker)) 316 | return NIN.Meisui; 317 | } 318 | } 319 | 320 | if (IsEnabled(CustomComboPreset.NinjaTCJTenriJindo)) { 321 | if (level >= NIN.Levels.TenriJindo && SelfHasEffect(NIN.Buffs.TenriJindoReady)) 322 | return NIN.TenriJindo; 323 | } 324 | 325 | return actionID; 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/MCH.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | 3 | using VariableVixen.XIVComboVX; 4 | 5 | namespace VariableVixen.XIVComboVX.Combos; 6 | 7 | internal static class MCH { 8 | public const byte JobID = 31; 9 | 10 | public const uint 11 | // Single target 12 | CleanShot = 2873, 13 | HeatedCleanShot = 7413, 14 | SplitShot = 2866, 15 | HeatedSplitShot = 7411, 16 | SlugShot = 2868, 17 | HeatedSlugshot = 7412, 18 | // Charges 19 | GaussRound = 2874, 20 | Ricochet = 2890, 21 | // AoE 22 | SpreadShot = 2870, 23 | AutoCrossbow = 16497, 24 | Scattergun = 25786, 25 | // Rook 26 | RookAutoturret = 2864, 27 | RookOverdrive = 7415, 28 | AutomatonQueen = 16501, 29 | QueenOverdrive = 16502, 30 | // Other 31 | BarrelStabiliser = 7414, 32 | Tactician = 16889, 33 | Dismantle = 2887, 34 | Wildfire = 2878, 35 | Detonator = 16766, 36 | Hypercharge = 17209, 37 | HeatBlast = 7410, 38 | HotShot = 2872, 39 | Drill = 16498, 40 | Bioblaster = 16499, 41 | AirAnchor = 16500, 42 | Chainsaw = 25788, 43 | DoubleCheck = 36979, 44 | Checkmate = 36980, 45 | BlazingShot = 36978, 46 | FullMetalField = 36982, 47 | Excavator = 36981; 48 | 49 | public static class Buffs { 50 | public const ushort 51 | Reassembled = 851, 52 | Hypercharged = 3864, 53 | ExcavatorReady = 3865, 54 | FullMetalMachinist = 3866; 55 | } 56 | 57 | public static class Debuffs { 58 | // public const ushort placeholder = 0; 59 | } 60 | 61 | public static class Levels { 62 | public const byte 63 | SlugShot = 2, 64 | HotShot = 4, 65 | GaussRound = 15, 66 | CleanShot = 26, 67 | Hypercharge = 30, 68 | HeatBlast = 35, 69 | RookOverdrive = 40, 70 | Wildfire = 45, 71 | Ricochet = 50, 72 | AutoCrossbow = 52, 73 | HeatedSplitShot = 54, 74 | Tactician = 56, 75 | Drill = 58, 76 | HeatedSlugshot = 60, 77 | Dismantle = 62, 78 | HeatedCleanShot = 64, 79 | BarrelStabiliser = 66, 80 | ChargedActionMastery = 74, 81 | AirAnchor = 76, 82 | QueenOverdrive = 80, 83 | Chainsaw = 90, 84 | DoubleCheck = 92, 85 | Checkmate = 92, 86 | Excavator = 96; 87 | } 88 | } 89 | 90 | internal class MachinistCleanShot: CustomCombo { 91 | public override CustomComboPreset Preset => CustomComboPreset.MachinistMainCombo; 92 | public override uint[] ActionIDs { get; } = [MCH.CleanShot, MCH.HeatedCleanShot]; 93 | 94 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 95 | MCHGauge gauge = GetJobGauge(); 96 | 97 | if (IsEnabled(CustomComboPreset.MachinistMainComboReassembledOverride)) { 98 | 99 | if (level is >= MCH.Levels.HotShot and < MCH.Levels.Drill) { 100 | bool canCleanShot = level >= MCH.Levels.CleanShot && lastComboMove is MCH.SlugShot; // note that Hot Shot is LESS potency than Clean Shot when part of the combo 101 | bool canHeatedShot = level >= MCH.Levels.HeatedSlugshot && lastComboMove is MCH.HeatedSplitShot or MCH.HeatedSlugshot; // HS is also weaker than Heated Slug/Clean Shot 102 | if (!canCleanShot && !canHeatedShot) { 103 | if (SelfHasEffect(MCH.Buffs.Reassembled)) { 104 | if (CanUse(MCH.HotShot)) 105 | return MCH.HotShot; 106 | } 107 | } 108 | } 109 | 110 | if (level >= MCH.Levels.Drill) { 111 | if (SelfHasEffect(MCH.Buffs.Reassembled)) { 112 | uint preference = gauge.Battery > 80 || level < MCH.Levels.AirAnchor 113 | ? MCH.Drill 114 | : MCH.AirAnchor; 115 | uint choice = 0; 116 | 117 | if (level >= MCH.Levels.Chainsaw) 118 | choice = PickByCooldown(preference, OriginalHook(MCH.Chainsaw), MCH.Drill, MCH.AirAnchor); 119 | 120 | if (level >= MCH.Levels.AirAnchor) 121 | choice = PickByCooldown(preference, MCH.Drill, MCH.AirAnchor); 122 | 123 | if (CanUse(MCH.Drill)) 124 | choice = MCH.Drill; // probably shouldn't return actionID so it can run through the chain below 125 | 126 | if (choice > 0 && CanUse(choice)) 127 | return choice; 128 | } 129 | } 130 | } 131 | 132 | if (IsEnabled(CustomComboPreset.MachinistMainComboHeatBlast) && level >= MCH.Levels.HeatBlast && gauge.IsOverheated) { 133 | 134 | if (IsEnabled(CustomComboPreset.MachinistHyperchargeWildfire) && level >= MCH.Levels.Wildfire) { 135 | if (gauge.OverheatTimeRemaining >= 9_000 && IsOffCooldown(MCH.Wildfire) && HasTarget) 136 | return MCH.Wildfire; 137 | } 138 | 139 | if (IsEnabled(CustomComboPreset.MachinistHeatBlastWeaveGaussRoundRicochet) && CanWeave(MCH.HeatBlast)) { // Heat Blast has a 1.5s cooldown instead of the normal GCD 140 | 141 | if (level >= MCH.Levels.Ricochet) { 142 | uint gauss = OriginalHook(MCH.GaussRound); 143 | uint ricochet = OriginalHook(MCH.Ricochet); 144 | 145 | return PickByCooldown(gauss, gauss, ricochet); 146 | } 147 | 148 | return MCH.GaussRound; 149 | } 150 | 151 | return OriginalHook(MCH.HeatBlast); 152 | } 153 | 154 | if (comboTime > 0) { 155 | 156 | if (lastComboMove is MCH.SplitShot or MCH.HeatedSplitShot && level >= MCH.Levels.SlugShot) 157 | return OriginalHook(MCH.SlugShot); 158 | 159 | if (lastComboMove is MCH.SlugShot or MCH.HeatedSlugshot && level >= MCH.Levels.CleanShot) 160 | return OriginalHook(MCH.CleanShot); 161 | 162 | } 163 | 164 | return OriginalHook(MCH.SplitShot); 165 | } 166 | } 167 | 168 | internal class MachinistGaussRicochet: CustomCombo { 169 | public override CustomComboPreset Preset => CustomComboPreset.MachinistGaussRoundRicochet; 170 | public override uint[] ActionIDs { get; } = [MCH.GaussRound, MCH.Ricochet, MCH.DoubleCheck, MCH.Checkmate]; 171 | 172 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 173 | 174 | if (level >= MCH.Levels.Ricochet) { 175 | 176 | if (IsEnabled(CustomComboPreset.MachinistGaussRoundRicochetLimiter) && !GetJobGauge().IsOverheated) 177 | return actionID; 178 | 179 | return PickByCooldown(actionID, OriginalHook(MCH.Ricochet), OriginalHook(MCH.GaussRound)); 180 | } 181 | 182 | return OriginalHook(MCH.GaussRound); 183 | } 184 | } 185 | 186 | internal class MachinistHypercharge: CustomCombo { 187 | public override CustomComboPreset Preset { get; } = CustomComboPreset.MchAny; 188 | public override uint[] ActionIDs { get; } = [MCH.Hypercharge]; 189 | 190 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 191 | MCHGauge gauge = GetJobGauge(); 192 | 193 | if (IsEnabled(CustomComboPreset.MachinistHyperchargeStabiliser)) { 194 | if (level >= MCH.Levels.BarrelStabiliser && !gauge.IsOverheated && gauge.Heat < 50 && !SelfHasEffect(MCH.Buffs.Hypercharged)) 195 | return MCH.BarrelStabiliser; 196 | } 197 | 198 | if (IsEnabled(CustomComboPreset.MachinistHyperchargeWildfire)) { 199 | 200 | if (level >= MCH.Levels.Wildfire && gauge.IsOverheated) { 201 | 202 | if (gauge.OverheatTimeRemaining >= 9_000 && IsOffCooldown(MCH.Wildfire) && HasTarget) 203 | return MCH.Wildfire; 204 | 205 | if (IsOnCooldown(MCH.Hypercharge) && !IsOriginal(MCH.Wildfire)) 206 | return MCH.Detonator; 207 | 208 | } 209 | 210 | } 211 | 212 | return actionID; 213 | } 214 | } 215 | 216 | internal class MachinistHeatBlastBlazingShot: CustomCombo { 217 | public override CustomComboPreset Preset => CustomComboPreset.MchAny; 218 | public override uint[] ActionIDs { get; } = [MCH.HeatBlast, MCH.BlazingShot]; 219 | 220 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 221 | MCHGauge gauge = GetJobGauge(); 222 | 223 | if (level < MCH.Levels.Hypercharge) 224 | return MCH.Hypercharge; 225 | 226 | if (IsEnabled(CustomComboPreset.MachinistSmartHeatup) && !gauge.IsOverheated) { 227 | if (IsEnabled(CustomComboPreset.MachinistHyperchargeStabiliser) && level >= MCH.Levels.BarrelStabiliser) { 228 | if (!gauge.IsOverheated && gauge.Heat < 50 && !SelfHasEffect(MCH.Buffs.Hypercharged)) 229 | return MCH.BarrelStabiliser; 230 | } 231 | 232 | return MCH.Hypercharge; 233 | } 234 | 235 | if (IsEnabled(CustomComboPreset.MachinistHyperchargeWildfire) && level >= MCH.Levels.Wildfire) { 236 | if (gauge.IsOverheated && gauge.OverheatTimeRemaining >= 9_000 && IsOffCooldown(MCH.Wildfire) && HasTarget) 237 | return MCH.Wildfire; 238 | } 239 | 240 | if ((actionID is MCH.HeatBlast or MCH.BlazingShot || level < MCH.Levels.AutoCrossbow) && level >= MCH.Levels.HeatBlast) { 241 | if (IsEnabled(CustomComboPreset.MachinistHeatBlastWeaveGaussRoundRicochet)) { 242 | if (gauge.IsOverheated && CanWeave(MCH.HeatBlast)) { // Heat Blast has a 1.5s cooldown instead of the normal GCD 243 | uint gauss = OriginalHook(MCH.GaussRound); 244 | uint ricochet = OriginalHook(MCH.Ricochet); 245 | 246 | if (level >= MCH.Levels.Ricochet) 247 | return PickByCooldown(gauss, gauss, ricochet); 248 | 249 | return gauss; 250 | } 251 | } 252 | 253 | return OriginalHook(MCH.HeatBlast); 254 | } 255 | 256 | return actionID; 257 | } 258 | } 259 | 260 | internal class MachinistSpreadShotFeature: CustomCombo { 261 | public override CustomComboPreset Preset => CustomComboPreset.MachinistSpreadShot; 262 | public override uint[] ActionIDs { get; } = [MCH.SpreadShot, MCH.Scattergun]; 263 | 264 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 265 | 266 | if (level >= MCH.Levels.AutoCrossbow && GetJobGauge().IsOverheated) 267 | return MCH.AutoCrossbow; 268 | 269 | return actionID; 270 | } 271 | } 272 | 273 | internal class MachinistAutoCrossbow: CustomCombo { 274 | public override CustomComboPreset Preset => CustomComboPreset.MachinistSmartHeatup; 275 | public override uint[] ActionIDs { get; } = [MCH.AutoCrossbow]; 276 | 277 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 278 | MCHGauge gauge = GetJobGauge(); 279 | 280 | if (level < MCH.Levels.Hypercharge) // cannot overheat, so auto crossbow doesn't even work 281 | return MCH.Hypercharge; 282 | 283 | if (!gauge.IsOverheated) { 284 | if (IsEnabled(CustomComboPreset.MachinistHyperchargeStabiliser) && level >= MCH.Levels.BarrelStabiliser) { 285 | if (!gauge.IsOverheated && gauge.Heat < 50 && !SelfHasEffect(MCH.Buffs.Hypercharged)) 286 | return MCH.BarrelStabiliser; 287 | } 288 | 289 | return MCH.Hypercharge; 290 | } 291 | 292 | return actionID; 293 | } 294 | } 295 | 296 | internal class MachinistOverdriveFeature: CustomCombo { 297 | public override CustomComboPreset Preset => CustomComboPreset.MachinistOverdrive; 298 | public override uint[] ActionIDs { get; } = [MCH.RookAutoturret, MCH.AutomatonQueen]; 299 | 300 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 301 | 302 | if (level >= MCH.Levels.RookOverdrive && GetJobGauge().IsRobotActive) 303 | return OriginalHook(MCH.RookOverdrive); 304 | 305 | return actionID; 306 | } 307 | } 308 | 309 | internal class MachinistDrillAirAnchorFeature: CustomCombo { 310 | public override CustomComboPreset Preset => CustomComboPreset.MachinistDrillAirAnchor; 311 | public override uint[] ActionIDs { get; } = [MCH.HotShot, MCH.AirAnchor, MCH.Drill]; 312 | 313 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 314 | uint preference = GetJobGauge().Battery > 80 315 | ? MCH.Drill 316 | : level >= MCH.Levels.AirAnchor 317 | ? MCH.AirAnchor 318 | : MCH.Drill; 319 | 320 | if (level >= MCH.Levels.Chainsaw && IsEnabled(CustomComboPreset.MachinistDrillAirAnchorPlus)) 321 | return PickByCooldown(preference, OriginalHook(MCH.Chainsaw), MCH.Drill, MCH.AirAnchor); 322 | 323 | if (level >= MCH.Levels.AirAnchor) 324 | return PickByCooldown(preference, MCH.Drill, MCH.AirAnchor); 325 | 326 | if (level >= MCH.Levels.Drill) 327 | return PickByCooldown(preference, MCH.HotShot, MCH.Drill); 328 | 329 | return MCH.HotShot; 330 | } 331 | } 332 | 333 | internal class MachinistTacticianDismantle: CustomCombo { 334 | public override CustomComboPreset Preset { get; } = CustomComboPreset.MachinistTacticianDismantle; 335 | public override uint[] ActionIDs { get; } = [MCH.Tactician, MCH.Dismantle]; 336 | 337 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 338 | 339 | // These three actions cannot be stacked, so even if Dismantle is unavailable, we don't want to waste Tactician 340 | if (SelfHasEffect(BRD.Buffs.Troubadour) || SelfHasEffect(DNC.Buffs.ShieldSamba)) 341 | return MCH.Dismantle; 342 | 343 | if (level <= MCH.Levels.Dismantle) 344 | return MCH.Tactician; 345 | 346 | if (!HasTarget || TargetDistance > 25) 347 | return MCH.Tactician; 348 | 349 | return PickByCooldown(actionID, MCH.Tactician, MCH.Dismantle); 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/DOL.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.Conditions; 2 | 3 | using VariableVixen.XIVComboVX; 4 | 5 | namespace VariableVixen.XIVComboVX.Combos; 6 | 7 | internal static class DOL { 8 | public const byte JobID = 99, 9 | MinID = 16, 10 | BtnID = 17, 11 | FshID = 18; 12 | 13 | public static class Buffs { 14 | public const ushort 15 | EurekaMoment = 2765, 16 | CollectorsStandard = 2418, 17 | CollectorsHighStandard = 3911, 18 | PrimingTouch = 3910; 19 | } 20 | 21 | public static class Debuffs { 22 | public const ushort 23 | Placeholder = 0; 24 | } 25 | 26 | public static class Levels { 27 | public const byte 28 | Snagging = 36, 29 | Gig = 61, 30 | Salvage = 67, 31 | VeteranTrade = 63, 32 | NaturesBounty = 69, 33 | SurfaceSlap = 71, 34 | PrizeCatch = 81, 35 | WiseToTheWorld = 90, 36 | PrimingTouch = 95; 37 | 38 | } 39 | } 40 | 41 | public static class BTN { 42 | public const uint 43 | Triangulate = 210, 44 | ArborCall = 211, 45 | FieldMastery = 218, 46 | ArborCall2 = 290, 47 | FieldMastery2 = 220, 48 | Sneak = 304, 49 | FieldMastery3 = 294, 50 | PioneersGift = 21178, 51 | TwelvesBounty = 282, 52 | FloraMastery = 4086, 53 | BountifulHarvest = 4087, 54 | AgelessWords = 215, 55 | BlessedHarvest = 222, 56 | BlessedHarvest2 = 224, 57 | TruthOfForests = 221, 58 | Collect = 815, 59 | Scour = 22186, 60 | BrazenWoodsman = 22187, 61 | MeticulousWoodsman = 22188, 62 | Scrutiny = 22189, 63 | PioneersGift2 = 25590, 64 | LuckOfThePioneer = 4095, 65 | BountifulHarvest2 = 273, 66 | GivingLand = 4590, 67 | NophicasTidings = 21204, 68 | CollectorsFocus = 21206, 69 | WiseToTheWorld = 26522, 70 | PrimingTouch = 34872; 71 | } 72 | public static class MIN { 73 | public const uint 74 | Prospect = 227, 75 | LayOfTheLand = 228, 76 | SharpVision = 235, 77 | LayOfTheLand2 = 291, 78 | SharpVision2 = 237, 79 | Sneak = 303, 80 | SharpVision3 = 295, 81 | MountaineersGift = 21177, 82 | TwelvesBounty = 280, 83 | ClearVision = 4072, 84 | BountifulYield = 4073, 85 | SolidReason = 232, 86 | KingsYield = 239, 87 | KingsYield2 = 241, 88 | TruthOfMountains = 238, 89 | Collect = 240, 90 | Scour = 22182, 91 | BrazenProspector = 22183, 92 | MeticulousProspector = 22184, 93 | Scrutiny = 22185, 94 | MountaineersGift2 = 25589, 95 | LuckOfTheMountaineer = 4081, 96 | BountifulYield2 = 272, 97 | GivingLand = 4589, 98 | NaldthalsTidings = 21203, 99 | CollectorsFocus = 21205, 100 | WiseToTheWorld = 26521, 101 | PrimingTouch = 34871; 102 | } 103 | 104 | public static class FSH { 105 | public const uint 106 | Mooch2 = 268, 107 | DoubleHook = 269, 108 | Bait = 288, 109 | Cast = 289, 110 | Hook = 296, 111 | Quit = 299, 112 | Snagging = 4100, 113 | Patience = 4102, 114 | Chum = 4104, 115 | FishEyes = 4105, 116 | Patience2 = 4106, 117 | SurfaceSlap = 4595, 118 | IdenticalCast = 4596, 119 | Gig = 7632, 120 | VeteranTrade = 7906, 121 | NaturesBounty = 7909, 122 | Salvage = 7910, 123 | ThaliaksFavour = 26804, 124 | MakeshiftBait = 26805, 125 | PrizeCatch = 26806, 126 | VitalSight = 26870, 127 | BaitedBreath = 26871, 128 | ElectricCurrent = 26872, 129 | TripleHook = 27523, 130 | SparefulHand = 37045, 131 | BigGameFishing = 37046; 132 | 133 | 134 | public static class Buffs { 135 | public const ushort 136 | AnglersArt = 2778; 137 | } 138 | } 139 | 140 | internal class NonFishingFeatures: CustomCombo { 141 | public override CustomComboPreset Preset { get; } = CustomComboPreset.DolAny; 142 | // No ActionIDs are set because this applies to a wide enough variety of actions that it's too much duplication 143 | 144 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 145 | 146 | if (IsEnabled(CustomComboPreset.GatherEurekaFeature)) { 147 | if (actionID is MIN.SolidReason or BTN.AgelessWords) { 148 | if (level >= DOL.Levels.WiseToTheWorld) { 149 | if (SelfHasEffect(DOL.Buffs.EurekaMoment)) { 150 | return IsJob(DOL.MinID) 151 | ? MIN.WiseToTheWorld 152 | : BTN.WiseToTheWorld; 153 | } 154 | } 155 | } 156 | } 157 | 158 | if (IsEnabled(CustomComboPreset.GatherJobCorrectionFeature)) { 159 | switch (LocalPlayer.ClassJob.RowId) { 160 | case DOL.BtnID: 161 | return actionID switch { 162 | MIN.Prospect => IsEnabled(CustomComboPreset.GatherJobCorrectionIgnoreDetectionsFeature) ? actionID : BTN.Triangulate, 163 | MIN.LayOfTheLand => IsEnabled(CustomComboPreset.GatherJobCorrectionIgnoreDetectionsFeature) ? actionID : BTN.ArborCall, 164 | MIN.SharpVision => BTN.FieldMastery, 165 | MIN.LayOfTheLand2 => IsEnabled(CustomComboPreset.GatherJobCorrectionIgnoreDetectionsFeature) ? actionID : BTN.ArborCall2, 166 | MIN.SharpVision2 => BTN.FieldMastery2, 167 | MIN.Sneak => BTN.Sneak, 168 | MIN.SharpVision3 => BTN.FieldMastery3, 169 | MIN.MountaineersGift => BTN.PioneersGift, 170 | MIN.TwelvesBounty => BTN.TwelvesBounty, 171 | MIN.ClearVision => BTN.FloraMastery, 172 | MIN.BountifulYield => OriginalHook(BTN.BountifulHarvest), 173 | MIN.SolidReason => BTN.AgelessWords, 174 | MIN.KingsYield => BTN.BlessedHarvest, 175 | MIN.KingsYield2 => BTN.BlessedHarvest2, 176 | MIN.TruthOfMountains => IsEnabled(CustomComboPreset.GatherJobCorrectionIgnoreDetectionsFeature) ? actionID : BTN.TruthOfForests, 177 | MIN.Collect => BTN.Collect, 178 | MIN.Scour => BTN.Scour, 179 | MIN.BrazenProspector => BTN.BrazenWoodsman, 180 | MIN.MeticulousProspector => BTN.MeticulousWoodsman, 181 | MIN.Scrutiny => BTN.Scrutiny, 182 | MIN.MountaineersGift2 => BTN.PioneersGift2, 183 | MIN.LuckOfTheMountaineer => BTN.LuckOfThePioneer, 184 | MIN.BountifulYield2 => OriginalHook(BTN.BountifulHarvest2), 185 | MIN.GivingLand => BTN.GivingLand, 186 | MIN.NaldthalsTidings => BTN.NophicasTidings, 187 | MIN.CollectorsFocus => BTN.CollectorsFocus, 188 | MIN.WiseToTheWorld => BTN.WiseToTheWorld, 189 | MIN.PrimingTouch => BTN.PrimingTouch, 190 | _ => actionID, 191 | }; 192 | case DOL.MinID: 193 | return actionID switch { 194 | BTN.Triangulate => IsEnabled(CustomComboPreset.GatherJobCorrectionIgnoreDetectionsFeature) ? actionID : MIN.Prospect, 195 | BTN.ArborCall => IsEnabled(CustomComboPreset.GatherJobCorrectionIgnoreDetectionsFeature) ? actionID : MIN.LayOfTheLand, 196 | BTN.FieldMastery => MIN.SharpVision, 197 | BTN.ArborCall2 => IsEnabled(CustomComboPreset.GatherJobCorrectionIgnoreDetectionsFeature) ? actionID : MIN.LayOfTheLand2, 198 | BTN.FieldMastery2 => MIN.SharpVision2, 199 | BTN.Sneak => MIN.Sneak, 200 | BTN.FieldMastery3 => MIN.SharpVision3, 201 | BTN.PioneersGift => MIN.MountaineersGift, 202 | BTN.TwelvesBounty => MIN.TwelvesBounty, 203 | BTN.FloraMastery => MIN.ClearVision, 204 | BTN.BountifulHarvest => OriginalHook(MIN.BountifulYield), 205 | BTN.AgelessWords => MIN.SolidReason, 206 | BTN.BlessedHarvest => MIN.KingsYield, 207 | BTN.BlessedHarvest2 => MIN.KingsYield2, 208 | BTN.TruthOfForests => IsEnabled(CustomComboPreset.GatherJobCorrectionIgnoreDetectionsFeature) ? actionID : MIN.TruthOfMountains, 209 | BTN.Collect => MIN.Collect, 210 | BTN.Scour => MIN.Scour, 211 | BTN.BrazenWoodsman => MIN.BrazenProspector, 212 | BTN.MeticulousWoodsman => MIN.MeticulousProspector, 213 | BTN.Scrutiny => MIN.Scrutiny, 214 | BTN.PioneersGift2 => MIN.MountaineersGift2, 215 | BTN.LuckOfThePioneer => MIN.LuckOfTheMountaineer, 216 | BTN.BountifulHarvest2 => OriginalHook(MIN.BountifulYield2), 217 | BTN.GivingLand => MIN.GivingLand, 218 | BTN.NophicasTidings => MIN.NaldthalsTidings, 219 | BTN.CollectorsFocus => MIN.CollectorsFocus, 220 | BTN.WiseToTheWorld => MIN.WiseToTheWorld, 221 | BTN.PrimingTouch => MIN.PrimingTouch, 222 | _ => actionID, 223 | }; 224 | } 225 | } 226 | 227 | return actionID; 228 | } 229 | 230 | 231 | } 232 | 233 | internal class FisherSwapFeatures: CustomCombo { 234 | public override CustomComboPreset Preset { get; } = CustomComboPreset.DolAny; 235 | // No ActionIDs are set because this applies to a wide enough variety of actions that it's too much duplication 236 | 237 | private static uint thaliak(uint actionID, byte level) { 238 | if (level >= 15 && SelfEffectStacks(FSH.Buffs.AnglersArt) >= 3) { 239 | 240 | if (actionID is FSH.Chum && LocalPlayer.CurrentGp < 100) 241 | return FSH.ThaliaksFavour; 242 | 243 | else if (actionID is FSH.Patience && LocalPlayer.CurrentGp < 200) 244 | return FSH.ThaliaksFavour; 245 | 246 | else if (actionID is FSH.Patience2 && LocalPlayer.CurrentGp < 560) 247 | return FSH.ThaliaksFavour; 248 | 249 | else if (actionID is FSH.FishEyes && LocalPlayer.CurrentGp < 550) 250 | return FSH.ThaliaksFavour; 251 | 252 | else if (actionID is FSH.Mooch2 && LocalPlayer.CurrentGp < 100 && CanUse(FSH.Mooch2)) 253 | return FSH.ThaliaksFavour; 254 | 255 | else if (actionID is FSH.VeteranTrade && LocalPlayer.CurrentGp < 200) 256 | return FSH.ThaliaksFavour; 257 | 258 | else if (actionID is FSH.NaturesBounty && LocalPlayer.CurrentGp < 100) 259 | return FSH.ThaliaksFavour; 260 | 261 | else if (actionID is FSH.SurfaceSlap && LocalPlayer.CurrentGp < 200) 262 | return FSH.ThaliaksFavour; 263 | 264 | else if (actionID is FSH.IdenticalCast && LocalPlayer.CurrentGp < 350) 265 | return FSH.ThaliaksFavour; 266 | 267 | else if (actionID is FSH.BaitedBreath && LocalPlayer.CurrentGp < 300) 268 | return FSH.ThaliaksFavour; 269 | 270 | else if (actionID is FSH.PrizeCatch && LocalPlayer.CurrentGp < 200) 271 | return FSH.ThaliaksFavour; 272 | 273 | } 274 | return actionID; 275 | } 276 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 277 | 278 | if (HasCondition(ConditionFlag.Fishing)) { 279 | 280 | if (actionID is FSH.Cast && IsEnabled(CustomComboPreset.FisherCastHookFeature)) 281 | return FSH.Hook; 282 | 283 | else if (actionID is FSH.DoubleHook && IsEnabled(CustomComboPreset.FisherCastMultiHookFeature21) && LocalPlayer.CurrentGp < 400) 284 | return FSH.Hook; 285 | 286 | else if (actionID is FSH.TripleHook && IsEnabled(CustomComboPreset.FisherCastMultiHookFeature32) && LocalPlayer.CurrentGp < 700) 287 | return IsEnabled(CustomComboPreset.FisherCastMultiHookFeature21) && LocalPlayer.CurrentGp < 400 ? FSH.Hook : FSH.DoubleHook; 288 | 289 | } 290 | else if (HasCondition(ConditionFlag.Diving)) { 291 | 292 | if (actionID is FSH.Cast && IsEnabled(CustomComboPreset.FisherCastGigFeature)) 293 | return FSH.Gig; 294 | 295 | else if (actionID is FSH.SurfaceSlap && IsEnabled(CustomComboPreset.FisherSurfaceTradeFeature)) 296 | return thaliak(FSH.VeteranTrade, level); 297 | 298 | else if (actionID is FSH.PrizeCatch && IsEnabled(CustomComboPreset.FisherPrizeBountyFeature)) 299 | return thaliak(FSH.NaturesBounty, level); 300 | 301 | else if (actionID is FSH.Snagging && IsEnabled(CustomComboPreset.FisherSnaggingSalvageFeature)) 302 | return FSH.Salvage; 303 | 304 | else if (actionID is FSH.IdenticalCast && IsEnabled(CustomComboPreset.FisherIdenticalSightFeature)) 305 | return FSH.VitalSight; 306 | 307 | else if (actionID is FSH.MakeshiftBait && IsEnabled(CustomComboPreset.FisherMakeshiftBreathFeature)) 308 | return thaliak(FSH.BaitedBreath, level); 309 | 310 | else if (actionID is FSH.Chum && IsEnabled(CustomComboPreset.FisherElectricChumFeature)) 311 | return FSH.ElectricCurrent; 312 | 313 | } 314 | else { 315 | 316 | if (actionID is FSH.Hook && IsEnabled(CustomComboPreset.FisherCastHookFeature)) 317 | return FSH.Cast; 318 | 319 | else if (actionID is FSH.TripleHook && IsEnabled(CustomComboPreset.FisherCastTripleHookFeature)) 320 | return FSH.Cast; 321 | 322 | else if (actionID is FSH.DoubleHook && IsEnabled(CustomComboPreset.FisherCastDoubleHookFeature)) 323 | return FSH.Cast; 324 | 325 | } 326 | 327 | return thaliak(actionID, level); 328 | } 329 | } 330 | 331 | internal class MeticulousPrimingTouchFeature: CustomCombo { 332 | 333 | public override CustomComboPreset Preset { get; } = CustomComboPreset.MeticulousPrimingTouchFeature; 334 | 335 | public override uint[] ActionIDs => [MIN.MeticulousProspector, BTN.MeticulousWoodsman]; 336 | 337 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 338 | 339 | if (level >= DOL.Levels.PrimingTouch) { 340 | if (LocalPlayer.CurrentGp >= 400) { 341 | if (SelfHasEffect(DOL.Buffs.CollectorsStandard) || SelfHasEffect(DOL.Buffs.CollectorsHighStandard)) { 342 | if (!SelfHasEffect(DOL.Buffs.PrimingTouch)) 343 | return IsJob(DOL.MinID) ? MIN.PrimingTouch : BTN.PrimingTouch; 344 | } 345 | } 346 | } 347 | 348 | return actionID; 349 | 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /XIVComboVX/Combos/DNC.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | using Dalamud.Game.ClientState.Statuses; 3 | 4 | namespace VariableVixen.XIVComboVX.Combos; 5 | 6 | internal static class DNC { 7 | public const byte JobID = 38; 8 | 9 | public const uint 10 | // Single Target 11 | Cascade = 15989, 12 | Fountain = 15990, 13 | ReverseCascade = 15991, 14 | Fountainfall = 15992, 15 | // AoE 16 | Windmill = 15993, 17 | Bladeshower = 15994, 18 | RisingWindmill = 15995, 19 | Bloodshower = 15996, 20 | // Dancing 21 | StandardStep = 15997, 22 | TechnicalStep = 15998, 23 | Tillana = 25790, 24 | // Fans 25 | FanDance1 = 16007, 26 | FanDance2 = 16008, 27 | FanDance3 = 16009, 28 | FanDance4 = 25791, 29 | // Other 30 | CuringWaltz = 16015, 31 | SecondWind = 7541, 32 | SaberDance = 16005, 33 | EnAvant = 16010, 34 | Devilment = 16011, 35 | Flourish = 16013, 36 | Improvisation = 16014, 37 | StarfallDance = 25792, 38 | LastDance = 36983, 39 | DanceOfTheDawn = 36985, 40 | FinishingMove = 36984; 41 | 42 | public static class Buffs { 43 | public const ushort 44 | ShieldSamba = 1826, 45 | FlourishingSymmetry = 3017, 46 | FlourishingFlow = 3018, 47 | FlourishingFinish = 2698, 48 | FlourishingStarfall = 2700, 49 | SilkenSymmetry = 2693, 50 | SilkenFlow = 2694, 51 | StandardStep = 1818, 52 | TechnicalStep = 1819, 53 | ThreefoldFanDance = 1820, 54 | FourfoldFanDance = 2699, 55 | LastDanceReady = 3867, 56 | DanceOfTheDawnReady = 3869, 57 | FinishingMoveReady = 3868; 58 | } 59 | 60 | public static class Debuffs { 61 | // public const ushort placeholder = 0; 62 | } 63 | 64 | public static class Levels { 65 | public const byte 66 | Fountain = 2, 67 | Windmill = 15, 68 | StandardStep = 15, 69 | ReverseCascade = 20, 70 | Bladeshower = 25, 71 | FanDance1 = 30, 72 | RisingWindmill = 35, 73 | Fountainfall = 40, 74 | Bloodshower = 45, 75 | FanDance2 = 50, 76 | CuringWaltz = 52, 77 | Devilment = 62, 78 | FanDance3 = 66, 79 | TechnicalStep = 70, 80 | Flourish = 72, 81 | SaberDance = 76, // [sic] - should be Sabre but america 82 | Tillana = 82, 83 | FanDance4 = 86, 84 | StarfallDance = 90; 85 | 86 | } 87 | } 88 | 89 | internal abstract class DancerCombo: CustomCombo { 90 | #pragma warning disable IDE0045 // Convert to conditional expression - helper function readability 91 | 92 | protected static bool DancerSmartDancing(out uint nextStep) { 93 | if (dancerNextDanceStep is null) { 94 | DNCGauge gauge = GetJobGauge(); 95 | 96 | if (gauge.IsDancing) { 97 | bool fast = SelfHasEffect(DNC.Buffs.StandardStep); 98 | int max = fast ? 2 : 4; 99 | 100 | dancerNextDanceStep = gauge.CompletedSteps >= max 101 | ? OriginalHook(fast ? DNC.StandardStep : DNC.TechnicalStep) 102 | : gauge.NextStep; 103 | } 104 | else { 105 | dancerNextDanceStep = 0; 106 | } 107 | } 108 | 109 | nextStep = dancerNextDanceStep.Value; 110 | return nextStep > 0; 111 | } 112 | 113 | #pragma warning restore IDE0045 // Convert to conditional expression 114 | } 115 | 116 | internal class DancerDanceComboCompatibility: DancerCombo { 117 | public override CustomComboPreset Preset => CustomComboPreset.DancerDanceComboCompatibility; 118 | public override uint[] ActionIDs => [ 119 | Service.Configuration.DancerEmboiteRedActionID, 120 | Service.Configuration.DancerEntrechatBlueActionID, 121 | Service.Configuration.DancerJeteGreenActionID, 122 | Service.Configuration.DancerPirouetteYellowActionID, 123 | ]; 124 | 125 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 126 | if (level >= DNC.Levels.StandardStep && GetJobGauge().IsDancing) { 127 | uint[] actionIDs = this.ActionIDs; 128 | 129 | if (actionID == actionIDs[0]) 130 | return OriginalHook(DNC.Cascade); 131 | 132 | if (actionID == actionIDs[1]) 133 | return OriginalHook(DNC.Fountain); 134 | 135 | if (actionID == actionIDs[2]) 136 | return OriginalHook(DNC.ReverseCascade); 137 | 138 | if (actionID == actionIDs[3]) 139 | return OriginalHook(DNC.Fountainfall); 140 | 141 | } 142 | 143 | return actionID; 144 | } 145 | } 146 | 147 | internal class DancerFanDanceCombos: DancerCombo { 148 | public override CustomComboPreset Preset => CustomComboPreset.DncAny; 149 | public override uint[] ActionIDs { get; } = [DNC.FanDance1, DNC.FanDance2]; 150 | 151 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 152 | bool can4 = level >= DNC.Levels.FanDance4 && SelfHasEffect(DNC.Buffs.FourfoldFanDance); 153 | bool can3 = level >= DNC.Levels.FanDance3 && SelfHasEffect(DNC.Buffs.ThreefoldFanDance); 154 | 155 | if (actionID is DNC.FanDance1) { 156 | if (IsEnabled(CustomComboPreset.DancerFanDance14Combo) && can4) 157 | return DNC.FanDance4; 158 | if (IsEnabled(CustomComboPreset.DancerFanDance13Combo) && can3) 159 | return DNC.FanDance3; 160 | } 161 | else if (actionID is DNC.FanDance2) { 162 | if (IsEnabled(CustomComboPreset.DancerFanDance24Combo) && can4) 163 | return DNC.FanDance4; 164 | if (IsEnabled(CustomComboPreset.DancerFanDance23Combo) && can3) 165 | return DNC.FanDance3; 166 | } 167 | 168 | return actionID; 169 | } 170 | } 171 | 172 | internal class DancerDanceStepCombo: DancerCombo { 173 | public override CustomComboPreset Preset => CustomComboPreset.DancerDanceStepCombo; 174 | public override uint[] ActionIDs { get; } = [DNC.StandardStep, DNC.TechnicalStep]; 175 | 176 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 177 | if (level >= DNC.Levels.StandardStep && DancerSmartDancing(out uint danceStep)) 178 | return danceStep; 179 | 180 | if (level >= DNC.Levels.TechnicalStep) { 181 | 182 | if (SelfHasEffect(DNC.Buffs.FlourishingFinish) && GetCooldown(DNC.StandardStep).CooldownRemaining >= 3) // tillana does esprit +50, check for that? 183 | return DNC.Tillana; 184 | 185 | if (IsEnabled(CustomComboPreset.DancerDanceStepComboSmartStandard) && CanUse(DNC.TechnicalStep) && GetCooldown(DNC.StandardStep).CooldownRemaining > 3) 186 | return DNC.TechnicalStep; 187 | 188 | if (IsEnabled(CustomComboPreset.DancerDanceStepComboSmartTechnical) && !CanUse(DNC.TechnicalStep) && CanUse(DNC.StandardStep)) 189 | return DNC.StandardStep; 190 | 191 | } 192 | else if (IsEnabled(CustomComboPreset.DancerDanceStepComboSmartTechnical)) { 193 | return DNC.StandardStep; 194 | } 195 | 196 | return actionID; 197 | } 198 | } 199 | 200 | internal class DancerFlourishFeature: DancerCombo { 201 | public override CustomComboPreset Preset => CustomComboPreset.DancerFlourishFeature; 202 | public override uint[] ActionIDs { get; } = [DNC.Flourish]; 203 | 204 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 205 | 206 | if (level >= DNC.Levels.FanDance4 && SelfHasEffect(DNC.Buffs.FourfoldFanDance)) 207 | return DNC.FanDance4; 208 | 209 | return actionID; 210 | } 211 | } 212 | 213 | internal class DancerSingleTargetMultibutton: DancerCombo { 214 | public override CustomComboPreset Preset => CustomComboPreset.DancerSingleTargetMultibutton; 215 | public override uint[] ActionIDs { get; } = [DNC.Cascade]; 216 | 217 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 218 | if (IsEnabled(CustomComboPreset.DancerSmartDanceFeature)) { 219 | if (level >= DNC.Levels.StandardStep && DancerSmartDancing(out uint danceStep)) 220 | return danceStep; 221 | } 222 | 223 | DNCGauge gauge = GetJobGauge(); 224 | 225 | if (CanWeave(actionID)) { 226 | 227 | if (IsEnabled(CustomComboPreset.DancerSingleTargetFanDanceWeave)) { 228 | 229 | if (level >= DNC.Levels.FanDance1) { 230 | if (level >= DNC.Levels.FanDance3 && SelfHasEffect(DNC.Buffs.ThreefoldFanDance)) 231 | return DNC.FanDance3; 232 | else if (gauge.Feathers > 0) 233 | return DNC.FanDance1; 234 | } 235 | 236 | if (IsEnabled(CustomComboPreset.DancerSingleTargetFanDanceFallback)) { 237 | if (level >= DNC.Levels.FanDance2) { 238 | if (level >= DNC.Levels.FanDance4 && SelfHasEffect(DNC.Buffs.FourfoldFanDance)) 239 | return DNC.FanDance4; 240 | else if (gauge.Feathers > 0) 241 | return DNC.FanDance2; 242 | } 243 | } 244 | 245 | } 246 | 247 | if (IsEnabled(CustomComboPreset.DancerSingleTargetFlourishWeave)) { 248 | if (level >= DNC.Levels.Flourish && InCombat && CanUse(DNC.Flourish)) { 249 | if (!( 250 | SelfHasEffect(DNC.Buffs.FlourishingSymmetry) 251 | || SelfHasEffect(DNC.Buffs.FlourishingFlow) 252 | || SelfHasEffect(DNC.Buffs.ThreefoldFanDance) 253 | || SelfHasEffect(DNC.Buffs.FourfoldFanDance) 254 | )) { 255 | return DNC.Flourish; 256 | } 257 | } 258 | } 259 | 260 | if (IsEnabled(CustomComboPreset.DancerSingleTargetDevilmentWeave) && level >= DNC.Levels.Devilment) { 261 | if (CanUse(DNC.Devilment)) 262 | return DNC.Devilment; 263 | } 264 | 265 | } 266 | 267 | if (IsEnabled(CustomComboPreset.DancerSingleTargetStarfall) && level >= DNC.Levels.StarfallDance) { 268 | IStatus? starfall = SelfFindEffect(DNC.Buffs.FlourishingStarfall); 269 | if (starfall is not null && starfall.RemainingTime <= Service.Configuration.DancerSingleTargetStarfallBuffThreshold) 270 | return DNC.StarfallDance; 271 | } 272 | 273 | if (IsEnabled(CustomComboPreset.DancerSingleTargetGaugeSpender) && level >= DNC.Levels.SaberDance && gauge.Esprit >= Service.Configuration.DancerSingleTargetGaugeThreshold) 274 | return OriginalHook(DNC.SaberDance); 275 | 276 | if (level >= DNC.Levels.Fountainfall && (SelfHasEffect(DNC.Buffs.FlourishingFlow) || SelfHasEffect(DNC.Buffs.SilkenFlow))) 277 | return DNC.Fountainfall; 278 | 279 | if (level >= DNC.Levels.ReverseCascade && (SelfHasEffect(DNC.Buffs.FlourishingSymmetry) || SelfHasEffect(DNC.Buffs.SilkenSymmetry))) 280 | return DNC.ReverseCascade; 281 | 282 | if (lastComboMove is DNC.Cascade && level >= DNC.Levels.Fountain) 283 | return DNC.Fountain; 284 | 285 | return actionID; 286 | } 287 | } 288 | 289 | internal class DancerAoeMultibutton: DancerCombo { 290 | public override CustomComboPreset Preset => CustomComboPreset.DancerAoeMultibutton; 291 | public override uint[] ActionIDs { get; } = [DNC.Windmill]; 292 | 293 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 294 | if (IsEnabled(CustomComboPreset.DancerSmartDanceFeature)) { 295 | if (level >= DNC.Levels.StandardStep && DancerSmartDancing(out uint danceStep)) 296 | return danceStep; 297 | } 298 | 299 | DNCGauge gauge = GetJobGauge(); 300 | 301 | if (CanWeave(actionID)) { 302 | 303 | if (IsEnabled(CustomComboPreset.DancerAoeFanDanceWeave)) { 304 | 305 | if (level >= DNC.Levels.FanDance2) { 306 | if (level >= DNC.Levels.FanDance4 && SelfHasEffect(DNC.Buffs.FourfoldFanDance)) 307 | return DNC.FanDance4; 308 | else if (gauge.Feathers > 0) 309 | return DNC.FanDance2; 310 | } 311 | 312 | if (IsEnabled(CustomComboPreset.DancerAoeFanDanceFallback)) { 313 | if (level >= DNC.Levels.FanDance1) { 314 | if (level >= DNC.Levels.FanDance3 && SelfHasEffect(DNC.Buffs.ThreefoldFanDance)) 315 | return DNC.FanDance3; 316 | else if (gauge.Feathers > 0) 317 | return DNC.FanDance1; 318 | } 319 | } 320 | 321 | } 322 | 323 | if (IsEnabled(CustomComboPreset.DancerAoeFlourishWeave)) { 324 | if (level >= DNC.Levels.Flourish && InCombat && CanUse(DNC.Flourish)) { 325 | if (!( 326 | SelfHasEffect(DNC.Buffs.FlourishingSymmetry) 327 | || SelfHasEffect(DNC.Buffs.FlourishingFlow) 328 | || SelfHasEffect(DNC.Buffs.ThreefoldFanDance) 329 | || SelfHasEffect(DNC.Buffs.FourfoldFanDance) 330 | )) { 331 | return DNC.Flourish; 332 | } 333 | } 334 | } 335 | 336 | } 337 | 338 | if (IsEnabled(CustomComboPreset.DancerAoeStarfall) && level >= DNC.Levels.StarfallDance) { 339 | IStatus? starfall = SelfFindEffect(DNC.Buffs.FlourishingStarfall); 340 | if (starfall is not null && starfall.RemainingTime <= Service.Configuration.DancerAoeStarfallBuffThreshold) 341 | return DNC.StarfallDance; 342 | } 343 | 344 | if (IsEnabled(CustomComboPreset.DancerAoeGaugeSpender) && level >= DNC.Levels.SaberDance && gauge.Esprit >= Service.Configuration.DancerAoeGaugeThreshold) 345 | return OriginalHook(DNC.SaberDance); 346 | 347 | if (level >= DNC.Levels.Bloodshower && (SelfHasEffect(DNC.Buffs.FlourishingFlow) || SelfHasEffect(DNC.Buffs.SilkenFlow))) 348 | return DNC.Bloodshower; 349 | 350 | if (level >= DNC.Levels.RisingWindmill && (SelfHasEffect(DNC.Buffs.FlourishingSymmetry) || SelfHasEffect(DNC.Buffs.SilkenSymmetry))) 351 | return DNC.RisingWindmill; 352 | 353 | if (lastComboMove is DNC.Windmill && level >= DNC.Levels.Bladeshower) 354 | return DNC.Bladeshower; 355 | 356 | return actionID; 357 | } 358 | } 359 | 360 | internal class DancerDevilmentFeature: DancerCombo { 361 | public override CustomComboPreset Preset { get; } = CustomComboPreset.DancerDevilmentFeature; 362 | public override uint[] ActionIDs { get; } = [DNC.Devilment]; 363 | 364 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) { 365 | 366 | if (level >= DNC.Levels.StarfallDance && SelfHasEffect(DNC.Buffs.FlourishingStarfall)) 367 | return DNC.StarfallDance; 368 | 369 | return actionID; 370 | } 371 | } 372 | 373 | internal class DancerCuringWindFeature: DancerCombo { 374 | public override CustomComboPreset Preset { get; } = CustomComboPreset.DncAny; 375 | public override uint[] ActionIDs { get; } = [DNC.CuringWaltz]; 376 | 377 | protected override uint Invoke(uint actionID, uint lastComboActionId, float comboTime, byte level) { 378 | 379 | if (IsEnabled(CustomComboPreset.DancerCuringWaltzLevelSync)) { 380 | if (level < DNC.Levels.CuringWaltz) 381 | return DNC.SecondWind; 382 | } 383 | 384 | if (IsEnabled(CustomComboPreset.DancerCuringWaltzCooldownSwap)) { 385 | if (!CanUse(DNC.CuringWaltz)) 386 | return DNC.SecondWind; 387 | } 388 | 389 | return actionID; 390 | } 391 | } 392 | --------------------------------------------------------------------------------