Skip to content
Caffeine Docs

CR Quick Start

This guide will walk you through creating a basic Destruction Warlock combat routine using the Caffeine Engine.

We will be following the “Easy Mode” Destruction Warlock guide found at Icy Veins for the entirety of this guide.

Assumptions

This guide assumes you have some basic programming knowledge, specifically with Lua, and that you are familiar enough with World of Warcraft to understand basic game concepts.

We also assume you are using an IDE, such as Visual Studio: Code, and that you have Caffeine with local loading enabled.

This guide assumes you have created a level 60 Destruction Warlock Class Trial, and are at the starting area with the dummies.

Foundations

Let’s get started on the foundation of our combat routine. Here is the basic “Skeleton” of a Caffeine Module:

local Unlocker, Caffeine = ...

local destro_lock = Caffeine.Module:New('Destro Lock')

Caffeine:Register(destro_lock)

Let’s go through this line-by-line.


local Unlocker, Caffeine = ...

This line allows you to access functions from your Unlocker environment and the Caffeine environment in this file.


local destro_lock = Caffeine.Module:New('Destro Lock')

Here, we are creating a Caffeine Module named “Destro Lock”, and assigning it to our variable, destro_lock. This will allow us to use module functions later on.


Caffeine:Register(destro_lock)

This is how you register your module with the Caffeine Engine. This is typically the last line of your module.

Units

With the Caffeine Engine, it is best practice to declare your Unit objects near the top of the file.

In the below example, we are adding some common units to the file.

local Unlocker, Caffeine = ...

-- Create our Module object
local destro_lock = Caffeine.Module:New('Destro Lock')

-- Adding our common units here
local player            = Caffeine.UnitManager:Get('player')
local target            = Caffeine.UnitManager:Get('target')
local none              = Caffeine.UnitManager:Get('none')

-- Register our module with the Caffeine Engine
Caffeine:Register(destro_lock)

This will allow us to utilize player, target, or none in the future without having to call the UnitManager’s Get() function each time.

Spellbook

The Caffeine Engine requires that you declare your spell objects that you are going to use in your Action Priority Lists.

To make it easy for you, we’ve adding some handy commands. Head in-game, and type /caffeine spell_dump. This will create a file in your Unlocker folder with all of your known spells already in the proper format. In this example, I have already dumped the spells and trimmed the unnecessary ones.

Let’s add them below!

local Unlocker, Caffeine = ...

-- Create our Module object
local destro_lock       = Caffeine.Module:New('Destro Lock')

-- Units
local player            = Caffeine.UnitManager:Get('player')
local target            = Caffeine.UnitManager:Get('target')
local none              = Caffeine.UnitManager:Get('none')

-- Here is where we are adding our spells
local immolate          = Caffeine.Globals.SpellBook:GetSpell(348)
local chaos_bolt        = Caffeine.Globals.SpellBook:GetSpell(116858)
local conflagrate       = Caffeine.Globals.SpellBook:GetSpell(17962)
local cataclysm         = Caffeine.Globals.SpellBook:GetSpell(152108)
local incinerate        = Caffeine.Globals.SpellBook:GetSpell(29722)
local summon_voidwalker = Caffeine.Globals.SpellBook:GetSpell(697)
local summon_infernal   = Caffeine.Globals.SpellBook:GetSpell(1122)
local fel_domination    = Caffeine.Globals.SpellBook:GetSpell(333889)

-- Register our module with the Caffeine Engine
Caffeine:Register(destro_lock)

Now, we have our spells, let’s use them!

Action Priority Lists

An APL is just that - a priority list of actions. It goes from the top down and executes the first action that is available.

Let’s add our first APL for Destruction Warlock. We’re going to clean up the spells and skeleton a bit here, to save space.

local Unlocker, Caffeine = ...

-- Create our Module object
local destro_lock = Caffeine.Module:New('Destro Lock')

-- Units Removed for Clarity
-- Spells Removed for Clarity

local default_apl = Caffeine.APL:New('default') -- Here we're adding the APL!

-- Register our module with the Caffeine Engine
Caffeine:Register(destro_lock)

Now we can get into the meat and potatoes of routine development - adding spells and conditions!

Spells & Conditions

We have our units, spellbook, and APL. Before we go forward, let’s take a look at the Icy Veins guide, starting with Single Target.

The first condition we have is

Maintain Immolate at all times.

Logically, to do this we want to ensure our target has immolate active, and that it isn’t going to expire in the next cast or so. Let’s get started.

Spell Conditional Basics

First thing’s first, let’s add Immolate to our APL.

default_apl:AddSpell(
    immolate:CastableIf(function(self)
        return true
    end):SetTarget(target)
)

default_apl:addSpell() is how we tell our APL that we are giving it a spell to look at.

spell:CastableIf() is us telling the apl “This is the spell we want to cast, but only if these conditions are true”

function(self) return true end means that this function will always return true, so we always want to cast this. We will change this soon to have real conditions!

:SetTarget(target) is us telling the APL who we want to cast Immolate on - and we’re referencing the target object we created earlier!

Now that you understand the basic structure of an APL Spell Conditional, let’s move forward!

Immolate: Understanding Auras

We need to determine if the target has Immolate active, so how do we do that? Well, it depends. Some spells (like Flame Shock on a Shaman) utilize the same spell ID for both the cast (in our case, immolate) and the aura. Immolate does not work like this. So, to get the aura ID, we’re going to download an addon called IDTip.

Now, head in game to your Warlock, and cast Immolate on a dummy! Hover over the debuff, and viola, you have the aura ID!

Let’s go ahead and add this to our spellbook like so:

local Unlocker, Caffeine = ...

-- Create our Module object
local destro_lock = Caffeine.Module:New('Destro Lock')

-- Units Removed for Clarity
-- Spells Removed for Clarity

-- Auras
local immolate_aura = Caffeine.Globals.SpellBook:GetSpell(157736)

-- APL
local default_apl = Caffeine.APL:New('default') -- Here we're adding the APL!

-- Register our module with the Caffeine Engine
Caffeine:Register(destro_lock)

Now, let’s take another step forward and utilize the aura in our conditional!

Immolate: Using Auras in Conditionals

default_apl:AddSpell(
    immolate:CastableIf(function(self)
        return self:IsKnownAndUsable() and not target:GetAuras():FindMy(immolate_aura):IsUp()
    end):SetTarget(target)
)

Here, we’ve added two conditions to our Spell Conditional. The first, IsKnownAndUsable() checks that we know the spell, and that it is available before we try to cast it. The second is a bit more complex.

First, we call our target object we made earlier, and run the built-in unit function GetAuras(). This returns an AuraTable that we can run several functions on (check the references!), in our case we are using FindMy(immolate_aura). This searches all of the target’s auras for an immolate debuff that was created by us. Then, we call IsUp(), which checks if the aura is active.

So to bring it all together here, this block checks if the spell is usable and if the target does not have our immolate debuff. If both of those conditions are true, it casts immolate on the target.

Let’s move on to the next line

Cast Chaos Bolt if you have 5 Soul Shards.

Chaos Bolt: Secondary Resources

We need to ensure our player has 5 Soul Shards before we cast Chaos Bolt - so how do we do that?

We use the player object we created earlier! The player object is a Unit object, giving us a ton of functions to utilize. In our case, we will be using GetPower(). GetPower() accepts an optional argument, which is an index for the power type you are trying to check. You can find a list on WoWPedia. For Soul Shards, we are going to use index 7. Let’s add it to our chaos_bolt conditional.

default_apl:AddSpell(
    chaos_bolt:CastableIf(function(self)
        return self:IsKnownAndUsable() and player:GetPower(7) == 5
    end):SetTarget(target)
)

Just like that, we have Chaos Bolt support! Let’s move on to the next line.

Cast Conflagrate if you have 2 charges.

Conflagrate: Charges

We need to determine how many charges of conflagrate we have. If we have 2, we want to cast Conflagrate. Pretty simple, right?

Let’s talk about SpellBook objects. The conflagrate object you created is much more than just CastableIf - it comes with all of the functions in Spell, one of which is GetCharges()! Let’s do it!

default_apl:AddSpell(
    conflagrate:CastableIf(function(self)
        return self:IsKnownAndUsable() and self:GetCharges() == 2
    end):SetTarget(target)
)

Here, we used self:GetCharges() instead of conflagrate:GetCharges() – that’s because self here refers to the spell object!

Let’s move on to the next line!

Cast Cataclysm when available.

Cataclysm: Ground Targeting

Cataclysm is a ground-targeted ability, meaning we can’t just feed the target object and expect it to work best. Luckily, Caffeine Engine has some nifty functions for ground casting.

But let’s take a step back - we want to cast cataclysm as soon as it’s available, so lets create the spell conditional.

default_apl:AddSpell(
    cataclysm:CastableIf(function(self)
        return self:IsKnownAndUsable()
    end):SetTarget(none)
)

Now that we have the basic spell conditional, let’s get started on targeting. Caffeine Engine’s UnitManager comes with a function FindEnemiesCentroid(radius, range). This returns the location within range that has the most enemies within radius of each other. We can use this to get the best spot to place Cataclysm! Let’s do it.

default_apl:AddSpell(
    cataclysm:CastableIf(function(self)
        return self:IsKnownAndUsable()
    end):SetTarget(none):OnCast(function(self)
        local best_location = Caffeine.UnitManager:FindEnemiesCentroid(8, 40)
        self:Click(best_location)
    end)
)

We use “OnCast” as once we cast cataclysm, the Targeting Reticle appears. So as we cast, we want to find the best location, and then click it!

Since cataclysm has an 8 yard radius and a 40 yard range, those are the arguments we gave to the Centroid function.

Cataclysm will now cast in the best possible location on cooldown!

All we have left are filler spells, so lets move on.

Single Target APL

Let’s bring it all together real quick, here is the single-target APL thus far:

local Unlocker, Caffeine = ...

-- Create our Module object
local destro_lock       = Caffeine.Module:New('Destro Lock')

-- Units
local player            = Caffeine.UnitManager:Get('player')
local target            = Caffeine.UnitManager:Get('target')
local none              = Caffeine.UnitManager:Get('none')

-- Spells
local immolate          = Caffeine.Globals.SpellBook:GetSpell(348)
local chaos_bolt        = Caffeine.Globals.SpellBook:GetSpell(116858)
local conflagrate       = Caffeine.Globals.SpellBook:GetSpell(17962)
local cataclysm         = Caffeine.Globals.SpellBook:GetSpell(152108)
local summon_infernal   = Caffeine.Globals.SpellBook:GetSpell(1122)
local incinerate        = Caffeine.Globals.SpellBook:GetSpell(29722)

-- Auras
local immolate_aura     = Caffeine.Globals.SpellBook:GetSpell(157736)

-- APL
local default_apl       = Caffeine.APL:New('default')

-- Maintain Immolate at all times
default_apl:AddSpell(
    immolate:CastableIf(function(self)
        return self:IsKnownAndUsable() and not target:GetAuras():FindMy(immolate_aura):IsUp()
    end):SetTarget(target)
)

-- Cast Chaos Bolt at 5 Soul Shards
default_apl:AddSpell(
    chaos_bolt:CastableIf(function(self)
        return self:IsKnownAndUsable() and player:GetPower(7) == 5
    end):SetTarget(target)
)

-- Cast Conflagrate at 2 charges
default_apl:AddSpell(
    conflagrate:CastableIf(function(self)
        return self:IsKnownAndUsable() and self:GetCharges() == 2
    end):SetTarget(target)
)

-- Cast Cataclysm when available
default_apl:AddSpell(
    cataclysm:CastableIf(function(self)
        return self:IsKnownAndUsable()
    end):SetTarget(none):OnCast(function(self)
        local best_location = Caffeine.UnitManager:FindEnemiesCentroid(8, 40)
        self:Click(best_location)
    end)
)

-- Cast Conflagrate
default_apl:AddSpell(
    conflagrate:CastableIf(function(self)
        return self:IsKnownAndUsable()
    end):SetTarget(target)
)

-- Cast Incinerate
default_apl:AddSpell(
    incinerate:CastableIf(function(self)
        return self:IsKnownAndUsable()
    end):SetTarget(target)
)

-- Register our module with the Caffeine Engine
Caffeine:Register(destro_lock)

We still have one more step before we get a working rotations! The Sync!

Module Sync

To have our module actually cast spells, we need to add a Sync() section. This will go right above where we register the module, like so:

... rest of our code

destro_lock:Sync(function()

end)

Caffeine:Register(destro_lock)

There is quite a bit we can do in our Sync! This runs every “tick”, so we can update things like pet status, checking if we’re facing the target, if the target exists, is hostile, is alive, and more! Let’s keep it simple first.

We’ll add checks to ensure we’re in combat, our target exists, is alive, and is an enemy, and that we’re not casting.

... rest of our code

destro_lock:Sync(function()
    if player:IsAffectingCombat() and not player:IsCasting() and target:Exists() and target:IsHostile() and not target:IsDead() then
        default_apl:Execute()
    end
end)

Caffeine:Register(destro_lock)

This concludes the basic, single-target portion of our Destruction Warlock guide. Expect a more advanced guide soon! Below is the complete, finished rotation.

Finished Rotation

local Unlocker, Caffeine = ...

-- Create our Module object
local destro_lock       = Caffeine.Module:New('Destro Lock')

-- Units
local player            = Caffeine.UnitManager:Get('player')
local target            = Caffeine.UnitManager:Get('target')
local none              = Caffeine.UnitManager:Get('none')

-- Spells
local immolate          = Caffeine.Globals.SpellBook:GetSpell(348)
local chaos_bolt        = Caffeine.Globals.SpellBook:GetSpell(116858)
local conflagrate       = Caffeine.Globals.SpellBook:GetSpell(17962)
local cataclysm         = Caffeine.Globals.SpellBook:GetSpell(152108)
local summon_infernal   = Caffeine.Globals.SpellBook:GetSpell(1122)
local incinerate        = Caffeine.Globals.SpellBook:GetSpell(29722)

-- Auras
local immolate_aura     = Caffeine.Globals.SpellBook:GetSpell(157736)

-- APL
local default_apl       = Caffeine.APL:New('default')

-- Maintain Immolate at all times
default_apl:AddSpell(
    immolate:CastableIf(function(self)
        return self:IsKnownAndUsable() and not target:GetAuras():FindMy(immolate_aura):IsUp()
    end):SetTarget(target)
)

-- Cast Chaos Bolt at 5 Soul Shards
default_apl:AddSpell(
    chaos_bolt:CastableIf(function(self)
        return self:IsKnownAndUsable() and player:GetPower(7) == 5
    end):SetTarget(target)
)

-- Cast Conflagrate at 2 charges
default_apl:AddSpell(
    conflagrate:CastableIf(function(self)
        return self:IsKnownAndUsable() and self:GetCharges() == 2
    end):SetTarget(target)
)

-- Cast Cataclysm when available
default_apl:AddSpell(
    cataclysm:CastableIf(function(self)
        return self:IsKnownAndUsable()
    end):SetTarget(none):OnCast(function(self)
        local best_location = Caffeine.UnitManager:FindEnemiesCentroid(8, 40)
        self:Click(best_location)
    end)
)

-- Cast Conflagrate
default_apl:AddSpell(
    conflagrate:CastableIf(function(self)
        return self:IsKnownAndUsable()
    end):SetTarget(target)
)

-- Cast Incinerate
default_apl:AddSpell(
    incinerate:CastableIf(function(self)
        return self:IsKnownAndUsable()
    end):SetTarget(target)
)

destro_lock:Sync(function()
    if player:IsAffectingCombat() and not player:IsCasting() and target:Exists() and target:IsHostile() and not target:IsDead() then
        default_apl:Execute()
    end
end)

-- Register our module with the Caffeine Engine
Caffeine:Register(destro_lock)

Further reading

Let’s go over some more features you may want to add to your own rotations!

Pet Management

Let’s go ahead and add some more basic components to the rotation. You can throw these in at the indicated points in the prior rotation.

-- Units
local pet               = Caffine.UnitManager:Get('pet')

-- Spells
local summon_voidwalker = Caffeine.Globals.SpellBook:GetSpell(697)
local fel_domination    = Caffeine.Globals.SpellBook:GetSpell(333889)

-- APLs
local resting_apl       = Caffeine.APL:New('resting')

The Pet unit is going to allow us to run Unit methods on our pet. We’re also going to use resting_apl for some out of combat actions.

So let’s take a look at our Sync. We currently have a sync that executes the DefaultAPL while we’re in combat, have a valid target, and are not casting. Let’s make it so when we’re not in combat it runs the RestingAPL.

destro_lock:Sync(function()
    if player:IsAffectingCombat() and not player:IsCasting() and target:Exists() and target:IsHostile() and not target:IsDead() then
        default_apl:Execute()
    end

    if not player:IsAffectingCombat() and not player:IsMoving() and not player:IsMounted() then
        resting_apl:Execute()
    end
end)

Now, lets add the summon pet spell to the APL! Since we already check if we are in combat, moving, or mounted in the Sync, we don’t need that check here.

resting_apl:AddSpell(
    summon_voidwalker:CastableIf(function(self)
        return self:IsKnownAndUsable() and (not pet:Exists() or not pet:IsAlive())
    end):SetTarget(none)
)

We did add a check to ensure our pet either is dead not pet:IsAlive() or doesn’t exist not pet:Exists() before attempting to summon him. Now, your CR should summon a voidwalker when you don’t have one out of combat.

Now we can add some pet management to the default, combat APL!

Typically, we don’t want to hard-cast Summon Voidwalker in combat, so let’s make sure we use Fel Domination to speed things up.

default_apl:AddSpell(
    fel_domination:CastableIf(function(self)
        return self:IsKnownAndUsable() and (not pet:Exists() or not pet:IsAlive())
    end):SetTarget(none)
)

default_apl:AddSpell(
    summon_voidwalker:CastableIf(function(self)
        return self:IsKnownAndUsable() and player:GetAuras():FindMy(fel_domination):IsUp()
    end):SetTarget(none)
)

With these checks, we will always try to cast Fel Domination before casting Summon Voidwalker. Remember that this APL runs in combat - so we want to make sure it’s fast. If Fel Domination isn’t available however, it will still attempt to summon your voidwalker.

Coming Soon

  • Pet Healing
  • Taming new pets
  • Multi-target and single-target APLs
  • Nested APLs
  • APL variables
  • Sequencers