├── .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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Windows image file caches
2 | Thumbs.db
3 | ehthumbs.db
4 |
5 | # Folder config file
6 | Desktop.ini
7 |
8 | # Recycle Bin used on file shares
9 | $RECYCLE.BIN/
10 |
11 | # Windows Installer files
12 | *.cab
13 | *.msi
14 | *.msm
15 | *.msp
16 |
17 | # Windows shortcuts
18 | *.lnk
19 |
20 | # =========================
21 | # Operating System Files
22 | # =========================
23 |
24 | # OSX
25 | # =========================
26 |
27 | .DS_Store
28 | .AppleDouble
29 | .LSOverride
30 |
31 | # Thumbnails
32 | ._*
33 |
34 | # Files that might appear in the root of a volume
35 | .DocumentRevisions-V100
36 | .fseventsd
37 | .Spotlight-V100
38 | .TemporaryItems
39 | .Trashes
40 | .VolumeIcon.icns
41 |
42 | # Directories potentially created on remote AFP share
43 | .AppleDB
44 | .AppleDesktop
45 | Network Trash Folder
46 | Temporary Items
47 | .apdisk
48 |
49 | # Actual Stuff in my folder.
50 | Libs/AceAddon-3.0
51 | Libs/AceBucket-3.0
52 | Libs/AceComm-3.0
53 | Libs/AceConfig-3.0
54 | Libs/AceConsole-3.0
55 | Libs/AceDB-3.0
56 | Libs/AceDBOptions-3.0
57 | Libs/AceEvent-3.0
58 | Libs/AceGUI-3.0
59 | Libs/AceGUI-3.0_SFX-Widgets/
60 | Libs/AceGUI-3.0-SharedMediaWidgets
61 | Libs/AceHook-3.0
62 | Libs/AceLocale-3.0
63 | Libs/AceSerializer-3.0
64 | Libs/AceTab-3.0
65 | Libs/AceTimer-3.0
66 | Libs/CallbackHandler-1.0
67 | Libs/LibArtifactData-1.0
68 | Libs/LibChatAnims/
69 | Libs/LibCompress
70 | Libs/LibDeflate
71 | Libs/LibCustomGlow-1.0/
72 | Libs/LibDataBroker-1-1
73 | Libs/LibDualSpec-1.0
74 | Libs/LibItemBuffs-1.0
75 | Libs/LibNameplateRegistry-1.0/
76 | Libs/LibRangeCheck-2.0
77 | Libs/LibSharedMedia-3.0
78 | Libs/LibSpellRange-1.0
79 | Libs/LibStub
80 | Libs/LibTranslit-1.0/
81 | Libs/SpellFlashCore
82 | CHANGES.txt
83 | Textures/LOGO-ORANGE.png
84 | Textures/LOGO-WHITE.png
85 | TODO.txt
86 | Textures/BLPconv.exe
87 | CHANGELOG.md
88 | .vscode/
89 | .release/
90 | Notes/
91 | Hekili Wrath.code-workspace
92 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | # description of this workflow, can be anything you want
2 | name: Package and Release
3 |
4 | # we need to let GitHub know _when_ we want to release, typically only when we create a new tag.
5 | # this will target only tags, and not all pushes to the master branch.
6 | # this part can be heavily customized to your liking, like targeting only tags that match a certain word,
7 | # other branches or even pullrequests.
8 | on:
9 | push:
10 | tags:
11 | - "v1*"
12 | workflow_dispatch:
13 |
14 | # a workflow is built up as jobs, and within these jobs are steps
15 | jobs:
16 |
17 | # "release" is a job, you can name it anything you want
18 | release:
19 |
20 | # we can run our steps on pretty much anything, but the "ubuntu-latest" image is a safe bet
21 | runs-on: ubuntu-latest
22 |
23 | # specify the environment variables used by the packager, matching the secrets from the project on GitHub
24 | env:
25 | CF_API_KEY: ${{ secrets.CF_API_KEY }}
26 | WOWI_API_TOKEN: ${{ secrets.WOWI_API_TOKEN }}
27 | WAGO_API_TOKEN: ${{ secrets.WAGO_API_TOKEN }}
28 | GITHUB_OAUTH: ${{ secrets.GITHUB_TOKEN }} # "GITHUB_TOKEN" is a secret always provided to the workflow
29 | # for your own token, the name cannot start with "GITHUB_"
30 |
31 | # "steps" holds a list of all the steps needed to package and release our AddOn
32 | steps:
33 |
34 | # we first have to clone the AddOn project, this is a required step
35 | - name: Clone Project
36 | uses: actions/checkout@v1 # note: checkout@v2 breaks git history, so generating a changelog and
37 | # file naming for non-tagged builds will not work properly
38 |
39 | # once cloned, we just run the GitHub Action for the packager project
40 | - name: Package and Release
41 | uses: BigWigsMods/packager@v2
42 | with:
43 | args: -g classic
44 |
--------------------------------------------------------------------------------
/UI/PopupSlider.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/Libs/TaintLess/TaintLess.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------