Build-Along: PoisonPal — Rogue Poison Reminder¶
Difficulty: Beginner | Mode: Blizzard Faithful | Reading time: ~20 minutes | Interface: 120001 (Patch 12.0.1)
Step 1: What We're Building¶
PoisonPal is a small, focused addon that solves a real problem every Rogue knows: forgetting to apply poisons before a dungeon or raid. It watches your poison buffs and warns you when a lethal or non-lethal poison is missing.
Features¶
- Missing poison alerts — warns in chat when you enter the world without poisons applied
- Settings panel — enable/disable, sound toggle, login check toggle
- Addon Compartment button — left-click to check status, right-click for settings
- Rogue-only — silently disables on non-Rogue characters
Why This Is a Great First Addon¶
PoisonPal uses the core building blocks you'll need for almost any addon:
- Event-driven logic — reacting to
UNIT_AURAandPLAYER_ENTERING_WORLDinstead of polling every frame C_UnitAuras.GetPlayerAuraBySpellID()— the modern aura API for checking player buffs by spell ID- Settings API — the modern Blizzard settings panel (not the deprecated
InterfaceOptionssystem) - Addon Compartment — the 10.1+ minimap dropdown that replaces LibDataBroker for simple addons
- Class filtering — checking
UnitClass()to only run on relevant characters
By the end, you'll have a working addon you can install and use immediately, built entirely with official Blizzard APIs in blizzard-faithful mode.
Poisons Are Buffs, Not Weapon Enchants
In modern retail WoW (Dragonflight onward), Rogue poisons are player buffs — not temporary weapon enchants. You activate a poison ability and it persists as a buff until cancelled or replaced. This means:
GetWeaponEnchantInfo()does NOT detect poisons. It only detects temporary weapon enchants like sharpening stones or weightstones.UNIT_INVENTORY_CHANGEDdoes NOT fire when poisons are applied or removed.- Poisons do not expire. There is no timer to track — just presence or absence.
The correct API is C_UnitAuras.GetPlayerAuraBySpellID(spellID), and the correct event is UNIT_AURA. AI coding assistants trained on older WoW code will frequently generate GetWeaponEnchantInfo() for poison detection — this is wrong for any WoW version from Dragonflight onward.
Plugin Shortcut
Want to skip the manual steps? You can generate a similar addon instantly:
But building it by hand teaches you the patterns you'll use in every addon.Step 2: Prerequisites¶
Before you start, make sure you have:
- Claude Code with the better-addons plugin installed (see Plugin Overview)
- A text editor — VS Code with the WoW API extension is recommended
- WoW 12.0+ game client for testing (you need a Rogue character)
Set Your Development Mode¶
Open Claude Code and set blizzard-faithful mode. This ensures all generated code uses only official, documented APIs:
Why Blizzard Faithful?
For a beginner tutorial, blizzard-faithful is the safest choice. The code we write will use only documented APIs, avoid hooking Blizzard frames, and survive any future patch without changes. The other modes (enhancement-artist, boundary-pusher, performance-zealot) are for when you want to push the boundaries — not where you want to start.
Step 3: Project Setup¶
Create a new folder for the addon. The folder name must match the .toc filename exactly — WoW uses the folder name to find the manifest.
Our addon will have three files:
PoisonPal/
PoisonPal.toc -- Manifest: metadata, file load order
Core.lua -- Initialization, events, poison checking, alerts
Config.lua -- Settings panel
Why not Init.lua + Core.lua?
The starter template splits initialization and logic into separate files. For a small addon like PoisonPal, that's unnecessary overhead. We'll combine everything into Core.lua and keep Config.lua separate since the Settings API code is self-contained. When your addon grows, split files by responsibility.
Create the TOC File¶
The .toc file is your addon's manifest. WoW reads it to decide what to load and in what order.
## Interface: 120001
## Title: PoisonPal
## Notes: Rogue poison reminder — warns when poisons are missing.
## Author: YourName
## Version: @project-version@
## SavedVariables: PoisonPalDB
## IconTexture: Interface\Icons\Ability_Rogue_DualWeild
## AddonCompartmentFunc: PoisonPal_OnCompartmentClick
## AddonCompartmentFuncOnEnter: PoisonPal_OnCompartmentEnter
## AddonCompartmentFuncOnLeave: PoisonPal_OnCompartmentLeave
Core.lua
Config.lua
Let's break down every field:
| Field | Purpose |
|---|---|
Interface: 120001 | Targets Patch 12.0.1. Addons below 120000 require "Load out of date addons" to be enabled. |
SavedVariables: PoisonPalDB | The global Lua variable name WoW persists between sessions. Available after ADDON_LOADED. |
IconTexture | Shown in the addon list and the Addon Compartment dropdown. Use any standard WoW icon path. |
AddonCompartmentFunc | The global function WoW calls when the player clicks your entry in the minimap dropdown. |
@project-version@ | A token that BigWigsMods/packager replaces with the git tag at build time. Shows "dev" locally. |
Common Mistake: Folder Name Mismatch
If your folder is named PoisonPal but the file is poisonpal.toc, the addon won't load. The .toc filename must match the folder name exactly, including capitalization on case-sensitive systems.
Step 4: Core Logic — Event Handling¶
Create Core.lua. This file handles everything: namespace setup, event dispatch, poison checking, slash commands, alerts, and the Addon Compartment.
Namespace and Defaults¶
Every file in a WoW addon receives two arguments: the addon name (string) and a shared namespace table. We use the namespace to share data between Core.lua and Config.lua.
local addonName, ns = ...
ns.addonName = addonName
-- Default settings (merged into SavedVariables on first load)
ns.defaults = {
enabled = true, -- Master toggle
playSound = true, -- Play a sound with warnings
showOnLogin = true, -- Check poisons on login
}
Poison Spell ID Tables¶
Modern Rogue poisons are player buffs. We need to know the spell IDs for each poison so we can check for them using the aura API. Rogues have two poison categories — lethal and non-lethal — and should always have one of each active.
-- Lethal poisons (spell IDs for the player buff)
local LETHAL_POISONS = {
[2823] = "Deadly Poison",
[315584] = "Instant Poison",
[8679] = "Wound Poison",
[381637] = "Amplifying Poison",
}
-- Non-lethal poisons (spell IDs for the player buff)
local NON_LETHAL_POISONS = {
[3408] = "Crippling Poison",
[5761] = "Numbing Poison",
[381664] = "Atrophic Poison",
}
Where Do These Spell IDs Come From?
You can find spell IDs on Wowhead by searching for the poison name. The spell ID is in the URL (e.g., wowhead.com/spell=2823/deadly-poison). You can also use /dump C_UnitAuras.GetPlayerAuraBySpellID(2823) in-game to verify a specific ID returns data when the poison is active.
Event Dispatcher¶
The table-dispatch pattern maps event names to handler functions. When WoW fires an event, we look it up in the table and call the matching handler. This is cleaner and faster than a chain of if/elseif checks.
-- Event dispatch table
local eventHandlers = {}
local eventFrame = CreateFrame("Frame")
eventFrame:SetScript("OnEvent", function(self, event, ...)
local handler = eventHandlers[event]
if handler then handler(self, event, ...) end
end)
local function RegisterEvent(event, handler)
eventHandlers[event] = handler
eventFrame:RegisterEvent(event)
end
local function UnregisterEvent(event)
eventHandlers[event] = nil
eventFrame:UnregisterEvent(event)
end
-- Export to namespace for Config.lua
ns.RegisterEvent = RegisterEvent
ns.UnregisterEvent = UnregisterEvent
Class Check¶
PoisonPal only makes sense for Rogues. We check the player's class early and skip all initialization if it doesn't match. The second return value from UnitClass() is the locale-independent class token — always "ROGUE" regardless of language.
-- Rogue class check (locale-independent)
local _, playerClass = UnitClass("player")
local isRogue = (playerClass == "ROGUE")
Poison Check Logic¶
The core of PoisonPal uses C_UnitAuras.GetPlayerAuraBySpellID() to check whether the player has an active poison buff. We iterate through each poison table and return the name of the first match found.
-- Check if the player has any buff from a given poison table
local function FindActivePoison(poisonTable)
for spellID, name in pairs(poisonTable) do
local aura = C_UnitAuras.GetPlayerAuraBySpellID(spellID)
if aura then
return name
end
end
return nil
end
-- Returns: lethalName or nil, nonLethalName or nil
local function GetPoisonStatus()
local lethal = FindActivePoison(LETHAL_POISONS)
local nonLethal = FindActivePoison(NON_LETHAL_POISONS)
return lethal, nonLethal
end
local function CheckPoisons()
if not isRogue or not ns.db or not ns.db.enabled then return end
local lethal, nonLethal = GetPoisonStatus()
local warnings = {}
if not lethal then
warnings[#warnings + 1] = "Lethal poison is MISSING!"
end
if not nonLethal then
warnings[#warnings + 1] = "Non-lethal poison is MISSING!"
end
return warnings, lethal, nonLethal
end
-- Export for Addon Compartment use
ns.CheckPoisons = CheckPoisons
ns.GetPoisonStatus = GetPoisonStatus
GetWeaponEnchantInfo() Does NOT Detect Poisons
This is the most common mistake when building a poison reminder addon. AI coding assistants trained on pre-Dragonflight code will generate GetWeaponEnchantInfo() for poison detection. This has not worked for poisons since Dragonflight (10.0). Poisons are now player buffs detected via C_UnitAuras.GetPlayerAuraBySpellID(spellID).
Alert Output¶
We keep alerts simple — chat messages with color coding. This is blizzard-faithful mode: no custom frames needed for basic warnings.
local CHAT_PREFIX = "|cff00cc66[PoisonPal]|r "
local function PrintWarnings(warnings)
if not warnings or #warnings == 0 then return end
for _, msg in ipairs(warnings) do
print(CHAT_PREFIX .. "|cffff6600" .. msg .. "|r")
end
-- Play warning sound if enabled
if ns.db.playSound then
PlaySound(SOUNDKIT.RAID_WARNING, "Master")
end
end
local function PrintStatus()
local warnings, lethal, nonLethal = CheckPoisons()
if lethal then
print(CHAT_PREFIX .. "|cff00ff00Lethal:|r " .. lethal)
else
print(CHAT_PREFIX .. "|cffff6600Lethal:|r None applied")
end
if nonLethal then
print(CHAT_PREFIX .. "|cff00ff00Non-lethal:|r " .. nonLethal)
else
print(CHAT_PREFIX .. "|cffff6600Non-lethal:|r None applied")
end
end
ns.PrintStatus = PrintStatus
Event Handlers¶
Now we wire up the events that trigger poison checks:
-- ADDON_LOADED: initialize SavedVariables
RegisterEvent("ADDON_LOADED", function(self, event, loadedAddon)
if loadedAddon ~= addonName then return end
UnregisterEvent("ADDON_LOADED")
-- Skip everything for non-Rogues
if not isRogue then return end
-- Initialize SavedVariables
if not PoisonPalDB then
PoisonPalDB = {}
end
ns.db = PoisonPalDB
-- Merge defaults for new/missing keys
for key, defaultValue in pairs(ns.defaults) do
if ns.db[key] == nil then
ns.db[key] = defaultValue
end
end
-- Register slash commands
SLASH_POISONPAL1 = "/poisonpal"
SLASH_POISONPAL2 = "/pp"
SlashCmdList["POISONPAL"] = function(input)
local cmd = strlower(strtrim(input or ""))
if cmd == "config" or cmd == "options" then
if ns.settingsCategoryID then
Settings.OpenToCategory(ns.settingsCategoryID)
end
elseif cmd == "check" then
PrintStatus()
else
print(CHAT_PREFIX .. "Commands:")
print(" /pp check — Check poison status now")
print(" /pp config — Open settings")
end
end
-- Register settings panel
if ns.RegisterSettings then
ns:RegisterSettings()
end
end)
-- PLAYER_ENTERING_WORLD: check poisons on login and instance entry
RegisterEvent("PLAYER_ENTERING_WORLD", function(self, event, isLogin, isReload)
if not isRogue or not ns.db or not ns.db.enabled then return end
if ns.db.showOnLogin and (isLogin or isReload) then
-- Delay slightly — aura data may not be ready immediately
C_Timer.After(2, function()
local warnings = CheckPoisons()
if warnings and #warnings > 0 then
PrintWarnings(warnings)
end
end)
end
end)
-- UNIT_AURA: fires when buffs change on a unit (including poison application/removal)
RegisterEvent("UNIT_AURA", function(self, event, unit)
if unit ~= "player" then return end
if not isRogue or not ns.db or not ns.db.enabled then return end
-- Check after a brief delay (aura data updates slightly after the event)
C_Timer.After(0.5, function()
local warnings = CheckPoisons()
if warnings and #warnings > 0 then
PrintWarnings(warnings)
end
end)
end)
Why UNIT_AURA, not UNIT_INVENTORY_CHANGED?
Rogue poisons are player buffs in modern WoW. UNIT_AURA fires when buffs and debuffs change on a unit — this is the correct event for detecting poison application and removal. UNIT_INVENTORY_CHANGED fires when equipment changes, which does not include poison buffs. This is the opposite of what older addon guides (and AI-generated code) will tell you.
RegisterUnitEvent for Efficiency
For production addons, prefer eventFrame:RegisterUnitEvent("UNIT_AURA", "player") over eventFrame:RegisterEvent("UNIT_AURA"). The unit-filtered version only fires for the specified unit, avoiding unnecessary handler calls when other units' auras change. We use the simpler form here for clarity.
Periodic Check¶
Since poisons don't expire in modern WoW, the periodic check is a safety net — it catches edge cases like poisons being removed by mechanics or the player cancelling a buff.
RegisterEvent("PLAYER_LOGIN", function()
if not isRogue then return end
UnregisterEvent("PLAYER_LOGIN")
-- Periodic check every 60 seconds as a safety net
ns.checkTicker = C_Timer.NewTicker(60, function()
if not ns.db or not ns.db.enabled then return end
local warnings = CheckPoisons()
if warnings and #warnings > 0 then
PrintWarnings(warnings)
end
end)
end)
Checkpoint
At this point, Core.lua handles class detection, SavedVariables, event-driven poison monitoring via aura detection, slash commands, and chat alerts. Save the file and move on to the settings panel.
Step 5: Alert System¶
Our blizzard-faithful approach keeps alerts in chat output using print() with WoW color codes. This is intentional — no custom frames to maintain, no taint risk, no combat lockdown issues. Chat messages work everywhere, always.
The color coding gives visual urgency:
| Color Code | Hex | Used For |
|---|---|---|
\|cff00cc66 | Green | PoisonPal prefix |
\|cffff6600 | Orange | Missing poison warnings |
\|cff00ff00 | Bright green | Active poison names / all-clear |
For the sound alert, PlaySound(SOUNDKIT.RAID_WARNING, "Master") plays the familiar raid warning sound through the master audio channel, so it's audible even if other sound channels are muted.
Want a Visual Alert Frame?
A blizzard-faithful addon can still create its own frames — the restriction is against hooking or modifying Blizzard's frames. If you want a popup alert instead of chat messages, create a simple frame with CreateFrame("Frame", nil, UIParent, "BackdropTemplate"), position it, and show/hide it on warnings. See the starter template's Core.lua for a complete draggable frame example.
Step 6: Settings Panel¶
Create Config.lua. This uses the modern Settings API introduced in 10.0 and updated in 11.0.2 — not the deprecated InterfaceOptions_AddCategory() or AceConfig.
local addonName, ns = ...
function ns:RegisterSettings()
-- Create a vertical layout category (auto-stacking controls)
local category = Settings.RegisterVerticalLayoutCategory(addonName)
ns.settingsCategoryID = category:GetID()
-- ================================================================
-- Checkbox: Enable/Disable
-- ================================================================
do
local setting = Settings.RegisterAddOnSetting(category,
"PoisonPal_Enabled", -- Unique ID
"enabled", -- Key in ns.db
ns.db, -- SavedVariables table
type(true), -- Variable type (boolean)
"Enable PoisonPal", -- Display name
ns.defaults.enabled -- Default value
)
Settings.CreateCheckbox(category, setting,
"Enable or disable poison monitoring and alerts.")
setting:SetValueChangedCallback(function(_, newValue)
ns.db.enabled = newValue
if newValue then
print("|cff00cc66[PoisonPal]|r Enabled.")
else
print("|cff00cc66[PoisonPal]|r Disabled.")
end
end)
end
-- ================================================================
-- Checkbox: Play Sound
-- ================================================================
do
local setting = Settings.RegisterAddOnSetting(category,
"PoisonPal_Sound",
"playSound",
ns.db,
type(true),
"Play Warning Sound",
ns.defaults.playSound
)
Settings.CreateCheckbox(category, setting,
"Play the raid warning sound when poisons are missing.")
end
-- ================================================================
-- Checkbox: Check on Login
-- ================================================================
do
local setting = Settings.RegisterAddOnSetting(category,
"PoisonPal_ShowOnLogin",
"showOnLogin",
ns.db,
type(true),
"Check on Login",
ns.defaults.showOnLogin
)
Settings.CreateCheckbox(category, setting,
"Automatically check poison status when you log in or reload the UI.")
end
-- Register the category (must be last)
Settings.RegisterAddOnCategory(category)
end
How the Settings API Works¶
The Settings API is a three-step process:
-
Register a category —
Settings.RegisterVerticalLayoutCategory(name)creates a section in Game Menu Options AddOns that auto-stacks controls vertically. -
Register settings —
Settings.RegisterAddOnSetting()binds a setting to a key in your SavedVariables table. The 11.0.2+ signature takes 7 arguments:Settings.RegisterAddOnSetting( categoryTbl, -- From RegisterVerticalLayoutCategory variable, -- Unique string ID (convention: "AddonName_Setting") variableKey, -- Key name in the table (string) variableTbl, -- Your SavedVariables table (ns.db) variableType, -- type() of the value: type(true), type(1), type("") name, -- Display name for the player defaultValue -- Used by the "Defaults" button ) -
Create controls —
Settings.CreateCheckbox(),Settings.CreateSlider(), andSettings.CreateDropdown()generate the visual controls. Each takes the category, the setting object, and tooltip text.
Do Not Use the Old API
InterfaceOptions_AddCategory() and InterfaceOptionsFrame_OpenToCategory() were removed in Dragonflight. If you see addon code using these, it's outdated. Always use Settings.RegisterAddOnCategory() and Settings.OpenToCategory().
Step 7: Addon Compartment¶
The Addon Compartment is the minimap dropdown introduced in 10.1.0. It replaced the old approach of creating a minimap button with LibDBIcon. Three TOC fields register your addon, and three global functions handle interaction.
We already declared the TOC fields in Step 3. Now add the callback functions to the bottom of Core.lua:
-- ============================================================================
-- Addon Compartment Callbacks
-- ============================================================================
-- These MUST be global functions — the TOC references them by name.
-- They only execute for Rogues because CheckPoisons() has the class guard.
-- Left-click: check poison status. Right-click: open settings.
function PoisonPal_OnCompartmentClick(addonName, buttonName)
if not isRogue then
print(CHAT_PREFIX .. "PoisonPal only works on Rogue characters.")
return
end
if buttonName == "RightButton" then
if ns.settingsCategoryID then
Settings.OpenToCategory(ns.settingsCategoryID)
end
else
PrintStatus()
end
end
-- Tooltip on hover
function PoisonPal_OnCompartmentEnter(addonName, menuButtonFrame)
GameTooltip:SetOwner(menuButtonFrame, "ANCHOR_LEFT")
GameTooltip:SetText("PoisonPal")
if isRogue then
local lethal, nonLethal = GetPoisonStatus()
if lethal then
GameTooltip:AddLine("Lethal: " .. lethal, 0, 1, 0)
else
GameTooltip:AddLine("Lethal: NONE", 1, 0.4, 0)
end
if nonLethal then
GameTooltip:AddLine("Non-lethal: " .. nonLethal, 0, 1, 0)
else
GameTooltip:AddLine("Non-lethal: NONE", 1, 0.4, 0)
end
GameTooltip:AddLine(" ")
GameTooltip:AddLine("Left-click to check status.", 0.7, 0.7, 0.7)
GameTooltip:AddLine("Right-click for settings.", 0.7, 0.7, 0.7)
else
GameTooltip:AddLine("Only active on Rogue characters.", 0.7, 0.7, 0.7)
end
GameTooltip:Show()
end
function PoisonPal_OnCompartmentLeave(addonName, menuButtonFrame)
GameTooltip:Hide()
end
The tooltip is the real value of the Addon Compartment integration. Players can hover over PoisonPal in the minimap dropdown and see their poison status at a glance — green for active (with poison name), orange for missing — without clicking anything.
Addon Compartment vs. LibDBIcon
The Addon Compartment is a native Blizzard feature — no library dependency required. It's the blizzard-faithful way to add a minimap presence. LibDBIcon creates a standalone minimap button with more customization options, but it requires an external library and adds complexity. For most addons, the Compartment is sufficient.
Step 8: Testing and Review¶
Install the Addon¶
Copy the PoisonPal folder to your WoW AddOns directory:
In-Game Testing¶
- Launch WoW and log in on a Rogue character
- Check the addon list (Game Menu AddOns) — PoisonPal should appear with its icon
- After loading in, you should see a warning if poisons aren't applied
- Apply poisons and type
/pp check— you should see the poison names displayed - Open settings with
/pp configand verify the controls work - Find PoisonPal in the minimap Addon Compartment dropdown and hover for the tooltip
- Log in on a non-Rogue character — PoisonPal should be completely silent
Useful Debug Commands¶
/reload -- Reload the UI without logging out
/dump PoisonPalDB -- Inspect your SavedVariables
/dump C_UnitAuras.GetPlayerAuraBySpellID(2823) -- Check Deadly Poison buff
/console scriptErrors 1 -- Show Lua errors inline
Install BugGrabber + BugSack
These two addons capture Lua errors and display them in a browsable window instead of spamming your chat. Essential for addon development.
Run a Review¶
With better-addons installed, run a code review:
The Reviewer agent checks for deprecated APIs, taint risks, Secret Values issues, combat lockdown violations, and Lua 5.1 compatibility. PoisonPal should score well — we used only documented APIs, avoided hooking any Blizzard frames, and don't touch any values that could be secret.
Common Mistakes¶
| Symptom | Cause | Fix |
|---|---|---|
| "Addon not found" in addon list | Folder name doesn't match .toc filename | Ensure both are exactly PoisonPal |
| No warnings appear | SavedVariables enabled is false, or not a Rogue | Check /dump PoisonPalDB and character class |
| Poisons applied but addon says missing | Using GetWeaponEnchantInfo() instead of C_UnitAuras | Modern poisons are buffs — use C_UnitAuras.GetPlayerAuraBySpellID() |
| Settings panel missing | RegisterSettings called before ADDON_LOADED | Ensure Settings registration happens inside the ADDON_LOADED handler |
| Slash commands don't work | SLASH_POISONPAL1 not set as global | Slash command globals must not be local |
| Warnings fire on every aura change | No debounce on UNIT_AURA handler | Use C_Timer.After(0.5, ...) to debounce |
Step 9: Publishing¶
When your addon is ready for public release, you need two things: a .pkgmeta file for the BigWigsMods/packager, and a GitHub Actions workflow.
.pkgmeta¶
Create .pkgmeta in the project root:
package-as: PoisonPal
enable-nolib-creation: no
ignore:
- .github
- .vscode
- .luacheckrc
- README.md
- CLAUDE.md
Since PoisonPal uses no external libraries, the config is minimal. package-as must match your .toc and folder name.
GitHub Actions¶
Create .github/workflows/release.yml:
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: BigWigsMods/packager@v2
with:
args: -g ${{ github.event.repository.full_name }}
env:
CF_API_KEY: ${{ secrets.CF_API_KEY }}
WAGO_API_TOKEN: ${{ secrets.WAGO_API_TOKEN }}
GITHUB_OAUTH: ${{ secrets.GITHUB_TOKEN }}
Release Workflow¶
- Create projects on CurseForge and/or Wago.io
- Add your project IDs to the
.toc: - Add API keys as GitHub repository secrets (
CF_API_KEY,WAGO_API_TOKEN) - Tag and push:
The packager builds a distributable .zip, replaces @project-version@ with the tag, and uploads to your configured platforms.
Step 10: Complete Code¶
Here are the three files in their entirety. Copy these into a PoisonPal folder, place it in your WoW AddOns directory, and you have a working addon.
PoisonPal.toc¶
## Interface: 120001
## Title: PoisonPal
## Notes: Rogue poison reminder — warns when poisons are missing.
## Author: YourName
## Version: @project-version@
## SavedVariables: PoisonPalDB
## IconTexture: Interface\Icons\Ability_Rogue_DualWeild
## AddonCompartmentFunc: PoisonPal_OnCompartmentClick
## AddonCompartmentFuncOnEnter: PoisonPal_OnCompartmentEnter
## AddonCompartmentFuncOnLeave: PoisonPal_OnCompartmentLeave
Core.lua
Config.lua
Core.lua¶
-- ============================================================================
-- Core.lua — PoisonPal: Rogue Poison Reminder
-- ============================================================================
-- Monitors poison buffs via C_UnitAuras, warns when missing.
-- Only active on Rogue characters. Uses blizzard-faithful patterns.
-- ============================================================================
local addonName, ns = ...
ns.addonName = addonName
-- Default settings
ns.defaults = {
enabled = true,
playSound = true,
showOnLogin = true,
}
-- Lethal poisons (spell IDs for the player buff)
local LETHAL_POISONS = {
[2823] = "Deadly Poison",
[315584] = "Instant Poison",
[8679] = "Wound Poison",
[381637] = "Amplifying Poison",
}
-- Non-lethal poisons (spell IDs for the player buff)
local NON_LETHAL_POISONS = {
[3408] = "Crippling Poison",
[5761] = "Numbing Poison",
[381664] = "Atrophic Poison",
}
-- ============================================================================
-- Event Dispatcher
-- ============================================================================
local eventHandlers = {}
local eventFrame = CreateFrame("Frame")
eventFrame:SetScript("OnEvent", function(self, event, ...)
local handler = eventHandlers[event]
if handler then handler(self, event, ...) end
end)
local function RegisterEvent(event, handler)
eventHandlers[event] = handler
eventFrame:RegisterEvent(event)
end
local function UnregisterEvent(event)
eventHandlers[event] = nil
eventFrame:UnregisterEvent(event)
end
ns.RegisterEvent = RegisterEvent
ns.UnregisterEvent = UnregisterEvent
-- ============================================================================
-- Class Check
-- ============================================================================
local _, playerClass = UnitClass("player")
local isRogue = (playerClass == "ROGUE")
-- ============================================================================
-- Poison Detection (Aura-based)
-- ============================================================================
local function FindActivePoison(poisonTable)
for spellID, name in pairs(poisonTable) do
local aura = C_UnitAuras.GetPlayerAuraBySpellID(spellID)
if aura then
return name
end
end
return nil
end
local function GetPoisonStatus()
local lethal = FindActivePoison(LETHAL_POISONS)
local nonLethal = FindActivePoison(NON_LETHAL_POISONS)
return lethal, nonLethal
end
local function CheckPoisons()
if not isRogue or not ns.db or not ns.db.enabled then return end
local lethal, nonLethal = GetPoisonStatus()
local warnings = {}
if not lethal then
warnings[#warnings + 1] = "Lethal poison is MISSING!"
end
if not nonLethal then
warnings[#warnings + 1] = "Non-lethal poison is MISSING!"
end
return warnings, lethal, nonLethal
end
ns.CheckPoisons = CheckPoisons
ns.GetPoisonStatus = GetPoisonStatus
-- ============================================================================
-- Chat Output
-- ============================================================================
local CHAT_PREFIX = "|cff00cc66[PoisonPal]|r "
local function PrintWarnings(warnings)
if not warnings or #warnings == 0 then return end
for _, msg in ipairs(warnings) do
print(CHAT_PREFIX .. "|cffff6600" .. msg .. "|r")
end
if ns.db.playSound then
PlaySound(SOUNDKIT.RAID_WARNING, "Master")
end
end
local function PrintStatus()
local warnings, lethal, nonLethal = CheckPoisons()
if lethal then
print(CHAT_PREFIX .. "|cff00ff00Lethal:|r " .. lethal)
else
print(CHAT_PREFIX .. "|cffff6600Lethal:|r None applied")
end
if nonLethal then
print(CHAT_PREFIX .. "|cff00ff00Non-lethal:|r " .. nonLethal)
else
print(CHAT_PREFIX .. "|cffff6600Non-lethal:|r None applied")
end
end
ns.PrintStatus = PrintStatus
-- ============================================================================
-- ADDON_LOADED
-- ============================================================================
RegisterEvent("ADDON_LOADED", function(self, event, loadedAddon)
if loadedAddon ~= addonName then return end
UnregisterEvent("ADDON_LOADED")
if not isRogue then return end
-- Initialize SavedVariables
if not PoisonPalDB then
PoisonPalDB = {}
end
ns.db = PoisonPalDB
for key, defaultValue in pairs(ns.defaults) do
if ns.db[key] == nil then
ns.db[key] = defaultValue
end
end
-- Slash commands
SLASH_POISONPAL1 = "/poisonpal"
SLASH_POISONPAL2 = "/pp"
SlashCmdList["POISONPAL"] = function(input)
local cmd = strlower(strtrim(input or ""))
if cmd == "config" or cmd == "options" then
if ns.settingsCategoryID then
Settings.OpenToCategory(ns.settingsCategoryID)
end
elseif cmd == "check" then
PrintStatus()
else
print(CHAT_PREFIX .. "Commands:")
print(" /pp check — Check poison status now")
print(" /pp config — Open settings")
end
end
-- Register settings panel
if ns.RegisterSettings then
ns:RegisterSettings()
end
end)
-- ============================================================================
-- PLAYER_ENTERING_WORLD
-- ============================================================================
RegisterEvent("PLAYER_ENTERING_WORLD", function(self, event, isLogin, isReload)
if not isRogue or not ns.db or not ns.db.enabled then return end
if ns.db.showOnLogin and (isLogin or isReload) then
C_Timer.After(2, function()
local warnings = CheckPoisons()
if warnings and #warnings > 0 then
PrintWarnings(warnings)
end
end)
end
end)
-- ============================================================================
-- UNIT_AURA — poison buff applied/removed
-- ============================================================================
RegisterEvent("UNIT_AURA", function(self, event, unit)
if unit ~= "player" then return end
if not isRogue or not ns.db or not ns.db.enabled then return end
C_Timer.After(0.5, function()
local warnings = CheckPoisons()
if warnings and #warnings > 0 then
PrintWarnings(warnings)
end
end)
end)
-- ============================================================================
-- Periodic Check — safety net for edge cases
-- ============================================================================
RegisterEvent("PLAYER_LOGIN", function()
if not isRogue then return end
UnregisterEvent("PLAYER_LOGIN")
ns.checkTicker = C_Timer.NewTicker(60, function()
if not ns.db or not ns.db.enabled then return end
local warnings = CheckPoisons()
if warnings and #warnings > 0 then
PrintWarnings(warnings)
end
end)
end)
-- ============================================================================
-- Addon Compartment Callbacks (must be global)
-- ============================================================================
function PoisonPal_OnCompartmentClick(addonName, buttonName)
if not isRogue then
print(CHAT_PREFIX .. "PoisonPal only works on Rogue characters.")
return
end
if buttonName == "RightButton" then
if ns.settingsCategoryID then
Settings.OpenToCategory(ns.settingsCategoryID)
end
else
PrintStatus()
end
end
function PoisonPal_OnCompartmentEnter(addonName, menuButtonFrame)
GameTooltip:SetOwner(menuButtonFrame, "ANCHOR_LEFT")
GameTooltip:SetText("PoisonPal")
if isRogue then
local lethal, nonLethal = GetPoisonStatus()
if lethal then
GameTooltip:AddLine("Lethal: " .. lethal, 0, 1, 0)
else
GameTooltip:AddLine("Lethal: NONE", 1, 0.4, 0)
end
if nonLethal then
GameTooltip:AddLine("Non-lethal: " .. nonLethal, 0, 1, 0)
else
GameTooltip:AddLine("Non-lethal: NONE", 1, 0.4, 0)
end
GameTooltip:AddLine(" ")
GameTooltip:AddLine("Left-click to check status.", 0.7, 0.7, 0.7)
GameTooltip:AddLine("Right-click for settings.", 0.7, 0.7, 0.7)
else
GameTooltip:AddLine("Only active on Rogue characters.", 0.7, 0.7, 0.7)
end
GameTooltip:Show()
end
function PoisonPal_OnCompartmentLeave(addonName, menuButtonFrame)
GameTooltip:Hide()
end
Config.lua¶
-- ============================================================================
-- Config.lua — PoisonPal Settings Panel
-- ============================================================================
-- Uses the modern Settings API (11.0.2+ signatures).
-- Creates controls in Game Menu > Options > AddOns > PoisonPal.
-- ============================================================================
local addonName, ns = ...
function ns:RegisterSettings()
local category = Settings.RegisterVerticalLayoutCategory(addonName)
ns.settingsCategoryID = category:GetID()
-- Enable/Disable
do
local setting = Settings.RegisterAddOnSetting(category,
"PoisonPal_Enabled",
"enabled",
ns.db,
type(true),
"Enable PoisonPal",
ns.defaults.enabled
)
Settings.CreateCheckbox(category, setting,
"Enable or disable poison monitoring and alerts.")
setting:SetValueChangedCallback(function(_, newValue)
ns.db.enabled = newValue
if newValue then
print("|cff00cc66[PoisonPal]|r Enabled.")
else
print("|cff00cc66[PoisonPal]|r Disabled.")
end
end)
end
-- Play Sound
do
local setting = Settings.RegisterAddOnSetting(category,
"PoisonPal_Sound",
"playSound",
ns.db,
type(true),
"Play Warning Sound",
ns.defaults.playSound
)
Settings.CreateCheckbox(category, setting,
"Play the raid warning sound when poisons are missing.")
end
-- Check on Login
do
local setting = Settings.RegisterAddOnSetting(category,
"PoisonPal_ShowOnLogin",
"showOnLogin",
ns.db,
type(true),
"Check on Login",
ns.defaults.showOnLogin
)
Settings.CreateCheckbox(category, setting,
"Automatically check poison status when you log in or reload the UI.")
end
Settings.RegisterAddOnCategory(category)
end
What You Learned¶
This tutorial covered the core building blocks of modern WoW addon development:
| Concept | What You Used |
|---|---|
| Project structure | .toc manifest, file load order, SavedVariables declaration |
| Namespace pattern | local addonName, ns = ... for cross-file communication |
| Event dispatch | Table-dispatch pattern with RegisterEvent / UnregisterEvent helpers |
| Initialization order | ADDON_LOADED for DB, PLAYER_LOGIN for features, PLAYER_ENTERING_WORLD for world state |
| Aura detection API | C_UnitAuras.GetPlayerAuraBySpellID() for poison buff detection |
| Timer API | C_Timer.After() for delays, C_Timer.NewTicker() for periodic checks |
| Settings API | RegisterVerticalLayoutCategory, RegisterAddOnSetting, CreateCheckbox |
| Addon Compartment | TOC fields + global callback functions for minimap dropdown integration |
| Slash commands | SLASH_NAME1 globals + SlashCmdList handler |
| Publishing | .pkgmeta + GitHub Actions + BigWigsMods/packager |
Common AI Mistakes This Tutorial Avoids¶
| Mistake | Why It's Wrong | What We Did Instead |
|---|---|---|
Using GetWeaponEnchantInfo() for poisons | Poisons are player buffs since Dragonflight, not weapon enchants | C_UnitAuras.GetPlayerAuraBySpellID() |
Listening to UNIT_INVENTORY_CHANGED | Equipment events don't fire for buff changes | UNIT_AURA event |
| Tracking poison expiry timers | Modern poisons are permanent until cancelled | Check presence/absence only |
Using InterfaceOptionsCheckButtonTemplate | Deprecated since Dragonflight | Modern Settings API |
Next Steps¶
- Starter Template — The full template with more patterns: draggable frames, combat lockdown queue, hooksecurefunc hooks
- API Cheat Sheet — Verified 12.0.1 API signatures for quick reference
- Code Patterns — 10 production patterns from real addons
- Plugin Commands — All 10
/wow-*commands with detailed usage - Enhancement Tutorials — Advanced tutorials for frame skinning and UI enhancement