Skip to content
Getting Started
💡

This quick < 20 minute start-up guide should get you from zero to hero and have your own addon successfully performing a rotation.

Prerequisites

  • If you haven't already, enable developer mode.

  • If you haven't already, download awful.

  • If you don't already have a code editor, grab VSCode.

  • Get these AddOns; they're invaluable tools that'll save you a lot of time:

idTip - Displays IDs in spell, talent, aura, and item tooltips.

BugSack - Stores Lua errors & their full debug stack to view later.

BugGrabber - Required dependency of BugSack.

Setting up your directory

Gimme your project's name as a variable:

e.g, awful, awfulRoutines, awfulBot, MyProject

  • Once awful is installed in your unlocker folder, create the awful/routines folder if it doesn't already exist.

  • Inside of routines/, create your project folder: awful/routines/example.

Our project directory:

.../awful/routines/example

Getting our code to load (bars)

Now, open the folder you made in your preferred code editor. VSCode is a great option if you're unsure.

First .lua file

You'll need to write some Lua to create awful scripts. Go ahead and create example.lua in your directory.

awful/routines/example/example.lua

Nice! Now, we're going to write some stuff in there!

local Unlocker, awful, project = ...

awful.DevMode = true
ℹī¸

This first line will be consistent throughout all files going forward, you're importing required namespaces into the file, including your own project's. You also enabled awful.DevMode, which will do a few neat things to make life as a dev easier.

Now, we need an awful-config.json

Your awful-config.json file tells awful which Lua files it should load, and in which order (which is important, more on that later). Any files that aren't explicitly defined here will not be loaded. It also unlocks some other cool goodies down the line with our CI/CD integrations. Go ahead and create awful-config.json in your directory.

awful/routines/example/awful-config.json

{
    "load": [
        "example.lua"
    ]
}
ℹī¸

The string "example.lua" in the awful-config load array refers to the example.lua file you created earlier. Make sure it's actually set to the name of your file, in this case: example.lua

Hello world?

Let's see if we're in. When you set awful.DevMode to true, the awful namespace became accessible globally. You should now be able to use /dump to read its contents via in-game chat. Try running the command below in WoW chat:

/dump awful.hello

  • If this printed something real nice to chat, you're in! Otherwise, back to step 1..

If you ever have questions or need help, we have an amazing community of skilled, active, and helpful routine developers in our discord: https://discord.gg/JkQEPpED6W

Setting up our routine

Now we're gonna make a couple more files & directories, and some changes to our core file, example.lua.

This should only take a couple of minutes. By the time you're done, you'll have a legit routine casting spells.

Create a directory for your class:

example/warrior

...and these files for your spec:

example/warrior/arms-actor.lua

example/warrior/arms-spells.lua

⚠ī¸

Make sure you include these files in your awful-config.json or they will not be loaded!

{
    "load": [
        "example.lua",
        "warrior/arms-actor.lua",
        "warrior/arms-spells.lua"
    ]
}

Initializing the routine

First, we'll create a routine actor object. You can create one for each specialization, it gives awful something to run when toggled on the correct class and spec.

Then we'll use actor:Init(callback, tickRate) to assign it a function to run on each tick.

What???

Don't stress the details, you'll have working code in a sec. Everything else is explained elsewhere in the docs.

For this example, I'm making a basic Arms Warrior routine on a class trial. If you're feeling confident, I encourage you to try a different class or throw in some extra spells & logic to challenge yourself!

Let's write some code lmao.

Core File: .../example/example.lua

  • First, we're going to create a table for our class inside our project's namespace. We're doing this in our core file, as we assigned it to be loaded first in our awful-config.

  • We'll also create our routine actor here, before initializing it in the actor file. Since we're playing Arms, our specialization index is 1. Be sure to replace 1 in this file with the correct spec index and "warrior" with the correct class, if you're playing something else.

Your specialization index can be acquired from the same dump command we used in-game, like this: /dump GetSpecialization(), or you can just count from top to bottom, or left to right spec, depending on game version in your talents frame under the specialization tab in-game.

local Unlocker, awful, project = ...

awful.DevMode = true

project.warrior = {}
project.warrior.arms = awful.Actor:New({ spec = 1, class = "warrior" })

Actor File: .../example/warrior/arms-actor.lua

local Unlocker, awful, project = ...
local arms = project.warrior.arms

-- stuff out here only runs once, when the file is first loaded.
print("Example warrior locked and loaded!")

-- this is the routine actor.
arms:Init(function()
    -- everything in here is running *on every tick*
    -- its goal is to "act" every frame
    -- that's why you will be spammed with this print when you toggle your routine.
    -- these are comments and you can remove them :)
    print("Wow, it's really running!")
end)

Now try a /reload. You should see your little OnLoad class print first. Go ahead and enable the routine by typing /awful toggle ... You should be spammed with that print you made above! If not, go back and make sure you followed every step perfectly. If even one character is wrong, it will not work.

Creating our spell objects

.../example/warrior/arms-spells.lua

  • We're gonna call this file our spell book - it'll be where we create all of our spell objects with awful.Spell

You'll always need to acquire the correct spell IDs for spell objects to work properly. This is where idTip comes in handy! If you didn't get the AddOns I listed at the start of this guide, go get 'em.

local Unlocker, awful, project = ...
local arms = project.warrior.arms

local Spell = awful.Spell
awful.Populate({
    execute = Spell(163201),
    slam = Spell(1464),
    mortalStrike = Spell(12294),
    overpower = Spell(7384),
    warbreaker = Spell(262161), -- <-- don't forget the comma here when you add more spells, this is a table!
}, arms, getfenv(1))
-- ^^^ make sure you replace "arms" here with your specialization's routine actor!

Here's what we've done in this file so far:

  • Created a local reference to our actor, arms

  • Populated it, as well as our current environment with some delicious spell objects.

Next, we'll initialize some callbacks for all of these spells. Prepare for some more code.

local Unlocker, awful, project = ...
local arms = project.warrior.arms

local Spell = awful.Spell
awful.Populate({
    execute = Spell(163201),
    slam = Spell(1464),
    mortalStrike = Spell(12294),
    overpower = Spell(7384),
    warbreaker = Spell(262161),
}, arms, getfenv(1))

execute:Callback(function(spell)
    spell:Cast(target)
end)

overpower:Callback(function(spell)
    spell:Cast(target)
end)

warbreaker:Callback(function(spell)
    spell:Cast(target)
end)

mortalStrike:Callback(function(spell)
    spell:Cast(target)
end)

slam:Callback(function(spell)
    spell:Cast(target)
end)

Nice! Now we have some usable spell objects! Now let's pull them into our actor.

It's ALIVE!!!

Back in our routine actor file, let's start by just calling all of our spell objects in a basic priority arrangement and make it auto attack to generate rage.

You'll want to pull up on a Training Dummy for this.

Lua is interpreted line by line, left to right, then top to bottom. Be sure to consciously sort your actions based on highest to lowest priority!

.../example/warrior/arms-actor.lua

local Unlocker, awful, project = ...
local arms = project.warrior.arms

print("Example warrior locked and loaded!")

arms:Init(function()
    -- only do this stuff if our target is an enemy
    if target.enemy then
        -- auto attack to generate rage
        StartAttack()
        -- spells we created in the spell book are magically available in our actor!
        execute()
        overpower()
        warbreaker()
        mortalStrike()
        slam()
    end
end)

Now, hit up that /reload, enable it by typing /awful toggle in chat, and you might notice... you have a functioning routine!!! 🎉

Souping it up a bit...

Seriously, if you made it this far... amazing work.

I'm really excited to see what you come up with in the future!

clappa

Before we wrap things up, let's clean this routine up just a bit... We're gonna add some relatively advanced logic like it's nothing:

  • Make it avoid attacking into physical immunities by defining the damage type of our damaging spells

  • Make it auto charge / leap to the target in a way that feels natural, by tracking player movement history

  • Make it cast warbreaker and avatar at high priority when /awful burst is used

  • Add some fancy alerts for our gap closers and burst abilities

  • Apply labels to some of our spell callbacks and pass them in the actor, to have various logic at different priorities for the same spell

  • Make slam stop eating up all of our rage

  • Have it maintain Rend on target

  • Use sweeping strikes when there's nothing else to do (lowest priority)

The spell book expands

local Unlocker, awful, project = ...
local arms = project.warrior.arms

local Spell = awful.Spell
awful.Populate({
    execute = Spell(163201, { damage = "physical" }),
    slam = Spell(1464, { damage = "physical" }),
    mortalStrike = Spell(12294, { damage = "physical" }),
    overpower = Spell(7384, { damage = "physical" }),
    warbreaker = Spell(262161, { damage = "physical", facingNotRequired = true }),
    rend = Spell(772, { damage = "physical", bleed = true }),
    sweepingStrikes = Spell(260708),
    charge = Spell(100),
    leap = Spell(6544),
    avatar = Spell(107574),
}, arms, getfenv(1))

charge:Callback("gapclose", function(spell)
    -- don't charge if we recently leaped
    if leap.cd > 29 then return end
    if target.distance > 12 and player.movingToward(target, { angle = 45, duration = 0.15 })  then
        if spell:Cast(target) then
            awful.alert("Charge (Gapclose)", spell.id)
        end
    end
end)

leap:Callback("gapclose", function(spell)
    -- don't leap if we recently charged
    if charge.recentlyUsed(2) then return end
    if target.distance > 12 and player.movingToward(target, { angle = 45, duration = 0.15 }) then
        if spell:AoECast(target) then
            awful.alert("Leap (Gapclose)", spell.id)
        end
    end
end)

execute:Callback(function(spell)
    spell:Cast(target)
end)

overpower:Callback(function(spell)
    spell:Cast(target)
end)

-- avatar during burst
avatar:Callback("burst", function(spell)
    if target.meleeRange and spell:Cast() then
        awful.alert("Avatar (Burst)", spell.id)
    end
end)

-- high priority warbreaker on /awful burst
warbreaker:Callback("burst", function(spell)
    if spell:Cast(target) then
        awful.alert("Warbreaker (Burst)", spell.id)
    end
end)

-- non-labelled warbreaker falls lower in prio list
warbreaker:Callback(function(spell)
    spell:Cast(target)
end)

mortalStrike:Callback(function(spell)
    spell:Cast(target)
end)

slam:Callback(function(spell)
    -- only use slam while capping out on rage so we can always MS on cooldown...
    if mortalStrike.cd > 3 or player.rage > 70 then
		spell:Cast(target)
    end
end)

-- maintain rend single target, high priority
rend:Callback("maintain", function(spell)
    if target.debuffRemains(spell.id, player) < 4 then
        spell:Cast(target)
    end
end)

-- spread rend, low priority
-- (rend spreading is probably not great, it's just an example of how we *can* do it)
rend:Callback("spread", function(spell)
    if player.rage > 55 then
        for _, enemy in ipairs(enemies) do
            if not enemy.isUnit(target) and enemy.debuffRemains(772, player) < 4 then
                if spell:Cast(enemy) then
                    awful.alert("Rend " .. enemy.class .. " (Spread)", spell.id)
                    return true
                end
            end
        end
    end
end)

sweepingStrikes:Callback(function(spell)
    if player.combat then
        spell:Cast()
	end
end)
  • See how we're able to set up callback functions for each spell object in a way that will allow us to stay organized? We can group our conditions and/or actions into neat little modular components that we can easily access from the actor via callback labels, like "burst", "maintain", or "spread" above. This naturally follows best practices for staying organized, maximizes performance by restricting the block of code from running unless the spell is off cd and usable, and gives us the ability to easily move around the priority of what may soon be huge blocks of code.

  • When we have another reason that a spell should be cast, we can add another callback under a different label and fit it into our existing priority list.

It does more stuff!

local Unlocker, awful, project = ...
local arms = project.warrior.arms

print("Example warrior locked and loaded!")

arms:Init(function()

	-- keep in mind we're only doing *anything* in this if/then statement if there is an enemy target.
	if target.enemy then
		StartAttack()
		-- burst!
		if awful.burst then
			avatar("burst")
			warbreaker("burst")
		end
		charge("gapclose")
		leap("gapclose")
		execute()
		rend("maintain")
		overpower()
		warbreaker()
		mortalStrike()
		-- rend("spread") -- hey, this is commented out, so it won't spread rend!
		slam()
		sweepingStrikes()
	end

end)

Sheeesh, this thing really cranks now. Nice!

  • I'm sure you'll come up with much better than this though. This is just an ultra-basic routine whipped up to get you introduced, it doesn't cover the tiniest fraction of what you'll find awful framework is capable of. I'll be doing my best to keep things thoroughly documented going into the future. So keep an eye out!

Keep playing around, don't be afraid to try new things. Experiment and have a good time. Most importantly, always show off what you make!

I hope this has been helpful. Send any feedback my way! Alexei#1234 on Discord.

Hop in the awful devs Discord, our community is growing and we'd love to have you!

...Now, click around through the stuff on the left and explore the power of awful framework!!!

Last updated on May 3, 2023