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:
@@ -0,0 +1,3 @@
|
||||
dist/*.zip
|
||||
*.swp
|
||||
.DS_Store
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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("", 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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
Executable
+39
@@ -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."
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user