├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── Feature-Request.yml │ ├── Bug-Report-Form.yml │ └── Recommendation-Report.yml ├── FUNDING.yml └── workflows │ └── main.yml ├── Textures ├── Cycle.blp ├── DRUID.blp ├── MAGE.blp ├── MONK.blp ├── Pause.blp ├── ROGUE.blp ├── RedX.tga ├── Cancel.blp ├── HUNTER.png ├── PALADIN.blp ├── PRIEST.blp ├── SHAMAN.blp ├── TACODOG.blp ├── Taco256.blp ├── WARLOCK.blp ├── WARNING.blp ├── WARRIOR.blp ├── WhiteUp.tga ├── BlueReset.tga ├── GreenPlus.tga ├── GreyStar.tga ├── LOGO-WHITE.blp ├── WhiteCopy.tga ├── WhiteDown.tga ├── WhiteEye.tga ├── WhiteMag.tga ├── WhiteReset.tga ├── WhiteRight.tga ├── WhiteStar.tga ├── YellowStar.tga ├── DEATHKNIGHT.blp ├── LOGO-ORANGE.blp ├── MonoCircle2.tga ├── MonoCircle5.tga └── GreenPlusOutline.tga ├── Libs ├── TaintLess │ ├── TaintLess.toc │ └── TaintLess.xml ├── LibDataBroker-1.1 │ └── LibDataBroker-1.1.lua └── LibDBIcon-1.0 │ └── LibDBIcon-1.0.lua ├── README.md ├── .travis.yml ├── .gitattributes ├── Classic ├── APLs │ ├── RogueCombat.simc │ ├── RogueAssassination.simc │ ├── WarriorArms.simc │ ├── WarriorArms.t.simc │ ├── WarriorFury.simc │ ├── WarriorFury.t.simc │ ├── template.js │ ├── DruidFeral.t.simc │ └── DruidFeral.simc └── Classes.lua ├── Hekili.toc ├── Bindings.xml ├── UI ├── PopupSlider.lua └── PopupSlider.xml ├── embeds.xml ├── .gitignore ├── .pkgmeta ├── Hekili.lua ├── MultilineEditor.lua ├── Utils.lua └── Formatting.lua /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /Textures/Cycle.blp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/Cycle.blp -------------------------------------------------------------------------------- /Textures/DRUID.blp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/DRUID.blp -------------------------------------------------------------------------------- /Textures/MAGE.blp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/MAGE.blp -------------------------------------------------------------------------------- /Textures/MONK.blp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/MONK.blp -------------------------------------------------------------------------------- /Textures/Pause.blp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/Pause.blp -------------------------------------------------------------------------------- /Textures/ROGUE.blp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/ROGUE.blp -------------------------------------------------------------------------------- /Textures/RedX.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/RedX.tga -------------------------------------------------------------------------------- /Textures/Cancel.blp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/Cancel.blp -------------------------------------------------------------------------------- /Textures/HUNTER.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/HUNTER.png -------------------------------------------------------------------------------- /Textures/PALADIN.blp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/PALADIN.blp -------------------------------------------------------------------------------- /Textures/PRIEST.blp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/PRIEST.blp -------------------------------------------------------------------------------- /Textures/SHAMAN.blp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/SHAMAN.blp -------------------------------------------------------------------------------- /Textures/TACODOG.blp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/TACODOG.blp -------------------------------------------------------------------------------- /Textures/Taco256.blp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/Taco256.blp -------------------------------------------------------------------------------- /Textures/WARLOCK.blp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/WARLOCK.blp -------------------------------------------------------------------------------- /Textures/WARNING.blp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/WARNING.blp -------------------------------------------------------------------------------- /Textures/WARRIOR.blp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/WARRIOR.blp -------------------------------------------------------------------------------- /Textures/WhiteUp.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/WhiteUp.tga -------------------------------------------------------------------------------- /Textures/BlueReset.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/BlueReset.tga -------------------------------------------------------------------------------- /Textures/GreenPlus.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/GreenPlus.tga -------------------------------------------------------------------------------- /Textures/GreyStar.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/GreyStar.tga -------------------------------------------------------------------------------- /Textures/LOGO-WHITE.blp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/LOGO-WHITE.blp -------------------------------------------------------------------------------- /Textures/WhiteCopy.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/WhiteCopy.tga -------------------------------------------------------------------------------- /Textures/WhiteDown.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/WhiteDown.tga -------------------------------------------------------------------------------- /Textures/WhiteEye.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/WhiteEye.tga -------------------------------------------------------------------------------- /Textures/WhiteMag.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/WhiteMag.tga -------------------------------------------------------------------------------- /Textures/WhiteReset.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/WhiteReset.tga -------------------------------------------------------------------------------- /Textures/WhiteRight.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/WhiteRight.tga -------------------------------------------------------------------------------- /Textures/WhiteStar.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/WhiteStar.tga -------------------------------------------------------------------------------- /Textures/YellowStar.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/YellowStar.tga -------------------------------------------------------------------------------- /Textures/DEATHKNIGHT.blp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/DEATHKNIGHT.blp -------------------------------------------------------------------------------- /Textures/LOGO-ORANGE.blp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/LOGO-ORANGE.blp -------------------------------------------------------------------------------- /Textures/MonoCircle2.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/MonoCircle2.tga -------------------------------------------------------------------------------- /Textures/MonoCircle5.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/MonoCircle5.tga -------------------------------------------------------------------------------- /Textures/GreenPlusOutline.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmsl/hekili/HEAD/Textures/GreenPlusOutline.tga -------------------------------------------------------------------------------- /Libs/TaintLess/TaintLess.toc: -------------------------------------------------------------------------------- 1 | ## Interface: 90001 2 | ## Title: TaintLess 3 | ## Notes: Eliminates certain classes of taint errors. 4 | ## Version: 20-10-19 5 | TaintLess.xml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hekili 2 | This priority helper supports all DPS and Tank specializations in World of Warcraft **Retail**. 3 | 4 | [Latest Release](https://github.com/Hekili/hekili/releases/latest) 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: c 3 | 4 | addons: 5 | apt: 6 | packages: 7 | - pandoc 8 | 9 | branches: 10 | only: 11 | - /^v?\d+\.\d+(\.\d+)?(-\S*)?$/ 12 | 13 | script: curl -s https://raw.githubusercontent.com/BigWigsMods/packager/master/release.sh | bash -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /Classic/APLs/RogueCombat.simc: -------------------------------------------------------------------------------- 1 | actions+=/kick,if=target.casting 2 | actions+=/slice_and_dice,if=buff.slice_and_dice.down&combo_points>=2 3 | actions+=/slice_and_dice,if=combo_points=5&buff.slice_and_dice.remains<3 4 | actions+=/adrenaline_rush,if=active_enemies>1&buff.blade_flurry.up|cooldown.blade_flurry.remains>10 5 | actions+=/blade_flurry,if=active_enemies>1 6 | actions+=/sinister_strike,if=combo_points<5&buff.slice_and_dice.remains>=3 7 | actions+=/eviscerate,if=combo_points>=5 8 | actions+=/sinister_strike 9 | -------------------------------------------------------------------------------- /Classic/APLs/RogueAssassination.simc: -------------------------------------------------------------------------------- 1 | actions.precombat=stealth 2 | 3 | actions+=/kick,if=target.casting 4 | actions+=/ambush,if=stealthed 5 | actions+=/backstab,if=!stealthed 6 | actions+=/slice_and_dice,if=buff.slice_and_dice.down&combo_points>=2 7 | actions+=/adrenaline_rush 8 | actions+=/backstab,if=combo_points<5 9 | actions+=/sinister_strike,if=combo_points<5 10 | actions+=/slice_and_dice,if=combo_points=5&buff.slice_and_dice.remains<3 11 | actions+=/eviscerate,if=combo_points=5 12 | actions+=/blade_flurry,if=active_enemies>1 13 | actions+=/adrenaline_rush,if=active_enemies>1&buff.blade_flurry.up 14 | actions+=/eviscerate,if=combo_points=5 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: Hekili 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['http://paypal.me/Hekili'] 13 | -------------------------------------------------------------------------------- /Hekili.toc: -------------------------------------------------------------------------------- 1 | ## Interface: 11505 2 | ## Version: @project-version@ 3 | ## Title: Hekili 4 | ## Author: Hekili 5 | ## X-Flavor: Classic 6 | ## Notes: Priority helper for many DPS and tanking specializations, based on SimulationCraft action lists. 7 | ## SavedVariables: HekiliDB 8 | ## OptionalDeps: AddOnSkins, ButtonForge, ConsolePort, ElvUI, LibDualSpec-1.0, Masque, WeakAuras 9 | ## X-Curse-Project-ID: 69254 10 | ## X-WoWI-ID: 24608 11 | ## X-Wago-ID: WYK9WXNL 12 | 13 | embeds.xml 14 | 15 | Hekili.lua 16 | Utils.lua 17 | Formatting.lua 18 | MultilineEditor.lua 19 | Constants.lua 20 | State.lua 21 | Events.lua 22 | 23 | Classes.lua 24 | 25 | Classic\Classes.lua 26 | Classic\Druid.lua 27 | Classic\Hunter.lua 28 | Classic\Mage.lua 29 | Classic\Paladin.lua 30 | Classic\Priest.lua 31 | Classic\Rogue.lua 32 | Classic\Shaman.lua 33 | Classic\Warlock.lua 34 | Classic\Warrior.lua 35 | 36 | Targets.lua 37 | Options.lua 38 | UI.lua 39 | Scripts.lua 40 | Core.lua 41 | -------------------------------------------------------------------------------- /Bindings.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | -------------------------------------------------------------------------------- /UI/PopupSlider.lua: -------------------------------------------------------------------------------- 1 | local strformat = string.format 2 | local noop = function() end 3 | 4 | HekiliPopupDropdownMixin = {}; 5 | 6 | function HekiliPopupDropdownMixin:OnLoad() 7 | local function UpdateText(slider, value, isMouse) 8 | if value % 1 > 0 then 9 | self.Text:SetText( strformat( "%.1f", value ) ) 10 | else 11 | self.Text:SetText( strformat( "%d", value ) ) 12 | end 13 | end 14 | self.Slider:RegisterPropertyChangeHandler( "OnValueChanged", UpdateText ) 15 | end 16 | 17 | function HekiliPopupDropdownMixin:OnShow() 18 | -- self.Toggle:RegisterEvents(); 19 | if ElvUI then 20 | local E = ElvUI[1] 21 | local S = E:GetModule( "Skins" ) 22 | S:HandleSliderFrame( self.Slider ) 23 | 24 | local r, g, b = unpack( E.media.rgbvaluecolor ) 25 | 26 | local name = self:GetName() 27 | local highlight = _G[ name .. "Highlight" ] 28 | 29 | highlight:SetTexture( E.Media.Textures.Highlight ) 30 | highlight:SetBlendMode( 'BLEND' ) 31 | highlight:SetDrawLayer( 'BACKGROUND' ) 32 | highlight:SetVertexColor( r, g, b ) 33 | 34 | self.Slider.backdrop:SetFrameLevel( self:GetFrameLevel() + 1 ) 35 | end 36 | 37 | self.Slider:SetFrameLevel( self:GetFrameLevel() + 2 ) 38 | 39 | self:ClearAllPoints() 40 | self:SetAllPoints( self.owningButton ) 41 | end 42 | 43 | function HekiliPopupDropdownMixin:OnHide() 44 | -- self.Toggle:UnregisterEvents(); 45 | end 46 | 47 | function HekiliPopupDropdownMixin:OnSetOwningButton() 48 | -- self.Toggle:UpdateVisibleState(); 49 | self.Slider:UpdateVisibleState() 50 | end 51 | 52 | 53 | HekiliPopupDropdownSliderMixin = {}; 54 | 55 | function HekiliPopupDropdownSliderMixin:OnLoad() 56 | self:SetAccessorFunction(self.Set or noop); 57 | self:SetMutatorFunction(self.Get or noop); 58 | end -------------------------------------------------------------------------------- /embeds.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature-Request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: I would like the addon to have a new feature, and I'll explain why. This template is NOT for anything related to the addon's current recommendations. 3 | title: "[FEATURE] REPLACE THIS TEXT WITH A BRIEF DESCRIPTION OF YOUR NEW FEATURE" 4 | labels: [enhancement, triage] 5 | assignees: 6 | - Hekili 7 | body: 8 | - type: checkboxes 9 | id: precheck 10 | attributes: 11 | label: Before You Begin 12 | description: Please confirm that you've taken these preliminary steps before submitting your feature request. 13 | options: 14 | - label: I confirm that I have downloaded the latest version of the addon. 15 | required: true 16 | - label: I am not playing on a private server. 17 | required: true 18 | - label: I checked for an [existing, open ticket](https://github.com/Hekili/hekili/labels/enhancement) for this request and was not able to find one. 19 | required: true 20 | - label: I edited the title of this feature request (above) so that it describes the issue I am reporting. 21 | required: true 22 | - type: textarea 23 | id: request 24 | attributes: 25 | label: Feature Request 26 | description: | 27 | Please describe the new feature. Why would it be helpful? Explain the benefits. 28 | You can CTRL+V to paste a screenshot (image) or supply links to screenshot images. 29 | placeholder: "Example: I'd like Hekili to flip all the recommendations upside down. This would help me because I hang like a bat while playing WoW." 30 | validations: 31 | required: true 32 | - type: textarea 33 | id: addl-info 34 | attributes: 35 | label: Additional Information 36 | description: Please provide any additional information regarding this issue that was not included above. 37 | placeholder: If there's nothing else to provide, you may leave this blank. 38 | - type: input 39 | id: contact 40 | attributes: 41 | label: Contact Information 42 | description: | 43 | I will contact you via this GitHub ticket with questions and updates. 44 | If you do not regularly check your GitHub email, please provide an alternate contact method (i.e., Discord ID) so that I can reach you if needed. 45 | placeholder: Hekili#0001 46 | -------------------------------------------------------------------------------- /Classic/APLs/WarriorArms.simc: -------------------------------------------------------------------------------- 1 | actions+=/sunder_armor,if=settings.debuff_sunder_enabled&debuff.sunder_armor.stack=settings.debuff_sunder_min_level 2 | actions+=/demoralizing_shout,if=settings.debuff_demoshout_enabled&!debuff.demoralizing_shout.up 3 | actions+=/call_action_list,name=cooldowns,if=target.level>=63 4 | actions+=/run_action_list,name=execute,if=target.health.pct<=20 5 | actions+=/run_action_list,name=rotation 6 | 7 | actions.execute+=/heroic_strike,use_off_gcd=1,if=active_enemies=1&((!settings.adaptive_queueing_enabled&cooldown.mortal_strike.remains>=rage.time_to_42|settings.adaptive_queueing_enabled&should_hs)|(target.health.pct<=20&settings.execute_queueing_enabled)) 8 | actions.execute+=/cleave,use_off_gcd=1,if=active_enemies>1&((!settings.adaptive_queueing_enabled&cooldown.mortal_strike.remains>=rage.time_to_50|settings.adaptive_queueing_enabled&should_cleave)|(target.health.pct<=20&settings.execute_queueing_enabled)) 9 | actions.execute+=/execute 10 | 11 | actions.rotation+=/heroic_strike,use_off_gcd=1,if=active_enemies=1&((!settings.adaptive_queueing_enabled&cooldown.mortal_strike.remains>=rage.time_to_42|settings.adaptive_queueing_enabled&should_hs)|(target.health.pct<=20&settings.execute_queueing_enabled)) 12 | actions.rotation+=/cleave,use_off_gcd=1,if=active_enemies>1&((!settings.adaptive_queueing_enabled&cooldown.mortal_strike.remains>=rage.time_to_50|settings.adaptive_queueing_enabled&should_cleave)|(target.health.pct<=20&settings.execute_queueing_enabled)) 13 | actions.rotation+=/overpower 14 | actions.rotation+=/whirlwind,if=active_enemies>=settings.ww_min_enemies|(!settings.adaptive_queueing_enabled&cooldown.mortal_strike.remains>=settings.ww_cd_diff|settings.adaptive_queueing_enabled&rage.time_to_55=settings.hamstring_cd_diff&cooldown.whirlwind.remains>=settings.hamstring_cd_diff&rage.current>hamstring_threshold 17 | actions.rotation+=/bloodrage,if=rage.current<90 18 | actions.rotation+=/battle_shout,if=!up 19 | actions.rotation+=/berserker_stance,use_off_gcd=1,if=!up 20 | actions.rotation+=/battle_stance,use_off_gcd=1,if=!up&settings.overpower_enabled&buff.overpower_ready.up&rage.current>=action.overpower.cost&rage.current=settings.debuff_sunder_min_level 2 | actions+=/demoralizing_shout,if=settings.debuff_demoshout_enabled&!debuff.demoralizing_shout.up 3 | actions+=/call_action_list,name=cooldowns,if=target.level>=63 4 | actions+=/run_action_list,name=execute,if=target.health.pct<=20 5 | actions+=/run_action_list,name=rotation 6 | 7 | actions.execute+=/heroic_strike,use_off_gcd=1,if=active_enemies=1&((!settings.adaptive_queueing_enabled&cooldown.mortal_strike.remains>=rage.time_to_42|settings.adaptive_queueing_enabled&should_hs)|(target.health.pct<=20&settings.execute_queueing_enabled)) 8 | actions.execute+=/cleave,use_off_gcd=1,if=active_enemies>1&((!settings.adaptive_queueing_enabled&cooldown.mortal_strike.remains>=rage.time_to_50|settings.adaptive_queueing_enabled&should_cleave)|(target.health.pct<=20&settings.execute_queueing_enabled)) 9 | actions.execute+=/execute 10 | 11 | actions.rotation+=/heroic_strike,use_off_gcd=1,if=active_enemies=1&((!settings.adaptive_queueing_enabled&cooldown.mortal_strike.remains>=rage.time_to_42|settings.adaptive_queueing_enabled&should_hs)|(target.health.pct<=20&settings.execute_queueing_enabled)) 12 | actions.rotation+=/cleave,use_off_gcd=1,if=active_enemies>1&((!settings.adaptive_queueing_enabled&cooldown.mortal_strike.remains>=rage.time_to_50|settings.adaptive_queueing_enabled&should_cleave)|(target.health.pct<=20&settings.execute_queueing_enabled)) 13 | actions.rotation+=/overpower 14 | actions.rotation+=/whirlwind,if=active_enemies>=settings.ww_min_enemies|(!settings.adaptive_queueing_enabled&cooldown.mortal_strike.remains>=settings.ww_cd_diff|settings.adaptive_queueing_enabled&rage.time_to_55=settings.hamstring_cd_diff&cooldown.whirlwind.remains>=settings.hamstring_cd_diff&rage.current>hamstring_threshold 17 | actions.rotation+=/bloodrage,if=rage.current<90 18 | actions.rotation+=/battle_shout,if=!up 19 | actions.rotation+=/berserker_stance,use_off_gcd=1,if=!up 20 | actions.rotation+=/battle_stance,use_off_gcd=1,if=!up&settings.overpower_enabled&buff.overpower_ready.up&rage.current>=action.overpower.cost&rage.current=settings.debuff_sunder_min_level 2 | actions+=/demoralizing_shout,if=settings.debuff_demoshout_enabled&!debuff.demoralizing_shout.up 3 | actions+=/call_action_list,name=cooldowns,if=target.level>=63 4 | actions+=/run_action_list,name=execute,if=target.health.pct<=20 5 | actions+=/run_action_list,name=rotation 6 | 7 | actions.execute+=/heroic_strike,use_off_gcd=1,if=active_enemies=1&((!settings.adaptive_queueing_enabled&cooldown.bloodthirst.remains>=rage.time_to_42|settings.adaptive_queueing_enabled&should_hs)|(target.health.pct<=20&settings.execute_queueing_enabled)) 8 | actions.execute+=/cleave,use_off_gcd=1,if=active_enemies>1&((!settings.adaptive_queueing_enabled&cooldown.bloodthirst.remains>=rage.time_to_50|settings.adaptive_queueing_enabled&should_cleave)|(target.health.pct<=20&settings.execute_queueing_enabled)) 9 | actions.execute+=/bloodthirst,if=settings.adaptive_exec_enabled&bt_over_exec&target.time_to_die>2 10 | actions.execute+=/execute 11 | 12 | actions.rotation+=/heroic_strike,use_off_gcd=1,if=active_enemies=1&((!settings.adaptive_queueing_enabled&cooldown.bloodthirst.remains>=rage.time_to_42|settings.adaptive_queueing_enabled&should_hs)|(target.health.pct<=20&settings.execute_queueing_enabled)) 13 | actions.rotation+=/cleave,use_off_gcd=1,if=active_enemies>1&((!settings.adaptive_queueing_enabled&cooldown.bloodthirst.remains>=rage.time_to_50|settings.adaptive_queueing_enabled&should_cleave)|(target.health.pct<=20&settings.execute_queueing_enabled)) 14 | actions.rotation+=/overpower 15 | actions.rotation+=/whirlwind,if=active_enemies>=ww_breakpoint 16 | actions.rotation+=/bloodthirst 17 | actions.rotation+=/whirlwind,if=!settings.adaptive_queueing_enabled&cooldown.bloodthirst.remains>=settings.ww_cd_diff|settings.adaptive_queueing_enabled&rage.time_to_55=settings.ww_cd_diff 18 | actions.rotation+=/hamstring,if=cooldown.bloodthirst.remains>=settings.hamstring_cd_diff&cooldown.whirlwind.remains>=settings.hamstring_cd_diff&rage.current>hamstring_threshold 19 | actions.rotation+=/bloodrage,if=rage.current<90 20 | actions.rotation+=/battle_shout,if=!up 21 | actions.rotation+=/berserker_stance,use_off_gcd=1,if=!up 22 | actions.rotation+=/battle_stance,use_off_gcd=1,if=!up&settings.overpower_enabled&buff.overpower_ready.remains>1&rage.current>=action.overpower.cost&rage.current=settings.debuff_sunder_min_level 2 | actions+=/demoralizing_shout,if=settings.debuff_demoshout_enabled&!debuff.demoralizing_shout.up 3 | actions+=/call_action_list,name=cooldowns,if=target.level>=63 4 | actions+=/run_action_list,name=execute,if=target.health.pct<=20 5 | actions+=/run_action_list,name=rotation 6 | 7 | actions.execute+=/heroic_strike,use_off_gcd=1,if=active_enemies=1&((!settings.adaptive_queueing_enabled&cooldown.bloodthirst.remains>=rage.time_to_42|settings.adaptive_queueing_enabled&should_hs)|(target.health.pct<=20&settings.execute_queueing_enabled)) 8 | actions.execute+=/cleave,use_off_gcd=1,if=active_enemies>1&((!settings.adaptive_queueing_enabled&cooldown.bloodthirst.remains>=rage.time_to_50|settings.adaptive_queueing_enabled&should_cleave)|(target.health.pct<=20&settings.execute_queueing_enabled)) 9 | actions.execute+=/bloodthirst,if=settings.adaptive_exec_enabled&bt_over_exec&target.time_to_die>2 10 | actions.execute+=/execute 11 | 12 | actions.rotation+=/heroic_strike,use_off_gcd=1,if=active_enemies=1&((!settings.adaptive_queueing_enabled&cooldown.bloodthirst.remains>=rage.time_to_42|settings.adaptive_queueing_enabled&should_hs)|(target.health.pct<=20&settings.execute_queueing_enabled)) 13 | actions.rotation+=/cleave,use_off_gcd=1,if=active_enemies>1&((!settings.adaptive_queueing_enabled&cooldown.bloodthirst.remains>=rage.time_to_50|settings.adaptive_queueing_enabled&should_cleave)|(target.health.pct<=20&settings.execute_queueing_enabled)) 14 | actions.rotation+=/overpower 15 | actions.rotation+=/whirlwind,if=active_enemies>=ww_breakpoint 16 | actions.rotation+=/bloodthirst 17 | actions.rotation+=/whirlwind,if=!settings.adaptive_queueing_enabled&cooldown.bloodthirst.remains>=settings.ww_cd_diff|settings.adaptive_queueing_enabled&rage.time_to_55=settings.ww_cd_diff 18 | actions.rotation+=/hamstring,if=cooldown.bloodthirst.remains>=settings.hamstring_cd_diff&cooldown.whirlwind.remains>=settings.hamstring_cd_diff&rage.current>hamstring_threshold 19 | actions.rotation+=/bloodrage,if=rage.current<90 20 | actions.rotation+=/battle_shout,if=!up 21 | actions.rotation+=/berserker_stance,use_off_gcd=1,if=!up 22 | actions.rotation+=/battle_stance,use_off_gcd=1,if=!up&settings.overpower_enabled&buff.overpower_ready.remains>1&rage.current>=action.overpower.cost&rage.current { 8 | if (token in variables) { 9 | return `(${variables[token]})`; 10 | } else { 11 | throw new Error(`Undefined variable: {{${token}}}`); 12 | } 13 | }); 14 | } 15 | 16 | function processTemplate(inputFile, outputFile) { 17 | if (inputFile === outputFile) { 18 | throw new Error("Input and output filenames must be different."); 19 | } 20 | if (!fs.existsSync(inputFile)) { 21 | throw new Error(`File not found: ${inputFile}`); 22 | } 23 | 24 | const content = fs.readFileSync(inputFile, 'utf-8'); 25 | const lines = content.split('\n'); 26 | 27 | const variables = {}; 28 | const processedLines = []; 29 | let pushReady = false; 30 | 31 | for (const line of lines) { 32 | const match = line.match(/#\s*([\w\d]+)=(.+)/); 33 | if (match) { 34 | // console.log(`var: ${line}`) 35 | const [, key, rawValue] = match; 36 | const processedValue = replaceTokens(rawValue, variables); 37 | variables[key.trim()] = processedValue.trim(); 38 | } else { 39 | // console.log(`apl: ${line}`) 40 | const processedLine = replaceTokens(line, variables); 41 | if (processedLine.trim() || pushReady) { 42 | processedLines.push(processedLine); 43 | if (processedLine.trim()) { 44 | pushReady = true; 45 | } 46 | } 47 | } 48 | } 49 | 50 | fs.writeFileSync(outputFile, processedLines.join('\n'), 'utf-8'); 51 | console.log(`File processed and saved to: ${outputFile}`); 52 | } 53 | 54 | function processDirectory() { 55 | const scriptDir = __dirname; 56 | const inputFiles = fs.readdirSync(scriptDir).filter(file => file.endsWith('.t.simc')); 57 | 58 | if (inputFiles.length === 0) { 59 | console.error("No files matching the pattern *.t.simc found in the script directory."); 60 | process.exit(1); 61 | } 62 | 63 | inputFiles.forEach(inputFile => { 64 | const inputPath = path.join(scriptDir, inputFile); 65 | const outputPath = path.join(scriptDir, inputFile.replace('.t.simc', '.simc')); 66 | try { 67 | processTemplate(inputPath, outputPath); 68 | } catch (error) { 69 | console.error(`Error processing ${inputFile}: ${error.message}`); 70 | } 71 | }); 72 | } 73 | 74 | const args = process.argv.slice(2); 75 | if (args.length === 0) { 76 | processDirectory(); 77 | } else if (args.length === 2) { 78 | const [inputFile, outputFile] = args; 79 | try { 80 | processTemplate(inputFile, outputFile); 81 | } catch (error) { 82 | console.error(`Error: ${error.message}`); 83 | process.exit(1); 84 | } 85 | } else { 86 | console.error("Usage: node template.js "); 87 | console.error(" node template.js (to process all *.t.simc files in the script directory)"); 88 | process.exit(1); 89 | } -------------------------------------------------------------------------------- /Libs/LibDataBroker-1.1/LibDataBroker-1.1.lua: -------------------------------------------------------------------------------- 1 | 2 | assert(LibStub, "LibDataBroker-1.1 requires LibStub") 3 | assert(LibStub:GetLibrary("CallbackHandler-1.0", true), "LibDataBroker-1.1 requires CallbackHandler-1.0") 4 | 5 | local lib, oldminor = LibStub:NewLibrary("LibDataBroker-1.1", 4) 6 | if not lib then return end 7 | oldminor = oldminor or 0 8 | 9 | 10 | lib.callbacks = lib.callbacks or LibStub:GetLibrary("CallbackHandler-1.0"):New(lib) 11 | lib.attributestorage, lib.namestorage, lib.proxystorage = lib.attributestorage or {}, lib.namestorage or {}, lib.proxystorage or {} 12 | local attributestorage, namestorage, callbacks = lib.attributestorage, lib.namestorage, lib.callbacks 13 | 14 | if oldminor < 2 then 15 | lib.domt = { 16 | __metatable = "access denied", 17 | __index = function(self, key) return attributestorage[self] and attributestorage[self][key] end, 18 | } 19 | end 20 | 21 | if oldminor < 3 then 22 | lib.domt.__newindex = function(self, key, value) 23 | if not attributestorage[self] then attributestorage[self] = {} end 24 | if attributestorage[self][key] == value then return end 25 | attributestorage[self][key] = value 26 | local name = namestorage[self] 27 | if not name then return end 28 | callbacks:Fire("LibDataBroker_AttributeChanged", name, key, value, self) 29 | callbacks:Fire("LibDataBroker_AttributeChanged_"..name, name, key, value, self) 30 | callbacks:Fire("LibDataBroker_AttributeChanged_"..name.."_"..key, name, key, value, self) 31 | callbacks:Fire("LibDataBroker_AttributeChanged__"..key, name, key, value, self) 32 | end 33 | end 34 | 35 | if oldminor < 2 then 36 | function lib:NewDataObject(name, dataobj) 37 | if self.proxystorage[name] then return end 38 | 39 | if dataobj then 40 | assert(type(dataobj) == "table", "Invalid dataobj, must be nil or a table") 41 | self.attributestorage[dataobj] = {} 42 | for i,v in pairs(dataobj) do 43 | self.attributestorage[dataobj][i] = v 44 | dataobj[i] = nil 45 | end 46 | end 47 | dataobj = setmetatable(dataobj or {}, self.domt) 48 | self.proxystorage[name], self.namestorage[dataobj] = dataobj, name 49 | self.callbacks:Fire("LibDataBroker_DataObjectCreated", name, dataobj) 50 | return dataobj 51 | end 52 | end 53 | 54 | if oldminor < 1 then 55 | function lib:DataObjectIterator() 56 | return pairs(self.proxystorage) 57 | end 58 | 59 | function lib:GetDataObjectByName(dataobjectname) 60 | return self.proxystorage[dataobjectname] 61 | end 62 | 63 | function lib:GetNameByDataObject(dataobject) 64 | return self.namestorage[dataobject] 65 | end 66 | end 67 | 68 | if oldminor < 4 then 69 | local next = pairs(attributestorage) 70 | function lib:pairs(dataobject_or_name) 71 | local t = type(dataobject_or_name) 72 | assert(t == "string" or t == "table", "Usage: ldb:pairs('dataobjectname') or ldb:pairs(dataobject)") 73 | 74 | local dataobj = self.proxystorage[dataobject_or_name] or dataobject_or_name 75 | assert(attributestorage[dataobj], "Data object not found") 76 | 77 | return next, attributestorage[dataobj], nil 78 | end 79 | 80 | local ipairs_iter = ipairs(attributestorage) 81 | function lib:ipairs(dataobject_or_name) 82 | local t = type(dataobject_or_name) 83 | assert(t == "string" or t == "table", "Usage: ldb:ipairs('dataobjectname') or ldb:ipairs(dataobject)") 84 | 85 | local dataobj = self.proxystorage[dataobject_or_name] or dataobject_or_name 86 | assert(attributestorage[dataobj], "Data object not found") 87 | 88 | return ipairs_iter, attributestorage[dataobj], 0 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /Classic/APLs/DruidFeral.t.simc: -------------------------------------------------------------------------------- 1 | # end_thresh=10 2 | # rip_now=settings.rip_enabled&combo_points.current>=settings.rip_cp&!debuff.rip.up&ttd>={{end_thresh}} 3 | # bite_before_rip=settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time 4 | # bite_before_rip_next={{bite_before_rip}}&debuff.rip.remains-energy.time_to_tick>=settings.bite_time 5 | # bite_over_rip=settings.bite_enabled&!settings.rip_enabled 6 | # bite_now=settings.bite_enabled&({{bite_before_rip}}|{{bite_over_rip}})&combo_points.current>=settings.bite_cp 7 | # can_powershift=settings.powershift_enabled&set_bonus.wolfshead=1&talent.furor.rank=5&mana.current>=action.cat_form.cost 8 | # no_finisher=!settings.bite_enabled&!settings.rip_enabled 9 | # bite_at_end=combo_points.current>=settings.bite_cp&(ttd<{{end_thresh}}|(debuff.rip.up&ttd-debuff.rip.remains<{{end_thresh}}))&!{{no_finisher}} 10 | # rip_next=settings.rip_enabled&({{rip_now}}|(combo_points.current>=settings.rip_cp&debuff.rip.remains<=energy.time_to_tick))&ttd-energy.time_to_tick>={{end_thresh}} 11 | # jit_shift=energy.time_to_tick>settings.powershift_time 12 | actions.innervate_or_shift+=/innervate,if=settings.innervate_enabled&action.innervate.known&mana.pct<=innervate_threshold&ttd>2 13 | actions.innervate_or_shift+=/best_mana_potion 14 | actions.innervate_or_shift+=/mana_rune 15 | actions.innervate_or_shift+=/cat_form,if={{can_powershift}} 16 | 17 | actions.ranged+=/tigers_fury,if=energy.current=100&target.outside5 18 | actions.ranged+=/faerie_fire_feral,if=debuff.faerie_fire_feral.remains<12&debuff.faerie_fire.remains<12&target.outside2 19 | 20 | actions.precombat+=/mark_of_the_wild,if=!up&!buff.gift_of_the_wild.up 21 | actions.precombat+=/thorns,if=!up 22 | actions.precombat+=/omen_of_clarity,if=!buff.omen_of_clarity.up 23 | actions.precombat+=/moonfire,if=!debuff.moonfire.up&target.outside2&mana.current>=action.moonfire.cost+(buff.bear_form.up&action.bear_form.cost|action.cat_form.cost) 24 | actions.precombat+=/cat_form,if=!buff.form.up 25 | 26 | actions+=/use_items,if=!buff.cat_form.up 27 | actions+=/potion,if=!buff.cat_form.up 28 | actions+=/best_mana_potion,if=!buff.form.up&mana.current60 39 | actions.bear+=/faerie_fire_feral 40 | 41 | actions.cat_solo+=/moonfire,if=!debuff.moonfire.up&target.outside2 42 | actions.cat_solo+=/rip,if={{rip_now}} 43 | actions.cat_solo+=/ferocious_bite,if={{bite_now}} 44 | actions.cat_solo+=/claw 45 | 46 | actions.cat_oom+=/rip,if={{rip_now}} 47 | actions.cat_oom+=/shred,if=energy.current>=63 48 | actions.cat_oom+=/ferocious_bite,if={{bite_now}} 49 | actions.cat_oom+=/shred 50 | actions.cat_oom+=/claw,if=!action.shred.known 51 | 52 | actions.cat_ps+=/call_action_list,name=innervate_or_shift,if=energy.current<10 53 | # if0={{rip_now}} 54 | actions.cat_ps+=/rip,if={{if0}} 55 | actions.cat_ps+=/call_action_list,name=innervate_or_shift,if={{if0}}&{{jit_shift}} 56 | # if1={{bite_now}}|{{bite_at_end}} 57 | actions.cat_ps+=/shred,if=!{{if0}}&{{if1}}&(energy.current>=63|(energy.current>=15&buff.clearcasting.up)) 58 | actions.cat_ps+=/ferocious_bite,if=!{{if0}}&{{if1}}&!buff.clearcasting.up 59 | # wait1=energy.current>=28&{{bite_before_rip}}&!{{bite_before_rip_next}} 60 | # wait2=energy.current>=15&(!{{bite_before_rip}}|{{bite_before_rip_next}}|{{bite_at_end}}) 61 | # wait3={{rip_next}} 62 | actions.cat_ps+=/call_action_list,name=innervate_or_shift,if=!{{if0}}&{{if1}}&!{{wait1}}&!{{wait2}}&!{{wait3}} 63 | actions.cat_ps+=/call_action_list,name=innervate_or_shift,if=!{{if0}}&{{if1}}&({{wait1}}|{{wait2}}|{{wait3}})&{{jit_shift}} 64 | # if2=energy.current>=28 65 | actions.cat_ps+=/shred,if=!{{if0}}&!{{if1}}&{{if2}} 66 | actions.cat_ps+=/claw,if=!{{if0}}&!{{if1}}&{{if2}}&energy.time_to_tick>1&settings.claw_trick_enabled 67 | actions.cat_ps+=/run_action_list,name=innervate_or_shift,if=!{{if0}}&!{{if1}}&{{if2}}&energy.time_to_tick>settings.powershift_time 68 | # if3=!{{rip_next}} 69 | actions.cat_ps+=/run_action_list,name=innervate_or_shift,if=!{{if0}}&!{{if1}}&!{{if2}}&{{if3}} 70 | # if4=energy.time_to_tick>settings.powershift_time 71 | actions.cat_ps+=/run_action_list,name=innervate_or_shift,if=!{{if0}}&!{{if1}}&!{{if2}}&!{{if3}}&{{if4}} -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug-Report-Form.yml: -------------------------------------------------------------------------------- 1 | name: In-Game Error 2 | description: | 3 | The addon does not load or there are error messages in-game. 4 | I'll use BugSack and BugGrabber to supply the messages. 5 | title: "[BUG] REPLACE THIS TEXT WITH A SHORT DESCRIPTION OF THE BUG YOU ARE REPORTING" 6 | labels: [bug, triage] 7 | assignees: 8 | - Hekili 9 | body: 10 | - type: checkboxes 11 | id: precheck 12 | attributes: 13 | label: Before You Begin 14 | description: Please confirm that you've taken these preliminary steps before submitting your issue report. 15 | options: 16 | - label: I confirm that I have downloaded the latest version of the addon. 17 | required: true 18 | - label: I am not playing on a private server. 19 | required: true 20 | - label: I checked for an [existing, open ticket](https://github.com/Hekili/hekili/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) for this issue and was not able to find one. 21 | required: true 22 | - label: I edited the title of this bug report (above) so that it describes the issue I am reporting. 23 | required: true 24 | - type: textarea 25 | id: description 26 | attributes: 27 | label: Describe the Issue 28 | description: Please describe the issue in question. Be specific and describe what you see. You can CTRL+V to paste a screenshot (image) or supply links to screenshot images. 29 | placeholder: "Example: The addon is not loading for my Beast Mastery Demon Hunter. The following error message appears in my BugSack." 30 | validations: 31 | required: true 32 | - type: textarea 33 | id: reproduction 34 | attributes: 35 | label: How to Reproduce 36 | description: "Steps to reproduce the behavior" 37 | placeholder: | 38 | 1. Enter game as a Havoc Demon Hunter. 39 | 2. Change specialization to Beast Mastery. 40 | 3. Realize Beast Mastery Demon Hunters don't exist. 41 | validations: 42 | required: true 43 | - type: input 44 | id: player-info 45 | attributes: 46 | label: Player Information (Link) 47 | description: | 48 | Please supply your character information by completing the following steps. 49 | * Log into your WoW character. 50 | * Type `/hekili` and press Enter. 51 | * Open the Issue Reporting section (left panel). 52 | * Copy all of the text from the text box. 53 | * Paste the text to Pastebin. 54 | * Provide the Pastebin link here. 55 | 56 | This step is essential, as most issues are related to specific classes, specializations, gear, talent choices, or other game systems. 57 | If you do not provide this information, I cannot triage your problem. 58 | **Note:** Some errors may prevent you from opening `/hekili`. If that's true for you, please note that in this box instead of providing the link. 59 | placeholder: http://pastebin.com/AbCdEfGh 60 | validations: 61 | required: true 62 | - type: input 63 | id: error-messages 64 | attributes: 65 | label: Error Messages (Link) 66 | description: | 67 | If there are error messages in-game, please install [BugSack](https://www.curseforge.com/wow/addons/bugsack) and [BugGrabber](https://www.curseforge.com/wow/addons/bug-grabber) -- both! -- in order to collect the error message. 68 | * Install BugSack and BugGrabber if you don't already have them. 69 | * Log into WoW, open BugSack and clear the sack. 70 | * Reload WoW (/rl). 71 | * When WoW finishes reloading, open BugSack and copy the **first** error message. 72 | * If you need to do something to trigger the error message, do that and then open BugSack and copy the error message. 73 | * Paste the message to Pastebin (it's free!). 74 | * Provide the Pastebin link here. 75 | placeholder: http://pastebin.com/aBcDeFgH 76 | validations: 77 | required: true 78 | - type: textarea 79 | id: addl-info 80 | attributes: 81 | label: Additional Information 82 | description: Please provide any additional information regarding this issue that was not included above. 83 | placeholder: Leave blank, if all necessary details are included above. 84 | - type: input 85 | id: contact 86 | attributes: 87 | label: Contact Information 88 | description: I will contact you via this GitHub ticket with questions and updates. If you do not regularly check your GitHub email, please provide an alternate contact method (i.e., Discord ID) so that I can reach you if needed. 89 | placeholder: Hekili#0001 90 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Recommendation-Report.yml: -------------------------------------------------------------------------------- 1 | name: Priority/Recommendation Issue 2 | description: The addon appears fully functional, but its recommendations don't match my expectations. 3 | title: "[REC] REPLACE THIS TEXT WITH YOUR CLASS/SPEC AND BRIEF ISSUE DETAILS" 4 | labels: [recommendation, triage] 5 | assignees: 6 | - Hekili 7 | body: 8 | - type: checkboxes 9 | id: precheck 10 | attributes: 11 | label: Before You Begin 12 | description: Please confirm that you've taken these preliminary steps before submitting your issue report. 13 | options: 14 | - label: I confirm that I have downloaded the latest version of the addon. 15 | required: true 16 | - label: I am not playing on a private server. 17 | required: true 18 | - label: I checked for an [existing, open ticket](https://github.com/Hekili/hekili/labels/recommendation) for this issue and was not able to find one. 19 | required: true 20 | - label: I edited the title of this issue (above) so that it describes the issue I am reporting. 21 | required: true 22 | - label: I am reporting an issue with the default priority included with the specialization (imported or edited priorities are not supported). 23 | required: true 24 | - type: textarea 25 | id: description 26 | attributes: 27 | label: Describe the Issue 28 | description: Please describe the issue in question. Be specific and describe what you see. You can CTRL+V to paste a screenshot (image) or supply links to screenshot images. 29 | placeholder: "Example: For my Retribution Paladin, I expect the addon to recommend a Holy Power spender when I have 5 Holy Power. However, the addon recommended Crusader Strike instead." 30 | validations: 31 | required: true 32 | - type: textarea 33 | id: reproduction 34 | attributes: 35 | label: How to Reproduce 36 | description: Tell me what I need to do to see the issue you are reporting. 37 | placeholder: | 38 | 1. Enter game as a Retribution Paladin. 39 | 2. Take the Seraphim, Final Reckoning, and Execution Sentence talents. 40 | 3. Use Final Reckoning. 41 | 4. Build to 5 Holy Power. 42 | 5. See recommendations. 43 | validations: 44 | required: true 45 | - type: input 46 | id: player-info 47 | attributes: 48 | label: Snapshot (Link) 49 | description: | 50 | Please supply a Snapshot of the addon's decision-making when you are seeing this issue in-game. **This is not a screenshot.** To generate a Snapshot, please complete the following steps. 51 | * Log into your WoW character. 52 | * Recreate the issue you are reporting (i.e., generate 5 Holy Power). 53 | * When you see the recommendation you disagree with, press `ALT-SHIFT-P` to Pause and Snapshot (or use `ALT-SHIFT-[` to Snapshot without pausing). 54 | * You can change these keybindings in `/hekili` > Toggles if needed. 55 | * Type `/hekili` and press Enter. 56 | * Open the Snapshots section on the left side. 57 | * Select the display which is showing the recommendation (usually Primary). 58 | * Select the snapshot number (usually 1, if you took only 1 snapshot). 59 | * Copy all of the text in the lower box. 60 | * Paste the text to Pastebin (https://pastebin.com). 61 | * Provide the Pastebin link here. 62 | 63 | This step is essential, as most issues are related to specific classes, specializations, gear, talent choices, or other game systems. 64 | If you do not provide this information, I cannot triage your problem. 65 | placeholder: http://pastebin.com/AbCdEfGh 66 | validations: 67 | required: true 68 | - type: input 69 | id: sim-result 70 | attributes: 71 | label: Raidbots Sim Report (Link) 72 | description: | 73 | The addon's recommendations are based on the SimulationCraft profiles generated for each specialization. It is very helpful, for testing purposes, to compare the addon's recommendations to your sim's actions. [Raidbots](https://www.raidbots.com/) is the fastest, easiest way to simulate your character. 74 | * If the issue happens in single-target, link to your Patchwerk sim with 1 boss target. 75 | * If the issue happens in sustained multi-target, link to your Patchwerk sim with the appropriate number of boss targets. 76 | * If the issue happens in more chaotic scenarios, link to your Hectic Add Cleave sim with 1 boss target. 77 | * You're welcome to link multiple sims using the **Additional Information** box, below. 78 | placeholder: https://www.raidbots.com/simbot/report/abcdEFghIjKlMNOPqrst 79 | - type: textarea 80 | id: addl-info 81 | attributes: 82 | label: Additional Information 83 | description: Please provide any additional information regarding this issue that was not included above. 84 | placeholder: Leave blank, if all necessary details are included above. 85 | - type: input 86 | id: contact 87 | attributes: 88 | label: Contact Information 89 | description: | 90 | I will contact you via this GitHub ticket with questions and updates. 91 | If you do not regularly check your GitHub email, please provide an alternate contact method (i.e., Discord ID) so that I can reach you if needed. 92 | placeholder: Hekili#0001 93 | -------------------------------------------------------------------------------- /Classic/APLs/DruidFeral.simc: -------------------------------------------------------------------------------- 1 | actions.innervate_or_shift+=/innervate,if=settings.innervate_enabled&action.innervate.known&mana.pct<=innervate_threshold&ttd>2 2 | actions.innervate_or_shift+=/best_mana_potion 3 | actions.innervate_or_shift+=/mana_rune 4 | actions.innervate_or_shift+=/cat_form,if=(settings.powershift_enabled&set_bonus.wolfshead=1&talent.furor.rank=5&mana.current>=action.cat_form.cost) 5 | 6 | actions.ranged+=/tigers_fury,if=energy.current=100&target.outside5 7 | actions.ranged+=/faerie_fire_feral,if=debuff.faerie_fire_feral.remains<12&debuff.faerie_fire.remains<12&target.outside2 8 | 9 | actions.precombat+=/mark_of_the_wild,if=!up&!buff.gift_of_the_wild.up 10 | actions.precombat+=/thorns,if=!up 11 | actions.precombat+=/omen_of_clarity,if=!buff.omen_of_clarity.up 12 | actions.precombat+=/moonfire,if=!debuff.moonfire.up&target.outside2&mana.current>=action.moonfire.cost+(buff.bear_form.up&action.bear_form.cost|action.cat_form.cost) 13 | actions.precombat+=/cat_form,if=!buff.form.up 14 | 15 | actions+=/use_items,if=!buff.cat_form.up 16 | actions+=/potion,if=!buff.cat_form.up 17 | actions+=/best_mana_potion,if=!buff.form.up&mana.current=action.cat_form.cost) 24 | actions+=/run_action_list,name=cat_ps 25 | 26 | actions.bear+=/maul,use_off_gcd=1,if=!buff.maul.up 27 | actions.bear+=/swipe_bear,if=rage.current>60 28 | actions.bear+=/faerie_fire_feral 29 | 30 | actions.cat_solo+=/moonfire,if=!debuff.moonfire.up&target.outside2 31 | actions.cat_solo+=/rip,if=(settings.rip_enabled&combo_points.current>=settings.rip_cp&!debuff.rip.up&ttd>=(10)) 32 | actions.cat_solo+=/ferocious_bite,if=(settings.bite_enabled&((settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)|(settings.bite_enabled&!settings.rip_enabled))&combo_points.current>=settings.bite_cp) 33 | actions.cat_solo+=/claw 34 | 35 | actions.cat_oom+=/rip,if=(settings.rip_enabled&combo_points.current>=settings.rip_cp&!debuff.rip.up&ttd>=(10)) 36 | actions.cat_oom+=/shred,if=energy.current>=63 37 | actions.cat_oom+=/ferocious_bite,if=(settings.bite_enabled&((settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)|(settings.bite_enabled&!settings.rip_enabled))&combo_points.current>=settings.bite_cp) 38 | actions.cat_oom+=/shred 39 | actions.cat_oom+=/claw,if=!action.shred.known 40 | 41 | actions.cat_ps+=/call_action_list,name=innervate_or_shift,if=energy.current<10 42 | actions.cat_ps+=/rip,if=((settings.rip_enabled&combo_points.current>=settings.rip_cp&!debuff.rip.up&ttd>=(10))) 43 | actions.cat_ps+=/call_action_list,name=innervate_or_shift,if=((settings.rip_enabled&combo_points.current>=settings.rip_cp&!debuff.rip.up&ttd>=(10)))&(energy.time_to_tick>settings.powershift_time) 44 | actions.cat_ps+=/shred,if=!((settings.rip_enabled&combo_points.current>=settings.rip_cp&!debuff.rip.up&ttd>=(10)))&((settings.bite_enabled&((settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)|(settings.bite_enabled&!settings.rip_enabled))&combo_points.current>=settings.bite_cp)|(combo_points.current>=settings.bite_cp&(ttd<(10)|(debuff.rip.up&ttd-debuff.rip.remains<(10)))&!(!settings.bite_enabled&!settings.rip_enabled)))&(energy.current>=63|(energy.current>=15&buff.clearcasting.up)) 45 | actions.cat_ps+=/ferocious_bite,if=!((settings.rip_enabled&combo_points.current>=settings.rip_cp&!debuff.rip.up&ttd>=(10)))&((settings.bite_enabled&((settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)|(settings.bite_enabled&!settings.rip_enabled))&combo_points.current>=settings.bite_cp)|(combo_points.current>=settings.bite_cp&(ttd<(10)|(debuff.rip.up&ttd-debuff.rip.remains<(10)))&!(!settings.bite_enabled&!settings.rip_enabled)))&!buff.clearcasting.up 46 | actions.cat_ps+=/call_action_list,name=innervate_or_shift,if=!((settings.rip_enabled&combo_points.current>=settings.rip_cp&!debuff.rip.up&ttd>=(10)))&((settings.bite_enabled&((settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)|(settings.bite_enabled&!settings.rip_enabled))&combo_points.current>=settings.bite_cp)|(combo_points.current>=settings.bite_cp&(ttd<(10)|(debuff.rip.up&ttd-debuff.rip.remains<(10)))&!(!settings.bite_enabled&!settings.rip_enabled)))&!(energy.current>=28&(settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)&!((settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)&debuff.rip.remains-energy.time_to_tick>=settings.bite_time))&!(energy.current>=15&(!(settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)|((settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)&debuff.rip.remains-energy.time_to_tick>=settings.bite_time)|(combo_points.current>=settings.bite_cp&(ttd<(10)|(debuff.rip.up&ttd-debuff.rip.remains<(10)))&!(!settings.bite_enabled&!settings.rip_enabled))))&!((settings.rip_enabled&((settings.rip_enabled&combo_points.current>=settings.rip_cp&!debuff.rip.up&ttd>=(10))|(combo_points.current>=settings.rip_cp&debuff.rip.remains<=energy.time_to_tick))&ttd-energy.time_to_tick>=(10))) 47 | actions.cat_ps+=/call_action_list,name=innervate_or_shift,if=!((settings.rip_enabled&combo_points.current>=settings.rip_cp&!debuff.rip.up&ttd>=(10)))&((settings.bite_enabled&((settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)|(settings.bite_enabled&!settings.rip_enabled))&combo_points.current>=settings.bite_cp)|(combo_points.current>=settings.bite_cp&(ttd<(10)|(debuff.rip.up&ttd-debuff.rip.remains<(10)))&!(!settings.bite_enabled&!settings.rip_enabled)))&((energy.current>=28&(settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)&!((settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)&debuff.rip.remains-energy.time_to_tick>=settings.bite_time))|(energy.current>=15&(!(settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)|((settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)&debuff.rip.remains-energy.time_to_tick>=settings.bite_time)|(combo_points.current>=settings.bite_cp&(ttd<(10)|(debuff.rip.up&ttd-debuff.rip.remains<(10)))&!(!settings.bite_enabled&!settings.rip_enabled))))|((settings.rip_enabled&((settings.rip_enabled&combo_points.current>=settings.rip_cp&!debuff.rip.up&ttd>=(10))|(combo_points.current>=settings.rip_cp&debuff.rip.remains<=energy.time_to_tick))&ttd-energy.time_to_tick>=(10))))&(energy.time_to_tick>settings.powershift_time) 48 | actions.cat_ps+=/shred,if=!((settings.rip_enabled&combo_points.current>=settings.rip_cp&!debuff.rip.up&ttd>=(10)))&!((settings.bite_enabled&((settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)|(settings.bite_enabled&!settings.rip_enabled))&combo_points.current>=settings.bite_cp)|(combo_points.current>=settings.bite_cp&(ttd<(10)|(debuff.rip.up&ttd-debuff.rip.remains<(10)))&!(!settings.bite_enabled&!settings.rip_enabled)))&(energy.current>=28) 49 | actions.cat_ps+=/claw,if=!((settings.rip_enabled&combo_points.current>=settings.rip_cp&!debuff.rip.up&ttd>=(10)))&!((settings.bite_enabled&((settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)|(settings.bite_enabled&!settings.rip_enabled))&combo_points.current>=settings.bite_cp)|(combo_points.current>=settings.bite_cp&(ttd<(10)|(debuff.rip.up&ttd-debuff.rip.remains<(10)))&!(!settings.bite_enabled&!settings.rip_enabled)))&(energy.current>=28)&energy.time_to_tick>1&settings.claw_trick_enabled 50 | actions.cat_ps+=/run_action_list,name=innervate_or_shift,if=!((settings.rip_enabled&combo_points.current>=settings.rip_cp&!debuff.rip.up&ttd>=(10)))&!((settings.bite_enabled&((settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)|(settings.bite_enabled&!settings.rip_enabled))&combo_points.current>=settings.bite_cp)|(combo_points.current>=settings.bite_cp&(ttd<(10)|(debuff.rip.up&ttd-debuff.rip.remains<(10)))&!(!settings.bite_enabled&!settings.rip_enabled)))&(energy.current>=28)&energy.time_to_tick>settings.powershift_time 51 | actions.cat_ps+=/run_action_list,name=innervate_or_shift,if=!((settings.rip_enabled&combo_points.current>=settings.rip_cp&!debuff.rip.up&ttd>=(10)))&!((settings.bite_enabled&((settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)|(settings.bite_enabled&!settings.rip_enabled))&combo_points.current>=settings.bite_cp)|(combo_points.current>=settings.bite_cp&(ttd<(10)|(debuff.rip.up&ttd-debuff.rip.remains<(10)))&!(!settings.bite_enabled&!settings.rip_enabled)))&!(energy.current>=28)&(!(settings.rip_enabled&((settings.rip_enabled&combo_points.current>=settings.rip_cp&!debuff.rip.up&ttd>=(10))|(combo_points.current>=settings.rip_cp&debuff.rip.remains<=energy.time_to_tick))&ttd-energy.time_to_tick>=(10))) 52 | actions.cat_ps+=/run_action_list,name=innervate_or_shift,if=!((settings.rip_enabled&combo_points.current>=settings.rip_cp&!debuff.rip.up&ttd>=(10)))&!((settings.bite_enabled&((settings.bite_enabled&debuff.rip.up&debuff.rip.remains>=settings.bite_time)|(settings.bite_enabled&!settings.rip_enabled))&combo_points.current>=settings.bite_cp)|(combo_points.current>=settings.bite_cp&(ttd<(10)|(debuff.rip.up&ttd-debuff.rip.remains<(10)))&!(!settings.bite_enabled&!settings.rip_enabled)))&!(energy.current>=28)&!(!(settings.rip_enabled&((settings.rip_enabled&combo_points.current>=settings.rip_cp&!debuff.rip.up&ttd>=(10))|(combo_points.current>=settings.rip_cp&debuff.rip.remains<=energy.time_to_tick))&ttd-energy.time_to_tick>=(10)))&(energy.time_to_tick>settings.powershift_time) -------------------------------------------------------------------------------- /Libs/LibDBIcon-1.0/LibDBIcon-1.0.lua: -------------------------------------------------------------------------------- 1 | 2 | ----------------------------------------------------------------------- 3 | -- LibDBIcon-1.0 4 | -- 5 | -- Allows addons to easily create a lightweight minimap icon as an alternative to heavier LDB displays. 6 | -- 7 | 8 | local DBICON10 = "LibDBIcon-1.0" 9 | local DBICON10_MINOR = 34 -- Bump on changes 10 | if not LibStub then error(DBICON10 .. " requires LibStub.") end 11 | local ldb = LibStub("LibDataBroker-1.1", true) 12 | if not ldb then error(DBICON10 .. " requires LibDataBroker-1.1.") end 13 | local lib = LibStub:NewLibrary(DBICON10, DBICON10_MINOR) 14 | if not lib then return end 15 | 16 | lib.disabled = lib.disabled or nil 17 | lib.objects = lib.objects or {} 18 | lib.callbackRegistered = lib.callbackRegistered or nil 19 | lib.callbacks = lib.callbacks or LibStub("CallbackHandler-1.0"):New(lib) 20 | lib.notCreated = lib.notCreated or {} 21 | 22 | function lib:IconCallback(event, name, key, value) 23 | if lib.objects[name] then 24 | if key == "icon" then 25 | lib.objects[name].icon:SetTexture(value) 26 | elseif key == "iconCoords" then 27 | lib.objects[name].icon:UpdateCoord() 28 | elseif key == "iconR" then 29 | local _, g, b = lib.objects[name].icon:GetVertexColor() 30 | lib.objects[name].icon:SetVertexColor(value, g, b) 31 | elseif key == "iconG" then 32 | local r, _, b = lib.objects[name].icon:GetVertexColor() 33 | lib.objects[name].icon:SetVertexColor(r, value, b) 34 | elseif key == "iconB" then 35 | local r, g = lib.objects[name].icon:GetVertexColor() 36 | lib.objects[name].icon:SetVertexColor(r, g, value) 37 | end 38 | end 39 | end 40 | if not lib.callbackRegistered then 41 | ldb.RegisterCallback(lib, "LibDataBroker_AttributeChanged__icon", "IconCallback") 42 | ldb.RegisterCallback(lib, "LibDataBroker_AttributeChanged__iconCoords", "IconCallback") 43 | ldb.RegisterCallback(lib, "LibDataBroker_AttributeChanged__iconR", "IconCallback") 44 | ldb.RegisterCallback(lib, "LibDataBroker_AttributeChanged__iconG", "IconCallback") 45 | ldb.RegisterCallback(lib, "LibDataBroker_AttributeChanged__iconB", "IconCallback") 46 | lib.callbackRegistered = true 47 | end 48 | 49 | local function getAnchors(frame) 50 | local x, y = frame:GetCenter() 51 | if not x or not y then return "CENTER" end 52 | local hhalf = (x > UIParent:GetWidth()*2/3) and "RIGHT" or (x < UIParent:GetWidth()/3) and "LEFT" or "" 53 | local vhalf = (y > UIParent:GetHeight()/2) and "TOP" or "BOTTOM" 54 | return vhalf..hhalf, frame, (vhalf == "TOP" and "BOTTOM" or "TOP")..hhalf 55 | end 56 | 57 | local function onEnter(self) 58 | if self.isMoving then return end 59 | local obj = self.dataObject 60 | if obj.OnTooltipShow then 61 | GameTooltip:SetOwner(self, "ANCHOR_NONE") 62 | GameTooltip:SetPoint(getAnchors(self)) 63 | obj.OnTooltipShow(GameTooltip) 64 | GameTooltip:Show() 65 | elseif obj.OnEnter then 66 | obj.OnEnter(self) 67 | end 68 | end 69 | 70 | local function onLeave(self) 71 | local obj = self.dataObject 72 | GameTooltip:Hide() 73 | if obj.OnLeave then obj.OnLeave(self) end 74 | end 75 | 76 | -------------------------------------------------------------------------------- 77 | 78 | local onClick, onMouseUp, onMouseDown, onDragStart, onDragStop, updatePosition 79 | 80 | do 81 | local minimapShapes = { 82 | ["ROUND"] = {true, true, true, true}, 83 | ["SQUARE"] = {false, false, false, false}, 84 | ["CORNER-TOPLEFT"] = {false, false, false, true}, 85 | ["CORNER-TOPRIGHT"] = {false, false, true, false}, 86 | ["CORNER-BOTTOMLEFT"] = {false, true, false, false}, 87 | ["CORNER-BOTTOMRIGHT"] = {true, false, false, false}, 88 | ["SIDE-LEFT"] = {false, true, false, true}, 89 | ["SIDE-RIGHT"] = {true, false, true, false}, 90 | ["SIDE-TOP"] = {false, false, true, true}, 91 | ["SIDE-BOTTOM"] = {true, true, false, false}, 92 | ["TRICORNER-TOPLEFT"] = {false, true, true, true}, 93 | ["TRICORNER-TOPRIGHT"] = {true, false, true, true}, 94 | ["TRICORNER-BOTTOMLEFT"] = {true, true, false, true}, 95 | ["TRICORNER-BOTTOMRIGHT"] = {true, true, true, false}, 96 | } 97 | 98 | function updatePosition(button) 99 | local angle = math.rad(button.db and button.db.minimapPos or button.minimapPos or 225) 100 | local x, y, q = math.cos(angle), math.sin(angle), 1 101 | if x < 0 then q = q + 1 end 102 | if y > 0 then q = q + 2 end 103 | local minimapShape = GetMinimapShape and GetMinimapShape() or "ROUND" 104 | local quadTable = minimapShapes[minimapShape] 105 | if quadTable[q] then 106 | x, y = x*80, y*80 107 | else 108 | local diagRadius = 103.13708498985 --math.sqrt(2*(80)^2)-10 109 | x = math.max(-80, math.min(x*diagRadius, 80)) 110 | y = math.max(-80, math.min(y*diagRadius, 80)) 111 | end 112 | button:SetPoint("CENTER", Minimap, "CENTER", x, y) 113 | end 114 | end 115 | 116 | function onClick(self, b) if self.dataObject.OnClick then self.dataObject.OnClick(self, b) end end 117 | function onMouseDown(self) self.isMouseDown = true; self.icon:UpdateCoord() end 118 | function onMouseUp(self) self.isMouseDown = false; self.icon:UpdateCoord() end 119 | 120 | do 121 | local function onUpdate(self) 122 | local mx, my = Minimap:GetCenter() 123 | local px, py = GetCursorPosition() 124 | local scale = Minimap:GetEffectiveScale() 125 | px, py = px / scale, py / scale 126 | if self.db then 127 | self.db.minimapPos = math.deg(math.atan2(py - my, px - mx)) % 360 128 | else 129 | self.minimapPos = math.deg(math.atan2(py - my, px - mx)) % 360 130 | end 131 | updatePosition(self) 132 | end 133 | 134 | function onDragStart(self) 135 | self:LockHighlight() 136 | self.isMouseDown = true 137 | self.icon:UpdateCoord() 138 | self:SetScript("OnUpdate", onUpdate) 139 | self.isMoving = true 140 | GameTooltip:Hide() 141 | end 142 | end 143 | 144 | function onDragStop(self) 145 | self:SetScript("OnUpdate", nil) 146 | self.isMouseDown = false 147 | self.icon:UpdateCoord() 148 | self:UnlockHighlight() 149 | self.isMoving = nil 150 | end 151 | 152 | local defaultCoords = {0, 1, 0, 1} 153 | local function updateCoord(self) 154 | local coords = self:GetParent().dataObject.iconCoords or defaultCoords 155 | local deltaX, deltaY = 0, 0 156 | if not self:GetParent().isMouseDown then 157 | deltaX = (coords[2] - coords[1]) * 0.05 158 | deltaY = (coords[4] - coords[3]) * 0.05 159 | end 160 | self:SetTexCoord(coords[1] + deltaX, coords[2] - deltaX, coords[3] + deltaY, coords[4] - deltaY) 161 | end 162 | 163 | local function createButton(name, object, db) 164 | local button = CreateFrame("Button", "LibDBIcon10_"..name, Minimap) 165 | button.dataObject = object 166 | button.db = db 167 | button:SetFrameStrata("MEDIUM") 168 | button:SetSize(31, 31) 169 | button:SetFrameLevel(8) 170 | button:RegisterForClicks("anyUp") 171 | button:RegisterForDrag("LeftButton") 172 | button:SetHighlightTexture(136477) --"Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight" 173 | local overlay = button:CreateTexture(nil, "OVERLAY") 174 | overlay:SetSize(53, 53) 175 | overlay:SetTexture(136430) --"Interface\\Minimap\\MiniMap-TrackingBorder" 176 | overlay:SetPoint("TOPLEFT") 177 | local background = button:CreateTexture(nil, "BACKGROUND") 178 | background:SetSize(20, 20) 179 | background:SetTexture(136467) --"Interface\\Minimap\\UI-Minimap-Background" 180 | background:SetPoint("TOPLEFT", 7, -5) 181 | local icon = button:CreateTexture(nil, "ARTWORK") 182 | icon:SetSize(17, 17) 183 | icon:SetTexture(object.icon) 184 | icon:SetPoint("TOPLEFT", 7, -6) 185 | button.icon = icon 186 | button.isMouseDown = false 187 | 188 | local r, g, b = icon:GetVertexColor() 189 | icon:SetVertexColor(object.iconR or r, object.iconG or g, object.iconB or b) 190 | 191 | icon.UpdateCoord = updateCoord 192 | icon:UpdateCoord() 193 | 194 | button:SetScript("OnEnter", onEnter) 195 | button:SetScript("OnLeave", onLeave) 196 | button:SetScript("OnClick", onClick) 197 | if not db or not db.lock then 198 | button:SetScript("OnDragStart", onDragStart) 199 | button:SetScript("OnDragStop", onDragStop) 200 | end 201 | button:SetScript("OnMouseDown", onMouseDown) 202 | button:SetScript("OnMouseUp", onMouseUp) 203 | 204 | lib.objects[name] = button 205 | 206 | if lib.loggedIn then 207 | updatePosition(button) 208 | if not db or not db.hide then button:Show() 209 | else button:Hide() end 210 | end 211 | lib.callbacks:Fire("LibDBIcon_IconCreated", button, name) -- Fire 'Icon Created' callback 212 | end 213 | 214 | -- We could use a metatable.__index on lib.objects, but then we'd create 215 | -- the icons when checking things like :IsRegistered, which is not necessary. 216 | local function check(name) 217 | if lib.notCreated[name] then 218 | createButton(name, lib.notCreated[name][1], lib.notCreated[name][2]) 219 | lib.notCreated[name] = nil 220 | end 221 | end 222 | 223 | lib.loggedIn = lib.loggedIn or false 224 | -- Wait a bit with the initial positioning to let any GetMinimapShape addons 225 | -- load up. 226 | if not lib.loggedIn then 227 | local f = CreateFrame("Frame") 228 | f:SetScript("OnEvent", function() 229 | for _, object in pairs(lib.objects) do 230 | updatePosition(object) 231 | if not lib.disabled and (not object.db or not object.db.hide) then object:Show() 232 | else object:Hide() end 233 | end 234 | lib.loggedIn = true 235 | f:SetScript("OnEvent", nil) 236 | f = nil 237 | end) 238 | f:RegisterEvent("PLAYER_LOGIN") 239 | end 240 | 241 | local function getDatabase(name) 242 | return lib.notCreated[name] and lib.notCreated[name][2] or lib.objects[name].db 243 | end 244 | 245 | function lib:Register(name, object, db) 246 | if not object.icon then error("Can't register LDB objects without icons set!") end 247 | if lib.objects[name] or lib.notCreated[name] then error("Already registered, nubcake.") end 248 | if not lib.disabled and (not db or not db.hide) then 249 | createButton(name, object, db) 250 | else 251 | lib.notCreated[name] = {object, db} 252 | end 253 | end 254 | 255 | function lib:Lock(name) 256 | if not lib:IsRegistered(name) then return end 257 | if lib.objects[name] then 258 | lib.objects[name]:SetScript("OnDragStart", nil) 259 | lib.objects[name]:SetScript("OnDragStop", nil) 260 | end 261 | local db = getDatabase(name) 262 | if db then db.lock = true end 263 | end 264 | 265 | function lib:Unlock(name) 266 | if not lib:IsRegistered(name) then return end 267 | if lib.objects[name] then 268 | lib.objects[name]:SetScript("OnDragStart", onDragStart) 269 | lib.objects[name]:SetScript("OnDragStop", onDragStop) 270 | end 271 | local db = getDatabase(name) 272 | if db then db.lock = nil end 273 | end 274 | 275 | function lib:Hide(name) 276 | if not lib.objects[name] then return end 277 | lib.objects[name]:Hide() 278 | end 279 | function lib:Show(name) 280 | if lib.disabled then return end 281 | check(name) 282 | lib.objects[name]:Show() 283 | updatePosition(lib.objects[name]) 284 | end 285 | function lib:IsRegistered(name) 286 | return (lib.objects[name] or lib.notCreated[name]) and true or false 287 | end 288 | function lib:Refresh(name, db) 289 | if lib.disabled then return end 290 | check(name) 291 | local button = lib.objects[name] 292 | if db then button.db = db end 293 | updatePosition(button) 294 | if not button.db or not button.db.hide then 295 | button:Show() 296 | else 297 | button:Hide() 298 | end 299 | if not button.db or not button.db.lock then 300 | button:SetScript("OnDragStart", onDragStart) 301 | button:SetScript("OnDragStop", onDragStop) 302 | else 303 | button:SetScript("OnDragStart", nil) 304 | button:SetScript("OnDragStop", nil) 305 | end 306 | end 307 | function lib:GetMinimapButton(name) 308 | return lib.objects[name] 309 | end 310 | 311 | function lib:EnableLibrary() 312 | lib.disabled = nil 313 | for name, object in pairs(lib.objects) do 314 | if not object.db or not object.db.hide then 315 | object:Show() 316 | updatePosition(object) 317 | end 318 | end 319 | for name, data in pairs(lib.notCreated) do 320 | if not data.db or not data.db.hide then 321 | createButton(name, data[1], data[2]) 322 | lib.notCreated[name] = nil 323 | end 324 | end 325 | end 326 | 327 | function lib:DisableLibrary() 328 | lib.disabled = true 329 | for name, object in pairs(lib.objects) do 330 | object:Hide() 331 | end 332 | end 333 | 334 | -------------------------------------------------------------------------------- /Hekili.lua: -------------------------------------------------------------------------------- 1 | -- Hekili.lua 2 | -- April 2014 3 | 4 | local addon, ns = ... 5 | local GetAddOnMetadata = GetAddOnMetadata or C_AddOns.GetAddOnMetadata 6 | Hekili = LibStub("AceAddon-3.0"):NewAddon( "Hekili", "AceConsole-3.0", "AceSerializer-3.0" ) 7 | Hekili.Version = GetAddOnMetadata( "Hekili", "Version" ) 8 | Hekili.Flavor = GetAddOnMetadata( "Hekili", "X-Flavor" ) or "Retail" 9 | 10 | local format = string.format 11 | local insert, concat = table.insert, table.concat 12 | 13 | if Hekili.Version == ( "@" .. "project-version" .. "@" ) then 14 | Hekili.Version = format( "Dev-%s (%s)", GetBuildInfo(), date( "%Y%m%d" ) ) 15 | end 16 | 17 | Hekili.AllowSimCImports = true 18 | 19 | Hekili.IsRetail = function() 20 | return Hekili.Flavor == "Retail" 21 | end 22 | Hekili.IsWrath = function() 23 | return Hekili.Flavor == "Wrath" 24 | end 25 | Hekili.IsClassic = function() 26 | return Hekili.Flavor == "Classic" 27 | end 28 | Hekili.IsDragonflight = function() 29 | return select( 4, GetBuildInfo() ) >= 100000 30 | end 31 | 32 | ns.PTR = false 33 | 34 | 35 | ns.Patrons = "Abom, Abra, Abuna, Aern, Aggronaught, akh270, Alasha, alcaras, Amera, ApexPlatypus, aphoenix, Archxlock, Aristocles, aro725, Artoo, Ash, av8ordoc, Battle Hermit VIA, Belatar, Borelia, Brangeddon, Bsirk/Kris, Cele, Chimmi, Coan, Cortland, Daz, DB, Der Baron, Dez, Drako, Enemy, Eryx, fuon, Garumako, Graemec, Grayscale, guhbjs, Hambrick, Hexel, Himea, Hollaputt, Hungrypilot, Ifor, Ingrathis, intheyear, Jacii, jawj, Jenkz, Katurn, Kingreboot, Kittykiller, Lagertha, Leorus, Loraniden, Lord Corn, Lovien, Manni, Mirando, mr. jing0, Mr_Hunter, MrBean73, mrminus, Muffin, Mumrikk, Nelix, neurolawl, Nighteyez, nomiss, nqrse, Orcodamus, Parameshvar, Rage, Ramen, Ramirez (Jon), Rebdull, Ridikulus0510, rockschtar, Roodie, Rusah, Samuraiwillz501, sarrge, Sarthol, Scerick, Sebstar, Seniroth, seriallos, Shakeykev, Shuck, Skeletor, Slem, Spaten, Spy, Srata, Stevi, Strozzy, Tekfire, Tevka, Theda99, Thordros, Tic[à]sentence, Tobi, todd, Torsti, tsukari, Tyazrael, Ulti.DTY, Val (Valdrath), Vaxum, Vsmit, Wargus (Shagus), Weedwalker, WhoaIsJustin, Wonder, zab, Zarggg, and zarrin-zuljin" 36 | 37 | 38 | do 39 | local cpuProfileDB = {} 40 | 41 | function Hekili:ProfileCPU( name, func ) 42 | cpuProfileDB[ name ] = func 43 | end 44 | 45 | ns.cpuProfile = cpuProfileDB 46 | 47 | 48 | local frameProfileDB = {} 49 | 50 | function Hekili:ProfileFrame( name, f ) 51 | frameProfileDB[ name ] = f 52 | end 53 | 54 | ns.frameProfile = frameProfileDB 55 | end 56 | 57 | 58 | ns.lib = { 59 | Format = {} 60 | } 61 | 62 | 63 | -- 04072017: Let's go ahead and cache aura information to reduce overhead. 64 | ns.auras = { 65 | target = { 66 | buff = {}, 67 | debuff = {} 68 | }, 69 | player = { 70 | buff = {}, 71 | debuff = {} 72 | } 73 | } 74 | 75 | Hekili.Class = { 76 | specs = {}, 77 | num = 0, 78 | 79 | file = "NONE", 80 | 81 | resources = {}, 82 | resourceAuras = {}, 83 | talents = {}, 84 | pvptalents = {}, 85 | auras = {}, 86 | auraList = {}, 87 | powers = {}, 88 | glyphs = {}, 89 | gear = {}, 90 | setBonuses = {}, 91 | 92 | knownAuraAttributes = {}, 93 | 94 | stateExprs = {}, 95 | stateFuncs = {}, 96 | stateTables = {}, 97 | 98 | abilities = {}, 99 | abilityByName = {}, 100 | abilityList = {}, 101 | itemList = {}, 102 | itemMap = {}, 103 | itemPack = { 104 | lists = { 105 | items = {} 106 | } 107 | }, 108 | 109 | packs = {}, 110 | 111 | pets = {}, 112 | totems = {}, 113 | 114 | potions = {}, 115 | potionList = {}, 116 | 117 | hooks = {}, 118 | range = 8, 119 | settings = {}, 120 | stances = {}, 121 | toggles = {}, 122 | variables = {}, 123 | } 124 | 125 | Hekili.Scripts = { 126 | DB = {}, 127 | Channels = {}, 128 | PackInfo = {}, 129 | } 130 | 131 | Hekili.State = {} 132 | 133 | ns.hotkeys = {} 134 | ns.keys = {} 135 | ns.queue = {} 136 | ns.targets = {} 137 | ns.TTD = {} 138 | 139 | ns.UI = { 140 | Displays = {}, 141 | Buttons = {} 142 | } 143 | 144 | ns.debug = {} 145 | ns.snapshots = {} 146 | 147 | 148 | function Hekili:Query( ... ) 149 | local output = ns 150 | 151 | for i = 1, select( '#', ... ) do 152 | output = output[ select( i, ... ) ] 153 | end 154 | 155 | return output 156 | end 157 | 158 | 159 | function Hekili:Run( ... ) 160 | local n = select( "#", ... ) 161 | local fn = select( n, ... ) 162 | 163 | local func = ns 164 | 165 | for i = 1, fn - 1 do 166 | func = func[ select( i, ... ) ] 167 | end 168 | 169 | return func( select( fn, ... ) ) 170 | end 171 | 172 | 173 | local debug = ns.debug 174 | local active_debug 175 | local current_display 176 | 177 | local lastIndent = 0 178 | 179 | function Hekili:SetupDebug( display ) 180 | if not self.ActiveDebug then return end 181 | if not display then return end 182 | 183 | current_display = display 184 | 185 | debug[ current_display ] = debug[ current_display ] or { 186 | log = {}, 187 | index = 1 188 | } 189 | active_debug = debug[ current_display ] 190 | active_debug.index = 1 191 | 192 | lastIndent = 0 193 | 194 | local pack = self.State.system.packName 195 | 196 | if not pack then return end 197 | 198 | self:Debug( "New Recommendations for [ %s ] requested at %s ( %.2f ); using %s( %s ) priority.", display, date( "%H:%M:%S"), GetTime(), self.DB.profile.packs[ pack ].builtIn and "built-in " or "", pack ) 199 | end 200 | 201 | 202 | function Hekili:Debug( ... ) 203 | if not self.ActiveDebug then return end 204 | if not active_debug then return end 205 | 206 | local indent, text = ... 207 | local start 208 | 209 | if type( indent ) ~= "number" then 210 | indent = lastIndent 211 | text = ... 212 | start = 2 213 | else 214 | lastIndent = indent 215 | start = 3 216 | end 217 | 218 | local prepend = format( indent > 0 and ( "%" .. ( indent * 4 ) .. "s" ) or "%s", "" ) 219 | text = text:gsub("\n", "\n" .. prepend ) 220 | 221 | active_debug.log[ active_debug.index ] = format( "%" .. ( indent > 0 and ( 4 * indent ) or "" ) .. "s" .. text, "", select( start, ... ) ) 222 | active_debug.index = active_debug.index + 1 223 | end 224 | 225 | 226 | local snapshots = ns.snapshots 227 | 228 | function Hekili:SaveDebugSnapshot( dispName ) 229 | local snapped = false 230 | local formatKey = ns.formatKey 231 | local state = Hekili.State 232 | 233 | for k, v in pairs( debug ) do 234 | if not dispName or dispName == k then 235 | for i = #v.log, v.index, -1 do 236 | v.log[ i ] = nil 237 | end 238 | 239 | -- Store aura data. 240 | local auraString = "\nplayer_buffs:" 241 | local now = GetTime() 242 | 243 | local class = Hekili.Class 244 | 245 | for i = 1, 40 do 246 | local name, _, count, debuffType, duration, expirationTime, source, _, _, spellId, canApplyAura, isBossDebuff, castByPlayer = UnitBuff( "player", i ) 247 | 248 | if not name then break end 249 | 250 | local aura = class.auras[ spellId ] 251 | local key = aura and aura.key 252 | if key and not state.auras.player.buff[ key ] then key = key .. " [MISSING]" end 253 | 254 | auraString = format( "%s\n %6d - %-40s - %3d - %-6.2f", auraString, spellId, key or ( "*" .. formatKey( name ) ), count > 0 and count or 1, expirationTime > 0 and ( expirationTime - now ) or 3600 ) 255 | end 256 | 257 | auraString = auraString .. "\n\nplayer_debuffs:" 258 | 259 | for i = 1, 40 do 260 | local name, _, count, debuffType, duration, expirationTime, source, _, _, spellId, canApplyAura, isBossDebuff, castByPlayer = UnitDebuff( "player", i ) 261 | 262 | if not name then break end 263 | 264 | local aura = class.auras[ spellId ] 265 | local key = aura and aura.key 266 | if key and not state.auras.player.debuff[ key ] then key = key .. " [MISSING]" end 267 | 268 | auraString = format( "%s\n %6d - %-40s - %3d - %-6.2f", auraString, spellId, key or ( "*" .. formatKey( name ) ), count > 0 and count or 1, expirationTime > 0 and ( expirationTime - now ) or 3600 ) 269 | end 270 | 271 | 272 | if not UnitExists( "target" ) then 273 | auraString = auraString .. "\n\ntarget_auras: target does not exist" 274 | else 275 | auraString = auraString .. "\n\ntarget_buffs:" 276 | 277 | for i = 1, 40 do 278 | local name, _, count, debuffType, duration, expirationTime, source, _, _, spellId, canApplyAura, isBossDebuff, castByPlayer = UnitBuff( "target", i ) 279 | 280 | if not name then break end 281 | 282 | local aura = class.auras[ spellId ] 283 | local key = aura and aura.key 284 | if key and not state.auras.target.buff[ key ] then key = key .. " [MISSING]" end 285 | 286 | auraString = format( "%s\n %6d - %-40s - %3d - %-6.2f", auraString, spellId, key or ( "*" .. formatKey( name ) ), count > 0 and count or 1, expirationTime > 0 and ( expirationTime - now ) or 3600 ) 287 | end 288 | 289 | auraString = auraString .. "\n\ntarget_debuffs:" 290 | 291 | for i = 1, 40 do 292 | local name, _, count, debuffType, duration, expirationTime, source, _, _, spellId, canApplyAura, isBossDebuff, castByPlayer = UnitDebuff( "target", i, "PLAYER" ) 293 | 294 | if not name then break end 295 | 296 | local aura = class.auras[ spellId ] 297 | local key = aura and aura.key 298 | if key and not state.auras.target.debuff[ key ] then key = key .. " [MISSING]" end 299 | 300 | auraString = format( "%s\n %6d - %-40s - %3d - %-6.2f", auraString, spellId, key or ( "*" .. formatKey( name ) ), count > 0 and count or 1, expirationTime > 0 and ( expirationTime - now ) or 3600 ) 301 | end 302 | end 303 | 304 | auraString = auraString .. "\n\n" 305 | 306 | insert( v.log, 1, auraString ) 307 | if Hekili.TargetDebug and Hekili.TargetDebug:len() > 0 then 308 | insert( v.log, 1, "targets:\n" .. Hekili.TargetDebug ) 309 | end 310 | insert( v.log, 1, self:GenerateProfile() ) 311 | 312 | local custom = "" 313 | 314 | local pack = self.DB.profile.packs[ state.system.packName ] 315 | if not pack.builtIn then 316 | custom = format( " |cFFFFA700(Custom: %s[%d])|r", state.spec.name, state.spec.id ) 317 | end 318 | 319 | local overview = format( "%s%s; %s|r", state.system.packName, custom, dispName ) 320 | local recs = Hekili.DisplayPool[ dispName ].Recommendations 321 | 322 | for i, rec in ipairs( recs ) do 323 | if not rec.actionName then 324 | if i == 1 then 325 | overview = format( "%s - |cFF666666N/A|r", overview ) 326 | end 327 | break 328 | end 329 | overview = format( "%s%s%s|cFFFFD100(%0.2f)|r", overview, ( i == 1 and " - " or ", " ), class.abilities[ rec.actionName ].name, rec.time ) 330 | end 331 | 332 | insert( v.log, 1, overview ) 333 | 334 | local snap = { 335 | header = "|cFFFFD100[" .. date( "%H:%M:%S" ) .. "]|r " .. overview, 336 | log = concat( v.log, "\n" ), 337 | data = ns.tableCopy( v.log ), 338 | recs = {} 339 | } 340 | 341 | insert( snapshots, snap ) 342 | snapped = true 343 | end 344 | end 345 | 346 | if snapped then 347 | if Hekili.DB.profile.screenshot then Screenshot() end 348 | return true 349 | end 350 | 351 | return false 352 | end 353 | 354 | Hekili.Snapshots = ns.snapshots 355 | 356 | 357 | 358 | ns.Tooltip = CreateFrame( "GameTooltip", "HekiliTooltip", UIParent, "GameTooltipTemplate" ) 359 | Hekili:ProfileFrame( "HekiliTooltip", ns.Tooltip ) 360 | -------------------------------------------------------------------------------- /MultilineEditor.lua: -------------------------------------------------------------------------------- 1 | -- MultilineEditor.lua 2 | -- Revised MultiLineEditBox, to allow for my own tweaks. 3 | 4 | local addon, ns = ... 5 | local Hekili = _G[ addon ] 6 | 7 | local Type, Version = "HekiliCustomEditor", 4 8 | local AceGUI = LibStub and LibStub("AceGUI-3.0", true) 9 | if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end 10 | 11 | -- Lua APIs 12 | local pairs = pairs 13 | 14 | -- WoW APIs 15 | local GetCursorInfo, GetSpellInfo, ClearCursor = GetCursorInfo, GetSpellInfo, ClearCursor 16 | local CreateFrame, UIParent = CreateFrame, UIParent 17 | local _G = _G 18 | 19 | -- local utilities 20 | local multiUnpack = ns.multiUnpack 21 | local formatValue = ns.lib.formatValue 22 | local orderedPairs = ns.orderedPairs 23 | 24 | local class = Hekili.Class 25 | local scripts = Hekili.Scripts 26 | local state = Hekili.State 27 | 28 | -- Global vars/functions that we don't upvalue since they might get hooked, or upgraded 29 | -- List them here for Mikk's FindGlobals script 30 | -- GLOBALS: ACCEPT, ChatFontNormal 31 | 32 | 33 | --[[----------------------------------------------------------------------------- 34 | 35 | Support functions 36 | 37 | -------------------------------------------------------------------------------]] 38 | 39 | if not HekiliCustomEditorInsertLink then 40 | local function HekiliCustomEditorInsertLink(text) 41 | for i = 1, AceGUI:GetWidgetCount(Type) do 42 | local editbox = _G[("HekiliCustomEditor%uEdit"):format(i)] 43 | if editbox and editbox:IsVisible() and editbox:HasFocus() then 44 | editbox:Insert(text) 45 | return true 46 | end 47 | end 48 | end 49 | 50 | -- upgradeable hook 51 | hooksecurefunc("ChatEdit_InsertLink", function(...) return HekiliCustomEditorInsertLink(...) end) 52 | end 53 | 54 | 55 | local function Layout(self) 56 | self:SetHeight(self.numlines * 14 + (self.disablebutton and 19 or 41) + self.labelHeight) 57 | 58 | if self.labelHeight == 0 then 59 | self.scrollBar:SetPoint("TOP", self.frame, "TOP", 0, -23) 60 | else 61 | self.scrollBar:SetPoint("TOP", self.label, "BOTTOM", 0, -19) 62 | end 63 | 64 | if self.disablebutton then 65 | self.scrollBar:SetPoint("BOTTOM", self.frame, "BOTTOM", 0, 21) 66 | self.scrollBG:SetPoint("BOTTOMLEFT", 0, 4) 67 | else 68 | self.scrollBar:SetPoint("BOTTOM", self.button, "TOP", 0, 18) 69 | self.scrollBG:SetPoint("BOTTOMLEFT", self.button, "TOPLEFT") 70 | end 71 | end 72 | 73 | --[[----------------------------------------------------------------------------- 74 | 75 | Scripts 76 | 77 | -------------------------------------------------------------------------------]] 78 | local function OnClick(self) -- Button 79 | self = self.obj 80 | self.editBox:ClearFocus() 81 | if not self:Fire("OnEnterPressed", self.editBox:GetText()) then 82 | self.button:Disable() 83 | end 84 | end 85 | 86 | local function OnCursorChanged(self, _, y, _, cursorHeight) -- EditBox 87 | self, y = self.obj.scrollFrame, -y 88 | local offset = self:GetVerticalScroll() 89 | if y < offset then 90 | self:SetVerticalScroll(y) 91 | else 92 | y = y + cursorHeight - self:GetHeight() 93 | if y > offset then 94 | self:SetVerticalScroll(y) 95 | end 96 | end 97 | end 98 | 99 | local function OnEditFocusLost(self) -- EditBox 100 | self:HighlightText(0, 0) 101 | self.obj:Fire("OnEditFocusLost") 102 | end 103 | 104 | 105 | --Is the member Inherited from parent options 106 | local isInherited = { 107 | set = true, 108 | get = true, 109 | func = true, 110 | confirm = true, 111 | validate = true, 112 | disabled = true, 113 | hidden = true 114 | } 115 | 116 | --Does a string type mean a literal value, instead of the default of a method of the handler 117 | local stringIsLiteral = { 118 | name = true, 119 | desc = true, 120 | icon = true, 121 | usage = true, 122 | width = true, 123 | image = true, 124 | fontSize = true, 125 | } 126 | 127 | --Is Never a function or method 128 | local allIsLiteral = { 129 | type = true, 130 | descStyle = true, 131 | imageWidth = true, 132 | imageHeight = true, 133 | } 134 | 135 | 136 | --gets an option from a given group, checking plugins 137 | local function GetSubOption(group, key) 138 | if group.plugins then 139 | for plugin, t in pairs(group.plugins) do 140 | if t[key] then 141 | return t[key] 142 | end 143 | end 144 | end 145 | 146 | return group.args[key] 147 | end 148 | 149 | 150 | local function GetOptionsMemberValue(membername, option, options, path, appName, ...) 151 | --get definition for the member 152 | local inherits = isInherited[membername] 153 | 154 | --get the member of the option, traversing the tree if it can be inherited 155 | local member 156 | 157 | if inherits then 158 | local group = options 159 | if group[membername] ~= nil then 160 | member = group[membername] 161 | end 162 | for i = 1, #path do 163 | group = GetSubOption(group, path[i]) 164 | if group[membername] ~= nil then 165 | member = group[membername] 166 | end 167 | end 168 | else 169 | member = option[membername] 170 | end 171 | 172 | --check if we need to call a functon, or if we have a literal value 173 | if ( not allIsLiteral[membername] ) and ( type(member) == "function" or ((not stringIsLiteral[membername]) and type(member) == "string") ) then 174 | --We have a function to call 175 | local info = {} 176 | --traverse the options table, picking up the handler and filling the info with the path 177 | local handler 178 | local group = options 179 | handler = group.handler or handler 180 | 181 | for i = 1, #path do 182 | group = GetSubOption(group, path[i]) 183 | info[i] = path[i] 184 | handler = group.handler or handler 185 | end 186 | 187 | info.options = options 188 | info.appName = appName 189 | info[0] = appName 190 | info.arg = option.arg 191 | info.handler = handler 192 | info.option = option 193 | info.type = option.type 194 | info.uiType = "dialog" 195 | info.uiName = appName 196 | 197 | local a, b, c ,d 198 | --using 4 returns for the get of a color type, increase if a type needs more 199 | if type(member) == "function" then 200 | --Call the function 201 | a,b,c,d = member(info, ...) 202 | else 203 | --Call the method 204 | if handler and handler[member] then 205 | a,b,c,d = handler[member](handler, info, ...) 206 | else 207 | error(format("Method %s doesn't exist in handler for type %s", member, membername)) 208 | end 209 | end 210 | table.wipe(info) 211 | return a,b,c,d 212 | else 213 | --The value isnt a function to call, return it 214 | return member 215 | end 216 | end 217 | 218 | 219 | local key_cache = setmetatable( {}, { 220 | __index = function( t, k ) 221 | t[k] = k:gsub( "(%S+)%[(%d+)%]", "%1.%2" ) 222 | return t[k] 223 | end 224 | } ) 225 | 226 | 227 | local function GenerateDiagnosticTooltip( widget, event ) 228 | --show a tooltip/set the status bar to the desc text 229 | local user = widget:GetUserDataTable() 230 | local opt = user.option 231 | local options = user.options 232 | local path = user.path 233 | local appName = user.appName 234 | 235 | local name = GetOptionsMemberValue( "name", opt, options, path, appName ) 236 | local arg, listName, actID = GetOptionsMemberValue( "arg", opt, options, path, appName ) 237 | local desc = GetOptionsMemberValue( "desc", opt, options, path, appName ) 238 | local usage = GetOptionsMemberValue( "usage", opt, options, path, appName ) 239 | local descStyle = opt.descStyle 240 | 241 | if descStyle and descStyle ~= "tooltip" then return end 242 | 243 | GameTooltip:SetOwner( widget.frame, "ANCHOR_TOPRIGHT" ) 244 | GameTooltip:SetText(name, 1, .82, 0, 1) 245 | 246 | if type( arg ) == "string" then 247 | GameTooltip:AddLine(arg, 1, 1, 1, 1) 248 | end 249 | 250 | local tested = false 251 | 252 | local packName, script = path[ 2 ], path[ #path ] 253 | -- print( unpack( path ) ) 254 | 255 | local pack = rawget( Hekili.DB.profile.packs, packName ) 256 | local list = pack and pack.lists[ listName ] 257 | local entry = list and list[ actID ] 258 | 259 | if pack and list and entry then 260 | local scriptID = packName .. ":" .. listName .. ":" .. actID 261 | local action = entry.action 262 | 263 | if script == 'criteria' then 264 | local result, warning = scripts:CheckScript( scriptID, action ) 265 | 266 | GameTooltip:AddDoubleLine( "Shown", ns.formatValue( result ), 1, 1, 1, 1, 1, 1 ) 267 | 268 | if warning then GameTooltip:AddLine( warning, 1, 0, 0 ) end 269 | 270 | else 271 | local result, warning = scripts:CheckScript( scriptID, action, script ) 272 | 273 | GameTooltip:AddLine( ns.formatValue( result ), 1, 1, 1, 1 ) 274 | 275 | if warning then GameTooltip:AddLine( warning, 1, 0, 0 ) end 276 | -- handle other types. 277 | end 278 | 279 | tested = true 280 | end 281 | 282 | local has_args = arg and ( next(arg) ~= nil ) 283 | 284 | if has_args then 285 | if tested then GameTooltip:AddLine(" ") end 286 | 287 | GameTooltip:AddLine( "Values" ) 288 | for k, v in orderedPairs( arg ) do 289 | if not key_cache[k]:find( "safebool" ) and not key_cache[k]:find( "safenum" ) and not key_cache[k]:find("floor") and not key_cache[k]:find( "ceil" ) and ( type(v) ~= "string" or not v:find( "function" ) ) then 290 | GameTooltip:AddDoubleLine( key_cache[ k ], ns.formatValue( v ), 1, 1, 1, 1, 1, 1 ) 291 | end 292 | end 293 | end 294 | 295 | if type( usage ) == "string" then 296 | GameTooltip:AddLine( "Usage: "..usage, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, 1 ) 297 | end 298 | 299 | GameTooltip:Show() 300 | 301 | end 302 | 303 | 304 | 305 | local function OnEnter(self) -- EditBox / ScrollFrame 306 | self = self.obj 307 | if not self.entered then 308 | self.entered = true 309 | GenerateDiagnosticTooltip(self, "OnEnter") 310 | end 311 | end 312 | 313 | 314 | local function OnLeave(self) -- EditBox / ScrollFrame 315 | self = self.obj 316 | if self.entered then 317 | self.entered = nil 318 | GameTooltip:Hide() 319 | self:Fire("OnLeave") 320 | end 321 | end 322 | 323 | local function OnMouseUp(self) -- ScrollFrame 324 | self = self.obj.editBox 325 | self:SetFocus() 326 | self:SetCursorPosition(self:GetNumLetters()) 327 | end 328 | 329 | local function OnReceiveDrag(self) -- EditBox / ScrollFrame 330 | local type, id, info = GetCursorInfo() 331 | if type == "spell" then 332 | info = GetSpellInfo(id, info) 333 | elseif type ~= "item" then 334 | return 335 | end 336 | ClearCursor() 337 | self = self.obj 338 | local editBox = self.editBox 339 | if not editBox:HasFocus() then 340 | editBox:SetFocus() 341 | editBox:SetCursorPosition(editBox:GetNumLetters()) 342 | end 343 | editBox:Insert(info) 344 | self.button:Enable() 345 | end 346 | 347 | local function OnSizeChanged(self, width, height) -- ScrollFrame 348 | self.obj.editBox:SetWidth(width) 349 | end 350 | 351 | local function OnTextChanged(self, userInput) -- EditBox 352 | if userInput then 353 | self = self.obj 354 | self:Fire("OnTextChanged", self.editBox:GetText()) 355 | self.button:Enable() 356 | end 357 | end 358 | 359 | local function OnTextSet(self) -- EditBox 360 | self:HighlightText(0, 0) 361 | self:SetCursorPosition(self:GetNumLetters()) 362 | self:SetCursorPosition(0) 363 | if self.Coloring then 364 | self.Coloring = nil 365 | else 366 | self.obj.button:Disable() 367 | end 368 | end 369 | 370 | local function OnVerticalScroll(self, offset) -- ScrollFrame 371 | local editBox = self.obj.editBox 372 | editBox:SetHitRectInsets(0, 0, offset, editBox:GetHeight() - offset - self:GetHeight()) 373 | end 374 | 375 | local function OnShowFocus(frame) 376 | frame.obj.editBox:SetFocus() 377 | frame:SetScript("OnShow", nil) 378 | end 379 | 380 | local function OnEditFocusGained(frame) 381 | AceGUI:SetFocus(frame.obj) 382 | frame.obj:Fire("OnEditFocusGained") 383 | end 384 | 385 | --[[----------------------------------------------------------------------------- 386 | 387 | Methods 388 | 389 | -------------------------------------------------------------------------------]] 390 | local methods = { 391 | ["OnAcquire"] = function(self) 392 | self.editBox:SetText("") 393 | self:SetDisabled(false) 394 | self:SetWidth(200) 395 | self:DisableButton(false) 396 | self:SetNumLines() 397 | self.entered = nil 398 | self:SetMaxLetters(0) 399 | end, 400 | 401 | ["OnRelease"] = function(self) 402 | self:ClearFocus() 403 | end, 404 | 405 | ["SetDisabled"] = function(self, disabled) 406 | local editBox = self.editBox 407 | if disabled then 408 | editBox:ClearFocus() 409 | editBox:EnableMouse(false) 410 | editBox:SetTextColor(0.5, 0.5, 0.5) 411 | self.label:SetTextColor(0.5, 0.5, 0.5) 412 | self.scrollFrame:EnableMouse(false) 413 | self.button:Disable() 414 | else 415 | editBox:EnableMouse(true) 416 | editBox:SetTextColor(1, 1, 1) 417 | self.label:SetTextColor(1, 0.82, 0) 418 | self.scrollFrame:EnableMouse(true) 419 | end 420 | end, 421 | 422 | ["SetLabel"] = function(self, text) 423 | if text and text ~= "" then 424 | self.label:SetText(text) 425 | if self.labelHeight ~= 10 then 426 | self.labelHeight = 10 427 | self.label:Show() 428 | end 429 | elseif self.labelHeight ~= 0 then 430 | self.labelHeight = 0 431 | self.label:Hide() 432 | end 433 | Layout(self) 434 | end, 435 | 436 | ["SetNumLines"] = function(self, value) 437 | if not value or value < 4 then 438 | value = 4 439 | end 440 | self.numlines = value 441 | Layout(self) 442 | end, 443 | 444 | ["SetText"] = function(self, text) 445 | self.editBox:SetText(text) 446 | end, 447 | 448 | ["GetText"] = function(self) 449 | return self.editBox:GetText() 450 | end, 451 | 452 | ["SetMaxLetters"] = function (self, num) 453 | self.editBox:SetMaxLetters(num or 0) 454 | end, 455 | 456 | ["DisableButton"] = function(self, disabled) 457 | self.disablebutton = disabled 458 | if disabled then 459 | self.button:Hide() 460 | else 461 | self.button:Show() 462 | end 463 | Layout(self) 464 | end, 465 | 466 | ["ClearFocus"] = function(self) 467 | self.editBox:ClearFocus() 468 | self.frame:SetScript("OnShow", nil) 469 | end, 470 | 471 | ["SetFocus"] = function(self) 472 | self.editBox:SetFocus() 473 | if not self.frame:IsShown() then 474 | self.frame:SetScript("OnShow", OnShowFocus) 475 | end 476 | end, 477 | 478 | ["GetCursorPosition"] = function(self) 479 | return self.editBox:GetCursorPosition() 480 | end, 481 | 482 | ["SetCursorPosition"] = function(self, ...) 483 | return self.editBox:SetCursorPosition(...) 484 | end, 485 | 486 | 487 | } 488 | 489 | --[[----------------------------------------------------------------------------- 490 | 491 | Constructor 492 | 493 | -------------------------------------------------------------------------------]] 494 | local backdrop = { 495 | bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], 496 | edgeFile = [[Interface\Tooltips\UI-Tooltip-Border]], edgeSize = 16, 497 | insets = { left = 4, right = 3, top = 4, bottom = 3 } 498 | } 499 | 500 | local function Constructor() 501 | local frame = CreateFrame("Frame", nil, UIParent) 502 | frame:Hide() 503 | 504 | local widgetNum = AceGUI:GetNextWidgetNum(Type) 505 | 506 | local label = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") 507 | label:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, -4) 508 | label:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, -4) 509 | label:SetJustifyH("LEFT") 510 | label:SetText(ACCEPT) 511 | label:SetHeight(10) 512 | 513 | local button = CreateFrame("Button", ("%s%dButton"):format(Type, widgetNum), frame, "UIPanelButtonTemplate") 514 | button:SetPoint("BOTTOMLEFT", 0, 4) 515 | button:SetHeight(22) 516 | button:SetWidth(label:GetStringWidth() + 24) 517 | button:SetText(ACCEPT) 518 | button:SetScript("OnClick", OnClick) 519 | button:Disable() 520 | 521 | local text = button:GetFontString() 522 | text:ClearAllPoints() 523 | text:SetPoint("TOPLEFT", button, "TOPLEFT", 5, -5) 524 | text:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -5, 1) 525 | text:SetJustifyV("MIDDLE") 526 | 527 | local scrollBG = CreateFrame("Frame", nil, frame, BackdropTemplateMixin and "BackdropTemplate" or nil) 528 | scrollBG:SetBackdrop(backdrop) 529 | scrollBG:SetBackdropColor(0, 0, 0) 530 | scrollBG:SetBackdropBorderColor(0.4, 0.4, 0.4) 531 | 532 | --scrollBG:SetBackdropBorderColor(1,0,0) 533 | 534 | local scrollFrame = CreateFrame("ScrollFrame", ("%s%dScrollFrame"):format(Type, widgetNum), frame, "UIPanelScrollFrameTemplate") 535 | 536 | local scrollBar = _G[scrollFrame:GetName() .. "ScrollBar"] 537 | scrollBar:ClearAllPoints() 538 | scrollBar:SetPoint("TOP", label, "BOTTOM", 0, -19) 539 | scrollBar:SetPoint("BOTTOM", button, "TOP", 0, 18) 540 | scrollBar:SetPoint("RIGHT", frame, "RIGHT") 541 | 542 | scrollBG:SetPoint("TOPRIGHT", scrollBar, "TOPLEFT", 0, 19) 543 | scrollBG:SetPoint("BOTTOMLEFT", button, "TOPLEFT") 544 | 545 | scrollFrame:SetPoint("TOPLEFT", scrollBG, "TOPLEFT", 5, -6) 546 | scrollFrame:SetPoint("BOTTOMRIGHT", scrollBG, "BOTTOMRIGHT", -4, 4) 547 | scrollFrame:SetScript("OnEnter", OnEnter) 548 | scrollFrame:SetScript("OnLeave", OnLeave) 549 | scrollFrame:SetScript("OnMouseUp", OnMouseUp) 550 | scrollFrame:SetScript("OnReceiveDrag", OnReceiveDrag) 551 | scrollFrame:SetScript("OnSizeChanged", OnSizeChanged) 552 | scrollFrame:HookScript("OnVerticalScroll", OnVerticalScroll) 553 | 554 | local editBox = CreateFrame("EditBox", ("%s%dEdit"):format(Type, widgetNum), scrollFrame) 555 | editBox:SetAllPoints() 556 | editBox:SetFontObject(ChatFontNormal) 557 | editBox:SetMultiLine(true) 558 | editBox:EnableMouse(true) 559 | editBox:SetAutoFocus(false) 560 | editBox:SetCountInvisibleLetters(false) 561 | editBox:SetScript("OnCursorChanged", OnCursorChanged) 562 | editBox:SetScript("OnEditFocusLost", OnEditFocusLost) 563 | editBox:SetScript("OnEnter", OnEnter) 564 | editBox:SetScript("OnEscapePressed", editBox.ClearFocus) 565 | editBox:SetScript("OnLeave", OnLeave) 566 | editBox:SetScript("OnMouseDown", OnReceiveDrag) 567 | editBox:SetScript("OnReceiveDrag", OnReceiveDrag) 568 | editBox:SetScript("OnTextChanged", OnTextChanged) 569 | editBox:SetScript("OnTextSet", OnTextSet) 570 | editBox:SetScript("OnEditFocusGained", OnEditFocusGained) 571 | 572 | if ns.lib.Format then 573 | local T = ns.lib.Format.Tokens; 574 | 575 | local SyntaxColors = {}; 576 | --- Assigns a color to multiple tokens at once. 577 | local function Color ( Code, ... ) 578 | for Index = 1, select( "#", ... ) do 579 | SyntaxColors[ select( Index, ... ) ] = Code; 580 | end 581 | end 582 | Color( "|cffB266FF", T.KEYWORD ) -- Reserved words 583 | 584 | Color( "|cffffffff", T.LEFTCURLY, T.RIGHTCURLY, 585 | T.LEFTBRACKET, T.RIGHTBRACKET, 586 | T.LEFTPAREN, T.RIGHTPAREN ) 587 | 588 | Color( "|cffFF66FF", T.UNKNOWN, T.ADD, T.SUBTRACT, T.MULTIPLY, T.DIVIDE, T.POWER, T.MODULUS, 589 | T.CONCAT, T.VARARG, T.ASSIGNMENT, T.PERIOD, T.COMMA, T.SEMICOLON, T.COLON, T.SIZE, 590 | T.EQUALITY, T.NOTEQUAL, T.LT, T.LTE, T.GT, T.GTE ) 591 | 592 | Color( "|cFFB2FF66", unpack( ns.keys ) ) 593 | 594 | Color( "|cffFFFF00", T.NUMBER ) 595 | Color( "|cff888888", T.STRING, T.STRING_LONG ) 596 | Color( "|cff55cc55", T.COMMENT_SHORT, T.COMMENT_LONG ) 597 | 598 | Color( "|cff55ddcc", -- Minimal standard Lua functions 599 | "assert", "error", "ipairs", "next", "pairs", "pcall", "print", "select", 600 | "tonumber", "tostring", "type", "unpack", 601 | -- Libraries 602 | "bit", "coroutine", "math", "string", "table" ) 603 | 604 | Color( "|cffddaaff", -- Some of WoW's aliases for standard Lua functions 605 | -- math 606 | "abs", "ceil", "floor", "max", "min", 607 | -- string 608 | "format", "gsub", "strbyte", "strchar", "strconcat", "strfind", "strjoin", 609 | "strlower", "strmatch", "strrep", "strrev", "strsplit", "strsub", "strtrim", 610 | "strupper", "tostringall", 611 | -- table 612 | "sort", "tinsert", "tremove", "wipe" ) 613 | 614 | ns.lib.Format.Enable( editBox, 4, SyntaxColors, true ) 615 | end 616 | 617 | scrollFrame:SetScrollChild(editBox) 618 | 619 | local widget = { 620 | button = button, 621 | editBox = editBox, 622 | frame = frame, 623 | label = label, 624 | labelHeight = 10, 625 | numlines = 4, 626 | scrollBar = scrollBar, 627 | scrollBG = scrollBG, 628 | scrollFrame = scrollFrame, 629 | type = Type 630 | } 631 | for method, func in pairs(methods) do 632 | widget[method] = func 633 | end 634 | button.obj, editBox.obj, scrollFrame.obj = widget, widget, widget 635 | 636 | local hcv = AceGUI:RegisterAsWidget(widget) 637 | 638 | if ElvUI then 639 | local E = ElvUI[1] 640 | 641 | if E.private.skins.ace3Enable or ( E.private.skins.ace3 and E.private.skins.ace3.enable ) then -- ElvUI options changed 7/2020. 642 | local S = E:GetModule('Skins') 643 | 644 | local frame = hcv.frame 645 | 646 | if not hcv.scrollBG.template then 647 | hcv.scrollBG:SetTemplate() 648 | end 649 | 650 | S:HandleButton(hcv.button) 651 | S:HandleScrollBar(hcv.scrollBar) 652 | hcv.scrollBar:Point('RIGHT', frame, 'RIGHT', 0 -4) 653 | hcv.scrollBG:Point('TOPRIGHT', hcv.scrollBar, 'TOPLEFT', -2, 19) 654 | hcv.scrollBG:Point('BOTTOMLEFT', hcv.button, 'TOPLEFT') 655 | hcv.scrollFrame:Point('BOTTOMRIGHT', hcv.scrollBG, 'BOTTOMRIGHT', -4, 8) 656 | end 657 | end 658 | 659 | return hcv 660 | end 661 | 662 | 663 | AceGUI:RegisterWidgetType(Type, Constructor, Version) 664 | -------------------------------------------------------------------------------- /Classic/Classes.lua: -------------------------------------------------------------------------------- 1 | local addon, ns = ... 2 | local Hekili = _G[ addon ] 3 | 4 | if not Hekili.IsWrath() and not Hekili.IsClassic() then return end 5 | 6 | local class, state = Hekili.Class, Hekili.State 7 | 8 | local RegisterEvent = ns.RegisterEvent 9 | 10 | function ns.updateTalents() 11 | for _, tal in pairs( state.talent ) do 12 | tal.enabled = false 13 | tal.rank = 0 14 | end 15 | 16 | for k, v in pairs( class.talents ) do 17 | local maxRank = v[ 2 ] 18 | 19 | local talent = rawget( state.talent, k ) or {} 20 | talent.enabled = false 21 | talent.rank = 0 22 | 23 | for i = #v, 3, -1 do 24 | local spell = v[i] 25 | local ability = class.abilities[ spell ] 26 | 27 | if ability then 28 | -- This is a talent, but it could also be an ability with multiple ranks. 29 | local spellID = select( 7, GetSpellInfo( ability.name ) ) or spell 30 | if IsPlayerSpell( spellID ) then 31 | talent.enabled = true 32 | talent.rank = i - 2 33 | break 34 | end 35 | elseif IsPlayerSpell( spell ) then 36 | talent.enabled = true 37 | talent.rank = i - 2 38 | break 39 | end 40 | end 41 | 42 | state.talent[ k ] = talent 43 | end 44 | 45 | local spec = state.spec.id or select( 3, UnitClass( "player" ) ) 46 | if not Hekili.DB.profile.specs[ spec ].usePackSelector then return end 47 | 48 | -- Swap priorities if needed. 49 | local tab1 = select( 5, GetTalentTabInfo(1) ) 50 | local tab2 = select( 5, GetTalentTabInfo(2) ) 51 | local tab3 = select( 5, GetTalentTabInfo(3) ) 52 | 53 | local fromPackage = Hekili.DB.profile.specs[ spec ].package 54 | 55 | for _, selector in ipairs( class.specs[ spec ].packSelectors ) do 56 | local toPackage = Hekili.DB.profile.specs[ state.spec.id ].autoPacks[ selector.key ] or "none" 57 | 58 | if not rawget( Hekili.DB.profile.packs, toPackage ) then toPackage = "none" end 59 | 60 | if type( selector.condition ) == "function" and selector.condition( tab1, tab2, tab3 ) or 61 | type( selector.condition ) == "number" and 62 | ( selector.condition == 1 and tab1 > max( tab2, tab3 ) or 63 | selector.condition == 2 and tab2 > max( tab1, tab3 ) or 64 | selector.condition == 3 and tab3 > max( tab1, tab2 ) ) then 65 | 66 | if toPackage ~= "none" and fromPackage ~= toPackage then 67 | Hekili.DB.profile.specs[ spec ].package = toPackage 68 | C_Timer.After( Hekili.PLAYER_ENTERING_WORLD and 0 or 5, function() Hekili:Notify( toPackage .. " priority activated." ) end ) 69 | end 70 | break 71 | end 72 | end 73 | end 74 | 75 | 76 | local HekiliSpecMixin = ns.HekiliSpecMixin 77 | 78 | function HekiliSpecMixin:RegisterGlyphs( glyphs ) 79 | for id, name in pairs( glyphs ) do 80 | self.glyphs[ id ] = name 81 | end 82 | end 83 | 84 | 85 | function ns.updateGlyphs() 86 | if Hekili.IsClassic() then return end 87 | 88 | for _, glyph in pairs( state.glyph ) do 89 | glyph.rank = 0 90 | end 91 | 92 | for i = 1, 6 do 93 | local enabled, rank, spellID = GetGlyphSocketInfo( i ) 94 | 95 | if enabled and spellID then 96 | local name = class.glyphs[ spellID ] 97 | 98 | if name then 99 | local glyph = rawget( state.glyph, name ) or {} 100 | glyph.rank = rank 101 | state.glyph[ name ] = glyph 102 | end 103 | end 104 | end 105 | end 106 | 107 | RegisterEvent( "GLYPH_ADDED", ns.updateGlyphs ) 108 | RegisterEvent( "GLYPH_REMOVED", ns.updateGlyphs ) 109 | RegisterEvent( "GLYPH_UPDATED", ns.updateGlyphs ) 110 | RegisterEvent( "USE_GLYPH", ns.updateGlyphs ) 111 | RegisterEvent( "PLAYER_LEVEL_UP", ns.updateGlyphs ) 112 | RegisterEvent( "PLAYER_ENTERING_WORLD", ns.updateGlyphs ) 113 | 114 | 115 | all = class.specs[ 0 ] 116 | 117 | 118 | all:RegisterAuras({ 119 | -- Phase 4 120 | -- Death's Verdict/Choice Buffs 121 | paragon_str = { 122 | id = 67708, 123 | duration = 15, 124 | max_stack = 1, 125 | copy = {67708, 67773} 126 | }, 127 | paragon_agi = { 128 | id = 67703, 129 | duration = 15, 130 | max_stack = 1, 131 | copy = {67703, 67772} 132 | }, 133 | -- When you deal damage you have a chance to gain Paragon, increasing your Strength or Agility by 450/510 for 15 sec. Your highest stat is always chosen. 134 | paragon = { 135 | --id = 67771, 136 | alias = { "paragon_agi", "paragon_str" }, 137 | aliasMode = "latest", 138 | aliasType = "buff", 139 | }, 140 | 141 | -- DBW Buffs 142 | aim_of_the_iron_dwarves = { 143 | -- crit: DK, Hunter, Paladin 144 | id = 71491, 145 | duration = 30, 146 | copy= {71491,71559}, 147 | }, 148 | agility_of_the_vrykul = { 149 | -- agi: Druid, Hunter, Rogue, Shaman 150 | id = 71485, 151 | duration = 30, 152 | copy= {71485,71556}, 153 | }, 154 | power_of_the_taunka = { 155 | -- ap: Hunter, Rogue, Shaman 156 | id = 71486, 157 | duration = 30, 158 | copy= {71486,71558}, 159 | }, 160 | precision_of_the_iron_dwarves = { 161 | -- arp: Rogue, Shaman, Warrior 162 | id = 71487, 163 | duration = 30, 164 | copy= {71487,71557}, 165 | }, 166 | speed_of_the_vrykul = { 167 | -- haste: DK, Druid, Paladin 168 | id = 71492, 169 | duration = 30, 170 | copy= {71492,71560}, 171 | }, 172 | strength_of_the_taunka = { 173 | -- str: DK, Paladin, Warrior 174 | id = 71484, 175 | duration = 30, 176 | copy= {71484,71561}, 177 | }, 178 | -- Your attacks have a chance to awaken the powers of the races of Northrend, temporarily transforming you and increasing your combat capabilities for 30 sec. 179 | deathbringers_will = { 180 | alias = {"aim_of_the_iron_dwarves", "agility_of_the_vrykul", "power_of_the_taunka", "precision_of_the_iron_dwarves", "speed_of_the_vrykul", "strength_of_the_taunka"}, 181 | aliasMode = "latest", 182 | aliasType = "buff", 183 | }, 184 | 185 | 186 | }) 187 | 188 | all:RegisterAbilities( { 189 | -- Phase 4 190 | 191 | abracadaver = { 192 | cast = 0, 193 | cooldown = 900, 194 | gcd = "off", 195 | 196 | items = { 51887, 50966 }, 197 | item = function() 198 | if equipped[ 51887 ] then return 51887 end 199 | return 50966 200 | end, 201 | 202 | toggle = "cooldowns", 203 | }, 204 | 205 | bauble_of_true_blood = { 206 | cast = 0, 207 | cooldown = 120, 208 | gcd = "off", 209 | 210 | items = { 50726, 50354}, 211 | item = function() 212 | if equipped[ 50726 ] then return 50726 end 213 | return 50354 214 | end, 215 | }, 216 | 217 | corroded_skeleton_key = { 218 | cast = 0, 219 | cooldown = 120, 220 | gcd = "off", 221 | 222 | item = 50356, 223 | 224 | handler = function() 225 | applyBuff( "hardened_skin" ) 226 | end, 227 | 228 | auras = { 229 | hardened_skin = { 230 | id = 71586, 231 | duration = 10, 232 | max_stack = 1 233 | } 234 | } 235 | }, 236 | deathbringers_will = { 237 | cast = 0, 238 | cooldown = 105, 239 | gcd = "off", 240 | unlisted = true, 241 | 242 | items = {50362, 50363}, 243 | item = function() 244 | if equipped[ 50362 ] then return 50362 end 245 | return 50363 246 | end, 247 | 248 | handler = function() 249 | applyBuff( "deathbringers_will" ) 250 | end, 251 | 252 | aura = "deathbringers_will", 253 | 254 | }, 255 | 256 | deaths_verdict = { 257 | cast = 0, 258 | cooldown = 45, 259 | gcd = "off", 260 | unlisted = true, 261 | 262 | items = {47115, 47131}, 263 | item = function() 264 | if equipped[ 47115 ] then return 47115 end 265 | return 47131 266 | end, 267 | 268 | handler = function() 269 | if stat.strength >= stat.agility then 270 | applyBuff( "paragon_str" ) 271 | else 272 | applyBuff( "paragon_agi" ) 273 | end 274 | end, 275 | 276 | aura = "paragon", 277 | 278 | }, 279 | 280 | deaths_choice = { 281 | cast = 0, 282 | cooldown = 45, 283 | gcd = "off", 284 | unlisted = true, 285 | 286 | items = {47303, 47464}, 287 | item = function() 288 | if equipped[ 47303 ] then return 47303 end 289 | return 47464 290 | end, 291 | 292 | 293 | handler = function() 294 | if stat.strength >= stat.agility then 295 | applyBuff( "paragon_str" ) 296 | else 297 | applyBuff( "paragon_agi" ) 298 | end 299 | end, 300 | 301 | aura = "paragon", 302 | 303 | }, 304 | 305 | ephemeral_snowflake = { 306 | cast = 0, 307 | cooldown = 120, 308 | gcd = "off", 309 | 310 | item = 50260, 311 | toggle = "cooldowns", 312 | 313 | handler = function() 314 | applyBuff( "urgency" ) 315 | end, 316 | 317 | auras = { 318 | urgency = { 319 | id = 71586, 320 | duration = 20, 321 | max_stack = 1 322 | } 323 | } 324 | }, 325 | 326 | icks_rotting_thumb = { 327 | cast = 0, 328 | cooldown = 180, 329 | gcd = "off", 330 | 331 | item = 50235, 332 | toggle = "defensives", 333 | 334 | handler = function() 335 | applyBuff( "increased_fortitude" ) 336 | end, 337 | 338 | auras = { 339 | increased_fortitude = { 340 | id = 71569, 341 | duration = 15, 342 | max_stack = 1 343 | } 344 | } 345 | }, 346 | 347 | maghias_misguided_quill = { 348 | cast = 0, 349 | cooldown = 120, 350 | gcd = "off", 351 | 352 | item = 50357, 353 | toggle = "cooldowns", 354 | 355 | handler = function() 356 | applyBuff( "elusive_power" ) 357 | end, 358 | 359 | auras = { 360 | elusive_power = { 361 | id = 71579, 362 | duration = 20, 363 | max_stack = 1 364 | } 365 | } 366 | }, 367 | 368 | medallion_of_the_alliance = { 369 | cast = 0, 370 | cooldown = 120, 371 | gcd = "off", 372 | 373 | item = 51377, 374 | toggle = "defensives" 375 | }, 376 | 377 | medallion_of_the_horde = { 378 | cast = 0, 379 | cooldown = 120, 380 | gcd = "off", 381 | 382 | item = 51378, 383 | toggle = "defensives", 384 | }, 385 | 386 | nevermelting_ice_crystal = { 387 | cast = 0, 388 | cooldown = 180, 389 | gcd = "off", 390 | 391 | item = 50259, 392 | toggle = "cooldowns", 393 | 394 | handler = function() 395 | applyBuff( "deadly_precision", nil, 5 ) 396 | end, 397 | 398 | auras = { 399 | deadly_precision = { 400 | id = 71563, 401 | duration = 20, 402 | max_stack = 5 403 | } 404 | } 405 | }, 406 | 407 | sindragosas_flawless_fang = { 408 | cast = 0, 409 | cooldown = 60, 410 | gcd = "off", 411 | 412 | items = { 50364, 50361 }, 413 | item = function() 414 | if equipped[ 50364 ] then return 50364 end 415 | return 50361 416 | end, 417 | toggle = "defensives", 418 | 419 | handler = function() 420 | applyBuff( "aegis_of_dalaran" ) 421 | end, 422 | 423 | auras = { 424 | aegis_of_dalaran = { 425 | id = 71638, 426 | duration = 10, 427 | max_stack = 1, 428 | copy = 71635 429 | } 430 | } 431 | }, 432 | 433 | sliver_of_pure_ice = { 434 | cast = 0, 435 | cooldown = 120, 436 | gcd = "off", 437 | 438 | items = { 50346, 50339 }, 439 | item = function() 440 | if equipped[ 50346 ] then return 50346 end 441 | return 50339 442 | end, 443 | toggle = "cooldowns", 444 | 445 | usable = function() 446 | local restores = equipped[ 50346 ] and 1830 or 1625 447 | return mana.deficit > restores, "mana deficit should exceed " .. restores .. " before using" 448 | end, 449 | 450 | handler = function() 451 | gain( equipped[ 50346 ] and 1830 or 1625, "mana" ) 452 | end, 453 | }, 454 | 455 | -- Phase 3 456 | 457 | antediluvian_cornerstone_grimoire = { 458 | cast = 0, 459 | cooldown = 900, 460 | gcd = "off", 461 | 462 | item = 49490, 463 | toggle = "cooldowns", 464 | }, 465 | 466 | antique_cornerstone_grimoire = { 467 | cast = 0, 468 | cooldown = 900, 469 | gcd = "off", 470 | 471 | item = 49308, 472 | toggle = "cooldowns", 473 | }, 474 | 475 | battlemasters_fury = { 476 | cast = 0, 477 | cooldown = 180, 478 | gcd = "off", 479 | 480 | item = 42133, 481 | toggle = "defensives", 482 | 483 | handler = function() 484 | applyBuff( "tremendous_fortitude" ) 485 | health.max = health.max + 4608 486 | end, 487 | 488 | auras = { 489 | tremendous_fortitude = { 490 | id = 67596, 491 | duration = 15, 492 | max_stack = 1 493 | } 494 | } 495 | }, 496 | 497 | battlemasters_precision = { 498 | cast = 0, 499 | cooldown = 180, 500 | gcd = "off", 501 | 502 | item = 42134, 503 | toggle = "defensives", 504 | 505 | handler = function() 506 | applyBuff( "tremendous_fortitude" ) 507 | health.max = health.max + 4608 508 | end, 509 | }, 510 | 511 | battlemasters_rage = { 512 | cast = 0, 513 | cooldown = 180, 514 | gcd = "off", 515 | 516 | item = 42136, 517 | toggle = "defensives", 518 | 519 | handler = function() 520 | applyBuff( "tremendous_fortitude" ) 521 | health.max = health.max + 4608 522 | end, 523 | }, 524 | 525 | battlemasters_ruination = { 526 | cast = 0, 527 | cooldown = 180, 528 | gcd = "off", 529 | 530 | item = 42137, 531 | toggle = "defensives", 532 | 533 | handler = function() 534 | applyBuff( "tremendous_fortitude" ) 535 | health.max = health.max + 4608 536 | end, 537 | }, 538 | 539 | battlemasters_vivacity = { 540 | cast = 0, 541 | cooldown = 180, 542 | gcd = "off", 543 | 544 | item = 42135, 545 | toggle = "defensives", 546 | 547 | handler = function() 548 | applyBuff( "tremendous_fortitude" ) 549 | health.max = health.max + 4608 550 | end, 551 | }, 552 | 553 | binding_light = { 554 | cast = 0, 555 | cooldown = 120, 556 | gcd = "off", 557 | 558 | items = { 47947, 47728 }, 559 | item = function() 560 | if equipped[ 47947 ] then return 47947 end 561 | return 47728 562 | end, 563 | toggle = "cooldowns", 564 | 565 | handler = function() 566 | applyBuff( "escalating_power" ) 567 | end, 568 | 569 | auras = { 570 | escalating_power = { 571 | id = 47947, 572 | duration = 20, 573 | max_stack = 8, 574 | copy = 67740 575 | } 576 | } 577 | }, 578 | 579 | binding_stone = { 580 | cast = 0, 581 | cooldown = 120, 582 | gcd = "off", 583 | 584 | items = { 48019, 47880 }, 585 | item = function() 586 | if equipped[ 48019 ] then return 48019 end 587 | return 47880 588 | end, 589 | toggle = "cooldowns", 590 | 591 | handler = function() 592 | applyBuff( "escalating_power" ) 593 | end, 594 | }, 595 | 596 | bitter_balebrew_charm = { 597 | cast = 0, 598 | cooldown = 600, 599 | gcd = "off", 600 | 601 | item = 49116, 602 | toggle = "cooldowns", 603 | }, 604 | 605 | brawlers_souvenir = { 606 | cast = 0, 607 | cooldown = 120, 608 | gcd = "off", 609 | 610 | item = 49080, 611 | toggle = "defensives", 612 | 613 | handler = function() 614 | applyBuff( "drunken_evasiveness" ) 615 | end, 616 | 617 | auras = { 618 | brawlers_fortitude = { 619 | id = 68443, 620 | duration = 20, 621 | max_stack = 1 622 | } 623 | } 624 | }, 625 | 626 | bubbling_brightbrew_charm = { 627 | cast = 0, 628 | cooldown = 600, 629 | gcd = "off", 630 | 631 | item = 49118, 632 | toggle = "cooldowns", 633 | }, 634 | 635 | eitriggs_oath = { 636 | cast = 0, 637 | cooldown = 120, 638 | gcd = "off", 639 | 640 | items = { 48021, 47882 }, 641 | item = function() 642 | if equipped[ 48021 ] then return 48021 end 643 | return 47882 644 | end, 645 | toggle = "defensives", 646 | 647 | handler = function() 648 | applyBuff( "hardening_armor" ) 649 | end, 650 | 651 | auras = { 652 | hardening_armor = { 653 | id = 67742, 654 | duration = 20, 655 | max_stack = 5, 656 | copy = 67728 657 | } 658 | } 659 | }, 660 | 661 | fervor_of_the_frostborn = { 662 | cast = 0, 663 | cooldown = 120, 664 | gcd = "off", 665 | 666 | items = { 47949, 47727 }, 667 | item = function() 668 | if equipped[ 47949 ] then return 47949 end 669 | return 47727 670 | end, 671 | toggle = "defensives", 672 | 673 | handler = function() 674 | applyBuff( "hardening_armor" ) 675 | end, 676 | }, 677 | 678 | fetish_of_volatile_power = { 679 | cast = 0, 680 | cooldown = 120, 681 | gcd = "off", 682 | 683 | items = { 48018, 47879 }, 684 | item = function() 685 | if equipped[ 48018 ] then return 48018 end 686 | return 47879 687 | end, 688 | toggle = "cooldowns", 689 | 690 | handler = function() 691 | applyBuff( "volatile_power" ) 692 | end, 693 | 694 | auras = { 695 | volatile_power = { 696 | id = 67744, 697 | duration = 20, 698 | max_stack = 8, 699 | copy = 67736 700 | } 701 | } 702 | }, 703 | 704 | glyph_of_indomitability = { 705 | cast = 0, 706 | cooldown = 120, 707 | gcd = "off", 708 | 709 | item = 47735, 710 | toggle = "defensives", 711 | 712 | handler = function() 713 | applyBuff( "defensive_tactics" ) 714 | end, 715 | 716 | auras = { 717 | defensive_tactics = { 718 | id = 67694, 719 | duration = 20, 720 | max_stack = 1 721 | } 722 | } 723 | }, 724 | 725 | juggernauts_vitality = { 726 | cast = 0, 727 | cooldown = 180, 728 | gcd = "off", 729 | 730 | items = { 47451, 47290 }, 731 | item = function() 732 | if equipped[ 47451 ] then return 47451 end 733 | return 47290 734 | end, 735 | toggle = "defensives", 736 | 737 | handler = function() 738 | applyBuff( "fortitude" ) 739 | end, 740 | }, 741 | 742 | mark_of_supremacy = { 743 | cast = 0, 744 | cooldown = 120, 745 | gcd = "off", 746 | 747 | item = 47734, 748 | toggle = "cooldowns", 749 | 750 | handler = function() 751 | applyBuff( "rage" ) 752 | end, 753 | 754 | auras = { 755 | rage = { 756 | id = 67695, 757 | duration = 20, 758 | max_stack = 1 759 | } 760 | } 761 | }, 762 | 763 | satrinas_impeding_scarab = { 764 | cast = 0, 765 | cooldown = 180, 766 | gcd = "off", 767 | 768 | items = { 47088, 47080 }, 769 | item = function() 770 | if equipped[ 47088 ] then return 47088 end 771 | return 47080 772 | end, 773 | toggle = "defensives", 774 | 775 | handler = function() 776 | applyBuff( "fortitude" ) 777 | end, 778 | }, 779 | 780 | shard_of_the_crystal_heart = { 781 | cast = 0, 782 | cooldown = 120, 783 | gcd = "off", 784 | 785 | item = 48772, 786 | toggle = "cooldowns", 787 | 788 | handler = function() 789 | applyBuff( "celerity" ) 790 | end, 791 | 792 | auras = { 793 | celerity = { 794 | id = 67683, 795 | duration = 20, 796 | max_stack = 1 797 | } 798 | } 799 | }, 800 | 801 | talisman_of_resurgence = { 802 | cast = 0, 803 | cooldown = 120, 804 | gcd = "off", 805 | 806 | item = 48779, 807 | toggle = "cooldowns", 808 | 809 | handler = function() 810 | applyBuff( "hospitality" ) 811 | end, 812 | 813 | auras = { 814 | hospitality = { 815 | id = 67684, 816 | duration = 20, 817 | max_stack = 1 818 | } 819 | } 820 | }, 821 | 822 | talisman_of_volatile_power = { 823 | cast = 0, 824 | cooldown = 120, 825 | gcd = "off", 826 | 827 | items = { 47946, 47726 }, 828 | item = function() 829 | if equipped[ 47946 ] then return 47946 end 830 | return 47726 831 | end, 832 | toggle = "cooldowns", 833 | 834 | handler = function() 835 | applyBuff( "volatile_power" ) 836 | end, 837 | }, 838 | 839 | vengeance_of_the_forsaken = { 840 | cast = 0, 841 | cooldown = 120, 842 | gcd = "off", 843 | 844 | items = { 48020, 47881 }, 845 | item = function() 846 | if equipped[ 48020 ] then return 48020 end 847 | return 47881 848 | end, 849 | toggle = "cooldowns", 850 | 851 | handler = function() 852 | applyBuff( "rising_fury" ) 853 | end, 854 | 855 | auras = { 856 | rising_fury = { 857 | id = 67747, 858 | duration = 20, 859 | max_stack = 5, 860 | copy = 67738 861 | } 862 | } 863 | }, 864 | 865 | victors_call = { 866 | cast = 0, 867 | cooldown = 120, 868 | gcd = "off", 869 | 870 | items = { 47948, 47725 }, 871 | item = function() 872 | if equipped[ 47948 ] then return 47948 end 873 | return 47725 874 | end, 875 | toggle = "cooldowns", 876 | 877 | handler = function() 878 | applyBuff( "rising_fury" ) 879 | end, 880 | }, 881 | 882 | -- Phase 2 883 | 884 | } ) -------------------------------------------------------------------------------- /Utils.lua: -------------------------------------------------------------------------------- 1 | -- Utils.lua 2 | -- June 2014 3 | 4 | local addon, ns = ... 5 | local Hekili = _G[ addon ] 6 | 7 | local format, gsub, lower = string.format, string.gsub, string.lower 8 | local insert, remove = table.insert, table.remove 9 | 10 | local class = Hekili.Class 11 | local state = Hekili.State 12 | 13 | local errors = {} 14 | local eIndex = {} 15 | 16 | ns.Error = function( output, ... ) 17 | if ... then 18 | output = format( output, ... ) 19 | end 20 | 21 | if not errors[ output ] then 22 | errors[ output ] = { 23 | n = 1, 24 | last = date( "%X", time() ) 25 | } 26 | eIndex[ #eIndex + 1 ] = output 27 | -- if Hekili.DB.profile.Verbose then Hekili:Print( output ) end 28 | else 29 | errors[ output ].n = errors[ output ].n + 1 30 | errors[ output ].last = date( "%X", time() ) 31 | end 32 | end 33 | 34 | 35 | function Hekili:Error( ... ) 36 | ns.Error( ... ) 37 | end 38 | 39 | Hekili.ErrorKeys = eIndex 40 | Hekili.ErrorDB = errors 41 | 42 | 43 | function Hekili:GetErrors() 44 | 45 | for i = 1, #eIndex do 46 | Hekili:Print( eIndex[i] .. " (n = " .. errors[ eIndex[i] ].n .. "), last at " .. errors[ eIndex[i] ].last .. "." ) 47 | end 48 | 49 | end 50 | 51 | 52 | function ns.SpaceOut( str ) 53 | str = str:gsub( "([!<>=|&()*%-%+/][?]?)", " %1 " ):gsub("%s+", " ") 54 | str = str:gsub( "([^%%])([%%]+)([^%%])", "%1 %2 %3" ) 55 | str = str:gsub( "%.%s+%(", ".(" ) 56 | str = str:gsub( "%)%s+%.", ")." ) 57 | 58 | str = str:gsub( "([<>~!|]) ([|=])", "%1%2" ) 59 | str = str:trim() 60 | return str 61 | end 62 | 63 | 64 | local LT = LibStub( "LibTranslit-1.0" ) 65 | 66 | -- Converts `s' to a SimC-like key: strip non alphanumeric characters, replace spaces with _, convert to lower case. 67 | function ns.formatKey( s ) 68 | s = LT:Transliterate( s ) 69 | return ( lower( s or '' ):gsub( "[^a-z0-9_ ]", "" ):gsub( "%s", "_" ) ) 70 | end 71 | 72 | 73 | ns.titleCase = function( s ) 74 | local helper = function( first, rest ) 75 | return first:upper()..rest:lower() 76 | end 77 | 78 | return s:gsub( "_", " " ):gsub( "(%a)([%w_']*)", helper ):gsub( "[Aa]oe", "AOE" ):gsub( "[Rr]jw", "RJW" ):gsub( "[Cc]hix", "ChiX" ):gsub( "(%W?)[Ss]t(%W?)", "%1ST%2" ) 79 | end 80 | 81 | 82 | local replacements = { 83 | ['_'] = " ", 84 | aoe = "AOE", 85 | rjw = "RJW", 86 | chix = "ChiX", 87 | st = "ST", 88 | cd = "CD", 89 | cds = "CDs" 90 | } 91 | 92 | ns.titlefy = function( s ) 93 | for k, v in pairs( replacements ) do 94 | s = s:gsub( '%f[%w]' .. k .. '%f[%W]', v ):gsub( "_", " " ) 95 | end 96 | 97 | return s 98 | end 99 | 100 | 101 | ns.fsub = function( s, pattern, repl ) 102 | return s:gsub( "%f[%w]" .. s .. "%f[%W]", repl ) 103 | end 104 | 105 | 106 | ns.escapeMagic = function( s ) 107 | return s:gsub( "([%(%)%.%%%+%-%*%?%[%^%$])", "%%%1" ) 108 | end 109 | 110 | 111 | local tblUnpack = {} 112 | 113 | ns.multiUnpack = function( ... ) 114 | 115 | table.wipe( tblUnpack ) 116 | 117 | for i = 1, select( '#', ... ) do 118 | for _, value in ipairs( select( i, ... ) ) do 119 | tblUnpack[ #tblUnpack + 1 ] = value 120 | end 121 | end 122 | 123 | return unpack( tblUnpack ) 124 | 125 | end 126 | 127 | 128 | ns.round = function( num, places ) 129 | 130 | return tonumber( format( "%." .. ( places or 0 ) .. "f", num ) ) 131 | 132 | end 133 | 134 | 135 | function ns.roundUp( num, places ) 136 | num = num or 0 137 | local tens = 10 ^ ( places or 0 ) 138 | 139 | return ceil( num * tens ) / tens 140 | end 141 | 142 | 143 | function ns.roundDown( num, places ) 144 | num = num or 0 145 | local tens = 10 ^ ( places or 0 ) 146 | 147 | return floor( num * tens ) / tens 148 | end 149 | 150 | 151 | -- Deep Copy 152 | -- from http://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value 153 | local function tableCopy( obj, seen ) 154 | if type(obj) ~= 'table' then return obj end 155 | if seen and seen[obj] then return seen[obj] end 156 | local s = seen or {} 157 | local res = setmetatable({}, getmetatable(obj)) 158 | s[obj] = res 159 | for k, v in pairs(obj) do res[ tableCopy(k, s) ] = tableCopy(v, s) end 160 | return res 161 | end 162 | ns.tableCopy = tableCopy 163 | 164 | 165 | local toc = {} 166 | local exclusions = { min = true, max = true, _G = true } 167 | 168 | ns.commitKey = function( key ) 169 | if not toc[ key ] and not exclusions[ key ] then 170 | ns.keys[ #ns.keys + 1 ] = key 171 | toc[ key ] = 1 172 | end 173 | end 174 | 175 | 176 | local orderedIndex = {} 177 | 178 | local sortHelper = function( a, b ) 179 | local a1, b1 = tostring(a), tostring(b) 180 | 181 | return a1 < b1 182 | end 183 | 184 | 185 | local function __genOrderedIndex( t ) 186 | 187 | for i = #orderedIndex, 1, -1 do 188 | orderedIndex[i] = nil 189 | end 190 | 191 | for key in pairs( t ) do 192 | table.insert( orderedIndex, key ) 193 | end 194 | table.sort( orderedIndex, sortHelper ) 195 | return orderedIndex 196 | end 197 | 198 | 199 | local function orderedNext( t, state ) 200 | local key = nil 201 | 202 | if state == nil then 203 | t.__orderedIndex = __genOrderedIndex( t ) 204 | key = t.__orderedIndex[ 1 ] 205 | else 206 | for i = 1, table.getn( t.__orderedIndex ) do 207 | if t.__orderedIndex[ i ] == state then 208 | key = t.__orderedIndex[ i+1 ] 209 | end 210 | end 211 | end 212 | 213 | if key then 214 | return key, t[ key ] 215 | end 216 | 217 | t.__orderedIndex = nil 218 | return 219 | end 220 | 221 | 222 | function ns.orderedPairs( t ) 223 | return orderedNext, t, nil 224 | end 225 | 226 | 227 | function ns.safeMin( ... ) 228 | local result 229 | 230 | for i = 1, select( "#", ... ) do 231 | local val = select( i, ... ) 232 | if val then result = ( not result or val < result ) and val or result end 233 | end 234 | 235 | return result or 0 236 | end 237 | 238 | 239 | function ns.safeMax( ... ) 240 | local result 241 | 242 | for i = 1, select( "#", ... ) do 243 | local val = select( i, ... ) 244 | if val and type(val) == 'number' then result = ( not result or val > result ) and val or result end 245 | end 246 | 247 | return result or 0 248 | end 249 | 250 | 251 | function ns.safeAbs( val ) 252 | val = tonumber( val ) 253 | if val < 0 then return -val end 254 | return val 255 | end 256 | 257 | 258 | -- Rivers' iterator for group members. 259 | function ns.GroupMembers( reversed, forceParty ) 260 | local unit = ( not forceParty and IsInRaid() ) and 'raid' or 'party' 261 | local numGroupMembers = forceParty and GetNumSubgroupMembers() or GetNumGroupMembers() 262 | local i = reversed and numGroupMembers or ( unit == 'party' and 0 or 1 ) 263 | 264 | return function() 265 | local ret 266 | 267 | if i == 0 and unit == 'party' then 268 | ret = 'player' 269 | elseif i <= numGroupMembers and i > 0 then 270 | ret = unit .. i 271 | end 272 | 273 | i = i + ( reversed and -1 or 1 ) 274 | return ret 275 | end 276 | end 277 | 278 | 279 | -- Use C_Timer.After but allow for function args. 280 | function Hekili:After( time, func, ... ) 281 | local args = { ... } 282 | local function delayfunc() 283 | func( unpack( args ) ) 284 | end 285 | 286 | C_Timer.After( time, delayfunc ) 287 | end 288 | 289 | function ns.FindRaidBuffByID(id) 290 | 291 | local unitName 292 | local buffCounter = 0 293 | local buffIterator = 1 294 | 295 | local name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 296 | 297 | if IsInRaid() or IsInGroup() then 298 | if IsInRaid() then 299 | unitName = "raid" 300 | for numGroupMembers=1, GetNumGroupMembers() do 301 | buffIterator = 1 302 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( unitName..numGroupMembers, buffIterator ) 303 | while( spellID ) do 304 | if spellID == id then buffCounter = buffCounter + 1 break end 305 | buffIterator = buffIterator + 1 306 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( unitName..numGroupMembers, buffIterator ) 307 | end 308 | end 309 | elseif IsInGroup() then 310 | unitName = "party" 311 | for numGroupMembers=1, GetNumGroupMembers() do 312 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( unitName..numGroupMembers, buffIterator ) 313 | while( spellID ) do 314 | if spellID == id then buffCounter = buffCounter + 1 break end 315 | buffIterator = buffIterator + 1 316 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( unitName..numGroupMembers, buffIterator ) 317 | end 318 | end 319 | buffIterator = 1 320 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( "player", buffIterator ) 321 | while( spellID ) do 322 | if spellID == id then buffCounter = buffCounter + 1 break end 323 | buffIterator = buffIterator + 1 324 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( "player", buffIterator ) 325 | end 326 | 327 | else 328 | unitName = "player" 329 | end 330 | 331 | end 332 | 333 | return buffCounter 334 | end 335 | 336 | function ns.FindLowHpPlayerWithoutBuffByID(id) 337 | 338 | local unitName 339 | local playerWithoutBuff = 0 340 | local buffFound = false 341 | local buffIterator = 1 342 | local name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 343 | 344 | if IsInRaid() or IsInGroup() then 345 | if IsInRaid() then 346 | unitName = "raid" 347 | for numGroupMembers=1, GetNumGroupMembers() do 348 | buffFound = false 349 | buffIterator = 1 350 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( unitName..numGroupMembers, buffIterator ) 351 | while( name ) do 352 | if spellID == id then buffFound = true break end 353 | buffIterator = buffIterator + 1 354 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( unitName..numGroupMembers, buffIterator ) 355 | end 356 | 357 | if not buffFound then 358 | local player = unitName..numGroupMembers 359 | local Health = (UnitHealth(player))/1000 360 | local HealthMax = (UnitHealthMax(player))/1000 361 | local HealthPercent = (UnitHealth(player)/UnitHealthMax(player))*100 362 | 363 | if HealthPercent <= 80 and UnitName(player) then 364 | playerWithoutBuff = playerWithoutBuff + 1 365 | end 366 | end 367 | end 368 | elseif IsInGroup() then 369 | unitName = "party" 370 | for numGroupMembers=1, GetNumGroupMembers() do 371 | buffFound = false 372 | buffIterator = 1 373 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( unitName..numGroupMembers, buffIterator ) 374 | while( name ) do 375 | if spellID == id then buffFound = true break end 376 | buffIterator = buffIterator + 1 377 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( unitName..numGroupMembers, buffIterator ) 378 | end 379 | 380 | if not buffFound then 381 | local player = unitName..numGroupMembers 382 | local Health = (UnitHealth(player))/1000 383 | local HealthMax = (UnitHealthMax(player))/1000 384 | local HealthPercent = (UnitHealth(player)/UnitHealthMax(player))*100 385 | 386 | if HealthPercent <= 80 and UnitName(player) then 387 | playerWithoutBuff = playerWithoutBuff + 1 388 | end 389 | end 390 | end 391 | 392 | buffFound = false 393 | buffIterator = 1 394 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( "player", buffIterator ) 395 | while( name ) do 396 | if spellID == id then buffFound = true break end 397 | buffIterator = buffIterator + 1 398 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( "player", buffIterator ) 399 | end 400 | 401 | if not buffFound then 402 | local player = "player" 403 | local Health = (UnitHealth(player))/1000 404 | local HealthMax = (UnitHealthMax(player))/1000 405 | local HealthPercent = (UnitHealth(player)/UnitHealthMax(player))*100 406 | 407 | if HealthPercent <= 80 then 408 | playerWithoutBuff = playerWithoutBuff + 1 409 | end 410 | end 411 | else 412 | unitName = "player" 413 | end 414 | 415 | end 416 | 417 | return playerWithoutBuff 418 | end 419 | 420 | function ns.FindRaidBuffLowestRemainsByID(id) 421 | 422 | local buffRemainsOld 423 | local buffRemainsNew 424 | local buffRemainsReturn 425 | local unitName = "player" 426 | 427 | local buffIterator = 1 428 | local name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 429 | 430 | if IsInRaid() or IsInGroup() then 431 | if IsInRaid() then 432 | unitName = "raid" 433 | for numGroupMembers=1, GetNumGroupMembers() do 434 | buffIterator = 1 435 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( unitName..numGroupMembers, buffIterator ) 436 | while( name ) do 437 | if spellID == id then 438 | 439 | if buffRemainsOld == nil then 440 | buffRemainsOld = expirationTime - GetTime() 441 | end 442 | 443 | local buffRemainsNew = expirationTime - GetTime() 444 | 445 | if buffRemainsNew < buffRemainsOld then 446 | buffRemainsReturn = buffRemainsNew 447 | else 448 | buffRemainsReturn = buffRemainsOld 449 | end 450 | 451 | break 452 | end 453 | buffIterator = buffIterator + 1 454 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( unitName..numGroupMembers, buffIterator ) 455 | end 456 | end 457 | elseif IsInGroup() then 458 | unitName = "party" 459 | for numGroupMembers=1, GetNumGroupMembers() do 460 | buffIterator = 1 461 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( unitName..numGroupMembers, buffIterator ) 462 | while( name ) do 463 | if spellID == id then 464 | 465 | if buffRemainsOld == nil then 466 | buffRemainsOld = expirationTime - GetTime() 467 | end 468 | 469 | local buffRemainsNew = expirationTime - GetTime() 470 | 471 | if buffRemainsNew < buffRemainsOld then 472 | buffRemainsReturn = buffRemainsNew 473 | else 474 | buffRemainsReturn = buffRemainsOld 475 | end 476 | 477 | break 478 | end 479 | buffIterator = buffIterator + 1 480 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( unitName..numGroupMembers, buffIterator ) 481 | end 482 | end 483 | 484 | buffIterator = 1 485 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( "player", buffIterator ) 486 | while( name ) do 487 | if spellID == id then 488 | 489 | if buffRemainsOld == nil then 490 | buffRemainsOld = expirationTime - GetTime() 491 | end 492 | 493 | local buffRemainsNew = expirationTime - GetTime() 494 | 495 | if buffRemainsNew < buffRemainsOld then 496 | buffRemainsReturn = buffRemainsNew 497 | else 498 | buffRemainsReturn = buffRemainsOld 499 | end 500 | 501 | break 502 | end 503 | buffIterator = buffIterator + 1 504 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( "player", buffIterator ) 505 | end 506 | end 507 | end 508 | 509 | return buffRemainsReturn == nil and 0 or buffRemainsReturn 510 | end 511 | 512 | -- Duplicate spell info lookup. 513 | function ns.FindUnitBuffByID( unit, id, filter ) 514 | local playerOrPet = false 515 | 516 | if filter == "PLAYER|PET" then 517 | playerOrPet = true 518 | filter = nil 519 | end 520 | 521 | local i = 1 522 | local name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( unit, i, filter ) 523 | 524 | if type( id ) == "table" then 525 | while( name ) do 526 | if id[ spellID ] and ( not playerOrPet or UnitIsUnit( caster, "player" ) or UnitIsUnit( caster, "pet" ) ) then break end 527 | i = i + 1 528 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( unit, i, filter ) 529 | end 530 | else 531 | while( name ) do 532 | if spellID == id and ( not playerOrPet or UnitIsUnit( caster, "player" ) or UnitIsUnit( caster, "pet" ) ) then break end 533 | i = i + 1 534 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff( unit, i, filter ) 535 | end 536 | end 537 | 538 | return name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 539 | end 540 | 541 | 542 | function ns.FindUnitDebuffByID( unit, id, filter ) 543 | local playerOrPet = false 544 | 545 | if filter == "PLAYER|PET" then 546 | playerOrPet = true 547 | filter = nil 548 | end 549 | 550 | local i = 1 551 | local name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitDebuff( unit, i, filter ) 552 | 553 | if type( id ) == "table" then 554 | while( name ) do 555 | if id[ spellID ] and ( not playerOrPet or UnitIsUnit( caster, "player" ) or UnitIsUnit( caster, "pet" ) ) then break end 556 | i = i + 1 557 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitDebuff( unit, i, filter ) 558 | end 559 | else 560 | while( name ) do 561 | if spellID == id and ( not playerOrPet or UnitIsUnit( caster, "player" ) or UnitIsUnit( caster, "pet" ) ) then break end 562 | i = i + 1 563 | name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitDebuff( unit, i, filter ) 564 | end 565 | end 566 | 567 | return name, icon, count, debuffType, duration, expirationTime, caster, stealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 568 | end 569 | 570 | 571 | function ns.IsActiveSpell( id ) 572 | local slot = FindSpellBookSlotBySpellID( id ) 573 | if not slot then return false end 574 | 575 | local _, _, spellID = GetSpellBookItemName( slot, "spell" ) 576 | return id == spellID 577 | end 578 | 579 | 580 | function Hekili:GetSpellLinkWithTexture( id, size, color ) 581 | if not id then return "" end 582 | 583 | local name, _, icon = GetSpellInfo( id ) 584 | 585 | if name and icon then 586 | if type( color ) == "boolean" then 587 | color = color and "ff00ff00" or "ffff0000" 588 | end 589 | 590 | if color == nil then color = "ff71d5ff" end 591 | 592 | return "|W|T" .. icon .. ":" .. ( size or 0 ) .. ":" .. ( size or "" ) .. ":::64:64:4:60:4:60|t " .. ( color and ( "|c" .. color ) or "" ) .. name .. ( color and "|r" or "" ) .. "|w" 593 | end 594 | 595 | return tostring( id ) 596 | end 597 | 598 | 599 | do 600 | local itemCache = {} 601 | 602 | function ns.CachedGetItemInfo( id ) 603 | if itemCache[ id ] then 604 | return unpack( itemCache[ id ] ) 605 | end 606 | 607 | local item = { GetItemInfo( id ) } 608 | if item[ 1 ] then 609 | itemCache[ id ] = item 610 | return unpack( item ) 611 | end 612 | end 613 | end 614 | 615 | 616 | -- Atlas -> Texture Stuff 617 | do 618 | local db = {} 619 | 620 | local function AddTexString( name, file, width, height, left, right, top, bottom ) 621 | local pctWidth = right - left 622 | local realWidth = width / pctWidth 623 | local lPoint = left * realWidth 624 | 625 | local pctHeight = bottom - top 626 | local realHeight = height / pctHeight 627 | local tPoint = top * realHeight 628 | 629 | db[ name ] = format( "|T%s:%%d:%%d:%%d:%%d:%d:%d:%d:%d:%d:%d:%%s|t", file, realWidth, realHeight, lPoint, lPoint + width, tPoint, tPoint + height ) 630 | end 631 | 632 | local function GetTexString( name, width, height, x, y, r, g, b ) 633 | return db[ name ] and format( db[ name ], width or 0, height or 0, x or 0, y or 0, ( r and g and b and ( r .. ":" .. g .. ":" .. b ) or "" ) ) or "" 634 | end 635 | 636 | local function AtlasToString( atlas, width, height, x, y, r, g, b ) 637 | if db[ atlas ] then 638 | return GetTexString( atlas, width, height, x, y, r, g, b ) 639 | end 640 | 641 | local a = C_Texture.GetAtlasInfo( atlas ) 642 | if not a then return atlas end 643 | 644 | AddTexString( atlas, a.file, a.width, a.height, a.leftTexCoord, a.rightTexCoord, a.topTexCoord, a.bottomTexCoord ) 645 | return GetTexString( atlas, width, height, x, y, r, g, b ) 646 | end 647 | 648 | local function GetAtlasFile( atlas ) 649 | local a = C_Texture.GetAtlasInfo( atlas ) 650 | return a and a.file or atlas 651 | end 652 | 653 | local function GetAtlasCoords( atlas ) 654 | local a = C_Texture.GetAtlasInfo( atlas ) 655 | return a and { a.leftTexCoord, a.rightTexCoord, a.topTexCoord, a.bottomTexCoord } 656 | end 657 | 658 | ns.AddTexString, ns.GetTexString, ns.AtlasToString, ns.GetAtlasFile, ns.GetAtlasCoords = AddTexString, GetTexString, AtlasToString, GetAtlasFile, GetAtlasCoords 659 | end 660 | 661 | 662 | function Hekili:GetSpec() 663 | return state.spec.id and class.specs[ state.spec.id ] 664 | end 665 | 666 | 667 | function Hekili:IsValidSpec() 668 | return state.spec.id and class.specs[ state.spec.id ] ~= nil 669 | end 670 | 671 | 672 | do 673 | local cache = {} 674 | 675 | function Hekili:Loadstring( str ) 676 | if cache[ str ] then return cache[ str ][ 1 ], cache[ str ][ 2 ] end 677 | local func, warn = loadstring( str ) 678 | cache[ str ] = { func, warn } 679 | return func, warn 680 | end 681 | end 682 | 683 | 684 | do 685 | local marked = {} 686 | local supermarked = {} 687 | local pool = {} 688 | 689 | function ns.Mark( table, key ) 690 | local data = remove( pool ) or {} 691 | data.t = table 692 | data.k = key 693 | insert( marked, data ) 694 | end 695 | 696 | function ns.SuperMark( table, keys ) 697 | supermarked[ table ] = keys 698 | end 699 | 700 | function ns.AddToSuperMark( table, key ) 701 | local sm = supermarked[ table ] 702 | if sm then 703 | insert( sm, key ) 704 | end 705 | end 706 | 707 | function ns.ClearMarks( super ) 708 | if super then 709 | for t, keys in pairs( supermarked ) do 710 | for key in pairs( keys ) do 711 | rawset( t, key, nil ) 712 | end 713 | end 714 | return 715 | end 716 | 717 | local data = remove( marked ) 718 | while( data ) do 719 | rawset( data.t, data.k, nil ) 720 | insert( pool, data ) 721 | data = remove( marked ) 722 | end 723 | end 724 | 725 | Hekili.Maintenance = { 726 | Dirty = marked, 727 | Cleaned = pool 728 | } 729 | end -------------------------------------------------------------------------------- /Formatting.lua: -------------------------------------------------------------------------------- 1 | -- Formatting.lua 2 | -- Modified from For all Indents and Purposes, info below. 3 | 4 | local addon, ns = ... 5 | local Hekili = _G[ addon ] 6 | 7 | --[[ For all Indents and Purposes 8 | 9 | Copyright (c) 2007 Kristofer Karlsson 10 | 11 | 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy of 14 | 15 | this software and associated documentation files (the "Software"), to deal in 16 | 17 | the Software without restriction, including without limitation the rights to 18 | 19 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 20 | 21 | the Software, and to permit persons to whom the Software is furnished to do so, 22 | 23 | subject to the following conditions: 24 | 25 | 26 | 27 | The above copyright notice and this permission notice shall be included in all 28 | 29 | copies or substantial portions of the Software. 30 | 31 | 32 | 33 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 34 | 35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 36 | 37 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 38 | 39 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 40 | 41 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 42 | 43 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 44 | 45 | ]] 46 | 47 | --- This is a specialized version of "For All Indents And Purposes", originally 48 | -- by krka , modified for Hack by Mud, aka 49 | -- Eric Tetz , and then further modified by Saiket 50 | -- for _DevPad. 51 | -- 52 | -- Modified by Hekili for Hekili, primarily to protect the "Accept" button 53 | -- functionality in AceConfigDialog driven environments. 54 | -- 55 | -- @usage Apply auto-indentation/syntax highlighting to an editbox like this: 56 | -- lib.Enable(Editbox, [TabWidth], [ColorTable], [SuppressIndent]); 57 | -- If TabWidth or ColorTable are omitted, those featues won't be applied. 58 | -- ColorTable should map TokenIDs and string Token values to color codes. 59 | -- @see lib.Tokens 60 | 61 | local lib = ns.lib.Format; 62 | 63 | local modf = math.modf 64 | local round = ns.round 65 | 66 | local UPDATE_INTERVAL = 0.2; -- Time to wait after last keypress before updating 67 | 68 | do 69 | local CursorPosition, CursorDelta; 70 | --- Callback for gsub to remove unescaped codes. 71 | local function StripCodeGsub ( Escapes, Code, End ) 72 | if ( #Escapes % 2 == 0 ) then -- Doesn't escape Code 73 | if ( CursorPosition and CursorPosition >= End - 1 ) then 74 | CursorDelta = CursorDelta - #Code; 75 | end 76 | return Escapes; 77 | end 78 | end 79 | --- Removes a single escape sequence. 80 | local function StripCode ( Pattern, Text, OldCursor ) 81 | CursorPosition, CursorDelta = OldCursor, 0; 82 | return Text:gsub( Pattern, StripCodeGsub ), 83 | OldCursor and CursorPosition + CursorDelta; 84 | end 85 | --- Strips Text of all color escape sequences. 86 | -- @param Cursor Optional cursor position to keep track of. 87 | -- @return Stripped text, and the updated cursor position if Cursor was given. 88 | function lib.StripColors ( Text, Cursor ) 89 | Text, Cursor = StripCode( "(|*)(|[Cc]%x%x%x%x%x%x%x%x)()", Text, Cursor ); 90 | return StripCode( "(|*)(|[Rr])()", Text, Cursor ); 91 | end 92 | end 93 | 94 | do 95 | local Enabled, Updaters = {}, {}; 96 | 97 | local CodeCache, ColoredCache = {}, {}; 98 | local NumLinesCache = {}; 99 | 100 | local SetTextBackup, GetTextBackup, InsertBackup; 101 | local GetCursorPositionBackup, SetCursorPositionBackup, HighlightTextBackup; 102 | --- Reapplies formatting to this editbox using settings from when it was enabled. 103 | -- @param ForceIndent If true, forces auto-indent even if the line count didn't 104 | -- change. If false, suppress indentation. If nil, only indent when line count changes. 105 | -- @return True if text was changed. 106 | function lib:Update ( ForceIndent ) 107 | if ( not Enabled[ self ] ) then 108 | return; 109 | end 110 | 111 | local Colored = GetTextBackup( self ); 112 | if ( ColoredCache[ self ] == Colored ) then 113 | return; 114 | end 115 | local Code, Cursor = lib.StripColors( Colored, 116 | GetCursorPositionBackup( self ) ); 117 | 118 | -- Count lines in text 119 | local NumLines, IndexLast = 0, 0; 120 | for Index in Code:gmatch( "[^\r\n]*()" ) do 121 | if ( IndexLast ~= Index ) then 122 | NumLines, IndexLast = NumLines + 1, Index; 123 | end 124 | end 125 | if ( ForceIndent == nil and NumLinesCache[ self ] ~= NumLines ) then 126 | ForceIndent = true; -- Reindent if line count changes 127 | end 128 | NumLinesCache[ self ] = NumLines; 129 | 130 | local ColoredNew, Cursor = lib.FormatCode( Code, 131 | ForceIndent and self.faiap_tabWidth, self.faiap_colorTable, Cursor ); 132 | CodeCache[ self ], ColoredCache[ self ] = Code, ColoredNew; 133 | 134 | if ( Colored ~= ColoredNew ) then 135 | self.Coloring = true 136 | SetTextBackup( self, ColoredNew ); 137 | SetCursorPositionBackup( self, Cursor ); 138 | return true; 139 | end 140 | end 141 | 142 | --- @return True if successfully disabled for this editbox. 143 | function lib:Disable () 144 | if ( not Enabled[ self ] ) then 145 | return; 146 | end 147 | Enabled[ self ] = false; 148 | self.GetText, self.SetText, self.Insert = nil; 149 | self.GetCursorPosition, self.SetCursorPosition, self.HighlightText = nil; 150 | 151 | local Code, Cursor = lib.StripColors( self:GetText(), 152 | self:GetCursorPosition() ); 153 | self:SetText( Code ); 154 | self:SetCursorPosition( Cursor ); 155 | 156 | self:SetMaxBytes( self.faiap_maxBytes ); 157 | self:SetCountInvisibleLetters( self.faiap_countInvisible ); 158 | self.faiap_maxBytes, self.faiap_countInvisible = nil; 159 | self.faiap_tabWidth, self.faiap_colorTable = nil; 160 | CodeCache[ self ], ColoredCache[ self ] = nil; 161 | NumLinesCache[ self ] = nil; 162 | return true; 163 | end 164 | 165 | --- Flags the editbox to be reformatted when its contents change. 166 | local function OnTextChanged ( self, ... ) 167 | if ( Enabled[ self ] ) then 168 | CodeCache[ self ] = nil; 169 | local Updater = Updaters[ self ]; 170 | Updater:Stop(); 171 | Updater:Play(); 172 | end 173 | if ( self.faiap_OnTextChanged ) then 174 | return self:faiap_OnTextChanged( ... ); 175 | end 176 | end 177 | 178 | --- Forces a re-indent for this editbox on tab. 179 | local function OnTabPressed ( self, ... ) 180 | if ( self.faiap_OnTabPressed ) then 181 | self:faiap_OnTabPressed( ... ); 182 | end 183 | return lib.Update( self, true ); 184 | end 185 | 186 | --- @return Cached plain text contents. 187 | local function GetCodeCached ( self ) 188 | local Code = CodeCache[ self ]; 189 | if ( not Code ) then 190 | Code = lib.StripColors( ( GetTextBackup( self ) ) ); 191 | CodeCache[ self ] = Code; 192 | end 193 | return Code; 194 | end 195 | 196 | --- @return Un-colored text as if FAIAP wasn't there. 197 | -- @param Raw True to return fully formatted contents. 198 | local function GetText( self, Raw ) 199 | if ( Raw ) then 200 | return GetTextBackup( self ); 201 | else 202 | return GetCodeCached( self ); 203 | end 204 | end 205 | 206 | --- Clears cached contents if set directly. 207 | -- This is necessary because OnTextChanged won't fire immediately or if the 208 | -- edit box is hidden. 209 | local function SetText ( self, ... ) 210 | CodeCache[ self ] = nil; 211 | return SetTextBackup( self, ... ); 212 | end 213 | 214 | local function Insert ( self, ... ) 215 | CodeCache[ self ] = nil; 216 | return InsertBackup( self, ... ); 217 | end 218 | 219 | --- @return Cursor position within un-colored text. 220 | local function GetCursorPosition ( self, ... ) 221 | local _, Cursor = lib.StripColors( GetTextBackup( self ), 222 | GetCursorPositionBackup( self, ... ) ); 223 | return Cursor; 224 | end 225 | 226 | --- Sets the cursor position relative to un-colored text. 227 | local function SetCursorPosition ( self, Cursor, ... ) 228 | local _, Cursor = lib.FormatCode( GetCodeCached( self ), 229 | nil, self.faiap_colorTable, Cursor ); 230 | return SetCursorPositionBackup( self, Cursor, ... ); 231 | end 232 | 233 | --- Highlights a substring relative to un-colored text. 234 | local function HighlightText ( self, Start, End, ... ) 235 | if ( Start ~= End and ( Start or End ) ) then 236 | local Code, _ = GetCodeCached( self ); 237 | if ( Start ) then 238 | _, Start = lib.FormatCode( GetCodeCached( self ), 239 | nil, self.faiap_colorTable, Start ); 240 | end 241 | if ( End ) then 242 | _, End = lib.FormatCode( GetCodeCached( self ), 243 | nil, self.faiap_colorTable, End ); 244 | end 245 | end 246 | return HighlightTextBackup( self, Start, End, ... ); 247 | end 248 | 249 | --- Updates the code a moment after the user quits typing. 250 | local function UpdaterOnFinished ( Updater ) 251 | return lib.Update( Updater.EditBox ); 252 | end 253 | 254 | local function HookHandler ( self, Handler, Script ) 255 | self[ "faiap_"..Handler ] = self:GetScript( Handler ); 256 | self:SetScript( Handler, Script ); 257 | end 258 | 259 | --- Enables syntax highlighting or auto-indentation on this edit box. 260 | -- Can be run again to change the TabWidth or ColorTable. 261 | -- @param TabWidth Tab width to indent code by, or nil for no indentation. 262 | -- @param ColorTable Table of tokens and token types to color codes used for 263 | -- syntax highlighting, or nil for no syntax highlighting. 264 | -- @param SuppressIndent Don't immediately re-indent text, even with TabWidth enabled. 265 | -- @return True if enabled and formatted. 266 | function lib:Enable ( TabWidth, ColorTable, SuppressIndent ) 267 | if ( not SetTextBackup ) then 268 | GetTextBackup, SetTextBackup = self.GetText, self.SetText; 269 | InsertBackup = self.Insert; 270 | GetCursorPositionBackup = self.GetCursorPosition; 271 | SetCursorPositionBackup = self.SetCursorPosition; 272 | HighlightTextBackup = self.HighlightText; 273 | end 274 | if ( not ( TabWidth or ColorTable ) ) then 275 | return lib.Disable( self ); 276 | end 277 | 278 | if ( not Enabled[ self ] ) then 279 | self.faiap_maxBytes = self:GetMaxBytes(); 280 | self.faiap_countInvisible = self:IsCountInvisibleLetters(); 281 | self:SetMaxBytes( 0 ); 282 | self:SetCountInvisibleLetters( false ); 283 | self.GetText, self.SetText = GetText, SetText; 284 | self.Insert = Insert; 285 | self.GetCursorPosition = GetCursorPosition; 286 | self.SetCursorPosition = SetCursorPosition; 287 | self.HighlightText = HighlightText; 288 | 289 | if ( Enabled[ self ] == nil ) then -- Never hooked before 290 | -- Note: Animation must not be parented to EditBox, or else lots of 291 | -- text will cause huge framerate drops after Updater:Play(). 292 | local Updater = CreateFrame( "Frame", nil, self ):CreateAnimationGroup(); 293 | Updaters[ self ], Updater.EditBox = Updater, self; 294 | Updater:CreateAnimation( "Animation" ):SetDuration( UPDATE_INTERVAL ); 295 | Updater:SetScript( "OnFinished", UpdaterOnFinished ); 296 | HookHandler( self, "OnTextChanged", OnTextChanged ); 297 | HookHandler( self, "OnTabPressed", OnTabPressed ); 298 | end 299 | Enabled[ self ] = true; 300 | end 301 | self.faiap_tabWidth, self.faiap_colorTable = TabWidth, ColorTable; 302 | ColoredCache[ self ] = nil; -- Force update with new tab width/colors 303 | 304 | return lib.Update( self, not SuppressIndent ); 305 | end 306 | end 307 | 308 | -- Token types 309 | lib.Tokens = {}; --- Token names to TokenTypeIDs, used to define custom ColorTables. 310 | local NewToken; 311 | do 312 | local Count = 0; 313 | --- @return A new token ID assigned to Name. 314 | function NewToken ( Name ) 315 | Count = Count + 1; 316 | lib.Tokens[ Name ] = Count; 317 | return Count; 318 | end 319 | end 320 | 321 | local TK_UNKNOWN = NewToken( "UNKNOWN" ); 322 | local TK_IDENTIFIER = NewToken( "IDENTIFIER" ); 323 | local TK_KEYWORD = NewToken( "KEYWORD" ); -- Reserved words 324 | 325 | local TK_ADD = NewToken( "ADD" ); 326 | local TK_ASSIGNMENT = NewToken( "ASSIGNMENT" ); 327 | local TK_COLON = NewToken( "COLON" ); 328 | local TK_COMMA = NewToken( "COMMA" ); 329 | local TK_COMMENT_LONG = NewToken( "COMMENT_LONG" ); 330 | local TK_COMMENT_SHORT = NewToken( "COMMENT_SHORT" ); 331 | local TK_CONCAT = NewToken( "CONCAT" ); 332 | local TK_DIVIDE = NewToken( "DIVIDE" ); 333 | local TK_EQUALITY = NewToken( "EQUALITY" ); 334 | local TK_GT = NewToken( "GT" ); 335 | local TK_GTE = NewToken( "GTE" ); 336 | local TK_LEFTBRACKET = NewToken( "LEFTBRACKET" ); 337 | local TK_LEFTCURLY = NewToken( "LEFTCURLY" ); 338 | local TK_LEFTPAREN = NewToken( "LEFTPAREN" ); 339 | local TK_LINEBREAK = NewToken( "LINEBREAK" ); 340 | local TK_LT = NewToken( "LT" ); 341 | local TK_LTE = NewToken( "LTE" ); 342 | local TK_MODULUS = NewToken( "MODULUS" ); 343 | local TK_MULTIPLY = NewToken( "MULTIPLY" ); 344 | local TK_NOTEQUAL = NewToken( "NOTEQUAL" ); 345 | local TK_NUMBER = NewToken( "NUMBER" ); 346 | local TK_PERIOD = NewToken( "PERIOD" ); 347 | local TK_POWER = NewToken( "POWER" ); 348 | local TK_RIGHTBRACKET = NewToken( "RIGHTBRACKET" ); 349 | local TK_RIGHTCURLY = NewToken( "RIGHTCURLY" ); 350 | local TK_RIGHTPAREN = NewToken( "RIGHTPAREN" ); 351 | local TK_SEMICOLON = NewToken( "SEMICOLON" ); 352 | local TK_SIZE = NewToken( "SIZE" ); 353 | local TK_STRING = NewToken( "STRING" ); 354 | local TK_STRING_LONG = NewToken( "STRING_LONG" ); -- [=[...]=] 355 | local TK_SUBTRACT = NewToken( "SUBTRACT" ); 356 | local TK_VARARG = NewToken( "VARARG" ); 357 | local TK_WHITESPACE = NewToken( "WHITESPACE" ); 358 | 359 | local strbyte = string.byte; 360 | local BYTE_0 = strbyte( "0" ); 361 | local BYTE_9 = strbyte( "9" ); 362 | local BYTE_ASTERISK = strbyte( "*" ); 363 | local BYTE_BACKSLASH = strbyte( "\\" ); 364 | local BYTE_CIRCUMFLEX = strbyte( "^" ); 365 | local BYTE_COLON = strbyte( ":" ); 366 | local BYTE_COMMA = strbyte( "," ); 367 | local BYTE_CR = strbyte( "\r" ); 368 | local BYTE_DOUBLE_QUOTE = strbyte( "\"" ); 369 | local BYTE_E = strbyte( "E" ); 370 | local BYTE_e = strbyte( "e" ); 371 | local BYTE_EQUALS = strbyte( "=" ); 372 | local BYTE_GREATERTHAN = strbyte( ">" ); 373 | local BYTE_HASH = strbyte( "#" ); 374 | local BYTE_LEFTBRACKET = strbyte( "[" ); 375 | local BYTE_LEFTCURLY = strbyte( "{" ); 376 | local BYTE_LEFTPAREN = strbyte( "(" ); 377 | local BYTE_LESSTHAN = strbyte( "<" ); 378 | local BYTE_LF = strbyte( "\n" ); 379 | local BYTE_MINUS = strbyte( "-" ); 380 | local BYTE_PERCENT = strbyte( "%" ); 381 | local BYTE_PERIOD = strbyte( "." ); 382 | local BYTE_PLUS = strbyte( "+" ); 383 | local BYTE_RIGHTBRACKET = strbyte( "]" ); 384 | local BYTE_RIGHTCURLY = strbyte( "}" ); 385 | local BYTE_RIGHTPAREN = strbyte( ")" ); 386 | local BYTE_SEMICOLON = strbyte( ";" ); 387 | local BYTE_SINGLE_QUOTE = strbyte( "'" ); 388 | local BYTE_SLASH = strbyte( "/" ); 389 | local BYTE_SPACE = strbyte( " " ); 390 | local BYTE_TAB = strbyte( "\t" ); 391 | local BYTE_TILDE = strbyte( "~" ); 392 | 393 | local Linebreaks = { 394 | [ BYTE_CR ] = true; 395 | [ BYTE_LF ] = true; 396 | } 397 | 398 | local Whitespace = { 399 | [ BYTE_SPACE ] = true; 400 | [ BYTE_TAB ] = true; 401 | } 402 | 403 | --- Mapping of bytes to the only tokens they can represent, or true if indeterminate 404 | local TokenBytes = { 405 | [ BYTE_ASTERISK ] = TK_MULTIPLY; 406 | [ BYTE_CIRCUMFLEX ] = TK_POWER; 407 | [ BYTE_COLON ] = TK_COLON; 408 | [ BYTE_COMMA ] = TK_COMMA; 409 | [ BYTE_DOUBLE_QUOTE ] = true; 410 | [ BYTE_EQUALS ] = true; 411 | [ BYTE_GREATERTHAN ] = true; 412 | [ BYTE_HASH ] = TK_SIZE; 413 | [ BYTE_LEFTBRACKET ] = true; 414 | [ BYTE_LEFTCURLY ] = TK_LEFTCURLY; 415 | [ BYTE_LEFTPAREN ] = TK_LEFTPAREN; 416 | [ BYTE_LESSTHAN ] = true; 417 | [ BYTE_MINUS ] = true; 418 | [ BYTE_PERCENT ] = TK_MODULUS; 419 | [ BYTE_PERIOD ] = true; 420 | [ BYTE_PLUS ] = TK_ADD; 421 | [ BYTE_RIGHTBRACKET ] = TK_RIGHTBRACKET; 422 | [ BYTE_RIGHTCURLY ] = TK_RIGHTCURLY; 423 | [ BYTE_RIGHTPAREN ] = TK_RIGHTPAREN; 424 | [ BYTE_SEMICOLON ] = TK_SEMICOLON; 425 | [ BYTE_SINGLE_QUOTE ] = true; 426 | [ BYTE_SLASH ] = TK_DIVIDE; 427 | [ BYTE_TILDE ] = true; 428 | } 429 | 430 | local strfind = string.find; 431 | --- Reads the next Lua identifier from its beginning. 432 | local function NextIdentifier ( Text, Pos ) 433 | local _, End = strfind( Text, "^[_%a][_%w]*", Pos ); 434 | if ( End ) then 435 | return TK_IDENTIFIER, End + 1; 436 | else 437 | return TK_UNKNOWN, Pos + 1; 438 | end 439 | end 440 | 441 | --- Reads all following decimal digits. 442 | local function NextNumberDecPart ( Text, Pos ) 443 | local _, End = strfind( Text, "^%d+", Pos ); 444 | return TK_NUMBER, End and End + 1 or Pos; 445 | end 446 | 447 | --- Reads the next scientific e notation exponent beginning after the 'e'. 448 | local function NextNumberExponentPart ( Text, Pos ) 449 | local Byte = strbyte( Text, Pos ); 450 | if ( not Byte ) then 451 | return TK_NUMBER, Pos; 452 | end 453 | if ( Byte == BYTE_MINUS ) then 454 | -- Handle this case: "1.2e-- comment" with "1.2e" as a number 455 | if ( strbyte( Text, Pos + 1 ) == BYTE_MINUS ) then 456 | return TK_NUMBER, Pos; 457 | end 458 | Pos = Pos + 1; 459 | end 460 | return NextNumberDecPart( Text, Pos ); 461 | end 462 | 463 | --- Reads the fractional part of a number beginning after the decimal. 464 | local function NextNumberFractionPart ( Text, Pos ) 465 | local _, Pos = NextNumberDecPart( Text, Pos ); 466 | if ( strfind( Text, "^[Ee]", Pos ) ) then 467 | return NextNumberExponentPart( Text, Pos + 1 ); 468 | else 469 | return TK_NUMBER, Pos; 470 | end 471 | end 472 | 473 | --- Reads all following hex digits. 474 | local function NextNumberHexPart ( Text, Pos ) 475 | local _, End = strfind( Text, "^%x+", Pos ); 476 | return TK_NUMBER, End and End + 1 or Pos; 477 | end 478 | 479 | --- Reads the next number from its beginning. 480 | local function NextNumber ( Text, Pos ) 481 | if ( strfind( Text, "^0[Xx]", Pos ) ) then 482 | return NextNumberHexPart( Text, Pos + 2 ); 483 | end 484 | local _, Pos = NextNumberDecPart( Text, Pos ); 485 | local Byte = strbyte( Text, Pos ); 486 | if ( Byte == BYTE_PERIOD ) then 487 | return NextNumberFractionPart( Text, Pos + 1 ); 488 | elseif ( Byte == BYTE_E or Byte == BYTE_e ) then 489 | return NextNumberExponentPart( Text, Pos + 1 ); 490 | else 491 | return TK_NUMBER, Pos; 492 | end 493 | end 494 | 495 | --- @return PosNext, EqualsCount if next token is a long string. 496 | local function NextLongStringStart ( Text, Pos ) 497 | local Start, End = strfind( Text, "^%[=*%[", Pos ); 498 | if ( End ) then 499 | return End + 1, End - Start - 1; 500 | end 501 | end 502 | 503 | --- Reads the next long string beginning after its opening brackets. 504 | local function NextLongString ( Text, Pos, EqualsCount ) 505 | local _, End = strfind( Text, "]"..( "=" ):rep( EqualsCount ).."]", Pos, true ); 506 | return TK_STRING_LONG, ( End or #Text ) + 1; 507 | end 508 | 509 | --- Reads the next short or long comment beginning after its dashes. 510 | local function NextComment ( Text, Pos ) 511 | local PosNext, EqualsCount = NextLongStringStart( Text, Pos ); 512 | if ( PosNext ) then 513 | local _, PosNext = NextLongString( Text, PosNext, EqualsCount ); 514 | return TK_COMMENT_LONG, PosNext; 515 | end 516 | -- Short comment; ends at linebreak 517 | local _, End = strfind( Text, "[^\r\n]*", Pos ); 518 | return TK_COMMENT_SHORT, End + 1; 519 | end 520 | 521 | local strchar = string.char; 522 | --- Reads the next single/double quoted string beginning at its opening quote. 523 | -- Note: Strings with unescaped newlines aren't properly terminated. 524 | local function NextString ( Text, Pos, QuoteByte ) 525 | local Pattern, Start = [[\*]]..strchar( QuoteByte ); 526 | while ( Pos ) do 527 | Start, Pos = strfind( Text, Pattern, Pos + 1 ); 528 | if ( Pos and ( Pos - Start ) % 2 == 0 ) then -- Not escaped 529 | return TK_STRING, Pos + 1; 530 | end 531 | end 532 | return TK_STRING, #Text + 1; 533 | end 534 | 535 | --- @return Token type or nil if end of string, position of char after token. 536 | local function NextToken ( Text, Pos ) 537 | local Byte = strbyte( Text, Pos ); 538 | if ( not Byte ) then 539 | return; 540 | end 541 | 542 | if ( Linebreaks[ Byte ] ) then 543 | return TK_LINEBREAK, Pos + 1; 544 | end 545 | 546 | if ( Whitespace[ Byte ] ) then 547 | local _, End = strfind( Text, "^[ \t]*", Pos + 1 ); 548 | return TK_WHITESPACE, End + 1; 549 | end 550 | 551 | local Token = TokenBytes[ Byte ]; 552 | if ( Token ) then 553 | if ( Token ~= true ) then -- Byte can only represent this token 554 | return Token, Pos + 1; 555 | end 556 | 557 | if ( Byte == BYTE_SINGLE_QUOTE or Byte == BYTE_DOUBLE_QUOTE ) then 558 | return NextString( Text, Pos, Byte ); 559 | 560 | elseif ( Byte == BYTE_LEFTBRACKET ) then 561 | local PosNext, EqualsCount = NextLongStringStart( Text, Pos ); 562 | if ( PosNext ) then 563 | return NextLongString( Text, PosNext, EqualsCount ); 564 | else 565 | return TK_LEFTBRACKET, Pos + 1; 566 | end 567 | end 568 | 569 | if ( Byte == BYTE_MINUS ) then 570 | if ( strbyte( Text, Pos + 1 ) == BYTE_MINUS ) then 571 | return NextComment( Text, Pos + 2 ); 572 | end 573 | return TK_SUBTRACT, Pos + 1; 574 | 575 | elseif ( Byte == BYTE_EQUALS ) then 576 | if ( strbyte( Text, Pos + 1 ) == BYTE_EQUALS ) then 577 | return TK_EQUALITY, Pos + 2; 578 | end 579 | return TK_ASSIGNMENT, Pos + 1; 580 | 581 | elseif ( Byte == BYTE_PERIOD ) then 582 | local Byte2 = strbyte( Text, Pos + 1 ); 583 | if ( Byte2 == BYTE_PERIOD ) then 584 | if ( strbyte( Text, Pos + 2 ) == BYTE_PERIOD ) then 585 | return TK_VARARG, Pos + 3; 586 | end 587 | return TK_CONCAT, Pos + 2; 588 | elseif ( Byte2 and Byte2 >= BYTE_0 and Byte2 <= BYTE_9 ) then 589 | return NextNumberFractionPart( Text, Pos + 2 ); 590 | end 591 | return TK_PERIOD, Pos + 1; 592 | 593 | elseif ( Byte == BYTE_LESSTHAN ) then 594 | if ( strbyte( Text, Pos + 1 ) == BYTE_EQUALS ) then 595 | return TK_LTE, Pos + 2; 596 | end 597 | return TK_LT, Pos + 1; 598 | 599 | elseif ( Byte == BYTE_GREATERTHAN ) then 600 | if ( strbyte( Text, Pos + 1 ) == BYTE_EQUALS ) then 601 | return TK_GTE, Pos + 2; 602 | end 603 | return TK_GT, Pos + 1; 604 | 605 | elseif ( Byte == BYTE_TILDE 606 | and strbyte( Text, Pos + 1 ) == BYTE_EQUALS 607 | ) then 608 | return TK_NOTEQUAL, Pos + 2; 609 | end 610 | elseif ( Byte >= BYTE_0 and Byte <= BYTE_9 ) then 611 | return NextNumber( Text, Pos ); 612 | else 613 | return NextIdentifier( Text, Pos ); 614 | end 615 | return TK_UNKNOWN, Pos + 1; 616 | end 617 | 618 | 619 | local Keywords = { 620 | [ "nil" ] = true; 621 | [ "true" ] = true; 622 | [ "false" ] = true; 623 | [ "local" ] = true; 624 | [ "and" ] = true; 625 | [ "or" ] = true; 626 | [ "not" ] = true; 627 | [ "while" ] = true; 628 | [ "for" ] = true; 629 | [ "in" ] = true; 630 | [ "do" ] = true; 631 | [ "repeat" ] = true; 632 | [ "break" ] = true; 633 | [ "until" ] = true; 634 | [ "if" ] = true; 635 | [ "elseif" ] = true; 636 | [ "then" ] = true; 637 | [ "else" ] = true; 638 | [ "function" ] = true; 639 | [ "return" ] = true; 640 | [ "end" ] = true; 641 | } 642 | 643 | local IndentOpen = { 0, 1 } 644 | local IndentClose = { -1, 0 } 645 | local IndentBoth = { -1, 1 } 646 | 647 | local Indents = { 648 | [ "do" ] = IndentOpen; 649 | [ "then" ] = IndentOpen; 650 | [ "repeat" ] = IndentOpen; 651 | [ "function" ] = IndentOpen; 652 | [ TK_LEFTPAREN ] = IndentOpen; 653 | [ TK_LEFTBRACKET ] = IndentOpen; 654 | [ TK_LEFTCURLY ] = IndentOpen; 655 | 656 | [ "until" ] = IndentClose; 657 | [ "elseif" ] = IndentClose; 658 | [ "end" ] = IndentClose; 659 | [ TK_RIGHTPAREN ] = IndentClose; 660 | [ TK_RIGHTBRACKET ] = IndentClose; 661 | [ TK_RIGHTCURLY ] = IndentClose; 662 | 663 | [ "else" ] = IndentBoth; 664 | } 665 | 666 | local strrep, strsub = string.rep, string.sub 667 | local tinsert = table.insert 668 | local TERMINATOR = "|r" 669 | local Buffer = {} 670 | 671 | --- Syntax highlights and indents a string of Lua code. 672 | -- @param CursorOld Optional cursor position to keep track of. 673 | -- @see lib.Enable 674 | -- @return Formatted text, and an updated cursor position if requested. 675 | function lib:FormatCode ( TabWidth, ColorTable, CursorOld ) 676 | if ( not ( TabWidth or ColorTable ) ) then 677 | return self, CursorOld; 678 | end 679 | 680 | wipe( Buffer ); 681 | local BufferLen = 0; 682 | local Cursor, CursorIndented; 683 | local ColorLast; 684 | 685 | local LineLast, PassedIndent = 0, false; 686 | local Depth, DepthNext = 0, 0; 687 | 688 | local TokenType, PosNext, Pos = TK_UNKNOWN, 1; 689 | while ( TokenType ) do 690 | Pos, TokenType, PosNext = PosNext, NextToken( self, PosNext ); 691 | 692 | if ( TokenType 693 | and ( PassedIndent or not TabWidth or TokenType ~= TK_WHITESPACE ) 694 | ) then 695 | PassedIndent = true; -- Passed leading whitespace 696 | local Token = strsub( self, Pos, PosNext - 1 ); 697 | 698 | local ColorCode; 699 | if ( ColorTable ) then -- Add coloring 700 | local Color = ColorTable[ Keywords[ Token ] and TK_KEYWORD or Token ] 701 | or ColorTable[ TokenType ]; 702 | ColorCode = ( ColorLast and not Color and TERMINATOR ) -- End color 703 | or ( Color ~= ColorLast and Color ); -- Change color 704 | if ( ColorCode ) then 705 | Buffer[ #Buffer + 1 ], BufferLen = ColorCode, BufferLen + #ColorCode; 706 | end 707 | ColorLast = Color; 708 | end 709 | 710 | Buffer[ #Buffer + 1 ], BufferLen = Token, BufferLen + #Token; 711 | 712 | if ( CursorOld and not Cursor 713 | and CursorOld < PosNext - 1 -- Before end of token 714 | ) then 715 | local Offset = PosNext - CursorOld - 1; -- Distance to end of token 716 | if ( Offset > #Token ) then -- Cursor was in a previous skipped token 717 | Offset = #Token; -- Move to start of current token 718 | end 719 | -- Note: Cursor must not be directly inside of color codes, i.e. 720 | -- |cffxxxxxx_ or _|r, else the cursor can interact with them directly. 721 | if ( ColorCode and ColorLast -- Added color start code before token 722 | and Offset == #Token -- Cursor at start of token 723 | ) then 724 | Offset = Offset + #ColorCode; -- Move to before color code 725 | end 726 | Cursor = BufferLen - Offset; 727 | end 728 | 729 | local Indent = TabWidth and ( 730 | ( TokenType == TK_IDENTIFIER and Indents[ Token ] ) 731 | or Indents[ TokenType ] ); 732 | if ( Indent ) then -- Apply token indent-modifier 733 | if ( DepthNext > 0 ) then 734 | DepthNext = DepthNext + Indent[ 1 ]; 735 | else 736 | Depth = Depth + Indent[ 1 ]; 737 | end 738 | DepthNext = DepthNext + Indent[ 2 ]; 739 | end 740 | end 741 | 742 | if ( TabWidth and ( not TokenType or TokenType == TK_LINEBREAK ) ) then 743 | -- Indent previous line 744 | local Indent = strrep( " ", Depth * TabWidth ); 745 | BufferLen = BufferLen + #Indent; 746 | tinsert( Buffer, LineLast + 1, Indent ); 747 | 748 | if ( Cursor and not CursorIndented ) then 749 | Cursor = Cursor + #Indent; 750 | if ( CursorOld < Pos ) then -- Cursor on this line 751 | CursorIndented = true; 752 | end -- Else cursor is on next line and must be indented again 753 | end 754 | 755 | LineLast, PassedIndent = #Buffer, false; 756 | Depth, DepthNext = Depth + DepthNext, 0; 757 | if ( Depth < 0 ) then 758 | Depth = 0; 759 | end 760 | end 761 | end 762 | return table.concat( Buffer ), Cursor or BufferLen; 763 | end 764 | 765 | 766 | local COLOR_NUMBERS = '|cFFFFD100' 767 | local COLOR_TRUE = '|cFF00FF00' 768 | local COLOR_FALSE = '|cFFFF0000' 769 | local COLOR_STRING = '|cFF008888' 770 | local COLOR_DEFAULT = '|cFFFFFFFF' 771 | local COLOR_NORMAL = '|r' 772 | 773 | 774 | function ns.formatValue( value ) 775 | 776 | if value == nil then value = 'nil' end 777 | 778 | if type( value ) == 'number' then 779 | -- Check for decimal places. 780 | if select(2, modf( value )) ~= 0 then 781 | return COLOR_NUMBERS .. round( value, 2 ) .. COLOR_NORMAL 782 | else 783 | return COLOR_NUMBERS .. value .. COLOR_NORMAL 784 | end 785 | 786 | elseif type( value ) == 'boolean' then 787 | if value then 788 | return COLOR_TRUE .. tostring( value ) .. COLOR_NORMAL 789 | else 790 | return COLOR_FALSE .. tostring( value ) .. COLOR_NORMAL 791 | end 792 | 793 | elseif type( value ) == 'string' then 794 | return COLOR_STRING .. value .. COLOR_NORMAL 795 | 796 | end 797 | 798 | return COLOR_DEFAULT .. tostring( value ) .. COLOR_NORMAL 799 | 800 | end 801 | --------------------------------------------------------------------------------