commit 2b97a68317405812e03cef2979bf18c462597737 Author: Florian Berthold Date: Thu May 7 10:43:16 2026 +0200 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). diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f0174d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +dist/*.zip +*.swp +.DS_Store diff --git a/CoaExporter/Catalogs/Common.lua b/CoaExporter/Catalogs/Common.lua new file mode 100644 index 0000000..dc47e98 --- /dev/null +++ b/CoaExporter/Catalogs/Common.lua @@ -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 diff --git a/CoaExporter/Catalogs/Skills.lua b/CoaExporter/Catalogs/Skills.lua new file mode 100644 index 0000000..7618e38 --- /dev/null +++ b/CoaExporter/Catalogs/Skills.lua @@ -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 diff --git a/CoaExporter/Catalogs/Talents.lua b/CoaExporter/Catalogs/Talents.lua new file mode 100644 index 0000000..92f8976 --- /dev/null +++ b/CoaExporter/Catalogs/Talents.lua @@ -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 diff --git a/CoaExporter/CoaExporter.toc b/CoaExporter/CoaExporter.toc new file mode 100644 index 0000000..04e9f1c --- /dev/null +++ b/CoaExporter/CoaExporter.toc @@ -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 diff --git a/CoaExporter/Collectors/Enchants.lua b/CoaExporter/Collectors/Enchants.lua new file mode 100644 index 0000000..27ecc2f --- /dev/null +++ b/CoaExporter/Collectors/Enchants.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 diff --git a/CoaExporter/Collectors/Gear.lua b/CoaExporter/Collectors/Gear.lua new file mode 100644 index 0000000..8b146a0 --- /dev/null +++ b/CoaExporter/Collectors/Gear.lua @@ -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 diff --git a/CoaExporter/Collectors/MysticScrollProbe.lua b/CoaExporter/Collectors/MysticScrollProbe.lua new file mode 100644 index 0000000..f4bad84 --- /dev/null +++ b/CoaExporter/Collectors/MysticScrollProbe.lua @@ -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 diff --git a/CoaExporter/Collectors/MysticScrolls.lua b/CoaExporter/Collectors/MysticScrolls.lua new file mode 100644 index 0000000..a8fbd23 --- /dev/null +++ b/CoaExporter/Collectors/MysticScrolls.lua @@ -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 diff --git a/CoaExporter/Collectors/Talents.lua b/CoaExporter/Collectors/Talents.lua new file mode 100644 index 0000000..4894a77 --- /dev/null +++ b/CoaExporter/Collectors/Talents.lua @@ -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 diff --git a/CoaExporter/Core.lua b/CoaExporter/Core.lua new file mode 100644 index 0000000..158dbef --- /dev/null +++ b/CoaExporter/Core.lua @@ -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 = 'Artifact'; qualityOrder = 1 + elseif slot == 1 then + quality = 'Legendary'; qualityOrder = 2 + elseif slot >= 2 and slot <= 4 then + quality = 'Epic'; qualityOrder = 3 + else + quality = 'Rare'; 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(' ', icon) or "" + local link = spellID > 0 and string.format("[%s](https://db.ascension.gg/?spell=%d)", name, spellID) or name + table.insert(enchantRows, { + quality = quality, qualityOrder = qualityOrder, slot = slot, + text = string.format("| %s | %s**%s** |", quality, iconImg, link), + }) + end + + table.sort(enchantRows, function(a, b) + if a.qualityOrder ~= b.qualityOrder then return a.qualityOrder < b.qualityOrder end + return a.slot < b.slot + end) + + table.insert(lines, "| Quality | Enchant |") + table.insert(lines, "|---------|---------|") + for _, row in ipairs(enchantRows) do table.insert(lines, row.text) end + return table.concat(lines, "\n") +end + +function AE:GenerateMarkdownFull() + local data = self:AssembleExport("all") or {} + local char = data.character or {} + local cls = (char.class or ""):lower() + local classIcon = string.format("/icons/classes/classicon_%s.png", cls ~= "" and cls or "unknown") + + local lines = {} + table.insert(lines, string.format("![%s](%s)", char.class or "Class", classIcon)) + table.insert(lines, string.format("By %s", char.name or "?")) + table.insert(lines, "") + table.insert(lines, string.format("Character: **%s** - Level %d %s %s on %s", + char.name or "?", char.level or 0, char.race or "?", char.class or "?", char.realm or "?")) + table.insert(lines, "") + table.insert(lines, "") + 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( + "") + 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) diff --git a/CoaExporter/Data/ScrollCatalog.lua b/CoaExporter/Data/ScrollCatalog.lua new file mode 100644 index 0000000..58b919b --- /dev/null +++ b/CoaExporter/Data/ScrollCatalog.lua @@ -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 \ No newline at end of file diff --git a/CoaExporter/UI/ExportFrame.lua b/CoaExporter/UI/ExportFrame.lua new file mode 100644 index 0000000..b542c38 --- /dev/null +++ b/CoaExporter/UI/ExportFrame.lua @@ -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 diff --git a/CoaExporter/Util/Json.lua b/CoaExporter/Util/Json.lua new file mode 100644 index 0000000..eab2b6e --- /dev/null +++ b/CoaExporter/Util/Json.lua @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..897131b --- /dev/null +++ b/README.md @@ -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: + +``` +/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//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 +``` diff --git a/scripts/install_addon_sub.sh b/scripts/install_addon_sub.sh new file mode 100755 index 0000000..0419ca8 --- /dev/null +++ b/scripts/install_addon_sub.sh @@ -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." diff --git a/scripts/regen_scroll_catalog.py b/scripts/regen_scroll_catalog.py new file mode 100644 index 0000000..3ed6974 --- /dev/null +++ b/scripts/regen_scroll_catalog.py @@ -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()