Initial CoaExporter: merge AscensionExporter + CoA skill/talent dumpers

Folds three previously-separate Lua addons into one for guild-member use:

  - ascension-char-exporter (per-character JSON/Wiki.js Markdown via /ascx)
  - CoA_SkillExporter      (skills/dispels/passives catalog via /skilldump)
  - CoA_TalentExporter     (talent-tree catalog via /talentdumpall)

The two CoA catalog dumpers were ~90% identical entry-walkers. Pulled the
shared C_CharacterAdvancement.GetAllEntries() loop into Catalogs/Common.lua
and have Skills.lua / Talents.lua register collectors that share a single
scan pass (so /coae catalog all walks the entry list once, not twice).

Per-character collectors (Talents, Gear, Enchants, MysticScrolls,
MysticScrollProbe) and the AtlasLootAscension-derived ScrollCatalog data
are kept verbatim, just rebranded.

Slash interface:

  /coae export {all|talents|gear|enchants|mdgear|mdenchants|md}
  /coae catalog {all|skills|talents|dispels [class]|passives [class]|status}
  /coae scrolls {scan|export|reset|status}
  /coae sv on|off | debug | help

Aliases: /coaexp, /ascx, /asxc, plus legacy /skilldump /talentdumpall
/dispels /passives that map to the catalog subcommands.

SavedVariables: CoaExporterSaved, CoaExporterConfig, CoaExporterScrollCache,
CoaExporterCatalog (skills/dispels/levelPassives/talents/_meta).
This commit is contained in:
2026-05-07 10:43:16 +02:00
commit 2b97a68317
17 changed files with 2988 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
dist/*.zip
*.swp
.DS_Store
+206
View File
@@ -0,0 +1,206 @@
-- CoaExporter / Catalogs / Common.lua
--
-- Shared engine for catalog dumps. Walks
-- C_CharacterAdvancement.GetAllEntries() in 100-entry batches via OnUpdate
-- and dispatches each entry to every registered collector. Used by
-- Catalogs/Skills.lua and Catalogs/Talents.lua so we only walk the (large)
-- entry list once.
CoaExporter = _G.CoaExporter or {}
_G.CoaExporter = CoaExporter
local AE = CoaExporter
AE.Catalog = AE.Catalog or {}
local C = AE.Catalog
C._collectors = C._collectors or {}
C._isRunning = false
-- Shared SavedVariable for catalog output. Each collector writes its own
-- top-level key (skills, dispels, levelPassives, talents, ...).
CoaExporterCatalog = CoaExporterCatalog or {}
local TalentScanner
local function GetCatalogTooltip(spellId)
if not TalentScanner then
TalentScanner = CreateFrame("GameTooltip", "CoaExpCatalogScanner", nil, "GameTooltipTemplate")
end
local scanner = TalentScanner
scanner:SetOwner(UIParent, "ANCHOR_NONE")
scanner:ClearLines()
local ok = pcall(function() scanner:SetHyperlink("spell:" .. spellId) end)
if not ok or scanner:NumLines() == 0 then return "No description" end
local text = ""
for i = 1, scanner:NumLines() do
local line = _G["CoaExpCatalogScannerTextLeft"..i]
if line then
local t = line:GetText()
if t and t ~= "" then text = text .. t .. "\n" end
end
end
return text
end
C.GetCatalogTooltip = GetCatalogTooltip
local function log(msg)
DEFAULT_CHAT_FRAME:AddMessage("CoaExp catalog: " .. tostring(msg))
end
C._log = log
-- Collectors register a handler that gets called for each valid entry.
-- spec = {
-- name = "skills", -- identifier (also used as SV key)
-- onStart = function(ctx) end, -- reset state
-- onEntry = function(ctx, entry, info) end,
-- onFinish = function(ctx) end, -- write into CoaExporterCatalog[name]
-- }
function C.Register(spec)
assert(spec and spec.name, "catalog collector needs a name")
C._collectors[spec.name] = spec
end
local function activeCollectors(filter)
local out = {}
if filter == nil or filter == "all" then
for _, c in pairs(C._collectors) do out[#out+1] = c end
elseif type(filter) == "string" then
local one = C._collectors[filter]
if one then out[#out+1] = one end
end
table.sort(out, function(a, b) return (a.name or "") < (b.name or "") end)
return out
end
-- One-pass scan. filter: "all", "skills", "talents", or nil.
function C.Run(filter, callback)
if C._isRunning then
log("already running, ignoring")
return false
end
if not C_CharacterAdvancement or not C_CharacterAdvancement.GetAllEntries then
log("ERROR: C_CharacterAdvancement.GetAllEntries missing - are you on Ascension 3.3.5?")
return false
end
local collectors = activeCollectors(filter)
if #collectors == 0 then
log("no collectors matched filter: " .. tostring(filter))
return false
end
local ctx = { startedAt = date(), filter = filter or "all" }
for _, col in ipairs(collectors) do
if col.onStart then col.onStart(ctx) end
end
local allEntries = C_CharacterAdvancement.GetAllEntries()
local entryKeys = {}
for k in pairs(allEntries) do table.insert(entryKeys, k) end
local total = #entryKeys
local idx = 1
log(string.format("scanning %d entries for [%s]", total, ctx.filter))
C._isRunning = true
local frame = CreateFrame("Frame")
frame:SetScript("OnUpdate", function(self)
if not C._isRunning then
self:SetScript("OnUpdate", nil)
return
end
local batch = 100
local processed = 0
while processed < batch and idx <= total do
local k = entryKeys[idx]
local v = allEntries[k]
local entry
if type(v) == "table" and v.Class then
entry = v
else
local id = tonumber(k) or (type(v) == "table" and v.ID)
if id then entry = C_CharacterAdvancement.GetEntryByInternalID(id) end
end
if entry and entry.Class then
-- Build a normalized info bag every collector can use.
local spellId = 0
local allSpells = {}
if entry.Spells then
if type(entry.Spells) == "table" then
spellId = tonumber(entry.Spells[1]) or 0
for _, sp in ipairs(entry.Spells) do
table.insert(allSpells, tonumber(sp) or 0)
end
else
spellId = tonumber(entry.Spells) or 0
table.insert(allSpells, spellId)
end
end
local name, _, icon
if spellId > 0 then name, _, icon = GetSpellInfo(spellId) end
local tooltip = GetCatalogTooltip(spellId > 0 and spellId or entry.ID)
if not name or name == "" then
name = tooltip:match("^([^\n]+)") or ("ID:" .. tostring(entry.ID))
end
local tierVal = tonumber(entry.PositionY) or tonumber(entry.Row) or tonumber(entry.y) or 0
local colVal = tonumber(entry.PositionX) or tonumber(entry.Column) or tonumber(entry.x) or tonumber(entry.Col) or 0
-- Garbage filter: tier=0/col=0/no-icon AND no-spell entries
local valid = true
if tierVal == 0 and colVal == 0 and (not icon or icon == "") then valid = false end
if spellId == 0 and (not icon or icon == "") then valid = false end
if valid then
local info = {
cls = tostring(entry.Class),
tab = tostring(entry.Tab or "General"),
entryType = tostring(entry.Type or "Talent"),
name = name,
spellId = spellId,
allSpells = allSpells,
icon = icon or "",
tier = tierVal,
col = colVal,
tooltip = tooltip,
}
for _, col in ipairs(collectors) do
if col.onEntry then col.onEntry(ctx, entry, info) end
end
end
end
idx = idx + 1
processed = processed + 1
end
if idx > total then
C._isRunning = false
self:SetScript("OnUpdate", nil)
for _, col in ipairs(collectors) do
if col.onFinish then col.onFinish(ctx) end
end
CoaExporterCatalog._meta = {
lastScanAt = ctx.startedAt,
filter = ctx.filter,
totalEntries = total,
}
log(string.format("done. %d entries processed; /reload to flush SavedVariables.", total))
if callback then callback(ctx) end
else
local pct = math.floor((idx / total) * 100)
if pct > 0 and pct % 10 == 0 and (ctx._lastPct or -1) ~= pct then
ctx._lastPct = pct
log(string.format(" %d%% (%d/%d)", pct, idx, total))
end
end
end)
return true
end
AE._loadedCatalogCommon = true
+192
View File
@@ -0,0 +1,192 @@
-- CoaExporter / Catalogs / Skills.lua
--
-- Dumps the per-class skill catalog: abilities, level passives, and
-- dispel abilities, for all 21 CoA classes. Plugs into Catalogs/Common.lua
-- so it shares the entry-walk with Catalogs/Talents.lua.
--
-- Output (in CoaExporterCatalog):
-- skills = { [class][tab] = { abilities, levelPassives, talents } }
-- dispels = { [class] = { skillData... } }
-- levelPassives = { [class] = { skillData... } }
CoaExporter = _G.CoaExporter or {}
_G.CoaExporter = CoaExporter
local AE = CoaExporter
local C = AE.Catalog
local DISPEL_KEYWORDS = {
"dispel", "dispels", "dispelling",
"removes 1", "removes 2", "removes 3",
"removing 1", "removing 2", "removing 3",
"purify", "cleanse", "cleansing",
"purge",
}
local function HasDispel(tooltip)
if not tooltip then return false end
local lower = string.lower(tooltip)
for _, kw in ipairs(DISPEL_KEYWORDS) do
if string.find(lower, kw) then return true end
end
return false
end
local function GetDispelSnippet(tooltip)
if not tooltip then return "" end
for line in string.gmatch(tooltip, "[^\n]+") do
local lower = string.lower(line)
if string.find(lower, "dispel") or
string.find(lower, "removes") or
string.find(lower, "purify") or
string.find(lower, "cleanse") then
return string.sub(line, 1, 150)
end
end
return ""
end
local function IsLevelPassive(name)
return name and string.find(name, "Level") and string.find(name, "Passive")
end
local skillsByClass, dispelsByClass, levelPassivesByClass
C.Register({
name = "skills",
onStart = function(_)
skillsByClass = {}
dispelsByClass = {}
levelPassivesByClass = {}
end,
onEntry = function(_, entry, info)
local cls = info.cls
local tab = info.tab
skillsByClass[cls] = skillsByClass[cls] or {}
skillsByClass[cls][tab] = skillsByClass[cls][tab] or { abilities = {}, levelPassives = {}, talents = {} }
dispelsByClass[cls] = dispelsByClass[cls] or {}
levelPassivesByClass[cls] = levelPassivesByClass[cls] or {}
local skill = {
name = info.name,
spellId = info.spellId,
advId = entry.ID or 0,
icon = info.icon,
tier = info.tier,
col = info.col,
type = info.entryType,
tab = tab,
tooltip = info.tooltip,
hasDispel = HasDispel(info.tooltip),
dispelSnippet = GetDispelSnippet(info.tooltip),
isLevelPassive = IsLevelPassive(info.name) and true or false,
}
if skill.isLevelPassive then
table.insert(skillsByClass[cls][tab].levelPassives, skill)
table.insert(levelPassivesByClass[cls], skill)
elseif info.entryType == "Ability" then
table.insert(skillsByClass[cls][tab].abilities, skill)
else
table.insert(skillsByClass[cls][tab].talents, skill)
end
if skill.hasDispel then
table.insert(dispelsByClass[cls], skill)
end
end,
onFinish = function(ctx)
CoaExporterCatalog.skills = skillsByClass
CoaExporterCatalog.dispels = dispelsByClass
CoaExporterCatalog.levelPassives = levelPassivesByClass
CoaExporterCatalog.skillsMeta = {
scanAt = ctx.startedAt,
}
local classCount, totalDispels, totalPassives = 0, 0, 0
for _, dispels in pairs(dispelsByClass) do
classCount = classCount + 1
totalDispels = totalDispels + #dispels
end
for _, p in pairs(levelPassivesByClass) do
totalPassives = totalPassives + #p
end
C._log(string.format("skills: %d classes, %d dispels, %d level passives",
classCount, totalDispels, totalPassives))
end,
})
-- Read-only helpers used by /coae catalog dispels|passives commands.
function AE.CatalogListDispels(arg)
local dispels = CoaExporterCatalog.dispels
if not dispels then
C._log("no skill catalog yet - run /coae catalog skills")
return
end
if arg and arg ~= "" then
local found = false
for cls, skills in pairs(dispels) do
if string.lower(cls) == string.lower(arg) then
found = true
C._log("=== DISPELS FOR " .. cls:upper() .. " ===")
for _, sk in ipairs(skills) do
local cat = sk.isLevelPassive and "[PASSIVE]"
or (sk.type == "Ability" and "[ABILITY]" or "[TALENT]")
C._log(" " .. cat .. " " .. sk.name .. " (" .. sk.tab .. ")")
if sk.dispelSnippet ~= "" then
C._log(" -> " .. sk.dispelSnippet)
end
end
end
end
if not found then C._log("class not found: " .. arg) end
else
C._log("=== DISPEL SUMMARY (ALL CLASSES) ===")
local sorted = {}
for cls in pairs(dispels) do table.insert(sorted, cls) end
table.sort(sorted)
for _, cls in ipairs(sorted) do
local sk = dispels[cls]
local a, p, t = 0, 0, 0
for _, s in ipairs(sk) do
if s.isLevelPassive then p = p + 1
elseif s.type == "Ability" then a = a + 1
else t = t + 1 end
end
C._log(string.format(" %s: %d total (A:%d P:%d T:%d)", cls, #sk, a, p, t))
end
end
end
function AE.CatalogListPassives(arg)
local passives = CoaExporterCatalog.levelPassives
if not passives then
C._log("no skill catalog yet - run /coae catalog skills")
return
end
if arg and arg ~= "" then
for cls, skills in pairs(passives) do
if string.lower(cls) == string.lower(arg) then
C._log("=== LEVEL PASSIVES FOR " .. cls:upper() .. " ===")
for _, sk in ipairs(skills) do
C._log(" " .. sk.name .. " (" .. sk.tab .. ")")
end
end
end
else
C._log("=== LEVEL PASSIVES SUMMARY ===")
local sorted = {}
for cls in pairs(passives) do table.insert(sorted, cls) end
table.sort(sorted)
for _, cls in ipairs(sorted) do
C._log(string.format(" %s: %d level passives", cls, #passives[cls]))
end
end
end
AE._loadedCatalogSkills = true
+117
View File
@@ -0,0 +1,117 @@
-- CoaExporter / Catalogs / Talents.lua
--
-- Dumps the full talent-tree definitions per class (every node, with
-- prerequisites, children, max ranks, costs, etc.). Plugs into
-- Catalogs/Common.lua so it shares the entry-walk with Skills.
--
-- Output (in CoaExporterCatalog):
-- talents = { [class][tab] = { talents = { ... } } }
CoaExporter = _G.CoaExporter or {}
_G.CoaExporter = CoaExporter
local AE = CoaExporter
local C = AE.Catalog
local function GetReqString(entry)
local raw = entry.RequiredIDs
if raw == nil then raw = entry.RequiredID end
if raw == nil then raw = entry.RequiredId end
if raw == nil then raw = entry.PrerequisiteIDs end
if raw == nil then raw = entry.PrereqIDs end
if raw == nil then raw = entry.Requires end
if type(raw) == "table" then
local out = {}
for _, v in pairs(raw) do
local n = tonumber(v)
if n and n > 0 then table.insert(out, n) end
end
table.sort(out)
return table.concat(out, ",")
end
local n = tonumber(raw)
if n and n > 0 then return tostring(n) end
return "0"
end
local function GetChildrenString(entry)
local raw = entry.ConnectedNodes
if type(raw) ~= "table" then return "0" end
local out = {}
for _, v in pairs(raw) do
local n = tonumber(v)
if n and n > 0 then table.insert(out, n) end
end
if #out == 0 then return "0" end
table.sort(out)
return table.concat(out, ",")
end
local hierarchy
C.Register({
name = "talents",
onStart = function(_)
hierarchy = {}
end,
onEntry = function(_, entry, info)
local cls = info.cls
local tab = info.tab
hierarchy[cls] = hierarchy[cls] or {}
hierarchy[cls][tab] = hierarchy[cls][tab] or { talents = {} }
-- Class tree can be named: "Class", the class name itself, "General", or "{ClassName} Class"
local isClassTalent = (tab == cls or tab == "Class" or tab == "General" or tab == (cls .. " Class"))
local talent = {
name = info.name,
spellId = info.spellId,
advId = entry.ID or 0,
icon = info.icon,
tier = info.tier,
col = info.col,
rank = tonumber(entry.MaxPoints or entry.MaxRank or 1),
reqs = GetReqString(entry),
children = GetChildrenString(entry),
cost = tonumber(entry.AECost or entry.TECost or 0),
type = info.entryType,
isClass = isClassTalent,
tt = info.tooltip,
}
if #(info.allSpells or {}) > 1 then
talent.allSpells = info.allSpells
end
if entry.MinLevel then talent.minLevel = tonumber(entry.MinLevel) end
if entry.ExclusiveWith then
talent.exclusiveWith = (type(entry.ExclusiveWith) == "table") and entry.ExclusiveWith or { entry.ExclusiveWith }
end
if entry.ChoiceIndex then talent.choiceIndex = tonumber(entry.ChoiceIndex) end
if entry.GroupID then talent.groupID = tonumber(entry.GroupID) end
if entry.Description then talent.description = tostring(entry.Description) end
if entry.FlavorText then talent.flavorText = tostring(entry.FlavorText) end
if entry.Category then talent.category = tostring(entry.Category) end
if entry.UnlockCondition then talent.unlockCondition = tostring(entry.UnlockCondition) end
table.insert(hierarchy[cls][tab].talents, talent)
end,
onFinish = function(ctx)
CoaExporterCatalog.talents = hierarchy
CoaExporterCatalog.talentsMeta = { scanAt = ctx.startedAt }
local classCount, totalNodes = 0, 0
for _, tabs in pairs(hierarchy) do
classCount = classCount + 1
for _, t in pairs(tabs) do
totalNodes = totalNodes + #(t.talents or {})
end
end
C._log(string.format("talents: %d classes, %d nodes", classCount, totalNodes))
end,
})
AE._loadedCatalogTalents = true
+22
View File
@@ -0,0 +1,22 @@
## Interface: 30300
## Title: CoA Exporter
## Notes: Per-character export (talents/gear/mystic enchants/scrolls) + game-data catalog dump (skills/dispels/passives/talents) for db.exil.es
## Author: Subd from CoA / Exiles EU
## Version: 1.0.0
## SavedVariables: CoaExporterSaved, CoaExporterConfig, CoaExporterScrollCache, CoaExporterCatalog
Util\Json.lua
Data\ScrollCatalog.lua
Collectors\Talents.lua
Collectors\Gear.lua
Collectors\Enchants.lua
Collectors\MysticScrolls.lua
Collectors\MysticScrollProbe.lua
Catalogs\Common.lua
Catalogs\Skills.lua
Catalogs\Talents.lua
UI\ExportFrame.lua
Core.lua
+38
View File
@@ -0,0 +1,38 @@
-- CoaExporter - Mystic Enchants (Glyphs) collector
CoaExporter = _G.CoaExporter or {}
_G.CoaExporter = CoaExporter
local AE = CoaExporter
function AE.CollectMysticEnchants()
local enchants = {}
if C_MysticEnchant and C_MysticEnchant.GetAppliedEnchant then
local numSlots = NUM_MYSTIC_ENCHANT_SLOTS or 12
for i = 1, numSlots do
local spellID = C_MysticEnchant.GetAppliedEnchant("player", i)
if spellID and spellID > 0 then
local name, _, icon = GetSpellInfo(spellID)
if name then
local iconPath = ""
if icon then
iconPath = icon:match("Interface\\Icons\\(.+)") or ""
iconPath = iconPath:lower()
end
table.insert(enchants, {
slot = i,
name = name,
spellID = spellID,
icon = iconPath
})
end
end
end
end
table.sort(enchants, function(a,b) return a.slot < b.slot end)
return { enchants = enchants }
end
AE._loadedEnchants = true
+129
View File
@@ -0,0 +1,129 @@
-- CoaExporter - Gear collector (equipped items only)
CoaExporter = _G.CoaExporter or {}
_G.CoaExporter = CoaExporter
local AE = CoaExporter
local SLOT_NAMES = {
[1] = "HEAD", [2] = "NECK", [3] = "SHOULDER", [4] = "SHIRT", [5] = "CHEST",
[6] = "WAIST", [7] = "LEGS", [8] = "FEET", [9] = "WRIST", [10] = "HANDS",
[11] = "FINGER1", [12] = "FINGER2", [13] = "TRINKET1", [14] = "TRINKET2",
[15] = "BACK", [16] = "MAINHAND", [17] = "OFFHAND", [18] = "RANGED", [19] = "TABARD",
}
local function parse_item_link(itemLink)
if not itemLink then return nil end
local itemString = itemLink:match("Hitem:([%d:]+)")
if not itemString then return nil end
local parts = {}
for v in string.gmatch(itemString, "([^:]+)") do
parts[#parts+1] = tonumber(v) or 0
end
local itemId = parts[1] or 0
local enchantId = parts[2] or 0
local gems = { parts[3] or 0, parts[4] or 0, parts[5] or 0, parts[6] or 0 }
local suffixId = parts[7] or 0
return {
itemId = itemId,
enchantId = enchantId,
gems = gems,
suffixId = suffixId,
}
end
local function EnsureGearScanner()
if not AE._gearScanner then
AE._gearScanner = CreateFrame("GameTooltip", "CoaExpGearScanner", nil, "GameTooltipTemplate")
AE._gearScanner:SetOwner(WorldFrame, "ANCHOR_NONE")
end
return AE._gearScanner
end
-- Slots that cannot carry a permanent enchant in 3.3.5. Scanning these
-- picks up green-coloured "Equip: ..." effect text on neck/rings/trinkets
-- which would pollute the gear table's Enchant column.
local SLOTS_WITH_NO_ENCHANT = {
[2] = true, [11] = true, [12] = true, [13] = true, [14] = true, [19] = true,
}
local function scan_enchant_name(itemLink, slot)
if not itemLink then return nil end
if slot and SLOTS_WITH_NO_ENCHANT[slot] then return nil end
local tt = EnsureGearScanner()
tt:ClearLines()
tt:SetHyperlink(itemLink)
local n = tt:NumLines() or 0
for i = 2, n do
local l = _G["CoaExpGearScannerTextLeft" .. i]
if l then
local r, g, b = l:GetTextColor()
local text = l:GetText()
if text and text ~= "" and r and g and b then
if r < 0.2 and g > 0.8 and b < 0.2 then
local lower = text:lower()
if not text:match("^Socket Bonus")
and not lower:match("^%(%d+%)%s*set")
and not lower:match("^set:")
and not lower:match("^equip:")
and not lower:match("^use:")
and not lower:match("^chance on hit:") then
return text
end
end
end
end
end
return nil
end
local function resolve_gems(itemLink, gemIds)
local arr = {}
for i = 1, 4 do
local gid = gemIds[i] or 0
if gid and gid > 0 then
local name, gemLink = GetItemGem(itemLink, i)
local gemItemId = 0
if gemLink then
local id = tonumber(string.match(gemLink, "item:(%d+)"))
if id then gemItemId = id end
end
table.insert(arr, { itemId = gemItemId, enchantId = gid, name = name or "", link = gemLink or "" })
end
end
return arr
end
function AE.CollectGear()
local out = { slots = {} }
for slot = 1, 19 do
local itemLink = GetInventoryItemLink("player", slot)
if itemLink then
local parsed = parse_item_link(itemLink) or {}
local itemId = parsed.itemId or 0
local itemName, _, itemQuality, itemLevel, _, itemType, itemSubType, _, equipSlot, texture = GetItemInfo(itemLink)
local gems = resolve_gems(itemLink, parsed.gems or {})
local enchantName = scan_enchant_name(itemLink, slot)
table.insert(out.slots, {
slot = slot,
slotName = SLOT_NAMES[slot] or tostring(slot),
itemId = itemId,
name = itemName or "",
quality = itemQuality or 0,
itemLevel = itemLevel or 0,
type = itemType or "",
subType = itemSubType or "",
equipSlot = equipSlot or "",
texture = texture or "",
link = itemLink,
gems = gems,
enchantId = parsed.enchantId or 0,
enchantName = enchantName or "",
})
end
end
return out
end
AE._loadedGear = true
@@ -0,0 +1,116 @@
-- CoaExporter - Mystic Scroll API probe + deep dump
-- Purpose: discover what data we can extract about mystic enchants on
-- Ascension's 3.3.5 client. Intentionally over-probes several API
-- signatures; failures are captured so we can see what's available.
CoaExporter = _G.CoaExporter or {}
_G.CoaExporter = CoaExporter
local AE = CoaExporter
local scanner
local function EnsureScanner()
if not scanner then
scanner = CreateFrame("GameTooltip", "CoaExpScrollProbe", nil, "GameTooltipTemplate")
scanner:SetOwner(WorldFrame, "ANCHOR_NONE")
end
return scanner
end
local function TipToLines(tooltipSetup)
local s = EnsureScanner()
s:ClearLines()
tooltipSetup(s)
local out = {}
for i = 1, s:NumLines() do
local left = _G["CoaExpScrollProbeTextLeft" .. i]
if left then
local t = left:GetText()
if t and t ~= "" then table.insert(out, t) end
end
end
return out
end
local function TryCall(fn, ...)
local ok, a, b, c, d, e, f, g = pcall(fn, ...)
if ok then return true, a, b, c, d, e, f, g end
return false, a
end
function AE.CollectMysticScrollProbe()
local result = {
api_surface = {},
slots = {},
enchants = {},
errors = {},
}
if C_MysticEnchant then
for k, v in pairs(C_MysticEnchant) do
if type(v) == "function" then
table.insert(result.api_surface, "C_MysticEnchant." .. k)
end
end
table.sort(result.api_surface)
else
table.insert(result.errors, "C_MysticEnchant global is nil")
end
local numSlots = NUM_MYSTIC_ENCHANT_SLOTS or 12
for i = 1, numSlots do
local slotData = { slot = i }
if C_MysticEnchant and C_MysticEnchant.GetAppliedEnchant then
local ok, spellID = TryCall(C_MysticEnchant.GetAppliedEnchant, "player", i)
if ok and spellID and spellID > 0 then
slotData.spellID = spellID
local name, _, icon = GetSpellInfo(spellID)
slotData.name = name
slotData.icon = icon and icon:match("Interface\\Icons\\(.+)"):lower() or nil
slotData.tooltip_lines = TipToLines(function(t)
t:SetHyperlink("spell:" .. spellID)
end)
end
end
for _, m in ipairs({ "GetSlotInfo", "GetSlotTier", "GetSlotQuality",
"GetEnchantSlotInfo", "GetSlotType" }) do
if C_MysticEnchant and C_MysticEnchant[m] then
local ok, r1, r2, r3 = TryCall(C_MysticEnchant[m], i)
if ok then
slotData[m] = { r1, r2, r3 }
end
end
end
table.insert(result.slots, slotData)
end
if C_MysticEnchant then
for _, fname in ipairs({
"GetKnownEnchants", "GetUnlockedEnchants", "GetAllEnchants",
"GetEnchants", "GetLearnedEnchants", "GetAvailableEnchants" }) do
if C_MysticEnchant[fname] then
local ok, list = TryCall(C_MysticEnchant[fname], "player")
if ok and type(list) == "table" then
result[fname] = list
end
end
end
end
for _, sd in ipairs(result.slots) do
if sd.spellID and C_MysticEnchant then
for _, m in ipairs({ "GetEnchantInfo", "GetEnchantQuality",
"GetEnchantTier", "GetEnchantDescription" }) do
if C_MysticEnchant[m] then
local ok, r1, r2, r3 = TryCall(C_MysticEnchant[m], sd.spellID)
if ok then
sd[m] = { r1, r2, r3 }
end
end
end
end
end
return result
end
AE._loadedMysticScrollProbe = true
+180
View File
@@ -0,0 +1,180 @@
-- CoaExporter / Collectors / MysticScrolls.lua
--
-- Async tooltip scraper for all known mystic enchant scrolls.
-- Iterates AE.ScrollCatalog (generated from AtlasLootAscension) and dumps
-- tooltip text + the spell taught by each scroll.
--
-- Usage: /coae scrolls scan → start scan (one-time; dumps progress to chat)
-- /coae scrolls export → print accumulated JSON of everything scanned
-- /coae scrolls reset → clear cache
--
-- Design notes:
-- - WoW lazy-loads item data. First GetItemInfo() for an unseen item
-- returns nil and triggers a server query. We retry with delay.
-- - Results accumulate in CoaExporterScrollCache SavedVariable so a
-- single run across sessions builds up the full DB.
-- - A "scan" pass fires a batch of GetItemInfo requests, then waits,
-- then re-scans and records whatever resolved. Unresolved entries are
-- retried on the next scan.
CoaExporter = _G.CoaExporter or {}
_G.CoaExporter = CoaExporter
local AE = CoaExporter
CoaExporterScrollCache = CoaExporterScrollCache or {
entries = {},
meta = {
lastScanAt = nil,
totalCatalog = 0,
resolved = 0,
unresolved = {},
},
}
local function EnsureScanner()
if not AE._scrollScanner then
AE._scrollScanner = CreateFrame("GameTooltip", "CoaExpScrollsTT", nil, "GameTooltipTemplate")
AE._scrollScanner:SetOwner(WorldFrame, "ANCHOR_NONE")
end
return AE._scrollScanner
end
local function TipLines(tt)
local out = {}
for i = 1, tt:NumLines() do
local left = _G[tt:GetName() .. "TextLeft" .. i]
if left then
local t = left:GetText()
if t and t ~= "" then out[#out+1] = t end
end
end
return out
end
local function ScanItemTooltip(itemID)
local tt = EnsureScanner()
tt:ClearLines()
tt:SetHyperlink("item:" .. itemID)
return TipLines(tt)
end
local function FlatCatalog()
local out = {}
if not AE.ScrollCatalog then return out end
for class, list in pairs(AE.ScrollCatalog) do
for _, e in ipairs(list) do
out[#out+1] = { itemID = e.itemID, name = e.name, class = class }
end
end
return out
end
local function TryResolve(entry)
local cache = CoaExporterScrollCache.entries
if cache[entry.itemID] and cache[entry.itemID].itemTooltip then
return true
end
local name, _, quality = GetItemInfo(entry.itemID)
if not name then
GameTooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
GameTooltip:SetHyperlink("item:" .. entry.itemID)
GameTooltip:Hide()
return false
end
local itemLines = ScanItemTooltip(entry.itemID)
local spellName, spellRank
if GetItemSpell then
spellName, spellRank = GetItemSpell(entry.itemID)
end
cache[entry.itemID] = {
itemID = entry.itemID,
class = entry.class,
scrollName = entry.name,
itemName = name,
quality = quality,
spellName = spellName,
spellRank = spellRank,
itemTooltip = table.concat(itemLines, "\n"),
fetchedAt = time(),
}
return #itemLines > 0
end
function AE.ScrollsScanPass()
local catalog = FlatCatalog()
local total = #catalog
CoaExporterScrollCache.meta.totalCatalog = total
local newlyResolved = 0
local unresolved = {}
for _, entry in ipairs(catalog) do
if TryResolve(entry) then
newlyResolved = newlyResolved + 1
else
unresolved[#unresolved+1] = entry.itemID
end
end
CoaExporterScrollCache.meta.lastScanAt = time()
CoaExporterScrollCache.meta.resolved = newlyResolved
CoaExporterScrollCache.meta.unresolved = unresolved
return { total = total, resolved = newlyResolved, unresolved = #unresolved }
end
function AE.ScrollsStartScan(callback)
local attempts = 0
local maxAttempts = 4
local function step()
attempts = attempts + 1
local stats = AE.ScrollsScanPass()
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"CoaExporter scrolls: pass %d/%d - resolved %d/%d (still unresolved: %d)",
attempts, maxAttempts, stats.resolved, stats.total, stats.unresolved))
if stats.unresolved > 0 and attempts < maxAttempts then
local t = CreateFrame("Frame")
local elapsed = 0
t:SetScript("OnUpdate", function(self, dt)
elapsed = elapsed + dt
if elapsed > 5 then
self:SetScript("OnUpdate", nil)
step()
end
end)
else
if callback then callback(stats) end
end
end
step()
end
function AE.ScrollsExport()
local out = {
schemaVersion = 1,
exportedAt = date("!%Y-%m-%dT%H:%M:%SZ"),
catalogTotal = AE.ScrollCatalogTotal or 0,
cacheMeta = CoaExporterScrollCache.meta,
entries = {},
}
for _, rec in pairs(CoaExporterScrollCache.entries) do
table.insert(out.entries, rec)
end
return out
end
function AE.ScrollsReset()
CoaExporterScrollCache = {
entries = {},
meta = { lastScanAt = nil, totalCatalog = 0, resolved = 0, unresolved = {} },
}
end
-- Compute total once on load (used in /coae catalog status etc.)
do
local total = 0
if AE.ScrollCatalog then
for _, list in pairs(AE.ScrollCatalog) do
total = total + #list
end
end
AE.ScrollCatalogTotal = total
end
AE._loadedMysticScrolls = true
+142
View File
@@ -0,0 +1,142 @@
-- CoaExporter - Talents collector (active spec only)
CoaExporter = _G.CoaExporter or {}
_G.CoaExporter = CoaExporter
local AE = CoaExporter
local function get_active_group()
if GetActiveTalentGroup then
local g = GetActiveTalentGroup()
if g == 0 then g = 1 end
return g or 1
end
return 1
end
local function collect_selected_for_tab(tabIndex, talentGroup)
local arr = {}
local debug = {}
local numTalents = GetNumTalents(tabIndex)
for talentIndex = 1, numTalents do
local name, iconTexture, tier, column, rank, maxRank
name, iconTexture, tier, column, rank, maxRank = GetTalentInfo(tabIndex, talentIndex)
if rank == 0 or rank == nil then
name, iconTexture, tier, column, rank, maxRank = GetTalentInfo(tabIndex, talentIndex, nil, nil, talentGroup)
end
if rank == 0 or rank == nil then
name, iconTexture, tier, column, rank, maxRank = GetTalentInfo(tabIndex, talentIndex, false, nil, talentGroup)
end
if rank == 0 or rank == nil then
name, iconTexture, tier, column, rank, maxRank = GetTalentInfo(tabIndex, talentIndex, false, false)
end
if rank and rank > 0 then
local link = GetTalentLink(tabIndex, talentIndex)
local spellId = 0
if link then
spellId = tonumber(string.match(link, "talent:(%d+)")) or 0
end
table.insert(arr, {
tabIndex = tabIndex,
talentIndex = talentIndex,
name = name or "",
rank = rank or 0,
maxRank = maxRank or 0,
spellId = spellId,
})
end
end
return arr, debug
end
function AE.CollectTalents()
local out = {
selected = {},
debug = {},
buildString = nil,
talents = {},
}
if C_CharacterAdvancement and C_CharacterAdvancement.ExportBuild then
local buildString = C_CharacterAdvancement.ExportBuild(true)
out.buildString = buildString
table.insert(out.debug, "Ascension API: C_CharacterAdvancement.ExportBuild() = " .. tostring(buildString))
local ca = C_CharacterAdvancement
if ca.GetKnownTalentEntries then
local entries = ca.GetKnownTalentEntries()
table.insert(out.debug, string.format("Found %d known talent entries", #entries))
for _, entry in ipairs(entries) do
local id = entry.ID or entry.id or 0
local name = entry.name or ""
local icon = entry.icon or ""
local rank, maxRank, tabIndex, tier, column
for _, fname in ipairs({ "GetTalentEntryInfo", "GetTalentEntry",
"GetAdvancementEntry", "GetEntryInfo" }) do
if type(ca[fname]) == "function" then
local ok, a, b, c, d, e, f = pcall(ca[fname], id)
if ok and a then
name = name ~= "" and name or (type(a) == "string" and a or name)
icon = icon ~= "" and icon or (type(b) == "string" and b or icon)
rank = rank or (type(c) == "number" and c or nil)
maxRank = maxRank or (type(d) == "number" and d or nil)
tabIndex = tabIndex or (type(e) == "number" and e or nil)
tier = tier or (type(f) == "number" and f or nil)
break
end
end
end
local talent = {
id = id,
name = name,
icon = icon,
rank = rank or 0,
maxRank = maxRank or 0,
tabIndex = tabIndex or 0,
tier = tier or 0,
column = column or 0,
}
table.insert(out.talents, talent)
table.insert(out.selected, {
id = id,
name = name,
tabIndex = tabIndex or 0,
rank = rank or 1,
maxRank = maxRank or 5,
icon = icon,
})
end
end
if buildString and buildString ~= "" then
out.hasTalents = true
end
return out
end
local active = get_active_group()
out.activeTalentGroup = active
local numTabs = GetNumTalentTabs()
table.insert(out.debug, "Fallback: using GetNumTalentTabs, numTabs=" .. tostring(numTabs))
for tabIndex = 1, numTabs do
local tabName, _, pointsSpent = GetTalentTabInfo(tabIndex, false, false, active)
table.insert(out.debug, string.format("tab%d: name=%s points=%d", tabIndex, tostring(tabName), tonumber(pointsSpent) or 0))
local selected = collect_selected_for_tab(tabIndex, active)
for _, v in ipairs(selected) do
table.insert(out.selected, v)
end
end
return out
end
AE._loadedTalents = true
+541
View File
@@ -0,0 +1,541 @@
-- CoaExporter - Core
local ADDON_NAME = ...
CoaExporter = CoaExporter or {}
local AE = CoaExporter
CoaExporterConfig = CoaExporterConfig or {}
CoaExporterConfig.enableSavedVariables = CoaExporterConfig.enableSavedVariables == true and true or false
CoaExporterSaved = CoaExporterSaved or {}
local function Norm(s)
if s == nil then return nil end
s = tostring(s):lower()
s = s:match("^%s*(.-)%s*$") or s
if s == "" then return nil end
return s
end
local function SafeCall(fn, ...)
local ok, r = pcall(fn, ...)
if ok then return r end
return nil
end
-- ===== Per-character export =====
function AE:AssembleExport(which)
which = Norm(which)
local nowIso = date("!%Y-%m-%dT%H:%M:%SZ")
local version, build, buildDate, toc = GetBuildInfo()
local name = UnitName("player") or ""
local level = UnitLevel("player") or 0
local _, class = UnitClass("player")
local _, race = UnitRace("player")
local faction = UnitFactionGroup("player") or ""
local realm = GetRealmName and GetRealmName() or (GetCVar and GetCVar("realmName")) or ""
local out = {
schemaVersion = 1,
exportedAt = nowIso,
client = { interface = toc or 30300, version = version, build = build, buildDate = buildDate },
character = { name = name or "", realm = realm or "", level = level or 0, class = class or "", race = race or "", faction = faction or "" },
}
local wantAll = (which == nil) or (which == "all")
if wantAll or which == "talents" then
if AE.CollectTalents then out.talents = SafeCall(AE.CollectTalents) or {} end
end
if wantAll or which == "gear" then
if AE.CollectGear then out.gear = SafeCall(AE.CollectGear) or {} end
end
if wantAll or which == "enchants" then
if AE.CollectMysticEnchants then out.mysticEnchants = SafeCall(AE.CollectMysticEnchants) or {} end
end
return out
end
function AE:ShowExport(text, titleText)
local show = _G.CoaExporter_ShowExportFrame
if type(show) == "function" then
show(text, titleText)
return
end
if not self._fallbackFrame then
local f = CreateFrame("Frame", "CoaExporterFallbackFrame", UIParent, "DialogBoxFrame")
f:SetSize(700, 500)
f:SetPoint("CENTER")
f:SetMovable(true)
f:EnableMouse(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", f.StartMoving)
f:SetScript("OnDragStop", f.StopMovingOrSizing)
local title = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
title:SetPoint("TOP", 0, -8)
f.title = title
local scroll = CreateFrame("ScrollFrame", "CoaExporterFallbackScroll", f, "UIPanelScrollFrameTemplate")
scroll:SetPoint("TOPLEFT", 16, -36)
scroll:SetPoint("BOTTOMRIGHT", -32, 16)
local edit = CreateFrame("EditBox", "CoaExporterFallbackEdit", scroll)
edit:SetMultiLine(true)
edit:SetAutoFocus(true)
edit:SetFontObject(ChatFontNormal)
edit:SetWidth(640)
edit:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
edit:SetScript("OnEditFocusGained", function(self) self:HighlightText() end)
scroll:SetScrollChild(edit)
f.editBox = edit
f.scrollFrame = scroll
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
close:SetPoint("TOPRIGHT", -4, -4)
self._fallbackFrame = f
if type(_G.CoaExporter_ShowExportFrame) ~= "function" then
_G.CoaExporter_ShowExportFrame = function(t, ti)
local ff = AE._fallbackFrame
if not ff then return end
ff:Show()
ff.editBox:SetText(t or "")
ff.title:SetText(ti or "CoaExporter (Ctrl+C)")
ff.editBox:HighlightText()
ff.editBox:SetFocus()
end
end
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: using fallback export window (UI module not loaded).")
end
local f = self._fallbackFrame
f:Show()
f.editBox:SetText(text or "")
f.title:SetText(titleText or "CoaExporter (Ctrl+C)")
f.editBox:HighlightText()
f.editBox:SetFocus()
end
function AE:Export(which)
local normWhich = Norm(which)
local data = self:AssembleExport(normWhich)
local function tiny_json_encode(v)
local tv = type(v)
if tv == 'nil' then return 'null' end
if tv == 'boolean' then return v and 'true' or 'false' end
if tv == 'number' then return tostring(v) end
if tv == 'string' then
local s = v:gsub('\\', '\\\\'):gsub('"', '\\"'):gsub('\n', '\\n'):gsub('\r', '\\r'):gsub('\t', '\\t')
return '"' .. s .. '"'
end
if tv == 'table' then
local n = 0
for k,_ in pairs(v) do if type(k) ~= 'number' then n = -1 break else n = math.max(n, k) end end
if n >= 1 then
local parts = {}
for i=1,n do parts[#parts+1] = tiny_json_encode(v[i]) end
return '[' .. table.concat(parts, ',') .. ']'
else
local parts = {}
for k,val in pairs(v) do parts[#parts+1] = tiny_json_encode(tostring(k)) .. ':' .. tiny_json_encode(val) end
return '{' .. table.concat(parts, ',') .. '}'
end
end
return 'null'
end
local encoder = _G.CoaExporter_Json_Encode or tiny_json_encode
local json = encoder(data)
local title = "CoaExporter"
if normWhich and normWhich ~= "all" then title = title .. " - " .. normWhich end
self:ShowExport(json, title .. " (Ctrl+C)")
if CoaExporterConfig.enableSavedVariables then
local key = (data.character.realm or "") .. ":" .. (data.character.name or "")
CoaExporterSaved[key] = data
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: export saved to SavedVariables for " .. key)
end
end
-- ===== Markdown generators =====
function AE:GenerateMarkdownGear()
local gear = self.CollectGear and self.CollectGear() or { slots = {} }
local ench = self.CollectMysticEnchants and self.CollectMysticEnchants() or { perSlot = {}, active = {} }
local bySlot = {}
for _, s in ipairs(gear.slots or {}) do bySlot[s.slot] = s end
local mystPer = {}
for _, e in ipairs(ench.perSlot or {}) do mystPer[e.slot] = e.name end
local ORDER = {
{1,"Head"},{2,"Neck"},{3,"Shoulders"},{15,"Back"},{5,"Chest"},
{9,"Wrist"},{16,"Mainhand"},{17,"Offhand"},{18,"Wand"},{10,"Gloves"},
{6,"Belt"},{7,"Legs"},{8,"Boots"},{11,"Ring 1"},{12,"Ring 2"},
{13,"Trinket 1"},{14,"Trinket 2"},
}
local function md_link(name, itemId)
if (itemId or 0) > 0 and name and name ~= "" then
return string.format("[%s](https://db.ascension.gg/?item=%d)", name, itemId)
elseif name and name ~= "" then
return name
else
return "-"
end
end
local lines = {}
table.insert(lines, "| Slot | Item | Location | Enchant | Alternative |")
table.insert(lines, "|------|------|----------|---------|-------------|")
for _, ent in ipairs(ORDER) do
local slotId, label = ent[1], ent[2]
local s = bySlot[slotId]
local itemCell, enchantCell = "-", "-"
if s then
itemCell = md_link(s.name, s.itemId)
local gearEnch = s.enchantName
if gearEnch and gearEnch ~= "" then
enchantCell = gearEnch
else
local myst = mystPer[slotId]
if myst and myst ~= "" then enchantCell = "Mystic: " .. myst end
end
end
table.insert(lines, string.format("| **%s** | %s | - | %s | - |", label, itemCell, enchantCell))
end
return table.concat(lines, "\n") .. "\n"
end
function AE:GenerateMarkdownEnchants()
local ench = self.CollectMysticEnchants and self.CollectMysticEnchants() or { enchants = {} }
local lines = {}
table.insert(lines, "## Mystic Enchants\n")
if #(ench.enchants or {}) == 0 then
table.insert(lines, "*No Mystic Enchants equipped.*\n")
return table.concat(lines, "\n")
end
local enchantRows = {}
for _, e in ipairs(ench.enchants or {}) do
local slot = e.slot or 0
local quality, qualityOrder
if slot >= 11 then
quality = '<span class="artifact">Artifact</span>'; qualityOrder = 1
elseif slot == 1 then
quality = '<span class="legendary">Legendary</span>'; qualityOrder = 2
elseif slot >= 2 and slot <= 4 then
quality = '<span class="epic">Epic</span>'; qualityOrder = 3
else
quality = '<span class="rare">Rare</span>'; qualityOrder = 4
end
local spellID = e.spellID or 0
local name = e.name or "Unknown"
local icon = e.icon or ""
local iconImg = icon ~= "" and string.format('<img src="/icons/spells/%s.png" width="20" height="20" style="vertical-align: middle;" /> ', icon) or ""
local link = spellID > 0 and string.format("[%s](https://db.ascension.gg/?spell=%d)", name, spellID) or name
table.insert(enchantRows, {
quality = quality, qualityOrder = qualityOrder, slot = slot,
text = string.format("| %s | %s**%s** |", quality, iconImg, link),
})
end
table.sort(enchantRows, function(a, b)
if a.qualityOrder ~= b.qualityOrder then return a.qualityOrder < b.qualityOrder end
return a.slot < b.slot
end)
table.insert(lines, "| Quality | Enchant |")
table.insert(lines, "|---------|---------|")
for _, row in ipairs(enchantRows) do table.insert(lines, row.text) end
return table.concat(lines, "\n")
end
function AE:GenerateMarkdownFull()
local data = self:AssembleExport("all") or {}
local char = data.character or {}
local cls = (char.class or ""):lower()
local classIcon = string.format("/icons/classes/classicon_%s.png", cls ~= "" and cls or "unknown")
local lines = {}
table.insert(lines, string.format("![%s](%s)", char.class or "Class", classIcon))
table.insert(lines, string.format("By %s", char.name or "?"))
table.insert(lines, "")
table.insert(lines, string.format("Character: **%s** - Level %d %s %s on %s",
char.name or "?", char.level or 0, char.race or "?", char.class or "?", char.realm or "?"))
table.insert(lines, "")
table.insert(lines, "<!-- Optional intro paragraph: describe the build, content target, philosophy. -->")
table.insert(lines, "")
table.insert(lines, "> **Note:** Exported by CoaExporter - gear, enchants, and talents are auto-populated.")
table.insert(lines, "> Items marked with unknown db.ascension.gg IDs are custom Ascension loot.")
table.insert(lines, "> {.is-info}")
table.insert(lines, "")
table.insert(lines, "## Gear")
table.insert(lines, "")
table.insert(lines, self:GenerateMarkdownGear())
table.insert(lines, self:GenerateMarkdownEnchants())
table.insert(lines, "")
table.insert(lines, "## Talents")
table.insert(lines, "")
local talents = data.talents or {}
local selected = talents.selected or {}
local buildString = talents.buildString
local list = talents.talents or selected
if buildString and buildString ~= "" then
table.insert(lines, string.format("**Ascension ExportBuild:** `%s`", buildString))
table.insert(lines, "")
table.insert(lines, string.format(
"<!-- The exil.es talent-calc uses a different build format. " ..
"Until the converter is in place, generate the iframe URL from " ..
"the exil.es calc UI by importing the ExportBuild string above, " ..
"then paste the resulting src= here:"))
table.insert(lines, string.format(
" <iframe src=\"/static/talents/?build=%s.XX_YY_ZZ..&embed=true\" " ..
"width=\"100%%\" height=\"840\" style=\"border:0;border-radius:8px;\"></iframe>",
(cls ~= "" and cls or "druid")))
table.insert(lines, "-->")
table.insert(lines, "")
end
if list and #list > 0 then
table.insert(lines, string.format("Known talents: **%d** entries.", #list))
table.insert(lines, "")
table.insert(lines, "| ID | Name | Rank |")
table.insert(lines, "|----|------|------|")
for _, t in ipairs(list) do
local nm = (t.name and t.name ~= "") and t.name or "*(unresolved)*"
local rk = (t.rank or 0) > 0 and tostring(t.rank) or "?"
local mr = (t.maxRank or 0) > 0 and tostring(t.maxRank) or "?"
table.insert(lines, string.format("| %d | %s | %s/%s |", t.id or 0, nm, rk, mr))
end
else
table.insert(lines, "*No talent data - run `/coae debug` to check collector.*")
end
return table.concat(lines, "\n")
end
-- ===== Slash command =====
local HELP = [[
CoaExporter commands:
/coae export all|talents|gear|enchants
/coae export mdgear|mdenchants|md (full wiki)
/coae catalog all|skills|talents
/coae catalog dispels [class]|passives [class]|status
/coae scrolls scan|export|reset|status
/coae sv on|off (SavedVariables for character export)
/coae debug
/coae help
]]
local function HandleExport(rest)
rest = Norm(rest) or "all"
if rest == "mdgear" then
AE:ShowExport(AE:GenerateMarkdownGear(), "CoaExporter - Markdown Gear (Ctrl+C)")
elseif rest == "mdenchants" then
AE:ShowExport(AE:GenerateMarkdownEnchants(), "CoaExporter - Markdown Enchants (Ctrl+C)")
elseif rest == "md" or rest == "mdfull" or rest == "wiki" then
AE:ShowExport(AE:GenerateMarkdownFull(), "CoaExporter - Wiki Markdown (Ctrl+C)")
elseif rest == "all" or rest == "talents" or rest == "gear" or rest == "enchants" then
AE:Export(rest)
else
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: unknown export target '" .. tostring(rest) .. "'")
end
end
local function HandleCatalog(rest)
rest = Norm(rest) or "status"
local sub, arg = rest:match("^(%S+)%s*(.*)$")
sub = sub or rest
if sub == "all" or sub == "skills" or sub == "talents" then
if not AE.Catalog or not AE.Catalog.Run then
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: catalog module not loaded")
return
end
AE.Catalog.Run(sub == "all" and "all" or sub)
elseif sub == "dispels" then
if AE.CatalogListDispels then AE.CatalogListDispels(arg) end
elseif sub == "passives" then
if AE.CatalogListPassives then AE.CatalogListPassives(arg) end
elseif sub == "status" then
local meta = (CoaExporterCatalog and CoaExporterCatalog._meta) or {}
local nSkills, nDispels, nPassives, nTalents = 0, 0, 0, 0
if CoaExporterCatalog then
for _ in pairs(CoaExporterCatalog.skills or {}) do nSkills = nSkills + 1 end
for _ in pairs(CoaExporterCatalog.dispels or {}) do nDispels = nDispels + 1 end
for _ in pairs(CoaExporterCatalog.levelPassives or {}) do nPassives = nPassives + 1 end
for _ in pairs(CoaExporterCatalog.talents or {}) do nTalents = nTalents + 1 end
end
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"CoaExporter catalog: lastScan=%s filter=%s | skills=%d cls dispels=%d cls passives=%d cls talents=%d cls",
tostring(meta.lastScanAt or "never"), tostring(meta.filter or "-"),
nSkills, nDispels, nPassives, nTalents))
else
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: /coae catalog [all|skills|talents|dispels|passives|status]")
end
end
local function HandleScrolls(rest)
rest = Norm(rest) or "status"
if rest == "scan" then
if not AE.ScrollsStartScan then
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: MysticScrolls collector not loaded")
return
end
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"CoaExporter scrolls: scanning %d scrolls, ~20s...",
AE.ScrollCatalogTotal or 0))
AE.ScrollsStartScan(function(stats)
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"CoaExporter scrolls: scan complete - %d resolved, %d unresolved",
stats.total - stats.unresolved, stats.unresolved))
end)
elseif rest == "export" then
if not AE.ScrollsExport then
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: MysticScrolls collector not loaded")
return
end
local data = AE.ScrollsExport()
local encoder = _G.CoaExporter_Json_Encode
if not encoder then
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: JSON encoder missing")
return
end
AE:ShowExport(encoder(data), "CoaExporter - Scrolls (Ctrl+C)")
elseif rest == "reset" then
if AE.ScrollsReset then AE.ScrollsReset() end
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter scrolls: cache cleared")
elseif rest == "status" then
local meta = CoaExporterScrollCache and CoaExporterScrollCache.meta or {}
local resolved = 0
if CoaExporterScrollCache then
for _ in pairs(CoaExporterScrollCache.entries or {}) do resolved = resolved + 1 end
end
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"CoaExporter scrolls: %d/%d resolved (last scan: %s)",
resolved, AE.ScrollCatalogTotal or 0,
meta.lastScanAt and date("%Y-%m-%d %H:%M:%S", meta.lastScanAt) or "never"))
else
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: /coae scrolls [scan|export|reset|status]")
end
end
local function HandleDebug()
local lines = {}
local function add(m) table.insert(lines, m) end
add("CoaExporter debug:")
add(string.format("- AddOn: %s", tostring(ADDON_NAME)))
add(string.format("- UI: %s", type(_G.CoaExporter_ShowExportFrame) == "function" and "yes" or "no"))
add(string.format("- JSON: %s", type(_G.CoaExporter_Json_Encode) == "function" and "yes" or "no"))
add(string.format("- Loaded: talents=%s gear=%s enchants=%s scrolls=%s probe=%s catCommon=%s catSkills=%s catTalents=%s",
tostring(AE._loadedTalents or false), tostring(AE._loadedGear or false),
tostring(AE._loadedEnchants or false), tostring(AE._loadedMysticScrolls or false),
tostring(AE._loadedMysticScrollProbe or false),
tostring(AE._loadedCatalogCommon or false), tostring(AE._loadedCatalogSkills or false),
tostring(AE._loadedCatalogTalents or false)))
if type(AE.CollectTalents) == "function" then
local t = SafeCall(AE.CollectTalents) or {}
add(string.format("Talents: OK, selected=%d, build=%s",
#(t.selected or {}), tostring(t.buildString or "-")))
else
add("Talents: MISSING")
end
if type(AE.CollectGear) == "function" then
local g = SafeCall(AE.CollectGear) or {}
add(string.format("Gear: OK, slots=%d", #(g.slots or {})))
else
add("Gear: MISSING")
end
if type(AE.CollectMysticEnchants) == "function" then
local e = SafeCall(AE.CollectMysticEnchants) or {}
add(string.format("MysticEnchants: OK, count=%d", #(e.enchants or {})))
else
add("MysticEnchants: MISSING")
end
add(string.format("ScrollCatalog: %d entries", AE.ScrollCatalogTotal or 0))
local meta = (CoaExporterCatalog and CoaExporterCatalog._meta) or {}
add(string.format("Catalog: lastScan=%s filter=%s",
tostring(meta.lastScanAt or "never"), tostring(meta.filter or "-")))
AE:ShowExport(table.concat(lines, "\n"), "CoaExporter - Debug")
end
local function HandleSv(rest)
if rest == "on" then
CoaExporterConfig.enableSavedVariables = true
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: SavedVariables export ENABLED")
elseif rest == "off" then
CoaExporterConfig.enableSavedVariables = false
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: SavedVariables export DISABLED")
else
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: /coae sv on|off")
end
end
local function Dispatch(msg)
msg = (msg or ""):lower()
if msg == "" or msg == "help" then
AE:ShowExport(HELP, "CoaExporter - Help")
return
end
local cmd, rest = msg:match("^(%S+)%s*(.*)$")
if cmd == "export" then return HandleExport(rest) end
if cmd == "catalog" then return HandleCatalog(rest) end
if cmd == "scrolls" then return HandleScrolls(rest) end
if cmd == "debug" then return HandleDebug() end
if cmd == "sv" then return HandleSv(Norm(rest)) end
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: unknown command. Type /coae help")
end
SLASH_COAE1 = "/coae"
SLASH_COAE2 = "/coaexp"
SLASH_COAE3 = "/ascx" -- back-compat: AscensionExporter users
SLASH_COAE4 = "/asxc"
SlashCmdList["COAE"] = Dispatch
-- Legacy CoA_*Exporter slash compatibility shims
SLASH_COAESKILLDUMP1 = "/skilldump"
SlashCmdList["COAESKILLDUMP"] = function()
if AE.Catalog and AE.Catalog.Run then AE.Catalog.Run("skills") end
end
SLASH_COAEDISPELS1 = "/dispels"
SlashCmdList["COAEDISPELS"] = function(arg)
if AE.CatalogListDispels then AE.CatalogListDispels(arg) end
end
SLASH_COAEPASSIVES1 = "/passives"
SlashCmdList["COAEPASSIVES"] = function(arg)
if AE.CatalogListPassives then AE.CatalogListPassives(arg) end
end
SLASH_COAETALENTDUMP1 = "/talentdumpall"
SLASH_COAETALENTDUMP2 = "/talentdump"
SlashCmdList["COAETALENTDUMP"] = function()
if AE.Catalog and AE.Catalog.Run then AE.Catalog.Run("talents") end
end
local f = CreateFrame("Frame")
f:RegisterEvent("ADDON_LOADED")
f:SetScript("OnEvent", function(_, event, addon)
if event == "ADDON_LOADED" and addon == "CoaExporter" then
CoaExporterConfig.enableSavedVariables = CoaExporterConfig.enableSavedVariables and true or false
end
end)
+856
View File
@@ -0,0 +1,856 @@
-- CoaExporter / Data / ScrollCatalog.lua
-- Auto-generated from AtlasLootAscension MysticEnchants.lua
-- 825 mystic scroll item IDs across 9 classes
-- Re-generate: scripts/regen_scroll_catalog.py
CoaExporter = _G.CoaExporter or {}
local AE = CoaExporter
AE.ScrollCatalog = {
Druid = {
{ itemID = 201374, name = "Ambush Predator" },
{ itemID = 201498, name = "Astral Alignment" },
{ itemID = 202587, name = "Astral Winds" },
{ itemID = 1013887, name = "Balance Mastery" },
{ itemID = 201054, name = "Bash and Thrash" },
{ itemID = 200529, name = "Blooming Power" },
{ itemID = 1013744, name = "Brambles" },
{ itemID = 200262, name = "Carnage Incarnate" },
{ itemID = 1013749, name = "Celestial Focus" },
{ itemID = 200722, name = "Celestial Replenishment" },
{ itemID = 201044, name = "Critical Cycles" },
{ itemID = 201139, name = "Cycles of Growth" },
{ itemID = 200888, name = "Deranged Druid" },
{ itemID = 201170, name = "Earthen Power Well" },
{ itemID = 1300147, name = "Efflorescence" },
{ itemID = 200785, name = "Faerie's Favor" },
{ itemID = 1013746, name = "Feral Aggression" },
{ itemID = 200846, name = "Feral Frenzy" },
{ itemID = 1013753, name = "Feral Instinct" },
{ itemID = 1013884, name = "Feral Mastery" },
{ itemID = 1013843, name = "Feral Swiftness" },
{ itemID = 201552, name = "Ferocious Clarity" },
{ itemID = 1013751, name = "Ferocity" },
{ itemID = 200798, name = "Flourishing Nourish" },
{ itemID = 201256, name = "Flow of Life" },
{ itemID = 201332, name = "Frenzied Growth" },
{ itemID = 201599, name = "Frenzied Slashing" },
{ itemID = 1013758, name = "Furor" },
{ itemID = 1013970, name = "Genesis" },
{ itemID = 554209, name = "Glyph of Berserk" },
{ itemID = 554274, name = "Glyph of Claw" },
{ itemID = 554005, name = "Glyph of Entangling Roots" },
{ itemID = 554206, name = "Glyph of Focus" },
{ itemID = 554013, name = "Glyph of Healing Touch" },
{ itemID = 554017, name = "Glyph of Hurricane" },
{ itemID = 554018, name = "Glyph of Innervate" },
{ itemID = 554016, name = "Glyph of Insect Swarm" },
{ itemID = 554014, name = "Glyph of Lifebloom" },
{ itemID = 554008, name = "Glyph of Mangle" },
{ itemID = 554007, name = "Glyph of Maul" },
{ itemID = 554213, name = "Glyph of Monsoon" },
{ itemID = 554020, name = "Glyph of Moonfire" },
{ itemID = 554211, name = "Glyph of Nourish" },
{ itemID = 554011, name = "Glyph of Rake" },
{ itemID = 554277, name = "Glyph of Rapid Rejuvenation" },
{ itemID = 554001, name = "Glyph of Rebirth" },
{ itemID = 554002, name = "Glyph of Regrowth" },
{ itemID = 554003, name = "Glyph of Rejuvenation" },
{ itemID = 554010, name = "Glyph of Rip" },
{ itemID = 554212, name = "Glyph of Savage Roar" },
{ itemID = 554009, name = "Glyph of Shred" },
{ itemID = 554015, name = "Glyph of Starfall" },
{ itemID = 554019, name = "Glyph of Starfire" },
{ itemID = 554273, name = "Glyph of Survival Instincts" },
{ itemID = 554012, name = "Glyph of Swiftmend" },
{ itemID = 201495, name = "Glyph of Typhoon" },
{ itemID = 554004, name = "Glyph of Wrath" },
{ itemID = 201572, name = "Graceful Resurgence" },
{ itemID = 1013885, name = "Guardian Mastery" },
{ itemID = 200858, name = "Guardians of the Grove" },
{ itemID = 1013757, name = "Improved Mark of the Wild" },
{ itemID = 1013742, name = "Improved Moonfire" },
{ itemID = 1013762, name = "Improved Rejuvenation" },
{ itemID = 200867, name = "Incarnation: King of the Jungle" },
{ itemID = 1013761, name = "Intensity" },
{ itemID = 200074, name = "Ironfur" },
{ itemID = 1013604, name = "Languish" },
{ itemID = 200457, name = "Localized Storm" },
{ itemID = 200088, name = "Mass Entanglement" },
{ itemID = 1013915, name = "Master Shapeshifter" },
{ itemID = 201515, name = "Moon's Wrath" },
{ itemID = 1013745, name = "Moonglow" },
{ itemID = 1013743, name = "Natural Shapeshifter" },
{ itemID = 1013760, name = "Naturalist" },
{ itemID = 201518, name = "Nature's Abundance" },
{ itemID = 200868, name = "Nature's Fervor" },
{ itemID = 1013759, name = "Nature's Focus" },
{ itemID = 1013978, name = "Nature's Grace" },
{ itemID = 1013901, name = "Nature's Majesty" },
{ itemID = 1013741, name = "Nature's Reach" },
{ itemID = 1013971, name = "Nature's Splendor" },
{ itemID = 1013600, name = "Nature's Synthesis" },
{ itemID = 1013747, name = "Omen of Clarity" },
{ itemID = 200816, name = "Omen of Doom" },
{ itemID = 200613, name = "Overflow" },
{ itemID = 201356, name = "Predator's Wrath: Combo" },
{ itemID = 200630, name = "Predator's Wrath: Focus" },
{ itemID = 200908, name = "Predator's Wrath: Wrathful Strikes" },
{ itemID = 1013755, name = "Predatory Strikes" },
{ itemID = 201219, name = "Primal Frenzy" },
{ itemID = 1013907, name = "Primal Fury" },
{ itemID = 201116, name = "Razor-sharp Claws" },
{ itemID = 201133, name = "Relentless Laceration" },
{ itemID = 200817, name = "Savage Barricade" },
{ itemID = 1013756, name = "Savage Fury" },
{ itemID = 200836, name = "Shifting Bark" },
{ itemID = 1013754, name = "Shredding Attacks" },
{ itemID = 200100, name = "Solar Beam" },
{ itemID = 200352, name = "Spirit of the Forest" },
{ itemID = 200089, name = "Stampeding Roar" },
{ itemID = 1013740, name = "Starlight Wrath" },
{ itemID = 202552, name = "Stellar Convergence" },
{ itemID = 202578, name = "Stonebark" },
{ itemID = 1013763, name = "Subtlety" },
{ itemID = 200353, name = "Sunfire" },
{ itemID = 1013610, name = "Surging Eclipse" },
{ itemID = 201546, name = "Symbiotic Nature" },
{ itemID = 201080, name = "TIMBERRR" },
{ itemID = 201598, name = "Tiger's Instinct" },
{ itemID = 200845, name = "Touch of Elune" },
{ itemID = 1013844, name = "Tranquil Spirit" },
{ itemID = 201375, name = "Ursoc's Rage" },
{ itemID = 200087, name = "Ursol's Vortex" },
{ itemID = 1013748, name = "Vengeance" },
{ itemID = 201545, name = "Verdant Insight" },
{ itemID = 11880, name = "Viscera" },
},
Hunter = {
{ itemID = 200154, name = "Aimed Shot" },
{ itemID = 202596, name = "Apex Predator" },
{ itemID = 1013949, name = "Aspect Mastery" },
{ itemID = 1013909, name = "Aspect of the Fox" },
{ itemID = 201472, name = "Barbed Shot" },
{ itemID = 201468, name = "Beastmaster's Gambit" },
{ itemID = 1013893, name = "Beastmastery Mastery" },
{ itemID = 554405, name = "Black Widow Strike" },
{ itemID = 200949, name = "Blood Tracker: Carving Strikes" },
{ itemID = 200544, name = "Blood Tracker: Deadly Bite" },
{ itemID = 1013868, name = "Careful Aim" },
{ itemID = 201107, name = "Charged Up" },
{ itemID = 201019, name = "Combat Rhythm" },
{ itemID = 201589, name = "Concussive Trauma" },
{ itemID = 200543, name = "Deadly Instinct" },
{ itemID = 1013908, name = "Defense of the Turtle" },
{ itemID = 1013800, name = "Deflection" },
{ itemID = 1013803, name = "Efficiency" },
{ itemID = 201191, name = "Expunge" },
{ itemID = 1013816, name = "Ferocity" },
{ itemID = 1013910, name = "Flanking Strike" },
{ itemID = 200343, name = "Flare Strike" },
{ itemID = 1013952, name = "Focused Aim" },
{ itemID = 1013900, name = "Focused Fire" },
{ itemID = 554145, name = "Glyph of Aimed Shot" },
{ itemID = 554154, name = "Glyph of Arcane Shot" },
{ itemID = 554149, name = "Glyph of Bestial Wrath" },
{ itemID = 554216, name = "Glyph of Explosive Shot" },
{ itemID = 554218, name = "Glyph of Explosive Trap" },
{ itemID = 554148, name = "Glyph of Hunter's Mark" },
{ itemID = 554217, name = "Glyph of Kill Shot" },
{ itemID = 554151, name = "Glyph of Mending" },
{ itemID = 554152, name = "Glyph of Multi-Shot" },
{ itemID = 200076, name = "Glyph of Quick Shot" },
{ itemID = 554220, name = "Glyph of Raptor Strike" },
{ itemID = 554150, name = "Glyph of Serpent Sting" },
{ itemID = 554146, name = "Glyph of Steady Shot" },
{ itemID = 554159, name = "Glyph of Wyvern Sting" },
{ itemID = 554163, name = "Glyph of the Hawk" },
{ itemID = 1013871, name = "Go for the Throat" },
{ itemID = 1013809, name = "Hawk Eye" },
{ itemID = 201555, name = "Hunting Frenzy" },
{ itemID = 1013810, name = "Improved Aspect of the Monkey" },
{ itemID = 1013802, name = "Improved Concussive Shot" },
{ itemID = 1013813, name = "Improved Mend Pet" },
{ itemID = 1013807, name = "Improved Stings" },
{ itemID = 1013947, name = "Improved Tracking" },
{ itemID = 200979, name = "Joint Onslaught" },
{ itemID = 201476, name = "Kindle for the Fire" },
{ itemID = 202595, name = "Laceration" },
{ itemID = 1013805, name = "Lethal Shots" },
{ itemID = 1013968, name = "Lock and Load" },
{ itemID = 200708, name = "Lone Wolf" },
{ itemID = 201586, name = "Marked Detonation" },
{ itemID = 1013891, name = "Marksmanship Mastery" },
{ itemID = 201469, name = "Molten Ammo" },
{ itemID = 1013808, name = "Mortal Shots" },
{ itemID = 200674, name = "Power Through" },
{ itemID = 200993, name = "Powershot" },
{ itemID = 200318, name = "Rain of Arrows" },
{ itemID = 1013870, name = "Rapid Killing" },
{ itemID = 200950, name = "Savage Momentum" },
{ itemID = 201040, name = "Spot Weakness" },
{ itemID = 201568, name = "Stalker's Mark" },
{ itemID = 200981, name = "Steady Pace" },
{ itemID = 201547, name = "Steady Shooting" },
{ itemID = 202597, name = "Sting Specialist" },
{ itemID = 1013869, name = "Survival Instincts" },
{ itemID = 1013892, name = "Survival Mastery" },
{ itemID = 1013799, name = "Survival Tactics" },
{ itemID = 1013967, name = "T.N.T." },
{ itemID = 1013817, name = "Thick Hide" },
{ itemID = 200396, name = "Trap Launcher" },
{ itemID = 201275, name = "Trick Shots" },
},
Mage = {
{ itemID = 201296, name = "Abjurer's Ward: Arcane" },
{ itemID = 201297, name = "Abjurer's Ward: Fire" },
{ itemID = 201298, name = "Abjurer's Ward: Frost" },
{ itemID = 201595, name = "Arcane Cascade" },
{ itemID = 1013624, name = "Arcane Concentration" },
{ itemID = 202592, name = "Arcane Conduit" },
{ itemID = 201528, name = "Arcane Dominance" },
{ itemID = 1013643, name = "Arcane Focus" },
{ itemID = 1013955, name = "Arcane Fortitude" },
{ itemID = 1013874, name = "Arcane Mastery" },
{ itemID = 1013785, name = "Arcane Meditation" },
{ itemID = 200126, name = "Arcane Orb" },
{ itemID = 201173, name = "Arcane Power Well" },
{ itemID = 1013627, name = "Arcane Shielding" },
{ itemID = 1013739, name = "Arcane Stability" },
{ itemID = 1013625, name = "Arcane Subtlety" },
{ itemID = 201171, name = "Arctic Power Well" },
{ itemID = 1013737, name = "Arctic Reach" },
{ itemID = 201446, name = "Barrage Overload" },
{ itemID = 201583, name = "Biting Frost" },
{ itemID = 201373, name = "Brand of the Pyromaniac" },
{ itemID = 1013616, name = "Burning Soul" },
{ itemID = 1013982, name = "Cremation" },
{ itemID = 200729, name = "Critical Time" },
{ itemID = 1013611, name = "Draconic Knowledge" },
{ itemID = 201467, name = "Elemental Torrent" },
{ itemID = 1013872, name = "Fire Mastery" },
{ itemID = 201195, name = "Fire Walk With Me" },
{ itemID = 201231, name = "Fired Up" },
{ itemID = 201534, name = "Flame Alignment" },
{ itemID = 1013617, name = "Flame Throwing" },
{ itemID = 200388, name = "Focal Point" },
{ itemID = 200163, name = "Focus Magic" },
{ itemID = 200181, name = "Frost Bomb" },
{ itemID = 1013622, name = "Frost Channeling" },
{ itemID = 1013873, name = "Frost Mastery" },
{ itemID = 1013853, name = "Frost Warding" },
{ itemID = 1013621, name = "Frostbite" },
{ itemID = 201158, name = "Frozen Flesh" },
{ itemID = 200513, name = "Frozen Haste" },
{ itemID = 200063, name = "Frozen Orb" },
{ itemID = 200735, name = "Glacial Cascade" },
{ itemID = 554207, name = "Glyph of Arcane Blast" },
{ itemID = 554100, name = "Glyph of Arcane Explosion" },
{ itemID = 554101, name = "Glyph of Arcane Missiles" },
{ itemID = 554103, name = "Glyph of Blink" },
{ itemID = 554221, name = "Glyph of Deep Freeze" },
{ itemID = 554275, name = "Glyph of Eternal Water" },
{ itemID = 554116, name = "Glyph of Evocation" },
{ itemID = 554107, name = "Glyph of Fire Blast" },
{ itemID = 554106, name = "Glyph of Fireball" },
{ itemID = 554114, name = "Glyph of Frost Nova" },
{ itemID = 554108, name = "Glyph of Frostbolt" },
{ itemID = 554120, name = "Glyph of Ice Armor" },
{ itemID = 554225, name = "Glyph of Ice Barrier" },
{ itemID = 554110, name = "Glyph of Ice Block" },
{ itemID = 554222, name = "Glyph of Living Bomb" },
{ itemID = 554118, name = "Glyph of Molten Armor" },
{ itemID = 554113, name = "Glyph of Polymorph" },
{ itemID = 554109, name = "Glyph of Scorch" },
{ itemID = 554111, name = "Glyph of Water Elemental" },
{ itemID = 200547, name = "Ice Barrage" },
{ itemID = 1013959, name = "Ice Floes" },
{ itemID = 1013699, name = "Ice Shards" },
{ itemID = 201543, name = "Icy Slaughter" },
{ itemID = 200142, name = "Icy Veins" },
{ itemID = 1013644, name = "Ignite" },
{ itemID = 1013618, name = "Impact" },
{ itemID = 1013620, name = "Improved Blizzard" },
{ itemID = 1013626, name = "Improved Counterspell" },
{ itemID = 1013612, name = "Improved Fire Blast" },
{ itemID = 1013614, name = "Improved Fireball" },
{ itemID = 1013738, name = "Improved Frostbolt" },
{ itemID = 1013648, name = "Improved Scorch" },
{ itemID = 1013956, name = "Incineration" },
{ itemID = 201556, name = "Kindling" },
{ itemID = 1013628, name = "Magic Attunement" },
{ itemID = 200067, name = "Mass Invisibility" },
{ itemID = 1013856, name = "Master of Elements" },
{ itemID = 200064, name = "Meteor" },
{ itemID = 1013623, name = "Permafrost" },
{ itemID = 201226, name = "Power Overwhelming" },
{ itemID = 1013860, name = "Precision" },
{ itemID = 200725, name = "Precision Impact" },
{ itemID = 201584, name = "Pyroclastic Surprise" },
{ itemID = 200697, name = "Rapid Blinking" },
{ itemID = 201290, name = "Rapid Shatter" },
{ itemID = 200123, name = "Ring of Frost" },
{ itemID = 200727, name = "Rune of Power" },
{ itemID = 201187, name = "Scorched Ground" },
{ itemID = 1013653, name = "Shatter" },
{ itemID = 1013619, name = "Spell Impact" },
{ itemID = 1013912, name = "Student of the Mind" },
{ itemID = 1013965, name = "Torment the Weak" },
{ itemID = 200372, name = "Touch of the Magi" },
{ itemID = 200916, name = "Trisolaris" },
{ itemID = 200585, name = "Wacky Wizardry" },
{ itemID = 203025, name = "Winter's Blaze" },
{ itemID = 1013615, name = "World in Flames" },
},
Paladin = {
{ itemID = 201051, name = "A New Dawn" },
{ itemID = 1013821, name = "Anticipation" },
{ itemID = 200157, name = "Aura Mastery" },
{ itemID = 1013822, name = "Benediction" },
{ itemID = 200975, name = "Blessed Hammer" },
{ itemID = 1013953, name = "Blessed Hands" },
{ itemID = 200593, name = "Blessing of the Divines" },
{ itemID = 200084, name = "Blinding Light" },
{ itemID = 200607, name = "Branding Zeal" },
{ itemID = 201277, name = "Consecrated Weapon" },
{ itemID = 200961, name = "Consecrated Zeal" },
{ itemID = 1013823, name = "Conviction" },
{ itemID = 200623, name = "Crimson Defense" },
{ itemID = 1013602, name = "Crusader of the Light" },
{ itemID = 201537, name = "Crusader's Judgement" },
{ itemID = 1013820, name = "Deflection" },
{ itemID = 1013833, name = "Divine Intellect" },
{ itemID = 1013609, name = "Divine Order" },
{ itemID = 201569, name = "Divine Reprieve" },
{ itemID = 1013601, name = "Divine Resurgence" },
{ itemID = 1013834, name = "Divine Strength" },
{ itemID = 1013981, name = "Divinity" },
{ itemID = 200116, name = "Execution Sentence" },
{ itemID = 1013847, name = "Eye for an Eye" },
{ itemID = 200110, name = "Gift of Light" },
{ itemID = 201439, name = "Glimmer of Light" },
{ itemID = 554035, name = "Glyph of Avenging Wrath" },
{ itemID = 554226, name = "Glyph of Beacon of Light" },
{ itemID = 554027, name = "Glyph of Consecration" },
{ itemID = 554026, name = "Glyph of Crusader Strike" },
{ itemID = 554230, name = "Glyph of Divine Plea" },
{ itemID = 554228, name = "Glyph of Divine Storm" },
{ itemID = 554036, name = "Glyph of Divinity" },
{ itemID = 554031, name = "Glyph of Exorcism" },
{ itemID = 554022, name = "Glyph of Hammer of Justice" },
{ itemID = 554227, name = "Glyph of Hammer of the Righteous" },
{ itemID = 554034, name = "Glyph of Holy Light" },
{ itemID = 554123, name = "Glyph of Holy Wrath" },
{ itemID = 554021, name = "Glyph of Judgement" },
{ itemID = 554232, name = "Glyph of Salvation" },
{ itemID = 554024, name = "Glyph of Seal of Command" },
{ itemID = 554121, name = "Glyph of Seal of Righteousness" },
{ itemID = 554037, name = "Glyph of Seal of Wisdom" },
{ itemID = 554122, name = "Glyph of Vengeance" },
{ itemID = 201240, name = "Guardian of Ancient Kings" },
{ itemID = 200410, name = "Hammerstorm" },
{ itemID = 1013830, name = "Healing Light" },
{ itemID = 1013836, name = "Heart of the Crusader" },
{ itemID = 1013895, name = "Holy Mastery" },
{ itemID = 201560, name = "Holy Templar" },
{ itemID = 200075, name = "Holy Templar: Light of Dawn" },
{ itemID = 1013828, name = "Illumination" },
{ itemID = 1013819, name = "Improved Blessing of Might" },
{ itemID = 1013824, name = "Improved Devotion Aura" },
{ itemID = 1013838, name = "Improved Hammer of Justice" },
{ itemID = 1013846, name = "Improved Judgements" },
{ itemID = 1013829, name = "Improved Lay on Hands" },
{ itemID = 1013837, name = "Improved Righteous Fury" },
{ itemID = 201164, name = "Light of the Guardian" },
{ itemID = 201159, name = "Light's Hammer" },
{ itemID = 201578, name = "Long Arm of the Law" },
{ itemID = 201436, name = "One With The Light" },
{ itemID = 201132, name = "Pious Strikes" },
{ itemID = 1013894, name = "Protection Mastery" },
{ itemID = 1013849, name = "Pursuit of Justice" },
{ itemID = 1013896, name = "Retribution Mastery" },
{ itemID = 201540, name = "Righteous Verdict" },
{ itemID = 201437, name = "Sacred Light" },
{ itemID = 1013902, name = "Sanctity of Battle" },
{ itemID = 200156, name = "Seal of Command" },
{ itemID = 1013835, name = "Seals of the Pure" },
{ itemID = 201393, name = "Spark of Hope" },
{ itemID = 1013827, name = "Spiritual Focus" },
{ itemID = 1013950, name = "Stoicism" },
{ itemID = 201202, name = "Swift Avenger" },
{ itemID = 200726, name = "Swift Execution" },
{ itemID = 201012, name = "Swift Favor" },
{ itemID = 201371, name = "Sword of Vengeance" },
{ itemID = 201438, name = "Synergistic Light" },
{ itemID = 201009, name = "Twist of Faith" },
{ itemID = 1013845, name = "Unyielding Faith" },
{ itemID = 1013848, name = "Vindication" },
},
Priest = {
{ itemID = 1013867, name = "Absolution" },
{ itemID = 200605, name = "Accelerated Ascension" },
{ itemID = 1013906, name = "Afterkindling" },
{ itemID = 200070, name = "Angelic Feather" },
{ itemID = 201156, name = "Armor of Faith" },
{ itemID = 200869, name = "Atonement" },
{ itemID = 1013905, name = "Blessed Vengeance" },
{ itemID = 201288, name = "Centered" },
{ itemID = 200479, name = "Circle of Life" },
{ itemID = 201463, name = "Dark Focus" },
{ itemID = 201172, name = "Dark Power Well" },
{ itemID = 1013700, name = "Darkness" },
{ itemID = 1013879, name = "Discipline Mastery" },
{ itemID = 1013786, name = "Divine Fury" },
{ itemID = 200072, name = "Divine Star" },
{ itemID = 201514, name = "Endless Void" },
{ itemID = 201185, name = "Energizing Prayer" },
{ itemID = 201237, name = "Faith Rekindled" },
{ itemID = 554061, name = "Glyph of Circle of Healing" },
{ itemID = 554063, name = "Glyph of Dispel Magic" },
{ itemID = 554064, name = "Glyph of Fear Ward" },
{ itemID = 554065, name = "Glyph of Flash Heal" },
{ itemID = 554069, name = "Glyph of Holy Nova" },
{ itemID = 554072, name = "Glyph of Inner Fire" },
{ itemID = 554073, name = "Glyph of Mind Flay" },
{ itemID = 554235, name = "Glyph of Penance" },
{ itemID = 554058, name = "Glyph of Power Word: Shield" },
{ itemID = 554062, name = "Glyph of Psychic Scream" },
{ itemID = 554060, name = "Glyph of Renew" },
{ itemID = 554076, name = "Glyph of Scourge Imprisonment" },
{ itemID = 554075, name = "Glyph of Shadow" },
{ itemID = 554067, name = "Glyph of Shadow Word: Pain" },
{ itemID = 554078, name = "Glyph of Smite" },
{ itemID = 1013904, name = "Glyph of Surge of Light" },
{ itemID = 201264, name = "Good Fortune" },
{ itemID = 1013606, name = "Grateful Prayer" },
{ itemID = 200111, name = "Halo" },
{ itemID = 1013696, name = "Healing Focus" },
{ itemID = 201126, name = "Higher Power" },
{ itemID = 200606, name = "Holy Ascension" },
{ itemID = 1013878, name = "Holy Mastery" },
{ itemID = 201165, name = "Holy Power Well" },
{ itemID = 200590, name = "Holy Radiance" },
{ itemID = 1013850, name = "Holy Reach" },
{ itemID = 200974, name = "Holy Servitude" },
{ itemID = 1013695, name = "Holy Specialization" },
{ itemID = 200784, name = "Holy Word: Chastise" },
{ itemID = 201249, name = "Impending Doom" },
{ itemID = 1013697, name = "Improved Healing" },
{ itemID = 1013687, name = "Improved Inner Fire" },
{ itemID = 1013688, name = "Improved Mana Burn" },
{ itemID = 1013702, name = "Improved Mind Blast" },
{ itemID = 1013685, name = "Improved Power Word: Fortitude" },
{ itemID = 1013686, name = "Improved Power Word: Shield" },
{ itemID = 1013710, name = "Improved Psychic Scream" },
{ itemID = 1013764, name = "Improved Renew" },
{ itemID = 1013703, name = "Improved Shadow Word: Pain" },
{ itemID = 1013708, name = "Improved Spirit Tap" },
{ itemID = 1013709, name = "Inspiration" },
{ itemID = 1300143, name = "Leap of Faith" },
{ itemID = 201565, name = "Light and Shadow" },
{ itemID = 1013689, name = "Martyrdom" },
{ itemID = 1013691, name = "Mental Agility" },
{ itemID = 201480, name = "Pendulum" },
{ itemID = 201517, name = "Power Word: Barrier" },
{ itemID = 201282, name = "Power of Generosity" },
{ itemID = 200748, name = "Prayer Circle" },
{ itemID = 201591, name = "Promise of Renewal" },
{ itemID = 554401, name = "Psychic Influence" },
{ itemID = 201580, name = "Psychic Infusion" },
{ itemID = 200143, name = "Purgation by Light" },
{ itemID = 200911, name = "Purity of Light" },
{ itemID = 200592, name = "Renewed Through Absolution" },
{ itemID = 1013698, name = "Searing Light" },
{ itemID = 1013704, name = "Shadow Affinity" },
{ itemID = 1013705, name = "Shadow Focus" },
{ itemID = 1013880, name = "Shadow Mastery" },
{ itemID = 1013765, name = "Shadow Reach" },
{ itemID = 201533, name = "Shadow Reserves" },
{ itemID = 200555, name = "Shadow Visions" },
{ itemID = 201581, name = "Shattered Renewal" },
{ itemID = 1013692, name = "Silent Resolve" },
{ itemID = 200153, name = "Soul Restoration" },
{ itemID = 200644, name = "Spirited Penance" },
{ itemID = 200776, name = "Twilight Paragon" },
{ itemID = 1013948, name = "Twin Disciplines" },
{ itemID = 1013693, name = "Unbreakable Will" },
{ itemID = 200078, name = "Void Eruption" },
{ itemID = 200195, name = "Void Herald: Grasp of Darkness" },
{ itemID = 200586, name = "Void Herald: Whispers of the Old Gods" },
{ itemID = 200099, name = "Void Shift" },
{ itemID = 201100, name = "Wanding" },
{ itemID = 200860, name = "Words of Healing" },
{ itemID = 200158, name = "Wraithweaver: Echoing Shadows" },
{ itemID = 201564, name = "Wraithweaver: Shadow Orbs" },
{ itemID = 200886, name = "Zany Zealot" },
},
Rogue = {
{ itemID = 201385, name = "Adrenaline Junkie" },
{ itemID = 1013977, name = "Aggression" },
{ itemID = 201086, name = "Assassin's Rush" },
{ itemID = 200577, name = "Assuaging Shadows" },
{ itemID = 201005, name = "Blade Vortex" },
{ itemID = 201450, name = "Bleeding Edge" },
{ itemID = 1013943, name = "Blood Spatter" },
{ itemID = 201048, name = "Blood-Soaked Mutilation" },
{ itemID = 201441, name = "Bloodshed" },
{ itemID = 1013670, name = "Camouflage" },
{ itemID = 1013659, name = "Close Quarters Combat" },
{ itemID = 1013882, name = "Combat Mastery" },
{ itemID = 201447, name = "Combat Proficiency" },
{ itemID = 200114, name = "Crimson Tempest" },
{ itemID = 1013662, name = "Deflection" },
{ itemID = 1013675, name = "Dirty Tricks" },
{ itemID = 200102, name = "Dispatch" },
{ itemID = 201304, name = "Doppelganger" },
{ itemID = 1013661, name = "Dual Wield Specialization" },
{ itemID = 1013666, name = "Endurance" },
{ itemID = 201542, name = "Energy Surge" },
{ itemID = 201092, name = "Envenomed" },
{ itemID = 201596, name = "Fatal Counter" },
{ itemID = 200556, name = "Festering Wound" },
{ itemID = 201113, name = "From the Shadows" },
{ itemID = 554139, name = "Glyph of Ambush" },
{ itemID = 554126, name = "Glyph of Backstab" },
{ itemID = 554143, name = "Glyph of Crippling Poison" },
{ itemID = 554125, name = "Glyph of Evasion" },
{ itemID = 554128, name = "Glyph of Eviscerate" },
{ itemID = 554242, name = "Glyph of Fan of Knives" },
{ itemID = 554138, name = "Glyph of Garrote" },
{ itemID = 554140, name = "Glyph of Ghostly Strike" },
{ itemID = 554135, name = "Glyph of Gouge" },
{ itemID = 554133, name = "Glyph of Hemorrhage" },
{ itemID = 554244, name = "Glyph of Mutilate" },
{ itemID = 554142, name = "Glyph of Preparation" },
{ itemID = 554127, name = "Glyph of Rupture" },
{ itemID = 554124, name = "Glyph of Sap" },
{ itemID = 554144, name = "Glyph of Sinister Strike" },
{ itemID = 554136, name = "Glyph of Slice and Dice" },
{ itemID = 554137, name = "Glyph of Sprint" },
{ itemID = 554131, name = "Glyph of Vigor" },
{ itemID = 200941, name = "Hastened Feint" },
{ itemID = 201329, name = "Hazardous Escape" },
{ itemID = 1013674, name = "Improved Ambush" },
{ itemID = 1013681, name = "Improved Eviscerate" },
{ itemID = 1013658, name = "Improved Gouge" },
{ itemID = 1013676, name = "Improved Poisons" },
{ itemID = 1013663, name = "Improved Sinister Strike" },
{ itemID = 1013669, name = "Initiative" },
{ itemID = 200397, name = "Kingsbane" },
{ itemID = 1013677, name = "Lethality" },
{ itemID = 1013657, name = "Lightning Reflexes" },
{ itemID = 200955, name = "Massacre" },
{ itemID = 1013668, name = "Master of Deception" },
{ itemID = 201320, name = "Methodical Approach" },
{ itemID = 201567, name = "Murder Rush" },
{ itemID = 200557, name = "Natural Energy" },
{ itemID = 201539, name = "No Remorse" },
{ itemID = 1013673, name = "Opportunity" },
{ itemID = 201602, name = "Phantom Stab" },
{ itemID = 200310, name = "Poison Bomb" },
{ itemID = 1013660, name = "Precision" },
{ itemID = 1013664, name = "Puncturing Wounds" },
{ itemID = 1013972, name = "Relentless Strikes" },
{ itemID = 1013679, name = "Remorseless Attacks" },
{ itemID = 201381, name = "Repartee" },
{ itemID = 200144, name = "Riposte" },
{ itemID = 201283, name = "Rush of Blood" },
{ itemID = 1013680, name = "Ruthlessness" },
{ itemID = 1013684, name = "Serrated Blades" },
{ itemID = 201118, name = "Shadow Blades" },
{ itemID = 200579, name = "Shadow of Death" },
{ itemID = 201031, name = "Shattering Execution" },
{ itemID = 201448, name = "Sinister Finisher" },
{ itemID = 201223, name = "Sinister Flurry" },
{ itemID = 1013883, name = "Subtlety Mastery" },
{ itemID = 201361, name = "Veiled Wire" },
{ itemID = 200671, name = "Venomous Edge" },
{ itemID = 1013735, name = "Vile Poisons" },
},
Shaman = {
{ itemID = 200118, name = "Air Ascendance" },
{ itemID = 1013725, name = "Ancestral Healing" },
{ itemID = 1013766, name = "Ancestral Knowledge" },
{ itemID = 1013726, name = "Anticipation" },
{ itemID = 202566, name = "Ascending Flames" },
{ itemID = 200109, name = "Astral Plane" },
{ itemID = 200559, name = "Booming Thunder" },
{ itemID = 1013715, name = "Call of Flame" },
{ itemID = 201493, name = "Capacitor Totem" },
{ itemID = 201413, name = "Cataclysmic Sundering" },
{ itemID = 201311, name = "Charged Tides" },
{ itemID = 200062, name = "Cloudburst Totem" },
{ itemID = 1013711, name = "Concussion" },
{ itemID = 1013712, name = "Convection" },
{ itemID = 201541, name = "Crackling Flames" },
{ itemID = 201831, name = "Defense of Thundarian" },
{ itemID = 200901, name = "Dynamic Charge" },
{ itemID = 1013714, name = "Earth's Grasp" },
{ itemID = 201473, name = "Earthen Grace" },
{ itemID = 200130, name = "Earthen Guardian Mastery" },
{ itemID = 200097, name = "Earthen Spike" },
{ itemID = 201492, name = "Earthquake" },
{ itemID = 200889, name = "Eccentric Elementalist" },
{ itemID = 201604, name = "Electric Surge" },
{ itemID = 201488, name = "Elemental Blast" },
{ itemID = 1013858, name = "Elemental Devastation" },
{ itemID = 201477, name = "Elemental Equilibrium" },
{ itemID = 1013716, name = "Elemental Focus" },
{ itemID = 1013975, name = "Elemental Fury" },
{ itemID = 201606, name = "Elemental Harmony" },
{ itemID = 1013888, name = "Elemental Mastery" },
{ itemID = 1013854, name = "Elemental Warding" },
{ itemID = 1013857, name = "Elemental Weapons" },
{ itemID = 1013890, name = "Enhancement Mastery" },
{ itemID = 1013855, name = "Eye of the Storm" },
{ itemID = 200061, name = "Flame Ascendance" },
{ itemID = 201486, name = "Forked Lightning" },
{ itemID = 200935, name = "Frugal Disposition" },
{ itemID = 201027, name = "Fury of the Wind" },
{ itemID = 554039, name = "Glyph of Chain Heal" },
{ itemID = 554051, name = "Glyph of Chain Lightning" },
{ itemID = 554249, name = "Glyph of Earth Shield" },
{ itemID = 200127, name = "Glyph of Earthen Guardian" },
{ itemID = 554247, name = "Glyph of Feral Spirit" },
{ itemID = 554057, name = "Glyph of Fire Elemental Totem" },
{ itemID = 554052, name = "Glyph of Fire Nova" },
{ itemID = 554049, name = "Glyph of Flame Shock" },
{ itemID = 554053, name = "Glyph of Flametongue Weapon" },
{ itemID = 554045, name = "Glyph of Frost Shock" },
{ itemID = 554042, name = "Glyph of Healing Wave" },
{ itemID = 554251, name = "Glyph of Hex" },
{ itemID = 554056, name = "Glyph of Lava" },
{ itemID = 554040, name = "Glyph of Lesser Healing Wave" },
{ itemID = 554055, name = "Glyph of Lightning Bolt" },
{ itemID = 554050, name = "Glyph of Lightning Shield" },
{ itemID = 554248, name = "Glyph of Riptide" },
{ itemID = 200149, name = "Glyph of Shamanistic Strikes" },
{ itemID = 554252, name = "Glyph of Stoneclaw Totem" },
{ itemID = 554048, name = "Glyph of Stormstrike" },
{ itemID = 554246, name = "Glyph of Thunder" },
{ itemID = 554250, name = "Glyph of Totem of Wrath" },
{ itemID = 554047, name = "Glyph of Windfury Weapon" },
{ itemID = 1013724, name = "Healing Focus" },
{ itemID = 1013859, name = "Healing Grace" },
{ itemID = 200124, name = "Healing Rain" },
{ itemID = 201513, name = "Healing Tide" },
{ itemID = 201475, name = "Hydromancer: Water Bolt" },
{ itemID = 201474, name = "Hydromancer: Water Nova" },
{ itemID = 1013736, name = "Improved Fire Nova" },
{ itemID = 1013728, name = "Improved Ghost Wolf" },
{ itemID = 1013723, name = "Improved Healing Wave" },
{ itemID = 1013945, name = "Improved Shields" },
{ itemID = 1013717, name = "Improved Water Shield" },
{ itemID = 201603, name = "Lava Ignition" },
{ itemID = 200174, name = "Lava Sweep" },
{ itemID = 201415, name = "Leader of the Elements" },
{ itemID = 200922, name = "Lightning Fangs" },
{ itemID = 200851, name = "Low Tide" },
{ itemID = 201481, name = "Mastery of Lightning" },
{ itemID = 200565, name = "Mending Tide" },
{ itemID = 201485, name = "Molten Outburst" },
{ itemID = 200999, name = "Nature's Flow" },
{ itemID = 201489, name = "Pack Alpha" },
{ itemID = 201314, name = "Primal Tide" },
{ itemID = 200807, name = "Primordial Aftershocks" },
{ itemID = 1013889, name = "Restoration Mastery" },
{ itemID = 1013713, name = "Reverberation" },
{ itemID = 1013911, name = "Shamanistic Focus" },
{ itemID = 201516, name = "Shock Tactics" },
{ itemID = 202576, name = "Spirit Link Totem" },
{ itemID = 200854, name = "Spreading Tides" },
{ itemID = 200652, name = "Stormborn" },
{ itemID = 200141, name = "Stormbound Instinct" },
{ itemID = 1013730, name = "Thundering Strikes" },
{ itemID = 1013720, name = "Tidal Focus" },
{ itemID = 1013721, name = "Tidal Mastery" },
{ itemID = 201571, name = "Tides of Restoration" },
{ itemID = 201482, name = "Totem Master: Fire" },
{ itemID = 1013722, name = "Totemic Focus" },
{ itemID = 201186, name = "Transcendental Embrace" },
{ itemID = 200924, name = "Ungrounded" },
{ itemID = 200925, name = "Wind Lash" },
{ itemID = 200090, name = "Windwalk Totem" },
{ itemID = 201394, name = "Zephyr" },
},
Warlock = {
{ itemID = 1013877, name = "Affliction Mastery" },
{ itemID = 1013776, name = "Aftermath" },
{ itemID = 1013985, name = "Agent of Chaos" },
{ itemID = 201454, name = "Agonizing Coil" },
{ itemID = 1013769, name = "Bane" },
{ itemID = 200712, name = "Bane of Havoc" },
{ itemID = 200093, name = "Blood Horror" },
{ itemID = 200112, name = "Burning Rush" },
{ itemID = 1013767, name = "Cataclysm" },
{ itemID = 201459, name = "Cataclysmic Burst" },
{ itemID = 201136, name = "Chaos Manifesting" },
{ itemID = 200804, name = "Cursed Shadows" },
{ itemID = 201453, name = "Curseweaver" },
{ itemID = 201576, name = "Dark Harvest" },
{ itemID = 201419, name = "Decisive Decimation" },
{ itemID = 1013791, name = "Demonic Brutality" },
{ itemID = 200570, name = "Demonic Influence" },
{ itemID = 200800, name = "Demonic Persistence" },
{ itemID = 200571, name = "Demonic Reoccurence" },
{ itemID = 200763, name = "Demonic Siphon" },
{ itemID = 200699, name = "Demonic Whirl" },
{ itemID = 1013876, name = "Demonology Mastery" },
{ itemID = 1013875, name = "Destruction Mastery" },
{ itemID = 1013773, name = "Destructive Reach" },
{ itemID = 201455, name = "Doomcaller's Wrath" },
{ itemID = 200952, name = "Dusk Till Dawn" },
{ itemID = 1013866, name = "Empowered Corruption" },
{ itemID = 200568, name = "Endless Agony" },
{ itemID = 200131, name = "Enslave Demon" },
{ itemID = 1013768, name = "Fel Concentration" },
{ itemID = 200996, name = "Fel Damnation" },
{ itemID = 200951, name = "Fire Attunement" },
{ itemID = 554089, name = "Glyph of Conflagrate" },
{ itemID = 554081, name = "Glyph of Corruption" },
{ itemID = 554092, name = "Glyph of Curse of Agony" },
{ itemID = 554094, name = "Glyph of Fear" },
{ itemID = 554084, name = "Glyph of Immolate" },
{ itemID = 554097, name = "Glyph of Imp" },
{ itemID = 554093, name = "Glyph of Incinerate" },
{ itemID = 554259, name = "Glyph of Life Tap" },
{ itemID = 554276, name = "Glyph of Quick Decay" },
{ itemID = 554091, name = "Glyph of Shadow Bolt" },
{ itemID = 554258, name = "Glyph of Soul Link" },
{ itemID = 554096, name = "Glyph of Voidwalker" },
{ itemID = 1013782, name = "Grim Reach" },
{ itemID = 200104, name = "Hand of Gul'dan" },
{ itemID = 200857, name = "Heating Up!" },
{ itemID = 200801, name = "Heretic of Gul'dan" },
{ itemID = 1013772, name = "Improved Corruption" },
{ itemID = 1013796, name = "Improved Curse of Agony" },
{ itemID = 1013780, name = "Improved Curse of Weakness" },
{ itemID = 1013954, name = "Improved Fear" },
{ itemID = 1013770, name = "Improved Shadow Bolt" },
{ itemID = 201451, name = "Inner Flame" },
{ itemID = 1013778, name = "Intensity" },
{ itemID = 1013792, name = "Master Summoner" },
{ itemID = 201456, name = "Nether Portal" },
{ itemID = 1013775, name = "Nightfall" },
{ itemID = 1013974, name = "Ruin" },
{ itemID = 201529, name = "Searing Flames" },
{ itemID = 200621, name = "Shadow Crash" },
{ itemID = 201570, name = "Shadow Funnel" },
{ itemID = 200150, name = "Shadowburn" },
{ itemID = 201460, name = "Soul Erosion" },
{ itemID = 200049, name = "Soul Harvest" },
{ itemID = 200152, name = "Soul Link" },
{ itemID = 1013771, name = "Soul Siphon" },
{ itemID = 200105, name = "Soul Swap" },
{ itemID = 201573, name = "Sudden Aftermath" },
{ itemID = 1013779, name = "Suppression" },
{ itemID = 201544, name = "Twilight Reaper" },
{ itemID = 200091, name = "Unending Resolve" },
{ itemID = 1013795, name = "Unholy Power" },
{ itemID = 201364, name = "Unstable Void" },
{ itemID = 200572, name = "Wild Felfire" },
},
Warrior = {
{ itemID = 200626, name = "Ambidextrous" },
{ itemID = 1013636, name = "Anticipation" },
{ itemID = 1013976, name = "Armored to the Teeth" },
{ itemID = 200048, name = "Avatar" },
{ itemID = 201217, name = "Battering Ram" },
{ itemID = 1013733, name = "Blood Craze" },
{ itemID = 1013605, name = "Blood and Thunder" },
{ itemID = 201230, name = "Bloodthirsty" },
{ itemID = 1013642, name = "Booming Voice" },
{ itemID = 201579, name = "Brutal Execution" },
{ itemID = 200240, name = "Bulwark" },
{ itemID = 1300145, name = "Colossus Smash" },
{ itemID = 1013646, name = "Commanding Presence" },
{ itemID = 201388, name = "Counterpoise" },
{ itemID = 201225, name = "Crackling Thunder" },
{ itemID = 1013645, name = "Cruelty" },
{ itemID = 1013647, name = "Deep Wounds" },
{ itemID = 1013732, name = "Deflection" },
{ itemID = 201142, name = "Desperation" },
{ itemID = 201432, name = "Devastating Blademaster" },
{ itemID = 200179, name = "Dragon Roar" },
{ itemID = 200507, name = "Dragon Warrior" },
{ itemID = 1013841, name = "Dual Wield Specialization" },
{ itemID = 201056, name = "Enduring Aegis" },
{ itemID = 1013656, name = "Enrage" },
{ itemID = 1013897, name = "Fury Mastery" },
{ itemID = 201609, name = "Gladiator Stance" },
{ itemID = 554170, name = "Glyph of Barbaric Insults" },
{ itemID = 554280, name = "Glyph of Blocking" },
{ itemID = 554174, name = "Glyph of Bloodthirst" },
{ itemID = 554171, name = "Glyph of Cleaving" },
{ itemID = 554184, name = "Glyph of Devastate" },
{ itemID = 554172, name = "Glyph of Execution" },
{ itemID = 554176, name = "Glyph of Hamstring" },
{ itemID = 554168, name = "Glyph of Heroic Strike" },
{ itemID = 554173, name = "Glyph of Mortal Strike" },
{ itemID = 554182, name = "Glyph of Overpower" },
{ itemID = 554166, name = "Glyph of Rapid Charge" },
{ itemID = 554181, name = "Glyph of Rending" },
{ itemID = 554167, name = "Glyph of Resonating Power" },
{ itemID = 554169, name = "Glyph of Revenge" },
{ itemID = 554265, name = "Glyph of Shield Wall" },
{ itemID = 554261, name = "Glyph of Shockwave" },
{ itemID = 554264, name = "Glyph of Spell Reflection" },
{ itemID = 554183, name = "Glyph of Sunder Armor" },
{ itemID = 554180, name = "Glyph of Sweeping Strikes" },
{ itemID = 554179, name = "Glyph of Victory Rush" },
{ itemID = 554175, name = "Glyph of Whirlwind" },
{ itemID = 201227, name = "Going For the Kill" },
{ itemID = 201590, name = "Heavy Thunder" },
{ itemID = 200611, name = "Heavyweight" },
{ itemID = 201291, name = "Here Comes The Big One" },
{ itemID = 1300144, name = "Heroic Leap" },
{ itemID = 202577, name = "Heroic Rush" },
{ itemID = 1013734, name = "Impale" },
{ itemID = 1013633, name = "Improved Charge" },
{ itemID = 1013839, name = "Improved Cleave" },
{ itemID = 1013649, name = "Improved Demoralizing Shout" },
{ itemID = 1013840, name = "Improved Execute" },
{ itemID = 1013630, name = "Improved Heroic Strike" },
{ itemID = 1013652, name = "Improved Overpower" },
{ itemID = 1013629, name = "Improved Rend" },
{ itemID = 1013638, name = "Improved Revenge" },
{ itemID = 1013631, name = "Improved Thunder Clap" },
{ itemID = 1013936, name = "Incite" },
{ itemID = 201289, name = "Insatiable" },
{ itemID = 1013651, name = "Iron Will" },
{ itemID = 201279, name = "Jab Cross" },
{ itemID = 201261, name = "Master of Arms" },
{ itemID = 201398, name = "Onslaught" },
{ itemID = 200969, name = "Outrage" },
{ itemID = 200631, name = "Overwhelming Rage" },
{ itemID = 201382, name = "Power Slam" },
{ itemID = 1013899, name = "Protection Mastery" },
{ itemID = 1013640, name = "Puncture" },
{ itemID = 200122, name = "Raging Blow" },
{ itemID = 201524, name = "Relentless Fury" },
{ itemID = 201532, name = "Revengeful Block" },
{ itemID = 201324, name = "Revitalizing Revenge" },
{ itemID = 201330, name = "Roar of Dread" },
{ itemID = 201161, name = "Seismic Shockwave" },
{ itemID = 201014, name = "Shield Finesse" },
{ itemID = 201380, name = "Shield Master's Resolve" },
{ itemID = 1013862, name = "Shield Mastery" },
{ itemID = 200125, name = "Siegebreaker" },
{ itemID = 1013607, name = "Spell Block" },
{ itemID = 200203, name = "Stormhammer" },
{ itemID = 201395, name = "Sword and Thunder" },
{ itemID = 1013632, name = "Tactical Mastery" },
{ itemID = 1013969, name = "Taste for Blood" },
{ itemID = 201499, name = "Throwing Mastery" },
{ itemID = 201047, name = "Thundering Blow" },
{ itemID = 200632, name = "Titan's Fury" },
{ itemID = 1013634, name = "Two-Handed Weapon Specialization" },
{ itemID = 1013654, name = "Unbridled Wrath" },
{ itemID = 201530, name = "Victorious" },
{ itemID = 202590, name = "Vigilant Vanguard" },
{ itemID = 201400, name = "War Banner" },
},
}
AE.ScrollCatalogTotal = 825
AE._loadedScrollCatalog = true
+125
View File
@@ -0,0 +1,125 @@
-- CoaExporter - Export UI
local function CreateOrGetFrame()
if CoaExporterFrame then return CoaExporterFrame end
local frame = CreateFrame("Frame", "CoaExporterFrame", UIParent, "DialogBoxFrame")
frame:SetSize(700, 520)
frame:SetPoint("CENTER")
frame:SetMovable(true)
frame:EnableMouse(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", frame.StartMoving)
frame:SetScript("OnDragStop", frame.StopMovingOrSizing)
local title = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
title:SetPoint("TOP", 0, -8)
frame.title = title
-- Two rows of buttons: per-character on top, catalog on bottom.
local function makeBtn(label, x, y, width, click)
local btn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
btn:SetSize(width or 90, 22)
btn:SetPoint("TOPLEFT", 16 + x, y)
btn:SetText(label)
btn:SetScript("OnClick", click)
return btn
end
local function ce()
return _G.CoaExporter
end
-- Row 1: character export
local x = 0
makeBtn("All", x, -32, 60, function() if ce() then ce():Export("all") end end)
x = x + 65
makeBtn("Talents", x, -32, 70, function() if ce() then ce():Export("talents") end end)
x = x + 75
makeBtn("Gear", x, -32, 60, function() if ce() then ce():Export("gear") end end)
x = x + 65
makeBtn("Enchants", x, -32, 80, function() if ce() then ce():Export("enchants") end end)
x = x + 85
makeBtn("MD Gear", x, -32, 75, function()
local ae = ce()
if ae and ae.GenerateMarkdownGear then
ae:ShowExport(ae:GenerateMarkdownGear() or "", "CoaExporter - Markdown Gear (Ctrl+C)")
end
end)
x = x + 80
makeBtn("MD Enchants", x, -32, 90, function()
local ae = ce()
if ae and ae.GenerateMarkdownEnchants then
ae:ShowExport(ae:GenerateMarkdownEnchants() or "", "CoaExporter - Markdown Enchants (Ctrl+C)")
end
end)
x = x + 95
makeBtn("MD Full", x, -32, 75, function()
local ae = ce()
if ae and ae.GenerateMarkdownFull then
ae:ShowExport(ae:GenerateMarkdownFull() or "", "CoaExporter - Wiki Markdown (Ctrl+C)")
end
end)
-- Row 2: catalog (game data) export
x = 0
makeBtn("Catalog: Skills", x, -58, 110, function()
if CoaExporter and CoaExporter.Catalog then
CoaExporter.Catalog.Run("skills")
end
end)
x = x + 115
makeBtn("Catalog: Talents", x, -58, 120, function()
if CoaExporter and CoaExporter.Catalog then
CoaExporter.Catalog.Run("talents")
end
end)
x = x + 125
makeBtn("Catalog: All", x, -58, 100, function()
if CoaExporter and CoaExporter.Catalog then
CoaExporter.Catalog.Run("all")
end
end)
x = x + 105
makeBtn("Scrolls: Scan", x, -58, 100, function()
if CoaExporter and CoaExporter.ScrollsStartScan then
CoaExporter.ScrollsStartScan(function(stats)
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"CoaExporter scrolls: scan complete - %d resolved, %d unresolved",
stats.total - stats.unresolved, stats.unresolved))
end)
end
end)
-- ScrollFrame + EditBox
local scrollFrame = CreateFrame("ScrollFrame", "CoaExporterScrollFrame", frame, "UIPanelScrollFrameTemplate")
scrollFrame:SetPoint("TOPLEFT", 16, -88)
scrollFrame:SetPoint("BOTTOMRIGHT", -32, 16)
local editBox = CreateFrame("EditBox", "CoaExporterEditBox", scrollFrame)
editBox:SetMultiLine(true)
editBox:SetAutoFocus(true)
editBox:SetFontObject(ChatFontNormal)
editBox:SetWidth(640)
editBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
editBox:SetScript("OnEditFocusGained", function(self) self:HighlightText() end)
scrollFrame:SetScrollChild(editBox)
frame.scrollFrame = scrollFrame
frame.editBox = editBox
local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
close:SetPoint("TOPRIGHT", -4, -4)
CoaExporterFrame = frame
return frame
end
function CoaExporter_ShowExportFrame(text, titleText)
local f = CreateOrGetFrame()
f:Show()
f.editBox:SetText(text or "")
f.title:SetText(titleText or "CoaExporter (Ctrl+C)")
f.editBox:HighlightText()
f.editBox:SetFocus()
end
+58
View File
@@ -0,0 +1,58 @@
-- Minimal JSON encoder for CoaExporter
local function escape_str(s)
s = tostring(s)
s = s:gsub('\\', '\\\\')
s = s:gsub('"', '\\"')
s = s:gsub('\n', '\\n')
s = s:gsub('\r', '\\r')
s = s:gsub('\t', '\\t')
return '"' .. s .. '"'
end
local function is_array(t)
if type(t) ~= 'table' then return false end
local n = 0
for k, _ in pairs(t) do
if type(k) ~= 'number' then return false end
n = n + 1
end
for i = 1, n do
if t[i] == nil then return false end
end
return true
end
local function encode_value(v)
local tv = type(v)
if tv == 'nil' then
return 'null'
elseif tv == 'boolean' then
return v and 'true' or 'false'
elseif tv == 'number' then
return tostring(v)
elseif tv == 'string' then
return escape_str(v)
elseif tv == 'table' then
if is_array(v) then
local parts = {}
for i = 1, #v do
parts[#parts+1] = encode_value(v[i])
end
return '[' .. table.concat(parts, ',') .. ']'
else
local parts = {}
for k, val in pairs(v) do
local key = escape_str(k)
parts[#parts+1] = key .. ':' .. encode_value(val)
end
return '{' .. table.concat(parts, ',') .. '}'
end
else
return 'null'
end
end
function CoaExporter_Json_Encode(obj)
return encode_value(obj)
end
+146
View File
@@ -0,0 +1,146 @@
CoA Exporter
============
One Lua addon for Children of Ascension. Two jobs:
1. **Per-character export** — your character's talents, gear, mystic
enchants, and the full mystic-scroll tooltip DB. Output as JSON or
Wiki.js Markdown for guides on the guild wiki / exil.es.
2. **Game-data catalog dump** — every skill, level passive, dispel, and
talent-tree node for all 21 CoA classes. Feeds db.exil.es and the
talent calculator.
Built for **Ascension WotLK 3.3.5** and depends on Ascension's
`C_CharacterAdvancement` / `C_MysticEnchant` APIs.
This is the merged successor to:
- `ascension-char-exporter` (per-character, `/ascx`)
- `CoA_SkillExporter` (`/skilldump`, `/dispels`, `/passives`)
- `CoA_TalentExporter` (`/talentdumpall`)
Install
-------
Copy `CoaExporter/` into your AddOns directory:
```
<wow>/Interface/AddOns/CoaExporter/CoaExporter.toc
```
Or, on Sub-Net's setup with the addons path symlinked:
```bash
./scripts/install_addon_sub.sh
```
Reload (`/reload` or relog) so the .toc is picked up.
Slash commands
--------------
Primary slash: `/coae`. Aliases: `/coaexp`, `/ascx`, `/asxc`.
### Per-character export
```
/coae export all JSON of talents + gear + mystic enchants
/coae export talents JSON, talents only
/coae export gear JSON, gear only
/coae export enchants JSON, mystic enchants only
/coae export mdgear Markdown gear table (Wiki.js)
/coae export mdenchants Markdown enchants table
/coae export md Full wiki page (header + gear + enchants + talents)
```
### Mystic scroll catalog (per-account, slow scan)
```
/coae scrolls scan Resolve all 825 scrolls' tooltips (~20s, retries)
/coae scrolls export JSON of the resolved cache
/coae scrolls reset Clear the cache
/coae scrolls status How many scrolls have been resolved
```
The cache lives in `CoaExporterScrollCache` (SavedVariables). Run scan
once per account; results persist across sessions and characters.
### Game-data catalog (for db.exil.es / talent-calc)
```
/coae catalog all One pass: dump skills + talents
/coae catalog skills Dump skills/dispels/passives only
/coae catalog talents Dump talent-tree nodes only
/coae catalog dispels [class] Print dispel summary (or for one class)
/coae catalog passives [class]Print level-passive summary (or for one class)
/coae catalog status Last scan time and counts
```
The catalog dump walks `C_CharacterAdvancement.GetAllEntries()` once and
fans out to all registered collectors, so `catalog all` only walks the
list once. After it finishes, `/reload` to flush the SavedVariable
`CoaExporterCatalog` to disk; pick it up in
`WTF/Account/<acct>/SavedVariables/CoaExporter.lua`.
The legacy slashes still work as aliases: `/skilldump``catalog skills`,
`/talentdumpall``catalog talents`, `/dispels` and `/passives` map to
the catalog list helpers.
### Misc
```
/coae sv on|off Toggle SavedVariables snapshot of /coae export
/coae debug Show collector status
/coae help
```
UI
--
`/coae` (with no args) opens the export window. Two rows of buttons:
- Row 1 — character: `All / Talents / Gear / Enchants / MD Gear / MD Enchants / MD Full`
- Row 2 — catalogs: `Catalog: Skills / Catalog: Talents / Catalog: All / Scrolls: Scan`
The window is a plain copy-out box. `Ctrl+C` after it auto-selects the
text.
SavedVariables
--------------
| Key | Written by | Notes |
|---------------------------|-------------------------------|-------|
| `CoaExporterSaved` | `/coae export …` when sv = on | Latest character export, keyed by `realm:name` |
| `CoaExporterConfig` | `/coae sv on/off` | Just `enableSavedVariables` for now |
| `CoaExporterScrollCache` | `/coae scrolls scan` | Mystic scroll item tooltips |
| `CoaExporterCatalog` | `/coae catalog …` | `skills`, `dispels`, `levelPassives`, `talents`, `_meta` |
Layout
------
```
CoaExporter/
├── CoaExporter.toc
├── Util/Json.lua
├── Data/ScrollCatalog.lua (auto-generated from AtlasLootAscension)
├── Collectors/ (per-character data)
│ ├── Talents.lua
│ ├── Gear.lua
│ ├── Enchants.lua
│ ├── MysticScrolls.lua
│ └── MysticScrollProbe.lua
├── Catalogs/ (game-data dumps for the wiki/calc)
│ ├── Common.lua (one entry-walk; fans out to collectors)
│ ├── Skills.lua
│ └── Talents.lua
├── UI/ExportFrame.lua
└── Core.lua (slash router)
```
Re-generate the scroll catalog from the latest AtlasLootAscension copy:
```
scripts/regen_scroll_catalog.py
```
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail
# Installs/syncs the CoaExporter addon into your WoW Ascension AddOns folder.
# Default target: /srv/add01/wow-ascension/Interface/AddOns
# Usage:
# scripts/install_addon_sub.sh [TARGET_ADDONS_DIR]
# Example:
# scripts/install_addon_sub.sh /srv/add01/wow-ascension/Interface/AddOns
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="${SCRIPT_DIR}/.."
SRC="${REPO_ROOT}/CoaExporter"
DEST_BASE="${1:-/srv/add01/wow-ascension/Interface/AddOns}"
DEST="${DEST_BASE}/CoaExporter"
if [[ ! -d "${SRC}" ]]; then
echo "ERROR: Source addon folder not found at: ${SRC}" >&2
exit 1
fi
echo "Installing CoaExporter from: ${SRC}"
echo "Target AddOns directory: ${DEST_BASE}"
mkdir -p "${DEST_BASE}"
if command -v rsync >/dev/null 2>&1; then
echo "Using rsync to copy files..."
rsync -a --delete "${SRC}/" "${DEST}/"
else
echo "rsync not found; using cp -a"
mkdir -p "${DEST}"
# Copy contents of SRC into DEST, preserving attributes
cp -a "${SRC}/." "${DEST}/"
fi
echo "Done. Addon installed to: ${DEST}"
echo "If needed, enable it on the character select screen and type /reload in-game."
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""
regen_scroll_catalog.py — re-generate CoaExporter/Data/ScrollCatalog.lua
from the AtlasLootAscension MysticEnchants.lua source of truth.
Usage:
python3 scripts/regen_scroll_catalog.py [--source PATH] [--out PATH]
"""
from __future__ import annotations
import argparse
import collections
import re
from pathlib import Path
DEFAULT_SOURCE = Path('/home/sub/public-repos/AtlasLootAscension/'
'AtlasLoot_OriginalWoW/MysticEnchants.lua')
DEFAULT_OUT = Path(__file__).parent.parent / \
'CoaExporter' / 'Data' / 'ScrollCatalog.lua'
def extract(source: Path):
text = source.read_text()
sections = re.findall(
r'\["(MysticEnchants\w+)"\]\s*=\s*\{(.*?)^\s*\}\s*,',
text, re.DOTALL | re.MULTILINE)
out = collections.defaultdict(list)
for cls, body in sections:
class_name = cls.replace('MysticEnchants', '')
for iid, name in re.findall(
r'\{\s*itemID\s*=\s*(\d+)(?:[^}]*?)?\}\s*,\s*(?://|--)\s*(.+?)(?:\n|$)',
body):
out[class_name].append((int(iid), name.strip()))
return out
def render_lua(by_class) -> str:
total = sum(len(v) for v in by_class.values())
lines = [
'-- CoaExporter / Data / ScrollCatalog.lua',
'-- Auto-generated from AtlasLootAscension MysticEnchants.lua',
f'-- {total} mystic scroll item IDs across {len(by_class)} classes',
'-- Re-generate: scripts/regen_scroll_catalog.py',
'',
'CoaExporter = _G.CoaExporter or {}',
'local AE = CoaExporter',
'',
'AE.ScrollCatalog = {',
]
for cls in sorted(by_class):
lines.append(f' {cls} = {{')
for iid, name in sorted(by_class[cls], key=lambda x: x[1]):
esc = name.replace('"', '\\"')
lines.append(f' {{ itemID = {iid}, name = "{esc}" }},')
lines.append(' },')
lines.extend([
'}',
'',
f'AE.ScrollCatalogTotal = {total}',
'AE._loadedScrollCatalog = true',
'',
])
return '\n'.join(lines)
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--source', type=Path, default=DEFAULT_SOURCE)
ap.add_argument('--out', type=Path, default=DEFAULT_OUT)
args = ap.parse_args()
by_class = extract(args.source)
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(render_lua(by_class))
total = sum(len(v) for v in by_class.values())
print(f'wrote {args.out} ({total} scrolls, {len(by_class)} classes)')
if __name__ == '__main__':
main()