├── .gitignore ├── res ├── icon.png ├── eno_swap.gif ├── souleater_combo.gif └── hypercharge_heat_blast.gif ├── .gitmodules ├── .editorconfig ├── XIVComboExpanded ├── Attributes │ ├── SecretCustomComboAttribute.cs │ ├── ParentComboAttribute.cs │ ├── ConflictingCombosAttribute.cs │ └── CustomComboInfoAttribute.cs ├── XIVComboExpanded.json ├── Combos │ ├── DOH.cs │ ├── BLU.cs │ ├── ADV.cs │ ├── DOL.cs │ ├── WHM.cs │ ├── AST.cs │ ├── SCH.cs │ ├── DRK.cs │ ├── GNB.cs │ ├── SGE.cs │ ├── MCH.cs │ ├── SMN.cs │ ├── WAR.cs │ ├── DNC.cs │ ├── NIN.cs │ ├── DRG.cs │ ├── SAM.cs │ └── MNK.cs ├── PluginAddressResolver.cs ├── GlobalSuppressions.cs ├── XIVComboExpanded.csproj ├── Service.cs ├── IconReplacer.cs ├── CooldownData.cs ├── PluginConfiguration.cs ├── CustomComboCache.cs ├── Interface │ └── ConfigWindow.cs └── XIVComboExpandedPlugin.cs ├── stylecop.json ├── README.md ├── XIVComboExpanded.sln ├── .github └── workflows │ └── build.yml └── statement.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | bin/ 3 | obj/ 4 | /lib/ 5 | -------------------------------------------------------------------------------- /res/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyvorld/XIVComboPlugin/master/res/icon.png -------------------------------------------------------------------------------- /res/eno_swap.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyvorld/XIVComboPlugin/master/res/eno_swap.gif -------------------------------------------------------------------------------- /res/souleater_combo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyvorld/XIVComboPlugin/master/res/souleater_combo.gif -------------------------------------------------------------------------------- /res/hypercharge_heat_blast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyvorld/XIVComboPlugin/master/res/hypercharge_heat_blast.gif -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/FFXIVClientStructs"] 2 | path = lib/FFXIVClientStructs 3 | url = https://github.com/aers/FFXIVClientStructs.git 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{cs,vb}] 2 | 3 | # IDE0003: Remove qualification 4 | dotnet_style_qualification_for_event =true:silent 5 | dotnet_style_qualification_for_field=true:silent 6 | dotnet_style_qualification_for_property=true:silent 7 | dotnet_style_qualification_for_method=true:silent 8 | dotnet_diagnostic.CA1822.severity=silent 9 | -------------------------------------------------------------------------------- /XIVComboExpanded/Attributes/SecretCustomComboAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XIVComboExpandedPlugin.Attributes; 4 | 5 | /// 6 | /// Attribute designating secret combos. 7 | /// 8 | [AttributeUsage(AttributeTargets.Field)] 9 | internal class SecretCustomComboAttribute : Attribute 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "orderingRules": { 5 | "systemUsingDirectivesFirst": true, 6 | "usingDirectivesPlacement": "outsideNamespace", 7 | "blankLinesBetweenUsingGroups": "require" 8 | }, 9 | "maintainabilityRules": { 10 | "topLevelTypes": [ "class", "interface", "struct", "enum" ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /XIVComboExpanded/XIVComboExpanded.json: -------------------------------------------------------------------------------- 1 | { 2 | "Author": "attick, daemitus, Khayle", 3 | "Name": "XIV Combo Expanded", 4 | "Punchline": "Condenses combos and mutually exclusive abilities onto a single button.", 5 | "Description": "This plugin condenses combos and mutually exclusive abilities onto a single button. For example, a 1-2-3 combo would become 1-1-1.", 6 | "RepoUrl": "https://github.com/daemitus/XIVComboPlugin", 7 | "IconUrl": "https://raw.githubusercontent.com/daemitus/XIVComboPlugin/master/res/icon.png", 8 | "ImageUrls": [], 9 | "DalamudApiLevel": 9 10 | } -------------------------------------------------------------------------------- /XIVComboExpanded/Combos/DOH.cs: -------------------------------------------------------------------------------- 1 | namespace XIVComboExpandedPlugin.Combos; 2 | 3 | internal static class DOH 4 | { 5 | public const byte ClassID = 0; 6 | public const byte JobID = 50; 7 | 8 | public const uint 9 | Placeholder = 0; 10 | 11 | public static class Buffs 12 | { 13 | public const ushort 14 | Placeholder = 0; 15 | } 16 | 17 | public static class Debuffs 18 | { 19 | public const ushort 20 | Placeholder = 0; 21 | } 22 | 23 | public static class Levels 24 | { 25 | public const byte 26 | Placeholder = 0; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /XIVComboExpanded/Combos/BLU.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | 3 | namespace XIVComboExpandedPlugin.Combos; 4 | 5 | internal static class BLU 6 | { 7 | public const byte JobID = 36; 8 | 9 | public const uint 10 | AngelWhisper = 18317; 11 | 12 | public static class Buffs 13 | { 14 | public const ushort 15 | Placeholder = 0; 16 | } 17 | 18 | public static class Debuffs 19 | { 20 | public const ushort 21 | Placeholder = 0; 22 | } 23 | 24 | public static class Levels 25 | { 26 | public const byte 27 | AngelWhisper = 1; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /XIVComboExpanded/Attributes/ParentComboAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XIVComboExpandedPlugin.Attributes; 4 | 5 | /// 6 | /// Attribute documenting required combo relationships. 7 | /// 8 | [AttributeUsage(AttributeTargets.Field)] 9 | internal class ParentComboAttribute : Attribute 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Presets that conflict with the given combo. 15 | internal ParentComboAttribute(CustomComboPreset parentPreset) 16 | { 17 | this.ParentPreset = parentPreset; 18 | } 19 | 20 | /// 21 | /// Gets the display name. 22 | /// 23 | public CustomComboPreset ParentPreset { get; } 24 | } 25 | -------------------------------------------------------------------------------- /XIVComboExpanded/Attributes/ConflictingCombosAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XIVComboExpandedPlugin.Attributes; 4 | 5 | /// 6 | /// Attribute documenting conflicting presets for each combo. 7 | /// 8 | [AttributeUsage(AttributeTargets.Field)] 9 | internal class ConflictingCombosAttribute : Attribute 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Presets that conflict with the given combo. 15 | internal ConflictingCombosAttribute(params CustomComboPreset[] conflictingPresets) 16 | { 17 | this.ConflictingPresets = conflictingPresets; 18 | } 19 | 20 | /// 21 | /// Gets the display name. 22 | /// 23 | public CustomComboPreset[] ConflictingPresets { get; } 24 | } 25 | -------------------------------------------------------------------------------- /XIVComboExpanded/PluginAddressResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Dalamud.Game; 4 | using Dalamud.Logging; 5 | 6 | namespace XIVComboExpandedPlugin; 7 | 8 | /// 9 | /// Plugin address resolver. 10 | /// 11 | internal class PluginAddressResolver : BaseAddressResolver 12 | { 13 | /// 14 | /// Gets the address of the member ComboTimer. 15 | /// 16 | public IntPtr ComboTimer { get; private set; } 17 | 18 | /// 19 | /// Gets the address of the member LastComboMove. 20 | /// 21 | public IntPtr LastComboMove => this.ComboTimer + 0x4; 22 | 23 | /// 24 | /// Gets the address of fpGetAdjustedActionId. 25 | /// 26 | public IntPtr GetAdjustedActionId { get; private set; } 27 | 28 | /// 29 | /// Gets the address of fpIsIconReplacable. 30 | /// 31 | public IntPtr IsActionIdReplaceable { get; private set; } 32 | 33 | /// 34 | protected override void Setup64Bit(ISigScanner scanner) 35 | { 36 | this.ComboTimer = scanner.GetStaticAddressFromSig("F3 0F 11 05 ?? ?? ?? ?? 48 83 C7 08"); 37 | 38 | this.GetAdjustedActionId = scanner.ScanText("E8 ?? ?? ?? ?? 89 03 8B 03"); // Client::Game::ActionManager.GetAdjustedActionId 39 | 40 | this.IsActionIdReplaceable = scanner.ScanText("E8 ?? ?? ?? ?? 84 C0 74 4C 8B D3"); 41 | 42 | PluginLog.Verbose("===== X I V C O M B O ====="); 43 | PluginLog.Verbose($"{nameof(this.GetAdjustedActionId)} 0x{this.GetAdjustedActionId:X}"); 44 | PluginLog.Verbose($"{nameof(this.IsActionIdReplaceable)} 0x{this.IsActionIdReplaceable:X}"); 45 | PluginLog.Verbose($"{nameof(this.ComboTimer)} 0x{this.ComboTimer:X}"); 46 | PluginLog.Verbose($"{nameof(this.LastComboMove)} 0x{this.LastComboMove:X}"); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XIVComboPlugin 2 | 3 | [![Build](https://github.com/MKhayle/XIVComboPlugin/actions/workflows/build.yml/badge.svg)](https://github.com/daemitus/XIVComboPlugin/actions/workflows/build.yml) 4 | 5 | This plugin condenses combos and mutually exclusive abilities onto a single button. Thanks to Meli for the initial start, and obviously goat for making any of this possible. 6 | 7 | ## About 8 | 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. 9 | 10 | 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. 11 | 12 | ## Installation 13 | * Type `/xlplugins` in-game to access the plugin installer and updater. Any releases on this github page have been removed to facilitate proper installation going forward. 14 | ## In-game usage 15 | * Type `/pcombo` to pull up a GUI for editing active combo replacements. 16 | * Drag the named ability from your ability list onto your hotbar to use. 17 | * 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. 18 | * The description associated with each combo should be enough to tell you which ability needs to be placed on the hotbar. 19 | ### Examples 20 | ![](https://github.com/MKhayle/xivcomboplugin/raw/master/res/souleater_combo.gif) 21 | ![](https://github.com/MKhayle/xivcomboplugin/raw/master/res/hypercharge_heat_blast.gif) 22 | ![](https://github.com/MKhayle/xivcomboplugin/raw/master/res/eno_swap.gif) 23 | 24 | ## Known Issues 25 | * None, for now! 26 | 27 | ## Full list of supported combos 28 | There used to be a list here, but it got annoying to update. You should install it and check out what's available! 29 | 30 | Place `https://github.com/daemitus/MyDalamudPlugins/raw/master/pluginmaster.json` in your /xlsettings 3rd party repo list to access this plugin. 31 | -------------------------------------------------------------------------------- /XIVComboExpanded.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31423.177 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XIVComboExpanded", "XIVComboExpanded\XIVComboExpanded.csproj", "{619DF476-7225-4783-95A5-D29A8816EE66}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8948C02A-914E-4A51-B1FE-2C608492EBF5}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | .gitignore = .gitignore 12 | README.md = README.md 13 | EndProjectSection 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Github Actions", "Github Actions", "{D3F73229-7C85-4017-B8C9-B7668022A4F0}" 16 | ProjectSection(SolutionItems) = preProject 17 | .github\workflows\build.yml = .github\workflows\build.yml 18 | EndProjectSection 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Debug|x64 = Debug|x64 24 | Release|Any CPU = Release|Any CPU 25 | Release|x64 = Release|x64 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {619DF476-7225-4783-95A5-D29A8816EE66}.Debug|Any CPU.ActiveCfg = Debug|x64 29 | {619DF476-7225-4783-95A5-D29A8816EE66}.Debug|Any CPU.Build.0 = Debug|x64 30 | {619DF476-7225-4783-95A5-D29A8816EE66}.Debug|x64.ActiveCfg = Debug|x64 31 | {619DF476-7225-4783-95A5-D29A8816EE66}.Debug|x64.Build.0 = Debug|x64 32 | {619DF476-7225-4783-95A5-D29A8816EE66}.Release|Any CPU.ActiveCfg = Release|x64 33 | {619DF476-7225-4783-95A5-D29A8816EE66}.Release|Any CPU.Build.0 = Release|x64 34 | {619DF476-7225-4783-95A5-D29A8816EE66}.Release|x64.ActiveCfg = Release|x64 35 | {619DF476-7225-4783-95A5-D29A8816EE66}.Release|x64.Build.0 = Release|x64 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {37674B88-2038-4C63-A979-84404391773A} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | # Put your personal access token in a repository secret named PAT for cross-repository access 4 | 5 | on: 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - master 10 | 11 | env: 12 | PUBLIC_NAME: XIVComboExpanded 13 | SOLUTION_NAME: XIVComboExpanded 14 | INTERNAL_NAME: XIVComboExpanded 15 | RELEASE_DIR: XIVComboExpanded\bin\Release\XIVComboExpanded 16 | PERSONAL_PLUGIN_REPO: daemitus/MyDalamudPlugins 17 | PR_PLUGIN_REPO: daemitus/DalamudPlugins 18 | DOTNET_CLI_TELEMETRY_OPTOUT: true 19 | 20 | jobs: 21 | build: 22 | runs-on: windows-2022 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v2 26 | with: 27 | submodules: recursive 28 | - name: Setup MSBuild 29 | uses: microsoft/setup-msbuild@v1.0.2 30 | - name: Download Dalamud 31 | run: | 32 | Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip 33 | Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev\" 34 | - name: Build 35 | run: | 36 | dotnet restore -r win ${{ env.SOLUTION_NAME }}.sln 37 | dotnet build --configuration Release 38 | - name: Test 39 | run: | 40 | dotnet test --no-restore --verbosity normal 41 | - uses: actions/upload-artifact@v2 42 | with: 43 | name: PluginRepoZip 44 | path: ${{ env.RELEASE_DIR }} 45 | if-no-files-found: error 46 | 47 | deploy: 48 | needs: build 49 | if: "contains(toJSON(github.event.commits.*.message), '[PUSH]')" 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v2 53 | with: 54 | repository: ${{ env.PERSONAL_PLUGIN_REPO }} 55 | token: ${{ secrets.PAT }} 56 | - uses: actions/download-artifact@v2 57 | with: 58 | name: PluginRepoZip 59 | path: plugins/${{ env.INTERNAL_NAME }} 60 | - uses: EndBug/add-and-commit@v7 61 | with: 62 | add: --all 63 | author_name: GitHub Action 64 | author_email: github-actions[bot]@users.noreply.github.com 65 | message: Update ${{ env.INTERNAL_NAME }} 66 | -------------------------------------------------------------------------------- /XIVComboExpanded/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 | // General 9 | [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "Preventing long lines", Scope = "module")] 10 | [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "I like regions", Scope = "module")] 11 | [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1123:Do not place regions within elements", Justification = "I like regions in elements too", Scope = "module")] 12 | [assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying", Scope = "module")] 13 | [assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better", Scope = "module")] 14 | [assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better", Scope = "module")] 15 | [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1127:Generic type constraints should be on their own line", Justification = "I like this better", Scope = "module")] 16 | [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't do those yet")] 17 | 18 | [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1602:Enumeration items should be documented", Justification = "Self explanatory.", Scope = "type", Target = "~T:XIVComboExpandedPlugin.CustomComboPreset")] 19 | [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Self explanatory.", Scope = "namespaceanddescendants", Target = "~N:XIVComboExpandedPlugin.Combos")] 20 | [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group jobs together.", Scope = "namespaceanddescendants", Target = "~N:XIVComboExpandedPlugin.Combos")] 21 | [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1132:Do not combine fields", Justification = "Not necessary.", Scope = "namespaceanddescendants", Target = "~N:XIVComboExpandedPlugin.Combos")] 22 | -------------------------------------------------------------------------------- /XIVComboExpanded/Combos/ADV.cs: -------------------------------------------------------------------------------- 1 | namespace XIVComboExpandedPlugin.Combos; 2 | 3 | internal static class ADV 4 | { 5 | public const byte ClassID = 0; 6 | public const byte JobID = 0; 7 | 8 | public const uint 9 | LucidDreaming = 1204, 10 | Swiftcast = 7561, 11 | AngelWhisper = 18317, 12 | VariantRaise2 = 29734; 13 | 14 | public static class Buffs 15 | { 16 | public const ushort 17 | Medicated = 49; 18 | } 19 | 20 | public static class Debuffs 21 | { 22 | public const ushort 23 | Placeholder = 0; 24 | } 25 | 26 | public static class Levels 27 | { 28 | public const byte 29 | Swiftcast = 18, 30 | VariantRaise2 = 90; 31 | } 32 | } 33 | 34 | internal class SwiftRaiseFeature : CustomCombo 35 | { 36 | protected internal override CustomComboPreset Preset => CustomComboPreset.AdvSwiftcastFeature; 37 | 38 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 39 | { 40 | if ((actionID == AST.Ascend && level >= AST.Levels.Ascend) || 41 | (actionID == SCH.Resurrection && level >= SCH.Levels.Resurrection) || 42 | (actionID == SGE.Egeiro && level >= SGE.Levels.Egeiro) || 43 | (actionID == WHM.Raise && level >= WHM.Levels.Raise) || 44 | (actionID == RDM.Verraise && level >= RDM.Levels.Verraise && !HasEffect(RDM.Buffs.Dualcast)) || 45 | (actionID == BLU.AngelWhisper && level >= BLU.Levels.AngelWhisper)) 46 | { 47 | if (level >= ADV.Levels.Swiftcast && IsOffCooldown(ADV.Swiftcast)) 48 | return ADV.Swiftcast; 49 | } 50 | 51 | return actionID; 52 | } 53 | } 54 | 55 | internal class VariantRaiseFeature : CustomCombo 56 | { 57 | protected internal override CustomComboPreset Preset => CustomComboPreset.AdvVariantRaiseFeature; 58 | 59 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 60 | { 61 | if ((actionID == AST.Ascend && level >= AST.Levels.Ascend) || 62 | (actionID == SCH.Resurrection && level >= SCH.Levels.Resurrection) || 63 | (actionID == SGE.Egeiro && level >= SGE.Levels.Egeiro) || 64 | (actionID == WHM.Raise && level >= WHM.Levels.Raise) || 65 | (actionID == RDM.Verraise && level >= RDM.Levels.Verraise && !HasEffect(RDM.Buffs.Dualcast)) || 66 | (actionID == BLU.AngelWhisper && level >= BLU.Levels.AngelWhisper)) 67 | { 68 | // Per Splatoon: 69 | // 1069: solo 70 | // 1075: group 71 | // 1076: savage 72 | if (level >= ADV.Levels.VariantRaise2 && CurrentTerritory == 1075u) 73 | return ADV.VariantRaise2; 74 | } 75 | 76 | return actionID; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /XIVComboExpanded/Attributes/CustomComboInfoAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | using XIVComboExpandedPlugin.Combos; 5 | 6 | namespace XIVComboExpandedPlugin.Attributes; 7 | 8 | /// 9 | /// Attribute documenting additional information for each combo. 10 | /// 11 | [AttributeUsage(AttributeTargets.Field)] 12 | internal class CustomComboInfoAttribute : Attribute 13 | { 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// Display name. 18 | /// Combo description. 19 | /// Associated job ID. 20 | /// Display order. 21 | internal CustomComboInfoAttribute(string fancyName, string description, byte jobID, [CallerLineNumber] int order = 0) 22 | { 23 | this.FancyName = fancyName; 24 | this.Description = description; 25 | this.JobID = jobID; 26 | this.Order = order; 27 | } 28 | 29 | /// 30 | /// Gets the display name. 31 | /// 32 | public string FancyName { get; } 33 | 34 | /// 35 | /// Gets the description. 36 | /// 37 | public string Description { get; } 38 | 39 | /// 40 | /// Gets the job ID. 41 | /// 42 | public byte JobID { get; } 43 | 44 | /// 45 | /// Gets the display order. 46 | /// 47 | public int Order { get; } 48 | 49 | /// 50 | /// Gets the job name. 51 | /// 52 | public string JobName => JobIDToName(this.JobID); 53 | 54 | private static string JobIDToName(byte key) 55 | { 56 | return key switch 57 | { 58 | 0 => "Adventurer", 59 | 1 => "Gladiator", 60 | 2 => "Pugilist", 61 | 3 => "Marauder", 62 | 4 => "Lancer", 63 | 5 => "Archer", 64 | 6 => "Conjurer", 65 | 7 => "Thaumaturge", 66 | 8 => "Carpenter", 67 | 9 => "Blacksmith", 68 | 10 => "Armorer", 69 | 11 => "Goldsmith", 70 | 12 => "Leatherworker", 71 | 13 => "Weaver", 72 | 14 => "Alchemist", 73 | 15 => "Culinarian", 74 | 16 => "Miner", 75 | 17 => "Botanist", 76 | 18 => "Fisher", 77 | 19 => "Paladin", 78 | 20 => "Monk", 79 | 21 => "Warrior", 80 | 22 => "Dragoon", 81 | 23 => "Bard", 82 | 24 => "White Mage", 83 | 25 => "Black Mage", 84 | 26 => "Arcanist", 85 | 27 => "Summoner", 86 | 28 => "Scholar", 87 | 29 => "Rogue", 88 | 30 => "Ninja", 89 | 31 => "Machinist", 90 | 32 => "Dark Knight", 91 | 33 => "Astrologian", 92 | 34 => "Samurai", 93 | 35 => "Red Mage", 94 | 36 => "Blue Mage", 95 | 37 => "Gunbreaker", 96 | 38 => "Dancer", 97 | 39 => "Reaper", 98 | 40 => "Sage", 99 | DOH.JobID => "Disciples of the Hand", 100 | DOL.JobID => "Disciples of the Land", 101 | _ => "Unknown", 102 | }; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /XIVComboExpanded/XIVComboExpanded.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | attick, daemitus 5 | - 6 | 1.4.0.24 7 | This plugin condenses combos and mutually exclusive abilities onto a single button. 8 | Copyleft attick 2020 baybeeee 9 | https://github.com/daemitus/XIVComboPlugin 10 | 11 | 12 | 13 | net7.0-windows 14 | x64 15 | enable 16 | latest 17 | true 18 | false 19 | true 20 | false 21 | bin\$(Configuration)\ 22 | CS1591 23 | 24 | 25 | 26 | 27 | true 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | $(appdata)\XIVLauncher\addon\Hooks\dev\ 36 | 37 | 38 | 39 | 40 | $(AssemblySearchPaths); 41 | $(DalamudLibPath); 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | all 54 | runtime; build; native; contentfiles; analyzers; buildtransitive 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /XIVComboExpanded/Service.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Data; 2 | using Dalamud.Game; 3 | using Dalamud.Game.ClientState; 4 | using Dalamud.Game.ClientState.Buddy; 5 | using Dalamud.Game.ClientState.Conditions; 6 | using Dalamud.Game.ClientState.JobGauge; 7 | using Dalamud.Game.ClientState.Objects; 8 | using Dalamud.Game.Command; 9 | using Dalamud.Game.Gui; 10 | using Dalamud.IoC; 11 | using Dalamud.Plugin; 12 | using Dalamud.Plugin.Services; 13 | 14 | namespace XIVComboExpandedPlugin; 15 | 16 | /// 17 | /// Dalamud and plugin services. 18 | /// 19 | internal class Service 20 | { 21 | /// 22 | /// Gets or sets the plugin configuration. 23 | /// 24 | internal static PluginConfiguration Configuration { get; set; } = null!; 25 | 26 | /// 27 | /// Gets or sets the plugin caching mechanism. 28 | /// 29 | internal static CustomComboCache ComboCache { get; set; } = null!; 30 | 31 | /// 32 | /// Gets or sets the plugin icon replacer. 33 | /// 34 | internal static IconReplacer IconReplacer { get; set; } = null!; 35 | 36 | /// 37 | /// Gets or sets the plugin address resolver. 38 | /// 39 | internal static PluginAddressResolver Address { get; set; } = null!; 40 | 41 | /// 42 | /// Gets the Dalamud plugin interface. 43 | /// 44 | [PluginService] 45 | internal static DalamudPluginInterface Interface { get; private set; } = null!; 46 | 47 | /// 48 | /// Gets the Dalamud buddy list. 49 | /// 50 | [PluginService] 51 | internal static IBuddyList BuddyList { get; private set; } = null!; 52 | 53 | /// 54 | /// Gets the Dalamud chat gui. 55 | /// 56 | [PluginService] 57 | internal static IChatGui ChatGui { get; private set; } = null!; 58 | 59 | /// 60 | /// Gets the Dalamud client state. 61 | /// 62 | [PluginService] 63 | internal static IClientState ClientState { get; private set; } = null!; 64 | 65 | /// 66 | /// Gets the Dalamud command manager. 67 | /// 68 | [PluginService] 69 | internal static ICommandManager CommandManager { get; private set; } = null!; 70 | 71 | /// 72 | /// Gets the Dalamud condition. 73 | /// 74 | [PluginService] 75 | internal static ICondition Condition { get; private set; } = null!; 76 | 77 | /// 78 | /// Gets the Dalamud data manager. 79 | /// 80 | [PluginService] 81 | internal static IDataManager DataManager { get; private set; } = null!; 82 | 83 | /// 84 | /// Gets the Dalamud framework manager. 85 | /// 86 | [PluginService] 87 | internal static IFramework Framework { get; private set; } = null!; 88 | 89 | /// 90 | /// Gets the Dalamud job gauges. 91 | /// 92 | [PluginService] 93 | internal static IJobGauges JobGauges { get; private set; } = null!; 94 | 95 | /// 96 | /// Gets the Dalamud object table. 97 | /// 98 | [PluginService] 99 | internal static IObjectTable ObjectTable { get; private set; } = null!; 100 | 101 | /// 102 | /// Gets the Dalamud target manager. 103 | /// 104 | [PluginService] 105 | internal static ITargetManager TargetManager { get; private set; } = null!; 106 | } 107 | -------------------------------------------------------------------------------- /XIVComboExpanded/IconReplacer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | using Dalamud.Hooking; 7 | using Dalamud.Logging; 8 | using Dalamud.Plugin.Services; 9 | using XIVComboExpandedPlugin.Combos; 10 | 11 | namespace XIVComboExpandedPlugin; 12 | 13 | /// 14 | /// This class facilitates the icon replacing. 15 | /// 16 | internal sealed partial class IconReplacer : IDisposable 17 | { 18 | private readonly List customCombos; 19 | private readonly Hook isIconReplaceableHook; 20 | private readonly Hook getIconHook; 21 | 22 | private IntPtr actionManager = IntPtr.Zero; 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | public IconReplacer(IGameInteropProvider gameInteropProvider) 28 | { 29 | this.customCombos = Assembly.GetAssembly(typeof(CustomCombo))!.GetTypes() 30 | .Where(t => !t.IsAbstract && IsDescendant(t, typeof(CustomCombo))) 31 | .Select(t => Activator.CreateInstance(t)) 32 | .Cast() 33 | .ToList(); 34 | 35 | this.getIconHook = gameInteropProvider.HookFromAddress(Service.Address.GetAdjustedActionId, this.GetIconDetour); 36 | this.isIconReplaceableHook = gameInteropProvider.HookFromAddress(Service.Address.IsActionIdReplaceable, this.IsIconReplaceableDetour); 37 | 38 | this.getIconHook.Enable(); 39 | this.isIconReplaceableHook.Enable(); 40 | } 41 | 42 | private static bool IsDescendant(Type clazz, Type ancestor) 43 | { 44 | if (clazz.BaseType == null) return false; 45 | if (clazz.BaseType == ancestor) return true; 46 | return IsDescendant(clazz.BaseType, ancestor); 47 | } 48 | 49 | private delegate ulong IsIconReplaceableDelegate(uint actionID); 50 | 51 | private delegate uint GetIconDelegate(IntPtr actionManager, uint actionID); 52 | 53 | /// 54 | public void Dispose() 55 | { 56 | this.getIconHook?.Dispose(); 57 | this.isIconReplaceableHook?.Dispose(); 58 | } 59 | 60 | /// 61 | /// Calls the original hook. 62 | /// 63 | /// Action ID. 64 | /// The result from the hook. 65 | internal uint OriginalHook(uint actionID) 66 | => this.getIconHook.Original(this.actionManager, actionID); 67 | 68 | private unsafe uint GetIconDetour(IntPtr actionManager, uint actionID) 69 | { 70 | this.actionManager = actionManager; 71 | 72 | try 73 | { 74 | if (Service.ClientState.LocalPlayer == null) 75 | return this.OriginalHook(actionID); 76 | 77 | var lastComboMove = *(uint*)Service.Address.LastComboMove; 78 | var comboTime = *(float*)Service.Address.ComboTimer; 79 | var level = Service.ClientState.LocalPlayer?.Level ?? 0; 80 | 81 | foreach (var combo in this.customCombos) 82 | { 83 | if (combo.TryInvoke(actionID, level, lastComboMove, comboTime, out var newActionID)) 84 | return newActionID; 85 | } 86 | 87 | return this.OriginalHook(actionID); 88 | } 89 | catch (Exception ex) 90 | { 91 | PluginLog.Error(ex, "Don't crash the game"); 92 | return this.OriginalHook(actionID); 93 | } 94 | } 95 | 96 | private ulong IsIconReplaceableDetour(uint actionID) => 1; 97 | } 98 | -------------------------------------------------------------------------------- /XIVComboExpanded/CooldownData.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace XIVComboExpandedPlugin; 4 | 5 | /// 6 | /// Internal cooldown data. 7 | /// 8 | [StructLayout(LayoutKind.Explicit)] 9 | internal struct CooldownData 10 | { 11 | [FieldOffset(0x0)] 12 | private readonly bool isCooldown; 13 | 14 | [FieldOffset(0x4)] 15 | private readonly uint actionID; 16 | 17 | [FieldOffset(0x8)] 18 | private readonly float cooldownElapsed; 19 | 20 | [FieldOffset(0xC)] 21 | private readonly float cooldownTotal; 22 | 23 | /// 24 | /// Gets a value indicating whether the action is on cooldown. 25 | /// 26 | public bool IsCooldown 27 | { 28 | get 29 | { 30 | var (cur, max) = Service.ComboCache.GetMaxCharges(this.ActionID); 31 | if (cur == max) 32 | return this.isCooldown; 33 | 34 | return this.cooldownElapsed < this.CooldownTotal; 35 | } 36 | } 37 | 38 | /// 39 | /// Gets the action ID on cooldown. 40 | /// 41 | public uint ActionID => this.actionID; 42 | 43 | /// 44 | /// Gets the elapsed cooldown time. 45 | /// 46 | public float CooldownElapsed 47 | { 48 | get 49 | { 50 | if (this.cooldownElapsed == 0) 51 | return 0; 52 | 53 | if (this.cooldownElapsed > this.CooldownTotal) 54 | return 0; 55 | 56 | return this.cooldownElapsed; 57 | } 58 | } 59 | 60 | /// 61 | /// Gets the total cooldown time. 62 | /// 63 | public float CooldownTotal 64 | { 65 | get 66 | { 67 | if (this.cooldownTotal == 0) 68 | return 0; 69 | 70 | var (cur, max) = Service.ComboCache.GetMaxCharges(this.ActionID); 71 | if (cur == max) 72 | return this.cooldownTotal; 73 | 74 | // Rebase to the current charge count 75 | var total = this.cooldownTotal / max * cur; 76 | 77 | if (this.cooldownElapsed > total) 78 | return 0; 79 | 80 | return total; 81 | } 82 | } 83 | 84 | /// 85 | /// Gets the cooldown time remaining. 86 | /// 87 | public float CooldownRemaining => this.IsCooldown ? this.CooldownTotal - this.CooldownElapsed : 0; 88 | 89 | /// 90 | /// Gets the maximum number of charges for an action at the current level. 91 | /// 92 | /// Number of charges. 93 | public ushort MaxCharges => Service.ComboCache.GetMaxCharges(this.ActionID).Current; 94 | 95 | /// 96 | /// Gets a value indicating whether the action has charges, not charges available. 97 | /// 98 | public bool HasCharges => this.MaxCharges > 1; 99 | 100 | /// 101 | /// Gets the remaining number of charges for an action. 102 | /// 103 | public ushort RemainingCharges 104 | { 105 | get 106 | { 107 | var (cur, _) = Service.ComboCache.GetMaxCharges(this.ActionID); 108 | 109 | if (!this.IsCooldown) 110 | return cur; 111 | 112 | return (ushort)(this.CooldownElapsed / (this.CooldownTotal / this.MaxCharges)); 113 | } 114 | } 115 | 116 | /// 117 | /// Gets the cooldown time remaining until the next charge. 118 | /// 119 | public float ChargeCooldownRemaining 120 | { 121 | get 122 | { 123 | if (!this.IsCooldown) 124 | return 0; 125 | 126 | var (cur, _) = Service.ComboCache.GetMaxCharges(this.ActionID); 127 | 128 | return this.CooldownRemaining % (this.CooldownTotal / cur); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /XIVComboExpanded/Combos/DOL.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.Conditions; 2 | 3 | namespace XIVComboExpandedPlugin.Combos; 4 | 5 | internal static class DOL 6 | { 7 | public const byte ClassID = 0; 8 | public const byte JobID = 51; 9 | 10 | public const uint 11 | AgelessWords = 215, 12 | SolidReason = 232, 13 | Cast = 289, 14 | Hook = 296, 15 | CastLight = 2135, 16 | Snagging = 4100, 17 | SurfaceSlap = 4595, 18 | Gig = 7632, 19 | VeteranTrade = 7906, 20 | NaturesBounty = 7909, 21 | Salvage = 7910, 22 | MinWiseToTheWorld = 26521, 23 | BtnWiseToTheWorld = 26522, 24 | ElectricCurrent = 26872, 25 | PrizeCatch = 26806; 26 | 27 | public static class Buffs 28 | { 29 | public const ushort 30 | EurekaMoment = 2765; 31 | } 32 | 33 | public static class Debuffs 34 | { 35 | public const ushort 36 | Placeholder = 0; 37 | } 38 | 39 | public static class Levels 40 | { 41 | public const byte 42 | Cast = 1, 43 | Hook = 1, 44 | Snagging = 36, 45 | Gig = 61, 46 | Salvage = 67, 47 | VeteranTrade = 63, 48 | NaturesBounty = 69, 49 | SurfaceSlap = 71, 50 | PrizeCatch = 81, 51 | WiseToTheWorld = 90; 52 | } 53 | } 54 | 55 | internal class MinerEurekaFeature : CustomCombo 56 | { 57 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DolEurekaFeature; 58 | 59 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 60 | { 61 | if (actionID == DOL.SolidReason) 62 | { 63 | if (level >= DOL.Levels.WiseToTheWorld && HasEffect(DOL.Buffs.EurekaMoment)) 64 | return DOL.MinWiseToTheWorld; 65 | } 66 | 67 | if (actionID == DOL.AgelessWords) 68 | { 69 | if (level >= DOL.Levels.WiseToTheWorld && HasEffect(DOL.Buffs.EurekaMoment)) 70 | return DOL.BtnWiseToTheWorld; 71 | } 72 | 73 | return actionID; 74 | } 75 | } 76 | 77 | internal class FisherCast : CustomCombo 78 | { 79 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DolAny; 80 | 81 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 82 | { 83 | if (actionID == DOL.Cast) 84 | { 85 | if (IsEnabled(CustomComboPreset.DolCastHookFeature)) 86 | { 87 | if (HasCondition(ConditionFlag.Fishing)) 88 | return DOL.Hook; 89 | } 90 | 91 | if (IsEnabled(CustomComboPreset.DolCastGigFeature)) 92 | { 93 | if (HasCondition(ConditionFlag.Diving)) 94 | return DOL.Gig; 95 | } 96 | } 97 | 98 | if (actionID == DOL.SurfaceSlap) 99 | { 100 | if (IsEnabled(CustomComboPreset.DolSurfaceTradeFeature)) 101 | { 102 | if (HasCondition(ConditionFlag.Diving)) 103 | return DOL.VeteranTrade; 104 | } 105 | } 106 | 107 | if (actionID == DOL.PrizeCatch) 108 | { 109 | if (IsEnabled(CustomComboPreset.DolPrizeBountyFeature)) 110 | { 111 | if (HasCondition(ConditionFlag.Diving)) 112 | return DOL.NaturesBounty; 113 | } 114 | } 115 | 116 | if (actionID == DOL.Snagging) 117 | { 118 | if (IsEnabled(CustomComboPreset.DolSnaggingSalvageFeature)) 119 | { 120 | if (HasCondition(ConditionFlag.Diving)) 121 | return DOL.Salvage; 122 | } 123 | } 124 | 125 | if (actionID == DOL.CastLight) 126 | { 127 | if (IsEnabled(CustomComboPreset.DolCastLightElectricCurrentFeature)) 128 | { 129 | if (HasCondition(ConditionFlag.Diving)) 130 | return DOL.ElectricCurrent; 131 | } 132 | } 133 | 134 | return actionID; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /XIVComboExpanded/PluginConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using Dalamud.Configuration; 6 | using Dalamud.Utility; 7 | using Newtonsoft.Json; 8 | using XIVComboExpandedPlugin.Attributes; 9 | using XIVComboExpandedPlugin.Combos; 10 | 11 | namespace XIVComboExpandedPlugin; 12 | 13 | /// 14 | /// Plugin configuration. 15 | /// 16 | [Serializable] 17 | public class PluginConfiguration : IPluginConfiguration 18 | { 19 | private static readonly HashSet SecretCombos; 20 | private static readonly Dictionary ConflictingCombos; 21 | private static readonly Dictionary ParentCombos; // child: parent 22 | 23 | static PluginConfiguration() 24 | { 25 | SecretCombos = Enum.GetValues() 26 | .Where(preset => preset.GetAttribute() != default) 27 | .ToHashSet(); 28 | 29 | ConflictingCombos = Enum.GetValues() 30 | .Distinct() // Prevent ArgumentExceptions from adding the same int twice, should not be seen anymore 31 | .ToDictionary( 32 | preset => preset, 33 | preset => preset.GetAttribute()?.ConflictingPresets ?? Array.Empty()); 34 | 35 | ParentCombos = Enum.GetValues() 36 | .Distinct() // Prevent ArgumentExceptions from adding the same int twice, should not be seen anymore 37 | .ToDictionary( 38 | preset => preset, 39 | preset => preset.GetAttribute()?.ParentPreset); 40 | } 41 | 42 | /// 43 | /// Gets or sets the configuration version. 44 | /// 45 | public int Version { get; set; } = 5; 46 | 47 | /// 48 | /// Gets or sets the collection of enabled combos. 49 | /// 50 | [JsonProperty("EnabledActionsV5")] 51 | public HashSet EnabledActions { get; set; } = new(); 52 | 53 | /// 54 | /// Gets or sets the collection of enabled combos. 55 | /// 56 | [JsonProperty("EnabledActionsV4")] 57 | public HashSet EnabledActions4 { get; set; } = new(); 58 | 59 | /// 60 | /// Gets or sets a value indicating whether to allow and display secret combos. 61 | /// 62 | [JsonProperty("Debug")] 63 | public bool EnableSecretCombos { get; set; } = false; 64 | 65 | /// 66 | /// Gets or sets a value indicating whether to hide the children of a feature if it is disabled. 67 | /// 68 | public bool HideChildren { get; set; } = false; 69 | 70 | /// 71 | /// Gets or sets an array of 4 ability IDs to interact with the combo. 72 | /// 73 | public uint[] DancerDanceCompatActionIDs { get; set; } = new uint[] 74 | { 75 | DNC.Cascade, 76 | DNC.Flourish, 77 | DNC.FanDance1, 78 | DNC.FanDance2, 79 | }; 80 | 81 | /// 82 | /// Save the configuration to disk. 83 | /// 84 | public void Save() 85 | => Service.Interface.SavePluginConfig(this); 86 | 87 | /// 88 | /// Gets a value indicating whether a preset is enabled. 89 | /// 90 | /// Preset to check. 91 | /// The boolean representation. 92 | public bool IsEnabled(CustomComboPreset preset) 93 | => this.EnabledActions.Contains(preset) && (this.EnableSecretCombos || !this.IsSecret(preset)); 94 | 95 | /// 96 | /// Gets a value indicating whether a preset is secret. 97 | /// 98 | /// Preset to check. 99 | /// The boolean representation. 100 | public bool IsSecret(CustomComboPreset preset) 101 | => SecretCombos.Contains(preset); 102 | 103 | /// 104 | /// Gets an array of conflicting combo presets. 105 | /// 106 | /// Preset to check. 107 | /// The conflicting presets. 108 | public CustomComboPreset[] GetConflicts(CustomComboPreset preset) 109 | => ConflictingCombos[preset]; 110 | 111 | /// 112 | /// Gets the parent combo preset if it exists, or null. 113 | /// 114 | /// Preset to check. 115 | /// The parent preset. 116 | public CustomComboPreset? GetParent(CustomComboPreset preset) 117 | => ParentCombos[preset]; 118 | } 119 | -------------------------------------------------------------------------------- /statement.txt: -------------------------------------------------------------------------------- 1 | Hello! I'm sure this plugin is bound to raise a few eyebrows in the community, so I'd like to give my stance and perspective on concerns publicly. I've been playing FFXIV without a sub lapse since around patch 2.5. It's a wonderful game that I love playing, and I have no desire to worsen its quality or see it fail or any other number of bad things. 2 | 3 | But just because I love this game doesn't mean I have to be content with its current state. As we've seen recently with the Paisely Park incident, there are definitely some lines that SE, the XIV team, and Yoshi-p consider to be "too far", and work to foil any projects that cross that line. In Live Letter #57, Yoshi-p has a public monologue about such tools, and how they have no desire to go full tryhard to ensure no bad third-party programs are being used. In that discussion, he mentions how some mods improve the game, gain a lot of popularity, and eventually get added in as official features, a la Counter-Strike becoming an entire standalone game. We can see a similar reaction to Paisely Park: It became hugely popular with the community, so the devs took that feedback and implemented something similar. It's not a perfect comparison, but it's pretty close. 4 | 5 | My aim is only ever to implement such features as fit that bill: Something that does not have a substantial impact on the way the game plays, but does bring about a more enjoyable experience. You can think of them as "cheating quality of life" if you'd like. I have no desire to "cheapen" the experience of playing this game, and only want it to be more enjoyable by improving or tweaking features that don't seem perfect. 6 | 7 | So let's talk about XIVCombo. At its core, it largely implements the functionality of buttons that exist in PvP. There, all weaponskill combos are consolidated onto a single button. Additionally, some other buttons that are mutually exclusive get put onto one button. Let's look at a few examples and how they work in PvE content: 8 | 9 | - Jump and Mirage Dive: As a DRG, you cannot use Mirage Dive unless you gain the Dive Ready buff from using (High) Jump. This buff is notably shorter in duration than Jump's cooldown. Therefore, if you for some reason decide to let the buff expire instead of using Mirage Dive, you STILL aren't able to use Jump. In other words, the ability to press one of those buttons automatically means the other can't be pressed. Further, because they are intrinsically linked, it makes sense to put them onto one button. There are numerous abilities spread across multiple jobs that are like this too. Hypercharge/Heat Blast, Third Eye/Seigan, Dream Within a Dream/Assassinate, and more. All of these can be consolidated. 10 | 11 | - Draw and Play: I don't know why they changed this. Draw was FINE the way it was before Shadowbringers. The addition of the Play ability is pure button bloat. All I'm doing is reverting to the behavior of previous game versions. 12 | 13 | - Transpose and Umbral Soul: This one is a bit interesting, because it has extremely minor implications of a "forced" playstyle. However, one thing about BLM I think should be intuitively obvious by the time you hit level 76: You should never use Transpose to enter Astral Fire except as an absolute last option. Therefore, the use of Umbral Soul when in Umbral Ice is superior to using Transpose in every situation. So it makes sense to not even have the option to use Transpose when Umbral Soul is available to use. 14 | 15 | For some jobs, I am intentionally not implementing features because I feel like they would trivialize playing the job by a large amount. This is why MNK, DNC, and RDM are largely untouched. I do not want to turn DNC or RDM into one-button jobs, and with Perfect Balance, MNK would require too much "smart" thinking from the program in order to work properly. 16 | 17 | My end goal in all of this is to encourage the XIV team to open their design options. As it stands, each expac adds and removes abilities for each job. It seems like the design team has an "optimal" number of keybinds they want a job to have, and make sure they are never too far above or below that number. A problem that I observe is that the number of redundant/uninteresting buttons takes up too many of that "magic number" of binds that the team aims to have. Let's look at DRG as an extreme example: 18 | - Chaos Thrust and Full Thrust combos both free up 3 buttons. 19 | - BOTD and Stardiver frees up 1 button. 20 | - Jump and Mirage Dive frees up 1 button. 21 | In total, by consolidating weaponskills and mutually exclusive buttons, DRG gains EIGHT new free spaces for new abilities. What sort of new job mechanics, buffs, or other abilities could the development team add in those eight slots? DRG gains the most free spaces of any jobs, but the possibilities are exciting and interesting. For a job that has a very linear use of abilities, there is little difference in satisfaction between pressing 1-2-3-4 and 1-1-1-1. 22 | 23 | Similarly, I think tanks could benefit massively from having more free slots. While each tank has their own distinctive "flavor" cooldown (TBN, Nascent, Heart of Stone, Sheltron), the overall tanking plan is extremely similar for all tanks. With more spots for abilities freed up, tanks could gain more disctinctive abilities. Mitigation and survival could become an even more dynamic experience. But the more redundant buttons there are, the fewer abilities the developers can play around with. 24 | 25 | I hope I've managed to convince you that I do not want to see this game in ruins. I want it to flourish and become the best it can possibly be. My hope is that this plugin serves as a community voice, giving feedback that would normally never reach the developers' ears. -------------------------------------------------------------------------------- /XIVComboExpanded/CustomComboCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using Dalamud.Game; 5 | using Dalamud.Game.ClientState.JobGauge.Types; 6 | using Dalamud.Game.ClientState.Objects.Types; 7 | using Dalamud.Game.ClientState.Statuses; 8 | using Dalamud.Plugin.Services; 9 | 10 | namespace XIVComboExpandedPlugin; 11 | 12 | /// 13 | /// Cached conditional combo logic. 14 | /// 15 | internal partial class CustomComboCache : IDisposable 16 | { 17 | private const uint InvalidObjectID = 0xE000_0000; 18 | 19 | // Invalidate these 20 | private readonly Dictionary<(uint StatusID, uint? TargetID, uint? SourceID), Status?> statusCache = new(); 21 | private readonly Dictionary cooldownCache = new(); 22 | 23 | // Do not invalidate these 24 | private readonly Dictionary cooldownGroupCache = new(); 25 | private readonly Dictionary jobGaugeCache = new(); 26 | private readonly Dictionary<(uint ActionID, uint ClassJobID, byte Level), (ushort CurrentMax, ushort Max)> chargesCache = new(); 27 | 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | public CustomComboCache() 32 | { 33 | Service.Framework.Update += this.Framework_Update; 34 | } 35 | 36 | private delegate IntPtr GetActionCooldownSlotDelegate(IntPtr actionManager, int cooldownGroup); 37 | 38 | /// 39 | public void Dispose() 40 | { 41 | Service.Framework.Update -= this.Framework_Update; 42 | } 43 | 44 | /// 45 | /// Get a job gauge. 46 | /// 47 | /// Type of job gauge. 48 | /// The job gauge. 49 | internal T GetJobGauge() where T : JobGaugeBase 50 | { 51 | if (!this.jobGaugeCache.TryGetValue(typeof(T), out var gauge)) 52 | gauge = this.jobGaugeCache[typeof(T)] = Service.JobGauges.Get(); 53 | 54 | return (T)gauge; 55 | } 56 | 57 | /// 58 | /// Finds a status on the given object. 59 | /// 60 | /// Status effect ID. 61 | /// Object to look for effects on. 62 | /// Source object ID. 63 | /// Status object or null. 64 | internal Status? GetStatus(uint statusID, GameObject? obj, uint? sourceID) 65 | { 66 | var key = (statusID, obj?.ObjectId, sourceID); 67 | if (this.statusCache.TryGetValue(key, out var found)) 68 | return found; 69 | 70 | if (obj is null) 71 | return this.statusCache[key] = null; 72 | 73 | if (obj is not BattleChara chara) 74 | return this.statusCache[key] = null; 75 | 76 | foreach (var status in chara.StatusList) 77 | { 78 | if (status.StatusId == statusID && (!sourceID.HasValue || status.SourceId == 0 || status.SourceId == InvalidObjectID || status.SourceId == sourceID)) 79 | return this.statusCache[key] = status; 80 | } 81 | 82 | return this.statusCache[key] = null; 83 | } 84 | 85 | /// 86 | /// Gets the cooldown data for an action. 87 | /// 88 | /// Action ID to check. 89 | /// Cooldown data. 90 | internal unsafe CooldownData GetCooldown(uint actionID) 91 | { 92 | if (this.cooldownCache.TryGetValue(actionID, out var found)) 93 | return found; 94 | 95 | var actionManager = FFXIVClientStructs.FFXIV.Client.Game.ActionManager.Instance(); 96 | if (actionManager == null) 97 | return this.cooldownCache[actionID] = default; 98 | 99 | var cooldownGroup = this.GetCooldownGroup(actionID); 100 | 101 | var cooldownPtr = actionManager->GetRecastGroupDetail(cooldownGroup - 1); 102 | cooldownPtr->ActionID = actionID; 103 | 104 | return this.cooldownCache[actionID] = *(CooldownData*)cooldownPtr; 105 | } 106 | 107 | /// 108 | /// Get the maximum number of charges for an action. 109 | /// 110 | /// Action ID to check. 111 | /// Max number of charges at current and max level. 112 | internal unsafe (ushort Current, ushort Max) GetMaxCharges(uint actionID) 113 | { 114 | var player = Service.ClientState.LocalPlayer; 115 | if (player == null) 116 | return (0, 0); 117 | 118 | var job = player.ClassJob.Id; 119 | var level = player.Level; 120 | if (job == 0 || level == 0) 121 | return (0, 0); 122 | 123 | var key = (actionID, job, level); 124 | if (this.chargesCache.TryGetValue(key, out var found)) 125 | return found; 126 | 127 | var cur = FFXIVClientStructs.FFXIV.Client.Game.ActionManager.GetMaxCharges(actionID, 0); 128 | var max = FFXIVClientStructs.FFXIV.Client.Game.ActionManager.GetMaxCharges(actionID, 90); 129 | return this.chargesCache[key] = (cur, max); 130 | } 131 | 132 | private byte GetCooldownGroup(uint actionID) 133 | { 134 | if (this.cooldownGroupCache.TryGetValue(actionID, out var cooldownGroup)) 135 | return cooldownGroup; 136 | 137 | var sheet = Service.DataManager.GetExcelSheet()!; 138 | var row = sheet.GetRow(actionID); 139 | 140 | return this.cooldownGroupCache[actionID] = row!.CooldownGroup; 141 | } 142 | 143 | private unsafe void Framework_Update(IFramework framework) 144 | { 145 | this.statusCache.Clear(); 146 | this.cooldownCache.Clear(); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /XIVComboExpanded/Combos/WHM.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | 3 | namespace XIVComboExpandedPlugin.Combos; 4 | 5 | internal static class WHM 6 | { 7 | public const byte ClassID = 6; 8 | public const byte JobID = 24; 9 | 10 | public const uint 11 | Cure = 120, 12 | Medica = 124, 13 | Raise = 125, 14 | Cure2 = 135, 15 | PresenceOfMind = 136, 16 | Holy = 139, 17 | Benediction = 140, 18 | Asylum = 3569, 19 | Tetragrammaton = 3570, 20 | Assize = 3571, 21 | PlenaryIndulgence = 7433, 22 | AfflatusSolace = 16531, 23 | AfflatusRapture = 16534, 24 | AfflatusMisery = 16535, 25 | Temperance = 16536, 26 | Holy3 = 25860, 27 | Aquaveil = 25861, 28 | LiturgyOfTheBell = 25862; 29 | 30 | public static class Buffs 31 | { 32 | public const ushort 33 | Placeholder = 0; 34 | } 35 | 36 | public static class Debuffs 37 | { 38 | public const ushort 39 | Placeholder = 0; 40 | } 41 | 42 | public static class Levels 43 | { 44 | public const byte 45 | Raise = 12, 46 | Cure2 = 30, 47 | AfflatusSolace = 52, 48 | AfflatusMisery = 74, 49 | AfflatusRapture = 76; 50 | } 51 | } 52 | 53 | internal class WhiteMageAfflatusSolace : CustomCombo 54 | { 55 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.WhiteMageSolaceMiseryFeature; 56 | 57 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 58 | { 59 | if (actionID == WHM.AfflatusSolace) 60 | { 61 | var gauge = GetJobGauge(); 62 | 63 | if (level >= WHM.Levels.AfflatusMisery && gauge.BloodLily == 3) 64 | { 65 | if (IsEnabled(CustomComboPreset.WhiteMageSolaceMiseryTargetFeature)) 66 | { 67 | if (TargetIsEnemy()) 68 | return WHM.AfflatusMisery; 69 | } 70 | else 71 | { 72 | return WHM.AfflatusMisery; 73 | } 74 | } 75 | } 76 | 77 | return actionID; 78 | } 79 | } 80 | 81 | internal class WhiteMageAfflatusRapture : CustomCombo 82 | { 83 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.WhiteMageRaptureMiseryFeature; 84 | 85 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 86 | { 87 | if (actionID == WHM.AfflatusRapture) 88 | { 89 | var gauge = GetJobGauge(); 90 | 91 | if (level >= WHM.Levels.AfflatusMisery && gauge.BloodLily == 3 && TargetIsEnemy()) 92 | return WHM.AfflatusMisery; 93 | } 94 | 95 | return actionID; 96 | } 97 | } 98 | 99 | internal class WhiteMageHoly : CustomCombo 100 | { 101 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.WhiteMageHolyMiseryFeature; 102 | 103 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 104 | { 105 | if (actionID == WHM.Holy || actionID == WHM.Holy3) 106 | { 107 | var gauge = GetJobGauge(); 108 | 109 | if (level >= WHM.Levels.AfflatusMisery && gauge.BloodLily == 3 && TargetIsEnemy()) 110 | return WHM.AfflatusMisery; 111 | } 112 | 113 | return actionID; 114 | } 115 | } 116 | 117 | internal class WhiteMageCure2 : CustomCombo 118 | { 119 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.WhmAny; 120 | 121 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 122 | { 123 | if (actionID == WHM.Cure2) 124 | { 125 | var gauge = GetJobGauge(); 126 | 127 | if (IsEnabled(CustomComboPreset.WhiteMageCureFeature)) 128 | { 129 | if (level < WHM.Levels.Cure2) 130 | return WHM.Cure; 131 | } 132 | 133 | if (IsEnabled(CustomComboPreset.WhiteMageAfflatusFeature)) 134 | { 135 | if (IsEnabled(CustomComboPreset.WhiteMageSolaceMiseryFeature)) 136 | { 137 | if (level >= WHM.Levels.AfflatusMisery && gauge.BloodLily == 3) 138 | { 139 | if (IsEnabled(CustomComboPreset.WhiteMageSolaceMiseryTargetFeature)) 140 | { 141 | if (TargetIsEnemy()) 142 | return WHM.AfflatusMisery; 143 | } 144 | else 145 | { 146 | return WHM.AfflatusMisery; 147 | } 148 | } 149 | } 150 | 151 | if (level >= WHM.Levels.AfflatusSolace && gauge.Lily > 0) 152 | return WHM.AfflatusSolace; 153 | } 154 | } 155 | 156 | return actionID; 157 | } 158 | } 159 | 160 | internal class WhiteMageMedica : CustomCombo 161 | { 162 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.WhmAny; 163 | 164 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 165 | { 166 | if (actionID == WHM.Medica) 167 | { 168 | var gauge = GetJobGauge(); 169 | 170 | if (IsEnabled(CustomComboPreset.WhiteMageAfflatusFeature)) 171 | { 172 | if (IsEnabled(CustomComboPreset.WhiteMageRaptureMiseryFeature)) 173 | { 174 | if (level >= WHM.Levels.AfflatusMisery && gauge.BloodLily == 3 && TargetIsEnemy()) 175 | return WHM.AfflatusMisery; 176 | } 177 | 178 | if (level >= WHM.Levels.AfflatusRapture && gauge.Lily > 0) 179 | return WHM.AfflatusRapture; 180 | } 181 | } 182 | 183 | return actionID; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /XIVComboExpanded/Combos/AST.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | using Dalamud.Game.ClientState.JobGauge.Enums; 4 | using Dalamud.Game.ClientState.JobGauge.Types; 5 | 6 | namespace XIVComboExpandedPlugin.Combos; 7 | 8 | internal static class AST 9 | { 10 | public const byte JobID = 33; 11 | 12 | public const uint 13 | Draw = 3590, 14 | Redraw = 3593, 15 | Benefic = 3594, 16 | Malefic = 3596, 17 | Malefic2 = 3598, 18 | Ascend = 3603, 19 | Lightspeed = 3606, 20 | Benefic2 = 3610, 21 | Synastry = 3612, 22 | CollectiveUnconscious = 3613, 23 | Gravity = 3615, 24 | Balance = 4401, 25 | Bole = 4404, 26 | Arrow = 4402, 27 | Spear = 4403, 28 | Ewer = 4405, 29 | Spire = 4406, 30 | EarthlyStar = 7439, 31 | Malefic3 = 7442, 32 | MinorArcana = 7443, 33 | SleeveDraw = 7448, 34 | Divination = 16552, 35 | CelestialOpposition = 16553, 36 | Malefic4 = 16555, 37 | Horoscope = 16557, 38 | NeutralSect = 16559, 39 | Play = 17055, 40 | CrownPlay = 25869, 41 | Astrodyne = 25870, 42 | FallMalefic = 25871, 43 | Gravity2 = 25872, 44 | Exaltation = 25873, 45 | Macrocosmos = 25874; 46 | 47 | public static class Buffs 48 | { 49 | public const ushort 50 | ClarifyingDraw = 2713; 51 | } 52 | 53 | public static class Debuffs 54 | { 55 | public const ushort 56 | Placeholder = 0; 57 | } 58 | 59 | public static class Levels 60 | { 61 | public const byte 62 | Ascend = 12, 63 | Benefic2 = 26, 64 | Draw = 30, 65 | Redraw = 40, 66 | Astrodyne = 50, 67 | MinorArcana = 70, 68 | CrownPlay = 70; 69 | } 70 | } 71 | 72 | internal class AstrologianMalefic : CustomCombo 73 | { 74 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.AstrologianMaleficDrawFeature; 75 | 76 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 77 | { 78 | if (actionID == AST.Malefic || actionID == AST.Malefic2 || actionID == AST.Malefic3 || actionID == AST.Malefic4 || actionID == AST.FallMalefic) 79 | { 80 | var gauge = GetJobGauge(); 81 | 82 | if (level >= AST.Levels.Draw && gauge.DrawnCard == CardType.NONE && HasCharges(AST.Draw)) 83 | return AST.Draw; 84 | } 85 | 86 | return actionID; 87 | } 88 | } 89 | 90 | internal class AstrologianGravity : CustomCombo 91 | { 92 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.AstrologianGravityDrawFeature; 93 | 94 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 95 | { 96 | if (actionID == AST.Gravity || actionID == AST.Gravity2) 97 | { 98 | var gauge = GetJobGauge(); 99 | 100 | if (level >= AST.Levels.Draw && gauge.DrawnCard == CardType.NONE && HasCharges(AST.Draw)) 101 | return AST.Draw; 102 | } 103 | 104 | return actionID; 105 | } 106 | } 107 | 108 | internal class AstrologianPlay : CustomCombo 109 | { 110 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.AstAny; 111 | 112 | private Dictionary CardSeals { get; } = new Dictionary 113 | { 114 | { CardType.BALANCE, SealType.SUN }, 115 | { CardType.BOLE, SealType.SUN }, 116 | { CardType.ARROW, SealType.MOON }, 117 | { CardType.EWER, SealType.MOON }, 118 | { CardType.SPEAR, SealType.CELESTIAL }, 119 | { CardType.SPIRE, SealType.CELESTIAL }, 120 | }; 121 | 122 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 123 | { 124 | if (actionID == AST.Play) 125 | { 126 | var gauge = GetJobGauge(); 127 | 128 | if (IsEnabled(CustomComboPreset.AstrologianPlayAstrodyneFeature)) 129 | { 130 | if (level >= AST.Levels.Astrodyne && !gauge.ContainsSeal(SealType.NONE)) 131 | return AST.Astrodyne; 132 | } 133 | 134 | if (IsEnabled(CustomComboPreset.AstrologianPlayDrawFeature)) 135 | { 136 | if (IsEnabled(CustomComboPreset.AstrologianPlayDrawAstrodyneFeature)) 137 | { 138 | var draw = GetCooldown(AST.Draw); 139 | 140 | if (level >= AST.Levels.Astrodyne && !gauge.ContainsSeal(SealType.NONE) && (draw.RemainingCharges == 0 || gauge.DrawnCard != CardType.NONE)) 141 | return AST.Astrodyne; 142 | } 143 | 144 | if (level >= AST.Levels.Draw && gauge.DrawnCard == CardType.NONE) 145 | return AST.Draw; 146 | } 147 | 148 | if (IsEnabled(CustomComboPreset.AstrologianPlayRedrawFeature)) 149 | { 150 | // Use redraw if and only if the player has has a card drawn and the drawn card is a seal type the 151 | // player already has. Note that there is no check here to see if the player already has 3 seals. 152 | // While players should never use Draw at 3 seals, if the player has not enabled the Play to Astrodyne 153 | // or Play to Draw to Astrodyne feature, we should still handle the Redraw check as normal, since the 154 | // ONLY reason to use Play at 3 seals is to try to fish for the 3-different-seals Astrodyne, even though 155 | // that's an unmitigated DPS loss over using Astrodyne at only 1 or 2 seals. 156 | 157 | if (level >= AST.Levels.Redraw && gauge.DrawnCard != CardType.NONE && HasEffect(AST.Buffs.ClarifyingDraw)) 158 | { 159 | var cardSeal = this.CardSeals.GetValueOrDefault(gauge.DrawnCard, SealType.NONE); 160 | 161 | if (gauge.ContainsSeal(cardSeal)) 162 | return AST.Redraw; 163 | } 164 | } 165 | } 166 | 167 | return actionID; 168 | } 169 | } 170 | 171 | internal class AstrologianDraw : CustomCombo 172 | { 173 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.AstrologianDrawLockoutFeature; 174 | 175 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 176 | { 177 | if (actionID == AST.Draw) 178 | { 179 | var gauge = GetJobGauge(); 180 | 181 | if (gauge.DrawnCard != CardType.NONE) 182 | // Malefic4 183 | return OriginalHook(AST.Malefic); 184 | } 185 | 186 | return actionID; 187 | } 188 | } 189 | 190 | internal class AstrologianBenefic2 : CustomCombo 191 | { 192 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.AstrologianBeneficSyncFeature; 193 | 194 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 195 | { 196 | if (actionID == AST.Benefic2) 197 | { 198 | if (level < AST.Levels.Benefic2) 199 | return AST.Benefic; 200 | } 201 | 202 | return actionID; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /XIVComboExpanded/Combos/SCH.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | 3 | namespace XIVComboExpandedPlugin.Combos; 4 | 5 | internal static class SCH 6 | { 7 | public const byte ClassID = 15; 8 | public const byte JobID = 28; 9 | 10 | public const uint 11 | Aetherflow = 166, 12 | EnergyDrain = 167, 13 | Resurrection = 173, 14 | Adloquium = 185, 15 | SacredSoil = 188, 16 | Lustrate = 189, 17 | Physick = 190, 18 | Indomitability = 3583, 19 | DeploymentTactics = 3585, 20 | EmergencyTactics = 3586, 21 | Dissipation = 3587, 22 | Excogitation = 7434, 23 | ChainStratagem = 7436, 24 | Aetherpact = 7437, 25 | WhisperingDawn = 16537, 26 | FeyIllumination = 16538, 27 | Recitation = 16542, 28 | FeyBless = 16543, 29 | SummonSeraph = 16545, 30 | Consolation = 16546, 31 | SummonEos = 17215, 32 | SummonSelene = 17216, 33 | Ruin2 = 17870; 34 | 35 | public static class Buffs 36 | { 37 | public const ushort 38 | Dissipation = 791, 39 | Recitation = 1896; 40 | } 41 | 42 | public static class Debuffs 43 | { 44 | public const ushort 45 | Placeholder = 0; 46 | } 47 | 48 | public static class Levels 49 | { 50 | public const byte 51 | Resurrection = 12, 52 | Adloquium = 30, 53 | Aetherflow = 45, 54 | Lustrate = 45, 55 | Excogitation = 62, 56 | ChainStratagem = 66, 57 | Recitation = 74, 58 | Consolation = 80, 59 | SummonSeraph = 80; 60 | } 61 | } 62 | 63 | internal class ScholarFeyBless : CustomCombo 64 | { 65 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.ScholarSeraphConsolationFeature; 66 | 67 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 68 | { 69 | if (actionID == SCH.FeyBless) 70 | { 71 | var gauge = GetJobGauge(); 72 | 73 | if (level >= SCH.Levels.Consolation && gauge.SeraphTimer > 0) 74 | return SCH.Consolation; 75 | } 76 | 77 | return actionID; 78 | } 79 | } 80 | 81 | internal class ScholarExcogitation : CustomCombo 82 | { 83 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SchAny; 84 | 85 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 86 | { 87 | if (actionID == SCH.Excogitation) 88 | { 89 | if (IsEnabled(CustomComboPreset.ScholarExcogitationRecitationFeature)) 90 | { 91 | if (level >= SCH.Levels.Recitation && IsOffCooldown(SCH.Recitation)) 92 | return SCH.Recitation; 93 | } 94 | 95 | if (IsEnabled(CustomComboPreset.ScholarExcogitationLustrateFeature)) 96 | { 97 | if (level < SCH.Levels.Excogitation || IsOnCooldown(SCH.Excogitation)) 98 | return SCH.Lustrate; 99 | } 100 | } 101 | 102 | return actionID; 103 | } 104 | } 105 | 106 | internal class ScholarEnergyDrain : CustomCombo 107 | { 108 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.ScholarEnergyDrainAetherflowFeature; 109 | 110 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 111 | { 112 | if (actionID == SCH.EnergyDrain) 113 | { 114 | var gauge = GetJobGauge(); 115 | 116 | if (level >= SCH.Levels.Aetherflow && gauge.Aetherflow == 0) 117 | return SCH.Aetherflow; 118 | } 119 | 120 | return actionID; 121 | } 122 | } 123 | 124 | internal class ScholarLustrate : CustomCombo 125 | { 126 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SchAny; 127 | 128 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 129 | { 130 | if (actionID == SCH.Lustrate) 131 | { 132 | var gauge = GetJobGauge(); 133 | 134 | if (IsEnabled(CustomComboPreset.ScholarLustrateRecitationFeature)) 135 | { 136 | if (level >= SCH.Levels.Recitation && IsOffCooldown(SCH.Recitation)) 137 | return SCH.Recitation; 138 | } 139 | 140 | if (IsEnabled(CustomComboPreset.ScholarLustrateExcogitationFeature)) 141 | { 142 | if (level >= SCH.Levels.Excogitation && IsOffCooldown(SCH.Excogitation)) 143 | return SCH.Excogitation; 144 | } 145 | 146 | if (IsEnabled(CustomComboPreset.ScholarLustrateAetherflowFeature)) 147 | { 148 | if (level >= SCH.Levels.Aetherflow && gauge.Aetherflow == 0) 149 | return SCH.Aetherflow; 150 | } 151 | } 152 | 153 | return actionID; 154 | } 155 | } 156 | 157 | internal class ScholarIndomitability : CustomCombo 158 | { 159 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.ScholarIndomAetherflowFeature; 160 | 161 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 162 | { 163 | if (actionID == SCH.Indomitability) 164 | { 165 | var gauge = GetJobGauge(); 166 | 167 | if (level >= SCH.Levels.Aetherflow && gauge.Aetherflow == 0 && !HasEffect(SCH.Buffs.Recitation)) 168 | return SCH.Aetherflow; 169 | } 170 | 171 | return actionID; 172 | } 173 | } 174 | 175 | internal class ScholarSacredSoil : CustomCombo 176 | { 177 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.ScholarSacredSoilAetherflowFeature; 178 | 179 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 180 | { 181 | if (actionID == SCH.SacredSoil) 182 | { 183 | var gauge = GetJobGauge(); 184 | 185 | if (level >= SCH.Levels.Aetherflow && gauge.Aetherflow == 0) 186 | return SCH.Aetherflow; 187 | } 188 | 189 | return actionID; 190 | } 191 | } 192 | 193 | internal class ScholarSummon : CustomCombo 194 | { 195 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.ScholarSeraphFeature; 196 | 197 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 198 | { 199 | if (actionID == SCH.SummonEos || actionID == SCH.SummonSelene) 200 | { 201 | var gauge = GetJobGauge(); 202 | 203 | if (gauge.SeraphTimer != 0 || HasPetPresent()) 204 | // Consolation 205 | return OriginalHook(SCH.SummonSeraph); 206 | } 207 | 208 | return actionID; 209 | } 210 | } 211 | 212 | internal class ScholarAdloquium : CustomCombo 213 | { 214 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.ScholarAdloquiumSyncFeature; 215 | 216 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 217 | { 218 | if (actionID == SCH.Adloquium) 219 | { 220 | if (level < SCH.Levels.Adloquium) 221 | return SCH.Physick; 222 | } 223 | 224 | return actionID; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /XIVComboExpanded/Combos/DRK.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | 3 | namespace XIVComboExpandedPlugin.Combos; 4 | 5 | internal static class DRK 6 | { 7 | public const byte JobID = 32; 8 | 9 | public const uint 10 | HardSlash = 3617, 11 | Unleash = 3621, 12 | SyphonStrike = 3623, 13 | Souleater = 3632, 14 | BloodWeapon = 3625, 15 | SaltedEarth = 3639, 16 | AbyssalDrain = 3641, 17 | CarveAndSpit = 3643, 18 | Quietus = 7391, 19 | Bloodspiller = 7392, 20 | FloodOfDarkness = 16466, 21 | EdgeOfDarkness = 16467, 22 | StalwartSoul = 16468, 23 | FloodOfShadow = 16469, 24 | EdgeOfShadow = 16470, 25 | LivingShadow = 16472, 26 | SaltAndDarkness = 25755, 27 | Shadowbringer = 25757; 28 | 29 | public static class Buffs 30 | { 31 | public const ushort 32 | BloodWeapon = 742, 33 | Darkside = 751, 34 | Delirium = 1972; 35 | } 36 | 37 | public static class Debuffs 38 | { 39 | public const ushort 40 | Placeholder = 0; 41 | } 42 | 43 | public static class Levels 44 | { 45 | public const byte 46 | SyphonStrike = 2, 47 | Souleater = 26, 48 | FloodOfDarkness = 30, 49 | BloodWeapon = 35, 50 | EdgeOfDarkness = 40, 51 | StalwartSoul = 40, 52 | SaltedEarth = 52, 53 | AbyssalDrain = 56, 54 | CarveAndSpit = 60, 55 | Bloodspiller = 62, 56 | Quietus = 64, 57 | Delirium = 68, 58 | Shadow = 74, 59 | LivingShadow = 80, 60 | SaltAndDarkness = 86, 61 | Shadowbringer = 90; 62 | } 63 | } 64 | 65 | internal class DarkSouleater : CustomCombo 66 | { 67 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DrkAny; 68 | 69 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 70 | { 71 | if (actionID == DRK.Souleater) 72 | { 73 | var gauge = GetJobGauge(); 74 | 75 | if (IsEnabled(CustomComboPreset.DarkDeliriumFeature)) 76 | { 77 | if (level >= DRK.Levels.Bloodspiller && level >= DRK.Levels.Delirium && HasEffect(DRK.Buffs.Delirium)) 78 | return DRK.Bloodspiller; 79 | } 80 | 81 | if (IsEnabled(CustomComboPreset.DarkSouleaterCombo)) 82 | { 83 | if (IsEnabled(CustomComboPreset.DarkSouleaterOvercapFeature)) 84 | { 85 | if (level >= DRK.Levels.Bloodspiller && gauge.Blood > 90 && HasEffect(DRK.Buffs.BloodWeapon)) 86 | return DRK.Bloodspiller; 87 | } 88 | 89 | if (comboTime > 0) 90 | { 91 | if (lastComboMove == DRK.SyphonStrike && level >= DRK.Levels.Souleater) 92 | { 93 | if (IsEnabled(CustomComboPreset.DarkSouleaterOvercapFeature)) 94 | { 95 | if (level >= DRK.Levels.Bloodspiller && (gauge.Blood > 80 || (gauge.Blood > 70 && HasEffect(DRK.Buffs.BloodWeapon)))) 96 | return DRK.Bloodspiller; 97 | } 98 | 99 | return DRK.Souleater; 100 | } 101 | 102 | if (lastComboMove == DRK.HardSlash && level >= DRK.Levels.SyphonStrike) 103 | return DRK.SyphonStrike; 104 | } 105 | 106 | return DRK.HardSlash; 107 | } 108 | } 109 | 110 | return actionID; 111 | } 112 | } 113 | 114 | internal class DarkStalwartSoul : CustomCombo 115 | { 116 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DrkAny; 117 | 118 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 119 | { 120 | if (actionID == DRK.StalwartSoul) 121 | { 122 | var gauge = GetJobGauge(); 123 | 124 | if (IsEnabled(CustomComboPreset.DarkDeliriumFeature)) 125 | { 126 | if (level >= DRK.Levels.Quietus && level >= DRK.Levels.Delirium && HasEffect(DRK.Buffs.Delirium)) 127 | return DRK.Quietus; 128 | } 129 | 130 | if (IsEnabled(CustomComboPreset.DarkStalwartSoulCombo)) 131 | { 132 | if (IsEnabled(CustomComboPreset.DarkStalwartSoulOvercapFeature)) 133 | { 134 | if (level >= DRK.Levels.Quietus && gauge.Blood > 90 && HasEffect(DRK.Buffs.BloodWeapon)) 135 | return DRK.Quietus; 136 | } 137 | 138 | if (comboTime > 0) 139 | { 140 | if (lastComboMove == DRK.Unleash && level >= DRK.Levels.StalwartSoul) 141 | { 142 | if (IsEnabled(CustomComboPreset.DarkStalwartSoulOvercapFeature)) 143 | { 144 | if (level >= DRK.Levels.Quietus && (gauge.Blood > 80 || (gauge.Blood > 70 && HasEffect(DRK.Buffs.BloodWeapon)))) 145 | return DRK.Quietus; 146 | } 147 | 148 | return DRK.StalwartSoul; 149 | } 150 | } 151 | 152 | return DRK.Unleash; 153 | } 154 | } 155 | 156 | return actionID; 157 | } 158 | } 159 | 160 | internal class DarkCarveAndSpitAbyssalDrain : CustomCombo 161 | { 162 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DrkAny; 163 | 164 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 165 | { 166 | if (actionID == DRK.CarveAndSpit || actionID == DRK.AbyssalDrain) 167 | { 168 | if (IsEnabled(CustomComboPreset.DarkBloodWeaponFeature)) 169 | { 170 | if (actionID == DRK.AbyssalDrain && level < DRK.Levels.AbyssalDrain) 171 | return DRK.BloodWeapon; 172 | 173 | if (actionID == DRK.CarveAndSpit && level < DRK.Levels.CarveAndSpit) 174 | return DRK.BloodWeapon; 175 | 176 | if (level >= DRK.Levels.BloodWeapon && IsOffCooldown(DRK.BloodWeapon)) 177 | return DRK.BloodWeapon; 178 | } 179 | } 180 | 181 | return actionID; 182 | } 183 | } 184 | 185 | internal class DarkQuietusBloodspiller : CustomCombo 186 | { 187 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DrkAny; 188 | 189 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 190 | { 191 | if (actionID == DRK.Quietus || actionID == DRK.Bloodspiller) 192 | { 193 | var gauge = GetJobGauge(); 194 | 195 | if (IsEnabled(CustomComboPreset.DarkLivingShadowFeature)) 196 | { 197 | if (level >= DRK.Levels.LivingShadow && gauge.Blood >= 50 && IsOffCooldown(DRK.LivingShadow)) 198 | return DRK.LivingShadow; 199 | } 200 | } 201 | 202 | return actionID; 203 | } 204 | } 205 | 206 | internal class DarkLivingShadow : CustomCombo 207 | { 208 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DrkAny; 209 | 210 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 211 | { 212 | if (actionID == DRK.LivingShadow) 213 | { 214 | var gauge = GetJobGauge(); 215 | 216 | if (IsEnabled(CustomComboPreset.DarkLivingShadowbringerFeature)) 217 | { 218 | if (level >= DRK.Levels.Shadowbringer && gauge.ShadowTimeRemaining > 0 && HasCharges(DRK.Shadowbringer)) 219 | return DRK.Shadowbringer; 220 | } 221 | 222 | if (IsEnabled(CustomComboPreset.DarkLivingShadowbringerHpFeature)) 223 | { 224 | if (level >= DRK.Levels.Shadowbringer && HasCharges(DRK.Shadowbringer) && IsOnCooldown(DRK.LivingShadow)) 225 | return DRK.Shadowbringer; 226 | } 227 | } 228 | 229 | return actionID; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /XIVComboExpanded/Combos/GNB.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | 3 | namespace XIVComboExpandedPlugin.Combos; 4 | 5 | internal static class GNB 6 | { 7 | public const byte JobID = 37; 8 | 9 | public const uint 10 | KeenEdge = 16137, 11 | NoMercy = 16138, 12 | BrutalShell = 16139, 13 | DemonSlice = 16141, 14 | SolidBarrel = 16145, 15 | GnashingFang = 16146, 16 | DemonSlaughter = 16149, 17 | SonicBreak = 16153, 18 | Continuation = 16155, 19 | JugularRip = 16156, 20 | AbdomenTear = 16157, 21 | EyeGouge = 16158, 22 | BowShock = 16159, 23 | BurstStrike = 16162, 24 | FatedCircle = 16163, 25 | Bloodfest = 16164, 26 | Hypervelocity = 25759, 27 | DoubleDown = 25760; 28 | 29 | public static class Buffs 30 | { 31 | public const ushort 32 | NoMercy = 1831, 33 | ReadyToRip = 1842, 34 | ReadyToTear = 1843, 35 | ReadyToGouge = 1844, 36 | ReadyToBlast = 2686; 37 | } 38 | 39 | public static class Debuffs 40 | { 41 | public const ushort 42 | BowShock = 1838; 43 | } 44 | 45 | public static class Levels 46 | { 47 | public const byte 48 | NoMercy = 2, 49 | BrutalShell = 4, 50 | SolidBarrel = 26, 51 | BurstStrike = 30, 52 | DemonSlaughter = 40, 53 | SonicBreak = 54, 54 | BowShock = 62, 55 | Continuation = 70, 56 | FatedCircle = 72, 57 | Bloodfest = 76, 58 | EnhancedContinuation = 86, 59 | CartridgeCharge2 = 88, 60 | DoubleDown = 90; 61 | } 62 | } 63 | 64 | internal class GunbreakerSolidBarrel : CustomCombo 65 | { 66 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.GunbreakerSolidBarrelCombo; 67 | 68 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 69 | { 70 | if (actionID == GNB.SolidBarrel) 71 | { 72 | if (comboTime > 0) 73 | { 74 | if (lastComboMove == GNB.BrutalShell && level >= GNB.Levels.SolidBarrel) 75 | { 76 | if (IsEnabled(CustomComboPreset.GunbreakerBurstStrikeFeature)) 77 | { 78 | var gauge = GetJobGauge(); 79 | var maxAmmo = level >= GNB.Levels.CartridgeCharge2 ? 3 : 2; 80 | 81 | if (IsEnabled(CustomComboPreset.GunbreakerBurstStrikeCont)) 82 | { 83 | if (level >= GNB.Levels.EnhancedContinuation && HasEffect(GNB.Buffs.ReadyToBlast)) 84 | return GNB.Hypervelocity; 85 | } 86 | 87 | if (level >= GNB.Levels.BurstStrike && gauge.Ammo == maxAmmo) 88 | return GNB.BurstStrike; 89 | } 90 | 91 | return GNB.SolidBarrel; 92 | } 93 | 94 | if (lastComboMove == GNB.KeenEdge && level >= GNB.Levels.BrutalShell) 95 | return GNB.BrutalShell; 96 | } 97 | 98 | return GNB.KeenEdge; 99 | } 100 | 101 | return actionID; 102 | } 103 | } 104 | 105 | internal class GunbreakerGnashingFang : CustomCombo 106 | { 107 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.GunbreakerGnashingFangCont; 108 | 109 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 110 | { 111 | if (actionID == GNB.GnashingFang) 112 | { 113 | if (level >= GNB.Levels.Continuation) 114 | { 115 | if (HasEffect(GNB.Buffs.ReadyToGouge)) 116 | return GNB.EyeGouge; 117 | 118 | if (HasEffect(GNB.Buffs.ReadyToTear)) 119 | return GNB.AbdomenTear; 120 | 121 | if (HasEffect(GNB.Buffs.ReadyToRip)) 122 | return GNB.JugularRip; 123 | } 124 | 125 | // Gnashing Fang > Savage Claw > Wicked Talon 126 | return OriginalHook(GNB.GnashingFang); 127 | } 128 | 129 | return actionID; 130 | } 131 | } 132 | 133 | internal class GunbreakerBurstStrikeFatedCircle : CustomCombo 134 | { 135 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.GnbAny; 136 | 137 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 138 | { 139 | if (actionID == GNB.BurstStrike) 140 | { 141 | if (IsEnabled(CustomComboPreset.GunbreakerBurstStrikeCont)) 142 | { 143 | if (level >= GNB.Levels.EnhancedContinuation && HasEffect(GNB.Buffs.ReadyToBlast)) 144 | return GNB.Hypervelocity; 145 | } 146 | } 147 | 148 | if (actionID == GNB.BurstStrike || actionID == GNB.FatedCircle) 149 | { 150 | var gauge = GetJobGauge(); 151 | 152 | if (IsEnabled(CustomComboPreset.GunbreakerDoubleDownFeature)) 153 | { 154 | if (level >= GNB.Levels.DoubleDown && gauge.Ammo >= 2 && IsOffCooldown(GNB.DoubleDown)) 155 | return GNB.DoubleDown; 156 | } 157 | 158 | if (IsEnabled(CustomComboPreset.GunbreakerEmptyBloodfestFeature)) 159 | { 160 | if (level >= GNB.Levels.Bloodfest && gauge.Ammo == 0) 161 | return GNB.Bloodfest; 162 | } 163 | } 164 | 165 | return actionID; 166 | } 167 | } 168 | 169 | internal class GunbreakerBowShockSonicBreak : CustomCombo 170 | { 171 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.GunbreakerBowShockSonicBreakFeature; 172 | 173 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 174 | { 175 | if (actionID == GNB.BowShock || actionID == GNB.SonicBreak) 176 | { 177 | if (level >= GNB.Levels.BowShock) 178 | return CalcBestAction(actionID, GNB.BowShock, GNB.SonicBreak); 179 | } 180 | 181 | return actionID; 182 | } 183 | } 184 | 185 | internal class GunbreakerDemonSlaughter : CustomCombo 186 | { 187 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.GunbreakerDemonSlaughterCombo; 188 | 189 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 190 | { 191 | if (actionID == GNB.DemonSlaughter) 192 | { 193 | if (comboTime > 0 && lastComboMove == GNB.DemonSlice && level >= GNB.Levels.DemonSlaughter) 194 | { 195 | if (IsEnabled(CustomComboPreset.GunbreakerFatedCircleFeature)) 196 | { 197 | var gauge = GetJobGauge(); 198 | var maxAmmo = level >= GNB.Levels.CartridgeCharge2 ? 3 : 2; 199 | 200 | if (level >= GNB.Levels.FatedCircle && gauge.Ammo == maxAmmo) 201 | return GNB.FatedCircle; 202 | } 203 | 204 | return GNB.DemonSlaughter; 205 | } 206 | 207 | return GNB.DemonSlice; 208 | } 209 | 210 | return actionID; 211 | } 212 | } 213 | 214 | internal class GunbreakerNoMercy : CustomCombo 215 | { 216 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.GnbAny; 217 | 218 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 219 | { 220 | if (actionID == GNB.NoMercy) 221 | { 222 | var gauge = GetJobGauge(); 223 | 224 | if (IsEnabled(CustomComboPreset.GunbreakerNoMercyDoubleDownFeature)) 225 | { 226 | if (level >= GNB.Levels.NoMercy && HasEffect(GNB.Buffs.NoMercy)) 227 | { 228 | if (level >= GNB.Levels.DoubleDown && gauge.Ammo >= 2 && IsOffCooldown(GNB.DoubleDown)) 229 | return GNB.DoubleDown; 230 | } 231 | } 232 | 233 | if (IsEnabled(CustomComboPreset.GunbreakerNoMercyAlwaysDoubleDownFeature)) 234 | { 235 | if (level >= GNB.Levels.NoMercy && HasEffect(GNB.Buffs.NoMercy)) 236 | { 237 | if (level >= GNB.Levels.DoubleDown) 238 | return GNB.DoubleDown; 239 | } 240 | } 241 | 242 | if (IsEnabled(CustomComboPreset.GunbreakerNoMercyFeature)) 243 | { 244 | if (level >= GNB.Levels.NoMercy && HasEffect(GNB.Buffs.NoMercy)) 245 | { 246 | if (level >= GNB.Levels.BowShock) 247 | return CalcBestAction(GNB.SonicBreak, GNB.SonicBreak, GNB.BowShock); 248 | 249 | if (level >= GNB.Levels.SonicBreak) 250 | return GNB.SonicBreak; 251 | } 252 | } 253 | } 254 | 255 | return actionID; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /XIVComboExpanded/Combos/SGE.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | 3 | namespace XIVComboExpandedPlugin.Combos; 4 | 5 | internal static class SGE 6 | { 7 | public const byte JobID = 40; 8 | 9 | public const uint 10 | Dosis = 24283, 11 | Diagnosis = 24284, 12 | Kardia = 24285, 13 | Prognosis = 24286, 14 | Egeiro = 24287, 15 | Physis = 24288, 16 | Phlegma = 24289, 17 | Eukrasia = 24290, 18 | Soteria = 24294, 19 | Druochole = 24296, 20 | Dyskrasia = 24297, 21 | Kerachole = 24298, 22 | Ixochole = 24299, 23 | Zoe = 24300, 24 | Pepsis = 24301, 25 | Physis2 = 24302, 26 | Taurochole = 24303, 27 | Toxikon = 24304, 28 | Haima = 24305, 29 | Phlegma2 = 24307, 30 | Rhizomata = 24309, 31 | Holos = 24310, 32 | Panhaima = 24311, 33 | Phlegma3 = 24313, 34 | Krasis = 24317, 35 | Pneuma = 24318; 36 | 37 | public static class Buffs 38 | { 39 | public const ushort 40 | Kardion = 2604; 41 | } 42 | 43 | public static class Debuffs 44 | { 45 | public const ushort 46 | Placeholder = 0; 47 | } 48 | 49 | public static class Levels 50 | { 51 | public const ushort 52 | Dosis = 1, 53 | Prognosis = 10, 54 | Egeiro = 12, 55 | Phlegma = 26, 56 | Soteria = 35, 57 | Druochole = 45, 58 | Dyskrasia = 46, 59 | Kerachole = 50, 60 | Ixochole = 52, 61 | Physis2 = 60, 62 | Taurochole = 62, 63 | Toxicon = 66, 64 | Haima = 70, 65 | Phlegma2 = 72, 66 | Dosis2 = 72, 67 | Rhizomata = 74, 68 | Holos = 76, 69 | Panhaima = 80, 70 | Phlegma3 = 82, 71 | Dosis3 = 82, 72 | Krasis = 86, 73 | Pneuma = 90; 74 | } 75 | } 76 | 77 | internal class SageDosis : CustomCombo 78 | { 79 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SgeAny; 80 | 81 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 82 | { 83 | if (actionID == SGE.Dosis) 84 | { 85 | if (IsEnabled(CustomComboPreset.SageDosisKardiaFeature)) 86 | { 87 | if (!HasEffect(SGE.Buffs.Kardion)) 88 | return SGE.Kardia; 89 | } 90 | } 91 | 92 | return actionID; 93 | } 94 | } 95 | 96 | internal class SageToxikon : CustomCombo 97 | { 98 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SgeAny; 99 | 100 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 101 | { 102 | if (actionID == SGE.Toxikon) 103 | { 104 | if (IsEnabled(CustomComboPreset.SageToxikonPhlegma)) 105 | { 106 | var phlegma = 107 | level >= SGE.Levels.Phlegma3 ? SGE.Phlegma3 : 108 | level >= SGE.Levels.Phlegma2 ? SGE.Phlegma2 : 109 | level >= SGE.Levels.Phlegma ? SGE.Phlegma : 0; 110 | 111 | if (phlegma != 0 && HasCharges(phlegma)) 112 | return OriginalHook(SGE.Phlegma); 113 | } 114 | } 115 | 116 | return actionID; 117 | } 118 | } 119 | 120 | internal class SageSoteria : CustomCombo 121 | { 122 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SgeAny; 123 | 124 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 125 | { 126 | if (actionID == SGE.Soteria) 127 | { 128 | if (IsEnabled(CustomComboPreset.SageSoteriaKardionFeature)) 129 | { 130 | if (!HasEffect(SGE.Buffs.Kardion) && IsOffCooldown(SGE.Soteria)) 131 | return SGE.Kardia; 132 | } 133 | } 134 | 135 | return actionID; 136 | } 137 | } 138 | 139 | internal class SageTaurochole : CustomCombo 140 | { 141 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SgeAny; 142 | 143 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 144 | { 145 | if (actionID == SGE.Taurochole) 146 | { 147 | var gauge = GetJobGauge(); 148 | 149 | if (IsEnabled(CustomComboPreset.SageTaurocholeRhizomataFeature)) 150 | { 151 | if (level >= SGE.Levels.Rhizomata && gauge.Addersgall == 0) 152 | return SGE.Rhizomata; 153 | } 154 | 155 | if (IsEnabled(CustomComboPreset.SageTaurocholeDruocholeFeature)) 156 | { 157 | if (level >= SGE.Levels.Taurochole && IsOffCooldown(SGE.Taurochole)) 158 | return SGE.Taurochole; 159 | 160 | return SGE.Druochole; 161 | } 162 | } 163 | 164 | return actionID; 165 | } 166 | } 167 | 168 | internal class SageDruochole : CustomCombo 169 | { 170 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SgeAny; 171 | 172 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 173 | { 174 | if (actionID == SGE.Druochole) 175 | { 176 | var gauge = GetJobGauge(); 177 | 178 | if (IsEnabled(CustomComboPreset.SageDruocholeRhizomataFeature)) 179 | { 180 | if (level >= SGE.Levels.Rhizomata && gauge.Addersgall == 0) 181 | return SGE.Rhizomata; 182 | } 183 | 184 | if (IsEnabled(CustomComboPreset.SageDruocholeTaurocholeFeature)) 185 | { 186 | if (level >= SGE.Levels.Taurochole && IsOffCooldown(SGE.Taurochole)) 187 | return SGE.Taurochole; 188 | } 189 | } 190 | 191 | return actionID; 192 | } 193 | } 194 | 195 | internal class SageIxochole : CustomCombo 196 | { 197 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SgeAny; 198 | 199 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 200 | { 201 | if (actionID == SGE.Ixochole) 202 | { 203 | var gauge = GetJobGauge(); 204 | 205 | if (IsEnabled(CustomComboPreset.SageIxocholeRhizomataFeature)) 206 | { 207 | if (level >= SGE.Levels.Rhizomata && gauge.Addersgall == 0) 208 | return SGE.Rhizomata; 209 | } 210 | } 211 | 212 | return actionID; 213 | } 214 | } 215 | 216 | internal class SageKerachole : CustomCombo 217 | { 218 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SgeAny; 219 | 220 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 221 | { 222 | if (actionID == SGE.Kerachole) 223 | { 224 | var gauge = GetJobGauge(); 225 | 226 | if (IsEnabled(CustomComboPreset.SageKeracholaRhizomataFeature)) 227 | { 228 | if (level >= SGE.Levels.Rhizomata && gauge.Addersgall == 0) 229 | return SGE.Rhizomata; 230 | } 231 | } 232 | 233 | return actionID; 234 | } 235 | } 236 | 237 | internal class SagePhlegma : CustomCombo 238 | { 239 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SgeAny; 240 | 241 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 242 | { 243 | if (actionID == SGE.Phlegma || actionID == SGE.Phlegma2 || actionID == SGE.Phlegma3) 244 | { 245 | var gauge = GetJobGauge(); 246 | 247 | if (IsEnabled(CustomComboPreset.SagePhlegmaDyskrasia)) 248 | { 249 | if (level >= SGE.Levels.Dyskrasia && HasNoTarget()) 250 | return OriginalHook(SGE.Dyskrasia); 251 | } 252 | 253 | if (IsEnabled(CustomComboPreset.SagePhlegmaToxicon)) 254 | { 255 | var phlegma = 256 | level >= SGE.Levels.Phlegma3 ? SGE.Phlegma3 : 257 | level >= SGE.Levels.Phlegma2 ? SGE.Phlegma2 : 258 | level >= SGE.Levels.Phlegma ? SGE.Phlegma : 0; 259 | 260 | if (level >= SGE.Levels.Toxicon && phlegma != 0 && HasNoCharges(phlegma) && gauge.Addersting > 0) 261 | return OriginalHook(SGE.Toxikon); 262 | } 263 | 264 | if (IsEnabled(CustomComboPreset.SagePhlegmaDyskrasia)) 265 | { 266 | var phlegma = 267 | level >= SGE.Levels.Phlegma3 ? SGE.Phlegma3 : 268 | level >= SGE.Levels.Phlegma2 ? SGE.Phlegma2 : 269 | level >= SGE.Levels.Phlegma ? SGE.Phlegma : 0; 270 | 271 | if (level >= SGE.Levels.Dyskrasia && phlegma != 0 && HasNoCharges(phlegma)) 272 | return OriginalHook(SGE.Dyskrasia); 273 | } 274 | } 275 | 276 | return actionID; 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /XIVComboExpanded/Interface/ConfigWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Numerics; 5 | 6 | using Dalamud.Interface; 7 | using Dalamud.Interface.Colors; 8 | using Dalamud.Interface.Windowing; 9 | using Dalamud.Utility; 10 | using ImGuiNET; 11 | using XIVComboExpandedPlugin.Attributes; 12 | 13 | namespace XIVComboExpandedPlugin.Interface; 14 | 15 | /// 16 | /// Plugin configuration window. 17 | /// 18 | internal class ConfigWindow : Window 19 | { 20 | private readonly Dictionary> groupedPresets; 21 | private readonly Dictionary presetChildren; 22 | private readonly Vector4 shadedColor = new(0.68f, 0.68f, 0.68f, 1.0f); 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | public ConfigWindow() 28 | : base("Custom Combo Setup") 29 | { 30 | this.RespectCloseHotkey = true; 31 | 32 | this.groupedPresets = Enum 33 | .GetValues() 34 | .Where(preset => (int)preset > 100 && preset != CustomComboPreset.Disabled) 35 | .Select(preset => (Preset: preset, Info: preset.GetAttribute())) 36 | .Where(tpl => tpl.Info != null && Service.Configuration.GetParent(tpl.Preset) == null) 37 | .OrderBy(tpl => tpl.Info.JobName) 38 | .ThenBy(tpl => tpl.Info.Order) 39 | .GroupBy(tpl => tpl.Info.JobName) 40 | .ToDictionary( 41 | tpl => tpl.Key, 42 | tpl => tpl.ToList()); 43 | 44 | var childCombos = Enum.GetValues().ToDictionary( 45 | tpl => tpl, 46 | tpl => new List()); 47 | 48 | foreach (var preset in Enum.GetValues()) 49 | { 50 | var parent = preset.GetAttribute()?.ParentPreset; 51 | if (parent != null) 52 | childCombos[parent.Value].Add(preset); 53 | } 54 | 55 | this.presetChildren = childCombos.ToDictionary( 56 | kvp => kvp.Key, 57 | kvp => kvp.Value 58 | .Select(preset => (Preset: preset, Info: preset.GetAttribute())) 59 | .OrderBy(tpl => tpl.Info.Order).ToArray()); 60 | 61 | this.SizeCondition = ImGuiCond.FirstUseEver; 62 | this.Size = new Vector2(740, 490); 63 | } 64 | 65 | /// 66 | public override void Draw() 67 | { 68 | ImGui.Text("This window allows you to enable and disable custom combos to your liking."); 69 | 70 | var showSecrets = Service.Configuration.EnableSecretCombos; 71 | if (ImGui.Checkbox("Enable secret forbidden knowledge", ref showSecrets)) 72 | { 73 | Service.Configuration.EnableSecretCombos = showSecrets; 74 | Service.Configuration.Save(); 75 | } 76 | 77 | if (ImGui.IsItemHovered()) 78 | { 79 | ImGui.BeginTooltip(); 80 | ImGui.TextUnformatted("Combos too dangerous for the common folk"); 81 | ImGui.EndTooltip(); 82 | } 83 | 84 | var hideChildren = Service.Configuration.HideChildren; 85 | if (ImGui.Checkbox("Hide children of disabled combos and features", ref hideChildren)) 86 | { 87 | Service.Configuration.HideChildren = hideChildren; 88 | Service.Configuration.Save(); 89 | } 90 | 91 | ImGui.BeginChild("scrolling", new Vector2(0, -1), true); 92 | 93 | ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 5)); 94 | 95 | var i = 1; 96 | 97 | foreach (var jobName in this.groupedPresets.Keys) 98 | { 99 | if (ImGui.CollapsingHeader(jobName)) 100 | { 101 | foreach (var (preset, info) in this.groupedPresets[jobName]) 102 | { 103 | this.DrawPreset(preset, info, ref i); 104 | } 105 | } 106 | else 107 | { 108 | i += this.groupedPresets[jobName].Count; 109 | } 110 | } 111 | 112 | ImGui.PopStyleVar(); 113 | 114 | ImGui.EndChild(); 115 | } 116 | 117 | private void DrawPreset(CustomComboPreset preset, CustomComboInfoAttribute info, ref int i) 118 | { 119 | var enabled = Service.Configuration.IsEnabled(preset); 120 | var secret = Service.Configuration.IsSecret(preset); 121 | var showSecrets = Service.Configuration.EnableSecretCombos; 122 | var conflicts = Service.Configuration.GetConflicts(preset); 123 | var parent = Service.Configuration.GetParent(preset); 124 | 125 | if (secret && !showSecrets) 126 | return; 127 | 128 | ImGui.PushItemWidth(200); 129 | 130 | if (ImGui.Checkbox(info.FancyName, ref enabled)) 131 | { 132 | if (enabled) 133 | { 134 | this.EnableParentPresets(preset); 135 | Service.Configuration.EnabledActions.Add(preset); 136 | foreach (var conflict in conflicts) 137 | { 138 | Service.Configuration.EnabledActions.Remove(conflict); 139 | } 140 | } 141 | else 142 | { 143 | Service.Configuration.EnabledActions.Remove(preset); 144 | } 145 | 146 | Service.Configuration.Save(); 147 | } 148 | 149 | if (secret) 150 | { 151 | ImGui.SameLine(); 152 | ImGui.Text(" "); 153 | ImGui.SameLine(); 154 | ImGui.PushFont(UiBuilder.IconFont); 155 | ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.HealerGreen); 156 | ImGui.Text(FontAwesomeIcon.Star.ToIconString()); 157 | ImGui.PopStyleColor(); 158 | ImGui.PopFont(); 159 | 160 | if (ImGui.IsItemHovered()) 161 | { 162 | ImGui.BeginTooltip(); 163 | ImGui.TextUnformatted("Secret"); 164 | ImGui.EndTooltip(); 165 | } 166 | } 167 | 168 | ImGui.PopItemWidth(); 169 | 170 | ImGui.PushStyleColor(ImGuiCol.Text, this.shadedColor); 171 | ImGui.TextWrapped($"#{i}: {info.Description}"); 172 | ImGui.PopStyleColor(); 173 | ImGui.Spacing(); 174 | 175 | if (conflicts.Length > 0) 176 | { 177 | var conflictText = conflicts.Select(conflict => 178 | { 179 | if (!showSecrets && Service.Configuration.IsSecret(conflict)) 180 | return string.Empty; 181 | 182 | var conflictInfo = conflict.GetAttribute(); 183 | return $"\n - {conflictInfo.FancyName}"; 184 | }).Aggregate((t1, t2) => $"{t1}{t2}"); 185 | 186 | if (conflictText.Length > 0) 187 | { 188 | ImGui.TextColored(this.shadedColor, $"Conflicts with: {conflictText}"); 189 | ImGui.Spacing(); 190 | } 191 | } 192 | 193 | if (preset == CustomComboPreset.DancerDanceComboCompatibility && enabled) 194 | { 195 | var actions = Service.Configuration.DancerDanceCompatActionIDs.Cast().ToArray(); 196 | 197 | var inputChanged = false; 198 | inputChanged |= ImGui.InputInt("Emboite (Red) ActionID", ref actions[0], 0); 199 | inputChanged |= ImGui.InputInt("Entrechat (Blue) ActionID", ref actions[1], 0); 200 | inputChanged |= ImGui.InputInt("Jete (Green) ActionID", ref actions[2], 0); 201 | inputChanged |= ImGui.InputInt("Pirouette (Yellow) ActionID", ref actions[3], 0); 202 | 203 | if (inputChanged) 204 | { 205 | Service.Configuration.DancerDanceCompatActionIDs = actions.Cast().ToArray(); 206 | Service.Configuration.Save(); 207 | } 208 | 209 | ImGui.Spacing(); 210 | } 211 | 212 | i++; 213 | 214 | var hideChildren = Service.Configuration.HideChildren; 215 | if (enabled || !hideChildren) 216 | { 217 | var children = this.presetChildren[preset]; 218 | if (children.Length > 0) 219 | { 220 | ImGui.Indent(); 221 | 222 | foreach (var (childPreset, childInfo) in children) 223 | this.DrawPreset(childPreset, childInfo, ref i); 224 | 225 | ImGui.Unindent(); 226 | } 227 | } 228 | } 229 | 230 | /// 231 | /// Iterates up a preset's parent tree, enabling each of them. 232 | /// 233 | /// Combo preset to enabled. 234 | private void EnableParentPresets(CustomComboPreset preset) 235 | { 236 | var parentMaybe = Service.Configuration.GetParent(preset); 237 | while (parentMaybe != null) 238 | { 239 | var parent = parentMaybe.Value; 240 | 241 | if (!Service.Configuration.EnabledActions.Contains(parent)) 242 | { 243 | Service.Configuration.EnabledActions.Add(parent); 244 | foreach (var conflict in Service.Configuration.GetConflicts(parent)) 245 | { 246 | Service.Configuration.EnabledActions.Remove(conflict); 247 | } 248 | } 249 | 250 | parentMaybe = Service.Configuration.GetParent(parent); 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /XIVComboExpanded/Combos/MCH.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Dalamud.Game.ClientState.JobGauge.Types; 4 | 5 | namespace XIVComboExpandedPlugin.Combos; 6 | 7 | internal static class MCH 8 | { 9 | public const byte JobID = 31; 10 | 11 | public const uint 12 | // Single target 13 | CleanShot = 2873, 14 | HeatedCleanShot = 7413, 15 | SplitShot = 2866, 16 | HeatedSplitShot = 7411, 17 | SlugShot = 2868, 18 | HeatedSlugshot = 7412, 19 | // Charges 20 | GaussRound = 2874, 21 | Ricochet = 2890, 22 | // AoE 23 | SpreadShot = 2870, 24 | AutoCrossbow = 16497, 25 | Scattergun = 25786, 26 | // Rook 27 | RookAutoturret = 2864, 28 | RookOverdrive = 7415, 29 | AutomatonQueen = 16501, 30 | QueenOverdrive = 16502, 31 | // Other 32 | Wildfire = 2878, 33 | Detonator = 16766, 34 | Hypercharge = 17209, 35 | HeatBlast = 7410, 36 | HotShot = 2872, 37 | Drill = 16498, 38 | Bioblaster = 16499, 39 | AirAnchor = 16500, 40 | Chainsaw = 25788; 41 | 42 | public static class Buffs 43 | { 44 | public const ushort 45 | Placeholder = 0; 46 | } 47 | 48 | public static class Debuffs 49 | { 50 | public const ushort 51 | Placeholder = 0; 52 | } 53 | 54 | public static class Levels 55 | { 56 | public const byte 57 | SlugShot = 2, 58 | GaussRound = 15, 59 | CleanShot = 26, 60 | Hypercharge = 30, 61 | HeatBlast = 35, 62 | RookOverdrive = 40, 63 | Wildfire = 45, 64 | Ricochet = 50, 65 | AutoCrossbow = 52, 66 | HeatedSplitShot = 54, 67 | Drill = 58, 68 | HeatedSlugshot = 60, 69 | HeatedCleanShot = 64, 70 | ChargedActionMastery = 74, 71 | AirAnchor = 76, 72 | QueenOverdrive = 80, 73 | Chainsaw = 90; 74 | } 75 | } 76 | 77 | internal class MachinistCleanShot : CustomCombo 78 | { 79 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.MachinistMainCombo; 80 | 81 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 82 | { 83 | if (actionID == MCH.CleanShot || actionID == MCH.HeatedCleanShot) 84 | { 85 | var gauge = GetJobGauge(); 86 | 87 | if (IsEnabled(CustomComboPreset.MachinistHypercomboFeature)) 88 | { 89 | if (gauge.IsOverheated && level >= MCH.Levels.HeatBlast) 90 | return MCH.HeatBlast; 91 | } 92 | 93 | if (comboTime > 0) 94 | { 95 | if (lastComboMove == MCH.SlugShot && level >= MCH.Levels.CleanShot) 96 | // Heated 97 | return OriginalHook(MCH.CleanShot); 98 | 99 | if (lastComboMove == MCH.SplitShot && level >= MCH.Levels.SlugShot) 100 | // Heated 101 | return OriginalHook(MCH.SlugShot); 102 | } 103 | 104 | // Heated 105 | return OriginalHook(MCH.SplitShot); 106 | } 107 | 108 | return actionID; 109 | } 110 | } 111 | 112 | internal class MachinistGaussRoundRicochet : CustomCombo 113 | { 114 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.MachinistGaussRoundRicochetFeature; 115 | 116 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 117 | { 118 | if (actionID == MCH.GaussRound || actionID == MCH.Ricochet) 119 | { 120 | var gauge = GetJobGauge(); 121 | 122 | if (IsEnabled(CustomComboPreset.MachinistGaussRoundRicochetFeatureOption)) 123 | { 124 | if (!gauge.IsOverheated) 125 | return actionID; 126 | } 127 | 128 | if (level >= MCH.Levels.Ricochet) 129 | return CalcBestAction(actionID, MCH.GaussRound, MCH.Ricochet); 130 | 131 | return MCH.GaussRound; 132 | } 133 | 134 | return actionID; 135 | } 136 | } 137 | 138 | internal class MachinistWildfire : CustomCombo 139 | { 140 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.MachinistHyperfireFeature; 141 | 142 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 143 | { 144 | if (actionID == MCH.Hypercharge) 145 | { 146 | if (level >= MCH.Levels.Wildfire && IsOffCooldown(MCH.Wildfire) && HasTarget()) 147 | return MCH.Wildfire; 148 | 149 | if (level >= MCH.Levels.Wildfire && IsOnCooldown(MCH.Hypercharge) && !IsOriginal(MCH.Wildfire)) 150 | return MCH.Detonator; 151 | } 152 | 153 | return actionID; 154 | } 155 | } 156 | 157 | internal class MachinistHeatBlastAutoCrossbow : CustomCombo 158 | { 159 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.MachinistOverheatFeature; 160 | 161 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 162 | { 163 | if (actionID == MCH.HeatBlast || actionID == MCH.AutoCrossbow) 164 | { 165 | var gauge = GetJobGauge(); 166 | 167 | if (IsEnabled(CustomComboPreset.MachinistHyperfireFeature)) 168 | { 169 | if (level >= MCH.Levels.Wildfire && IsOffCooldown(MCH.Wildfire) && HasTarget()) 170 | return MCH.Wildfire; 171 | } 172 | 173 | if (level >= MCH.Levels.Hypercharge && !gauge.IsOverheated) 174 | return MCH.Hypercharge; 175 | 176 | if (level < MCH.Levels.AutoCrossbow) 177 | return MCH.HeatBlast; 178 | } 179 | 180 | return actionID; 181 | } 182 | } 183 | 184 | internal class MachinistSpreadShot : CustomCombo 185 | { 186 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.MachinistSpreadShotFeature; 187 | 188 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 189 | { 190 | if (actionID == MCH.SpreadShot || actionID == MCH.Scattergun) 191 | { 192 | var gauge = GetJobGauge(); 193 | 194 | if (level >= MCH.Levels.AutoCrossbow && gauge.IsOverheated) 195 | return MCH.AutoCrossbow; 196 | } 197 | 198 | return actionID; 199 | } 200 | } 201 | 202 | internal class MachinistRookAutoturret : CustomCombo 203 | { 204 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.MachinistOverdriveFeature; 205 | 206 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 207 | { 208 | if (actionID == MCH.RookAutoturret || actionID == MCH.AutomatonQueen) 209 | { 210 | var gauge = GetJobGauge(); 211 | 212 | if (level >= MCH.Levels.RookOverdrive && gauge.IsRobotActive) 213 | // Queen Overdrive 214 | return OriginalHook(MCH.RookOverdrive); 215 | } 216 | 217 | return actionID; 218 | } 219 | } 220 | 221 | internal class MachinistDrillAirAnchorChainsaw : CustomCombo 222 | { 223 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.MachinistHotShotDrillChainsawFeature; 224 | 225 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 226 | { 227 | if (actionID == MCH.HotShot || actionID == MCH.AirAnchor || actionID == MCH.Drill || actionID == MCH.Chainsaw) 228 | { 229 | if (level >= MCH.Levels.Chainsaw) 230 | return CalcBestAction(actionID, MCH.Chainsaw, MCH.AirAnchor, MCH.Drill); 231 | 232 | if (level >= MCH.Levels.AirAnchor) 233 | return CalcBestAction(actionID, MCH.AirAnchor, MCH.Drill); 234 | 235 | if (level >= MCH.Levels.Drill) 236 | return CalcBestAction(actionID, MCH.Drill, MCH.HotShot); 237 | 238 | return MCH.HotShot; 239 | } 240 | 241 | return actionID; 242 | } 243 | } 244 | 245 | internal class MachinistAirAnchorChainsaw : CustomCombo 246 | { 247 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.MachinistHotShotChainsawFeature; 248 | 249 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 250 | { 251 | if (actionID == MCH.HotShot || actionID == MCH.AirAnchor || actionID == MCH.Chainsaw) 252 | { 253 | if (level >= MCH.Levels.Chainsaw) 254 | return CalcBestAction(actionID, MCH.Chainsaw, MCH.AirAnchor); 255 | 256 | if (level >= MCH.Levels.AirAnchor) 257 | return MCH.AirAnchor; 258 | 259 | return MCH.HotShot; 260 | } 261 | 262 | return actionID; 263 | } 264 | } 265 | 266 | internal class MachinistBioblaster : CustomCombo 267 | { 268 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.MachinistBioblasterChainsawFeature; 269 | 270 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 271 | { 272 | if (actionID == MCH.Bioblaster) 273 | { 274 | if (level >= MCH.Levels.Chainsaw) 275 | return CalcBestAction(actionID, MCH.Chainsaw, MCH.Bioblaster); 276 | } 277 | 278 | return actionID; 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /XIVComboExpanded/Combos/SMN.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | 3 | namespace XIVComboExpandedPlugin.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 | Ruin = 163, 12 | Ruin2 = 172, 13 | Ruin3 = 3579, 14 | Ruin4 = 7426, 15 | Fester = 181, 16 | Painflare = 3578, 17 | DreadwyrmTrance = 3581, 18 | SummonBahamut = 7427, 19 | EnkindleBahamut = 7429, 20 | EnergySyphon = 16510, 21 | Outburst = 16511, 22 | EnergyDrain = 16508, 23 | SummonCarbuncle = 25798, 24 | RadiantAegis = 25799, 25 | Aethercharge = 25800, 26 | SearingLight = 25801, 27 | AstralFlow = 25822, 28 | TriDisaster = 25826, 29 | CrimsonCyclone = 25835, 30 | MountainBuster = 25836, 31 | Slipstream = 25837, 32 | CrimsonStrike = 25885, 33 | Gemshine = 25883, 34 | PreciousBrilliance = 25884; 35 | 36 | public static class Buffs 37 | { 38 | public const ushort 39 | FurtherRuin = 2701, 40 | IfritsFavor = 2724, 41 | GarudasFavor = 2725, 42 | TitansFavor = 2853; 43 | } 44 | 45 | public static class Debuffs 46 | { 47 | public const ushort 48 | Placeholder = 0; 49 | } 50 | 51 | public static class Levels 52 | { 53 | public const byte 54 | SummonCarbuncle = 2, 55 | RadiantAegis = 2, 56 | Gemshine = 6, 57 | EnergyDrain = 10, 58 | Fester = 10, 59 | PreciousBrilliance = 26, 60 | Painflare = 40, 61 | EnergySyphon = 52, 62 | Ruin3 = 54, 63 | Ruin4 = 62, 64 | SearingLight = 66, 65 | EnkindleBahamut = 70, 66 | Rekindle = 80, 67 | ElementalMastery = 86, 68 | SummonPhoenix = 80; 69 | } 70 | } 71 | 72 | internal class SummonerFester : CustomCombo 73 | { 74 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SummonerEDFesterFeature; 75 | 76 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 77 | { 78 | if (actionID == SMN.Fester) 79 | { 80 | var gauge = GetJobGauge(); 81 | 82 | if (level >= SMN.Levels.EnergyDrain && !gauge.HasAetherflowStacks) 83 | return SMN.EnergyDrain; 84 | } 85 | 86 | return actionID; 87 | } 88 | } 89 | 90 | internal class SummonerPainflare : CustomCombo 91 | { 92 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SummonerESPainflareFeature; 93 | 94 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 95 | { 96 | if (actionID == SMN.Painflare) 97 | { 98 | var gauge = GetJobGauge(); 99 | 100 | if (level >= SMN.Levels.EnergySyphon && !gauge.HasAetherflowStacks) 101 | return SMN.EnergySyphon; 102 | 103 | if (level >= SMN.Levels.EnergyDrain && !gauge.HasAetherflowStacks) 104 | return SMN.EnergyDrain; 105 | 106 | if (level < SMN.Levels.Painflare) 107 | return SMN.Fester; 108 | } 109 | 110 | return actionID; 111 | } 112 | } 113 | 114 | internal class SummonerRuin : CustomCombo 115 | { 116 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SmnAny; 117 | 118 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 119 | { 120 | if (actionID == SMN.Ruin || actionID == SMN.Ruin2 || actionID == SMN.Ruin3) 121 | { 122 | var gauge = GetJobGauge(); 123 | 124 | if (IsEnabled(CustomComboPreset.SummonerRuinTitansFavorFeature)) 125 | { 126 | if (level >= SMN.Levels.ElementalMastery && HasEffect(SMN.Buffs.TitansFavor)) 127 | return SMN.MountainBuster; 128 | } 129 | 130 | if (IsEnabled(CustomComboPreset.SummonerRuinFeature)) 131 | { 132 | if (level >= SMN.Levels.Gemshine) 133 | { 134 | if (gauge.IsIfritAttuned || gauge.IsTitanAttuned || gauge.IsGarudaAttuned) 135 | return OriginalHook(SMN.Gemshine); 136 | } 137 | } 138 | 139 | if (IsEnabled(CustomComboPreset.SummonerFurtherRuinFeature)) 140 | { 141 | if (level >= SMN.Levels.Ruin4 && gauge.SummonTimerRemaining == 0 && gauge.AttunmentTimerRemaining == 0 && HasEffect(SMN.Buffs.FurtherRuin)) 142 | return SMN.Ruin4; 143 | } 144 | } 145 | 146 | return actionID; 147 | } 148 | } 149 | 150 | internal class SummonerOutburstTriDisaster : CustomCombo 151 | { 152 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SmnAny; 153 | 154 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 155 | { 156 | if (actionID == SMN.Outburst || actionID == SMN.TriDisaster) 157 | { 158 | var gauge = GetJobGauge(); 159 | 160 | if (IsEnabled(CustomComboPreset.SummonerOutburstTitansFavorFeature)) 161 | { 162 | if (level >= SMN.Levels.ElementalMastery && HasEffect(SMN.Buffs.TitansFavor)) 163 | return SMN.MountainBuster; 164 | } 165 | 166 | if (IsEnabled(CustomComboPreset.SummonerOutburstFeature)) 167 | { 168 | if (level >= SMN.Levels.PreciousBrilliance) 169 | { 170 | if (gauge.IsIfritAttuned || gauge.IsTitanAttuned || gauge.IsGarudaAttuned) 171 | return OriginalHook(SMN.PreciousBrilliance); 172 | } 173 | } 174 | 175 | if (IsEnabled(CustomComboPreset.SummonerFurtherOutburstFeature)) 176 | { 177 | if (level >= SMN.Levels.Ruin4 && gauge.SummonTimerRemaining == 0 && gauge.AttunmentTimerRemaining == 0 && HasEffect(SMN.Buffs.FurtherRuin)) 178 | return SMN.Ruin4; 179 | } 180 | } 181 | 182 | return actionID; 183 | } 184 | } 185 | 186 | internal class SummonerGemshinePreciousBrilliance : CustomCombo 187 | { 188 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SmnAny; 189 | 190 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 191 | { 192 | if (actionID == SMN.Gemshine || actionID == SMN.PreciousBrilliance) 193 | { 194 | var gauge = GetJobGauge(); 195 | 196 | if (IsEnabled(CustomComboPreset.SummonerShinyTitansFavorFeature)) 197 | { 198 | if (level >= SMN.Levels.ElementalMastery && HasEffect(SMN.Buffs.TitansFavor)) 199 | return SMN.MountainBuster; 200 | } 201 | 202 | if (IsEnabled(CustomComboPreset.SummonerShinyEnkindleFeature)) 203 | { 204 | if (level >= SMN.Levels.EnkindleBahamut && !gauge.IsIfritAttuned && !gauge.IsTitanAttuned && !gauge.IsGarudaAttuned && gauge.SummonTimerRemaining > 0) 205 | // Rekindle 206 | return OriginalHook(SMN.EnkindleBahamut); 207 | } 208 | 209 | if (IsEnabled(CustomComboPreset.SummonerFurtherShinyFeature)) 210 | { 211 | if (level >= SMN.Levels.Ruin4 && gauge.SummonTimerRemaining == 0 && gauge.AttunmentTimerRemaining == 0 && HasEffect(SMN.Buffs.FurtherRuin)) 212 | return SMN.Ruin4; 213 | } 214 | } 215 | 216 | return actionID; 217 | } 218 | } 219 | 220 | internal class SummonerDemiFeature : CustomCombo 221 | { 222 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SmnAny; 223 | 224 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 225 | { 226 | if (actionID == SMN.Aethercharge || actionID == SMN.DreadwyrmTrance || actionID == SMN.SummonBahamut) 227 | { 228 | if (IsEnabled(CustomComboPreset.SummonerDemiCarbuncleFeature) && !HasPetPresent()) 229 | return SMN.SummonCarbuncle; 230 | 231 | var gauge = GetJobGauge(); 232 | 233 | if (IsEnabled(CustomComboPreset.SummonerDemiSearingLightFeature)) 234 | { 235 | if (level >= SMN.Levels.SearingLight && gauge.IsBahamutReady && InCombat() && IsOffCooldown(SMN.SearingLight)) 236 | return SMN.SearingLight; 237 | } 238 | 239 | if (IsEnabled(CustomComboPreset.SummonerDemiEnkindleFeature)) 240 | { 241 | if (level >= SMN.Levels.EnkindleBahamut && !gauge.IsIfritAttuned && !gauge.IsTitanAttuned && !gauge.IsGarudaAttuned && gauge.SummonTimerRemaining > 0) 242 | // Rekindle 243 | return OriginalHook(SMN.EnkindleBahamut); 244 | } 245 | } 246 | 247 | return actionID; 248 | } 249 | } 250 | 251 | internal class SummonerRadiantCarbundleFeature : CustomCombo 252 | { 253 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SummonerRadiantCarbuncleFeature; 254 | 255 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 256 | { 257 | if (actionID == SMN.RadiantAegis) 258 | { 259 | var gauge = GetJobGauge(); 260 | 261 | if (level >= SMN.Levels.SummonCarbuncle && !HasPetPresent() && gauge.Attunement == 0) 262 | return SMN.SummonCarbuncle; 263 | } 264 | 265 | return actionID; 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /XIVComboExpanded/Combos/WAR.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Dalamud.Game.ClientState.JobGauge.Types; 4 | 5 | namespace XIVComboExpandedPlugin.Combos; 6 | 7 | internal static class WAR 8 | { 9 | public const byte ClassID = 3; 10 | public const byte JobID = 21; 11 | 12 | public const uint 13 | HeavySwing = 31, 14 | Maim = 37, 15 | Berserk = 38, 16 | ThrillOfBattle = 40, 17 | Overpower = 41, 18 | StormsPath = 42, 19 | StormsEye = 45, 20 | InnerBeast = 49, 21 | SteelCyclone = 51, 22 | Infuriate = 52, 23 | FellCleave = 3549, 24 | Decimate = 3550, 25 | RawIntuition = 3551, 26 | Equilibrium = 3552, 27 | InnerRelease = 7389, 28 | MythrilTempest = 16462, 29 | ChaoticCyclone = 16463, 30 | NascentFlash = 16464, 31 | InnerChaos = 16465, 32 | Bloodwhetting = 25751, 33 | PrimalRend = 25753; 34 | 35 | public static class Buffs 36 | { 37 | public const ushort 38 | Berserk = 86, 39 | InnerRelease = 1177, 40 | NascentChaos = 1897, 41 | PrimalRendReady = 2624, 42 | SurgingTempest = 2677; 43 | } 44 | 45 | public static class Debuffs 46 | { 47 | public const ushort 48 | Placeholder = 0; 49 | } 50 | 51 | public static class Levels 52 | { 53 | public const byte 54 | Maim = 4, 55 | Berserk = 6, 56 | StormsPath = 26, 57 | ThrillOfBattle = 30, 58 | InnerBeast = 35, 59 | MythrilTempest = 40, 60 | StormsEye = 50, 61 | Infuriate = 50, 62 | FellCleave = 54, 63 | RawIntuition = 56, 64 | Equilibrium = 58, 65 | Decimate = 60, 66 | InnerRelease = 70, 67 | MythrilTempestTrait = 74, 68 | NascentFlash = 76, 69 | InnerChaos = 80, 70 | Bloodwhetting = 82, 71 | PrimalRend = 90; 72 | } 73 | } 74 | 75 | internal class WarriorStormsPathCombo : CustomCombo 76 | { 77 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.WarriorStormsPathCombo; 78 | 79 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 80 | { 81 | if (actionID == WAR.StormsPath) 82 | { 83 | var gauge = GetJobGauge(); 84 | 85 | if (IsEnabled(CustomComboPreset.WarriorStormsPathInnerReleaseFeature)) 86 | { 87 | if (level >= WAR.Levels.InnerRelease && HasEffect(WAR.Buffs.InnerRelease)) 88 | return OriginalHook(WAR.FellCleave); 89 | } 90 | 91 | if (comboTime > 0) 92 | { 93 | if (lastComboMove == WAR.Maim && level >= WAR.Levels.StormsPath) 94 | { 95 | if (IsEnabled(CustomComboPreset.WarriorStormsPathOvercapFeature)) 96 | { 97 | if (level >= WAR.Levels.InnerBeast && gauge.BeastGauge > 80) 98 | // Fell Cleave 99 | return OriginalHook(WAR.InnerBeast); 100 | } 101 | 102 | #if DEBUG 103 | if (level >= WAR.Levels.StormsEye) 104 | { 105 | var surgingTempest = FindEffect(WAR.Buffs.SurgingTempest); 106 | if (surgingTempest is null) 107 | return WAR.StormsEye; 108 | 109 | // Medicated + Opener 110 | if (HasEffect(ADV.Buffs.Medicated) && surgingTempest.RemainingTime > 10) 111 | return WAR.StormsPath; 112 | 113 | var innerReleleaseCd = GetCooldown(WAR.InnerRelease).CooldownRemaining; 114 | var surgingTempestFromIR = Math.Max(0, 10 - innerReleleaseCd); 115 | if (surgingTempest.RemainingTime + 30 + surgingTempestFromIR < 60) 116 | return WAR.StormsEye; 117 | } 118 | #endif 119 | 120 | return WAR.StormsPath; 121 | } 122 | 123 | if (lastComboMove == WAR.HeavySwing && level >= WAR.Levels.Maim) 124 | { 125 | if (IsEnabled(CustomComboPreset.WarriorStormsPathOvercapFeature)) 126 | { 127 | if (level >= WAR.Levels.InnerBeast && gauge.BeastGauge > 90) 128 | // Fell Cleave 129 | return OriginalHook(WAR.InnerBeast); 130 | } 131 | 132 | return WAR.Maim; 133 | } 134 | } 135 | 136 | return WAR.HeavySwing; 137 | } 138 | 139 | return actionID; 140 | } 141 | } 142 | 143 | internal class WarriorStormsEyeCombo : CustomCombo 144 | { 145 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.WarriorStormsEyeCombo; 146 | 147 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 148 | { 149 | if (actionID == WAR.StormsEye) 150 | { 151 | if (comboTime > 0) 152 | { 153 | if (lastComboMove == WAR.Maim && level >= WAR.Levels.StormsEye) 154 | return WAR.StormsEye; 155 | 156 | if (lastComboMove == WAR.HeavySwing && level >= WAR.Levels.Maim) 157 | return WAR.Maim; 158 | } 159 | 160 | return WAR.HeavySwing; 161 | } 162 | 163 | return actionID; 164 | } 165 | } 166 | 167 | internal class WarriorMythrilTempestCombo : CustomCombo 168 | { 169 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.WarriorMythrilTempestCombo; 170 | 171 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 172 | { 173 | if (actionID == WAR.MythrilTempest) 174 | { 175 | var gauge = GetJobGauge(); 176 | 177 | if (IsEnabled(CustomComboPreset.WarriorMythrilTempestInnerReleaseFeature)) 178 | { 179 | if (level >= WAR.Levels.InnerRelease && HasEffect(WAR.Buffs.InnerRelease)) 180 | return OriginalHook(WAR.Decimate); 181 | } 182 | 183 | if (comboTime > 0) 184 | { 185 | if (lastComboMove == WAR.Overpower && level >= WAR.Levels.MythrilTempest) 186 | { 187 | if (IsEnabled(CustomComboPreset.WarriorMythrilTempestOvercapFeature)) 188 | { 189 | if (level >= WAR.Levels.MythrilTempestTrait && gauge.BeastGauge > 80) 190 | return OriginalHook(WAR.Decimate); 191 | } 192 | 193 | return WAR.MythrilTempest; 194 | } 195 | } 196 | 197 | return WAR.Overpower; 198 | } 199 | 200 | return actionID; 201 | } 202 | } 203 | 204 | internal class WarriorFellCleaveDecimate : CustomCombo 205 | { 206 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.WarAny; 207 | 208 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 209 | { 210 | if (actionID == WAR.InnerBeast || actionID == WAR.FellCleave || actionID == WAR.SteelCyclone || actionID == WAR.Decimate) 211 | { 212 | if (IsEnabled(CustomComboPreset.WarriorPrimalBeastFeature)) 213 | { 214 | if (level >= WAR.Levels.PrimalRend && HasEffect(WAR.Buffs.PrimalRendReady)) 215 | return WAR.PrimalRend; 216 | } 217 | 218 | if (IsEnabled(CustomComboPreset.WarriorInfuriateBeastFeature)) 219 | { 220 | var gauge = GetJobGauge(); 221 | 222 | if (level >= WAR.Levels.Infuriate && gauge.BeastGauge < 50 && !HasEffect(WAR.Buffs.InnerRelease)) 223 | return WAR.Infuriate; 224 | } 225 | } 226 | 227 | return actionID; 228 | } 229 | } 230 | 231 | internal class WarriorBerserkInnerRelease : CustomCombo 232 | { 233 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.WarriorPrimalReleaseFeature; 234 | 235 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 236 | { 237 | if (actionID == WAR.Berserk || actionID == WAR.InnerRelease) 238 | { 239 | if (level >= WAR.Levels.PrimalRend && HasEffect(WAR.Buffs.PrimalRendReady)) 240 | return WAR.PrimalRend; 241 | } 242 | 243 | return actionID; 244 | } 245 | } 246 | 247 | internal class WarriorNascentFlashFeature : CustomCombo 248 | { 249 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.WarriorNascentFlashSyncFeature; 250 | 251 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 252 | { 253 | if (actionID == WAR.NascentFlash) 254 | { 255 | if (level >= WAR.Levels.NascentFlash) 256 | return WAR.NascentFlash; 257 | 258 | if (level >= WAR.Levels.RawIntuition) 259 | return WAR.RawIntuition; 260 | } 261 | 262 | return actionID; 263 | } 264 | } 265 | 266 | internal class WarriorBloodwhetting : CustomCombo 267 | { 268 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.WarAny; 269 | 270 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 271 | { 272 | if (actionID == WAR.Bloodwhetting || actionID == WAR.RawIntuition) 273 | { 274 | if (IsEnabled(CustomComboPreset.WarriorHealthyBalancedDietFeature)) 275 | { 276 | if (level >= WAR.Levels.Bloodwhetting) 277 | { 278 | if (IsOffCooldown(WAR.Bloodwhetting)) 279 | return WAR.Bloodwhetting; 280 | } 281 | else if (level >= WAR.Levels.RawIntuition) 282 | { 283 | if (IsOffCooldown(WAR.RawIntuition)) 284 | return WAR.RawIntuition; 285 | } 286 | 287 | if (level >= WAR.Levels.ThrillOfBattle && IsOffCooldown(WAR.ThrillOfBattle)) 288 | return WAR.ThrillOfBattle; 289 | 290 | if (level >= WAR.Levels.Equilibrium && IsOffCooldown(WAR.Equilibrium)) 291 | return WAR.Equilibrium; 292 | } 293 | } 294 | 295 | return actionID; 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /XIVComboExpanded/Combos/DNC.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | using Dalamud.Game.ClientState.JobGauge.Types; 5 | 6 | namespace XIVComboExpandedPlugin.Combos; 7 | 8 | internal static class DNC 9 | { 10 | public const byte JobID = 38; 11 | 12 | public const uint 13 | // Single Target 14 | Cascade = 15989, 15 | Fountain = 15990, 16 | ReverseCascade = 15991, 17 | Fountainfall = 15992, 18 | // AoE 19 | Windmill = 15993, 20 | Bladeshower = 15994, 21 | RisingWindmill = 15995, 22 | Bloodshower = 15996, 23 | // Dancing 24 | StandardStep = 15997, 25 | TechnicalStep = 15998, 26 | Tillana = 25790, 27 | // Fans 28 | FanDance1 = 16007, 29 | FanDance2 = 16008, 30 | FanDance3 = 16009, 31 | FanDance4 = 25791, 32 | // Other 33 | SaberDance = 16005, 34 | EnAvant = 16010, 35 | Devilment = 16011, 36 | Flourish = 16013, 37 | Improvisation = 16014, 38 | StarfallDance = 25792; 39 | 40 | public static class Buffs 41 | { 42 | public const ushort 43 | FlourishingSymmetry = 3017, 44 | FlourishingFlow = 3018, 45 | FlourishingFinish = 2698, 46 | FlourishingStarfall = 2700, 47 | SilkenSymmetry = 2693, 48 | SilkenFlow = 2694, 49 | StandardStep = 1818, 50 | TechnicalStep = 1819, 51 | ThreefoldFanDance = 1820, 52 | FourfoldFanDance = 2699; 53 | } 54 | 55 | public static class Debuffs 56 | { 57 | public const ushort 58 | Placeholder = 0; 59 | } 60 | 61 | public static class Levels 62 | { 63 | public const byte 64 | Cascade = 1, 65 | Fountain = 2, 66 | Windmill = 15, 67 | StandardStep = 15, 68 | ReverseCascade = 20, 69 | Bladeshower = 25, 70 | RisingWindmill = 35, 71 | Fountainfall = 40, 72 | Bloodshower = 45, 73 | FanDance3 = 66, 74 | TechnicalStep = 70, 75 | Flourish = 72, 76 | Tillana = 82, 77 | FanDance4 = 86, 78 | StarfallDance = 90; 79 | } 80 | } 81 | 82 | internal class DancerDanceComboCompatibility : CustomCombo 83 | { 84 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DancerDanceComboCompatibility; 85 | 86 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 87 | { 88 | var actionIDs = Service.Configuration.DancerDanceCompatActionIDs; 89 | 90 | if (actionIDs.Contains(actionID)) 91 | { 92 | var gauge = GetJobGauge(); 93 | 94 | if (level >= DNC.Levels.StandardStep && gauge.IsDancing) 95 | { 96 | if (actionID == actionIDs[0] || (actionIDs[0] == 0 && actionID == DNC.Cascade)) 97 | return OriginalHook(DNC.Cascade); 98 | 99 | if (actionID == actionIDs[1] || (actionIDs[1] == 0 && actionID == DNC.Flourish)) 100 | return OriginalHook(DNC.Fountain); 101 | 102 | if (actionID == actionIDs[2] || (actionIDs[2] == 0 && actionID == DNC.FanDance1)) 103 | return OriginalHook(DNC.ReverseCascade); 104 | 105 | if (actionID == actionIDs[3] || (actionIDs[3] == 0 && actionID == DNC.FanDance2)) 106 | return OriginalHook(DNC.Fountainfall); 107 | } 108 | } 109 | 110 | return actionID; 111 | } 112 | } 113 | 114 | internal class DancerFanDance12 : CustomCombo 115 | { 116 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DncAny; 117 | 118 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 119 | { 120 | if (actionID == DNC.FanDance1 || actionID == DNC.FanDance2) 121 | { 122 | var gauge = GetJobGauge(); 123 | 124 | if (IsEnabled(CustomComboPreset.DancerFanDance3Feature)) 125 | { 126 | if (IsEnabled(CustomComboPreset.DancerFanDance4Feature)) 127 | { 128 | if (gauge.Feathers == 4) 129 | { 130 | if (level >= DNC.Levels.FanDance3 && HasEffect(DNC.Buffs.ThreefoldFanDance)) 131 | return DNC.FanDance3; 132 | 133 | return actionID; 134 | } 135 | 136 | if (level >= DNC.Levels.FanDance4 && HasEffect(DNC.Buffs.FourfoldFanDance)) 137 | return DNC.FanDance4; 138 | } 139 | 140 | if (level >= DNC.Levels.FanDance3 && HasEffect(DNC.Buffs.ThreefoldFanDance)) 141 | return DNC.FanDance3; 142 | } 143 | } 144 | 145 | return actionID; 146 | } 147 | } 148 | 149 | internal class DancerStandardStepTechnicalStep : CustomCombo 150 | { 151 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DancerDanceStepCombo; 152 | 153 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 154 | { 155 | if (actionID == DNC.StandardStep) 156 | { 157 | var gauge = GetJobGauge(); 158 | 159 | if (level >= DNC.Levels.StandardStep && gauge.IsDancing && HasEffect(DNC.Buffs.StandardStep)) 160 | { 161 | if (gauge.CompletedSteps < 2) 162 | return gauge.NextStep; 163 | 164 | return OriginalHook(DNC.StandardStep); 165 | } 166 | 167 | return DNC.StandardStep; 168 | } 169 | 170 | if (actionID == DNC.TechnicalStep) 171 | { 172 | var gauge = GetJobGauge(); 173 | 174 | if (level >= DNC.Levels.TechnicalStep && gauge.IsDancing && HasEffect(DNC.Buffs.TechnicalStep)) 175 | { 176 | if (gauge.CompletedSteps < 4) 177 | return gauge.NextStep; 178 | } 179 | 180 | // Tillana 181 | return OriginalHook(DNC.TechnicalStep); 182 | } 183 | 184 | return actionID; 185 | } 186 | } 187 | 188 | internal class DancerFlourish : CustomCombo 189 | { 190 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DncAny; 191 | 192 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 193 | { 194 | if (actionID == DNC.Flourish) 195 | { 196 | if (IsEnabled(CustomComboPreset.DancerFlourishFan3Feature)) 197 | { 198 | if (level >= DNC.Levels.FanDance3 && HasEffect(DNC.Buffs.ThreefoldFanDance)) 199 | return DNC.FanDance3; 200 | } 201 | 202 | if (IsEnabled(CustomComboPreset.DancerFlourishFan4Feature)) 203 | { 204 | if (level >= DNC.Levels.FanDance4 && HasEffect(DNC.Buffs.FourfoldFanDance)) 205 | return DNC.FanDance4; 206 | } 207 | } 208 | 209 | return actionID; 210 | } 211 | } 212 | 213 | internal class DancerCascadeFountain : CustomCombo 214 | { 215 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DncAny; 216 | 217 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 218 | { 219 | if (actionID == DNC.Cascade) 220 | { 221 | if (IsEnabled(CustomComboPreset.DancerSingleTargetMultibutton)) 222 | { 223 | if (level >= DNC.Levels.Fountainfall && (HasEffect(DNC.Buffs.FlourishingFlow) || HasEffect(DNC.Buffs.SilkenFlow))) 224 | return DNC.Fountainfall; 225 | 226 | if (level >= DNC.Levels.ReverseCascade && (HasEffect(DNC.Buffs.FlourishingSymmetry) || HasEffect(DNC.Buffs.SilkenSymmetry))) 227 | return DNC.ReverseCascade; 228 | 229 | if (lastComboMove == DNC.Cascade && level >= DNC.Levels.Fountain) 230 | return DNC.Fountain; 231 | } 232 | 233 | if (IsEnabled(CustomComboPreset.DancerSingleTargetProcs)) 234 | { 235 | if (level >= DNC.Levels.ReverseCascade && (HasEffect(DNC.Buffs.FlourishingSymmetry) || HasEffect(DNC.Buffs.SilkenSymmetry))) 236 | return DNC.ReverseCascade; 237 | } 238 | } 239 | 240 | if (actionID == DNC.Fountain) 241 | { 242 | if (IsEnabled(CustomComboPreset.DancerSingleTargetProcs)) 243 | { 244 | if (level >= DNC.Levels.Fountainfall && (HasEffect(DNC.Buffs.FlourishingFlow) || HasEffect(DNC.Buffs.SilkenFlow))) 245 | return DNC.Fountainfall; 246 | } 247 | } 248 | 249 | return actionID; 250 | } 251 | } 252 | 253 | internal class DancerWindmillBladeshower : CustomCombo 254 | { 255 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DncAny; 256 | 257 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 258 | { 259 | if (actionID == DNC.Windmill) 260 | { 261 | if (IsEnabled(CustomComboPreset.DancerAoeMultibutton)) 262 | { 263 | if (level >= DNC.Levels.Bloodshower && (HasEffect(DNC.Buffs.FlourishingFlow) || HasEffect(DNC.Buffs.SilkenFlow))) 264 | return DNC.Bloodshower; 265 | 266 | if (level >= DNC.Levels.RisingWindmill && (HasEffect(DNC.Buffs.FlourishingSymmetry) || HasEffect(DNC.Buffs.SilkenSymmetry))) 267 | return DNC.RisingWindmill; 268 | 269 | if (lastComboMove == DNC.Windmill && level >= DNC.Levels.Bladeshower) 270 | return DNC.Bladeshower; 271 | } 272 | 273 | if (IsEnabled(CustomComboPreset.DancerAoeProcs)) 274 | { 275 | if (level >= DNC.Levels.RisingWindmill && (HasEffect(DNC.Buffs.FlourishingSymmetry) || HasEffect(DNC.Buffs.SilkenSymmetry))) 276 | return DNC.RisingWindmill; 277 | } 278 | } 279 | 280 | if (actionID == DNC.Bladeshower) 281 | { 282 | if (IsEnabled(CustomComboPreset.DancerAoeProcs)) 283 | { 284 | if (level >= DNC.Levels.Bloodshower && (HasEffect(DNC.Buffs.FlourishingFlow) || HasEffect(DNC.Buffs.SilkenFlow))) 285 | return DNC.Bloodshower; 286 | } 287 | } 288 | 289 | return actionID; 290 | } 291 | } 292 | 293 | internal class DancerDevilment : CustomCombo 294 | { 295 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DancerDevilmentFeature; 296 | 297 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 298 | { 299 | if (actionID == DNC.Devilment) 300 | { 301 | if (level >= DNC.Levels.StarfallDance && HasEffect(DNC.Buffs.FlourishingStarfall)) 302 | return DNC.StarfallDance; 303 | } 304 | 305 | return actionID; 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /XIVComboExpanded/Combos/NIN.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | 3 | namespace XIVComboExpandedPlugin.Combos; 4 | 5 | internal static class NIN 6 | { 7 | public const byte ClassID = 29; 8 | public const byte JobID = 30; 9 | 10 | public const uint 11 | SpinningEdge = 2240, 12 | GustSlash = 2242, 13 | Hide = 2245, 14 | Assassinate = 8814, 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 | TenChiJin = 7403, 26 | HakkeMujinsatsu = 16488, 27 | Meisui = 16489, 28 | Jin = 18807, 29 | Bunshin = 16493, 30 | Huraijin = 25876, 31 | PhantomKamaitachi = 25774, 32 | ForkedRaiju = 25777, 33 | FleetingRaiju = 25778; 34 | 35 | public static class Buffs 36 | { 37 | public const ushort 38 | Mudra = 496, 39 | Kassatsu = 497, 40 | Suiton = 507, 41 | Hidden = 614, 42 | Bunshin = 1954, 43 | RaijuReady = 2690; 44 | } 45 | 46 | public static class Debuffs 47 | { 48 | public const ushort 49 | Placeholder = 0; 50 | } 51 | 52 | public static class Levels 53 | { 54 | public const byte 55 | GustSlash = 4, 56 | Hide = 10, 57 | Mug = 15, 58 | AeolianEdge = 26, 59 | Ninjitsu = 30, 60 | Suiton = 45, 61 | HakkeMujinsatsu = 52, 62 | ArmorCrush = 54, 63 | Huraijin = 60, 64 | TenChiJin = 70, 65 | Meisui = 72, 66 | EnhancedKassatsu = 76, 67 | Bunshin = 80, 68 | PhantomKamaitachi = 82, 69 | Raiju = 90; 70 | } 71 | } 72 | 73 | internal class NinjaAeolianEdge : CustomCombo 74 | { 75 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.NinAny; 76 | 77 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 78 | { 79 | if (actionID == NIN.AeolianEdge) 80 | { 81 | var gauge = GetJobGauge(); 82 | 83 | if (IsEnabled(CustomComboPreset.NinjaAeolianNinjutsuFeature)) 84 | { 85 | if (level >= NIN.Levels.Ninjitsu && HasEffect(NIN.Buffs.Mudra)) 86 | return OriginalHook(NIN.Ninjutsu); 87 | } 88 | 89 | if (IsEnabled(CustomComboPreset.NinjaAeolianEdgeRaijuFeature)) 90 | { 91 | if (level >= NIN.Levels.Raiju && HasEffect(NIN.Buffs.RaijuReady)) 92 | return NIN.FleetingRaiju; 93 | } 94 | 95 | if (IsEnabled(CustomComboPreset.NinjaAeolianEdgeHutonFeature)) 96 | { 97 | if (level >= NIN.Levels.Huraijin && gauge.HutonTimer == 0) 98 | return NIN.Huraijin; 99 | 100 | if (comboTime > 0) 101 | { 102 | if (lastComboMove == NIN.GustSlash && level >= NIN.Levels.ArmorCrush && gauge.HutonTimer <= 30_000) 103 | return NIN.ArmorCrush; 104 | } 105 | } 106 | 107 | if (IsEnabled(CustomComboPreset.NinjaAeolianEdgeCombo)) 108 | { 109 | if (comboTime > 0) 110 | { 111 | if (lastComboMove == NIN.GustSlash && level >= NIN.Levels.AeolianEdge) 112 | return NIN.AeolianEdge; 113 | 114 | if (lastComboMove == NIN.SpinningEdge && level >= NIN.Levels.GustSlash) 115 | return NIN.GustSlash; 116 | } 117 | 118 | return NIN.SpinningEdge; 119 | } 120 | } 121 | 122 | return actionID; 123 | } 124 | } 125 | 126 | internal class NinjaArmorCrush : CustomCombo 127 | { 128 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.NinAny; 129 | 130 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 131 | { 132 | if (actionID == NIN.ArmorCrush) 133 | { 134 | if (IsEnabled(CustomComboPreset.NinjaArmorCrushRaijuFeature)) 135 | { 136 | if (level >= NIN.Levels.Raiju && HasEffect(NIN.Buffs.RaijuReady)) 137 | return NIN.ForkedRaiju; 138 | } 139 | 140 | if (IsEnabled(CustomComboPreset.NinjaArmorCrushNinjutsuFeature)) 141 | { 142 | if (level >= NIN.Levels.Ninjitsu && HasEffect(NIN.Buffs.Mudra)) 143 | return OriginalHook(NIN.Ninjutsu); 144 | } 145 | 146 | if (IsEnabled(CustomComboPreset.NinjaArmorCrushCombo)) 147 | { 148 | if (comboTime > 0) 149 | { 150 | if (lastComboMove == NIN.GustSlash && level >= NIN.Levels.ArmorCrush) 151 | return NIN.ArmorCrush; 152 | 153 | if (lastComboMove == NIN.SpinningEdge && level >= NIN.Levels.GustSlash) 154 | return NIN.GustSlash; 155 | } 156 | 157 | return NIN.SpinningEdge; 158 | } 159 | } 160 | 161 | return actionID; 162 | } 163 | } 164 | 165 | internal class NinjaHuraijin : CustomCombo 166 | { 167 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.NinAny; 168 | 169 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 170 | { 171 | if (actionID == NIN.Huraijin) 172 | { 173 | if (level >= NIN.Levels.Raiju && HasEffect(NIN.Buffs.RaijuReady)) 174 | { 175 | if (IsEnabled(CustomComboPreset.NinjaHuraijinForkedRaijuFeature)) 176 | return NIN.ForkedRaiju; 177 | 178 | if (IsEnabled(CustomComboPreset.NinjaHuraijinFleetingRaijuFeature)) 179 | return NIN.FleetingRaiju; 180 | } 181 | 182 | if (IsEnabled(CustomComboPreset.NinjaHuraijinNinjutsuFeature)) 183 | { 184 | if (level >= NIN.Levels.Ninjitsu && HasEffect(NIN.Buffs.Mudra)) 185 | return OriginalHook(NIN.Ninjutsu); 186 | } 187 | 188 | if (IsEnabled(CustomComboPreset.NinjaHuraijinArmorCrushCombo)) 189 | { 190 | var gauge = GetJobGauge(); 191 | 192 | if (comboTime > 0 && gauge.HutonTimer > 0) 193 | { 194 | if (lastComboMove == NIN.GustSlash && level >= NIN.Levels.ArmorCrush) 195 | return NIN.ArmorCrush; 196 | } 197 | } 198 | } 199 | 200 | return actionID; 201 | } 202 | } 203 | 204 | internal class NinjaHakkeMujinsatsu : CustomCombo 205 | { 206 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.NinAny; 207 | 208 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 209 | { 210 | if (actionID == NIN.HakkeMujinsatsu) 211 | { 212 | if (IsEnabled(CustomComboPreset.NinjaHakkeMujinsatsuNinjutsuFeature)) 213 | { 214 | if (level >= NIN.Levels.Ninjitsu && HasEffect(NIN.Buffs.Mudra)) 215 | return OriginalHook(NIN.Ninjutsu); 216 | } 217 | 218 | if (IsEnabled(CustomComboPreset.NinjaHakkeMujinsatsuCombo)) 219 | { 220 | if (comboTime > 0) 221 | { 222 | if (lastComboMove == NIN.DeathBlossom && level >= NIN.Levels.HakkeMujinsatsu) 223 | return NIN.HakkeMujinsatsu; 224 | } 225 | 226 | return NIN.DeathBlossom; 227 | } 228 | } 229 | 230 | return actionID; 231 | } 232 | } 233 | 234 | internal class NinjaKassatsu : CustomCombo 235 | { 236 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.NinjaKassatsuTrickFeature; 237 | 238 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 239 | { 240 | if (actionID == NIN.Kassatsu) 241 | { 242 | if ((level >= NIN.Levels.Hide && HasEffect(NIN.Buffs.Hidden)) || 243 | (level >= NIN.Levels.Suiton && HasEffect(NIN.Buffs.Suiton))) 244 | return NIN.TrickAttack; 245 | } 246 | 247 | return actionID; 248 | } 249 | } 250 | 251 | internal class NinjaHide : CustomCombo 252 | { 253 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.NinAny; 254 | 255 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 256 | { 257 | if (actionID == NIN.Hide) 258 | { 259 | if (IsEnabled(CustomComboPreset.NinjaHideNinjutsuFeature)) 260 | { 261 | if (level >= NIN.Levels.Ninjitsu && HasEffect(NIN.Buffs.Mudra)) 262 | return OriginalHook(NIN.Ninjutsu); 263 | } 264 | 265 | if (IsEnabled(CustomComboPreset.NinjaHideMugFeature)) 266 | { 267 | if (level >= NIN.Levels.Mug && InCombat()) 268 | return NIN.Mug; 269 | } 270 | } 271 | 272 | return actionID; 273 | } 274 | } 275 | 276 | internal class NinjaChi : CustomCombo 277 | { 278 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.NinjaKassatsuChiJinFeature; 279 | 280 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 281 | { 282 | if (actionID == NIN.Chi) 283 | { 284 | if (level >= NIN.Levels.EnhancedKassatsu && HasEffect(NIN.Buffs.Kassatsu)) 285 | return NIN.Jin; 286 | } 287 | 288 | return actionID; 289 | } 290 | } 291 | 292 | internal class NinjaTenChiJin : CustomCombo 293 | { 294 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.NinjaTCJMeisuiFeature; 295 | 296 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 297 | { 298 | if (actionID == NIN.TenChiJin) 299 | { 300 | if (level >= NIN.Levels.Meisui && HasEffect(NIN.Buffs.Suiton)) 301 | return NIN.Meisui; 302 | } 303 | 304 | return actionID; 305 | } 306 | } 307 | 308 | internal class NinjaNinjitsu : CustomCombo 309 | { 310 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.NinAny; 311 | 312 | protected override uint Invoke(uint actionID, uint lastComboActionID, float comboTime, byte level) 313 | { 314 | if (actionID == NIN.Ninjutsu) 315 | { 316 | if (level >= NIN.Levels.Raiju && HasEffect(NIN.Buffs.RaijuReady) && !HasEffect(NIN.Buffs.Mudra)) 317 | { 318 | if (IsEnabled(CustomComboPreset.NinjaNinjitsuForkedRaijuFeature)) 319 | return NIN.ForkedRaiju; 320 | 321 | if (IsEnabled(CustomComboPreset.NinjaNinjitsuFleetingRaijuFeature)) 322 | return NIN.FleetingRaiju; 323 | } 324 | } 325 | 326 | return actionID; 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /XIVComboExpanded/Combos/DRG.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Types; 2 | 3 | namespace XIVComboExpandedPlugin.Combos; 4 | 5 | internal static class DRG 6 | { 7 | public const byte ClassID = 4; 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 | // Buff abilities 40 | LanceCharge = 85, 41 | DragonSight = 7398, 42 | BattleLitany = 3557; 43 | 44 | public static class Buffs 45 | { 46 | public const ushort 47 | SharperFangAndClaw = 802, 48 | EnhancedWheelingThrust = 803, 49 | DiveReady = 1243; 50 | } 51 | 52 | public static class Debuffs 53 | { 54 | public const ushort 55 | Placeholder = 0; 56 | } 57 | 58 | public static class Levels 59 | { 60 | public const byte 61 | VorpalThrust = 4, 62 | Disembowel = 18, 63 | FullThrust = 26, 64 | SpineshatterDive = 45, 65 | DragonfireDive = 50, 66 | ChaosThrust = 50, 67 | BattleLitany = 52, 68 | HeavensThrust = 86, 69 | ChaoticSpring = 86, 70 | FangAndClaw = 56, 71 | WheelingThrust = 58, 72 | Geirskogul = 60, 73 | SonicThrust = 62, 74 | DragonSight = 66, 75 | MirageDive = 68, 76 | LifeOfTheDragon = 70, 77 | CoerthanTorment = 72, 78 | HighJump = 74, 79 | RaidenThrust = 76, 80 | Stardiver = 80, 81 | WyrmwindThrust = 90; 82 | } 83 | } 84 | 85 | internal class DragoonJump : CustomCombo 86 | { 87 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DragoonJumpFeature; 88 | 89 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 90 | { 91 | if (actionID == DRG.Jump || actionID == DRG.HighJump) 92 | { 93 | if (level >= DRG.Levels.MirageDive && HasEffect(DRG.Buffs.DiveReady)) 94 | return DRG.MirageDive; 95 | } 96 | 97 | return actionID; 98 | } 99 | } 100 | 101 | internal class DragoonCoerthanTorment : CustomCombo 102 | { 103 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DrgAny; 104 | 105 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 106 | { 107 | if (actionID == DRG.CoerthanTorment) 108 | { 109 | var gauge = GetJobGauge(); 110 | 111 | if (IsEnabled(CustomComboPreset.DragoonCoerthanWyrmwindFeature)) 112 | { 113 | if (gauge.FirstmindsFocusCount == 2) 114 | return DRG.WyrmwindThrust; 115 | } 116 | 117 | if (IsEnabled(CustomComboPreset.DragoonCoerthanTormentCombo)) 118 | { 119 | if (comboTime > 0) 120 | { 121 | if (lastComboMove == DRG.SonicThrust && level >= DRG.Levels.CoerthanTorment) 122 | return DRG.CoerthanTorment; 123 | 124 | if ((lastComboMove == DRG.DoomSpike || lastComboMove == DRG.DraconianFury) && level >= DRG.Levels.SonicThrust) 125 | return DRG.SonicThrust; 126 | } 127 | 128 | // Draconian Fury 129 | return OriginalHook(DRG.DoomSpike); 130 | } 131 | } 132 | 133 | return actionID; 134 | } 135 | } 136 | 137 | internal class DragoonChaosThrust : CustomCombo 138 | { 139 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DrgAny; 140 | 141 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 142 | { 143 | if (actionID == DRG.ChaosThrust || actionID == DRG.ChaoticSpring) 144 | { 145 | if (IsEnabled(CustomComboPreset.DragoonFangThrustFeature)) 146 | { 147 | if (level >= DRG.Levels.FangAndClaw && (HasEffect(DRG.Buffs.SharperFangAndClaw) || HasEffect(DRG.Buffs.EnhancedWheelingThrust))) 148 | return DRG.WheelingThrust; 149 | } 150 | 151 | if (IsEnabled(CustomComboPreset.DragoonChaosThrustCombo)) 152 | { 153 | if (level >= DRG.Levels.FangAndClaw && HasEffect(DRG.Buffs.SharperFangAndClaw)) 154 | return DRG.FangAndClaw; 155 | 156 | if (level >= DRG.Levels.WheelingThrust && HasEffect(DRG.Buffs.EnhancedWheelingThrust)) 157 | return DRG.WheelingThrust; 158 | 159 | if (comboTime > 0) 160 | { 161 | if (lastComboMove == DRG.Disembowel && level >= DRG.Levels.ChaosThrust) 162 | // ChaoticSpring 163 | return OriginalHook(DRG.ChaosThrust); 164 | 165 | if ((lastComboMove == DRG.TrueThrust || lastComboMove == DRG.RaidenThrust) && level >= DRG.Levels.Disembowel) 166 | return DRG.Disembowel; 167 | } 168 | 169 | if (IsEnabled(CustomComboPreset.DragoonChaosThrustComboOption)) 170 | return DRG.Disembowel; 171 | 172 | // Vorpal Thrust 173 | return OriginalHook(DRG.TrueThrust); 174 | } 175 | } 176 | 177 | return actionID; 178 | } 179 | } 180 | 181 | internal class DragoonFullThrust : CustomCombo 182 | { 183 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DrgAny; 184 | 185 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 186 | { 187 | if (actionID == DRG.FullThrust || actionID == DRG.HeavensThrust) 188 | { 189 | if (IsEnabled(CustomComboPreset.DragoonFangThrustFeature)) 190 | { 191 | if (level >= DRG.Levels.FangAndClaw && (HasEffect(DRG.Buffs.SharperFangAndClaw) || HasEffect(DRG.Buffs.EnhancedWheelingThrust))) 192 | return DRG.FangAndClaw; 193 | } 194 | 195 | if (IsEnabled(CustomComboPreset.DragoonFullThrustCombo)) 196 | { 197 | if (level >= DRG.Levels.WheelingThrust && HasEffect(DRG.Buffs.EnhancedWheelingThrust)) 198 | return DRG.WheelingThrust; 199 | 200 | if (level >= DRG.Levels.FangAndClaw && HasEffect(DRG.Buffs.SharperFangAndClaw)) 201 | return DRG.FangAndClaw; 202 | 203 | if (comboTime > 0) 204 | { 205 | if (lastComboMove == DRG.VorpalThrust && level >= DRG.Levels.FullThrust) 206 | // Heavens' Thrust 207 | return OriginalHook(DRG.FullThrust); 208 | 209 | if ((lastComboMove == DRG.TrueThrust || lastComboMove == DRG.RaidenThrust) && level >= DRG.Levels.VorpalThrust) 210 | return DRG.VorpalThrust; 211 | } 212 | 213 | if (IsEnabled(CustomComboPreset.DragoonFullThrustComboOption)) 214 | return DRG.VorpalThrust; 215 | 216 | // Vorpal Thrust 217 | return OriginalHook(DRG.TrueThrust); 218 | } 219 | } 220 | 221 | return actionID; 222 | } 223 | } 224 | 225 | internal class DragoonStardiver : CustomCombo 226 | { 227 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DrgAny; 228 | 229 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 230 | { 231 | if (actionID == DRG.Stardiver) 232 | { 233 | var gauge = GetJobGauge(); 234 | 235 | if (IsEnabled(CustomComboPreset.DragoonStardiverNastrondFeature)) 236 | { 237 | if (level >= DRG.Levels.Geirskogul && (!gauge.IsLOTDActive || IsOffCooldown(DRG.Nastrond) || IsOnCooldown(DRG.Stardiver))) 238 | // Nastrond 239 | return OriginalHook(DRG.Geirskogul); 240 | } 241 | 242 | if (IsEnabled(CustomComboPreset.DragoonStardiverDragonfireDiveFeature)) 243 | { 244 | if (level < DRG.Levels.Stardiver || !gauge.IsLOTDActive || IsOnCooldown(DRG.Stardiver) || (IsOffCooldown(DRG.DragonfireDive) && gauge.LOTDTimer > 7.5)) 245 | return DRG.DragonfireDive; 246 | } 247 | } 248 | 249 | return actionID; 250 | } 251 | } 252 | 253 | internal class DragoonDives : CustomCombo 254 | { 255 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DragoonDiveFeature; 256 | 257 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 258 | { 259 | if (actionID == DRG.SpineshatterDive || actionID == DRG.DragonfireDive || actionID == DRG.Stardiver) 260 | { 261 | if (level >= DRG.Levels.Stardiver) 262 | { 263 | var gauge = GetJobGauge(); 264 | 265 | if (gauge.IsLOTDActive) 266 | return CalcBestAction(actionID, DRG.SpineshatterDive, DRG.DragonfireDive, DRG.Stardiver); 267 | 268 | return CalcBestAction(actionID, DRG.SpineshatterDive, DRG.DragonfireDive); 269 | } 270 | 271 | if (level >= DRG.Levels.DragonfireDive) 272 | return CalcBestAction(actionID, DRG.SpineshatterDive, DRG.DragonfireDive); 273 | 274 | if (level >= DRG.Levels.SpineshatterDive) 275 | return DRG.SpineshatterDive; 276 | } 277 | 278 | return actionID; 279 | } 280 | } 281 | 282 | internal class DragoonGierskogul : CustomCombo 283 | { 284 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DragoonGeirskogulWyrmwindFeature; 285 | 286 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 287 | { 288 | if (actionID == DRG.Geirskogul) 289 | { 290 | if (level >= DRG.Levels.WyrmwindThrust) 291 | { 292 | var gauge = GetJobGauge(); 293 | 294 | if (gauge.FirstmindsFocusCount == 2) 295 | { 296 | var action = gauge.IsLOTDActive 297 | ? DRG.Nastrond 298 | : DRG.Geirskogul; 299 | 300 | if (IsOnCooldown(action)) 301 | return DRG.WyrmwindThrust; 302 | } 303 | } 304 | } 305 | 306 | return actionID; 307 | } 308 | } 309 | 310 | internal class DragoonLanceCharge : CustomCombo 311 | { 312 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.DragoonLanceChargeFeature; 313 | 314 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 315 | { 316 | if (actionID == DRG.LanceCharge) 317 | { 318 | if (!IsOnCooldown(DRG.LanceCharge)) 319 | return DRG.LanceCharge; 320 | 321 | if (level >= DRG.Levels.DragonSight && !IsOnCooldown(DRG.DragonSight)) 322 | return DRG.DragonSight; 323 | 324 | if (level >= DRG.Levels.BattleLitany && !IsOnCooldown(DRG.BattleLitany)) 325 | return DRG.BattleLitany; 326 | } 327 | 328 | return actionID; 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /XIVComboExpanded/Combos/SAM.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.JobGauge.Enums; 2 | using Dalamud.Game.ClientState.JobGauge.Types; 3 | 4 | namespace XIVComboExpandedPlugin.Combos; 5 | 6 | internal static class SAM 7 | { 8 | public const byte JobID = 34; 9 | 10 | public const uint 11 | // Single target 12 | Hakaze = 7477, 13 | Jinpu = 7478, 14 | Shifu = 7479, 15 | Yukikaze = 7480, 16 | Gekko = 7481, 17 | Kasha = 7482, 18 | // AoE 19 | Fuga = 7483, 20 | Mangetsu = 7484, 21 | Oka = 7485, 22 | Fuko = 25780, 23 | // Iaijutsu and Tsubame 24 | Iaijutsu = 7867, 25 | TsubameGaeshi = 16483, 26 | KaeshiHiganbana = 16484, 27 | Shoha = 16487, 28 | // Misc 29 | HissatsuShinten = 7490, 30 | HissatsuKyuten = 7491, 31 | HissatsuSenei = 16481, 32 | HissatsuGuren = 7496, 33 | Ikishoten = 16482, 34 | Shoha2 = 25779, 35 | OgiNamikiri = 25781, 36 | KaeshiNamikiri = 25782; 37 | 38 | public static class Buffs 39 | { 40 | public const ushort 41 | MeikyoShisui = 1233, 42 | EyesOpen = 1252, 43 | Jinpu = 1298, 44 | Shifu = 1299, 45 | OgiNamikiriReady = 2959; 46 | } 47 | 48 | public static class Debuffs 49 | { 50 | public const ushort 51 | Placeholder = 0; 52 | } 53 | 54 | public static class Levels 55 | { 56 | public const byte 57 | Jinpu = 4, 58 | Shifu = 18, 59 | Gekko = 30, 60 | Mangetsu = 35, 61 | Kasha = 40, 62 | Oka = 45, 63 | Yukikaze = 50, 64 | MeikyoShisui = 50, 65 | HissatsuKyuten = 64, 66 | HissatsuGuren = 70, 67 | HissatsuSenei = 72, 68 | TsubameGaeshi = 76, 69 | Shoha = 80, 70 | Shoha2 = 82, 71 | Hyosetsu = 86, 72 | Fuko = 86, 73 | OgiNamikiri = 90; 74 | } 75 | } 76 | 77 | internal class SamuraiYukikaze : CustomCombo 78 | { 79 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SamuraiYukikazeCombo; 80 | 81 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 82 | { 83 | if (actionID == SAM.Yukikaze) 84 | { 85 | if (level >= SAM.Levels.MeikyoShisui && HasEffect(SAM.Buffs.MeikyoShisui)) 86 | return SAM.Yukikaze; 87 | 88 | if (comboTime > 0) 89 | { 90 | if (lastComboMove == SAM.Hakaze && level >= SAM.Levels.Yukikaze) 91 | return SAM.Yukikaze; 92 | } 93 | 94 | return SAM.Hakaze; 95 | } 96 | 97 | return actionID; 98 | } 99 | } 100 | 101 | internal class SamuraiGekko : CustomCombo 102 | { 103 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SamuraiGekkoCombo; 104 | 105 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 106 | { 107 | if (actionID == SAM.Gekko) 108 | { 109 | if (level >= SAM.Levels.MeikyoShisui && HasEffect(SAM.Buffs.MeikyoShisui)) 110 | return SAM.Gekko; 111 | 112 | if (comboTime > 0) 113 | { 114 | if (lastComboMove == SAM.Jinpu && level >= SAM.Levels.Gekko) 115 | return SAM.Gekko; 116 | 117 | if (lastComboMove == SAM.Hakaze && level >= SAM.Levels.Jinpu) 118 | return SAM.Jinpu; 119 | } 120 | 121 | if (IsEnabled(CustomComboPreset.SamuraiGekkoOption)) 122 | return SAM.Jinpu; 123 | 124 | return SAM.Hakaze; 125 | } 126 | 127 | return actionID; 128 | } 129 | } 130 | 131 | internal class SamuraiKasha : CustomCombo 132 | { 133 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SamuraiKashaCombo; 134 | 135 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 136 | { 137 | if (actionID == SAM.Kasha) 138 | { 139 | if (level >= SAM.Levels.MeikyoShisui && HasEffect(SAM.Buffs.MeikyoShisui)) 140 | return SAM.Kasha; 141 | 142 | if (comboTime > 0) 143 | { 144 | if (lastComboMove == SAM.Shifu && level >= SAM.Levels.Kasha) 145 | return SAM.Kasha; 146 | 147 | if (lastComboMove == SAM.Hakaze && level >= SAM.Levels.Shifu) 148 | return SAM.Shifu; 149 | } 150 | 151 | if (IsEnabled(CustomComboPreset.SamuraiKashaOption)) 152 | return SAM.Shifu; 153 | 154 | return SAM.Hakaze; 155 | } 156 | 157 | return actionID; 158 | } 159 | } 160 | 161 | internal class SamuraiMangetsu : CustomCombo 162 | { 163 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SamuraiMangetsuCombo; 164 | 165 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 166 | { 167 | if (actionID == SAM.Mangetsu) 168 | { 169 | if (level >= SAM.Levels.MeikyoShisui && HasEffect(SAM.Buffs.MeikyoShisui)) 170 | return SAM.Mangetsu; 171 | 172 | if (comboTime > 0) 173 | { 174 | if ((lastComboMove == SAM.Fuga || lastComboMove == SAM.Fuko) && level >= SAM.Levels.Mangetsu) 175 | return SAM.Mangetsu; 176 | } 177 | 178 | // Fuko 179 | return OriginalHook(SAM.Fuga); 180 | } 181 | 182 | return actionID; 183 | } 184 | } 185 | 186 | internal class SamuraiOka : CustomCombo 187 | { 188 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SamuraiOkaCombo; 189 | 190 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 191 | { 192 | if (actionID == SAM.Oka) 193 | { 194 | if (level >= SAM.Levels.MeikyoShisui && HasEffect(SAM.Buffs.MeikyoShisui)) 195 | return SAM.Oka; 196 | 197 | if (comboTime > 0) 198 | { 199 | if ((lastComboMove == SAM.Fuga || lastComboMove == SAM.Fuko) && level >= SAM.Levels.Oka) 200 | return SAM.Oka; 201 | } 202 | 203 | // Fuko 204 | return OriginalHook(SAM.Fuga); 205 | } 206 | 207 | return actionID; 208 | } 209 | } 210 | 211 | internal class SamuraiTsubame : CustomCombo 212 | { 213 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SamAny; 214 | 215 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 216 | { 217 | if (actionID == SAM.TsubameGaeshi) 218 | { 219 | var gauge = GetJobGauge(); 220 | 221 | if (IsEnabled(CustomComboPreset.SamuraiTsubameGaeshiShohaFeature)) 222 | { 223 | if (level >= SAM.Levels.Shoha && gauge.MeditationStacks >= 3) 224 | return SAM.Shoha; 225 | } 226 | 227 | if (IsEnabled(CustomComboPreset.SamuraiTsubameGaeshiIaijutsuFeature)) 228 | { 229 | if (level >= SAM.Levels.TsubameGaeshi && gauge.Sen == Sen.NONE) 230 | return OriginalHook(SAM.TsubameGaeshi); 231 | 232 | return OriginalHook(SAM.Iaijutsu); 233 | } 234 | } 235 | 236 | return actionID; 237 | } 238 | } 239 | 240 | internal class SamuraiIaijutsu : CustomCombo 241 | { 242 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SamAny; 243 | 244 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 245 | { 246 | if (actionID == SAM.Iaijutsu) 247 | { 248 | var gauge = GetJobGauge(); 249 | 250 | if (IsEnabled(CustomComboPreset.SamuraiIaijutsuShohaFeature)) 251 | { 252 | if (level >= SAM.Levels.Shoha && gauge.MeditationStacks >= 3) 253 | return SAM.Shoha; 254 | } 255 | 256 | if (IsEnabled(CustomComboPreset.SamuraiIaijutsuTsubameGaeshiFeature)) 257 | { 258 | if (level >= SAM.Levels.TsubameGaeshi && gauge.Sen == Sen.NONE) 259 | return OriginalHook(SAM.TsubameGaeshi); 260 | 261 | return OriginalHook(SAM.Iaijutsu); 262 | } 263 | } 264 | 265 | return actionID; 266 | } 267 | } 268 | 269 | internal class SamuraiShinten : CustomCombo 270 | { 271 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SamAny; 272 | 273 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 274 | { 275 | if (actionID == SAM.HissatsuShinten) 276 | { 277 | var gauge = GetJobGauge(); 278 | 279 | if (IsEnabled(CustomComboPreset.SamuraiShintenShohaFeature)) 280 | { 281 | if (level >= SAM.Levels.Shoha && gauge.MeditationStacks >= 3) 282 | return SAM.Shoha; 283 | } 284 | 285 | if (IsEnabled(CustomComboPreset.SamuraiShintenSeneiFeature)) 286 | { 287 | if (level >= SAM.Levels.HissatsuSenei && IsOffCooldown(SAM.HissatsuSenei)) 288 | return SAM.HissatsuSenei; 289 | 290 | if (IsEnabled(CustomComboPreset.SamuraiSeneiGurenFeature)) 291 | { 292 | if (level >= SAM.Levels.HissatsuGuren && level < SAM.Levels.HissatsuSenei && IsOffCooldown(SAM.HissatsuGuren)) 293 | return SAM.HissatsuGuren; 294 | } 295 | } 296 | } 297 | 298 | return actionID; 299 | } 300 | } 301 | 302 | internal class SamuraiSenei : CustomCombo 303 | { 304 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SamAny; 305 | 306 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 307 | { 308 | if (actionID == SAM.HissatsuSenei) 309 | { 310 | if (IsEnabled(CustomComboPreset.SamuraiSeneiGurenFeature)) 311 | { 312 | if (level >= SAM.Levels.HissatsuGuren && level < SAM.Levels.HissatsuSenei) 313 | return SAM.HissatsuGuren; 314 | } 315 | } 316 | 317 | return actionID; 318 | } 319 | } 320 | 321 | internal class SamuraiKyuten : CustomCombo 322 | { 323 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SamAny; 324 | 325 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 326 | { 327 | if (actionID == SAM.HissatsuKyuten) 328 | { 329 | var gauge = GetJobGauge(); 330 | 331 | if (IsEnabled(CustomComboPreset.SamuraiKyutenShoha2Feature)) 332 | { 333 | if (level >= SAM.Levels.Shoha2 && gauge.MeditationStacks >= 3) 334 | return SAM.Shoha2; 335 | } 336 | 337 | if (IsEnabled(CustomComboPreset.SamuraiKyutenGurenFeature)) 338 | { 339 | if (level >= SAM.Levels.HissatsuGuren && IsOffCooldown(SAM.HissatsuGuren)) 340 | return SAM.HissatsuGuren; 341 | } 342 | } 343 | 344 | return actionID; 345 | } 346 | } 347 | 348 | internal class SamuraiIkishoten : CustomCombo 349 | { 350 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.SamuraiIkishotenNamikiriFeature; 351 | 352 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 353 | { 354 | if (actionID == SAM.Ikishoten) 355 | { 356 | if (level >= SAM.Levels.OgiNamikiri) 357 | { 358 | var gauge = GetJobGauge(); 359 | 360 | if (IsEnabled(CustomComboPreset.SamuraiIkishotenShohaFeature)) 361 | { 362 | if (level >= SAM.Levels.Shoha && gauge.MeditationStacks >= 3) 363 | return SAM.Shoha; 364 | } 365 | 366 | if (gauge.Kaeshi == Kaeshi.NAMIKIRI) 367 | return SAM.KaeshiNamikiri; 368 | 369 | if (HasEffect(SAM.Buffs.OgiNamikiriReady)) 370 | return SAM.OgiNamikiri; 371 | } 372 | } 373 | 374 | return actionID; 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /XIVComboExpanded/XIVComboExpandedPlugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | using Dalamud.Game; 5 | using Dalamud.Game.Command; 6 | using Dalamud.Interface.Windowing; 7 | using Dalamud.Plugin; 8 | using Dalamud.Plugin.Services; 9 | using XIVComboExpandedPlugin.Interface; 10 | 11 | namespace XIVComboExpandedPlugin; 12 | 13 | /// 14 | /// Main plugin implementation. 15 | /// 16 | public sealed partial class XIVComboExpandedPlugin : IDalamudPlugin 17 | { 18 | private const string Command = "/pcombo"; 19 | 20 | private readonly WindowSystem windowSystem; 21 | private readonly ConfigWindow configWindow; 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// Dalamud plugin interface. 27 | /// Dalamud signature scanner. 28 | /// Dalamud game interop provider. 29 | public XIVComboExpandedPlugin( 30 | DalamudPluginInterface pluginInterface, 31 | ISigScanner sigScanner, 32 | IGameInteropProvider gameInteropProvider) 33 | { 34 | pluginInterface.Create(); 35 | 36 | Service.Configuration = pluginInterface.GetPluginConfig() as PluginConfiguration ?? new PluginConfiguration(); 37 | Service.Address = new PluginAddressResolver(); 38 | Service.Address.Setup((SigScanner)sigScanner); 39 | 40 | if (Service.Configuration.Version == 4) 41 | this.UpgradeConfig4(); 42 | 43 | Service.ComboCache = new CustomComboCache(); 44 | Service.IconReplacer = new IconReplacer(gameInteropProvider); 45 | 46 | this.configWindow = new(); 47 | this.windowSystem = new("XIVComboExpanded"); 48 | this.windowSystem.AddWindow(this.configWindow); 49 | 50 | Service.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; 51 | Service.Interface.UiBuilder.Draw += this.windowSystem.Draw; 52 | 53 | Service.CommandManager.AddHandler(Command, new CommandInfo(this.OnCommand) 54 | { 55 | HelpMessage = "Open a window to edit custom combo settings.", 56 | ShowInHelp = true, 57 | }); 58 | } 59 | 60 | /// 61 | public string Name => "XIV Combo Expanded"; 62 | 63 | /// 64 | public void Dispose() 65 | { 66 | Service.CommandManager.RemoveHandler(Command); 67 | 68 | Service.Interface.UiBuilder.OpenConfigUi -= this.OnOpenConfigUi; 69 | Service.Interface.UiBuilder.Draw -= this.windowSystem.Draw; 70 | 71 | Service.IconReplacer?.Dispose(); 72 | Service.ComboCache?.Dispose(); 73 | } 74 | 75 | private void OnOpenConfigUi() 76 | => this.configWindow.IsOpen = true; 77 | 78 | private void OnCommand(string command, string arguments) 79 | { 80 | var argumentsParts = arguments.Split(); 81 | 82 | switch (argumentsParts[0]) 83 | { 84 | case "setall": 85 | { 86 | foreach (var preset in Enum.GetValues()) 87 | { 88 | Service.Configuration.EnabledActions.Add(preset); 89 | } 90 | 91 | Service.ChatGui.Print("All SET"); 92 | Service.Configuration.Save(); 93 | break; 94 | } 95 | 96 | case "unsetall": 97 | { 98 | foreach (var preset in Enum.GetValues()) 99 | { 100 | Service.Configuration.EnabledActions.Remove(preset); 101 | } 102 | 103 | Service.ChatGui.Print("All UNSET"); 104 | Service.Configuration.Save(); 105 | break; 106 | } 107 | 108 | case "set": 109 | { 110 | var targetPreset = argumentsParts[1].ToLowerInvariant(); 111 | foreach (var preset in Enum.GetValues()) 112 | { 113 | if (preset.ToString().ToLowerInvariant() != targetPreset) 114 | continue; 115 | 116 | Service.Configuration.EnabledActions.Add(preset); 117 | Service.ChatGui.Print($"{preset} SET"); 118 | } 119 | 120 | Service.Configuration.Save(); 121 | break; 122 | } 123 | 124 | case "secrets": 125 | { 126 | Service.Configuration.EnableSecretCombos = !Service.Configuration.EnableSecretCombos; 127 | 128 | Service.ChatGui.Print(Service.Configuration.EnableSecretCombos 129 | ? $"Secret combos are now shown" 130 | : $"Secret combos are now hidden"); 131 | 132 | Service.Configuration.Save(); 133 | break; 134 | } 135 | 136 | case "toggle": 137 | { 138 | var targetPreset = argumentsParts[1].ToLowerInvariant(); 139 | foreach (var preset in Enum.GetValues()) 140 | { 141 | if (preset.ToString().ToLowerInvariant() != targetPreset) 142 | continue; 143 | 144 | if (Service.Configuration.EnabledActions.Contains(preset)) 145 | { 146 | Service.Configuration.EnabledActions.Remove(preset); 147 | Service.ChatGui.Print($"{preset} UNSET"); 148 | } 149 | else 150 | { 151 | Service.Configuration.EnabledActions.Add(preset); 152 | Service.ChatGui.Print($"{preset} SET"); 153 | } 154 | } 155 | 156 | Service.Configuration.Save(); 157 | break; 158 | } 159 | 160 | case "unset": 161 | { 162 | var targetPreset = argumentsParts[1].ToLowerInvariant(); 163 | foreach (var preset in Enum.GetValues()) 164 | { 165 | if (preset.ToString().ToLowerInvariant() != targetPreset) 166 | continue; 167 | 168 | Service.Configuration.EnabledActions.Remove(preset); 169 | Service.ChatGui.Print($"{preset} UNSET"); 170 | } 171 | 172 | Service.Configuration.Save(); 173 | break; 174 | } 175 | 176 | case "list": 177 | { 178 | var filter = argumentsParts.Length > 1 179 | ? argumentsParts[1].ToLowerInvariant() 180 | : "all"; 181 | 182 | if (filter == "set") 183 | { 184 | foreach (var preset in Enum.GetValues() 185 | .Select(preset => Service.Configuration.IsEnabled(preset))) 186 | { 187 | Service.ChatGui.Print(preset.ToString()); 188 | } 189 | } 190 | else if (filter == "unset") 191 | { 192 | foreach (var preset in Enum.GetValues() 193 | .Select(preset => !Service.Configuration.IsEnabled(preset))) 194 | { 195 | Service.ChatGui.Print(preset.ToString()); 196 | } 197 | } 198 | else if (filter == "all") 199 | { 200 | foreach (var preset in Enum.GetValues()) 201 | { 202 | Service.ChatGui.Print(preset.ToString()); 203 | } 204 | } 205 | else 206 | { 207 | Service.ChatGui.PrintError("Available list filters: set, unset, all"); 208 | } 209 | 210 | break; 211 | } 212 | 213 | default: 214 | this.configWindow.Toggle(); 215 | break; 216 | } 217 | 218 | Service.Configuration.Save(); 219 | } 220 | 221 | private void UpgradeConfig4() 222 | { 223 | Service.Configuration.Version = 5; 224 | Service.Configuration.EnabledActions = Service.Configuration.EnabledActions4 225 | .Select(preset => (int)preset switch 226 | { 227 | 27 => 3301, 228 | 75 => 3302, 229 | 73 => 3303, 230 | 25 => 2501, 231 | 26 => 2502, 232 | 56 => 2503, 233 | 70 => 2504, 234 | 71 => 2505, 235 | 110 => 2506, 236 | 95 => 2507, 237 | 41 => 2301, 238 | 42 => 2302, 239 | 63 => 2303, 240 | 74 => 2304, 241 | 33 => 3801, 242 | 31 => 3802, 243 | 34 => 3803, 244 | 43 => 3804, 245 | 50 => 3805, 246 | 72 => 3806, 247 | 103 => 3807, 248 | 44 => 2201, 249 | 0 => 2202, 250 | 1 => 2203, 251 | 2 => 2204, 252 | 3 => 3201, 253 | 4 => 3202, 254 | 57 => 3203, 255 | 85 => 3204, 256 | 20 => 3701, 257 | 52 => 3702, 258 | 96 => 3703, 259 | 97 => 3704, 260 | 22 => 3705, 261 | 30 => 3706, 262 | 83 => 3707, 263 | 84 => 3708, 264 | 23 => 3101, 265 | 24 => 3102, 266 | 47 => 3103, 267 | 58 => 3104, 268 | 66 => 3105, 269 | 102 => 3106, 270 | 54 => 2001, 271 | 82 => 2002, 272 | 106 => 2003, 273 | 17 => 3001, 274 | 18 => 3002, 275 | 19 => 3003, 276 | 87 => 3004, 277 | 88 => 3005, 278 | 89 => 3006, 279 | 90 => 3007, 280 | 91 => 3008, 281 | 92 => 3009, 282 | 107 => 3010, 283 | 108 => 3011, 284 | 5 => 1901, 285 | 6 => 1902, 286 | 59 => 1903, 287 | 7 => 1904, 288 | 55 => 1905, 289 | 86 => 1906, 290 | 69 => 1907, 291 | 48 => 3501, 292 | 49 => 3502, 293 | 68 => 3503, 294 | 53 => 3504, 295 | 93 => 3505, 296 | 101 => 3506, 297 | 94 => 3507, 298 | 11 => 3401, 299 | 12 => 3402, 300 | 13 => 3403, 301 | 14 => 3404, 302 | 15 => 3405, 303 | 81 => 3406, 304 | 60 => 3407, 305 | 61 => 3408, 306 | 64 => 3409, 307 | 65 => 3410, 308 | 109 => 3411, 309 | 29 => 2801, 310 | 37 => 2802, 311 | 39 => 2701, 312 | 40 => 2702, 313 | 8 => 2101, 314 | 9 => 2102, 315 | 10 => 2103, 316 | 78 => 2104, 317 | 79 => 2105, 318 | 67 => 2106, 319 | 104 => 2107, 320 | 35 => 2401, 321 | 36 => 2402, 322 | 76 => 2403, 323 | 77 => 2404, 324 | _ => 0, 325 | }) 326 | .Where(id => id != 0) 327 | .Select(id => (CustomComboPreset)id) 328 | .ToHashSet(); 329 | Service.Configuration.EnabledActions4 = new(); 330 | Service.Configuration.Save(); 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /XIVComboExpanded/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 | namespace XIVComboExpandedPlugin.Combos; 8 | 9 | internal static class MNK 10 | { 11 | public const byte ClassID = 2; 12 | public const byte JobID = 20; 13 | 14 | public const uint 15 | Bootshine = 53, 16 | TrueStrike = 54, 17 | SnapPunch = 56, 18 | TwinSnakes = 61, 19 | ArmOfTheDestroyer = 62, 20 | Demolish = 66, 21 | PerfectBalance = 69, 22 | Rockbreaker = 70, 23 | DragonKick = 74, 24 | Meditation = 3546, 25 | RiddleOfFire = 7395, 26 | Brotherhood = 7396, 27 | FourPointFury = 16473, 28 | Enlightenment = 16474, 29 | HowlingFist = 25763, 30 | MasterfulBlitz = 25764, 31 | RiddleOfWind = 25766, 32 | ShadowOfTheDestroyer = 25767; 33 | 34 | public static class Buffs 35 | { 36 | public const ushort 37 | OpoOpoForm = 107, 38 | RaptorForm = 108, 39 | CoerlForm = 109, 40 | PerfectBalance = 110, 41 | LeadenFist = 1861, 42 | FormlessFist = 2513, 43 | DisciplinedFist = 3001; 44 | } 45 | 46 | public static class Debuffs 47 | { 48 | public const ushort 49 | Demolish = 246; 50 | } 51 | 52 | public static class Levels 53 | { 54 | public const byte 55 | Bootshine = 1, 56 | TrueStrike = 4, 57 | SnapPunch = 6, 58 | Meditation = 15, 59 | TwinSnakes = 18, 60 | ArmOfTheDestroyer = 26, 61 | Rockbreaker = 30, 62 | Demolish = 30, 63 | FourPointFury = 45, 64 | HowlingFist = 40, 65 | DragonKick = 50, 66 | PerfectBalance = 50, 67 | FormShift = 52, 68 | EnhancedPerfectBalance = 60, 69 | MasterfulBlitz = 60, 70 | RiddleOfFire = 68, 71 | Brotherhood = 70, 72 | Enlightenment = 70, 73 | RiddleOfWind = 72, 74 | ShadowOfTheDestroyer = 82; 75 | } 76 | } 77 | 78 | internal class MonkAoECombo : CustomCombo 79 | { 80 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.MonkAoECombo; 81 | 82 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 83 | { 84 | if (actionID == MNK.MasterfulBlitz) 85 | { 86 | var gauge = GetJobGauge(); 87 | 88 | // Blitz 89 | if (level >= MNK.Levels.MasterfulBlitz && !gauge.BeastChakra.Contains(BeastChakra.NONE)) 90 | return OriginalHook(MNK.MasterfulBlitz); 91 | 92 | if (level >= MNK.Levels.PerfectBalance && HasEffect(MNK.Buffs.PerfectBalance)) 93 | { 94 | // Solar 95 | if (level >= MNK.Levels.EnhancedPerfectBalance && !gauge.Nadi.HasFlag(Nadi.SOLAR)) 96 | { 97 | if (level >= MNK.Levels.FourPointFury && !gauge.BeastChakra.Contains(BeastChakra.RAPTOR)) 98 | return MNK.FourPointFury; 99 | 100 | if (level >= MNK.Levels.Rockbreaker && !gauge.BeastChakra.Contains(BeastChakra.COEURL)) 101 | return MNK.Rockbreaker; 102 | 103 | if (level >= MNK.Levels.ArmOfTheDestroyer && !gauge.BeastChakra.Contains(BeastChakra.OPOOPO)) 104 | // Shadow of the Destroyer 105 | return OriginalHook(MNK.ArmOfTheDestroyer); 106 | 107 | return level >= MNK.Levels.ShadowOfTheDestroyer 108 | ? MNK.ShadowOfTheDestroyer 109 | : MNK.Rockbreaker; 110 | } 111 | 112 | // Lunar. Also used if we have both Nadi as Tornado Kick/Phantom Rush isn't picky, or under 60. 113 | return level >= MNK.Levels.ShadowOfTheDestroyer 114 | ? MNK.ShadowOfTheDestroyer 115 | : MNK.Rockbreaker; 116 | } 117 | 118 | // FPF with FormShift 119 | if (level >= MNK.Levels.FormShift && HasEffect(MNK.Buffs.FormlessFist)) 120 | { 121 | if (level >= MNK.Levels.FourPointFury) 122 | return MNK.FourPointFury; 123 | } 124 | 125 | // 1-2-3 combo 126 | if (level >= MNK.Levels.FourPointFury && HasEffect(MNK.Buffs.RaptorForm)) 127 | return MNK.FourPointFury; 128 | 129 | if (level >= MNK.Levels.ArmOfTheDestroyer && HasEffect(MNK.Buffs.OpoOpoForm)) 130 | // Shadow of the Destroyer 131 | return OriginalHook(MNK.ArmOfTheDestroyer); 132 | 133 | if (level >= MNK.Levels.Rockbreaker && HasEffect(MNK.Buffs.CoerlForm)) 134 | return MNK.Rockbreaker; 135 | 136 | // Shadow of the Destroyer 137 | return OriginalHook(MNK.ArmOfTheDestroyer); 138 | } 139 | 140 | return actionID; 141 | } 142 | } 143 | 144 | internal class MonkHowlingFistEnlightenment : CustomCombo 145 | { 146 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.MonkHowlingFistMeditationFeature; 147 | 148 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 149 | { 150 | if (actionID == MNK.HowlingFist || actionID == MNK.Enlightenment) 151 | { 152 | var gauge = GetJobGauge(); 153 | 154 | if (level >= MNK.Levels.Meditation && gauge.Chakra < 5) 155 | return MNK.Meditation; 156 | } 157 | 158 | return actionID; 159 | } 160 | } 161 | 162 | internal class MonkDragonKick : CustomCombo 163 | { 164 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.MnkAny; 165 | 166 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 167 | { 168 | if (actionID == MNK.DragonKick) 169 | { 170 | var gauge = GetJobGauge(); 171 | 172 | if (IsEnabled(CustomComboPreset.MonkDragonKickMeditationFeature)) 173 | { 174 | if (level >= MNK.Levels.Meditation && gauge.Chakra < 5 && !InCombat()) 175 | return MNK.Meditation; 176 | } 177 | 178 | if (IsEnabled(CustomComboPreset.MonkDragonKickBalanceFeature)) 179 | { 180 | if (level >= MNK.Levels.MasterfulBlitz && !gauge.BeastChakra.Contains(BeastChakra.NONE)) 181 | return OriginalHook(MNK.MasterfulBlitz); 182 | } 183 | 184 | if (IsEnabled(CustomComboPreset.MonkBootshineFeature)) 185 | { 186 | if (level < MNK.Levels.DragonKick) 187 | return MNK.Bootshine; 188 | 189 | if (HasEffect(MNK.Buffs.LeadenFist) && (HasEffect(MNK.Buffs.OpoOpoForm) || HasEffect(MNK.Buffs.PerfectBalance) || HasEffect(MNK.Buffs.FormlessFist))) 190 | return MNK.Bootshine; 191 | } 192 | } 193 | 194 | return actionID; 195 | } 196 | } 197 | 198 | internal class MonkTwinSnakes : CustomCombo 199 | { 200 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.MnkAny; 201 | 202 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 203 | { 204 | if (actionID == MNK.TwinSnakes) 205 | { 206 | if (IsEnabled(CustomComboPreset.MonkTwinSnakesFeature)) 207 | { 208 | if (level < MNK.Levels.TwinSnakes) 209 | return MNK.TrueStrike; 210 | 211 | if (IsEnabled(CustomComboPreset.MonkFormlessSnakesOption)) 212 | { 213 | if (level >= MNK.Levels.FormShift && HasEffect(MNK.Buffs.FormlessFist)) 214 | return MNK.TwinSnakes; 215 | } 216 | 217 | if (FindEffect(MNK.Buffs.DisciplinedFist)?.RemainingTime > 6.0) 218 | return MNK.TrueStrike; 219 | } 220 | } 221 | 222 | return actionID; 223 | } 224 | } 225 | 226 | internal class MonkTrueStrike : CustomCombo 227 | { 228 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.MonkTrueStrikeFeature; 229 | 230 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 231 | { 232 | if (actionID == MNK.TrueStrike && level >= MNK.Levels.TwinSnakes) 233 | { 234 | 235 | if (IsEnabled(CustomComboPreset.MonkFormlessStrikeOption)) 236 | { 237 | if (level >= MNK.Levels.FormShift && HasEffect(MNK.Buffs.FormlessFist)) 238 | return MNK.TrueStrike; 239 | } 240 | 241 | var buff = FindEffect(MNK.Buffs.DisciplinedFist); 242 | if (buff == null || buff.RemainingTime <= 6.0) 243 | return MNK.TwinSnakes; 244 | } 245 | 246 | return actionID; 247 | } 248 | } 249 | 250 | internal class MonkDemolish : CustomCombo 251 | { 252 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.MnkAny; 253 | 254 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 255 | { 256 | if (actionID == MNK.Demolish) 257 | { 258 | if (IsEnabled(CustomComboPreset.MonkDemolishFeature)) 259 | { 260 | if (level < MNK.Levels.Demolish || FindTargetEffect(MNK.Debuffs.Demolish)?.RemainingTime > 6.0) 261 | return MNK.SnapPunch; 262 | } 263 | } 264 | 265 | return actionID; 266 | } 267 | } 268 | 269 | internal class MonkSnapPunch : CustomCombo 270 | { 271 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.MnkAny; 272 | 273 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 274 | { 275 | if (actionID == MNK.SnapPunch) 276 | { 277 | if (IsEnabled(CustomComboPreset.MonkSnapPunchFeature)) 278 | { 279 | if (level < MNK.Levels.SnapPunch || FindTargetEffect(MNK.Debuffs.Demolish) == null || FindTargetEffect(MNK.Debuffs.Demolish)?.RemainingTime < 6.0) 280 | return MNK.Demolish; 281 | } 282 | } 283 | 284 | return actionID; 285 | } 286 | } 287 | 288 | internal class MonkPerfectBalance : CustomCombo 289 | { 290 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.MonkPerfectBalanceFeature; 291 | 292 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 293 | { 294 | if (actionID == MNK.PerfectBalance) 295 | { 296 | var gauge = GetJobGauge(); 297 | 298 | if (!gauge.BeastChakra.Contains(BeastChakra.NONE) && level >= MNK.Levels.MasterfulBlitz) 299 | // Chakra actions 300 | return OriginalHook(MNK.MasterfulBlitz); 301 | } 302 | 303 | return actionID; 304 | } 305 | } 306 | 307 | internal class MonkRiddleOfFire : CustomCombo 308 | { 309 | protected internal override CustomComboPreset Preset { get; } = CustomComboPreset.MnkAny; 310 | 311 | protected override uint Invoke(uint actionID, uint lastComboMove, float comboTime, byte level) 312 | { 313 | if (actionID == MNK.RiddleOfFire) 314 | { 315 | var brotherhood = IsEnabled(CustomComboPreset.MonkRiddleOfFireBrotherhood); 316 | var wind = IsEnabled(CustomComboPreset.MonkRiddleOfFireWind); 317 | 318 | if (brotherhood && wind) 319 | { 320 | if (level >= MNK.Levels.RiddleOfWind) 321 | return CalcBestAction(actionID, MNK.RiddleOfFire, MNK.Brotherhood, MNK.RiddleOfWind); 322 | 323 | if (level >= MNK.Levels.Brotherhood) 324 | return CalcBestAction(actionID, MNK.RiddleOfFire, MNK.Brotherhood); 325 | 326 | return actionID; 327 | } 328 | 329 | if (brotherhood) 330 | { 331 | if (level >= MNK.Levels.Brotherhood) 332 | return CalcBestAction(actionID, MNK.RiddleOfFire, MNK.Brotherhood); 333 | 334 | return actionID; 335 | } 336 | 337 | if (wind) 338 | { 339 | if (level >= MNK.Levels.RiddleOfWind) 340 | return CalcBestAction(actionID, MNK.RiddleOfFire, MNK.RiddleOfWind); 341 | 342 | return actionID; 343 | } 344 | } 345 | 346 | return actionID; 347 | } 348 | } 349 | --------------------------------------------------------------------------------