Important note… This post was created by Noxxious and was copied from the following link:
My thanks to the creator of the tutorial for what is, in my opinion, the easiest to follow option available to learn how to create your own addons for world of warcraft.
Creating a WoW Addon – Part 1: A Fresh Start : r/wowaddondev
The only reason for this re-post is because I wanted a place to see it all in one page instead of clicking through multiple links and I also think it could benefit others. I take no credit for the creation of this tutorial. Please visit the link above if you would like to see the original post from its creator.
Creating a WoW Addon – Part 1: A Fresh Start
Part 1: A Fresh Start
So, you want to make a World of Warcraft addon?
Good news! This small series will show you how to build an addon from scratch. We won’t be using any libraries to begin with. There’s just something about coding an addon from scratch that hits the spot… but we will eventually reach a point where we use a library – to create a minimap button. Trust me, you don’t want to do this yourself.
Note: This guide appears very strangely on “Old Reddit” – So try to use New Reddit to at least view the guide if possible.
First, a little bit about me:
I am a new-ish World of Warcraft addon developer that ran into a few issues creating my first few addons. Now, with all of those things ironed out, I’m here to help other new, aspiring addon developers.
To start, we’re going to cover a few semi-important things.
- This addon guide is written using World of Warcraft: Classic (Season of Discovery) as a base. Though, most of the things in this guide can be used on any version of World of Warcraft. Some API may differ between game versions.
- We will be primarily using Visual Studio Code (VSCode) to code this.
- Before starting, you’ll want to install VSCode and then install a few extensions that are helpful for us. You can also use Project IDX (Google) and install these extensions with their appropriate VSIX files.
- Ketho’s WoW API
- Septh’s WoW Bundle
- Stanzilla’s WoW TOC
- We’ll also want to grab a couple useful development addons. You can get these directly from Curseforge.
- DevTool
- TextureAtlasViewer
- Before starting, you’ll want to install VSCode and then install a few extensions that are helpful for us. You can also use Project IDX (Google) and install these extensions with their appropriate VSIX files.
- This guide covers the following:
- Creating frames
- Creating variables, saved variables and functions
- Creating frames that listen to events
- Learning general Lua language syntax
- Debugging and testing your addon
- Creating a Github repository to store your addon code and manage versions
- Publishing your addon to Curseforge
We are going to be building an addon that records the total number of kills a character has and the total amount of gold they gain.
Let’s get into the first part:
Step 1: Your .TOC File
First, we’ll need to make a folder for your addon to live. Navigate to your World of Warcraft install folder and into Interface/AddOns
. In this folder we will make a new folder. Name this folder the same name you’ll be giving your addon. In this case, I’ll create a folder called "NoxxAddon"
– I know, very original of me.
In this folder, you’re going to create a new file. It will be called "NoxxAddon.toc"
in my case, and this is where all of your addons settings will reside. You can name this your addon’s name and add .toc to the end. Inside of this .toc file, we will paste the following:
## Interface: 99999
## Title: Your Addon Name
## Notes: Describe your addon.
## Author: Your Name
## Version: 1.0.0
## RequiredDeps:
## X-Curse-Project-ID: 999999
## SavedVariables:
## SavedVariablesPerCharacter:
Now, there are a few things we need to modify:
- Interface Number
- Title
- Notes
- Author
- X-Curse-Project-ID
Interface
To start, let’s get that Interface number out of the way. To get the interface version you are developing your addon for, head into World of Warcraft and paste the following in your chat box and press ‘Enter.’
/dump select(4, GetBuildInfo())

The very top number that prints is the game’s interface version number. If this number is not up to date, players who use your addon will get an Outdated error when attempting to load World of Warcraft or when they open their addon list. Make sure to update this number with each WoW version release.
- Title, Notes & Author
For your Title, Notes and Author fields, just fill out your addon’s name, a description of your addon and of course your name for the author field.
- X-Curse-Project-ID
For this field, we will leave it as all 9’s until we upload our addon to Curseforge for people to download. Once we have our project created with Curseforge, we will be able to find our project’s unique ID and put it here.
Let’s proceed to Step 2, where we’ll be officially building your addon’s first file!
Step 2: MyAddon.lua
World of Warcraft addons are coded using a language known as Lua. It’s a very straightforward language and is easy to learn once you get rolling. We’re going to start by creating a new file in your addon’s folder. I will be naming mine "NoxxAddon.lua"
– keeping our file names consistent. At this point you should have the following folder setup:
- MyAddonFolder
- MyAddon.toc
- MyAddon.lua
If you have this set up correctly, we’re going to move on and make sure this file is referenced in our .toc file so it loads. Before we reference this file in our .toc file, let’s add a print
statement to make sure it loads correctly. In your .lua file we’ve just created, copy and paste this line of code, or change it to your liking:
print("MyAddon successfully loaded!")
Once you’ve saved your Lua file, let’s head back into our .toc file and add our .lua file so it loads when we login to World of Warcraft. Open your .toc file and add the file name to the bottom. It should look as such (keep your interface number, title, etc.):
## Interface: 11502
## Title: NoxxAddon
## Notes: An addon that does nothing yet.
## Author: Noxxious
## Version: 1.0.0
## RequiredDeps:
## X-Curse-Project-ID: 99999
## SavedVariables:
## SavedVariablesPerCharacter:
NoxxAddon.lua
Once we have this saved, go ahead and load into WoW (or /reload
if you’re already logged in) and check your chat box to find your message printed. If it prints, you’ve successfully created and loaded your first addon!
Now for the sake of this tutorial, we’ll be creating an addon that has a settings interface with a few simple settings and a main addon window. The main function, albeit kind of lame, will be a “Slayer” booklet of sorts:
- Log the amount of NPC kills the player has.
- Log the amount of gold looted since the addon has been installed.
- Add settings to:
- Disable/Enable the tracking of these stats.
- Reset the player’s log.
Creating a WoW Addon – Part 2: Creating a Frame
Part 2: Creating a Frame
Creating a frame in World of Warcraft for your addon is a simple process once you do it a few times.
When developing a World of Warcraft addon, a large part of that is creating frames and using Blizzard’s API to do things in-game to make things easier, look better, etc. For the most part, a lot of this is really easy when you learn how to use libraries like Ace3
and LibDBIcon
, etc.
We will not be using these to start with so we can learn the basics of creating frames, using API, etc. This will also teach us a good deal of Lua.
Step 1: Creating the Frame
To start, let’s load up your .lua file you created in the previous tutorial. It should look something like this:
print("MyAddon successfully loaded!")
We’re going to adjust this by just adding code below it. The very first thing we’re going to do is create a frame using Blizzard’s frame template:
"BasicFrameTemplateWithInset"
Using this template provides us with a “back bone” of sorts for our frame, including a close button, title bar, background and border. All of this can be done manually for a different aesthetic if you like, but for this tutorial we’re going to use this template.
To start, let’s define our frame by making a local variable, which starts with local and is given a name following it. For the purpose of readability (which I always recommend) we’ll be starting with local mainFrame
and giving it the information as seen below:
local mainFrame = CreateFrame("Frame", "MyAddonMainFrame", UIParent, "BasicFrameTemplateWithInset")
The above code shows how Blizzard’s API handles frame creation of any sort using the CreateFrame
global function. Keep in mind, functions are case sensitive. Usually global functions, like CreateFrame
shown above, starts in uppercase and proceeds to use CamelCase for each word in the name. Local functions (functions only used in your addon/the file they are declared in) start lowercase and user camelCase for each word in the name.
For now, we are going to stick to official Blizzard API functions to create things in World of Warcraft. The above code has a few arguments that the function takes. The things inside the paranthesis are arguments.
First Argument
The CreateFrame
global function accepts 4 arguments. The first is the kind of frame CreateFrame
will create. In our case, we’re sticking with "Frame"
, but there are others like "Button", "ScrollFrame"
, etc. that can be used.
Second Argument
The second argument the function looks for is the name. This is not always needed, but in most cases, a name should be provided. We’re using "MyAddonMainFrame"
as the frame’s official name. This will make things easier to debug in-game when using /fstack
to debug our frames.
Third Argument
The third argument the function expects is the parent name. Every frame must have a parent. In our case, our addon is just coming to life and has no other parents that we’ve created, so we’re using UIParent
meaning the frame’s parent is basically the game.
Fourth and Final Argument
The fourth and final argument the function expects is the template being used. This is an optional argument and isn’t needed. Some frames don’t need templates, but in our case, we’re using one. "BasicFrameTemplateWithInset"
ensures that the frame we create will use the template named and include ease-of-use features like the close button.
Step 2: Adjusting the Frame
Now that the frame has been created, we need to adjust it to our liking. For instance, we need to set a size for the frame. The frame does not have a default size, position, etc. That’s all for us to decide. To start, we’re going to set a size using a different function called SetSize()
. SetSize()
takes two arguments, an X and Y in units. We’re making our frame 500×350 pixels in this case. The entire code so far should appear as such:
First Argument
X – How wide is the frame going to be?
Second Argument
Y – How tall is the frame going to be?
local mainFrame = CreateFrame("Frame", "MyAddonMainFrame", UIParent, "BasicFrameTemplateWithInset")
mainFrame:SetSize(500, 350)
Next, we’ll need to set the position of the frame within our client. To do this, we use the function SetPoint()
. This function expects 1 additional mandatory argument (besides itself, which is already called in SetPoint()
) but can take up to 5 additional total arguments, 4 of which are optional. For our purposes, we’re going to use all 5.
First Argument
The starting point of the region. We’re going to use "CENTER"
in all uppercase.
Second Argument
The relativeTo
argument. We need to tell this frame basically what we’re parenting it to. In our case, it’s the game, so it will still be UIParent
.
Third Argument
The relativePoint
argument expects the other region to anchor to. In our case, we’re sticking with "CENTER"
.
Fourth and Fifth Argument
These are the offset-X and offset-Y arguments. We’ll be using 0’s for our purpose, causing the frame to start directly in the center of the screen.
Your code should look as such:
local mainFrame = CreateFrame("Frame", "MyAddonMainFrame", UIParent, "BasicFrameTemplateWithInset")
mainFrame:SetSize(500, 350)
mainFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
This tells the code to make the frame 500×350 and center it directly in the middle of our screen. Now, we need to give the frame a title. There are a few ways we can do this but learning short-hand methods to do this is the best way, so we’re going to learn that.
Step 3: Title the Frame
We’re now going to title the frame by “tacking onto” our mainFrame
variable. We can do this by concatenating with .’s to extend mainFrame
. There are a few things we’ll need to add for this: TitleBg
and title
. TitleBg
is included with "BasicFrameTemplateWithInset"
by default, so we can modify its height before adding a title into it.
mainFrame.TitleBg:SetHeight(30)
In this code, we’re setting the height to the title background to 30 units. This will be enough to fit our title inside of it. Next, we’ll add the title
and define what it is, since the template does not come with one included in the frame. We can concatenate onto mainFrame
to do this.
mainFrame.title = mainFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
You’ll notice the code above is slightly different because of the function we’re calling CreateFontString
. This function expects a total of 2 additional mandatory arguments. A name for the font string, how it’s applied to its parent and, optionally, what font is being used.
We’re not done yet, though. We also need to position this title and instruct it what text we are going to show. We can do that with the below code and its arguments.
mainFrame.title = mainFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
mainFrame.title:SetPoint("TOPLEFT", mainFrame.TitleBg, "TOPLEFT", 5, -3)
mainFrame.title:SetText("MyAddon")
You’ll notice we’re attaching mainFrame.title
to TitleBg
as its parent. Using SetPoint()
and SetText()
we can position and give text to the title
.
Step 4: Save and Check
Go ahead and save your .lua file. You can login or /reload
your game if you’re already logged in to check how the window looks. You’ll notice fairly quickly that the window shows when you reload or login. We’re going to fix that really quick by appending mainFrame:Hide()
to the end of our currently written code.
Currently, your code should look like:
local mainFrame = CreateFrame("Frame", "MyAddonMainFrame", UIParent, "BasicFrameTemplateWithInset")
mainFrame:SetSize(500, 350)
mainFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
mainFrame.TitleBg:SetHeight(30)
mainFrame.title = mainFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
mainFrame.title:SetPoint("TOPLEFT", mainFrame.TitleBg, "TOPLEFT", 5, -3)
mainFrame.title:SetText("MyAddon")
mainFrame:Hide()
Finally, we’re going to add a bit of code to make the frame interactable, movable and make sound when opened and closed. This code is fairly straightforward but some of the things we use in the below code will be explained later in more detail. Go ahead and append this below block of code to your current code:
mainFrame:EnableMouse(true)
mainFrame:SetMovable(true)
mainFrame:RegisterForDrag("LeftButton")
mainFrame:SetScript("OnDragStart", function(self)
self:StartMoving()
end)
mainFrame:SetScript("OnDragStop", function(self)
self:StopMovingOrSizing()
end)
mainFrame:SetScript("OnShow", function()
PlaySound(808)
end)
mainFrame:SetScript("OnHide", function()
PlaySound(808)
end)
Just for a little bit of context, PlaySound()
, StartMoving()
and StopMovingOrSizing()
are all global functions that Blizzard’s API supports. We will dive into more API in the future, and for the most part, you won’t need to know exactly how the ones above function in order to use them.
Creating a WoW Addon – Part 3: Creating a Slash Command
Part 3: Creating a Slash Command
Now that we have a frame created with a title, have it movable, closable and making sound, we need to make it more interesting.
Right now, the frame doesn’t show because we’ve hidden it with mainFrame:Hide()
and as you can expect, we can either delete that line or show it on command with mainFrame:Show()
to get it to show again.
We’re going to handle that by creating a slash command to show it if it’s hidden, or close it if it’s shown. In the future, this will be handled by a minimap icon that we can create utilizing a library.
Step 1: The Slash Command
To tell an addon about a slash command, we’ll need to use the following code:
SLASH_MYADDON1 = "/myaddon"
SlashCmdList["MYADDON"] = function()
end
With the above code, we’re defining a few things: SLASH_
and MYADDON
in a single line, separated by _
. This is important as the below SlashCmdList
uses the same exact thing. For example:
SLASH_THISISUNIQUE1 = "/unique"
SLASH_THISISUNIQUE2 = "/uni"
SlashCmdList["THISISUNIQUE"] = function()
end
The above code utilizes THISISUNIQUE
and SlashCmdList
also includes this exact string in the quotes to know what slash commands to read. For now, we’re going to stick to using the above code in any fashion you want. Keep in mind, can have multiple slash commands. You can stick with just one for now.
Your code should look somewhat like this:
SLASH_MYADDON1 = "/myaddon"
SlashCmdList["MYADDON"] = function()
end
Step 2: The Function
Now, we need to tell the addon what to do when either of those slash commands are typed in. If a user type /myaddon
or /ma
we want the addon to show on the screen. To do that, inside of the SlashCmdList["MYADDON"] = function()
portion of the code, we will use mainFrame:Show()
.
Your code should appear as such now:
SLASH_MYADDON1 = "/myaddon"
SLASH_MYADDON2 = "/ma"
SlashCmdList["MYADDON"] = function()
mainFrame:Show()
end
Now, anytime someone uses your slash commands, the function will perform all of the actions inside function()
and end
. In this case, it’s simply showing the window.
Now, with "BasicFrameTemplateWithInset"
that we used earlier, we have a close button. When clicked, the template already knows the call the Hide()
function, as that’s included in the template already. However, it is good practice to code for all possibilities, and our function above should also close the window if the slash command is typed and the window is opened.
How can we do that?
We need our function to ask a question to answer this.
Is the frame showing or not?
We can ask this question in Lua by using an if then, else
statement. Inside of the function, we’re going to delete mainFrame:Show()
and instead replace it with the below code:
if mainFrame:IsShown() then
mainFrame:Hide()
else
mainFrame:Show()
end
This code essentially asks, in Lua:
If the main frame of the addon is currently showing, then we should hide it, or else we should show it.
We know when the game loads, the frame is not going to be showing. The code will show it and mark the frame as shown in the background somewhere. The next time we run the slash command, it will know it’s showing and hide it instead. Go ahead and try this now without using the close button.
Step 3: Making Our Frame Important
Now, sometimes we want to close our frame with ‘Escape’ on our keyboard. It’s essential that your addon’s main starting frame does this. We can tell the game to do this by simply adding your frame to the list of “special” frames in World of Warcraft. To do this, we can use one line of code appended to the end of our existing code:
table.insert(UISpecialFrames, "MyAddonMainFrame")
In the above line of code, we’re inserting our mainFrame
(which is named "MyAddonMainFrame"
, or whatever name you gave it during Part 2) into a table called UISpecialFrames
, which Blizzard creates for our interface automatically.
You can kind of think of this table as such:
UISpecialFrames = { "CharacterScreen", "FriendsScreen", "MyAddonMainFrame" }
We know the friends panel, character panel, etc. all close when you press the ‘Escape’ key, so this is just adding to the list of frames that are special and should be closed when pressing the ‘Escape’ key.
And with that, Part 3 has been completed. The code block we implemented should be as such:
SLASH_MYADDON1 = "/myaddon"
SlashCmdList["MYADDON"] = function()
if mainFrame:IsShown() then
mainFrame:Hide()
else
mainFrame:Show()
end
end
table.insert(UISpecialFrames, "MyAddonMainFrame")
Creating a WoW Addon – Part 4: The Addon Does Stuff?
Step 4: The Addon Does Stuff?
Now that we have our addon working entirely as expected, let’s get it doing some “useful” things.
Step 1: Display the Character Name
The first thing we want the addon to do is print the character name at the top of the mainFrame
screen we’ve created.
This is going to be done using official Blizzard API for getting names in-game. We’re going to again short-hand this off mainFrame
to achieve a simple result. The code will use UnitName()
API from Blizzard to get the UnitId
name.
How does it know what unit ID to get the name of?
Well, there are several UnitIds which you can pass to this function. The one we’re interested in right now is “player” specifically. You can find a list of applicable unitIds to pass to this function on the Warcraft Wiki.
Let’s code this as such:
mainFrame.playerName = mainFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
mainFrame.playerName:SetPoint("TOPLEFT", mainFrame, "TOPLEFT", 15, -35)
mainFrame.playerName:SetText("Character: " .. UnitName("player"))
We can put this block of code anywhere we want really, but we need to make sure it’s below our initial statement of local mainFrame
and good practice is to keep it close to all of our other mainFrame
code.
The mainFrame we’ve created will now display the current character’s name at the top of the addon on the left side. It will also have a nice padding of about 10 pixels/units from the top and the left.
The character name is affixed to this position, meaning when the window moves the text will move as well. We’ve set the SetPoint()
to the mainFrame
parent to allow this. It’s how everything inside of a window gets “stuck” to its position.
For the playerName
variable of mainFrame
we are using "Character: " .. UnitName("player")
. For example, a person playing on their character named Noxxious will have this printed to MyAddon:
Character: Noxxious
We can go a step further and also print their level in parenthesis next to it. You’ve probably guessed how we can do this. Take a look at the below code:
mainFrame.playerName:SetText("Character: " .. UnitName("player") .. " (Level " .. UnitLevel("player") .. ")")
We should now see something like this in the addon, depending on your character of course:
Character: Noxxious (Level 50)
Using the above code, we added the players level to the string. To concatenate strings together, between quotes, .. is used. To concatenate booleans (true or false values) to strings, you can use the tostring
function:
local trueOrFalse = true
print("trueOrFalse is " .. tostring(trueOrFalse))
or
local trueOrFalse = tostring(true)
print("trueOrFalse is " .. trueOrFalse)
Using the above code can be very useful in debugging code but will give errors if you don’t turn the boolean into a string before printing it.
Step 2: Variables, Tables and Saving Them, Oh My!
Moving on, we need to actually define some variables and tables for our addon, and modify our .toc file, to save those variables and tables even after the character logs out.
To start, we’re going to make a saved variable in our .toc file under the ## SavedVariablesPerCharacter
section. We’re going to open our .toc file and make the below adjustment:
## SavedVariablesPerCharacter: MyAddonDB
What this does is tell our addon that anything stored under the MyAddonDB
variable will persist even after a character logs out. We will be able to write and read from this table.
Without adding this to our SavedVariablesPerCharacter
section of the .toc file, each reload or re-log would erase this information. If we want all the data we store to save across ALL characters, we can instead move MyAddonDB
to ## SavedVariables
instead. For the purpose of this addon, the data we store is going to be per-character, so we’ll keep it as is. Your .toc file code should now read something like (using my tag-along addon names as example):
## Interface: 11502
## Title: NoxxAddon
## Notes: An addon that does nothing yet.
## Author: Noxxious
## Version: 1.0.0
## RequiredDeps:
## X-Curse-Project-ID: 99999
## SavedVariables:
## SavedVariablesPerCharacter: MyAddonDB
NoxxAddon.lua
So now that we have told our addon to save anything "MyAddonDB"
forever, we’ll need to define it in our addon now.
Let’s open our addon’s .lua file again. At the top, we’re going to define this variable, but make it blank to start. We do this by using Lua to make a statement:
If this variable does not exist already, please create it, and it should be blank.
We do this by using the following code at the top of all of our code, as it needs defined before it can be used by any code below it:
if not MyAddonDB then
MyAddonDB = {}
end
-- local mainFrame code is here...
This sets us up to use the MyAddonDB
variable without any errors or conflicts, as we’re essentially defining it the same way we would by using the below code to initialize the table each time the player logs in:
local MyAddonDB = {}
But, we won’t be doing it the above way because we want the variable to persist across game sessions.
The {}
symbols indicate that we won’t be storing a number or a string, we’ll instead be storing a table of data. Tables in Lua are always between curly braces. Here is an example of how our table might look if we were to directly write it instead of having functions write it for us in the future:
local MyAddonDB = {
totalKills = 16,
totalLootedGold = 10,
totalLootedSilver = 56,
totalLootedCopper = 12,
}
If we wanted to check how many total kills a player has in the database, we could do so via calling MyAddonDB.totalKills
as such:
print("MyAddonDB currently has " .. tostring(MyAddonDB.totalKills) .. " kills.")
-- Prints: "MyAddonDB currently has 16 kills."
The difference between if not MyAddonDB then
and MyAddonDB = {}
is the overwrite factor. If we use the second one, each time a player logs in, the table will be reset to blank. The first one checks if the table exists or not yet, and if it does, leave it alone. We again use the english-form statement below and translate it to Lua:
If this variable does not exist already, please create it…
Now, it’s time to use some different types of Blizzard API and functions to start recording information about events and actions the player takes!
Step 3: Code Review
Before we proceed, your code should appear as such in its entirety:
Your .toc file
## Interface: 11502
## Title: MyAddon
## Notes: A description here.
## Author: Your Name
## Version: 1.0.0
## RequiredDeps:
## X-Curse-Project-ID: 99999
## SavedVariables:
## SavedVariablesPerCharacter: MyAddonDB
MyAddon.lua
Your .lua file
if not MyAddonDB then
MyAddonDB = {}
end
local mainFrame = CreateFrame("Frame", "MyAddonMainFrame", UIParent, "BasicFrameTemplateWithInset")
mainFrame:SetSize(500, 350)
mainFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
mainFrame.TitleBg:SetHeight(30)
mainFrame.title = mainFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
mainFrame.title:SetPoint("TOPLEFT", mainFrame.TitleBg, "TOPLEFT", 5, -3)
mainFrame.title:SetText("MyAddon")
mainFrame:Hide()
mainFrame:EnableMouse(true)
mainFrame:SetMovable(true)
mainFrame:RegisterForDrag("LeftButton")
mainFrame:SetScript("OnDragStart", function(self)
self:StartMoving()
end)
mainFrame:SetScript("OnDragStop", function(self)
self:StopMovingOrSizing()
end)
mainFrame:SetScript("OnShow", function()
PlaySound(808)
end)
mainFrame:SetScript("OnHide", function()
PlaySound(808)
end)
mainFrame.playerName = mainFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
mainFrame.playerName:SetPoint("TOPLEFT", mainFrame, "TOPLEFT", 15, -35)
mainFrame.playerName:SetText("Character: " .. UnitName("player") .. " (Level " .. UnitLevel("player") .. ")")
SLASH_MYADDON1 = "/myaddon"
SlashCmdList["MYADDON"] = function()
if mainFrame:IsShown() then
mainFrame:Hide()
else
mainFrame:Show()
end
end
table.insert(UISpecialFrames, "MyAddonMainFrame")
Really quick here: Some of the code above, such as UnitName
and UnitLevel
should typically always check for nil
before attempting to print or do anything with them. This can save you from running into errors with your code. In the worst-case scenario, addon errors could crash someone’s game during a critical moment, which is never good.
We will cover checking for nil
in a bit. For now, the above code is considered safe because the player has to be present to use the addon in the first place.
Creating a WoW Addon – Part 5: More Functions & Other Lua
Part 5: More Functions & Other Lua
Congratulations on making it this far! We’re probably around 30% or so done with the addon. Let’s keep going!
We’re getting pretty far. So far, our addon opens with a command, makes sound, prints the character’s name to the top of the addon and we also have a saved variable named MyAddonDB
that’s blank and ready for writing to!
Step 1: Understanding Player Regen Events
Our first step is to call an event to “listen” for whenever the player exits combat. These are the "PLAYER_REGEN_ENABLED"
and "PLAYER_REGEN_DISABLED"
events.
When the player has regeneration enabled, this means they are out of combat and they are able to regenerate health again. This is a good spot to view the combat log.
The way that code works during these events is by listening for them to “fire.” When a player logs in, they are normally not in combat, so the event wont fire. But, when a player enters combat and then exits combat, the "PLAYER_REGEN_ENABLED"
event fires once they leave combat, and all code you have for that event will execute once, until the player leaves combat again.
A lot of World of Warcraft addons use multiple functions to create something unique. Take my addon NoxxLFG for example. It reads messages, but also reads the name of player who posts it, their class, the time they post it, the roles they are looking for, the dungeon/raid and sub dungeon/raid they post, etc. All of this information is gathered using different functions and methods, with a combined end result.
So, how are we going to “listen” to these events to get information?
We are going to use multiple functions to read and record information to our MyAddonDB
variable. Blizzard gives us a vast list of global API we can use in our addon to get information that normally doesn’t appear on our screens. This is not only very helpful, but is the back bone to every single World of Warcraft addon ever created.
Okay, enough blabbering, let’s get into some juicy code.
Step 2: Using Player Regen Events
To use a player regen event, we now need an invisible frame that will listen to events for us. We need to register this frame with an event listener so it knows its purpose is to listen to events. We do this by using the following code:
I. Create the Frame
This frame is invisible, unlike the frame we created earlier. We will not define a template, size, position, etc. A frame is needed anytime we want to listen to an event.
local eventListenerFrame = CreateFrame("Frame", "MyAddonEventListenerFrame", UIParent)
II. Create a Function for the Event
This function will be called any time the event of "PLAYER_REGEN_ENABLED"
fires.
local function eventHandler(self, event, ...)
if event == "PLAYER_REGEN_ENABLED" then
end
end
III. Register the Frame to Listen to Events
The "PLAYER_REGEN_ENABLED"
event will not fire without first registering the event with the frame.
eventListenerFrame:SetScript("OnEvent", eventHandler)
eventListenerFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
Now that we have the eventListenerFrame
registered with RegisterEvent
, it will now listen for that event to fire in-game. When it does fire, it will call the eventHandler
function from the SetScript("OnEvent")
function.
Keep in mind, code reads from top to bottom. For eventListenerFrame:SetScript("OnEvent", eventHandler)
to call the eventHandler
function, eventHandler
needs to be written above wherever it’s called. The entire code block should read as follows:
local eventListenerFrame = CreateFrame("Frame", "MyAddonEventListenerFrame", UIParent)
local function eventHandler(self, event, ...)
if event == "PLAYER_REGEN_ENABLED" then
end
end
eventListenerFrame:SetScript("OnEvent", eventHandler)
eventListenerFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
Notice we have nothing for the function to do between if event == "PLAYER_REGEN_ENABLED" then
and end
– we will now write that function. First, we’ll need to debug to make sure we’re doing the correct thing, and that the event listening actually works.
For now, let’s put a print statement in here to test it:
print("Debugging: You have exited combat!")
The eventHandler function should now read as follows:
local function eventHandler(self, event, ...)
Β Β Β if event == "PLAYER_REGEN_ENABLED" then
Β Β Β Β Β Β Β print("Debugging: You have exited combat!")
Β Β Β end
end
Step 3: Debug and Test
Now, when the event fires, you should see a message print to the chat box. You should see this message each time your character leaves combat.
Log in or /reload
your game and go kill some things to test this out! Critters, NPCs; whatever you can get your hands on.
If we can confirm this is working at this point, we’re ready to move onto some Blizzard API to read some information from the combat log!
Creating a WoW Addon – Part 6: Parsing Data
Part 6: Parsing Data Using Variables and Functions
Difficulty: π‘π‘ Medium
Now that our print statement is sending messages to the chat window when we engage with combat, we can replace the print statement with variables, functions and if statements to do the things we want to do: record NPC deaths
Step 1: Defining Local Function Variables
Let’s start by defining some variables inside of our eventHandler function to hold or define some information we will be using.
To start, we are going to define a few variables just to make writing and reading our function easier.
1. Variables that parse the most recent combat log event
local _, eventType = CombatLogGetCurrentEventInfo()
According to the WoW API documentation, using CombatLogGetCurrentEventInfo()
will always return the current combat event information if we use it during the "COMBAT_LOG_EVENT_UNFILTERED"
event.
The above code is defining a variable of eventType
(which we can name anything) using the returned data from the function CombatLogGetCurrentEventInfo()
.
CombatLogGetCurrentEventInfo()
actually returns 11-13 pieces of information when called, but we only have use out of the first 2 returned pieces of information in this context. And, out of those first 2, we really only need 1 of them (the first piece of data returned is timestamp data, which we won’t be using).
So, we can mark the unneeded timestamp variable as _
instead of giving it a name, because it will go unused and doesn’t need to take up space in our limited local variable limit (200 in Lua).
Let’s use the print function to try and print the information from the eventType
variable.
print(eventType)
If we were to print this code in-game with our addon, it should print the sub event of our combat log entry… or we could get an error.
Sometimes our code can try to access information that does not exist. Functions or variables will usually return nil
if it cannot find the requested information, or something went wrong when trying to retrieve information.
Step 2: Always Check for nil
A type of 'nil'
is returned anytime information is not available and can cause loads of errors if not dealt with properly.
Safe code is code that will always return a value. In the case of eventType
, there is a chance it may return nil
information instead of actual information. There are many reasons for this happening and most of the time it’s hard to know everything that can cause a nil
error.
Instead of trying to find every possible scenario where we may run into a return of nil
on eventType
, we will instead write our code to handle nil
and if our eventType
is not nil
, we can write our code to handle that as well.
The correct way we want to check this type of “unsafe” code is to check for nil first. We can do this by using the below code:
if eventType ~= nil then
print(eventType)
end
However, we can shorten this a bit by removing the ~= nil
part. Using just if eventType
basically asks if eventType
is not nil
. See below:
if eventType then
print(eventType)
else
print("No data found!")
end
In english, or “pseudocode”, the above code states:
If eventType
has data associated with it, let’s go ahead and print that data, otherwise, let me know that it does not.
If we actually put the above code into the game, every sub event during combat should print its type. At the end of our combat, it should print a sub type of "PARTY_KILL"
. This is the event type we want our function to listen to.
Now that we have this information gathered from the CombatLogGetCurrentEventInfo()
function and stored in variables called _
(unused), and eventType
, we can move on and use this to start recording our kills to our MyAddonDB
database.
Our code should currently read as follows:
local eventListenerFrame = CreateFrame("Frame", "MyAddonEventListenerFrame", UIParent)
local function eventHandler(self, event, ...)
local _, eventType = CombatLogGetCurrentEventInfo()
if event == "COMBAT_LOG_EVENT_UNFILTERED" then
if eventType then
print(eventType)
else
print("No data found!")
end
end
end
eventListenerFrame:SetScript("OnEvent", eventHandler)
eventListenerFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
This code will get pretty annoying after a while because of the constant printing to the chat box while in combat, so let’s move on and make it less annoying and do what we want it to do.
Creating a WoW Addon – Part 7: Writing to Our Database
Part 7: Writing to our Database
Difficulty: π‘π‘ Medium
We’re going to be writing to our database using the information given. Not only will it track the kills you get, but also any kills you log while in a party. This will be a long-ish one (but semi-easy), so buckle up!
Step 1: Filtering Our eventType Variable
First, we’ll be filtering out eventType
so we aren’t printing so much to the combat log. If you did print the eventType
to the chat box, I’m sure you saw a few events print. The one we’re looking for specifically is "PARTY_KILL"
, letting us know the mob has died from us, or someone in our party.
How are we going to modify the code to only work with "PARTY_KILL"
sub events?
Well, let’s use the following code:
In our eventHandler function, we’re going to insert this line of code inside of our event check code:
if eventType and eventType == "PARTY_KILL" then
end
This code states:
If eventType
is not nil
and eventType
is equal to "PARTY_KILL"
, then we can proceed.
Both of the requirements in the if
statement need to be true for the code to proceed.
We’re making sure that eventType
is not nil
and that eventType
is equal to "PARTY_KILL"
as well.
If these are both true then we need to debug by printing another nice message. Let’s modify our above code to this:
if eventType and eventType == "PARTY_KILL" then
print("An enemy has been successfully defeated!")
end
Nice, now let’s head into the game and kill something to check if this works. If you get this message after killing an enemy, we can move on!
Step 2: Finally, Let’s Write to the Database
Since we have the message printing to our chat box when we kill something, we’re going to modify our code to write to the MyAddonDB
database we created in our previous code. Since we’re not actually logging what monster was killed, we can modify the code we have slightly to work for us in this context.
To do this, we’re going to need to handle two things:
- Does
MyAddonDB.kills
exist yet? If not, write it and make it equal 1. - If it does exist, get the value of MyAddonDB.kills and add 1 to it.
To do this, let’s write the first part of our code below:
if eventType and eventType == "PARTY_KILL" then
if not MyAddonDB.kills then
MyAddonDB.kills = 1
end
end
This is the first bullet point that our code needs to handle.
If MyAddonDB.kills
is nil
, then we need to create it.
You’ll notice we used if not
, essentially meaning if there isn’t MyAddonDB.kills
. We could also use if MyAddonDB.kills == nil then
, but the above short-hand method is quicker to write and clearer to read. Remember, we always want to check for nil
before attempting to modify a piece of data or access a piece of data.
Now how do we handle the second bullet point of our code handling? Well, this is where the else
portion of an if then (elseif) else
statement comes in to play.
Let’s modify our code to this now:
if eventType and eventType == "PARTY_KILL" then
if not MyAddonDB.kills then
MyAddonDB.kills = 1
else
MyAddonDB.kills = MyAddonDB.kills + 1
end
end
This is basically stating (in english):
If MyAddonDB.kills
does not exist, let’s make it and set it equal to 1. If it does exist, we’re going to get the amount that MyAddonDB.kills
has stored already and add 1 to it.
You might ask, why not just initialize MyAddonDB.kills
with 0 before ever accessing it? Great question, and the way we’ve handled the nil
check in our addon interface basically takes care of this. You can do this either way you prefer though.
Now, have this information storing in our database. To debug this and make sure it’s actually working, we’re going to use an addon I mentioned in Part 1 of this tutorial called DevTools to view our database.
If you don’t have the addon, go ahead and download it and install it using Curseforge. Once installed, login or /reload
your game and type /dev
in-game to open it.
Once opened, in the bottom left text field, we’re going to type MyAddonDB
and press the ‘Enter’ key. Our database should show in the top left-hand window of the DevTools addon.
Go ahead and click on MyAddonDB
to open it. In the right-hand window, we should see our MyAddonDB
table with (1) entry showing. If it says (0), we need to first kill a creature.
We can click on this table to view its content. It should show an entry called kills and that entry should be equal to the amount of kills we have recorded.
It goes without saying that we don’t want our players checking DevTools to see how many kills they have. We need to implement this within our addon’s mainFrame
window so they can open our addon to view these stats. Let’s go ahead and handle that.
Step 3: Make Our Data Available in the Addon
In our mainFrame
of the addon, we have our playerName
variable showing the character name and level. Let’s put some additional information below this showing the player’s total kills.
Same as before, let’s make a new variable called mainFrame.totalPlayerKills
and set it to our database value. However, we will need an if statement here to make sure the data is not nil before we display it.
We can do an in-line if
statement by simply putting ()
‘s and an or
. This is short-hand if-else that we’ll explain a little bit about below.
mainFrame.totalPlayerKills = mainFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
mainFrame.totalPlayerKills:SetPoint("TOPLEFT", mainFrame.playerName, "BOTTOMLEFT", 0, -10)
mainFrame.totalPlayerKills:SetText("Total Kills: " .. (MyAddonDB.kills or "0"))
As a quick side note, VSCode may display some orange/yellow lines below our totalPlayerKills
and playerName
variables. You can ignore these.
Now that we have our totalPlayerKills
showing, let’s explain it a bit… specifically this line: mainFrame.totalPlayerKills:SetText("Total Kills: " .. (MyAddonDB.kills or "0"))
This line is going to always display “Total Kills: ” and the number of kills we have. If MyAddonDB.kills
does not exist, or returns nil
, it will instead write “0” afterward instead of giving us an error.
If we just wrote mainFrame.totalPlayerKills:SetText("Total Kills: " .. MyAddonDB.kills)
we would throw an error for any new players that install our addon because they may attempt to open our addon before they kill a creature. We handle this by the “0” portion of the code that states:
If MyAddonDB.kills
is nil
then we’ll resort to showing “0” instead of MyAddonDB.kills
and ultimately throwing an error.
We have set the point of our “Total Kills: ” line by parenting it to our playerName
variable, and using the offset-Y parameter in SetPoint()
. It will always be 10 units under our playerName
string of the addon.
Now, we’re not entirely done. You will notice that if we kill an enemy, our addon will not update our kill count that it shows. This is because font strings are not inherently dynamic. We can make this “appear” dynamic using the "OnShow"
function event. Let’s do that now.
Step 4: Updating Text When Opening the Addon
There are a few ways to do this, but our addon is light-weight, meaning there isn’t much memory being used by it and it’s not going to hinder performance by updating a font string, or even fifty, by opening the addon.
What we’re going to do is modify the mainFrame:SetScript("OnShow", function())
we already have:
Let’s make it say the below instead:
mainFrame:SetScript("OnShow", function()
PlaySound(808)
mainFrame.totalPlayerKills:SetText("Total Kills: " .. (MyAddonDB.kills or "0"))
end)
Importantly, we also need to move this function to be below our mainFrame.totalPlayerKills
variable.
This tells our addon the following (in english):
Any time we open/show the mainFrame
of our addon, we want to set our totalPlayerKills
variable to equal MyAddonDB.kills
or 0 if we have none.
There are many ways to handle this, but in our situation, this is the best method to handle this. (We will talk later about niche cases like a player killing a monster while the window is showing/open. This is considered a quality of life improvement and isn’t needed right away, but we will implement it.)
Step 5: Code Review
And… we’re done with player kills showing in our addon! Give yourself a pat on the back! You’ve made it very far and we should actually having a functioning addon! Before we proceed, our code should appear as such so far:
if not MyAddonDB then
MyAddonDB = {}
end
local mainFrame = CreateFrame("Frame", "MyAddonMainFrame", UIParent, "BasicFrameTemplateWithInset")
mainFrame:SetSize(500, 350)
mainFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
mainFrame.TitleBg:SetHeight(30)
mainFrame.title = mainFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
mainFrame.title:SetPoint("TOPLEFT", mainFrame.TitleBg, "TOPLEFT", 5, -3)
mainFrame.title:SetText("MyAddon")
mainFrame:Hide()
mainFrame:EnableMouse(true)
mainFrame:SetMovable(true)
mainFrame:RegisterForDrag("LeftButton")
mainFrame:SetScript("OnDragStart", function(self)
self:StartMoving()
end)
mainFrame:SetScript("OnDragStop", function(self)
self:StopMovingOrSizing()
end)
mainFrame:SetScript("OnHide", function()
PlaySound(808)
end)
mainFrame.playerName = mainFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
mainFrame.playerName:SetPoint("TOPLEFT", mainFrame, "TOPLEFT", 15, -35)
mainFrame.playerName:SetText("Character: " .. UnitName("player") .. " (Level " .. UnitLevel("player") .. ")")
mainFrame.totalPlayerKills = mainFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
mainFrame.totalPlayerKills:SetPoint("TOPLEFT", mainFrame.playerName, "BOTTOMLEFT", 0, -10)
mainFrame.totalPlayerKills:SetText("Total Kills: " .. (MyAddonDB.kills or "0"))
mainFrame:SetScript("OnShow", function()
PlaySound(808)
mainFrame.totalPlayerKills:SetText("Total Kills: " .. (MyAddonDB.kills or "0"))
end)
SLASH_MYADDON1 = "/myaddon"
SlashCmdList["MYADDON"] = function()
if mainFrame:IsShown() then
mainFrame:Hide()
else
mainFrame:Show()
end
end
table.insert(UISpecialFrames, "MyAddonMainFrame")
local eventListenerFrame = CreateFrame("Frame", "MyAddonEventListenerFrame", UIParent)
local function eventHandler(self, event, ...)
local _, eventType = CombatLogGetCurrentEventInfo()
if event == "COMBAT_LOG_EVENT_UNFILTERED" then
if eventType and eventType == "PARTY_KILL" then
if not MyAddonDB.kills then
MyAddonDB.kills = 1
else
MyAddonDB.kills = MyAddonDB.kills + 1
end
end
end
end
eventListenerFrame:SetScript("OnEvent", eventHandler)
eventListenerFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
Creating a WoW Addon – Part 8: Counting Currency
Part 8: Counting Currency
Difficulty: π π π Medium
Now, we’re going to get into some juicy stuff. Let’s see what’s in store for this part.
We’ll be doing the following:
- Reading Chat Messages
- Utilizing pattern matching to find Gold, Silver and Copper amounts looted.
This will be a larger part of the tutorial as we will be explaining quite a bit of the code and exactly what it’s doing. Let’s not make this any longer and get onto some code!
As a side note, you don’t need to understand everything that’s going on here. Matching strings to patterns would normally be considered fairly advanced code and these are things you can learn from documentation or future projects. Heck, you can just ask AI how to pattern match a string for what you’re looking for and it can teach you a fairly good amount about how patterns work in any language.
Step 1: Registering a New Event
For this portion of our addon to function, we will need to first register a new event that our addon is going to listen for.
We already have our eventListenerFrame created, and currently it’s registered to the COMBAT_LOG_EVENT_UNFILTERED event. Now, we’ll need to register it to another event called “CHAT_MSG_MONEY”.
This event, when fired, means that a player has looted money (gold, silver, copper), and that money is displayed as a message to the player. The great thing about using “CHAT_MSG_MONEY” is that even if the player has these messages hidden in the chat log, our addon can still read them.
Let’s register this by finding our eventListenerFrame:RegisterEvent line and add a new one below it.
Ctrl + F and searching for eventListenerFrame is a fast way to find this line. Ctrl + F is a fantastic way to search your code for something you need.
eventListenerFrame:RegisterEvent(“CHAT_MSG_MONEY”)
Now that we have that registered, we can modify our eventHandler to handle this event.
if event == “COMBAT_LOG_EVENT_UNFILTERED” then
if eventType == “PARTY_KILL” then
if not MyAddonDB.kills then
MyAddonDB.kills = 1
else
MyAddonDB.kills = MyAddonDB.kills + 1
end
end
elseif event == “CHAT_MSG_MONEY” then
— Our code for this event will go here.
end
Above, we changed the following code from:
if event == “COMBAT_LOG_EVENT_UNFILTERED” then
— Our code for this event is here…
end
to:
if event == “COMBAT_LOG_EVENT_UNFILTERED” then
— Our code for this event is here…
elseif event == “CHAT_MSG_MONEY” then
— Our code for this event is here…
end
With this modified code, when someone loots something, it fires an event. If that event is “CHAT_MSG_MONEY” then our code inside of that event in the function will execute. Currently, it does nothing.
Let’s move on to doing something with that code instead of the nothing that it’s doing now.
Step 2: Using the Chat Message
To use this information, we will now append some code to our if else statement. We’ll be using string matching to check if the chat message contains Gold, Silver, or Copper.
Inside of our if else statement, let’s add this code:
local msg = …
local gold = tonumber(string.match(msg, “(%d+) Gold”)) or 0
local silver = tonumber(string.match(msg, “(%d+) Silver”)) or 0
local copper = tonumber(string.match(msg, “(%d+) Copper”)) or 0
The msg variable assigns … to it. … is pulling the only payload information from “CHAT_MSG_MONEY” that it provides, which is a string that says something along the lines of:
You loot 42 Copper.
With this information, our gold, silver and copper variables are searching the msg string for a pattern. The (%d+) portion of our parse is looking for any one or more digits (d+) information before ” Gold”, ” Silver” or ” Copper” and converts it to a number. Thankfully, the message you receive when looting money in-game is always followed by ‘ Copper’, ‘ Silver’ or ‘ Gold’. This makes this pattern work flawlessly for us forever unless Blizzard changes this message in the future.
Step 3: Making Our Variables Work
We’re now going to store this information to our database. Here’s how we can do this:
MyAddonDB.gold = (MyAddonDB.gold or 0) + gold
The above code stores any looted gold into our MyAddonDB.gold database. This is short-hand code that checks to make sure MyAddonDB.gold exists, or makes it 0, before writing to it. If it doesn’t exist, it will create it and make its value 0 + gold upon creation.
We can simply copy and paste the above code for silver and copper as well.
MyAddonDB.silver = (MyAddonDB.silver or 0) + silver
MyAddonDB.copper = (MyAddonDB.copper or 0) + copper
These strings we’re parsing are case-sensitive in our case, hence the capitals in Gold, Silver and Copper. There is a way to ignore case sensitivity when searching for patterns, and if you’d like to do this, you’re more than welcome to. Consider it a bonus project!
Step 4: Handling >= 100 for Our Currencies
We want to convert our copper and silver gained to their respective higher currencies, if they go above 100. To do this, we’re going to start with copper, as it’s the lowest currency value World of Warcraft has.
if MyAddonDB.copper >= 100 then
MyAddonDB.silver = MyAddonDB.silver + math.floor(MyAddonDB.copper / 100)
MyAddonDB.copper = MyAddonDB.copper % 100
end
The above code checks for copper. If it exists and is equal to or over 100.
It then converts the copper to silver, dividing the copper by 100 in whole number form (using math.floor).
Let’s duplicate this code for silver to convert to gold.
if MyAddonDB.copper >= 100 then
MyAddonDB.silver = MyAddonDB.silver + math.floor(MyAddonDB.copper / 100)
MyAddonDB.copper = MyAddonDB.copper % 100
end
if MyAddonDB.silver >= 100 then
MyAddonDB.gold = MyAddonDB.gold + math.floor(MyAddonDB.silver / 100)
MyAddonDB.silver = MyAddonDB.silver % 100
end
The entire above code will now write gold, silver and copper information to our database and convert any amount >= 100.
Creating a WoW Addon – Part 9: Viewing The Fruit of Our Labors
Part 9: Viewing The Fruit of Our Labors
Difficulty: π’ Easy
Now, we can show the fruit of our labors to the players- or… just us for now. Let’s move on!
Step 1: Adding a mainFrame.totalCurrency property
First, we’ll need to do exactly as the step above states. Find mainFrame.totalPlayerKills and add mainFrame.totalCurrency underneath that block of code.
If you’d like to try and do it yourself first, go ahead. I’ll put the below code within Spoiler tags just in case. Don’t be ashamed to open them!
mainFrame.totalCurrency = mainFrame:CreateFontString(nil, “OVERLAY”, “GameFontNormal”)
mainFrame.totalCurrency:SetPoint(“TOPLEFT”, mainFrame.totalPlayerKills, “BOTTOMLEFT”, 0, -10)
mainFrame.totalCurrency:SetText(“Gold: ” .. (MyAddonDB.gold or “0”) .. ” Silver: ” .. (MyAddonDB.silver or “0”) .. ” Copper: ” .. (MyAddonDB.copper or “0”))
And that’s it! We covered nil and everything! Go ahead and log in or /reload your game and let’s check out how it works!
There is one more thing we will do later, which is update this to look nicer. Stick around for that!
Step 2: Oh Wait, We Forgot Something…
We didn’t account for the Gold, Silver and Copper fields changing after we loot things! No worries, we can adjust this in our mainFrame:SetScript(“OnShow”, function()) again!
Let’s add the below code to this block of existing code:
mainFrame.totalCurrency:SetText(“Gold: ” .. (MyAddonDB.gold or “0”) .. ” Silver: ” .. (MyAddonDB.silver or “0”) .. ” Copper: ” .. (MyAddonDB.copper or “0”))
And that’s it! Or is it?
Let’s handle a “sorta-edge-casey” scenario while we have it on our minds.
What if these players kill or loot things with the addon open?
Luckily we’ve structured our code well, so either of the functions we use to perform the addition of these things to our database is under our FontStrings that we’ve created to show it in the addon. So, the way we perform this is fairly straightforward!
Inside of our eventHandler function, at the bottom outside of all if then else statements, we’re going to add this bit of code:
if mainFrame:IsShown() then
mainFrame.totalPlayerKills:SetText(“Total Kills: ” .. (MyAddonDB.kills or “0”))
mainFrame.totalCurrency:SetText(“Gold: ” .. (MyAddonDB.gold or “0”) .. ” Silver: ” .. (MyAddonDB.silver or “0”) .. ” Copper: ” .. (MyAddonDB.copper or “0”))
end
So, anytime an event that is registered takes place, if mainFrame is shown/open at that time, our FontString properties will update! Now you may be asking the question:
Can’t we just get rid of the “OnShow” mainFrame function and use this instead without if mainFrame:IsShown()?
or
Can’t we just get rid of the SetText()’s earlier in the file and just use the “OnShow” function?
Well… yes! We could! Either way you want to do this is up to you! In this specific case, neither one is a worse option and should function exactly the same! when you have an addon that gets bigger and the focus is on performance, then I would choose the second route of setting the SetText() functions “OnShow”.
Step 3: Code Review
Before we head into our Settings interface development, let’s review the entirety of our .lua code file below (our .toc file has not changed):
if not MyAddonDB then
MyAddonDB = {}
end
local mainFrame = CreateFrame(“Frame”, “MyAddonMainFrame”, UIParent, “BasicFrameTemplateWithInset”)
mainFrame:SetSize(500, 350)
mainFrame:SetPoint(“CENTER”, UIParent, “CENTER”, 0, 0)
mainFrame.TitleBg:SetHeight(30)
mainFrame.title = mainFrame:CreateFontString(nil, “OVERLAY”, “GameFontHighlight”)
mainFrame.title:SetPoint(“TOPLEFT”, mainFrame.TitleBg, “TOPLEFT”, 5, -3)
mainFrame.title:SetText(“MyAddon”)
mainFrame:Hide()
mainFrame:EnableMouse(true)
mainFrame:SetMovable(true)
mainFrame:RegisterForDrag(“LeftButton”)
mainFrame:SetScript(“OnDragStart”, function(self)
self:StartMoving()
end)
mainFrame:SetScript(“OnDragStop”, function(self)
self:StopMovingOrSizing()
end)
mainFrame:SetScript(“OnHide”, function()
PlaySound(808)
end)
mainFrame.playerName = mainFrame:CreateFontString(nil, “OVERLAY”, “GameFontNormal”)
mainFrame.playerName:SetPoint(“TOPLEFT”, mainFrame, “TOPLEFT”, 15, -35)
mainFrame.playerName:SetText(“Character: ” .. UnitName(“player”) .. ” (Level ” .. UnitLevel(“player”) .. “)”)
mainFrame.totalPlayerKills = mainFrame:CreateFontString(nil, “OVERLAY”, “GameFontNormal”)
mainFrame.totalPlayerKills:SetPoint(“TOPLEFT”, mainFrame.playerName, “BOTTOMLEFT”, 0, -10)
mainFrame.totalPlayerKills:SetText(“Total Kills: ” .. (MyAddonDB.kills or “0”))
mainFrame.totalCurrency = mainFrame:CreateFontString(nil, “OVERLAY”, “GameFontNormal”)
mainFrame.totalCurrency:SetPoint(“TOPLEFT”, mainFrame.totalPlayerKills, “BOTTOMLEFT”, 0, -10)
mainFrame.totalCurrency:SetText(“Gold: ” .. (MyAddonDB.gold or “0”) .. ” Silver: ” .. (MyAddonDB.silver or “0”) .. ” Copper: ” .. (MyAddonDB.copper or “0”))
mainFrame:SetScript(“OnShow”, function()
PlaySound(808)
mainFrame.totalPlayerKills:SetText(“Total Kills: ” .. (MyAddonDB.kills or “0”))
mainFrame.totalCurrency:SetText(“Gold: ” .. (MyAddonDB.gold or “0”) .. ” Silver: ” .. (MyAddonDB.silver or “0”) .. ” Copper: ” .. (MyAddonDB.copper or “0”))
end)
SLASH_MYADDON1 = “/myaddon”
SlashCmdList[“MYADDON”] = function()
if mainFrame:IsShown() then
mainFrame:Hide()
else
mainFrame:Show()
end
end
table.insert(UISpecialFrames, “MyAddonMainFrame”)
local eventListenerFrame = CreateFrame(“Frame”, “MyAddonEventListenerFrame”, UIParent)
local function eventHandler(self, event, …)
local _, eventType = CombatLogGetCurrentEventInfo()
if event == “COMBAT_LOG_EVENT_UNFILTERED” then
if eventType and eventType == “PARTY_KILL” then
if not MyAddonDB.kills then
MyAddonDB.kills = 1
else
MyAddonDB.kills = MyAddonDB.kills + 1
end
end
elseif event == “CHAT_MSG_MONEY” then
local msg = …
local gold = tonumber(string.match(msg, “(%d+) Gold”)) or 0
local silver = tonumber(string.match(msg, “(%d+) Silver”)) or 0
local copper = tonumber(string.match(msg, “(%d+) Copper”)) or 0
MyAddonDB.gold = (MyAddonDB.gold or 0) + gold
MyAddonDB.silver = (MyAddonDB.silver or 0) + silver
MyAddonDB.copper = (MyAddonDB.copper or 0) + copper
if MyAddonDB.copper >= 100 then
MyAddonDB.silver = MyAddonDB.silver + math.floor(MyAddonDB.copper / 100)
MyAddonDB.copper = MyAddonDB.copper % 100
end
if MyAddonDB.silver >= 100 then
MyAddonDB.gold = MyAddonDB.gold + math.floor(MyAddonDB.silver / 100)
MyAddonDB.silver = MyAddonDB.silver % 100
end
end
if mainFrame:IsShown() then
mainFrame.totalPlayerKills:SetText(“Total Kills: ” .. (MyAddonDB.kills or “0”))
mainFrame.totalCurrency:SetText(“Gold: ” .. (MyAddonDB.gold or “0”) .. ” Silver: ” .. (MyAddonDB.silver or “0”) .. ” Copper: ” .. (MyAddonDB.copper or “0”))
end
end
eventListenerFrame:SetScript(“OnEvent”, eventHandler)
eventListenerFrame:RegisterEvent(“COMBAT_LOG_EVENT_UNFILTERED”)
eventListenerFrame:RegisterEvent(“CHAT_MSG_MONEY”)
Creating a WoW Addon – Part 10: Creating a Settings Interface
Part 10: Developing a Separate Interface for Settings
Difficulty: π‘π‘ Medium
This isn’t terribly difficult, but we will be implementing some things into this tutorial that aren’t 100% necessary just to re-cover some things, like .toc file updating, creating a new file, etc. Let’s do it!
First, take a breath if you haven’t yet. This is a lot to take in. Especially Part 8 and 9. Once you’re ready, we can move on!
Step 1: Creating Another File
To start, we will be creating a new file for all of our settings code to live in. This is not necessary, but will keep things cleaner and more organized for you in the future.
To start, let’s create a new file in your addon folder called Settings.lua. Remember, this should be in the same folder as your main addon file, which is probably named MyAddon.lua.
Now, in your .toc file, make sure to add Settings.lua to the bottom of the file, right under your first .lua file.
After you’ve added the new file to your .toc file, let’s re-open Settings.lua and make our settings frame!
Step 2: Creating a Settings Frame
First, we’ll start again by making a new frame. We can call this frame settingsFrame and it should be a local variable.
local settingsFrame = CreateFrame(“Frame”, “MyAddonSettingsFrame”, UIParent, “BasicFrameTemplateWithInset”)
settingsFrame:SetSize(400, 300)
settingsFrame:SetPoint(“CENTER”)
settingsFrame.TitleBg:SetHeight(30)
settingsFrame.title = settingsFrame:CreateFontString(nil, “OVERLAY”, “GameFontHighlight”)
settingsFrame.title:SetPoint(“CENTER”, settingsFrame.TitleBg, “CENTER”, 0, -3)
settingsFrame.title:SetText(“MyAddon Settings”)
settingsFrame:Hide()
settingsFrame:EnableMouse(true)
settingsFrame:SetMovable(true)
settingsFrame:RegisterForDrag(“LeftButton”)
settingsFrame:SetScript(“OnDragStart”, function(self)
self:StartMoving()
end)
settingsFrame:SetScript(“OnDragStop”, function(self)
self:StopMovingOrSizing()
end)
The above code creates a settings frame and a title for the frame. Once again, this frame will come with a close button. The frame will also be moveable!
By default, this frame will be hidden. It will only show when we press a button to show it. This is also going to be the file we create our minimap button in.
Once you have your settings frame created, save your file and log in or /reload your game. You shouldn’t see anything show up, but feel free to remove the settingsFrame:Hide() line or add a slash command yourself to view the settings frame.
Step 3: Creating Our Checkboxes
If all is good in settingsFrame land, let’s move on to create two settings!
These settings are going to be Checkbox settings. If the checkbox is checked they equal true. If it is unchecked it equals false.
For this, we are actually going to be making a new function to create our checkboxes dynamically from a table. But first, we’ll need a table to hold all of our settings.
In your Settings.lua file, at the very top, create a table called settings. We’re making it local to the addon.
local settings = {}
This is how a table looks in Lua. Tables are defined by using {}’s. Currently, it is a blank table. Let’s add a setting to it. Change settings to this:
local settings = {
{
settingText = “Enable tracking of Kills”,
settingKey = “enableKillTracking”,
settingTooltip = “While enabled, your kills will be tracked.”,
},
}
Once again we have {} that defines a table, and inside of those we have another set of {}’s. This means we have nested another table inside of our original table.
Let’s make a second one so you can see how this table keeps expanding as we add to it.
local settings = {
{
settingText = “Enable tracking of Kills”,
settingKey = “enableKillTracking”,
settingTooltip = “While enabled, your kills will be tracked.”,
},
{
settingText = “Enable tracking of Currency”,
settingKey = “enableCurrencyTracking”,
settingTooltip = “While enabled, your currency gained will be tracked.”,
},
}
Hopefully, with the above code laid out, you can see a pattern in how these tables are formed. Now, we’ll need to make a function that will loop over these settings, set their default value, create a checkbox and lay them out on the settings frame accordingly.
To handle the display of these settings, let’s create a variable above this table called checkboxes and we’ll also make this local. Give it an initial value of 0.
local checkboxes = 0
Step 4: Creating Our Checkbox Creation Function
Let’s start our local function and name it CreateCheckbox. We’re also going to give it a few arguments.
- checkboxText
- key
- checkboxTooltip
local function CreateCheckbox(checkboxText, key, checkboxTooltip)
— Code to go here soon…
end
Inside of this function, we need to create the Checkbox frame, put text to the right of it, make it interactable and place it correctly onto the settings frame.
This function will also be our method of giving a default value to our settings. In our case, the default value will be true (checked).
Let’s do this now.
local function CreateCheckbox(checkboxText, key, checkboxTooltip)
local checkbox = CreateFrame(“CheckButton”, “MyAddonCheckboxID” .. checkboxes, settingsFrame, “UICheckButtonTemplate”)
checkbox.Text:SetText(checkboxText)
checkbox:SetPoint(“TOPLEFT”, settingsFrame, “TOPLEFT”, 10, -30 + (checkboxes * -30))
end
The above code is a simple function that creates a new Checkbox anytime it is called. It will position it correctly on the settings frame as well.
In our CreateCheckbox function, we’re going to make sure our Checkbox equals the state of the database key when it’s created. If the setting is true (checked), we want the Checkbox to be checked as well.
We will do that by adding this into our CreateCheckbox function.
if MyAddonDB.settingsKeys[key] == nil then
MyAddonDB.settingsKeys[key] = true
end
checkbox:SetChecked(MyAddonDB.settingsKeys[key])
Above, we check for the key in our settingsKeys table of MyAddonDB to see if it’s nil. If it is, we will set it to the default value of true. If it is not nil, we will leave it alone.
We don’t want to login to the game just yet, because we still have to create the event that will set MyAddonDB.settingsKeys to nil or empty before we use it.
Next, we’re going to add our checkbox functionality and tooltip text that tells the player what exactly the setting does!
We’ll also be increasing the checkboxes variable we put at the top of the file by 1. Add the below code to your CreateCheckbox function.
checkbox:SetScript(“OnEnter”, function(self)
GameTooltip:SetOwner(self, “ANCHOR_RIGHT”)
GameTooltip:SetText(checkboxTooltip, nil, nil, nil, nil, true)
end)
checkbox:SetScript(“OnLeave”, function(self)
GameTooltip:Hide()
end)
checkbox:SetScript(“OnClick”, function(self)
MyAddonDB.settingsKeys[key] = self:GetChecked()
end)
checkboxes = checkboxes + 1
return checkbox
Step 5: Code Review
Now that we’ve gotten this far, our entire Settings.lua file should be as such:
local checkboxes = 0
local settings = {
{
settingText = “Enable tracking of Kills”,
settingKey = “enableKillTracking”,
settingTooltip = “While enabled, your kills will be tracked.”,
},
{
settingText = “Enable tracking of Currency”,
settingKey = “enableCurrencyTracking”,
settingTooltip = “While enabled, your currency gained will be tracked.”,
},
}
local settingsFrame = CreateFrame(“Frame”, “MyAddonSettingsFrame”, UIParent, “BasicFrameTemplateWithInset”)
settingsFrame:SetSize(400, 300)
settingsFrame:SetPoint(“CENTER”)
settingsFrame.TitleBg:SetHeight(30)
settingsFrame.title = settingsFrame:CreateFontString(nil, “OVERLAY”, “GameFontHighlight”)
settingsFrame.title:SetPoint(“TOP”, settingsFrame.TitleBg, “TOP”, 0, -3)
settingsFrame.title:SetText(“MyAddon Settings”)
settingsFrame:EnableMouse(true)
settingsFrame:SetMovable(true)
settingsFrame:RegisterForDrag(“LeftButton”)
settingsFrame:SetScript(“OnDragStart”, function(self)
self:StartMoving()
end)
settingsFrame:SetScript(“OnDragStop”, function(self)
self:StopMovingOrSizing()
end)
local function CreateCheckbox(checkboxText, key, checkboxTooltip)
local checkbox = CreateFrame(“CheckButton”, “MyAddonCheckboxID” .. checkboxes, settingsFrame, “UICheckButtonTemplate”)
checkbox.Text:SetText(checkboxText)
checkbox:SetPoint(“TOPLEFT”, settingsFrame, “TOPLEFT”, 10, -30 + (checkboxes * -30))
if MyAddonDB.settingsKeys[key] == nil then
MyAddonDB.settingsKeys[key] = true
end
checkbox:SetChecked(MyAddonDB.settingsKeys[key])
checkbox:SetScript(“OnEnter”, function(self)
GameTooltip:SetOwner(self, “ANCHOR_RIGHT”)
GameTooltip:SetText(checkboxTooltip, nil, nil, nil, nil, true)
end)
checkbox:SetScript(“OnLeave”, function(self)
GameTooltip:Hide()
end)
checkbox:SetScript(“OnClick”, function(self)
MyAddonDB.settingsKeys[key] = self:GetChecked()
end)
checkboxes = checkboxes + 1
return checkbox
end
We’re not quite done yet, and we won’t yet notice any difference when pulling up our settings frame, if you can get to it without an error that is.
Next, we need to tell our addon to loop through and create these settings when the player logs in based on the entries we have in our settings table. We also need to define our settingsKeys table as blank so we can use it.
Creating a WoW Addon – Part 11: Telling Our Addon to Create Our Checkboxes (Settings)
Part 11: Telling Our Addon to Create Checkboxes
Difficulty: π‘π‘ Medium
**Now that we have our CreateCheckboxes function setup, we can finally tell the addon to create those checkboxes by looping through our settings table.
Step 1: Creating Our Checkboxes with a for loop
First, we’ll need an invisible frame in our Settings.lua file to listen for the “PLAYER_LOGIN” event.
Let’s add the following code to do so:
local eventListenerFrame = CreateFrame(“Frame”, “MyAddonSettingsEventListenerFrame”, UIParent)
eventListenerFrame:RegisterEvent(“PLAYER_LOGIN”)
eventListenerFrame:SetScript(“OnEvent”, function(self, event)
if event == “PLAYER_LOGIN” then
for _, setting in pairs(settings) do
CreateCheckbox(setting.settingText, setting.settingKey, setting.settingTooltip)
end
end
end)
The above code creates an invisible frame, very similarly to the frame we created to listen to events in our other .lua file.
Instead, this frame is registered to the “PLAYER_LOGIN” event. When that event fires, the CreateCheckbox() function is called, which creates all the checkboxes it needs by “looping” through our settings table.
You’ll notice that it loops through the settings table, shown in ()’s of the for loop. At each stop of the settings table, we’re defining that specific table as setting and we can access the components of that table by using a . and its variable name within that table.
For instance, in our for loop, setting.settingText will equal “Enable tracking of Kills” on the first loop.
The line for _, setting in pairs(settings) do is an example of a “for loop”, starting with for, giving a condition, and ending with do and end.
In this case, the condition isn’t especially noticeable, but in english the for loop is basically stating:
FOR
For every…
_, SETTING in PAIRS(settings)
Entry in the settings table
DO
Do the following until we reach the end of the settings table
Another example of a for loop is shown below:
for i = 1, 10 do
print(i)
end
This for loop counts from 1 to 10 and prints out the number it is at each loop. Every time the code inside of the for loop is executed, i increases by 1 until it reaches the for loop condition, which is 10.
Next, we’ll need to define our MyAddonDB.settingsKeys table. Let’s do so by appending the following code inside of our “PLAYER_LOGIN” eventHandler event, at the top:
if not MyAddonDB.settingsKeys then
MyAddonDB.settingsKeys = {}
end
Our full “PLAYER_LOGIN” event should appear as such:
if event == “PLAYER_LOGIN” then
if not MyAddonDB.settingsKeys then
MyAddonDB.settingsKeys = {}
end
for _, setting in pairs(settings) do
CreateCheckbox(setting.settingText, setting.settingKey, setting.settingTooltip)
end
end
Let’s move on and put these settings to use in our addon!
Step 2: Using Our Settings in Our Addon
To access the value of one of our settings, we need to access it in the database.
Think of each step as “hopping into a box, and then another box, then another…”
MyAddonDB > settingsKeys > key
The above, concatenated with .’s becomes…
MyAddonDB.settingsKeys.enableKillTracking
In our main addon .lua file, we will want to find this line of code: if event == “COMBAT_LOG_EVENT_UNFILTERED” then. Once we find this line, we’re going to change it to this:
if event == “COMBAT_LOG_EVENT_UNFILTERED” and MyAddonDB.settingsKeys.enableKillTracking then
Since both conditions in the if then statement need to be true, the following code inside of it will not run unless the event equals “COMBAT_LOG_EVENT_UNFILTERED” and MyAddonDB.settingsKeys.enableKillTracking is true.
The other main addon event we can modify with our setting is elseif event == “CHAT_MSG_MONEY” then. We can modify this to instead be:
elseif event == “CHAT_MSG_MONEY” and MyAddonDB.settingsKeys.enableCurrencyTracking then
In our Settings.lua file, we made sure that these settings are a default of true if the setting has never loaded before, so we don’t have to worry about our addon not working “right out of the box.”
The player will have to go into the settings menu to disable these manually.
Now, the question is…
How are players going to open the settings for this addon?
Creating a WoW Addon – Part 12: Creating a Minimap Button
Part 12: Creating a Minimap Button using LibDBIcon 1.0 and Ace3 (WoW Addon Libraries)
Difficulty: π‘π‘ Medium
We’ve made it pretty far! This will bet the second to last step of our addon creation, where we’ll be covering quality of life changes in Part 13! Let’s get started with using libraries!
Step 1: Downloading and Installing the Libraries
The very first thing we’ll need to do is to download and install two libraries to the folder for our addon, and an image for our minimap button icon:
- Download LibDBIcon-1.0
- Download Ace3
- Download Image
Let’s find these resources below.
- We can find the LibDBIcon-1.0 Library download here.
- We can find the Ace3 Library download here.
- We’re also going to need to download an image for our minimap button. This image needs to be a power of 2 in order to work, and typically should be in .TGA format. I recommend using a 512px square image. If you cannot find a suitable image, you can download and use the image here.
Make sure to download the .zip files and don’t install the addons directly to World of Warcraft, as this won’t do much for us.
Once you have the files downloaded, extract the folders inside to the folder that contains your main .lua file.
The image file should be in the same folder that contains your main .lua file for the purposes of this tutorial. It should be named minimap.tga, as the code below will be looking for that image specifically.
But, let’s take it a step further, and create a NEW folder for our libraries to live inside of. In your addon’s folder, create a new folder called libs. Place our LibDBIcon-1.0 folder and Ace3 folder inside of it.
Your folder structure should now align as follows:
- MyAddon (your addon folder)
- libs (folder)
- LibDBIcon-1.0 (folder)
- Ace3 (folder)
- minimap.tga (image)
- MyAddon.lua
- MyAddon.toc
- Settings.lua
- libs (folder)
Once you have LibDBIcon and Ace3 installed into the libs folder for your addon, we’ll need to tell our .toc file about them.
Let’s modify our .toc file to appear as such:
## Interface: 11502
## Title: MyAddon
## Notes: A description here.
## Author: Your Name
## Version: 1.0.0
## RequiredDeps:
## X-Curse-Project-ID: 99999
## SavedVariables:
## SavedVariablesPerCharacter: MyAddonDB
# Libraries
libs/Ace3/AceAddon-3.0/AceAddon-3.0.lua
libs/Ace3/AceDB-3.0/AceDB-3.0.lua
libs/LibDBIcon-1.0/embeds.xml
# Main Files
MyAddon.lua
Settings.lua
We added #Libraries and #Main Files to our .toc file for ease of organization. Your library files should always load first, so they are above our other .lua files.
Now that we have our .toc file updated and our LibDBIcon files installed, let’s create our button!
Step 2: Creating the Minimap Button
To create the minimap button, we need to register our addon with the Ace3 and LibDBIcon-1.0 libraries by declaring a couple variables. Let’s throw all of this code at the bottom of our Settings.lua file so we can keep track of it.
Go ahead and append the following code to the bottom of Settings.lua:
local addon = LibStub(“AceAddon-3.0”):NewAddon(“MyAddon”)
MyAddonMinimapButton = LibStub(“LibDBIcon-1.0”, true)
The code above allows us to register “MyAddon” with the Ace3 and LibDBIcon-1.0 libraries through LibStub().
Now that we’re registered, let’s go ahead and put the rest of our code for the minimap button below the above code we added:
local miniButton = LibStub(“LibDataBroker-1.1”):NewDataObject(“MyAddon”, {
type = “data source”,
text = “MyAddon”,
icon = “Interface\\AddOns\\MyAddon\\minimap.tga”,
OnClick = function(self, btn)
if btn == “LeftButton” then
MyAddon:ToggleMainFrame()
elseif btn == “RightButton” then
if settingsFrame:IsShown() then
settingsFrame:Hide()
else
settingsFrame:Show()
end
end
end,
OnTooltipShow = function(tooltip)
if not tooltip or not tooltip.AddLine then
return
end
tooltip:AddLine(“MyAddon\n\nLeft-click: Open MyAddon\nRight-click: Open MyAddon Settings”, nil, nil, nil, nil)
end,
})
function addon:OnInitialize()
self.db = LibStub(“AceDB-3.0”):New(“MyAddonMinimapPOS”, {
profile = {
minimap = {
hide = false,
},
},
})
MyAddonMinimapButton:Register(“MyAddon”, miniButton, self.db.profile.minimap)
end
MyAddonMinimapButton:Show(“MyAddon”)
We won’t go into great detail how this code works, as most of this is pulled from the documentation provided by the libraries. But, we can pick out a couple things to note:
- We have an OnClick function being called, and within that function there is another function we have not yet written called MyAddon:ToggleMainFrame(). We will write this function in our other .lua file in a moment.
- There is a tooltip:AddLine() function being called. This will show a tooltip when a person hovers over the minimap button. We always want to make sure this gives instructions to the user on how to operate the minimap button.
Now, let’s get to creating our MyAddon:ToggleMainFrame() function.
Step 3: Creating the MyAddon:ToggleMainFrame() Function
This code is fairly simple, and we’ve already used it previously, but we’re defining it as a global variable so we can call it across files.
First, we’re going to throw this code at the top of our main .lua file.
MyAddon = MyAddon or {}
Here, we initialized MyAddon with a blank or nil state.
Next, put the below code at the bottom of your other main .lua file.
function MyAddon:ToggleMainFrame()
if not mainFrame:IsShown() then
mainFrame:Show()
else
mainFrame:Hide()
end
end
You’ll notice that we did not declare this function using local, meaning this can be called by any addon that wants to call it.
This is useful if you ever create an addon that another addon may want to use as a dependency, or for API purposes that gathers information that your addon can easily access.
In our case, we’re defining it globally so we can call it inside of our Settings.lua file from our main .lua file.
Now…
We’re officially done developing MyAddon!
Congratulations on developing your first World of Warcraft addon! Go ahead and test it! Play around with it! Make changes to it! Delete it and start your own addon! Whatever you want to do from here is all you!
However, there is still more to learn!
Creating a WoW Addon – Part 13: Quality of Life Changes
Part 13: Quality of Life Changes
Difficulty: π’ Easy
Congratulations on finishing your first World of Warcraft addon! Below, we’ll cover some quality of life changes we can make that you may have noticed while developing the addon. Let’s get into it!
Final Step: Our Big QoL Change!
Our big quality of life change is… you guessed it! The gold, silver and copper display text in our addon’s interface!
This was (purposely) made poorly. In an effort to get it working, we skimped on the appearance of this information to the player. Not only was it horrible looking, but it was syntactically wrong. We’re going to correct that now.
There are several ways to improve this: icons, colored text, etc. We’re going to take the colored text route for now. Feel free to try and add icons for the coins yourself! It’s challenging but fruitful! For now, let’s make some changes.
In our main .lua file, let’s find our mainFrame.totalCurrency property. We’re going to make a slight modification. On the SetText() line, we’re going to change it to:
mainFrame.totalCurrency:SetText(“Total Currency Collected:”)
This is going to be our “start line” for our currency display. Next, we’re going to add some more FontStrings that will display our gold, silver and copper separately, with some color!
Let’s add this code right under the line we just modified!
mainFrame.currencyGold = mainFrame:CreateFontString(nil, “OVERLAY”, “GameFontNormal”)
mainFrame.currencyGold:SetPoint(“TOPLEFT”, mainFrame.totalCurrency, “BOTTOMLEFT”, 10, -15)
mainFrame.currencyGold:SetText(“|cFFFFD700Gold: |cFFFFFFFF” .. (MyAddonDB.gold or “0”))
mainFrame.currencySilver = mainFrame:CreateFontString(nil, “OVERLAY”, “GameFontNormal”)
mainFrame.currencySilver:SetPoint(“TOPLEFT”, mainFrame.currencyGold, “BOTTOMLEFT”, 0, -15)
mainFrame.currencySilver:SetText(“|cFFC7C7C7FSilver: |cFFFFFFFF” .. (MyAddonDB.silver or “0”))
mainFrame.currencyCopper = mainFrame:CreateFontString(nil, “OVERLAY”, “GameFontNormal”)
mainFrame.currencyCopper:SetPoint(“TOPLEFT”, mainFrame.currencySilver, “BOTTOMLEFT”, 0, -15)
mainFrame.currencyCopper:SetText(“|cFFD7BEA5Copper: |cFFFFFFFF” .. (MyAddonDB.copper or “0”))
You’ll notice |cFF and |r are used to color the text and reset the text color.
The code following |cFF is HEX color coding. For example, red text would be: |cFFFF0000.
|cFF + FF0000 (Hex Color Code)
Use |r to reset your text color 1 level. By that I mean, if you use multiple |c color codes without using |r, you can overlap colors just fine, but using |r will only revert one level of color change back. Let me give an example:
SetText(“This is default yellow. |cFFFFFFFFThis is White! |cFFFF0000This is red! |r This is back to white, not default yellow!”)
Hopefully that clears it up a bit!
Go ahead and login or /reload your game to checkout the change we made! Visually, it should align much better with our standards! Once again, you can take this a step further and incorporate the icons into the text and make it pop even more!
We’re also going to delete totalCurrency:SetText() from the “OnShow” script for mainFrame and add our gold, silver and copper currency strings instead.
The code, in its entirety for mainFrame:SetScript(“OnShow”, function(), should look like this:
mainFrame:SetScript(“OnShow”, function()
PlaySound(808)
mainFrame.totalPlayerKills:SetText(“Total Kills: ” .. (MyAddonDB.kills or “0”))
mainFrame.currencyGold:SetText(“|cFFFFD700Gold: |cFFFFFFFF” .. (MyAddonDB.gold or “0”))
mainFrame.currencySilver:SetText(“|cFFC7C7C7FSilver: |cFFFFFFFF” .. (MyAddonDB.silver or “0”))
mainFrame.currencyCopper:SetText(“|cFFD7BEA5Copper: |cFFFFFFFF” .. (MyAddonDB.copper or “0”))
end)
We can also go down a bit further in the code and do the same thing inside of our eventHandler function for if mainFrame:IsShown() then if then statement. The full code here should look as such:
if mainFrame:IsShown() then
mainFrame.totalPlayerKills:SetText(“Total Kills: ” .. (MyAddonDB.kills or “0”))
mainFrame.currencyGold:SetText(“|cFFFFD700Gold: |cFFFFFFFF” .. (MyAddonDB.gold or “0”))
mainFrame.currencySilver:SetText(“|cFFC7C7C7FSilver: |cFFFFFFFF” .. (MyAddonDB.silver or “0”))
mainFrame.currencyCopper:SetText(“|cFFD7BEA5Copper: |cFFFFFFFF” .. (MyAddonDB.copper or “0”))
end
We’re done! Our addon is functioning perfectly (for now, and without much testing)!
We can test this some more and call it a day, or we can go one step further with a few bonus tasks and include some nice, new, shiny features! If you feel like giving that a go, let’s do it! Click below for the bonus tasks!
Creating a WoW Addon – Part 14: Bonus Tasks!
Secret Part 14: Bonus Tasks
Thanks for sticking around and going through the bonus tasks. It feels like I’ve tricked you into this section, because it’s not going to be all fun and games…
This section is going to be labeled with a HARD difficulty. Let’s do that now.
Difficulty: π΄π΄π΄π΄ Hard
Yep… There it is.
Okay… okay… Each idea will have its own difficulty level. Doing them all can be somewhat hard to for beginners, so I don’t want to scare you out of doing these activities.
I’m not going to outright give you the code to add a feature. At least not immediately. First, I’ll be giving you the idea for what we’re going to add, and you’re going to add it, or do your best adding it, using the resources I will provide you.
Below all the information and resources for the idea, I will provide the code to accomplish the idea inside of spoiler tags.
This is an exercise to prove to yourself that you’ve learned something. That you can add to this addon in your own way.
Enough of the pep talk. Let’s get into some ideas you can start implementing yourself.
Keep in mind, these are optional. You do not have to implement them. You can come up with your own if you’d like! This is just an excersize to get you thinking about WoW addon development and into the groove.
Idea #1: Milestone Chat Messages
Difficulty: π‘π‘ Medium
The concept behind this idea is fairly simple. Every 250 Kills, print a chat message telling the player they’ve reached a new milestone. Tell them how many kills or how much gold they’ve gained since they’ve started tracking with your addon, or both! The gold is kind of hard to do alone, so it’s not necessary to add this milestone message for gold unless you plan on creating a table with gold milestones that are hardcoded, which can get quite sloppy and unrealistic pretty quickly. You can instead tell the player how much gold they’ve gained when they reach a monster milestone!
We’ll also want to add a new setting in our Settings.lua file so they can turn these messages off of course, but it should start as true, similar to the other settings.
To implement this, you’ll need to use the following:
- if then statements
- The print() function
- Math (Hint: Modulo Operator)
Resources: None.
Try to implement this idea now! To start, and to debug, you can set the kill milestone threshold to something like 2 or 10 instead of 250. When you have working code, you can change it to 250.
See Below for the Spoiled Code
Idea #2: Login Message
Difficulty: π’ Easy
This one is fairly simple. We will want to print a message every time the player logs in or /reloads their game to tell them the addon has loaded. We can also print the version number in this print message.
To implement this, you’ll need to use the following:
- A new version variable
- The print() function
Resources: None.
Give it a go!
See Below for the Spoiled Code
Idea #3: Track Player Deaths
Difficulty: π π π Medium
We can use our eventListenerFrame in our main addon .lua file to listen for player death events. There are a few ways we could do this, but we should try to stick to the easiest way.
To implement this, you’ll need to use the following:
- if then statements
- A new database entry/variable
- Creating a Chat Listener (Hint: “CHAT_MSG_SYSTEM”)
- Making sure PlayerName2 is nil (You don’t want whispers racking up death counts)
Resources: CHAT_MSG_SYSTEM – Wiki
Let’s see if you can get this working! No spoilers for this one!
I hope you’ve enjoyed the addon series so far, I’ve had a blast making it! And from a beginner addon developer to another, I hope you create awesome addons. Please let me know what addons you’ve created after this guide! I’d love to check them out!
In another unrelated part of from this guide, I will be posting a tutorial/walk through on how to most-easily create and maintain a GitHub repository for your code. This keeps your code-base safe and manageable. Be on the lookout for that soon!
TehNoxx, NoxxLFG Author
SPOILED CODE
Monster Kill Milestones
Code: Inside of event == (MyAddon.lua)
— Code above…
if event == “COMBAT_LOG_EVENT_UNFILTERED” and MyAddonDB.settingsKeys.enableKillTracking then
if eventType and eventType == “PARTY_KILL” then
if not MyAddonDB.kills then
MyAddonDB.kills = 1
else
MyAddonDB.kills = MyAddonDB.kills + 1
end
end
— ADDED CODE SHOWN BELOW
if MyAddonDB.kills and MyAddonDB.kills % 250 == 0 then
print(“|cFF00FF00Kill Milestone! |cFFFFFFFFYou have killed a total of ” .. MyAddonDB.kills .. ” enemies! You have looted a total of ” .. MyAddonDB.gold .. ” gold!|r|r”)
end
elseif event == “CHAT_MSG_MONEY” then
— Code below…
Print Version Number
Code: Version Variable (Top of Settings.lua File)
local version = “1.0.0”
Code: Print Login Message (Inside of “PLAYER_LOGIN”, MyAddon.lua)
— Code above…
eventListenerFrame:SetScript(“OnEvent”, function(self, event)
if event == “PLAYER_LOGIN” then
print(“MyAddon |cFFFFFFFFF” .. version .. “|r Loaded”)
— Code below…
Hi, this is a comment.
To get started with moderating, editing, and deleting comments, please visit the Comments screen in the dashboard.
Commenter avatars come from Gravatar.